Brainstormed design covering UX revamp of all four in-vault admin panes (Settings, Devices, Trash, History) to match the fullscreen visual language. Closes functional gaps along the way: per-device session-timeout UI, revoke button surfacing, SHA256 fingerprint + added-by display, per-item purge countdown, and a new history index pane. Item history uses option A (aggregate existing field_history per item) — no new core storage, no schema change. Ships in v0.5.x inside the current vault.ts shell; Phase 3 shell rearchitecture and Phase 4 command palette deferred to their own rounds. Roadmap entry reconciled to point at the spec. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.localdirectly. - Devices: the
revoke_deviceSW 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-sidedevices.json) or theadded_byfield that's already inDeviceEntry. - 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
◷ historyslot 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_historyper 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/<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) |
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):
.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/· localmuted 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_configSW 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.jsonis the whole point of displaying it. Popup-width wrapping handled by.fingerprint { word-break: break-all }— wraps to two lines under ~360px. by Ysemantics: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_deviceSW 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 linetrashed X ago · purges in Y days. - Per-item purge countdown computed client-side from
trashed_at + retention_seconds - nowand formatted viashared/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_historymap; emit entries that have ≥1 history-tracked field with non-empty history. Count = sum offield_history[*].length. Last-changed = maxreplaced_atacross 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.tsper 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 fromshared/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
valueStoremap (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'
| '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/<id> URL to #history/<id> 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/<itemId>→ per-item view (field-history.ts)#field-history/<itemId>→ 301-style redirect to#history/<itemId>(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 listCLI 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.mdmove-to-recent on land;extension/ARCHITECTURE.mdnote the new◷ historyroute + 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