ADR-0004 — Admin: master list → detail page, without a router¶
Status: accepted — implemented (backlog item 0036, mockup v11) · revised 2026-07-28 · Scope: admin/index.html + admin/server.js
Ratification 2026-07-28 (implementation, mockup v11). The original accordion form (
<details>/<summary>) is abandoned in favor of a master list → detail page: a DOM state swap (VIEW = {mode:'list'|'detail'}, functionsrenderList/renderDetail), still without a router or URL. Decision 1 below is therefore revised accordingly; figures 1-2 illustrate the initial accordion exploration and hold for the spirit (two states, no navigation), not for the final form.Deviations accepted at ratification (relative to decisions 2 and 3 as written): - Decision 2 — zone editing lives in the existing Zones dialog (the "Zones…" button on the Drive row of the detail), which already carries the per-zone actions (extend / revoke / add), rather than in inline editable rows in the panel. Same function, form = reused modal (zero dependencies, no rewrite). - Decision 3 — the server exposes the raw policy flags (
/api/profiles→policy); the front end derives the rights labels from them (capsHtml,SVCDEFtable). The invariant that holds is the essential one: the front end never asserts a right as hard-coded — each "green / red" comes from a real flag. The flag → label mapping (shared, a single table) lives on the front-end side rather than the server side; the 0037 switch (trash = deletion) will still act only on a server flag (delete), not on the front end. Related backlog items: 0036 (card clarity), 0037 (semantics of deletion in a zone)TL;DR — The administration web page (which manages the connected Google accounts) drops the expandable "accordion" cards in favor of a clickable list that switches to a per-account detail page — with no URL or router, just a change of on-screen state — so that it stays a single page with no dependencies; and the rights displayed for each account are derived in the browser from the raw policy flags served by the server (
/api/profiles), rather than hard-coded in the page.
Context¶
The local web admin is a single vanilla HTML/CSS/JS page (no framework,
no router, no build), served by admin/server.js in pure stdlib Node
(zero dependencies). Data arrives as JSON (GET /api/profiles), actions
go out as POST /api/<alias>/<action> to bin/mag.
We want to move from a simple list of cards to: (a) an overview (all accounts, summarized); (b) a per-account detail where Drive write zones are visible and editable inline (per zone: extend, revoke, edit).
Two possible forms: a master list alone with in-place expansion, or master + detail view (which requires some form of navigation/routing).
Cross-cutting constraint: the warning text shown when a zone is set ("the LLM will be able to create/modify/trash…") depends on an unresolved policy decision (backlog item 0037: trash = deletion? immutable zone root?). The UI must not freeze that decision.
Decision¶
1. Master list → detail page, by DOM state swap (~~accordion
<details>/<summary>~~, revised — see ratification at the top). A single state
VIEW = {mode:'list'|'detail', alias} and two render paths (renderList /
renderDetail) in the same #cards: the list shows one row per account
(toggle · padlock · count · chevron); clicking a row renders the detail page
(rights per app, zones, danger zone); "‹ Accounts" goes back to the
list. Still no router — neither a hash on the front end nor an HTML route on the
server side: a simple state toggle, a single artifact.
2. Zone editing lives in the expanded panel of the card. Each zone
is a row of the panel carrying its own actions (extend / revoke /
edit); the collapsed summary (1 temporary · expires in 1 h 58) remains the
condensed view. The "add a zone" button is at the foot of the panel.
3. The zone warning is DATA supplied by the server, not a string coded in the front end. server.js (which holds the policy) exposes, per
service and per profile, the list of verbs actually allowed (create,
modify, trash…) derived from the policy flags. The front end renders this
list; it never asserts a verb as hard-coded.
stateDiagram-v2
direction LR
[*] --> Liste
state "Overview (a)\nN collapsed cards: connection · access · zones summary" as Liste
state "Expanded card (b)\neditable zones: extend / revoke / edit · + add" as Detail
Liste --> Detail : click on a card (native details/summary)
Detail --> Liste : click again (collapse)
Detail --> Detail : zone action -> POST /api/<alias>/<action> -> re-render
note right of Detail
Only one panel open at a time (accordion).
No URL, no router: a simple DOM state toggle.
end note
Figure 1 — Interaction model: the overview and the detail are two
states of the SAME card (collapsed / expanded), linked by native <details>; no
navigation or URL is introduced.
flowchart TD
subgraph FRONT["admin/index.html (rendering)"]
A["Zone panel of an account"]
end
subgraph BACK["admin/server.js (holds the policy)"]
B["Capability descriptor\nallowed verbs = f(policy flags)"]
end
subgraph OPEN["backlog item 0037 (decision NOT settled)"]
C["trash = deletion?\nimmutable zone root?"]
end
A -->|"reads and displays the list of verbs"| B
C -.->|"will flip a flag, later"| B
A -. "does NOT know" .-x C
classDef front fill:#eeedfe,stroke:#534ab7,color:#222;
classDef back fill:#e1f5ee,stroke:#0f6e56,color:#222;
classDef open fill:#faeeda,stroke:#854f0b,color:#222;
class A front
class B back
class C open
Figure 2 — Decoupling: the front end (purple) depends on an abstraction, the
capability descriptor (green); decision 0037 (amber) will act only on a server
flag. Solid line = accepted dependency; dotted = future influence; cross
= the front end has no direct link to 0037. Settling 0037 will not touch
index.html.
Rejected options¶
- Master + detail view with a hash router (
#/profile/perso): requires a home-made mini-router (parsinglocation.hash, view swap, back-button handling) - two render paths to keep in sync. YAGNI for 5–20 accounts.
- Server HTML route
/profile/<alias>: duplicates the layout, one network round trip per account, breaks the single page. - Detail in a
<dialog>modal: dependency-free but locks you into one account, prevents scanning/comparing zones across accounts; heavier than inline. - Warning hard-coded in the front end (current state of the mockup): couples the UI to the 0037 semantics, which are still open.
Consequences¶
- + Zero new dependencies; a single render path; accessible and free
expansion (native
<details>, already used for the ⋯ menu). - + Scales by scrolling; only one panel open at a time keeps the DOM small and attention focused.
- + 0037 can be settled without touching
index.html: only the verb descriptor inserver.jschanges. - − Long expanded panel on a small screen → mitigated by the accordion (one open)
and a
scrollIntoViewon opening. - − Small backend addition: expose the allowed verbs per service/profile
in
/api/profiles(or a dedicated endpoint). This is the right home — the policy lives on the server side (DIP). - At 20+ accounts, plan a filter/search at the top of the list (cheap, later); out of scope for the POC.