# Vault-tab management surfaces revamp **Date:** 2026-05-23 **Status:** Spec, awaiting review **Surface:** Browser extension management panes — `extension/src/popup/components/` (shared between popup and vault tab) ## Problem Four "management" surfaces in the extension — **Settings**, **Devices**, **Trash**, and **field history** — all shipped in the 1C-β₂ / device-auth waves but in the *pre-fullscreen-redesign* visual language. They read as popup-derived forms stretched across the vault tab, with inconsistent typography, no glyph buttons, no focus rings, and no visual section grouping. Several functional gaps remain alongside the visual debt: - **Settings**: per-device session-timeout config UI was specced in the vault-tab design (2026-04-27) but never built; the only way to change session behavior today is to edit `chrome.storage.local` directly. - **Devices**: the `revoke_device` SW message handler exists but no UI surfaces it — revocation is CLI-only. Device entries don't expose the SHA256 fingerprint (used for verifying against the server-side `devices.json`) or the `added_by` field that's already in `DeviceEntry`. - **Trash**: per-item purge countdown isn't shown — users see "trashed N days ago" but have to mentally add the retention window to figure out when it'll be gone. - **History**: the per-item field-history viewer (`field-history.ts`) is only reachable from an item detail page; there's no entry point to discover *which* items have history. This spec applies the fullscreen visual-language tokens to all four panes and closes the gaps above. It deliberately stays small and ships in the v0.5.x train, in the current `vault.ts` shell — Phase 3 shell rearchitecture is out of scope. ## Goals - All four management panes adopt the fullscreen visual language: glyph buttons, focus ring, uppercase section headers with 1px bottom rule, accent tokens, required-field pill where applicable. - Close the four functional gaps above (session timeout UI, revoke button surfacing, fingerprint + added-by display, purge countdown, history index). - Add **one new pane** — "items with history" index — reachable from a new `◷ history` slot in the sidebar bottom-nav. - Zero core or wasm changes; zero data-model changes; zero new schema versions. ## Non-goals - **Phase 3 shell rearchitecture** (three-pane layout, `shell/three-pane.ts`, `keymap.ts`) — separate effort, separate spec. - **Phase 4 command palette** — deferred to its own brainstorm round. - **Item-level snapshot history** — option B/C from brainstorm; this spec uses option A (aggregate existing `field_history` per item; no new core storage). - **Settings as a hub with sub-tabs** — would introduce sub-tab pattern not used elsewhere; defer. - **Trash polish**: hover-preview, multi-select bulk-restore — defer. - **Devices polish**: rotate-key flow, "last seen" detail (would need new data) — defer. - **History polish**: diff view between historical values — defer. ## Scope summary | Surface | Files touched | New? | |---|---|---| | Settings | `popup/components/settings-vault.ts`, `vault.css`/`popup.css` | modify | | Devices | `popup/components/devices.ts`, SW response shape | modify | | Trash | `popup/components/trash.ts` | modify | | History — index | `popup/components/item-history-index.ts` | **NEW** | | History — per-item | `popup/components/field-history.ts` | polish only (no rename) | | Glyph constants | `shared/glyphs.ts` | depends-on-or-creates | | Time helper | `shared/relative-time.ts` | **NEW** (extracted from 3 call sites) | | Routing | `vault/vault.ts` | add `#history` + `#history/` routes | | Nav | sidebar bottom-nav | grows 3 → 4 (`▦ trash · ⌬ devices · ⚙ settings · ◷ history`) | --- ## Architecture ### Component map ``` extension/src/ ├── shared/ │ ├── glyphs.ts ← depends on (or creates if absent): │ │ GLYPH_TRASH ▦, GLYPH_DEVICES ⌬, │ │ GLYPH_SETTINGS ⚙, GLYPH_LOCK ⏻, │ │ GLYPH_HISTORY ◷, GLYPH_REVOKE ⊘, │ │ GLYPH_RESTORE ⤺, GLYPH_REVEAL ⊙, │ │ GLYPH_COPY ⧉ │ └── relative-time.ts ← NEW (small util — inlined in 3 places today) ├── popup/components/ │ ├── settings-vault.ts ← rewrite layout; add session-timeout row │ ├── devices.ts ← add fingerprint, "added by"; surface revoke button │ ├── trash.ts ← add per-item purge countdown │ ├── field-history.ts ← visual polish only (filename kept) │ └── item-history-index.ts ← NEW: "items with history" index └── vault/ ├── vault.ts ← add #history routes + bottom-nav slot ├── vault.css ← four shared utility classes (below) └── popup.css ← same classes (shared components render in both) ``` ### SW message protocol — 99% reuse | Capability | Message | Status | |---|---|---| | Read/write vault settings | `get_vault_settings` / `update_vault_settings` | exists | | Read/write session timeout (per-device) | `get_session_config` / `update_session_config` | exists | | List active + revoked devices | `list_devices` / `list_revoked` | exists | | Register / revoke device | `register_this_device` / `revoke_device` | exists | | List trashed, restore, purge | `list_trashed` / `restore_item` / `purge_item` / `purge_all_trash` | exists | | Per-item field history | `get_field_history` | exists (reused for index + per-item) | **Single shape change:** extend `ListDevicesResponse` to include `fingerprint: string` per entry — SHA256 of the device's ed25519 public key, computed via the existing `core::device::fingerprint()` function. No new message round-trip. ### Shared CSS utility classes Defined in `vault.css` and `popup.css` (shared because components render in both contexts): ```css .section-header { text-transform: uppercase; font-weight: 500; letter-spacing: 1px; color: var(--text-muted); border-bottom: 1px solid var(--border-subtle); padding-bottom: 4px; margin-bottom: 10px; } .glyph-btn { min-width: 28px; font-family: ui-monospace, monospace; background: transparent; color: var(--text-muted); border: 1px solid var(--border-subtle); border-radius: 3px; padding: 2px 6px; cursor: pointer; } .glyph-btn:hover { color: var(--text); background: var(--bg-input); } .glyph-btn:focus-visible { box-shadow: var(--focus-ring); outline: none; } .glyph-btn[data-danger]:hover { color: var(--danger); border-color: var(--danger); } .kv-row { display: flex; justify-content: space-between; align-items: baseline; padding: 4px 0; } .kv-row > .k { color: var(--text-muted); } .kv-row > .v { color: var(--text); font-variant-numeric: tabular-nums; } .fingerprint { font-family: ui-monospace, monospace; color: var(--text-muted); font-size: 11px; word-break: break-all; /* wraps to two lines in popup (~360px) */ } ``` ### Visual language reference All tokens come from the existing fullscreen UX redesign spec (`2026-04-30-relicario-fullscreen-ux-redesign-design.md`, "Visual language" section). No new tokens introduced. New glyph constants added to `shared/glyphs.ts` if not already present: `GLYPH_HISTORY ◷`, `GLYPH_REVOKE ⊘`, `GLYPH_RESTORE ⤺`, `GLYPH_REVEAL ⊙`, `GLYPH_COPY ⧉`. --- ## A. Settings pane Two-tier section grouping makes the storage distinction explicit: **VAULT SETTINGS · synced** lives in the encrypted vault (replicated via git), **THIS DEVICE · local** lives in `chrome.storage.local` (per-device, not synced). **ACTIONS** is destructive/expensive operations. ``` ◀ settings unsaved · ⌘+S to save no changes VAULT SETTINGS · synced ───────────────────────────────────────────────────────────── ┌──────────────────────────┐ ┌──────────────────────────┐ │ RETENTION │ │ GENERATOR │ │ trash [30 days ▾] │ │ length 24 │ │ history [last 5 ▾] │ │ words 4 │ │ │ │ [ configure defaults ↻ ] │ └──────────────────────────┘ └──────────────────────────┘ ┌──────────────────────────┐ │ ATTACHMENTS │ │ max size [25 MB ▾] │ └──────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ AUTOFILL ORIGINS │ │ github.com acknowledged 2d ago ⊘ │ │ gitlab.adlee.work acknowledged 5d ago ⊘ │ └─────────────────────────────────────────────────────────┘ THIS DEVICE · local ───────────────────────────────────────────────────────────── ┌──────────────────────────┐ │ SESSION │ │ ○ lock every time │ │ ● after inactivity │ │ [15 min ▾] │ └──────────────────────────┘ ACTIONS ───────────────────────────────────────────────────────────── [ backup & restore ] [ import from… ] ``` ### Decisions - **Two-tier grouping** with `· synced` / `· local` muted suffixes makes storage scope unambiguous without being preachy. - **Two-column where fields are small** (Retention ↔ Generator; Attachments standalone in row 2). Collapses to single-column under 720px viewport — same rule the login form uses. - **Session timeout** wires the existing `get_session_config` / `update_session_config` SW messages to a radio (`every_time` / `inactivity`) + minutes dropdown (5/15/30/60). Already-validated config shape from the vault-tab spec. - **Form header** reuses the "unsaved · ⌘+S to save" / "no changes" subtitle from the form-layout spec — gives Settings the same dirty-state feedback as item edits. - **Generator section** keeps the current "configure defaults" button opening the popover from Phase 2A — no inline expansion. - **Actions** uses text-labelled buttons (not glyph buttons) since they open dedicated panes; text is clearer than icons for navigation-style actions. --- ## B. Devices pane Single column (this is a list, not a form). Three-line per-entry rhythm: name (+ `← you` marker or `⊘` revoke glyph), full SHA256 fingerprint, then `added X ago · by Y`. ``` ◀ devices ACTIVE · 3 ───────────────────────────────────────────────────────────── ⌬ Aaron's laptop ← you SHA256:8f3a:c7d2:1e44:9b08:6f55:a201:de9c:4477 added 2 months ago · by Aaron ⌬ Aaron's phone ⊘ SHA256:9c11:e4f8:2a91:db32:7c0e:51bb:e8a4:1f6d added 3 weeks ago · by Aaron's laptop ⌬ work-laptop ⊘ SHA256:b277:35aa:c1e0:8f44:62b9:0d3e:7c1f:5d92 added 8 days ago · by Aaron's laptop REVOKED · 1 ───────────────────────────────────────────────────────────── ▸ show 1 revoked device ``` ### Revoke confirmation — inline two-step Clicking `⊘` expands a confirmation panel in place (no modal): ``` ⌬ Aaron's phone SHA256:9c11:e4f8:2a91:db32:… added 3 weeks ago · by Aaron's laptop ┌─────────────────────────────────────────────────────┐ │ Revoke this device? It won't be able to sign │ │ commits or push changes after revocation. │ │ │ │ [ cancel ] [ revoke ] │ └─────────────────────────────────────────────────────┘ ``` ### Unregistered state — top banner ``` ◀ devices ┌─────────────────────────────────────────────────────────┐ │ This device isn't registered. │ │ Registering generates an ed25519 keypair and adds the │ │ public key to .relicario/devices.json on the remote. │ │ │ │ [ register this device ] │ └─────────────────────────────────────────────────────────┘ ACTIVE · 2 ───────────────────────────────────────────────────────────── … ``` ### Decisions - **Full fingerprint shown** (no truncation): verifiability against `.relicario/devices.json` is the whole point of displaying it. Popup-width wrapping handled by `.fingerprint { word-break: break-all }` — wraps to two lines under ~360px. - **`by Y` semantics**: `DeviceEntry.added_by` — the name of the device that signed the registration commit. Already in the data model, just unsurfaced today. - **Inline two-step revoke** keeps the lightness of the rest of the extension's UX; modal would feel disproportionate. The current device gets no revoke button (the CLI keeps the "revoke self" escape hatch since that needs different post-revoke handling). - **Revoked section** collapsed by default with count; expanded entries get the same three-line rhythm minus the revoke button, plus `revoked X ago · by Y`. - **Unregistered banner**: fleshed-out copy explaining what registration *does* (current one-liner feels mysterious per memory of past confusion). Same flow underneath — click → modal with device-name input → `register_this_device` SW message. --- ## C. Trash pane ``` ◀ trash 3 items · oldest purges in 22 days ───────────────────────────────────────────────────────────── 🔑 GitHub login ⤺ trashed 8 days ago · purges in 22 days 📝 Recovery note ⤺ trashed 12 days ago · purges in 18 days 🔑 old-aws-root ⤺ trashed 18 days ago · purges in 12 days ───────────────────────────────────────────────────────────── [ empty trash ] ``` ### Decisions - **Two-line per-entry**: type icon + name + `⤺` restore glyph; muted second line `trashed X ago · purges in Y days`. - **Per-item purge countdown** computed client-side from `trashed_at + retention_seconds - now` and formatted via `shared/relative-time.ts`. Updates on pane render (no live timer — sub-day precision unnecessary). - **Header summary stays** ("3 items · oldest purges in 22 days") — useful at-a-glance. - **Destructive empty button anchored bottom-right** — separated from per-row restore buttons to reduce mis-clicks. Confirmation flow unchanged from today. - **Sort**: trashed-date descending (newest first). Defer "sort by purge urgency" toggle — not strongly requested and adds toolbar real estate. - **Type icons** stay as today (emoji per item type) — the global glyph treatment is for *action* buttons; type icons are content-classification and read better as the existing emoji set. --- ## D. History — index pane (NEW) Reachable from a new `◷ history` bottom-nav slot. Sorted by most-recent-change descending. ``` ◀ history 5 items have field history ───────────────────────────────────────────────────────────── 🔑 GitHub login 3 changes · last 2 days ago 🔑 AWS prod 1 change · last 2 weeks ago 📝 Recovery note 2 changes · last 3 weeks ago 🔑 Cloudflare 1 change · last 1 month ago 🌐 personal-email 4 changes · last 2 months ago ``` ### Decisions - **Implementation**: iterate manifest entries, fetch + decrypt each item (already cached in session state where decrypted), inspect `field_history` map; emit entries that have ≥1 history-tracked field with non-empty history. Count = sum of `field_history[*].length`. Last-changed = max `replaced_at` across all history entries. - **Click row** → routes to `#history/` (per-item view below). - **Empty state** when no items have history: *"No field history yet. Edits to passwords, TOTP secrets, and concealed fields will appear here."* - **No excerpts** in the index — keeping it lean; the per-item view is one click away. --- ## E. History — per-item view (existing, polished) Existing component (`field-history.ts`, filename kept). Visual polish only — apply the section-header rule, glyph buttons, focus ring, accent tokens. **No structural changes to content layout.** ``` ◀ history · GitHub login PASSWORD · 3 entries ───────────────────────────────────────────────────────────── current ●●●●●●●●●●●●●●●●●●●●●●●● ⊙ ⧉ set 2 days ago ─── previous ●●●●●●●●●●●●●●●●●●● ⊙ ⧉ 3 weeks ago ─── previous ●●●●●●●●●●●●●●●● ⊙ ⧉ 2 months ago TOTP_SECRET · 1 entry ───────────────────────────────────────────────────────────── current ●●●●●●●●●●●●●●●●●●●●●●●● ⊙ ⧉ set 1 month ago ─── previous ●●●●●●●●●●●●●●●●●●● ⊙ ⧉ (created · 3 months ago) ``` ### Decisions - **Filename kept** as `field-history.ts` per user feedback during brainstorm. - **Routing**: continues to be reachable from item-detail "view history" button (`#history/`). Now also reachable by drilling into the history index pane. - **Reveal/copy glyphs** updated to `⊙ ⧉` constants from `shared/glyphs.ts`. - **Per-field uppercase header** + 1px rule applied — matches the new visual rhythm. - **Values stay concealed by default** (existing behavior). Reveal toggle and copy button per entry. Values held in component-local `valueStore` map (not DOM attributes) — existing security pattern preserved. --- ## Routing & sidebar nav changes ### `vault.ts` `VaultView` union changes ```ts type VaultView = | 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings' | 'settings-vault' | 'backup' | 'import' | 'history' // NEW — covers both index and per-item, payload distinguishes ``` The existing `'field-history'` view value is **removed** from the union — the hash-parse layer normalizes any `#field-history/` URL to `#history/` before view resolution, so consumers only ever see the new value. The per-item component (`field-history.ts`) is unchanged in identity; only its dispatch key changes. Hash routes: - `#history` → index pane (`item-history-index.ts`) - `#history/` → per-item view (`field-history.ts`) - `#field-history/` → 301-style redirect to `#history/` (one release of backward compat for any bookmarked URLs) ### Sidebar bottom-nav ``` ▦ trash · ⌬ devices · ⚙ settings · ◷ history · ⏻ lock ``` Five glyph buttons in a row at ~240px sidebar width: comfortable at 1ch each + padding. Verified. --- ## Testing UI work — verification is mostly manual smoke: - Each pane loads, renders, round-trips edits - Settings round-trip: change retention/session/attachments → reload → values persist; session-timeout actually fires lock after configured minutes - Devices: fingerprint string matches `relicario device list` CLI output; revoke happy path + cancel; unregistered banner → register flow → confirm - Trash: per-item purge countdown updates correctly; empty trash → confirms then purges - History: index sorted by recency; click drills in; empty state when no history; both `#history/` and the item-detail entry point reach the same view - Cross-context: each shared component renders correctly in both popup (~360px) and vault tab (full) **Unit tests** — only where logic warrants: - `shared/relative-time.ts` — table-driven test of fixture timestamps → strings - Purge-countdown formatting No new test infrastructure. Extension currently has no snapshot tests per inventory; not adding any here. --- ## Rollout - Single PR, v0.5.x train. - No data-model migration, no schema change, no core or wasm changes. - Purely `extension/src/`: one new shared util, one new pane file, four modified components, CSS additions, two routing additions. - Doc updates: `STATUS.md` move-to-recent on land; `extension/ARCHITECTURE.md` note the new `◷ history` route + 4th sidebar slot. --- ## Risks | Risk | Mitigation | |---|---| | Bottom-nav crowding (4 + lock = 5 items at ~240px sidebar) | Glyphs are 1ch each; ample room verified, but confirm at narrowest viewport during smoke | | Fingerprint length in popup context (~330px monospace) | CSS `word-break: break-all` on `.fingerprint`; no truncation | | `shared/glyphs.ts` may not exist yet | Spec creates it if absent (called out in §1) — depends-on-or-creates | | Decrypting all items for the history index pane | Most items are already cached after a session warm-up; cost is per-pane-load not per-event; acceptable for family-vault item counts | | Inline revoke confirmation could be missed (no modal blocker) | Two-step pattern matches other extension confirmations (delete item, empty trash); copy is explicit about consequence | --- ## Out of scope (deferred to future rounds) - Phase 3 shell rearchitecture (three-pane layout, command palette, drag-resize panes) - Phase 4 command palette - Item-level snapshot history (option B/C) - Settings-as-hub with sub-tabs - Trash multi-select / bulk-restore / hover preview - Devices rotate-key flow / "last seen" detail - History diff view between adjacent values - Whole-revamp animations or transitions