Files
server-manager/AGENTS.md
Agent 5cc5303826 feat: track notification read receipts per user
- New notification_reads table (migration 008) with unique (notification_id, user_id)
- Notification model: markAsRead inserts into notification_reads; getForUser JOINs
  to show per-user read status; getUnreadCount checks NOT EXISTS in reads table
- getAll() includes read_count subquery for admin view
- Admin view shows 'X / Y' read count for broadcast notifications,
  'Read'/'Unread' badge for user-targeted ones
- Migrates existing read notifications into notification_reads
- AGENTS.md updated with new conventions
2026-06-13 14:08:07 -04:00

4.5 KiB

ServerManager — AGENTS.md

Repo-specific guidance for OpenCode sessions. Verified against code.

Stack

  • PHP 8.3+, custom MVC (not Laravel), MySQL/MariaDB, phpseclib 3.0, phpdotenv
  • Autoload: PSR-4 ServerManager\src/; src/Helpers/functions.php auto-loaded
  • Android companion app at android/ (Jetpack Compose, Material3, Retrofit, Kotlinx Serialization)

Entry point

public/index.phpApp::boot()App::run()Router::dispatch()

Commands

  • composer install — install deps (no dev deps in composer.json)
  • php bin/generate-key.php — generate master key at /etc/servermanager/master.key
  • php bin/monitor.php — collect server metrics (runs via cron every minute)
  • sudo bash install.sh — full Ubuntu 22.04+/24.04+ install (needs sudo)
  • mysql servermanager < database/migrations/001_initial_schema.sql — db setup
  • export ANDROID_HOME=/opt/android-sdk && cd android && ./gradlew assembleDebug — build APK → public/sysadmin.apk

Non-obvious conventions

  • Route params use :id syntax (/servers/:id), not {id}. Patterns: :id(\d+), :slug([a-zA-Z0-9\-]+)
  • Router supports method spoofing via _method POST field
  • Session name is SM_SESSION; sessions stored server-side in sessions table
  • View engine: custom PHP templates (extract() + include), not Twig/Blade
  • SSH credentials encrypted with AES-256-CBC; master key at /etc/servermanager/master.key
  • vendor/ is properly gitignored now (/vendor/ in .gitignore, no leading space)
  • Android build outputs ignored: android/.gradle/, android/build/, android/app/build/, android/local.properties
  • Bump versionCode and versionName in android/app/build.gradle.kts on every code change to the Android app

Backend gotchas

  • View::json() has : never return type — calls exit immediately. Do not put code after it expecting to run.
  • AuditService::log(string $action, string $entityType, ?int $entityId, ?array $details = null, ?array $metadata = null) — requires 3 positional args minimum. Calling with 2 args causes Error 500.
  • RoleMiddleware defaults to 'admin' required level. Hierarchy: super_admin (3) > admin (2) > operator (1).
  • Role helper functions in views: is_super_admin(), is_admin(), is_operator()
  • CSRF: middleware checks $_POST['_csrf_token'] then $_SERVER['HTTP_X_CSRF_TOKEN']. Token expiry 7200s default.
  • API routes have no middleware (auth is manual via $this->authenticateRequest() in controller). Web routes use AuthMiddleware / RoleMiddleware / CSRFMiddleware.
  • Admin group routes (/admin/*) already include AuthMiddleware + RoleMiddleware; individual routes add CSRFMiddleware.
  • Rate limiting on POST /login only (via RateLimitMiddleware)
  • Every credential access must be logged at warning level via AuditService
  • Notification read receipts tracked in notification_reads table: (notification_id, user_id) unique pair, with read_at timestamp

Permission validation on server create/edit

  • When creating or editing a server, required SSH permissions can be specified and are validated via SSH before saving.
  • PermissionValidator::validate(array $serverConfig, array $permissions) connects to the remote server and runs check commands for each permission.
  • Permission types: sudo (checks passwordless sudo), command (checks command -v), file_read (test -r), file_write (test -w), custom (arbitrary command, exit code 0 = pass).
  • ServerPermission model handles CRUD for the server_permissions table.
  • Both web (ServerController::store/update) and API (ApiController::serverAdd/serverUpdate) flows include permission validation.
  • On validation failure, the web flow preserves form input via $_SESSION['_form_input'] and redirects back with error details.

Database

  • Migrations run manually: mysql servermanager < database/migrations/NNN_name.sql
  • 8 migrations: 001_initial_schema, 002_server_access, 003_teams, 004_soft_deletes, 005_app_config_and_notifications, 006_agent_tokens, 007_server_permission_checks, 008_notification_reads
  • Users table has role ENUM: super_admin, admin, operator
  • api_token column on users for Bearer token auth
  • server_permissions table stores required SSH permissions per server (FK to servers.id CASCADE)

Testing / CI

  • No tests, no CI, no linter, no formatter, no typechecker exist in this repo

Notable missing files

  • .env.example does NOT exist (referenced in composer.json post-install-cmd but missing from repo)
  • No Makefile, Dockerfile, or CI workflows