Skip to content

Surfc — Tech Debt Audit

Surfc — Tech Debt Audit

Date: 2026-04-24 Scope: React PWA in surfc/ only (marketing site surfc-web/ not audited) Context: SUR-220 migrates app-shell routing to react-router-dom. SUR-247 is the follow-up that replaces the useUI view-state machine with route-derived state. This audit is sequenced so the phased plan plugs into both.


Summary

Three structural problems account for most of the debt visible from App.jsx:

  1. App.jsx is a junction drawer. It hosts auth/encryption gates, the full Unlock screen (135 inline lines), media-query detection, capture-animation state, action-sheet management for both notes and ideas, responsive nav construction, and all 14 view switch-cases. At 762 lines it is 25% larger than CLAUDE.md claims and continues to absorb concerns.
  2. useUI view-state is not routing. The hand-rolled popstate bridge in src/hooks/useUI.js + the pathname workaround in src/router.js form two parallel navigation systems that coordinate through _sid tokens. SUR-220 is the correct reset; this audit flags what must land with it and what can wait for SUR-247.
  3. Two mega-hooks own most app behaviour. useKeyManagement.js (816 lines) and useNoteForm.js (466 lines) each mix three or four concerns. They are the next-biggest refactor surface after App.jsx itself and are not blocked by SUR-220 — they can be done in parallel.

The rest is a tail of known-but-uncaptured cleanup: broken #faq links on authenticated pages, a deprecated iOS meta tag, posthog-node leaking into the client bundle, stale SYSTEM_ARCHITECTURE.md, and zero direct unit coverage on db.js migrations.


App.jsx — structural walkthrough

Starting at the file the user pointed at, here is what is in it and where each concern should move.

LinesConcernWhere it should live
36–171UnlockScreen componentsrc/components/UnlockScreen.jsx — its own file, pulls PIN-redeem UI out of the shell.
178–188isDesktopLayout/isTabletLayout resize listenersrc/hooks/useMediaQuery.js (new); also replaces the duplicated window.innerWidth >= 640 check in useUI.js:4.
231–260captureAnimImg + capturePreviewPendingRef + handleCaptureReady/handleCapturePreviewEndInside useNoteForm (owns the capture lifecycle already) or a dedicated useCaptureAnimation hook.
262–279activeNote/activeAction/activeIdea/activeIdeaAction + handlersuseActionSheet hook or extract <NoteActionOverlay> and <IdeaActionOverlay> components. Current shape guarantees every App-level re-render touches sheet state.
303–366Auth/waitlist/encryption/migration gate ladderExtract <AppGates> wrapper so App renders a single authenticated tree. Also sets up a clean boundary for the SUR-220 router (gates must run before the route tree mounts).
381, 422–429activeIdeasCount, hasMouse, canCapture, desktopTabs, bottomNavItemsMove the derived nav model into a <ShellNavigation> component. window.matchMedia(...) is called on every render today (line 422) and isn’t reactive.
431–687The big JSX — mobile-view switch, nav bar, sidebar/detail columnsOnce SUR-220 lands this becomes <Routes> and most of this file disappears.

