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>
467 lines
24 KiB
Markdown
467 lines
24 KiB
Markdown
# 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/<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):
|
|
|
|
```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/<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
|
|
|
|
```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/<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 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
|