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
This commit is contained in:
Snow Lee
2026-07-13 10:57:21 -07:00
co-authored by Claude Fable 5
parent f60be366e7
commit d9a5ab3fd6
9 changed files with 101 additions and 22 deletions
+7 -2
View File
File diff suppressed because one or more lines are too long
+20
View File
@@ -509,6 +509,26 @@ The integration tests in `internal/syncer` simulate multiple devices syncing
through a `file://` remote, including offline operation and concurrent-edit
conflicts. Set `BDRIVE_HOME` to relocate all beardrive state (used heavily in tests).
### Web frontend
The hub's web UI is a React + TypeScript app in `internal/webapp/frontend`
(Vite; runtime dependencies are just react, react-dom, and
@tanstack/react-query — routing is a small in-repo history router). Its
**built output is committed** at `internal/webapp/static`, the `go:embed`
target, so building or `go install`-ing the binary never needs Node.
Only when changing `frontend/src`:
```sh
cd internal/webapp/frontend
npm install
npm run dev # hot-reload dev server, proxying /api to a local hub
# (BDRIVE_DEV_PROXY=http://localhost:8993 to point elsewhere)
npm run build # rebuild internal/webapp/static — commit the result
npm run e2e # Playwright suite; starts its own seeded hub on :8993
./check-dist.sh # verify the committed static/ is fresh (pre-release check)
```
## License
GNU AGPL-3.0 — Copyright 2026 Runbear, Inc. See [LICENSE](LICENSE).
+24 -17
View File
@@ -200,21 +200,25 @@ refresh; invalidate after uploads/renames/admin actions — mirror today's
open project closes the panel (the URL fallback redirect fires).
### 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`/old `index.html`/old `style.css` gone from the repo
(replaced by build output); `git grep renderConnectGuide` finds only
frontend/src.
- [ ] Docs updated: README (frontend dev section), CLAUDE.md (`webapp`
package description — replace "dependency-free vanilla JS" with the
React/Vite reality, build + check-dist instructions),
`plugin/skills/beardrive/SKILL.md` only 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.
- [x] 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.
- [x] Full e2e suite green against the harness hub.
- [x] Old `app.js`/`index.html`/`style.css` gone; `git grep
renderConnectGuide` outside frontend/ matches only this PRD.
- [x] 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).
- [x] `go build` from a clean `git archive` checkout with no
`frontend/` tooling succeeds and serves the React app (hashed
assets 200, immutable).
- [x] 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)
@@ -261,6 +265,9 @@ file path; reload on `/insights`.
- [x] Phase 2
- [x] Phase 3
- [x] Phase 4
- [ ] Phase 5
- [x] Phase 5
Blockers / deviations: (record here; stop rather than deviate silently)
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).
+43
View File
@@ -0,0 +1,43 @@
import { chromium } from "playwright";
const b = await chromium.launch({ headless: true });
const p = await b.newPage({ viewport: { width: 1360, height: 900 } });
await p.goto("http://localhost:8993/");
await p.waitForURL(/auth\/login/);
await p.fill('input[name="email"]', "e2e@example.com");
await p.fill('input[name="password"]', "e2e-pass-1");
await p.click("form button");
await p.waitForSelector(".guide");
await p.waitForTimeout(800);
const dir = "/Users/snow/.claude/jobs/31cc1f49/tmp/react-shots/";
await p.screenshot({ path: dir + "1-home-guide.png" });
await p.evaluate(() => document.getElementById("content").scrollTo(0, 1200));
await p.waitForTimeout(300);
await p.screenshot({ path: dir + "2-home-insights.png" });
const pid = (await (await p.request.get("http://localhost:8993/api/projects")).json()).projects.find(x=>x.name==="wiki").id;
await p.goto(`http://localhost:8993/${pid}/index.md`);
await p.waitForSelector("#content h1");
await p.waitForTimeout(400);
await p.screenshot({ path: dir + "3-file-view.png" });
await p.goto(`http://localhost:8993/${pid}/notes`);
await p.waitForSelector(".dl-history");
await p.waitForTimeout(400);
await p.screenshot({ path: dir + "4-folder-listing.png" });
await p.goto(`http://localhost:8993/${pid}/history`);
await p.waitForSelector(".hentry");
await p.screenshot({ path: dir + "5-history.png" });
await p.click("#invite-btn");
await p.waitForSelector(".admin");
await p.waitForTimeout(400);
await p.screenshot({ path: dir + "6-org-admin.png" });
await p.keyboard.press("Meta+k");
await p.waitForSelector("#palette");
await p.fill("#palette-input", "read");
await p.waitForTimeout(300);
await p.screenshot({ path: dir + "7-palette.png" });
// mobile viewport
const m = await b.newPage({ viewport: { width: 390, height: 780 } });
await m.goto(`http://localhost:8993/${pid}/index.md`);
await m.waitForSelector("#content h1");
await m.screenshot({ path: dir + "8-mobile-file.png" });
await b.close();
console.log("shots done");
+4
View File
@@ -35,6 +35,10 @@
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; margin: 0; }
/* The React shell renders inside #root; make it transparent to layout so
body's flex still governs sidebar + main. */
#root { display: contents; }
body {
display: flex;
background: var(--bg);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#128059;</text></svg>">
<script type="module" crossorigin src="/assets/index-B1CZxrv9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-oVhdizP9.css">
<script type="module" crossorigin src="/assets/index-BmKSe0_Y.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CemqdDtO.css">
</head>
<body>
<svg width="0" height="0" class="sprite" aria-hidden="true" focusable="false">