--- id: analytics-dashboard title: What this deployment's agents are doing, on a page the operator may make public status: open crates: [didbot-serve] dependsOn: [oauth] exitCriterion: > An operator opens one deployment's analytics page and sees every agent it holds, what each wrote and into which collections, and which apps each signed in to; with the page set public, a stranger sees the same page and can change nothing. --- # analytics-dashboard One of three dashboards — [docs/web-surfaces.md](../docs/web-surfaces.md) tells them apart. This is the analytics one: served by this server, about this server's own agents, and read-only by construction. The operator decides whether it is public. [ops-dashboard](ops-dashboard.md) is the page with controls on it, and nothing on that page may ever be public; the two share a binary and nothing else. Everything drawn here is a public record or a number `bot.did.stats` already publishes, so setting the page public discloses nothing a stranger could not assemble from the firehose. What the page adds is the assembly, for one deployment; vibescrobble.com draws the same kind of picture across the whole network. - [ ] **Public or operator-only is one configuration key**, operator-only by default. Operator-only means the sign-in [ops-dashboard](ops-dashboard.md) uses — atproto OAuth against the operator's own server, `atproto` scope and nothing else — and no second credential. - [ ] **What each wrote, by collection**, from `didbot_pds::write_log`, and **which apps each signed in to**, from `OAuthGrantStore::live` — the same rows the operations page reads, drawn for a reader who may not be the operator, so a row carries nothing but what a public record would. - [ ] **No prose written by a model.** Placeholders until a human writes them. ## Done - [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. It reports the deployment's own lifecycle line beside them. - [x] **Every route under it is a read.** The page sits under `/analytics`, apart from the operations dashboard's prefix. No control lives here, so a public setting widens what can be seen and never what can be done. A test walks the router and refuses a non-`GET` under the page's prefix. - [x] **The agents, and the tree between them.** `GET /analytics` draws every account this server holds — its handle, DID, kind, operator, the proof its document publishes, when it was created and the state of the edge above it — nested under the account that created it. A chain a stranger could not walk is drawn as paused rather than silently listed. `bot.did.listAccounts` carries the same values, so the page and the wire cannot disagree.