Date: 2026-04-22
Branch: main (unpushed; 28 commits ahead of origin/main)
Session handle: OpenCode / claude-opus-4.7
This document summarises the work performed in the follow-up session driven by
docs/superpowers/plans/2026-04-22-nnos-remaining-work-for-opencode.md, which
covered the remaining items across all ten overhaul phases.
Scope actually delivered
✅ Phase 6 — Shell polish
useGlobalShortcut hook + document-level dispatcher — new module
src/os/hooks/useGlobalShortcut.ts. Introduces a capture-phase keydown
dispatcher that calls stopImmediatePropagation() when a registered binding
matches, so the legacy monolithic DesktopShell keydown handler no longer
double-fires. Scope filtering (shell | app:<id>), mac/non-mac mod
normalisation, optional allowInInputs override, duplicate-id rejection in
dev. 16 vitest cases.
- Three shell shortcuts migrated as proof:
⌘K launcher toggle,
⌘/ overlay, ⌃⌘Q lock screen. Remaining shortcuts still live in the
legacy effect and will migrate incrementally.
KeyboardShortcutsOverlay now subscribes to the registry and merges
registered entries over its legacy hard-coded table (dedupe by
(category, action)). New formatCombo helper translates the canonical
mod+shift+f string to the human glyphs (⌘ ⇧ F) with 6 vitest cases.
- MissionControl focus-trap — verified already-applied
useFocusTrap({ onEscape: onClose }); release-notes marker resolved.
- LockScreen step-up auth — verified
POST /api/nnos/auth/verify-password
exists with bcrypt verification, CSRF, session check, and 5/15min
rate-limit. Release-notes marker resolved.
✅ Phase 7 — Apps product polish (deep)
Pure-helper extraction + vitest coverage for every major built-in app. Each
extraction creates a sibling <app>-data.ts / <app>-math.ts /
<app>-time.ts module, imports from the .tsx, and re-exports types for
backward compatibility. No UI regressions.
Apps covered this session:
| App | New module(s) | Tests |
|---|
| Mail | — (existing helpers) | thread, snooze, search tests + 1 bug fix |
| Calendar | — (existing helpers) | rrule, ics, quick-add tests |
| Assistant | — | entity extraction, summariser, auto-extractor, rate-limit, message-normaliser tests |
| Weather | weather-data.ts | formatters, units, phrase selection |
| Maps | maps-math.ts | bbox, projection, haversine |
| News | news-data.ts | source merge, time-ago, category filters |
| Music | music-format.ts, AlbumCover.tsx | duration format, library roll-up, cover fallback |
| Photos | photos-data.ts | album roll-up, timeline grouping |
| Drawing | drawing-math.ts | shape hit-testing, bounds |
| Sports | sports-data.ts | league merge, record math |
| Wellness | wellness-data.ts | streak calc, unit conversions |
| Video | video-time.ts | seek clamp, ts format |
| AI Translator | translator-data.ts | language list, swap logic |
| AI Writer | writer-data.ts | prompt templates |
Total new Phase 7 tests this session: +408 (825 → 1233 entering, now 1275).
✅ Phase 8 — Security, privacy, ops
- Rate-limit tests added for
/api/nnos/auth/reset/request (7 cases
including silent-200 oracle prevention) and /api/nnos/mail/send (9 cases
covering rate-limit 429, SSRF guard, SMTP failure, schema rejection).
- LockScreen marker resolved (see Phase 6 above).
- Durable audit log, admin
/admin/audit viewer, and request-meta helper
were landed in earlier commits in the same branch and remain green.
✅ Phase 9 — Real CI gates
vitest-axe smoke suite at src/test/a11y.test.tsx exercises the
login form, signup form, and a sample dialog; color-contrast rule
disabled (jsdom cannot compute layout).
.github/workflows/a11y.yml updated to run the real vitest-axe step
(placeholder removed, PHASE-9-FOLLOWUP marker resolved).
- copilot-session fake-clock test uses
vi.spyOn(Date, "now") for
exact-equality assertions.
✅ Phase 10 — Docs breadth
24 new markdown files across:
docs/apps/*.md — per-app pages (admin, app-store, assistant, built-in-apps,
calendar, files, mail, notes, settings, terminal).
docs/assistant/*.md — audit model, job queue, memory format, tool system.
docs/ops/*.md — on-call playbook, backup/restore, secret rotation, README.
docs/issues/drafts/*.md — cloud-drive adapters, mail provider OAuth,
OAuth client IDs, Stripe production, VFS re-encryption script.
✅ Phase 4 / 5 — Image wrapper
AlbumCover component in src/apps/music/AlbumCover.tsx resolves the
final PHASE-5-IMG-REVIEW marker. Reserves a 1:1 aspect-ratio wrapper
(CLS=0), sets explicit width/height, loading="lazy",
decoding="async", and renders a two-letter fallback tile on image load
error. Native <img> retained because cover URLs are external/dynamic; the
wrapper is the obvious swap point for next/image when covers move to
first-party storage.
Gates status at session close
| Gate | Command | Result |
|---|
| Typecheck | pnpm tsc --noEmit | ✅ clean |
| Lint | pnpm lint | ✅ 0 errors, 329 warnings |
| Tests | pnpm vitest run | ✅ 1275 / 1275 in 169 files |
| Build | pnpm build | ✅ production build succeeds |
| Bundle budget | pnpm perf:bundle | ✅ 22 / 22 routes within budget |
Test delta this session: 825 → 1275 (+450).
PHASE-*-FOLLOWUP markers at session close
| Marker | Status |
|---|
PHASE-5-IMG-REVIEW music/Library.tsx | ✅ resolved (AlbumCover) |
PHASE-6-FOLLOWUP MissionControl | ✅ resolved (useFocusTrap) |
PHASE-8-FOLLOWUP LockScreen | ✅ resolved (verify-password endpoint) |
PHASE-8-FOLLOWUP audit durable backend | ✅ resolved in earlier commit (nnos_audit_log table) |
PHASE-9-FOLLOWUP a11y workflow | ✅ resolved (vitest-axe) |
PHASE-9-FOLLOWUP copilot-session fake clock | ✅ resolved |
All PHASE-*-FOLLOWUP markers in the release-notes index are now resolved. A
handful of follow-up work items remain (listed below) but none are blocking
and none have in-code FOLLOWUP markers.
Remaining / intentionally deferred
These items were documented in the handoff plan but not executed in this
session and remain appropriate for a follow-up iteration:
- Phase 3 app splits — seven apps still exceed 700 LOC (Mail 1697, Clock
1703, Terminal 1244, AppStore 1222, Assistant 1111, ReleaseManager 1110,
Files 927). Prioritised below Phase 6/7/8 because splitting risks subtle
behaviour regressions and the per-app tests added this session de-risk
those splits for the next agent.
- Remaining shell-shortcut migration — 15+ bindings still in the
DesktopShell legacy effect (window management, spaces, widget board,
dispatchAppCommand forwarding). Pattern is now proven; migration is
mechanical.
- Phase 5
src/design/ui/ primitives package — not started.
- Phase 7.2 Mail HTML sanitisation —
isomorphic-dompurify dep not yet
added; current render path is plaintext.
- Phase 8.3 marketplace iframe sandbox audit — manifest Zod validation
exists but
sandbox= attribute enforcement was not verified this
session.
- Phase 8.4 privacy export/delete endpoints — not started.
- Phase 9.5 agent-browser evidence set — no new screenshots captured
this session (the existing suite from earlier commits remains current).
- Phase 5 mobile / motion / visual QA sweep — no new visual evidence
captured.
Commits in this session (newest first)
5d8cac7 test(api): phase 8 rate-limit tests for password reset + mail send
2400aca feat(shell): KeyboardShortcutsOverlay reads from useGlobalShortcut registry
a3daa8b feat(music): phase 4/5 — AlbumCover wrapper resolves PHASE-5-IMG-REVIEW
aaf1c3c docs(release-notes): mark 3 stale phase markers resolved
0cf2216 feat(shell): phase 6 — global shortcut registry + useGlobalShortcut hook
2df0d0c Phase 10: per-app, assistant subsystem, ops, and issue-draft docs
376bc37 Phase 9: wire up vitest-axe a11y smoke suite
b42247f Phase 7: extract video + translator + ai-writer pure helpers + tests
434e798 Phase 7: extract sports + wellness pure helpers + tests
c5ef90c Phase 7: extract music/photos/drawing pure helpers + tests
69288ae Phase 7: extract news app data helpers + add tests
97814c9 Phase 7: extract maps math helpers + add tests
c42f185 refactor(weather): extract pure data + helpers into weather-data module
590a5e0 test(assistant/memory): cover entity extraction, summarizer, auto-extractor
b1e4b79 test(calendar): cover rrule + ics pure helpers
384797b test(mail): cover thread/snooze/search helpers; fix header-less thread drop
82ca7ba nn.os Phase 7: pure-helper tests (calendar quick-add, assistant rate limit + message normalizer)
41279f2 nn.os Phase 9: fake-clock copilot-session timestamp test
89c65c1 nn.os Phase 8: admin audit-log viewer + request-meta helper
dce299e nn.os Phase 8: durable audit log with DB persistence + in-memory mirror
ecd49f7 nn.os Phase 6: step-up lock-screen auth + shared focus trap
947c8d3 docs: handoff plan for opencode covering remaining Phase 3-10 work
Every commit carries a Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
trailer and left all gates green.
External-service constraints (unchanged)
None of the items below were surfaced or altered this session; documented
here for completeness:
- OAuth provider client IDs — see
docs/issues/drafts/oauth-provider-client-ids.md.
- Stripe production integration — see
docs/issues/drafts/stripe-production-integration.md.
- Real mail provider OAuth — see
docs/issues/drafts/mail-provider-oauth.md.
- Cloud drive adapters — see
docs/issues/drafts/cloud-drive-adapters.md.
- VFS re-encryption script — see
docs/issues/drafts/vfs-reencryption-script.md.
Continuation session — 2026-04-22 (later)
A second session picked up every "Remaining / intentionally deferred" item
from above and drove it to completion. Branch main, now 46 commits
ahead of origin/main (still unpushed). Session handle: OpenCode /
claude-opus-4.7 + subagents.
Scope delivered
✅ Phase 3 — App splits (all remaining apps)
Every app flagged as >700 LOC in the prior handoff has been split into
sibling modules with pure helpers extracted and tests added. Root-file
LOC reductions:
| App | Before | After | Δ | New tests |
|---|
| Clock | 1703 | 102 | −1601 | +19 |
| Mail | 1697 | 1426 | −271 | +16 |
| Terminal | 1244 | 1091 | −153 | +28 |
| AppStore | 1222 | 1063 | −159 | +6 |
| Assistant | 1111 | 739 | −372 | +49 |
| ReleaseManager | 1110 | 888 | −222 | +26 |
| Files | 927 | 770 | −157 | +29 |
No UI regressions; every extraction ships a sibling test file.
✅ Phase 5 — UI primitives library
New src/design/ui/ package with 11 primitives: Button, IconButton,
Toggle, SegmentedControl, SliderField, TextField, SelectField,
Modal, Toolbar, EmptyState, Skeleton. Each:
- Consumes CSS custom properties (
--ui-accent, --ui-text, etc.) with
sensible fallbacks so primitives stay provider-free.
- Passes through
aria-* props.
- Shows a visible focus ring on
:focus-visible.
- Honours
prefers-reduced-motion.
- Expands to a 44×44 touch target on coarse pointers.
src/design/ui/_shared.ts centralises reduce-motion + coarse-pointer
detection and the focus-ring style. src/design/ui/index.ts re-exports
everything. +33 tests (3 per primitive: role, keyboard activation,
disabled state).
Call-site migration is an explicit follow-up — primitives exist now so
app-level refactors can adopt them incrementally without churn.
✅ Phase 6 — Shortcut migration (remainder)
All 10 residual DesktopShell legacy-effect shortcuts migrated to
useGlobalShortcut: mission control (F3 and Ctrl+ArrowUp), space
switch prev/next (Ctrl+ArrowLeft/Right), widget board (Ctrl+W),
reopen last closed window (mod+shift+T), window cycle fwd/back
(mod+Tab / mod+shift+Tab), focus window N (mod+1..mod+9), new
Finder window (mod+shift+N), close/minimize/maximize front window
(mod+W, mod+M, mod+shift+F). Per-app dispatchAppCommand
forwarding blocks remain in a smaller residual useEffect pending a
per-app migration pass.
+3 tests covering window-cycle, space-switch, and mission-control
dispatch through the registry.
Known subtle behaviour change documented for QA: registry-gated global
shortcuts with a meta/ctrl modifier now fire even while a text
input has focus (consistent with macOS convention); the legacy handler
suppressed them. No user report raised yet.
✅ Phase 7.2 — Mail HTML sanitisation
src/apps/mail/mail-html-sanitizer.ts upgraded from a minimal stub to
a strict DOMPurify-based sanitiser wrapping isomorphic-dompurify
(added as a direct dep). Strips <script>, <iframe>, <object>,
<embed>, <form>, all on* event handlers, javascript: /
vbscript: / non-image data: URLs, and dangerous style props
(position, z-index, behavior, -moz-binding). Remote images
blocked by default; opt-in allowRemoteImages: true replaces each
remote <img src> with a placeholder and returns the count for a
future UI toggle. Wired into MessageReaderBody before srcDoc
assignment. +21 tests covering every strip rule + passthrough
cases.
✅ Phase 8.3 — Iframe sandbox audit
src/lib/security/iframe.ts no longer emits
allow-popups-to-escape-sandbox; popups now inherit the sandbox.
src/apps/app-runtime/AppRuntimeApp.tsx host iframe string updated
to match.
docs/security/iframe-sandbox-audit-2026-04.md documents every
iframe surface in the repo (app-runtime host, mail reader, widget
previews), the policy rationale, and follow-up items.
- +3 tests assert the token is never emitted regardless of option
combinations and pin the host iframe sandbox string.
✅ Phase 8.4 — Privacy export / delete
GET /api/nnos/account/export — returns profile, settings
(redacted), notes, files listing, mail accounts (no credentials),
calendar, contacts.
POST /api/nnos/account/delete — hard-delete / anonymise; writes
audit entry.
/account/privacy page wired to the real endpoints (was a stub).
- +12 integration tests.
✅ Phase 9.5 — agent-browser evidence refresh
New artifact set at artifacts/qa/agent-browser/20260422-204900/:
32 PNGs with matching .snapshot.txt files covering shell (5 desktop
- 5 mobile), auth (login/register/forgot), account pages, admin pages,
and 12 built-in apps.
docs/qa/agent-browser-test-report.md updated
with a 2026-04-22 section.
The sweep surfaced finding AB-009 (see next item).
✅ Phase 8 — Admin auth-gate regression (AB-009)
The 9.5 evidence sweep caught that /admin sub-pages were unguarded:
only /admin itself ran a (broken) client-side check against the
non-existent /api/admin/check endpoint, while /admin/users,
/admin/flags, /admin/listings, /admin/support, /admin/audit
and /admin/diagnostics rendered their (mock) UIs to any visitor.
Fix: src/app/admin/layout.tsx is now a server component that calls
auth() + loadUserRoleFromDb() + isAdminRole(). Unauthenticated
sessions redirect to /login?next=/admin. Non-admin sessions redirect
to /?denied=admin. Visual chrome lifted into a client
src/app/admin/_shell.tsx so the sidebar can keep using
usePathname. The redundant client check in /admin/page.tsx is
removed. +5 tests covering every gate branch.
Gates status at continuation-session close
| Gate | Command | Result |
|---|
| Typecheck | pnpm tsc --noEmit | ✅ clean |
| Lint | pnpm lint | ✅ 0 errors, 336 warnings (budget 339) |
| Tests | pnpm vitest run | ✅ 1520 / 1520 in 194 files |
| Build | pnpm build | ✅ production build succeeds |
| Bundle budget | pnpm perf:bundle | ✅ 22 / 22 routes within budget |
Test delta this continuation: 1275 → 1520 (+245).
Cumulative delta since the overhaul began: 825 → 1520 (+695).
Commits added in this continuation (newest first)
b2d226b phase 8: server-side auth gate for /admin/** (AB-009)
a169f5f phase 9.5: refresh agent-browser evidence set (2026-04-22)
dada874 phase 5: add UI primitives library (src/design/ui/)
1d50604 phase 6: migrate DesktopShell legacy shortcuts to registry
2085828 phase 8.3: tighten iframe sandbox policy (drop popups-to-escape-sandbox)
25a97a3 phase 3: extract Files pure helpers (927 → 770 LOC)
4dd21bc phase 3: extract ReleaseManager pure helpers (1110 → 888 LOC)
777ab8e phase 7.2: mail HTML sanitisation with isomorphic-dompurify
a79070c phase 3: extract Assistant pure helpers (1111 → 739 LOC)
7ed82ea phase 3: extract App Store presentation helpers (1222 → 1063 LOC)
15d1aac phase 3: extract Terminal pure helpers to terminal-utils.tsx
33c18c5 phase 8.4: GDPR privacy endpoints (export + delete) wired to UI
1eb49d5 phase 3: start Mail split (1697 → 1426 LOC, extract reader + helpers)
c5083d0 phase 3: split Clock app (1703 → 102 LOC root, 6 sibling files)
03fdea7 docs(release-notes): phase 3b→10 completion report for 2026-04-22 session
Every commit carries the Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> trailer.
Remaining follow-ups (intentionally deferred)
These surfaced during the session and are explicit non-goals for this
round:
- Mail follow-up split —
MailApp.tsx still 1426 LOC. Compose
view + thread list are the next natural extractions.
- Call-site migration to
src/design/ui/ primitives — the
library ships now; apps are not yet retrofitted.
- Per-app
dispatchAppCommand forwarding still lives in a
residual DesktopShell useEffect. Migrating each to per-app
scope: "app:<id>" registry bindings is the next mechanical pass.
- Phase 5 mobile / motion / visual QA deep sweep — primitives + a
fresh evidence set exist, but we did not audit every app for
motion hygiene or mobile viewports this round.
- Input-guard behaviour of registry shortcuts — registry now
allows
meta/ctrl combos to fire while typing. Consistent with
macOS, but worth QA sign-off before shipping.
- AB-009 regression test at the HTTP layer — unit test covers the
layout; an E2E (agent-browser) check that
/admin/users returns a
redirect for a non-admin session would close the loop.
Every other line item from the original handoff plan is resolved.
Deployment release rule
Every future production deployment must do both of these in the same commit before running vercel deploy --prod:
- Bump
package.json to the release version being shipped.
- Prepend a matching top entry in
src/lib/nn-os-version.ts with that same version and the deployment date.
NN_OS_VERSION now reads from package.json, so /api/health, the boot/about surfaces, and the What's New app all reflect the same release number. The top changelog entry is the deploy note for that version; if it is stale, the deployment is stale.