- #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
15 KiB
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.cssverbatim; 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)
go build ./...must work with no Node installed. Built assets are committed atinternal/webapp/static/(the existinggo:embed statictarget ininternal/webapp/server.go). Frontend source lives ininternal/webapp/frontend/andvite buildwrites to../static/.- 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). - Storage-blind + privacy: the frontend learns everything from
/api/config(+/api/projectsin hub mode). Never surface storage details; never expect or display human actor emails from the heat API. - Never commit
internal/webapp/manual_serve_test.go(untracked local demo harness). Checkgit statusbefore every commit. - Runtime deps allowed:
react,react-dom,@tanstack/react-query. (react-router-domwas 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 issrc/nav.ts— a ~40-line synchronous history router — plus the hand-portedparseRouteinsrc/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 understatic/assets/. - Dev:
vite devwithserver.proxyfor/api,/auth,/s→http://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.tsfor the JSON shapes of/api/config,/api/projects,tree,heat,history, orgs, shares, admin. Derive them from the Go handler structs ininternal/webapp/.
Server-side changes (the ONLY Go changes allowed)
server.go frontend(): serveassets/*(hashed filenames) withCache-Control: public, max-age=31536000, immutable; keepno-cacheforindex.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 ininternal/webapp/static/and is committed. The branch'sstatic/is the React build from Phase 0 on (the old files live onmain/git show main:internal/webapp/static/app.jsfor porting reference); the parity gate in Phase 5 is what makes the branch mergeable. style.cssported verbatim as global stylesheet; SVG sprite ported (kept inline inindex.html, as before).- Dev proxy works against a local hub (
BDRIVE_DEV_PROXYoverrides thelocalhost:8080default). - Cache-header change in
frontend()+ assets served hashed (covered byTestFrontendSPAFallback). check-dist.shworks.- e2e harness committed (
e2e_serve_test.go, gated byBDRIVE_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): thegetJSON/postJSONlayer with 401→login redirect,/api/configboot, 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 livebdrive 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 —
parseRouteported verbatim intosrc/router.ts, a single catch-all route, no route-matching library (encoded slashes must survive). Mutations mustawait 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/insightsroute; 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
panelprop 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.cssgone;git grep renderConnectGuideoutside frontend/ matches only this PRD. - Docs updated: README §Development gains a "Web frontend" section;
CLAUDE.md Commands +
webapppackage 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 buildfrom a cleangit archivecheckout with nofrontend/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)
cd internal/webapp/frontend && npm run build— clean.go build ./... && go vet ./... && go test ./...— green.- 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-frontendoffmain. One PR at the end; do not merge/deploy/push tomain. - Commit per phase (more is fine), message prefix
feat(webapp): [phase N]. - Never commit:
manual_serve_test.go,node_modules/, scratch dirs. Add.gitignoreentries infrontend/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).