--- id: ops-dashboard title: A human can see what their agents are doing and stop them status: blocked crates: [didbot-serve] dependsOn: [scope-policy, app-allowlist, write-policy, e-stop] exitCriterion: > An owner signs in with their own atproto account and can see, for one agent, every scope it was refused and which rule refused it. --- # ops-dashboard Served by this server, about this server. It shows live state — agents, grants, refusals — and carries the emergency stop. It owns no policy: everything it can change is operational, and everything it reports about itself is only as trustworthy as the host it runs on. Sign-in is atproto OAuth against the owner's own server, requesting the `atproto` scope and nothing else. The owner loads the page here and stays on it; the approval happens in a popup, somewhere we do not run. - [ ] **Sign in with authentication only.** No write scope on the owner's account, ever, and the client metadata that says so is public. Needs [oauth](oauth.md). Until it exists, the dashboard is gated behind the operator credential from [auth-types](auth-types.md) instead, and the page says on its face that this is a stand-in. - [ ] **The refusal log.** What was asked, what was granted, what was removed and which rule removed it — for an agent, an app, or the whole server. Needs [write-policy](write-policy.md), [scope-policy](scope-policy.md) and [app-allowlist](app-allowlist.md), none of which exist yet; the panel says so and shows no numbers, rather than a fabricated zero. - [ ] **Collections nothing covers.** A ceiling is an enumeration, so a new lexicon is silently outside every profile until somebody names it. - [ ] **Scope the stop** beyond "everything" — [e-stop](e-stop.md)'s own open item, not this epic's to close. - [ ] **Grants and app authorisations.** Needs [oauth](oauth.md); reported as not implemented rather than invented. Every live grant names its client (`OAuthGrantStore::live`), and whether policy refuses it now is a `Subject::Token` judged against the current tree, not a stored state. A refusal row names its client (`DenialRow::client_id`), and so does each admitted write (`didbot_pds::write_log`), so what an app wrote can be listed for the app. - [ ] **No prose written by a model.** Placeholders until a human writes them — holds for every string this epic's own work has added so far. ## Done - [x] **Say what it cannot be trusted about.** The page carries a trust note stating that a compromised server can lie here and that anything security-relevant has to be checked against the owner's own records, and a machine-readable `/dashboard/api/capabilities` says the same thing per panel: live, not implemented, or unreachable, never a number standing in for one it does not have. - [x] **The homepage half of this surface**: an unauthenticated `/dashboard/api/about`, reusing `bot.did.stats`'s own numbers, so a person opening a running deployment's address with no credential still gets a real answer about what it is and what it holds — per `docs/web-surfaces.md`. - [x] **The visual**: prototype F's travelling-wave field (`crates/didbot-site-anim`) as the page's background, with F's bordered-readout-box and column language carrying the panels, opaque enough that the field never competes with a value or a label. Built opt-in (`scripts/build-dashboard-wasm.sh`), same as the site's own wasm step, so `cargo build`/`cargo test` need no wasm32 toolchain.