Files
relicario/docs/superpowers/specs/2026-05-23-vault-tab-management-surfaces-revamp-design.md
adlee-was-taken 30816c2fe3 docs: implementation plan for vault-tab management surfaces revamp
12 tasks covering settings/devices/trash/history pane revamps, plus
groundwork (glyph constants, relative-time util, ssh-fingerprint util,
shared CSS classes) and routing/nav wiring. Tasks are TDD where the
work is testable (utils) and bite-sized manual-smoke where it's UI.

Spec corrections folded in:
- Devices revoke is upgrade (text+confirm → glyph+inline), not greenfield
- Fingerprint via webcrypto in extension, no SW shape change, no WASM
- Routing keeps 'field-history' as internal dispatch key; only user-facing
  hash normalizes #field-history → #history for backward compat

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:01:01 -04:00

24 KiB

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: revocation works via a plain text "revoke" button + browser confirm() dialog — functional but inconsistent with the rest of the extension's UX. 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, new shared/ssh-fingerprint.ts modify + 1 new util
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/<itemId> 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)

No SW shape changes. Fingerprint is computed client-side in devices.ts via crypto.subtle.digest('SHA-256', …) against the base64-decoded ed25519 key blob from DeviceEntry.public_key. Result is formatted as SHA256:<base64-no-pad> to match the SSH convention (and what relicario device list prints from core::device::fingerprint()). Pure extension change — no message round-trip, no WASM export, no Rust change.

Shared CSS utility classes

Defined in vault.css and popup.css (shared because components render in both contexts):

.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/<itemId> (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/<itemId>). 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

type VaultView =
  | 'list' | 'detail' | 'add' | 'edit'
  | 'trash' | 'devices' | 'settings' | 'settings-vault'
  | 'field-history'        // existing — per-item view (internal dispatch key kept)
  | 'history'              // NEW — index pane only
  | 'backup' | 'import'

The user-facing hash changes (#history is the new entry point, #history/<id> is the per-item view), but the internal dispatch keeps 'field-history' for the per-item view to minimize the diff to working code. Normalization happens in parseHash:

  • #history{ view: 'history' } → index pane (item-history-index.ts)
  • #history/<itemId>{ view: 'field-history', id: <itemId> } → per-item view (field-history.ts)
  • #field-history/<itemId> → rewritten to #history/<itemId> in the address bar, then resolved as above (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/<id> 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