Files
beardrive/docs/react-migration-prd.md
Snow LeeandClaude Fable 5 d9a5ab3fd6 feat(webapp): [phase 5] parity gate — docs, layout fix, migration complete
- #root { display: contents } so body's flex layout sees through the
  React mount point (main pane was stuck at content width)
- README: Web frontend development section; CLAUDE.md: commands +
  webapp description rewritten for the React/Vite reality (committed
  dist, check-dist pre-release step, the no-router-library and
  no-post-commit-DOM-patching lessons)
- verified: clean git-archive checkout builds with no Node and serves
  the React app; visual parity pass across desktop + mobile surfaces;
  42 e2e specs green; plugin docs untouched (no frontend internals)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-13 10:57:21 -07:00

15 KiB
Raw Permalink Blame History

PRD: Migrate the bdrive web frontend to React + TypeScript

This is the authoritative spec for rewriting internal/webapp/static/ (vanilla JS: app.js ~2,150 lines, style.css, index.html) as a React + TypeScript app. Implement the phases in order; a phase is done only when every acceptance box in its checklist is checked. Record progress and blockers in §Status at the bottom.

Goals

  • Feature-for-feature parity with the current SPA — same routes, same behaviors, same visual design (port style.css verbatim; do not redesign).
  • Maintainable component/hook structure replacing the 90-function single file.
  • Keep the product's single-binary story fully intact.

