Security settings — redesign

/settings/security
Mode

★ Design decisions

Two IA calls the team asked the design experts to make, not punt, plus one encoding pattern this surface promotes as a candidate DESIGN.md rule. All reasoned from L1 industry convention (Google Account, GitHub, Linear, Stripe) + perceptual-UX.

Confirmed 1 · Page H1 + scope → "Security", the one security home

Rename the H1 to "Security" (matching the nav label) and pull "how you sign in" — password, passkeys, 2FA — onto this page from /settings/account. Account keeps only profile: name, email, avatar, linked OAuth identity. Why: the universal mental model co-locates sign-in methods and active sessions under one "Security" tab — Google's Security tab does it; GitHub's "Password and authentication" does it. A user asking "is my account safe?" should land in one place that answers both "how do I get in?" and "who's currently in?". "Active Credentials" answered only the second question and forced a context-switch to Account for the first — the more dangerous half. The cost is a one-time component move (PasswordRow, PasskeyManagement, 2FA from Account → Security); the payoff is removing a split that no peer product tolerates.

Confirmed 2 · Cast + Share-links → compact status chips, not full sections

Demote both to a single "Active under your identity" chip strip that links out. Why: credentials answer "what can sign in as me?" Casts and share-links answer a different question — "what's running or shared under my identity?" — and rendering them as full sections dilutes the page's one job and inflates scroll. As chips they stay visible (status at the edge) without competing. The one carve-out: a live cast running as you is a security action to stop, so the cast chip keeps a one-tap Stop affordance inline; share-links, being passive, get only Manage →. This mirrors how Stripe surfaces "active" secondary objects as status rows that link to their own management surface rather than expanding them in the credentials view.

DESIGN.md candidate 3 · Credential rows encode two times — provenance + recency

Every credential/connector row surfaces two timestamps: added (provenance, muted, second line) and last used (recency, emphasized, first line). Why: recency-of-use, not age, is the revoke-safety signal. Showing both lets a user tell "old but actively used → do not revoke" from "old and unused → safe to cull" at a glance — age alone conflates the two and a single relative time hides whichever one you didn't pick. Recently-used rows (< 7 days) carry a sage active-pip (color + shape, not hue alone) and a faint left-edge accent so a live credential reads as load-bearing; their Revoke surfaces a one-beat friction caption ("Used Nh ago — revoke anyway?") that the product backs with a confirm step. Stale rows recede (cool/muted used-time, plain Revoke) so the safe-to-cull set is the visually quiet set. Candidate rule for DESIGN.md: "Credential rows encode two times — provenance (added, muted) + recency (last used, emphasized); recency is the revoke-safety signal."

★ B. Attention state — the hero

/settings/security page.tsx

The full page with the Attention Tray populated. Tray is the only place anxiety is allowed to live — and every row offers exactly one next move. Below it, the page returns to calm.

www.sageox.ai/settings/security

Security

How you sign in, and everything that can act as you. Review anything that looks unfamiliar.

12credentials 5types 7-day sign-ins newest 2h ago · this device
Needs your attention 4 Dismiss all reviewed
New CLI sign-in from Portland, OR — a location you haven't used before
2 days ago · IP 203.0.113.47 · ox CLI v2.4.1 on macOS
Personal access token ci-deploy expires in 3 days
oxp_3kf…9waq · added Mar 12 · last used 4h ago
Two-factor authentication is not enabled
Recommended for accounts with active CLI tokens
3 credentials unused for 90+ days
2 browser sessions · 1 MCP grant — likely safe to revoke

How you sign in

Methods that authenticate you moved from /settings/account

Active credentials

12 total · grouped by type
Chrome on macOS This device
Portland, OR · IP 198.51.100.9 · active now
Browser31
Safari on iPhone
203.0.113.22 · Portland, OR · iOS 18.4
used 3h ago first seen Apr 02
Firefox on Windows
203.0.113.88 · Seattle, WA · Windows 11
used 94 days ago first seen Nov 18
Chrome on Linux
203.0.113.140 · Portland, OR · Ubuntu
used 120 days ago first seen Oct 09
CLI42 + New token
ci-deploy oxp_3kf…9waq
PAT · expires in 3 days
used 4h ago added Mar 12
macbook-laptop
device session · 198.51.100.9 · macOS 15.3
used 2h ago added Feb 20
work-laptop oxp_aa1…77zz
PAT · no expiry
used 6d ago added Jan 04
legacy-gitlab glpat (legacy)
GitLab PAT · migrate to oxp_
used 210 days ago added 2025
MCP23 Connect client
Claude Desktop
grant · read + write
used 1d ago added Apr 30
MCPJam Inspector
grant · read only
used 98 days ago added Feb 11
App24
SageOx iOS
iPhone 15 · v3.2.0 · push on
used 5h ago installed Dec 14
SageOx Android
Pixel 8 · v3.1.4 · push off
used 3d ago installed Jan 22
Devices15
Scribe — Conf Room A
hardware · firmware 1.8.2 · Ox Scribe
used 12h ago paired Mar 28
Active under your identity Sprint review cast · live 23m 3 share links 2 active · 1 expiring Manage →

