Browse Source

docs: add project architecture specifications

softword 2 months ago
parent
commit
887bbf3711

+ 50 - 0
AGENTS.md

@@ -0,0 +1,50 @@
+# OM-Link Engine
+
+## Project Overview
+OM-Link is a PHP/Fat-Free Framework application with a React administration UI. It exposes a same-origin JSON API for session authentication, navigation, notifications, encrypted configuration, and user administration.
+
+## Tech Stack
+PHP 8.5+, Fat-Free Framework 3.9, MySQL via F3 `DB\SQL`, React 19, React Router 7, MUI 9, Tailwind CSS 4, and Vite 7.
+
+## Architecture
+`index.php` boots F3, loads `cfg/config.bin`, then discovers `PluginStrategy` subclasses from `src/`. `OpenMind\Api\Api` registers `/api/v1` routes; React in `ui/app` consumes them through a same-origin client.
+
+## Commands
+`composer install` · `npm ci` · `npm run buildDev` · `npm run buildProd` · `npm run watch` · `npm pack` · `scripts/generate-config-bin.sh` (only when `cfg/config.bin` is absent). No test, lint, or deploy command is declared.
+
+## Code Style
+Use PSR-4 namespaces under `OpenMind\` and `Modules\`; match nearby PHP style and `declare(strict_types=1)` in modern API code. Preserve frontend import/component conventions; `.prettierrc` defines single quotes, semicolons, LF endings, and 200-column print width.
+
+## Coding Rules
+- A plugin directory is discovered only when it contains a same-named PHP initializer extending `PluginStrategy`.
+- Register API routes only in `src/OpenMind/Api/Api.php`; controllers must return JSON through `Controller` helpers.
+- Protect authenticated state changes with the session CSRF token; enforce authorization server-side, not only in React routes.
+- Keep the `username|password` input to password hashing; a username change requires a new password.
+- Preserve the final-admin and self-deletion invariants in user management.
+- Treat `cfg/config.bin` and its plaintext source as sensitive configuration; preserve backup/restore behavior on writes.
+- Keep generated `assets/app/` aligned with the Vite build; do not manually edit its hashed assets.
+
+## Design System
+React pages use MUI components plus Tailwind utility classes. `theme.js` defines the palette, navigation colors, typography, and radius; `AppShell` renders the responsive dark navigation drawer with route-aware selection.
+
+## Constraints / What to Avoid
+Do not bypass F3 session/CSRF checks, expose password hashes or configuration values, relax database uniqueness/locking safeguards, or add a frontend route without matching authorized navigation/API behavior.
+
+## Workflow
+Inspect the applicable spec before changing Tier 1 modules. Make the smallest compatible change, then review route, authorization, configuration, and generated-asset implications. No automated test suite is configured.
+
+When you modify code, update the relevant file in `specs/`.
+
+## References
+- @specs/file-structure.md — repository map and boot flow
+- @specs/commands.md — setup, build, package, and database commands
+- @specs/conventions-backend.md — PHP/F3 conventions
+- @specs/conventions-frontend.md — React/MUI conventions
+- @specs/conventions-database.md — MySQL schema and persistence rules
+- @specs/api-routes.md — JSON API contract
+- @specs/module-index.md — module priority map
+- @specs/identity/spec.md — authentication and user-management contracts
+- @specs/configuration/spec.md — configuration storage contract
+- @specs/integrations/index.md — external boundaries
+- @specs/rules/add-api-endpoint.md — API change recipe
+- @specs/rules/add-plugin.md — plugin change recipe

+ 29 - 0
specs/api-routes.md

@@ -0,0 +1,29 @@
+Read this when changing the JSON API contract, request authorization, or a React call to `/api/v1`.
+
+# API Routes
+
+All routes are registered in `src/OpenMind/Api/Api.php`. Responses are JSON with `Cache-Control: no-store`. `CSRF` means `X-CSRF-Token` must equal the token stored in the PHP session.
+
+| Method | Path | Handler | Description | Authorization |
+|---|---|---|---|---|
+| GET | `/api/v1/auth/session` | `AuthController::session` | Returns authenticated state, safe user DTO, roles, and CSRF token. | Public |
+| POST | `/api/v1/auth/login` | `AuthController::login` | Validates JSON credentials, regenerates the session ID, stores the user, and returns session state. | Public |
+| POST | `/api/v1/auth/logout` | `AuthController::logout` | Clears session and returns anonymous state. | CSRF |
+| GET | `/api/v1/navigation` | `NavigationController::index` | Returns permission-filtered, ordered menu entries. | Authenticated |
+| GET | `/api/v1/admin/config` | `AdminController::load` | Returns decrypted configuration text. | `admin` |
+| PUT | `/api/v1/admin/config` | `AdminController::save` | Encrypts and replaces configuration text. | `admin` + CSRF |
+| GET | `/api/v1/admin/users` | `UserController::index` | Returns safe user DTOs and configured role choices. | `admin` |
+| POST | `/api/v1/admin/users` | `UserController::create` | Creates a user from username, password, and configured roles. | `admin` + CSRF |
+| PUT | `/api/v1/admin/users/@id` | `UserController::update` | Updates selected user fields. | `admin` + CSRF |
+| DELETE | `/api/v1/admin/users/@id` | `UserController::delete` | Deletes an eligible user. | `admin` + CSRF |
+| GET | `/api/v1/notifications` | `NotificationController::index` | Returns notifications grouped by caption. | Authenticated |
+| DELETE | `/api/v1/notifications` | `NotificationController::delete` | Clears every notification. | Authenticated + CSRF |
+| DELETE | `/api/v1/notifications/@code` | `NotificationController::delete` | Deletes notifications matching a non-negative integer code. | Authenticated + CSRF |
+
+## Contract Notes
+- `Controller::decodeJsonRequest()` accepts only a JSON object; malformed or empty bodies are rejected.
+- The authentication/session response is the source of the current CSRF token. The client caches it only in memory.
+- Unauthorized routes return 401; insufficient roles return 403; user-management business conflicts return 409 with a stable `code`.
+- Route registration is separate from legacy system methods that are not registered in `System::setSystemRoutes()`.
+
+See [identity/spec.md](identity/spec.md), [configuration/spec.md](configuration/spec.md), and [rules/add-api-endpoint.md](rules/add-api-endpoint.md).

+ 36 - 0
specs/commands.md

@@ -0,0 +1,36 @@
+Read this when you need the supported setup, build, packaging, or configuration commands.
+
+# Commands
+
+## Dependencies
+
+| Purpose | Command | Source |
+|---|---|---|
+| Install PHP dependencies | `composer install` | `composer.json` / `composer.lock` |
+| Install Node dependencies | `npm ci` | `package-lock.json` |
+
+## Frontend
+
+| Purpose | Command | Effect |
+|---|---|---|
+| Development build | `npm run buildDev` | Vite development-mode build to `assets/app/`. |
+| Production build | `npm run buildProd` | Vite production-mode build to `assets/app/`. |
+| Watch build | `npm run watch` | Rebuilds `assets/app/` on source changes. |
+| Package | `npm pack` | Creates an npm package using `package.json` `files`. |
+
+## Configuration
+
+| Purpose | Command | Constraint |
+|---|---|---|
+| Generate runtime config | `scripts/generate-config-bin.sh` | Reads `cfg/config.ini`; fails if `cfg/config.bin` already exists. |
+
+The application bootstrap requires `cfg/config.bin`. The script uses the project's reversible base64+gzip encoding; do not treat it as cryptographic protection. See [configuration/spec.md](configuration/spec.md).
+
+## Database
+
+`omlink.sql` is a MySQL dump for the `omlink` database. It contains `DROP DATABASE`, `CREATE DATABASE`, and sample user data; review it before importing and never run it against a database that must be preserved.
+
+## Not Declared
+No `test`, `lint`, `format`, development server, migration, deployment, or release command is declared in the tracked manifests. Do not invent one in project documentation; use the repository's established operational process.
+
+See [conventions-database.md](conventions-database.md) and [integrations/index.md](integrations/index.md).

+ 34 - 0
specs/configuration/spec.md

@@ -0,0 +1,34 @@
+Read this when changing runtime configuration loading, configuration administration, or config-file generation.
+
+# Configuration
+
+## Purpose
+The application loads F3 globals from a runtime configuration file and provides an administrator-only API to read and replace its encoded content with a one-file backup.
+
+## Data Model
+
+| Artifact | Format | Role |
+|---|---|---|
+| `cfg/config.ini` | Plaintext F3 INI | Source configuration, including globals such as DSN, cache, base URL, and feature values. |
+| `cfg/config.bin` | `base64(gzcompress(ini-text))` | Runtime configuration loaded by `index.php`. |
+| `cfg/config.bin.old` | Previous `config.bin` bytes | Single backup created before an administrator save. |
+| `config.ini` | Plaintext INI | Minimal root-level configuration artifact; not the source used by the generator script. |
+
+## Business Rules
+- Startup must validate PHP 8.5+, require `cfg/config.bin`, decode it to a temporary file, apply F3 configuration, then remove the temporary file.
+- `Encrypt()`/`Decrypt()` provide reversible base64+gzip encoding, not secrecy. Treat every configuration artifact as sensitive because it can contain credentials.
+- The generator uses `cfg/config.ini` and refuses to overwrite an existing `cfg/config.bin`.
+- Only an authenticated `admin` may read decoded configuration or save a replacement; save also requires CSRF validation.
+- Save first renames `config.bin` to `config.bin.old`, writes encoded replacement content, and restores the backup if writing the new file fails.
+- The API accepts arbitrary string text; therefore callers must preserve valid F3 INI syntax and required runtime globals. No schema validation exists in the application.
+
+## Constants
+
+| Constant | Value | Location |
+|---|---|---|
+| `AdminController::CONFIGURATION_FILE` | `cfg/config.bin` | `src/OpenMind/Api/AdminController.php` |
+| `AdminController::CONFIGURATION_BACKUP_FILE` | `cfg/config.bin.old` | `src/OpenMind/Api/AdminController.php` |
+| `OMUtils::DEFAULT_SOCKET_TIMEOUT_SECONDS` | `5` | `src/OpenMind/System/OMUtils.php` |
+
+## Cross-links
+See [API routes](../api-routes.md), [encrypted configuration integration](../integrations/encrypted-config.md), and [commands](../commands.md).

+ 28 - 0
specs/conventions-backend.md

@@ -0,0 +1,28 @@
+Read this when changing PHP, Fat-Free Framework integration, plugins, or API controllers.
+
+# Backend Conventions
+
+## Namespaces and Plugins
+- Composer maps `OpenMind\` to `src/OpenMind/` and `Modules\` to `src/Modules/`.
+- A discovered plugin lives in a directory containing `<DirectoryName>.php`; its derived class must extend `PluginStrategy`.
+- `ApplicationComposition` boots `System` before the remaining plugins. Register plugin initialization work in constructors.
+- Use `PluginStrategy::AddRoute()` for F3 route registration and `AddMenuItem()` for permission-filtered menu entries.
+
+## API Controllers
+- API route declarations belong in `OpenMind\Api\Api`; controllers extend `Controller`.
+- Use `sendJson()` / `sendError()` rather than emitting ad hoc response formats. They set JSON, status, and `Cache-Control: no-store`.
+- Decode request objects through `decodeJsonRequest()` and return 400 for invalid JSON or required-field failures.
+- Call `requireAuthentication()` for protected reads; call `requireRole()` plus `validateCsrfToken()` before protected mutations.
+- Return DTOs, never a mapper or credential fields directly. See [api-routes.md](api-routes.md) and [identity/spec.md](identity/spec.md).
+
+## Persistence and Errors
+- F3 globals (`$f3`, `$frameworkUtils`) are the established dependency access pattern; retain it where surrounding code does.
+- `User` and `Notifications` are `DB\SQL\Mapper` classes. Use parameterized mapper predicates for request values.
+- Wrap multi-step user mutation checks and writes in a transaction; use the existing row-locking pattern where the invariant requires serialization.
+- Unexpected controller failures are logged server-side and return a safe generic message.
+- API errors are JSON; `ErrorHandlers` identifies `/api/v1` requests before setting the status.
+
+## Formatting
+Modern API/composition files use strict types, typed properties, trailing commas, and tabs. Older system files are mixed-style; make surgical changes and preserve their local style.
+
+See [rules/add-api-endpoint.md](rules/add-api-endpoint.md) and [rules/add-plugin.md](rules/add-plugin.md).

+ 27 - 0
specs/conventions-database.md

@@ -0,0 +1,27 @@
+Read this when changing MySQL schema, F3 mappers, or data-integrity rules.
+
+# Database Conventions
+
+## Connection
+`OpenMind\System\System` creates the F3 `DB\SQL` connection from `DSN`, `dbuser`, and `dbpass` F3 globals. It uses PDO exceptions and enables persistent connections on Linux and all CLI requests. Keep credentials in configuration, not source code.
+
+## Schema Source
+`omlink.sql` is the tracked schema source. It defines four tables:
+
+| Table | Engine | Role |
+|---|---|---|
+| `users` | InnoDB | Accounts, username-prefixed password hashes, serialized roles, optional avatar. |
+| `notifications` | InnoDB | System notification records. |
+| `label_bindings` | InnoDB | Legacy label/barcode data; no current PHP mapper uses it. |
+| `queuesystem` | MEMORY | Legacy queued command records; no current PHP mapper uses it. |
+
+## Active Mapper Rules
+- `User` maps `users`; its `roles` column is a PHP-serialized array and is loaded with `allowed_classes => false`.
+- `Notifications` maps `notifications`; `emptyAll()` truncates the entire table.
+- `users.username` is unique (`uq_users_username`). Keep it as the final concurrency safeguard even when application prechecks exist.
+- `UserController` locks user rows with `SELECT id FROM users FOR UPDATE` inside transactions before checks that could remove the final administrator.
+
+## Change Constraints
+There is no migration framework or seed command. Any schema change must account for the dump, existing mapper field names, and the database-level uniqueness constraint. Do not document a migration workflow that does not exist.
+
+See [identity/spec.md](identity/spec.md) and [integrations/mysql.md](integrations/mysql.md).

+ 26 - 0
specs/conventions-frontend.md

@@ -0,0 +1,26 @@
+Read this when changing the React application, MUI components, routing, or frontend API calls.
+
+# Frontend Conventions
+
+## Application Structure
+- `main.jsx` mounts `StrictMode`, MUI `ThemeProvider`, `CssBaseline`, and `BrowserRouter`.
+- Routes and session coordination live in `routes.jsx`; feature pages live in `features/<area>/`.
+- `api/client.js` is the only shared HTTP client. Route feature requests through it rather than direct `fetch()` calls.
+
+## Authentication and Routing
+- `getSession()` initializes in-memory CSRF state before protected mutations.
+- The client always uses same-origin requests with `credentials: 'include'`; non-GET requests attach `X-CSRF-Token` only when a token is available.
+- Frontend redirects are usability controls only. The API remains the authorization authority.
+- Use `APPLICATION_BASE_PATH` for internal application and static-asset paths so the `/phengine` deployment prefix is retained.
+
+## UI
+- Prefer MUI components for structure, controls, feedback, and responsive behavior.
+- Tailwind utilities are used through `className` for layout and small visual adjustments.
+- Theme defaults are in `theme.js`, including the custom `navigation` palette, typography families, and shared border radius; global CSS only imports Tailwind layers and sets root sizing/margins.
+- `AppShell` derives top-level links and populated groups from the flat navigation payload, assigns known icons with a fallback icon, and selects entries from `location.pathname`.
+- Pages use local state/hooks, explicit loading/error feedback, and cleanup guards for asynchronous effects; use responsive `sx` values for control sizing/alignment when a layout needs a breakpoint-specific adjustment.
+
+## Formatting
+`.prettierrc` specifies single quotes, semicolons, LF endings, no trailing commas, a 200-column print width, and 2-column indentation width. Existing files use tabs; preserve local formatting and do not reformat unrelated code.
+
+See [api-routes.md](api-routes.md) and [integrations/browser-api.md](integrations/browser-api.md).

+ 30 - 0
specs/file-structure.md

@@ -0,0 +1,30 @@
+Read this when you need the repository map, boot order, or generated-file boundaries.
+
+# File Structure
+
+| Path | Purpose |
+|---|---|
+| `index.php` | HTTP/CLI bootstrap: Composer autoload, F3 singleton, encrypted runtime config, plugin composition, then `run()`. |
+| `composer.json` | PHP 8.5 and Fat-Free Framework dependencies; PSR-4 maps `OpenMind\` and `Modules\`. |
+| `cfg/config.ini` | Plaintext source of F3 globals and MySQL DSN; used to produce the runtime config. |
+| `cfg/config.bin` | Runtime configuration consumed by `index.php`; generated locally and not tracked. |
+| `scripts/generate-config-bin.sh` | Creates `cfg/config.bin` from `cfg/config.ini` only if no target exists. |
+| `src/OpenMind/System/` | Bootstrap, plugin discovery, framework services, mappers, roles, notifications, and error handling. |
+| `src/OpenMind/Api/` | API route registry and JSON controllers. |
+| `src/OpenMind/Admin/` | Administration plugin that registers the `admin` role and menu hierarchy. |
+| `src/Modules/Blank/` | Example plugin registering the `demo` role and menu entry. |
+| `ui/app/` | Vite React source application. |
+| `ui/app/src/api/client.js` | Same-origin API client and in-memory CSRF token holder. |
+| `ui/app/src/features/` | Authentication, shell, administration, notification, home, and demo UI features. |
+| `assets/app/` | Vite build output, including hashed JS/CSS and the manifest; do not edit manually. |
+| `assets/images/` | Static images referenced by legacy menu entries and React pages. |
+| `vite.config.js` | Builds `ui/app` into `assets/app` with base `/phengine/assets/app/` and suppresses Rollup `"use client"` module-directive warnings. |
+| `omlink.sql` | MySQL schema and sample data dump. |
+
+## Boot and Request Flow
+1. `index.php` validates PHP, decrypts `cfg/config.bin`, and invokes `ApplicationComposition` over `src/`.
+2. Composition boots `System` first, then each discovered same-name directory initializer that extends `PluginStrategy`.
+3. `System` configures F3 errors, cache, and the `DB` connection; `Api` registers JSON routes.
+4. Vite-built React assets load under the configured base path and call `/api/v1` through `ui/app/src/api/client.js`.
+
+See [module-index.md](module-index.md), [api-routes.md](api-routes.md), and [rules/add-plugin.md](rules/add-plugin.md).

+ 42 - 0
specs/identity/spec.md

@@ -0,0 +1,42 @@
+Read this when changing authentication, sessions, CSRF, roles, users, or user-management endpoints.
+
+# Identity and User Management
+
+## Purpose
+This subsystem authenticates database-backed users into PHP sessions and lets administrators manage accounts and roles without exposing credentials.
+
+## Data Model
+
+| Entity | Field | Type / storage | Notes |
+|---|---|---|---|
+| `users` | `id` | auto-increment integer | Account identifier. |
+| `users` | `username` | `varchar(255)`, unique | Login identity and password-hash input. |
+| `users` | `saltedpassword` | `varchar(255)` | Bcrypt hash of `username|password`. |
+| `users` | `roles` | `varchar(4096)` | PHP-serialized role array; unserialized with classes disabled. |
+| PHP session | `SESSION.user` | integer | Current authenticated user ID. |
+| PHP session | `SESSION.csrf_token` | 64-character hex string | Random 32-byte token used for state-changing API requests. |
+| F3 global | `USERROLES` | array of strings | Runtime role registry populated by plugins. |
+
+## Business Rules
+- Authentication loads by username and verifies `username|password` against the stored bcrypt hash.
+- Login starts a session when required, regenerates its ID, then stores only the user ID. Session responses include a CSRF token for anonymous and authenticated callers.
+- Server-side state-changing protected operations require a CSRF token using `hash_equals`; client route guards never replace this check.
+- Roles assigned through user administration must be strings in the current runtime role registry and are deduplicated.
+- Usernames are trimmed and must be non-empty. The unique database constraint is authoritative under concurrency.
+- If a username changes, a non-empty replacement password is required because the username is part of the password-hash input.
+- An administrator cannot delete their own account. The final account with the `admin` role cannot lose that role or be deleted.
+- Update/delete serialize final-admin checks with a transaction and `SELECT id FROM users FOR UPDATE`; failures roll back the transaction.
+- Public DTOs return only `id`, `username`, and, for administrator management, roles. Password hashes and serialized persistence fields never leave the API.
+
+## Constants
+
+| Constant | Value | Location |
+|---|---|---|
+| `User::PASSWORD_SEPARATOR` | `|` | `src/OpenMind/System/User.php` |
+| `Controller::CSRF_SESSION_KEY` | `SESSION.csrf_token` | `src/OpenMind/Api/Controller.php` |
+| `Controller::CSRF_HEADER` | `HTTP_X_CSRF_TOKEN` | `src/OpenMind/Api/Controller.php` |
+| `UserController::ADMIN_ROLE` | `admin` | `src/OpenMind/Api/UserController.php` |
+| `UserController::USERNAME_CONSTRAINT` | `uq_users_username` | `src/OpenMind/Api/UserController.php` |
+
+## Cross-links
+See [API routes](../api-routes.md), [MySQL integration](../integrations/mysql.md), [browser API integration](../integrations/browser-api.md), and the [API endpoint rule](../rules/add-api-endpoint.md).

+ 40 - 0
specs/integrations/browser-api.md

@@ -0,0 +1,40 @@
+Read this when changing browser-to-server API requests, sessions, CSRF, or JSON response behavior.
+
+# Browser JSON API
+
+## Implementing Files
+
+| File | Key symbols |
+|---|---|
+| `ui/app/src/api/client.js` | `apiRequest`, `getSession`, `login`, `logout`, navigation/notification functions |
+| `src/OpenMind/Api/Api.php` | Versioned route registry |
+| `src/OpenMind/Api/Controller.php` | JSON, request decoding, current user, CSRF, authorization helpers |
+| `src/OpenMind/Api/AuthController.php` | Session inspection, login, logout |
+| `src/OpenMind/Api/*Controller.php` | Navigation, configuration, user, and notification handlers |
+
+## Flow
+1. React boot calls `getSession()` through `apiRequest()`.
+2. The client resolves the URL against the Vite asset base, sends same-origin cookies, and accepts JSON.
+3. The API reads the PHP session, produces a safe session DTO, and supplies a CSRF token when absent.
+4. Later non-GET calls attach the in-memory token as `X-CSRF-Token`.
+5. Controllers validate JSON/authentication/roles/CSRF as applicable, then return explicit JSON DTOs.
+
+## Trust Boundaries
+
+| Boundary | Mechanism | Location |
+|---|---|---|
+| Client-supplied URL path | API root is derived from same-origin Vite base; client uses fixed resource paths | `ui/app/src/api/client.js` |
+| Credential input | Requires JSON object plus non-empty string username/password; verifies bcrypt hash server-side | `AuthController::login()` |
+| Session fixation | Regenerates session ID after successful login | `AuthController::login()` |
+| Cross-site state change | Session token compared with `hash_equals` before protected mutations | `Controller::validateCsrfToken()` and mutation controllers |
+| Authorization | Authentication and role checks run in PHP before data access | `Controller::requireAuthentication()` / `requireRole()` |
+| Sensitive output | Controllers return explicit DTOs instead of mapper fields | `Controller`, `UserController`, `NotificationController` |
+| Browser-only route control | React redirects unauthorized users, but does not grant access | `ui/app/src/routes.jsx` |
+
+## Config
+- Base path: Vite `base` is `/phengine/assets/app/`; the API client removes `/assets/app` to reach application root.
+- Credentials: `fetch(..., { credentials: 'include' })`.
+- Cache policy: API responses send `Cache-Control: no-store`.
+- Notification polling: 30,000 ms in `NotificationMenu`.
+
+See [API routes](../api-routes.md) and [identity spec](../identity/spec.md).

+ 38 - 0
specs/integrations/encrypted-config.md

@@ -0,0 +1,38 @@
+Read this when changing configuration encoding, runtime loading, backup behavior, or administrator configuration access.
+
+# Encoded Configuration Files
+
+## Implementing Files
+
+| File | Key symbols |
+|---|---|
+| `index.php` | `LoadEncryptedConfig('cfg/config.bin')` during bootstrap |
+| `src/OpenMind/System/OMUtils.php` | `LoadEncryptedConfig`, `Encrypt`, `Decrypt` |
+| `src/OpenMind/Api/AdminController.php` | Admin read/save and backup restoration |
+| `scripts/generate-config-bin.sh` | First-time runtime config generation |
+| `cfg/config.ini` | Plaintext source used by the generator |
+
+## Flow
+1. Bootstrap checks the PHP version and asks `OMUtils` to load `cfg/config.bin`.
+2. The utility decodes it to a temporary file, calls F3 `config()`, then deletes the temporary file.
+3. The generator encodes `cfg/config.ini` into a previously absent `cfg/config.bin`.
+4. An administrator can read plaintext through the API or submit replacement text.
+5. Save renames the prior runtime config to `.old`, writes the encoded replacement, and restores the backup if writing fails.
+
+## Trust Boundaries
+
+| Boundary | Mechanism | Location |
+|---|---|---|
+| Runtime config presence | Bootstrap throws when the required file is absent | `OMUtils::LoadEncryptedConfig()` |
+| Sensitive config disclosure | `admin` role required before reading plaintext | `AdminController::load()` |
+| Sensitive config mutation | `admin` role and CSRF validation required | `AdminController::save()` |
+| Partial replacement failure | Backup rename and restore attempt after write failure | `AdminController::saveConfiguration()` |
+| Generator overwrite | Script exits if `cfg/config.bin` already exists | `scripts/generate-config-bin.sh` |
+
+## Config
+- Runtime file: `cfg/config.bin`.
+- Backup file: `cfg/config.bin.old`.
+- Encoding: `base64_encode(gzcompress())`; decoding is `gzuncompress(base64_decode())`.
+- The encoding is reversible; it is not a confidentiality mechanism.
+
+See [configuration spec](../configuration/spec.md) and [commands](../commands.md).

+ 11 - 0
specs/integrations/index.md

@@ -0,0 +1,11 @@
+Read this when a change crosses the browser, database, or configuration filesystem boundary.
+
+# Integration Index
+
+| Integration | Direction | Protocol | Implementing files | Documentation |
+|---|---|---|---|---|
+| Browser JSON API | Bidirectional | Same-origin HTTPS/HTTP + JSON + PHP session cookie | `ui/app/src/api/client.js`, `src/OpenMind/Api/` | [browser-api.md](browser-api.md) |
+| MySQL persistence | Outbound | PDO/MySQL via F3 `DB\SQL` | `System.php`, `User.php`, `Notifications.php`, API controllers | [mysql.md](mysql.md) |
+| Encoded configuration files | Bidirectional | Local filesystem, base64+gzip INI payload | `index.php`, `OMUtils.php`, `AdminController.php`, generator script | [encrypted-config.md](encrypted-config.md) |
+
+Each integration has integrity or authorization controls that must not be weakened. See the linked trust-boundary tables.

+ 37 - 0
specs/integrations/mysql.md

@@ -0,0 +1,37 @@
+Read this when changing MySQL connections, mappers, transactions, or persistence invariants.
+
+# MySQL Persistence
+
+## Implementing Files
+
+| File | Key symbols |
+|---|---|
+| `src/OpenMind/System/System.php` | F3 `DB\SQL` connection and PDO options |
+| `src/OpenMind/System/User.php` | `users` mapper, role serialization, password hash generation |
+| `src/OpenMind/System/Notifications.php` | `notifications` mapper and table-wide clear operation |
+| `src/OpenMind/Api/UserController.php` | Account CRUD, row locks, transactions, duplicate detection |
+| `omlink.sql` | Schema, unique index, and sample data |
+
+## Flow
+1. F3 globals provide DSN and database credentials to `System`.
+2. `System` creates the `DB\SQL` connection with PDO exceptions and stores it as F3 `DB`.
+3. Mappers obtain the global connection and issue F3 mapper operations.
+4. User updates/deletes begin a transaction, lock user rows, enforce account invariants, then commit or roll back.
+
+## Trust Boundaries
+
+| Boundary | Mechanism | Location |
+|---|---|---|
+| Connection secrets | Values originate in runtime configuration, not request input | `cfg/config.bin` loaded by `index.php` |
+| Request values in queries | Mapper predicates use placeholders for username/ID lookups | `User`, `AuthController`, `UserController` |
+| Duplicate identities | Application precheck plus `uq_users_username` database constraint | `UserController`, `omlink.sql` |
+| Final administrator race | Transaction and `SELECT id FROM users FOR UPDATE` serialize checks and mutations | `UserController` |
+| Role deserialization | `unserialize(..., ['allowed_classes' => false])` prevents object instantiation | `User::__construct()` |
+| Table-wide notification deletion | Only authenticated, CSRF-protected notification API path reaches `emptyAll()` | `NotificationController::delete()` |
+
+## Config
+- Driver: MySQL DSN from F3 `DSN` global.
+- PDO: `ERRMODE_EXCEPTION` is set; persistence is enabled on Linux or CLI.
+- F3 query logging: disabled with `$db->log(false)` to avoid known framework memory leakage.
+
+See [database conventions](../conventions-database.md) and [identity spec](../identity/spec.md).

+ 26 - 0
specs/module-index.md

@@ -0,0 +1,26 @@
+Read this when choosing the analysis depth required before modifying a module.
+
+# Module Index
+
+Tier 1 requires full context loading. Tier 2 is customized business logic. Tier 3 is standard framework usage. Tier 4 is thin/generated. Tier 5 is reference-only.
+
+| Module | Path | Tier | Summary | Spec link |
+|---|---|---:|---|---|
+| Bootstrap | `index.php` | 1 | Starts F3, loads encrypted runtime configuration, and boots plugins. | [file structure](file-structure.md) |
+| Plugin composition | `src/OpenMind/System/ApplicationComposition.php`, `PluginStrategy.php` | 1 | Discovers initializer classes by directory convention and maintains menu/route registration primitives. | [add plugin rule](rules/add-plugin.md) |
+| System services | `src/OpenMind/System/System.php`, `OMUtils.php`, `ErrorHandlers.php` | 1 | Database/cache initialization, config codec, framework error responses, and legacy utilities. | [integrations](integrations/index.md) |
+| Identity and user management | `src/OpenMind/System/User.php`, `UserRoles.php`, `src/OpenMind/Api/AuthController.php`, `UserController.php`, `Controller.php` | 1 | Session login, CSRF, roles, account persistence, and user-mutation invariants. | [identity spec](identity/spec.md) |
+| API registry | `src/OpenMind/Api/Api.php` | 2 | Maps all versioned JSON routes to controllers. | [API routes](api-routes.md) |
+| Configuration administration | `src/OpenMind/Api/AdminController.php`, `cfg/`, `scripts/generate-config-bin.sh` | 1 | Reads, encodes, backs up, and replaces runtime configuration. | [configuration spec](configuration/spec.md) |
+| Navigation | `src/OpenMind/Api/NavigationController.php`, `src/OpenMind/Admin/Admin.php` | 2 | Builds the role-filtered and ordered application menu. | [API routes](api-routes.md) |
+| Notifications | `src/OpenMind/System/Notifications.php`, `src/OpenMind/Api/NotificationController.php` | 2 | Persists, groups, polls, and deletes notification records. | [API routes](api-routes.md) |
+| Blank plugin | `src/Modules/Blank/Blank.php` | 4 | Example/demo plugin and permission registration. | [add plugin rule](rules/add-plugin.md) |
+| Database schema | `omlink.sql` | 1 | MySQL schema, constraints, and sample data source. | [database conventions](conventions-database.md) |
+| React boot and routes | `ui/app/src/main.jsx`, `App.jsx`, `routes.jsx` | 2 | React/MUI setup, session bootstrap, guarded routes, and page selection. | [frontend conventions](conventions-frontend.md) |
+| Frontend API client | `ui/app/src/api/client.js` | 1 | Same-origin URL resolution, cookies, JSON errors, and in-memory CSRF handling. | [browser API integration](integrations/browser-api.md) |
+| Admin React features | `ui/app/src/features/admin/` | 2 | Configuration editor and user-management UI. | [API routes](api-routes.md) |
+| Shell and notification UI | `ui/app/src/features/shell/`, `features/notifications/` | 2 | Responsive dark drawer, route-aware selected navigation, contextual header, and 30-second notification polling. | [frontend conventions](conventions-frontend.md) |
+| Home, auth, and demo pages | `ui/app/src/features/auth/`, `features/home/`, `features/demo/` | 3 | Standard page-level views and login form. | [frontend conventions](conventions-frontend.md) |
+| Visual configuration | `ui/app/src/theme.js`, `styles.css` | 2 | Customized MUI palette, navigation tokens, typography, shape, and Tailwind/global style entry points. | [frontend conventions](conventions-frontend.md) |
+| Frontend build output | `vite.config.js`, `assets/app/` | 4 | Vite configuration (including filtered `"use client"` warnings) and generated application assets. | [commands](commands.md) |
+| Static assets | `assets/images/` | 5 | Images referenced by the UI and legacy menus. | [file structure](file-structure.md) |

+ 23 - 0
specs/rules/add-api-endpoint.md

@@ -0,0 +1,23 @@
+Read this when adding or changing a versioned JSON API endpoint and its React caller.
+
+# Add or Change an API Endpoint
+
+1. **Choose the authorization contract.** Classify the route as public, authenticated, role-protected, and/or state-changing. See [identity spec](../identity/spec.md).
+2. **Register the route** in `src/OpenMind/Api/Api.php` using `AddRoute()` and the `/api/v1` path convention.
+   ```php
+   $this->AddRoute('GET /api/v1/example', __NAMESPACE__ . '\\ExampleController->index');
+   ```
+3. **Implement the controller** as a `Controller` subclass. Use `requireAuthentication()` / `requireRole()` first. For mutations, reject invalid CSRF before persistence.
+   ```php
+   if (!$this->requireRole('admin')) return;
+   if (!$this->validateCsrfToken()) {
+       $this->sendError('Invalid CSRF token', 403);
+       return;
+   }
+   ```
+4. **Validate JSON input** with `decodeJsonRequest()` and return only an explicit JSON DTO via `sendJson()`. Do not serialize mappers or return credential/configuration fields.
+5. **Preserve persistence invariants.** Use parameterized mapper predicates; wrap serialized multi-step mutation checks in a transaction when needed. See [MySQL integration](../integrations/mysql.md).
+6. **Add the frontend client call** to `ui/app/src/api/client.js` using `apiRequest()`. Do not bypass its base-path, cookie, JSON-error, or CSRF handling.
+7. **Connect the UI and documentation.** Add/update the feature and route only when the server authorizes it, then update [API routes](../api-routes.md) and the relevant domain/integration spec.
+
+No automated test command is configured; review the method/path, authorization, CSRF, request validation, safe response shape, and affected frontend call before delivery.

+ 31 - 0
specs/rules/add-plugin.md

@@ -0,0 +1,31 @@
+Read this when adding or changing a discoverable PHP plugin, permission, or menu entry.
+
+# Add or Change a Plugin
+
+1. **Use the discovery convention.** Create a directory below `src/` with an initializer named after the directory. Its namespace must follow the path and the class must extend `PluginStrategy`.
+   ```php
+   namespace Modules\Example;
+
+   use OpenMind\System\PluginStrategy;
+
+   class Example extends PluginStrategy
+   {
+       public function __construct()
+       {
+       }
+   }
+   ```
+2. **Keep plugin construction safe at boot.** `ApplicationComposition` instantiates every valid plugin after `System`; configuration, database, and framework globals are then available.
+3. **Register permissions before relying on them.** Add role names to `UserRoles` in the plugin constructor. User administration accepts only roles in this registry.
+   ```php
+   (new UserRoles())->add('example');
+   ```
+4. **Register menu entries through `AddMenuItem()`.** Use `permissionName` for protected entries. The method publishes entries only when the restored user has the required role.
+   ```php
+   $this->AddMenuItem('Example', ['url' => 'example', 'permissionName' => 'example']);
+   ```
+5. **For API behavior, add routes in `OpenMind\Api\Api`, not in the new plugin.** Follow [add-api-endpoint.md](add-api-endpoint.md).
+6. **For an interactive page, add a matching React route and ensure the navigation URL resolves under the application base path.** Frontend route guards are not a substitute for API authorization.
+7. **Update documentation.** Add the subsystem to [module index](../module-index.md); create a domain spec only if it introduces a non-obvious data model and business contracts.
+
+See [backend conventions](../conventions-backend.md) and [file structure](../file-structure.md).