Non-goals (do NOT do these)

  • No Go API changes. No new endpoints, no changed response shapes.
  • No visual redesign, no CSS framework, no CSS modules — global stylesheet.
  • /auth/* pages and /s/<token> share pages stay server-rendered Go.
  • Markdown stays rendered server-side (/api/.../render); the frontend injects the returned HTML. Never add a client-side markdown renderer.
  • No SSR, no Next.js. Vite static build only.

Hard invariants (violating any of these fails the phase)

  1. go build ./... must work with no Node installed. Built assets are committed at internal/webapp/static/ (the existing go:embed static target in internal/webapp/server.go). Frontend source lives in internal/webapp/frontend/ and vite build writes to ../static/.
  2. Routing semantics unchanged: native History-API paths — /<project-id>/<url-encoded path> (hub), /<path> (volume mode), /<project-id>/insights, /<project-id>/history, /join/<token>. No hash routing; slashes stay literal; all API/asset URLs root-absolute. Reserved prefixes /api/, /auth/, /s/ must still 404 rather than fall back to the SPA shell (server behavior — keep it).
  3. Storage-blind + privacy: the frontend learns everything from /api/config (+ /api/projects in hub mode). Never surface storage details; never expect or display human actor emails from the heat API.
  4. Never commit internal/webapp/manual_serve_test.go (untracked local demo harness). Check git status before every commit.
  5. Runtime deps allowed: react, react-dom, @tanstack/react-query. (react-router-dom was allowed, adopted, then REMOVED in Phase 3: v7 wraps navigation in React.startTransition, which left the old view on screen for seconds after the URL changed. Routing is src/nav.ts — a ~40-line synchronous history router — plus the hand-ported parseRoute in src/router.ts. Do not reintroduce a router library.) Anything beyond these requires updating this PRD with a justification first. Dev deps: vite, typescript, @vitejs/plugin-react, @playwright/test (+ types).

Toolchain & layout

internal/webapp/frontend/        # source (committed)
  package.json  vite.config.ts  tsconfig.json  index.html
  src/          # components, hooks, api types
  e2e/          # Playwright parity suite + hub harness
internal/webapp/static/          # vite build output (committed, generated)
  • Vite build.outDir: ../static, emptyOutDir: true, hashed asset filenames under static/assets/.
  • Dev: vite dev with server.proxy for /api, /auth, /shttp://localhost:8080.
  • Freshness: add internal/webapp/frontend/check-dist.sh (npm ci && npm run build && git diff --exit-code ../static) and document it in CLAUDE.md as a pre-release step.
  • TS API types: hand-write src/api/types.ts for the JSON shapes of /api/config, /api/projects, tree, heat, history, orgs, shares, admin. Derive them from the Go handler structs in internal/webapp/.

Server-side changes (the ONLY Go changes allowed)

  • server.go frontend(): serve assets/* (hashed filenames) with Cache-Control: public, max-age=31536000, immutable; keep no-cache for index.html/everything else. Adjust the embedded-asset test if any.
  • A committed e2e hub harness (see §Verification).

Component map (port targets)

Current (app.js) React module
boot, loadProjects, loadOrgs, acceptInviteFromURL App.tsx, useConfig, useProjects, useOrgs, InviteAccept
selectProject, updateOrgBar, updateAdminBar, sidebar DOM Layout, Sidebar, OrgBar, AdminBar
parseRoute/applyRoute/pushURL/urlFor* React Router routes + usePathRoute helper (path encoding per current encodePath/decodePath)
refreshTree, renderNode/renderChildren, expandTo, applyTreeExpansion, markActive, revealInTree FileTree (+ expansion state persisted as today)
openFolder, renderFolderListing, renderFolderHistory FolderListing
openFile, showMeta, fixLinks, openWikilink FileView (dangerouslySetInnerHTML on server HTML + link-rewrite effect that re-runs on content change)
setCrumb Breadcrumbs
refreshHeat, heatFor/heatText/heatLevel useHeat + heat dot components
showProjectHome, renderConnectGuide, guideSteps, GUIDE_AGENTS ProjectHome, ConnectGuide (tabs; localStorage key bdrive-guide-agent; stale value falls back to first tab; pre-filled origin + project id)
showInsights, renderInsights, squarify, insightsTreemap/Matrix/Chart, renderHotPath, staleColor Insights + SVG components (port math as-is)
showHistory, historyEntryRow, initHistory HistoryView
showOrgAdmin, showHubSettings, showPending OrgAdmin, HubSettings, PendingApprovals
modalPrompt/modalConfirm, toast, showShareDialog, updateShareButton Modal, Toaster, ShareDialog
initUpload, uploadFile, sha256Hex UploadDropzone + useUpload (init→content→commit flow, presign or relay)
showEmptyState, createProject EmptyState
scrollMemo/pendingScroll/rememberScroll per-route scroll restoration hook

Server state through TanStack Query (poll interval from /api/config refresh; invalidate after uploads/renames/admin actions — mirror today's refreshAll). Reuse the SVG icon sprite from the current index.html.

Phases

Phase 0 — scaffold & pipeline

  • Vite+React+TS workspace at internal/webapp/frontend/; build lands in internal/webapp/static/ and is committed. The branch's static/ is the React build from Phase 0 on (the old files live on main / git show main:internal/webapp/static/app.js for porting reference); the parity gate in Phase 5 is what makes the branch mergeable.
  • style.css ported verbatim as global stylesheet; SVG sprite ported (kept inline in index.html, as before).
  • Dev proxy works against a local hub (BDRIVE_DEV_PROXY overrides the localhost:8080 default).
  • Cache-header change in frontend() + assets served hashed (covered by TestFrontendSPAFallback).
  • check-dist.sh works.
  • e2e harness committed (e2e_serve_test.go, gated by BDRIVE_E2E_SERVE=1, port 8993, fresh state each run) and wired as Playwright's webServer; 4 shell specs green. Also ported in Phase 0 (the e2e login flow needed it): the getJSON/postJSON layer with 401→login redirect, /api/config boot, and the title/vault-name wiring (src/api/http.ts, src/hooks/useConfig.ts).
  • go build ./..., go vet ./..., go test ./... green.

Phase 1 — shell: boot, session, projects, routing

  • Boot from /api/config; volume vs hub mode both render (hub via the e2e suite; volume verified against a live bdrive web <dir>).
  • Project list + selection; project color chips (port projColor).
  • Empty state + create project; /join/<token> invite accept (token survives the login redirect — covered by e2e).
  • Deep link + refresh on every route resolves (SPA fallback); unknown project ids fall back to a real project.
  • Sign-out link, admin bar (with pending count), org bar render per session flags (admin vs member covered by e2e). Architecture note: the URL is the source of truth — parseRoute ported verbatim into src/router.ts, a single catch-all route, no route-matching library (encoded slashes must survive). Mutations must await useHubRefresh() before navigating to a new project id, or the unknown-id fallback bounces off the stale list.

Phase 2 — file browsing (long pole)

  • Tree with expansion persistence, active marking, reveal-in-tree (deep links unfold the way to the file — e2e covered).
  • Folder listing incl. heat dots (members) + folder history strip.
  • File view: server-rendered markdown; wikilinks + relative links; meta/provenance line. LESSON (do not regress): never patch the dangerouslySetInnerHTML subtree after commit (the classic fixLinks approach) — React re-applies the markup on unrelated updates and silently discards DOM patches. Instead: transform the HTML string before rendering (img src, target=_blank) and handle link clicks by delegation on the container (FileView.tsx).
  • Breadcrumbs; per-route scroll restoration (location.key memo, restore on POP, re-applied as async sections grow).
  • Download + raw file view; share button state; share dialog (e2e: mint → public fetch → revoke → 404).
  • Upload via the topbar button + file picker (direct/relay per upload/init) with tree refresh + open after commit.
  • Command palette (⌘K fuzzy files/projects/actions) and the topbar overflow menu. Harness note: helpers.login caches one session cookie per identity — the server rate-limits credential POSTs (10/min/IP) and per-spec form logins trip it with flaky timeouts.

Phase 3 — project home, insights, history

  • Project home at /<pid>: connect guide (3 tabs: "Claude Code & Cowork" plugin flow, Hermes CLI, Codex CLI; copy buttons; persisted tab with stale-value fallback; commands pre-filled with hub origin + project id).
  • Insights embedded below the guide for admins/org-owners only (canInsights = hub admin or project-org owner), plus dedicated /insights route; members see neither.
  • Insights views: squarified treemap, reads×freshness scatter with danger quadrant, hot-path list with agent/human split, agent coverage matrix — math ported as-is into JSX SVG.
  • History view: whole-project / subtree (prefix) / per-file (path) modes, newest first, add/edit/delete tags, device attribution, expandable linkified notes; folder listings link into the subtree feed. (Blob version links remain server-side; the classic app had no version-viewer UI either.)
  • Vault-name click returns to project home. NOTE (structural, discovered here): react-router-dom removed — see invariant 5. The original 17 parity checks are ported in e2e/home.spec.ts; suite is 34 specs, ~13s, stable across runs.

Phase 4 — admin surfaces

  • Org admin: rename, members (role change/remove, self marked), projects (rename/delete), invite create/list/revoke with expiry + uses, org-wide share audit with revoke; member view is read-only.
  • Hub settings + pending-approval queue (approve/deny), policy toggles (verification disabled without SMTP), read-only domains/self-signup/admins rows.
  • Destructive actions confirm via modal; toasts on success/error. Design note: panels are NOT routes (parity with the classic app) — Browser takes a panel prop that replaces the content pane and hides file actions; HubApp owns the state and clears it on any pathname change. Known accepted deviation: deleting the currently open project closes the panel (the URL fallback redirect fires).

Phase 5 — parity gate & swap

  • The 17 original smoke checks ported (e2e/home.spec.ts) and extended: login→browse→markdown, upload roundtrip, share create/revoke (+public fetch), org admin flows, history views, 404 on /api/nope. 42 specs total, ~16s, stable.
  • Full e2e suite green against the harness hub.
  • Old app.js/index.html/style.css gone; git grep renderConnectGuide outside frontend/ matches only this PRD.
  • Docs updated: README §Development gains a "Web frontend" section; CLAUDE.md Commands + webapp package description rewritten for the React/Vite reality (committed dist, check-dist, the no-router and no-DOM-patching lessons); plugin docs mention no frontend internals (verified — nothing to change).
  • go build from a clean git archive checkout with no frontend/ tooling succeeds and serves the React app (hashed assets 200, immutable).
  • Visual parity pass: screenshots of home/guide, embedded insights, file view, folder listing, history, org admin, palette, mobile — after fixing #root { display: contents } (body's flex layout must see through the React mount point).

Verification (every phase)

  1. cd internal/webapp/frontend && npm run build — clean.
  2. go build ./... && go vet ./... && go test ./... — green.
  3. Playwright checks for all surfaces finished so far.

e2e harness (build in Phase 0, commit it): frontend/e2e/hub.mjs — builds bdrive (go build -o /tmp/... ./cmd/bdrive), then starts it with a scratch BDRIVE_HOME, hub mode on file://<scratch>/storage, uploads enabled, builtin auth with one pre-seeded account (write the auth users_db JSON directly, or reuse the approach in the untracked manual_serve_test.go — programmatic seeding — in a small committed Go helper under internal/webapp/ guarded by an env var, e.g. BDRIVE_E2E_SERVE=1 go test -run TestE2EServe). Seed a handful of files (markdown with wikilinks, nested dirs, one binary) and some read-heat data. Wire it as Playwright's webServer. Port: 8993. Test account: e2e@example.com / a fixed password.

The 17 existing parity checks (from the previous smoke suite) cover: landing URL is /<pid> (no insights redirect); 3 guide tabs in order; one active tab; claude tab installs plugin (marketplace add + install); claude tab /beardrive:install connect to <origin>, project <pid>; claude tab has no raw CLI; claude tab mentions Cowork; stale saved "cowork" tab falls back safely; codex tab full CLI flow (brew, login origin, init --project, hooks install --agent codex); tab choice persisted in localStorage; insights embedded on home for admins; guide renders above insights; dedicated /insights route still works; vault-name click goes home; browser back/forward across home↔file↔insights; reload on a deep file path; reload on /insights.

Git conventions

  • Branch: feat/react-frontend off main. One PR at the end; do not merge/deploy/push to main.
  • Commit per phase (more is fine), message prefix feat(webapp): [phase N].
  • Never commit: manual_serve_test.go, node_modules/, scratch dirs. Add .gitignore entries in frontend/ for node_modules etc.

Status

  • Phase 0
  • Phase 1
  • Phase 2
  • Phase 3
  • Phase 4
  • Phase 5

Blockers / deviations: none open. Two documented deviations, both deliberate: react-router-dom replaced by the in-repo synchronous router (invariant 5), and deleting the currently-open project closes the org admin panel (Phase 4 note).