# 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):

```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'
  | '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
