- Vite+React+TypeScript workspace at internal/webapp/frontend; vite build emits committed assets into internal/webapp/static (the go:embed target), so plain 'go build' still needs no Node - style.css and the SVG sprite ported verbatim; boot layer (api/config, 401->login redirect) ported so the auth flow works end to end - frontend(): content-hashed assets/* now served immutable; index.html stays no-cache (TestFrontendSPAFallback covers both) - committed e2e harness (BDRIVE_E2E_SERVE=1, deterministic seeded hub on :8993) wired as Playwright webServer; 4 shell specs green - check-dist.sh guards against stale committed build output Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
12 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,react-router-dom,@tanstack/react-query. 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. - Project list + selection; project color chips (port
projColor). - Empty state + create project;
/join/<token>invite accept. - Deep link + refresh on every route resolves (SPA fallback).
- Sign-out link, admin bar, org bar render per session flags.
Phase 2 — file browsing (long pole)
- Tree with expansion persistence, active marking, reveal-in-tree.
- Folder listing incl. heat dots (members) + folder history strip.
- File view: server-rendered markdown injected; wikilinks + relative
links rewritten (
fixLinkssemantics); meta/provenance line. - Breadcrumbs; per-route scroll restoration.
- Download + raw file view; share button state; share dialog.
- Drag-drop upload (presign and relay paths) with refresh after commit.
- Command palette (⌘K: file names, projects, actions — port the
palette overlay from the old shell) and the topbar overflow menu
(
more-btn/more-menu) for narrow viewports.
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; commands pre-filled with hub origin + project id). - Insights embedded below the guide for admins/org-owners only
(
canSeeInsightslogic), plus dedicated/insightsroute. - Insights views: treemap (squarify), device matrix, chart, hot-path list — visually equivalent.
- History view:
?path=/?prefix=modes, newest-first entries, blob version links, device attribution. - Vault-name click returns to project home.
Phase 4 — admin surfaces
- Org admin: rename, members (role change/remove), invite create/list/ revoke with expiry, org shares list + revoke.
- Hub settings + pending-approval queue (approve/deny), policy toggles.
- All actions confirm via modal where the current UI does; toasts on success/error.
Phase 5 — parity gate & swap
- Port the 17 checks from the pre-existing smoke suite (see
§Verification) into
frontend/e2e/as Playwright tests; extend with: login→browse→open markdown file, upload roundtrip, share create/ revoke, org admin invite flow, history view, 404 on/api/nope. - Full e2e suite green against the harness hub.
- Old
app.js/oldindex.html/oldstyle.cssgone from the repo (replaced by build output);git grep renderConnectGuidefinds only frontend/src. - Docs updated: README (frontend dev section), CLAUDE.md (
webapppackage description — replace "dependency-free vanilla JS" with the React/Vite reality, build + check-dist instructions),plugin/skills/beardrive/SKILL.mdonly if it mentions frontend internals (it likely doesn't — verify). go build ./...from a clean checkout with no Node succeeds and serves the React app.
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: (record here; stop rather than deviate silently)