From aa0f308208ad11bda739df979c9d5f04d8519b79 Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 11 Aug 2026 07:51:14 +0800 Subject: [PATCH] docs(webui): spec for admin system config (branding, login terms, security, backup) --- .../specs/2026-08-11-system-config-design.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-system-config-design.md diff --git a/docs/superpowers/specs/2026-08-11-system-config-design.md b/docs/superpowers/specs/2026-08-11-system-config-design.md new file mode 100644 index 0000000..be42762 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-system-config-design.md @@ -0,0 +1,161 @@ +# System Config — Admin System Parameters — Design + +Date: 2026-08-11 +Status: Approved (approach A: WebUI-owned JSON store, user confirmed each section) + +## Goal + +Add a "System config" entry to the top-right Configuration dropdown, opening an +admin-only dialog with four tabs — General (branding), Login terms, Security, +Data backup. Parameters are a WebUI-owned override layer stored in +`~/.evoscientist/system-config.json`; unset fields fall back to env/code +defaults. No Python backend changes. + +Decisions locked in during brainstorming: + +- **Login terms:** display-only on the login page (no agreement checkbox). +- **Security:** captcha policy, session & password policy, auth on/off toggle. + Session expiry stays **fixed** (no sliding renewal — rejected 2026-08-08). +- **Backup:** WebUI + backend data, manual export AND scheduled automatic + backups to a server directory, with a history list (download/delete). +- **Permission:** admin role only (UI hides entry; server double-checks). +- **Approach A:** WebUI self-managed storage, no backend changes. + +## 1. Storage — `src/lib/server/systemConfig.ts` + +Follows the `userStore` pattern: read/write `~/.evoscientist/system-config.json` +(via `webuiDataDir()`), atomic write (temp file + rename), in-memory cache with +mtime invalidation. + +```ts +interface SystemConfig { + branding: { wordmark: string; logoFile: string | null; faviconFile: string | null }; + loginTerms: { enabled: boolean; markdown: string }; + security: { + authEnabled: boolean | null; // null = follow env + sessionTtlHours: number | null; + passwordMinLength: number | null; + captchaEnabled: boolean | null; + captchaThreshold: number | null; // failures before captcha + }; + backup: { + backendDataDir: string; + schedule: { enabled: boolean; intervalHours: number; keepCount: number }; + }; +} +``` + +`null` = no override → fall back to env/code default. The config file is never +required to exist; `getSystemConfig()` returns defaults when absent. + +`effectiveSecurity()` (same module) is the single derivation point: +auth.ts / captcha.ts / loginFailures.ts call it instead of reading the file +directly. Unit-testable without touching auth internals. + +## 2. API layer + +| Route | Auth | Purpose | +|---|---|---| +| `GET/PUT /api/system/config` | admin | full config read/update | +| `POST /api/system/branding` | admin | multipart logo/favicon upload → `~/.evoscientist/branding/{logo,favicon}.`; `DELETE` restores default | +| `GET /api/system/config/public` | none | public subset: `{ wordmark, logoVersion, faviconVersion, loginTerms: { enabled, markdown } }` (version = file mtime, for cache-busting) | +| `GET /api/system/branding/logo` `/favicon` | none | stream branding file, `Cache-Control: public, max-age=60`; 404 → caller falls back to bundled asset | +| `POST /api/system/backup` | admin | run backup now | +| `GET /api/system/backup` | admin | history list (name, size, mtime, origin manual/auto) | +| `GET/DELETE /api/system/backup/[file]` | admin | download / delete an archive (filename validated: `^backup-\d{8}T\d{6}\.tar\.gz$`, no path traversal) | + +Admin check = `requireActor(request)` + `roleForUsername(username) === "admin"`, +else 403. Cross-origin guard (`isCrossOrigin`) on all routes, per the +`/api/system/version` pattern. + +## 3. General tab (branding) + +- **Wordmark:** text input, non-empty, ≤30 chars. Applied to the header + wordmark (`page.tsx`, currently hardcoded "AI4Scientist") and the login page + title. +- **Logo:** file picker (PNG/SVG/JPEG, ≤512KB), local preview before save. + Header `` becomes `` + (dynamic src — next/image doesn't fit), falling back to + `/evoscientist-logo.png` when no custom logo. "Restore default" button. +- **Favicon:** same upload flow. Root layout injects + `` + when a custom favicon exists (server component reads config at render). +- **Consumers:** header and login page SWR-fetch `/api/system/config/public` + (works unauthenticated — the login page needs it). Saving in the dialog + revalidates the SWR key so branding updates immediately. + +## 4. Login terms tab + +- Enable toggle + Markdown textarea with rendered preview (`MarkdownContent`). +- When enabled, the login page renders a scrollable terms card + (`max-h`, `MarkdownContent`) below the title, sourced from + `/api/system/config/public`. Display-only — no checkbox, no gating. + +## 5. Security tab + +Every control is an override with a "restore default" affordance: + +- **Auth toggle:** tri-state — follow env (default) / force on / force off. + `isAuthenticationEnabled()` checks the override first. Force-off gets a + confirm dialog (danger: opens access). The existing env-disabled code paths + (login page bypass, `authEnabled=false` UI) are reused as-is. +- **Session TTL:** hours, 1–720. Applies to newly issued sessions only; + existing sessions expire on their original fixed `expiresAt`. +- **Password min length:** 6–64. Enforced in `userStore.setUserPassword`; + existing passwords unaffected. +- **Captcha:** tri-state (default/on/off) + failure threshold 1–10. + `loginFailures`/`captcha` read `effectiveSecurity()`. Threshold ignored when + captcha is off. + +## 6. Data backup tab + +- **Manual:** "Back up now" → `POST /api/system/backup` → server runs + `tar -czf` over `webuiDataDir()` + configured `backendDataDir`, excluding + `backups/` itself and `*.tmp`. Output: + `~/.evoscientist/backups/backup-.tar.gz`. +- **Scheduled:** enable + interval hours (1–168) + keep count (1–100). + In-process `setInterval` singleton on `globalThis` (survives dev hot reload + without duplicates), lazily started on first config read. Each run uses the + same archive function as manual (origin recorded as `auto`), then prunes + oldest archives beyond keep count. A mutex prevents overlapping runs. +- **History:** table of archives (name, size, date, origin) with download and + delete. +- **Backend data dir:** path text input, validated on save (must exist and be + readable). Used by both manual and scheduled backups. + +Errors (missing dir, disk full, tar failure) go to `errorLogStore` and surface +as toasts; scheduler exceptions are caught and logged, never crash the process. + +## 7. Dialog & entry point + +- Configuration dropdown gains "System config" (admin only — role from + `/api/auth/me`; non-admin simply doesn't see it). +- `SystemConfigDialog.tsx`: four tabs (General / Login terms / Security / + Backup), one form state loaded via `GET /api/system/config`, single Save per + tab (`PUT`), unsaved-changes guard on close. + +## Error handling + +- Config file missing/corrupt → defaults + errorLog entry; never 500 the page. +- Upload rejects oversize/wrong type with 400 + toast. +- Non-admin API calls → 403 (UI never offers the entry). +- Backup failures → errorLog + toast; scheduler keeps running. + +## Testing & verification + +- Unit: `systemConfig.ts` (defaults, round-trip, atomic write, corrupt file), + `effectiveSecurity()` precedence, backup archive (temp-dir fixture: tar + contents, exclusions, keep-count pruning, mutex), route tests (403 for + non-admin, 400 validation, filename traversal rejected). +- Existing auth/captcha/loginFailures tests stay green. +- Manual gate (dev :4716): admin changes branding → header + login page + + favicon update; non-admin sees no menu item and gets 403; scheduled backup + fires and appears in history; restore-default flows. + +## Out of scope + +- Backend-owned config storage (approach B — rejected). +- Login-terms agreement checkbox / re-consent flow (display-only chosen). +- Sliding/idle session renewal (rejected 2026-08-08). +- Backup restore UI, remote/off-host backup targets. +- Multi-admin audit trail of config changes.