- FileTree with fold state, lone-root auto-open, reveal-on-deep-link; folder listings with heat dots and the journals-backed change feed - FileView: markdown (HTML transformed BEFORE render, link clicks delegated — React re-applies dangerouslySetInnerHTML markup on unrelated updates, so post-commit DOM patching loses handlers), images, text, download card - breadcrumbs, per-route scroll restoration (location.key memo) - topbar actions: share dialog (mint/copy/open/revoke), history/upload/ download buttons, ⋯ overflow menu; upload via upload/init direct or relay path, then tree refresh + open - ⌘K command palette: fuzzy files/projects/actions with stemming - e2e: 11 new browse specs (22 total green in ~12s); helpers cache one session cookie per identity to stay under the 10/min auth rate limit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
13 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 (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; 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)