docs(architecture): reflect personal/global topology scope feature
This commit is contained in:
@ -231,8 +231,11 @@ CREATE TABLE IF NOT EXISTS labs (
|
|||||||
deviceIds TEXT NOT NULL, -- JSON string: string[]
|
deviceIds TEXT NOT NULL, -- JSON string: string[]
|
||||||
topology TEXT NOT NULL, -- JSON string: TopologyLink[]
|
topology TEXT NOT NULL, -- JSON string: TopologyLink[]
|
||||||
semaphoreSetupTemplateId TEXT NOT NULL DEFAULT '',
|
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 (
|
CREATE TABLE IF NOT EXISTS bookings (
|
||||||
id TEXT PRIMARY KEY,
|
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 |
|
| 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 |
|
| 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`) |
|
| 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)
|
### 4.3 Settings (key/value config)
|
||||||
|
|
||||||
@ -346,9 +349,9 @@ All `/api/*` routes return JSON. Every route except the public auth/config endpo
|
|||||||
|
|
|
|
||||||
+-- /labs
|
+-- /labs
|
||||||
| +-- GET / # List labs (parses deviceIds/topology JSON) [auth]
|
| +-- GET / # List labs (parses deviceIds/topology JSON) [auth]
|
||||||
| +-- POST / # Create lab [auth]
|
| +-- POST / # Create lab; sets ownerId=req.user [auth]
|
||||||
| +-- PUT /{id} # Update lab [auth]
|
| +-- PUT /{id} # Update lab; 403 if not owner/admin/legacy [auth]
|
||||||
| +-- DELETE /{id} # Delete + cancel upcoming bookings [auth]
|
| +-- DELETE /{id} # Delete + cancel upcoming bookings; same 403 guard [auth]
|
||||||
|
|
|
|
||||||
+-- /bookings
|
+-- /bookings
|
||||||
| +-- GET / # List bookings (int flags > booleans) [auth]
|
| +-- GET / # List bookings (int flags > booleans) [auth]
|
||||||
@ -390,8 +393,10 @@ Auth model
|
|||||||
+-- Storage: browser localStorage (ghostgrid_token, ghostgrid_user)
|
+-- Storage: browser localStorage (ghostgrid_token, ghostgrid_user)
|
||||||
+-- Middleware
|
+-- Middleware
|
||||||
| +-- requireAuth — verifies JWT, sets req.user; applied to all data routes
|
| +-- requireAuth — verifies JWT, sets req.user; applied to all data routes
|
||||||
| +-- requireAdmin — checks users.role === 'admin' ⚠ DEFINED BUT NOT WIRED
|
| +-- requireAdmin — checks users.role === 'admin' ⚠ DEFINED BUT NOT WIRED to routes
|
||||||
+-- Roles: role column defaults to 'User'; no route currently enforces admin
|
+-- 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.
|
**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 Templates + Topology
|
||||||
+-- Lab CRUD; Semaphore setup/teardown template selection
|
+-- 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)
|
+-- Topology link editor (fromDevice > toDevice, link type)
|
||||||
+-- TopologyPanel: SVG layout by node count (1 / 2 / 3 / circular)
|
+-- 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 |
|
| `DeviceType` | `'Switch' \| 'Access-Point' \| 'Firewall' \| 'Controller' \| (string & {})` — presets + free-form |
|
||||||
| `Device` | `status: 'online' \| 'offline' \| 'unknown'`; `emergencySheet` markdown; optional `cmkHostname`, `lastCheckedAt` |
|
| `Device` | `status: 'online' \| 'offline' \| 'unknown'`; `emergencySheet` markdown; optional `cmkHostname`, `lastCheckedAt` |
|
||||||
| `TopologyLink` | `{ fromDevice, toDevice, type }` (e.g. `LACP-Trunk`, `Uplink`, `OOB-Management`) |
|
| `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 |
|
| `Booking` | `status: 'active' \| 'upcoming' \| 'completed' \| 'cancelled'`; ansible trigger flags + job IDs |
|
||||||
| `LogEntry` | `type: 'maintenance' \| 'booking' \| 'status' \| 'system'` |
|
| `LogEntry` | `type: 'maintenance' \| 'booking' \| 'status' \| 'system'` |
|
||||||
| `User` | `{ id, name, role, email }` (never password on the client) |
|
| `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 |
|
| +-- role column ('User'/'admin') exists |
|
||||||
| +-- ⚠ requireAdmin defined but NOT applied — any |
|
| +-- ⚠ requireAdmin defined but NOT applied — any |
|
||||||
| authenticated user can read/write settings + users |
|
| authenticated user can read/write settings + users |
|
||||||
|
| +-- Lab ownership enforced on PUT/DELETE /api/labs/:id |
|
||||||
|
| (owner || admin || legacy ownerId=''); 403 otherwise |
|
||||||
+-------------------------------------------------------------+
|
+-------------------------------------------------------------+
|
||||||
| Secret Handling |
|
| Secret Handling |
|
||||||
| +-- Integration secrets stored in settings table |
|
| +-- 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 |
|
| 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 |
|
| 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 |
|
| 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.
|
- `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.
|
- 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`.
|
- 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`.
|
- 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.
|
- 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**.
|
- All user-facing strings are in **English**.
|
||||||
|
|||||||
Reference in New Issue
Block a user