Posture band = "status at the edge" (§11 Rule 9). One line, surface-2, divider-separated cells. The 7-day sparkline is decorative-but-honest — last bar is the current spike (today's sign-ins). No hero card; anxiety would be manufactured. Counts use font-variant-numeric: tabular-nums so they don't jitter on re-render.

Attention Tray severity is color + SHAPE (§11 Rule 3). danger = octagon + red square chip; warn = triangle + amber square chip; info/review = filled circle + sage round chip. A colorblind user reads the glyph and chip-radius, not the hue. Each row has exactly one primary next-move (Copper only on Rotate, since rotation is the affirmative create-like action); everything else is ghost/danger-ghost. Tray is display:none when empty — see State A.

Rows encode two times — provenance + recency (decision 3, DESIGN.md candidate). The right cell carries used … (recency, --text, tabular, the load-bearing revoke-safety signal) on the first line and added/first-seen/paired/installed … (provenance, --text-subtle, smaller) on the second. The duplicated "created …" fragment was removed from .secondary — provenance now lives in the usage cell, not buried in hover-only metadata. Why two times: age conflates "old but live" with "old and idle"; recency-of-use disambiguates them at a glance, so a user can cull confidently without opening each row. On .active rows (used < 7 days) the date provenance label adapts to the credential type — browser sessions first seen, MCP grants added, devices paired, app sessions installed, CLI PATs added.

Active-row cue is color + SHAPE (Rule 3), and is doubly redundant. Recently-used rows get a sage .pip (round, filled — not hue-only) before the used time and a faint sage left-edge accent on the row. Stale rows get neither and their used time reads cool/muted (--text-subtle, normal weight) so the safe-to-cull set is the visually quiet set — the eye is pulled toward what's live, away from what's idle.

Anti-accidental-revoke guard. On .active rows the Revoke button carries a data-guard caption ("Used Nh ago — revoke anyway?") that surfaces on hover/focus as one beat of friction. This mockup shows only the cue; the real product gates revoke of a recently-used credential behind a confirm step (a single dialog: "name was used Nh ago. Revoke it?"). Stale rows keep the plain Revoke with no guard — they read as safe, so we don't tax the common cull path. Devices say "unpair anyway?" to match the verb.

Calm mode. The .added provenance line is optional metadata, so under html[data-calm="true"] it dims to ~50% and reveals on hover/focus — same treatment as the .gkey chips and sign-in descriptions. The used recency line and the active-pip stay full-contrast: they're signal, not noise.

Copper budget on this viewport: exactly one — the Rotate CTA in the tray. The "How you sign in" Add-2FA and "Sign out everywhere else" are ghost; chips use neutral/danger-tint, never Copper. The guard caption is amber (warn), not Copper.

A. Clean state — tray hidden

/settings/security page.tsx

Everything healthy. The Attention Tray is removed from the DOM entirely (not collapsed) — calm by default; we never render an empty "0 issues, all good!" card. The posture band carries the all-clear at the edge.

www.sageox.ai/settings/security

Security

How you sign in, and everything that can act as you.

4credentials 2types all reviewed newest 2h ago · this device

How you sign in

Methods that authenticate you

Active credentials

4 total
Chrome on macOS This device
Portland, OR · active now
CLI21 + New token
ci-deploy oxp_3kf…9waq
PAT · expires Sep 2026
used 4h ago added Mar 12
macbook-laptop
device session · macOS 15.3
used 2h ago added Feb 20

Empty tray = no tray (Apple calm). When the signal list is empty the component returns null — we do not render a celebratory "you're all caught up" card. The posture band's all reviewed dot is the only acknowledgement, and it's at the edge. Manufacturing a green hero where nothing is wrong trains users to ignore the tray when something is wrong.

Both rows are active. Both CLI credentials were used within a day, so both carry the sage pip + left accent + guarded Revoke — the clean state has nothing safe-to-cull, which is exactly why no row recedes. The dual-timestamp cell still shows provenance (added …) underneath so the user can read age even when everything is live.

Identity strip also hidden when there are no active casts or share-links — same rule.

C. Self-healing error — one source failed

/settings/security ActiveCredentialsList.tsx

The MCP grant source timed out. Rule 11: the banner names the failed source, offers Retry, and still shows everything that loaded. Contrast with today's dead-end below.

Before — today's dead-end banner
Some credentials couldn't be loaded. Retry before taking security actions.
After — self-healing
Couldn't load MCP grants — request timed out. The other 4 sources loaded; you can still review and revoke them below. Last attempt 8s ago.
www.sageox.ai/settings/security

Security

How you sign in, and everything that can act as you.

9credentials 4types loaded 1 source pending this device
Couldn't load MCP grants — request timed out. The other 4 sources loaded; review and revoke them below. Last attempt 8s ago.

Active credentials

9 loaded · MCP pending
Chrome on macOS This device
Portland, OR · active now
CLI42 + New token
ci-deploy oxp_3kf…9waq
PAT · expires in 3 days
used 4h ago added Mar 12
macbook-laptop
device session · macOS 15.3
used 2h ago added Feb 20
MCP — pending
Couldn't reach the MCP grant service
Other credentials are unaffected · retry doesn't re-fetch what already loaded

Before → After (Rule 11, self-healing).

Beforegeneric "Some credentials couldn't be loaded. Retry before taking security actions" — no Retry button, names no source, and tells the user to stop. Pure dead-end. Afternames the failed source (MCP grants), keeps the 9 credentials that loaded fully actionable, and offers two retry affordances: a page-level banner button and a localized "Retry just MCP" in the group header that re-fetches only the failed source.

The failed group is not a hole. Its header still renders with a — pending count chip (amber, distinct from the round numeric chips) so the taxonomy stays stable and the user knows the type exists. A retry that succeeds slots the rows in without a layout jump. The error stub row carries no usage cell — there's no credential to time, so we don't fake one.

Loaded rows keep the dual-timestamp + guard. Even mid-failure, the 9 credentials that did load show provenance + recency and guard their active Revoke — degraded loading never strips the safety affordances off the data that is present.

Posture band degrades honestly: "4 types loaded · 1 source pending" instead of claiming a total it can't verify.

Anatomy notes

Component-level rationale. Each lives inline in the states above; collected here for implementors.

1 · Posture band. New reusable pattern candidate. Single-line, edge-placed status summary for any entity page. Cells divider-separated; tabular-nums; one optional Tufte micro-sparkline; right-aligned "you are here" anchor. Proposed token: nothing new — composes from surface-2 + existing text tiers.

2 · Attention Tray (DDR-004). Reuses the mandatory schema in packages/ui/src/AttentionTray/types.ts. Signal sources for this surface: token-expiry, unused-credential sweep, new-location sign-in, 2FA-absent. Severity rendered with the color+shape pair table above. One primary action per row; onDismiss per signal; whole tray unmounts when signals.length === 0.

3 · How you sign in. Pulled from /settings/account per decision 1. Components to move: PasswordRow, PasskeyManagement, the 2FA toggle out of DangerZone. Status pills use color+shape: filled = on/set, triangle = action-needed/off.

4 · Current-device anchor. Pinned above the groups, sage-tinted, 3px left rule + "This device" badge + monogram icon — three redundant signals so it reads as "you are here" even in calm/desaturated mode.

5 · Credential list density + dual timestamp (decision 3). Candidate new spec: row = icon + name + right-aligned usage cell (two values) + ghost Revoke. The usage cell stacks used … (recency, --text, tabular — the load-bearing revoke-safety variable) over added/first-seen/paired/installed … (provenance, --text-subtle). Recency < 7 days marks the row .active: sage pip (color + SHAPE, Rule 3) + faint sage left-edge accent + a data-guard caption on Revoke; the product backs that caption with a confirm step so a live credential gets one beat of friction before revoke. Stale (90d+) rows mute the name + read the used-time cool, drop the pip/accent, and keep a plain Revoke — they're the safe-to-cull set and should recede. Secondary metadata (IP/location/version/firmware/push/expiry) stays max-height:0; opacity:0 and expands on :hover/:focus-within (Rule 10); the duplicated created-date was removed from it since provenance now lives in the usage cell. Per-row Revoke is ghost-until-hover. Group header carries the create/manage affordance and a hover-reveal bulk control — this replaces the floating BulkRevokeRail. Keyboard digits 1–5 still jump to group (the .gkey mono chips); tabindex on rows enables J/K sequential review (Rule 12).

6 · Identity strip. Cast chip keeps inline Stop (remote-stop is a security action); share-links chip is Manage-only (passive). Both link to their own surfaces (/cast, /settings/sharing).

Owner files

  • apps/web/app/(protected)/settings/security/page.tsx — page shell, posture band, tray host
  • apps/web/src/components/settings/security/ActiveCredentialsList.tsx — groups + rows + dual-timestamp cell + revoke guard + self-healing banner
  • packages/ui/src/AttentionTray/ — DDR-004 tray (schema + shell)
  • Move from apps/web/app/(protected)/settings/account/: PasswordRow, PasskeyManagement, 2FA toggle (decision 1)
  • Retire: floating BulkRevokeRail (bulk control now co-located in group headers)