diff --git a/plans/web-ui-redesign-plan.md b/plans/web-ui-redesign-plan.md new file mode 100644 index 0000000..e3361bc --- /dev/null +++ b/plans/web-ui-redesign-plan.md @@ -0,0 +1,477 @@ +# Web UI Redesign — Implementation Plan (Phase 1) + +**Status:** Stages A, B, C complete + gates approved (989 tests pass, cov 94.12%, ruff+mypy clean). Stage D (service management, SECURITY-CRITICAL) not started. Uncommitted. +**Owner / executor:** Sonnet (implementation) under Opus supervision (review gate after every stage). +**Scope:** Presentation redesign + new read-only panels + service management. **No** time-series store, **no** light theme, **no** rebuild of Gitea job/log views. +**Target files root:** `src/ci_orchestrator/web/`, `src/ci_orchestrator/operations/`. + +This plan is written to be executed by a Sonnet-class model. Each stage is +self-contained, ends with a hard **STOP — supervisor review** gate, and lists +exact files, signatures, tests, and acceptance criteria. Do not start a stage +before the previous stage's gate is approved. Keep the checklist below current +(tick `- [x]` as tasks land); the supervisor or a subagent updates it. + +--- + +## Progress checklist (keep this updated) + +Legend: `[ ]` todo · `[~]` in progress · `[x]` done · `[!]` blocked + +### Stage A — Restyle foundation + bug fixes +- [x] A1. Swap CSS tokens to real Gitea dark palette (accent → blue `#4183c4`) +- [x] A2. `web/filters.py` — number/format Jinja filters + registration in `app.py` +- [x] A3. Status header partial (`partials/status_header.html`) + health thresholds +- [x] A4. Fix unthemed action pages (htmx swap + full-page `HX-Request` fallback) +- [x] A5. Fix polling-wipes-form-input bug (`hx-preserve` on inputs) +- [x] A6. Inline SVG icon sprite (`partials/icons.html`) + apply to card headers +- [x] A7. Stage A tests green · ruff · mypy --strict · coverage ≥90 +- [x] **A-GATE — supervisor review** (Opus: gates green 895 pass / cov 94.15% / ruff+mypy clean; theming fix + action-slot verified) + +### Stage B — Read-only panels +- [x] B1. `operations/host.py` — `host_resources()` (/proc, 2-sample CPU%) +- [x] B2. `operations/host.py` — `vmware_module_health()` (lsmod + vmware-host.service) +- [x] B3. `operations/inventory.py` — `list_templates()` (+ `.vmem` health, last backup) +- [x] B4. `operations/inventory.py` — `list_backups()` / `list_artifacts()` (path-contained) +- [x] B5. GET partials + routes: `/resources`, `/vmware`, `/templates`, `/files` +- [x] B6. Backup card → template **dropdown** (validated against `list_templates`) +- [x] B7. Slow-poll / on-demand refresh for expensive panels (dir-size cache) +- [x] B8. Stage B tests green · ruff · mypy --strict · coverage ≥90 +- [x] **B-GATE — supervisor review** (Opus: gates green 958 pass / cov 94.10% / ruff+mypy clean; containment + 2-sample CPU + backup-name 400 verified) + +### Stage C — VM enrichment + Gitea link-out + audit polish +- [x] C1. `parse_clone_name()` + `repo/job` (job_id + started) column in VM table +- [x] C2. Gitea base URL in config/context; nav `Gitea ↗` + contextual links +- [x] C3. Audit page restyle (relative time, action icons, simple filter) +- [x] C4. Stage C tests green · ruff · mypy --strict · coverage ≥90 +- [x] **C-GATE — supervisor review** (Opus: gates green 989 pass / cov 94.12% / ruff+mypy clean; subagent cut off mid-C2 by session limit — Opus finished wiring + fixed 2 left-over test bugs: empty-VM header assert, OIDC keyring stub) + +### Stage D — Service management (SECURITY-CRITICAL, last) +- [ ] D1. `operations/services.py` — allow-list, `service_states()`, `control_service()` +- [ ] D2. POST `/services/{service_id}/{action}` — confirm + CSRF + audit + refusals +- [ ] D3. `ci-web` self-restart via detached `systemd-run` (response returns first) +- [ ] D4. Services panel partial (state-aware buttons, active-VM warning on stop) +- [ ] D5. `sudoers.d/ci-web` (exact-command NOPASSWD) + installer `visudo -c` validate +- [ ] D6. Config `[web].managed_services` override + `config.example.toml` + `WEB-UI.md` +- [ ] D7. Security regression tests (unknown id → 400 & systemctl never called; etc.) +- [ ] D8. Stage D tests green · ruff · mypy --strict · coverage ≥90 +- [ ] **D-GATE — supervisor review + deploy note** + +--- + +## Locked design decisions + +| Decision | Value | +|---|---| +| Theme | **Dark only**, mirroring Gitea `gitea-auto` dark values | +| CSS approach | Hand-written, extend existing tokens. **No framework, no CDN** (LAN offline) | +| Data over time | **None** — all panels point-in-time snapshots | +| Job history / logs | Not rebuilt — link out to Gitea | +| New runtime dep | **None** — host metrics via `/proc`, not psutil | +| Service control | Full start/stop/restart over a **fixed allow-list** | +| Read routes | `web/app.py` · Mutating routes | `web/routes.py` (existing split, keep it) | + +### Real Gitea dark palette (source: `gitea.emulab.it` theme `gitea-auto`) + +``` +--color-primary #4183c4 (accent — replaces green #3fb950) +--color-primary-contrast #ffffff +--color-body #1e1f20 (page bg) +--color-box-body #161718 (card bg) +--color-box-header #1b1c1e (card header bg) +--color-nav-bg #18191b (navbar bg) +--color-secondary-bg #2e3033 (input / subtle panel) +--color-text #d2d4d8 +--color-text-light #c0c2c7 (dim) +--color-secondary #3f4248 (border-ish) +--color-success-text #3fb950 --color-error-text #f85149 --color-warning-text #d29922 +--color-info-text #2f81f7 +``` + +### Health thresholds (status header + row colors) +- Disk `used_pct` > 85 → **red**, > 70 → **amber**, else green. +- Any runner unit not `active` → **red**. +- Orphan VM present → **amber**. +- VMware `vmmon` or `vmnet` not loaded, or `vmware-host.service` not active → **red**. + +--- + +## Architecture changes (file map) + +New files: +- `web/filters.py` — Jinja formatting filters (pure functions, unit-tested). +- `operations/services.py` — managed-service allow-list + status + control. +- `operations/inventory.py` — template discovery + directory listing (path-contained). +- `web/templates/partials/`: `status_header.html`, `resources.html`, `vmware.html`, + `templates.html`, `files.html`, `services.html`, `icons.html`. + +Edited files: +- `web/static/app.css` — token swap + new components (gauge, stat tile, service controls). +- `web/app.py` — register filters; new GET routes/partials; status-header context. +- `web/routes.py` — service-control POST; backup route → dropdown validation. +- `operations/host.py` — add `host_resources()`, `vmware_module_health()`. +- `web/templates/base.html` — nav `Gitea ↗`; `#action-slot`. +- `web/templates/partials/{vms,cache,host,audit,confirm,result}.html` — restyle + fixes. +- `config.py` / `config.example.toml` — `[web].gitea_url`, `[web].managed_services`. +- `docs/WEB-UI.md` — sudoers, service panel, new panels. + +**Isolation rule (why the split):** `operations/host.py` already carries +disk+runner+backup. Adding resources+modules+templates+files+service-control +would bloat it past what fits in one reviewer's head. Keep host-level probes in +`host.py`, inventory/listing in `inventory.py`, service control in +`services.py`. Each is independently testable and imported by both CLI and web. + +--- + +## Stage A — Restyle foundation + bug fixes + +**Goal:** the dashboard *looks* professional and the two known UI bugs die. +No new data sources. This stage alone fixes "look grezzo / colpo d'occhio / +layout piatto" and the unthemed-action-page complaint. + +### A1 — CSS tokens +Edit `web/static/app.css` `:root`: replace the palette with the Gitea dark +values above (accent `#4183c4`; `--bg`→`#1e1f20`; `--panel`→`#161718`; +`--panel-2`→`#2e3033`; `--border`→`#3f4248`; `--text`→`#d2d4d8`; +`--text-dim`→`#c0c2c7`; `--accent`→`#4183c4`; `--accent-hover` a lighter blue; +keep `--danger`/`--warn` as-is). Update `.btn-primary` contrast text to +`#ffffff`. No structural HTML change in A1. + +### A2 — Jinja filters (`web/filters.py`) +Pure functions + a `register(templates: Jinja2Templates)` helper called from +`app.py` `build_app`. Signatures: +```python +def humanbytes(n: int | float) -> str # 12_345_678 -> "11.8 MB" +def duration(seconds: int | float | None) -> str # 3783 -> "1h 03m"; None -> "-" +def relative_time(value: str | datetime | None) -> str # "2m ago"; None -> "-" +def localtime(value: str | datetime | None) -> str # ISO -> host-local "YYYY-MM-DD HH:MM" +def shorten_path(p: str | Path, keep: int = 2) -> str # ".../Templates/LinuxBuild2404" +def thousands(n: int | float) -> str # 812 -> "812"; 12345 -> "12,345" +``` +Apply in templates: `cache.html` (`total_bytes|humanbytes`, `entry_count|thousands`), +`vms.html` (`uptime_sec|duration`), `host.html` (disk GB formatting tidy), +`audit.html` (`ts|relative_time` with `ts|localtime` in `title=`). + +### A3 — Status header (`partials/status_header.html`) +A row of stat tiles fed from the existing dashboard context (no new fetch): +VM count (+orphan count), runner `active/total`, disk `used_pct` gauge, cache +size. Apply health-threshold classes. Included at top of `dashboard.html`, +above `.dashboard-grid`. Add a `_health_summary(...)` helper in `app.py` that +derives the tile values/colors from already-fetched context (keep it pure). + +### A4 — Fix unthemed action pages +Root cause: mutating `