Specific smells to call out in the JSX block:

  • Four ${ui.mobileView === '...' conditionals wrapping main-content divs (473, 495, 512, 530, 543, 557, 588, 600) plus two special-case view-visible/view-hidden blocks (617, 630). Classic “early router” scaffolding that SUR-220 collapses.
  • 16 style={{}} inline-style hits in this file alone (86 across the app) — mixes CSS-variable convention with ad-hoc inline props, mostly in the UnlockScreen block.
  • bottomNavItems and desktopTabs duplicate their isActive predicates (['home', 'sources', 'active-ideas'].includes(ui.mobileView)) — these string-array contains checks become route matches post-SUR-220.

After the extractions in the table above, App.jsx can reasonably get under 200 lines (gates + router + global overlays).


Prioritised debt list

Scored Priority = (Impact + Risk) × (6 − Effort) per the tech-debt playbook. Impact/risk/effort are 1–5.

P0 — do alongside SUR-220

#ItemCategoryImpactRiskEffortPriority
1Extract UnlockScreen, <AppGates>, <ShellNavigation>, action-sheet overlays from App.jsx. Add useMediaQuery hook.Code53324
2Fix SUR-209 dead #faq / #what-is-surfc links in ProfileScreen.jsx:205 and SettingsModal.jsx:95Infra/UX32125
3Remove posthog-node from dependencies (move to dev if still needed for scripts)Dependency22120
4Replace window.confirm unsynced-changes dialog in useAuth.js:308 with a styled modal (or accept native dialog explicitly)Code/UX22216

P1 — next refactor sprint (post-SUR-220, before v1.1)

#ItemCategoryImpactRiskEffortPriority
5SUR-247: replace useUI with route-derived state + drop previousMobileView/previousNoteView history shimsArchitecture43414
6Split useKeyManagement.js (816 lines → useEncryptionEnrollment, useEncryptionUnlock, useDeviceTransfer, useLegacyMigration, signInEncryptionCheck)Code34414
7Split useNoteForm.js (useCapture / useTranscribe / useIdeaDiscovery / useNoteFormState) and rework NoteForm’s 30-prop interface (context or colocated state)Code43321
8Decompose syncFromCloud (86-line function in useAuth.js:160) into explicit phases: outbox-flush, cloud-pull, book-backfill, image-hydrate, reloadArchitecture34321
9Add direct unit tests for db.js CRUD + every db.version(n).upgrade path (currently untested — flagged in CLAUDE.md)Test24318
10Split App.behaviour.test.jsx (480 lines) into focused specs (gates, unlock, capture, action-sheets) — paves the way for extracting App.jsxTest23220

P2 — background cleanup

#ItemCategoryImpactRiskEffortPriority
11Refresh docs/architecture/SYSTEM_ARCHITECTURE.md (stale banner per CLAUDE.md, doesn’t describe surfc-web split)Docs22216
12Replace deprecated apple-mobile-web-app-capable meta in index.html:8 with mobile-web-app-capable (keep legacy tag for older iOS)Infra12115
13ADRs for (a) Master Key architecture, (b) two-repo split SUR-218, (c) SUR-201 pathname-router workaround. Missing structural context for new contributors.Docs22216
14Further split useAuth.js into useSession/useSync/useAccountDeletion (344 lines post–km extraction)Code22312
15Consolidate inline style={{}} (86 hits) into styles.css so theming is enforceable; audit use of CSS varsCode2139
16Remove callTranscribeImage/callDiscoverIdeas double-wrapper in src/api.js vs invokeAnthropicProxy in src/supabase.js — pick oneCode12212
17Strip debugLog/addDebug path from useNoteForm.js (production-visible debug surface)Code11110
18Remove src/router.js policy-route handling once SUR-220 lands — Netlify 301-bounces these now, so the matcher is dead codeCode11110

Phased plan

The plan is designed so feature work can continue in parallel. Nothing here requires a freeze.

Phase 0 — Unblock SUR-220 (1 sprint, already in-flight)

Merge with the SUR-220 PR or as a short stack of commits beneath it:

  • #1 App.jsx extraction pass. UnlockScreen, AppGates, ShellNavigation, action-sheet overlays, useMediaQuery. Target: App.jsx < 250 lines.
  • #2 SUR-209 link fix. One-line change in each of two components, either cross-domain to surfc.app/#faq or in-app help page if SUR-209 adds one.
  • #3 posthog-node dependency cleanup. If scripts/ needs it, move to devDependencies and verify the client build shrinks.
  • #4 window.confirm → styled modal for the unsynced-changes path.

Justification: each of these is independent of the router choice, stops compounding debt in App.jsx, and shrinks the surface SUR-220 has to touch.

Phase 1 — SUR-247 foundation (next sprint after SUR-220)

  • #5 SUR-247. With routes in place, delete the useUI history bridge, the _sid coordination, previousMobileView, and the src/router.js matcher. Replace mobileView checks with useLocation/<Route> matches.
  • #10 Break up App.behaviour.test.jsx before major component splits — 480-line tests are hard to move with confidence.

Phase 2 — Mega-hook decomposition (v1.1)

  • #6 useKeyManagement split. Natural seams at the // ── comments already in the file (Reset / Eager restore / Enrollment / Unlock / Transfer / Migration / Sign-in check). Keep keyManager module-level state untouched — only the React wiring is redistributed.
  • #7 useNoteForm split + NoteForm context. Biggest developer-velocity win in the file audit: the 30-prop component is the single loudest prop-drilling smell in the repo.
  • #8 syncFromCloud phasing. Makes sync failures observable (which phase failed?) and unlocks targeted retries.

Phase 3 — Supporting debt (background, any sprint)

  • #9 db.js unit tests. Highest-risk untested surface — a migration bug here silently corrupts user data on device.
  • #11–13 Docs + ADRs. Low-risk, high-onboarding-value.
  • #14–18 Tail cleanup.

Business justification (short form)

  • Velocity today is capped by App.jsx. Every feature that touches auth/encryption/navigation lands in the same file, which makes review slow and test setup expensive. The Phase 0 extraction is a one-time cost that repays on every PR afterwards.
  • SUR-247 is the correct shape for v1.1’s App.jsx refactor. Doing the router migration first (SUR-220) then the view-state replacement (SUR-247) avoids re-doing the JSX splits twice.
  • useKeyManagement at 816 lines is the largest file in the repo and owns the crypto critical path. A bug there is user-data-destructive. Splitting it gives us narrower tests and smaller blast radius on changes.
  • db.js migrations have no direct coverage. Each new Dexie version(n) currently ships on trust. This is the one place where the cost of a silent failure is “lose a user’s notes” rather than “show the wrong screen”.
  • SUR-209 and the posthog-node bundle inclusion are day-one quick wins — they aren’t blocking anything, but they’re embarrassing when noticed.

Quick wins (< 1 hour each)

If you need a low-risk warmup before the structural work:

  1. SUR-209 link fix (#2)
  2. posthog-node dependency cleanup (#3)
  3. Deprecated meta tag (#12)
  4. Strip debugLog from useNoteForm (#17)
  5. Delete matchPolicyRoute + related code paths from router.js once SUR-220 is in (#18)

Each is < 20 lines of change with existing test coverage sufficient to verify.