feat(web): Executions card replaces Live VMs
The dashboard could only show what was powered on this instant, so it could
not answer "did the last build pass?" — and no local execution history exists
to answer it from (`report job` reads a PowerShell-era JSONL path that nothing
writes on this host, and the recent artifact dirs are empty).
Recent runs now come from Gitea's Actions API (`operations/executions.py`):
`/api/v1/admin/actions/runs` for every repo on the instance, falling back to
`/api/v1/user/actions/runs` when the token is not admin-scoped. One list call
per refresh, cached 10s, so the card's 5s htmx poll costs Gitea at most one
call per window. Live VMs are grouped under the run that spawned them — the
job id is `{run_id}-{run_attempt}[-suffix]` and the clone name carries it — so
a matrix run collects several VMs on one row. Every failure mode (no token,
401, unreachable Gitea, malformed payload) degrades to a notice plus the plain
VM rows, each keeping its Remove button, so the card is never less useful than
the table it replaces. Orphans stay where they were: the sweep in Maintenance,
the count in the status-header tile.
Auth reuses the access token from the operator's own OIDC login, which
`auth.py` previously discarded. SECURITY: that token is kept server-side only
(`app.state.gitea_tokens`, dropped on logout) and never written into the
session — `SessionMiddleware` signs the session cookie but does not encrypt
it, and this service is deployed over plain HTTP on the LAN, so a bearer token
in there would be readable in the browser and on the wire. An expired token is
refreshed once; `[web] gitea_token_cred` names an optional keyring PAT for the
cases a login token cannot cover (service restart, non-admin operator).
`GET /vms` and `partials/vms.html` are gone; `POST /vms/{id}/remove` and
`POST /vms/cleanup` are untouched.
Design: docs/superpowers/specs/2026-07-25-executions-card-design.md
This commit is contained in:
+65
-9
@@ -1,10 +1,11 @@
|
||||
# Web UI — Management Dashboard
|
||||
|
||||
Server-rendered dashboard (`ci web serve`) for the local CI/CD system: live
|
||||
VMs, cargo cache, disk, and act_runner status, plus confirm-gated destructive
|
||||
actions (VM remove/cleanup, cache purge, template backup) with an audit
|
||||
trail. In-process only — it imports `operations/` and `backends.load_backend`
|
||||
directly and never shells out to the CLI.
|
||||
Server-rendered dashboard (`ci web serve`) for the local CI/CD system: recent
|
||||
CI executions with their live VMs (§8), cargo cache, disk, and act_runner
|
||||
status, plus confirm-gated destructive actions (VM remove/cleanup, cache
|
||||
purge, template backup) with an audit trail. In-process only — it imports
|
||||
`operations/` and `backends.load_backend` directly and never shells out to
|
||||
the CLI.
|
||||
|
||||
See `plans/web-ui-management-plan.md` (steps W0–W7) for the design history
|
||||
and `config.example.toml`'s `[web]` / `[web.oidc]` section for every
|
||||
@@ -105,6 +106,11 @@ application **before** enabling `[web.oidc]`:
|
||||
5. Add the Gitea username(s) allowed to log in to `[web].allowed_users`.
|
||||
**The allowlist is fail-closed**: an empty list denies everyone, it
|
||||
never means "allow all".
|
||||
6. For the Executions card (§8), add an API read scope to
|
||||
`[web.oidc].scopes` — `read:repository`, plus `read:admin` if the account
|
||||
is a Gitea admin and you want runs from every repo on the instance. New
|
||||
scopes take effect after one fresh login (Gitea shows the consent screen
|
||||
again); existing sessions keep working with the old scopes until then.
|
||||
|
||||
---
|
||||
|
||||
@@ -123,8 +129,10 @@ systemctl status ci-web.service
|
||||
journalctl -u ci-web.service -f
|
||||
journalctl -u ci-web.service -n 100 --no-pager
|
||||
|
||||
# Health check (unauthenticated, always 200)
|
||||
curl -s http://127.0.0.1:8800/healthz
|
||||
# Health check (unauthenticated, always 200). Use the address the service is
|
||||
# bound to -- uvicorn listens on `[web].bind_host` only, so on this host
|
||||
# (192.168.10.11) a loopback request gets connection-refused, not a 200.
|
||||
curl -s http://192.168.10.11:8800/healthz
|
||||
```
|
||||
|
||||
Once code changes: `sudo bash setup-web-linux.sh --host <same-as-before>`
|
||||
@@ -239,7 +247,7 @@ allow_stop}` tables. A malformed row is skipped, never crashes startup.
|
||||
- **`vmware-host` stop/restart interrupts running jobs.** The confirm step
|
||||
shows the current count of active VMs ("N active VM(s) — this interrupts
|
||||
running jobs") when stopping or restarting `vmware-host.service`, computed
|
||||
from the same VM backend the Live VMs panel uses — best-effort, degrades
|
||||
from the same VM backend the Executions card uses — best-effort, degrades
|
||||
to no warning if the backend is unavailable, never blocks the action.
|
||||
- **Every action is audited exactly once per stage** (`service.start` /
|
||||
`service.stop` / `service.restart`, `target` = the unit name), same as
|
||||
@@ -272,7 +280,55 @@ on the live host with `sudo visudo -c` before relying on the panel.
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
## 8. Executions card (recent Gitea Actions runs)
|
||||
|
||||
The dashboard's top wide table is **Executions**: the most recent workflow
|
||||
runs, the live ones included, each with its status, workflow, branch/commit,
|
||||
trigger, duration, and the build VM(s) that run spawned (with their `Remove`
|
||||
button). It replaced the old read-only "Live VMs" table — a VM alone could
|
||||
never answer "did the last build pass?".
|
||||
|
||||
Runs come from Gitea's Actions API: `/api/v1/admin/actions/runs` (every repo
|
||||
on the instance, admin token) with `/api/v1/user/actions/runs` as the
|
||||
non-admin fallback. `[web].executions_limit` (default 15) caps how many.
|
||||
Results are cached for 10s, so the card's 5s refresh costs Gitea at most one
|
||||
call per 10s window; the VM columns are re-read on every refresh.
|
||||
|
||||
**Where the API token comes from.** Normally nowhere extra: the dashboard
|
||||
reuses the access token from your own OIDC login, so what the card shows is
|
||||
exactly what your Gitea account can see. That token is kept **server-side
|
||||
only** (an in-process map keyed by username) and dropped on logout — it is
|
||||
never written into the session cookie, which is signed but *not* encrypted and
|
||||
travels over plain HTTP on the LAN. An expired token is refreshed once
|
||||
automatically; a refusal that refresh cannot fix shows a "log in again" link
|
||||
in the card.
|
||||
|
||||
Optional fallback: store a Gitea PAT in the keyring and name it in
|
||||
`[web].gitea_token_cred`. It is used only when no logged-in token is
|
||||
available — after a service restart (which empties the in-process map while
|
||||
session cookies stay valid), or for an operator whose own token lacks the
|
||||
scope. Set it exactly like the other secrets:
|
||||
|
||||
```bash
|
||||
sudo -u ci-runner /opt/ci/venv/bin/python -m ci_orchestrator creds set \
|
||||
--target GiteaApiToken --user gitea-api
|
||||
```
|
||||
|
||||
**Degraded states** never take the card down: no token, an expired token, or
|
||||
an unreachable Gitea each render a warning notice *and still list every
|
||||
running VM* below it, so the dashboard is never less useful than the VM table
|
||||
it replaced.
|
||||
|
||||
**Orphaned VMs are not handled in this card.** A powered-on VM whose run is no
|
||||
longer in the window (or that no run ever claimed) keeps a row of its own
|
||||
flagged `no run`, with its `Remove` button — but the orphan *sweep* lives in
|
||||
the **Maintenance** card ("Cleanup orphaned VMs") and the orphan *count* in
|
||||
the status-header VMs tile. Those two are the places to look at orphans; the
|
||||
Executions card only avoids hiding them.
|
||||
|
||||
---
|
||||
|
||||
## 9. Troubleshooting
|
||||
|
||||
**Login loop (bounces back to `/login` after authenticating with Gitea)**
|
||||
: The registered Redirect URI in Gitea does not exactly match
|
||||
|
||||
Reference in New Issue
Block a user