From d78ade4629b628d9800691fe23d1e5b081b21037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=BCckner?= Date: Wed, 10 Jun 2026 16:20:42 +0200 Subject: [PATCH] docs(architecture): reflect personal/global topology scope feature --- ARCHITECTURE.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dd0f1a0..c91f55c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -231,8 +231,11 @@ CREATE TABLE IF NOT EXISTS labs ( deviceIds TEXT NOT NULL, -- JSON string: string[] topology TEXT NOT NULL, -- JSON string: TopologyLink[] semaphoreSetupTemplateId TEXT NOT NULL DEFAULT '', - semaphoreTeardownTemplateId TEXT NOT NULL DEFAULT '' + semaphoreTeardownTemplateId TEXT NOT NULL DEFAULT '', + scope TEXT NOT NULL DEFAULT 'global', -- 'global' | 'personal' + ownerId TEXT NOT NULL DEFAULT '' -- userId of creator; '' = legacy (pre-migration) ); +-- scope/ownerId were added after initial release via idempotent ALTER TABLE in server-db.ts CREATE TABLE IF NOT EXISTS bookings ( id TEXT PRIMARY KEY, @@ -295,7 +298,7 @@ CREATE TABLE IF NOT EXISTS caddy ( | JSON columns | `labs.deviceIds` and `labs.topology` are JSON strings, parsed in the API layer | | Booleans | Booking flags (`notified`, `emailSent`, `ansible*Triggered`) are `INTEGER` 0/1, mapped to JS booleans on read | | Cascade | No FK cascades; referential cleanup is done in code (e.g. deleting a device scrubs it from every lab's `deviceIds`/`topology`) | -| Schema changes | Fresh-install model — edit the `CREATE TABLE` block in `server-db.ts` directly; there is no migration helper | +| Schema changes | Fresh-install model — edit the `CREATE TABLE` block in `server-db.ts` directly. To add columns to an existing DB, place idempotent `ALTER TABLE … ADD COLUMN` calls (wrapped in try/catch) in `server-db.ts` after the `db.exec()` block — they run on every startup and are no-ops when the column already exists | ### 4.3 Settings (key/value config) @@ -346,9 +349,9 @@ All `/api/*` routes return JSON. Every route except the public auth/config endpo | +-- /labs | +-- GET / # List labs (parses deviceIds/topology JSON) [auth] -| +-- POST / # Create lab [auth] -| +-- PUT /{id} # Update lab [auth] -| +-- DELETE /{id} # Delete + cancel upcoming bookings [auth] +| +-- POST / # Create lab; sets ownerId=req.user [auth] +| +-- PUT /{id} # Update lab; 403 if not owner/admin/legacy [auth] +| +-- DELETE /{id} # Delete + cancel upcoming bookings; same 403 guard [auth] | +-- /bookings | +-- GET / # List bookings (int flags > booleans) [auth] @@ -390,8 +393,10 @@ Auth model +-- Storage: browser localStorage (ghostgrid_token, ghostgrid_user) +-- Middleware | +-- requireAuth — verifies JWT, sets req.user; applied to all data routes -| +-- requireAdmin — checks users.role === 'admin' ⚠ DEFINED BUT NOT WIRED -+-- Roles: role column defaults to 'User'; no route currently enforces admin +| +-- requireAdmin — checks users.role === 'admin' ⚠ DEFINED BUT NOT WIRED to routes ++-- Roles: role column defaults to 'User' ++-- Lab ownership: PUT/DELETE /api/labs/:id enforce inline ownership check +| (owner || admin || legacy-lab with ownerId=''); 403 otherwise ``` **Local flow:** `register` (bcrypt hash, role `User`) / `login` (bcrypt compare) > issue JWT > client stores token + user. @@ -588,6 +593,9 @@ Device Inventory Lab Templates + Topology +-- Lab CRUD; Semaphore setup/teardown template selection ++-- Scope toggle (Global / Personal) per lab; Personal labs visible only to owner + admins ++-- List sectioned: "My Topologies" / "Global Topologies" / "Others' Personal" (admin only) ++-- Edit/Delete buttons hidden for labs the current user cannot modify +-- Topology link editor (fromDevice > toDevice, link type) +-- TopologyPanel: SVG layout by node count (1 / 2 / 3 / circular) @@ -612,7 +620,7 @@ The single contract between frontend and backend — imported by **both** `serve | `DeviceType` | `'Switch' \| 'Access-Point' \| 'Firewall' \| 'Controller' \| (string & {})` — presets + free-form | | `Device` | `status: 'online' \| 'offline' \| 'unknown'`; `emergencySheet` markdown; optional `cmkHostname`, `lastCheckedAt` | | `TopologyLink` | `{ fromDevice, toDevice, type }` (e.g. `LACP-Trunk`, `Uplink`, `OOB-Management`) | -| `LabTemplate` | `deviceIds: string[]`, `topology: TopologyLink[]`, optional Semaphore template IDs | +| `LabTemplate` | `deviceIds: string[]`, `topology: TopologyLink[]`, optional Semaphore template IDs; `scope: 'global' \| 'personal'`, `ownerId: string` (userId of creator, `''` for legacy rows) | | `Booking` | `status: 'active' \| 'upcoming' \| 'completed' \| 'cancelled'`; ansible trigger flags + job IDs | | `LogEntry` | `type: 'maintenance' \| 'booking' \| 'status' \| 'system'` | | `User` | `{ id, name, role, email }` (never password on the client) | @@ -693,6 +701,8 @@ Backup : ghostgrid.db + ghostgrid.db-wal + ghostgrid.db-shm | +-- role column ('User'/'admin') exists | | +-- ⚠ requireAdmin defined but NOT applied — any | | authenticated user can read/write settings + users | +| +-- Lab ownership enforced on PUT/DELETE /api/labs/:id | +| (owner || admin || legacy ownerId=''); 403 otherwise | +-------------------------------------------------------------+ | Secret Handling | | +-- Integration secrets stored in settings table | @@ -774,7 +784,7 @@ GhostGrid/ | CheckMK/Semaphore outage | Integration loops catch errors, log them, and retry next cycle; non-fatal | | Caddy admin API unreachable | `pushCaddyConfig()` failures are logged as warnings; routes apply when Caddy starts | | Data loss | Back up `ghostgrid.db` + `-wal`/`-shm`; each instance has its own DB | -| Schema evolution | Edit the `CREATE TABLE` block in `server-db.ts` (fresh-install model, no migrations); new settings need seed + allow-list (+ `SECRET_KEYS` if secret) | +| Schema evolution | Edit the `CREATE TABLE` block in `server-db.ts` (fresh-install model). For live DBs, add new columns via idempotent `ALTER TABLE … ADD COLUMN` (try/catch) after the `db.exec()` block. New settings need seed + allow-list (+ `SECRET_KEYS` if secret) | --- @@ -842,7 +852,7 @@ Express (server.ts) ──► better-sqlite3 (ghostgrid.db, WAL) - `labs.deviceIds` / `labs.topology` are JSON strings in SQLite, parsed in the API. - Booking boolean flags are 0/1 integers in SQLite, mapped on read. - A new settings key must be: **seeded** in `server-db.ts`, **allow-listed** in `PUT /api/settings`, and (if secret) added to `SECRET_KEYS`. -- Schema changes go straight into the `CREATE TABLE` block in `server-db.ts` — fresh-install model, no migration helper. +- Schema changes go into the `CREATE TABLE` block in `server-db.ts` (fresh-install). For adding columns to live DBs, use idempotent `ALTER TABLE … ADD COLUMN` (try/catch) placed after the `db.exec()` block. - The SPA catch-all (`app.get('*')`) + static serving are registered **last** in `startServer()`, after every `/api` route — otherwise GET `/api/*` falls through to `index.html`. All `/api` responses carry `Cache-Control: no-store`. - One Caddy per container; `POST /load` replaces the whole config. Only the `CADDY_MANAGER=true` instance may push/seed/edit routes — never let the non-manager push. - All user-facing strings are in **English**.