9 Commits
Author SHA1 Message Date
4f04b78c71 feat(webapp): frontmatter moves to a collapsible side panel (BEA-154) (#187)
A doc's YAML frontmatter rendered as a table pinned to the top of the
reading column, so on anything with more than two or three keys the
document itself started below the fold. It is a panel beside the prose
now — a sticky rail on a wide window, a closed disclosure above the body
on anything narrower — and the reading column starts with the document.

The table was built on the server and handed to the client inside one HTML
string, so this is not a CSS change: markdown.go splits the parse
(frontmatterPairs) from the markup, /api/render gains an ordered
`frontmatter` field, and the viewer switches to RenderMarkdownPairs.

RenderMarkdown keeps its exact output — it is the public share page, and
every /s/ link ever minted serves it. shares_test now pins that, because
nothing else would have failed if a later cleanup moved shares.go onto the
pairs path.

Values cross the wire as literal text plus a `code` flag rather than
pre-escaped HTML, so the panel is ordinary React text nodes and never
touches dangerouslySetInnerHTML: "a value containing markup renders as
text" holds by construction.

The rail's breakpoint is 1400px, not the 1180px the plan named — 768 of
prose + 28 + 240 of rail needs 1036px of column, and at 1280 the reading
measure lost 110px, which is the squeeze the panel exists to avoid.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:55:29 -07:00
Snow Lee (Sungwon)andGitHub 2875e033be feat(webapp): mermaid fences render as diagrams, in the viewer and on share pages (BEA-91) (#143)
A ```mermaid fence rendered as a wall of `graph TD` source on both surfaces.
It now renders as an SVG in the hub file viewer and on public /s/<token>
markdown share pages.

Mermaid ships inside the binary (no CDN, so air-gapped self-hosters keep
working) and is imported lazily: a document with no fence downloads none of
it. A fence that doesn't parse — the common case for hand-written wiki
diagrams — keeps today's <pre><code> plus a small note, and one bad fence
never stops the good ones beside it. A blocked or offline chunk lands in the
same place.

The share page is the harder half: it is server-rendered Go HTML with no
JavaScript, and its `sandbox allow-scripts` CSP makes the origin opaque, so a
module script and every import() it makes arrive with `Origin: null`. The CSP
is unchanged and gains no allow-same-origin; instead the real-asset branch of
frontend() now sets Access-Control-Allow-Origin, which only ever touches files
that are already public and cookie-less. The script tag itself is injected
only when the rendered document actually contains a fence.

Also fixes an embed bug this change surfaced: `//go:embed static` silently
skips names beginning with `_`, and Vite's first shared chunk is
`_commonjsHelpers-<hash>.js`. The build passed, the commit looked right, and
the served app was blank. It is `all:static` now, with a test that every file
on disk is in the binary.
2026-08-10 14:18:18 -07:00
11dd7ac528 fix(webapp): the viewer credits the account you signed in with, like History does (BEA-37) (#84)
* fix(webapp): viewer credits the signed-in account, like History does

RemoteSource.Files built each FileInfo from op.Author alone and dropped
op.User/op.UserName, so the file viewer header showed a git/OS identity
the user never signed in with while History rows for the same op showed
the account. Carry both fields through FileInfo -> Node -> /render and
render them with the whoChanged() helper History already uses, so there
is one attribution rule in the frontend rather than two.

The guard on the meta line stays on the raw fields: whoChanged() answers
"unknown" rather than "", and plain-folder (DirSource) mode has no
identity at all and must keep printing nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(webapp): plain-folder render carries no empty identity fields

Locks the other half of BEA-37: DirSource has no account behind a file,
and sending empty user fields would make whoChanged() print "unknown"
where plain-folder mode has always printed nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:18:32 +09:00
Snow LeeandClaude Fable 5 cb621c153e feat(web): friendly 404 for missing paths; HTML files render as sandboxed pages
Two viewer features plus the security fix the second one surfaced:

- Missing file/folder paths now get a not-found view: the path, and the
  hint that a just-created file may still be uploading or syncing from a
  teammate's device — the tree polls every few seconds so it appears on
  its own, plus a Check again button that refetches immediately. The
  topbar's share/download actions no longer show for nonexistent files.

- Opening an .html file renders it as a page (sandboxed iframe,
  allow-scripts only) instead of showing source text.

- SECURITY: /api/file was already serving synced HTML inline as
  text/html on the hub origin with session cookies — a stored-XSS
  surface reachable by direct navigation, previously masked only by the
  viewer showing HTML as text. Inline HTML and SVG responses now carry
  'Content-Security-Policy: sandbox allow-scripts' (the same wall as
  /s/* share pages); downloads are exempt (attachments never execute in
  the hub origin).

Tests: Go CSP-header matrix (html/svg sandboxed, md clean, download
exempt); e2e: sandboxed-iframe rendering incl. in-frame content + CSP
assertion, and the not-found → late-upload → Check again flow. 44 specs
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-16 11:01:38 -07:00
Snow LeeandClaude Fable 5 e701555257 feat(webapp): [phase 0] React+TS frontend scaffold, build pipeline, e2e harness
- 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
2026-07-13 09:57:14 -07:00
Snow LeeandClaude Opus 4.8 0254c8d464 feat(web): native URL path routing (no hash, no %2F)
Replace the frontend's hash-based routing with the History API and real
`/` paths, so URLs read like native file paths:

  /#p-4e61c7d4/shared%2Fidea.md  ->  /p-4e61c7d4/shared/idea.md
  /#shared%2Fnotes.md            ->  /shared/notes.md          (volume mode)
  /#join/<token>                 ->  /join/<token>

Client (app.js):
- parseRoute() reads location.pathname; pushURL/syncURL push native paths
  (segments percent-encoded, "/" kept literal); a popstate handler restores
  back/forward. Invites read from /join/<token>.
- All api/ fetches and the app.js/style.css refs are now root-absolute so a
  deep path doesn't break relative URL resolution.

Server (server.go):
- New Server.frontend handler: real assets serve directly; every other
  non-API/auth/share GET returns index.html (SPA fallback), so deep links
  and refreshes resolve instead of 404ing. Reserved prefixes stay 404s.
- Invite links minted as /join/<token> (orgs.go).

Tests: TestFrontendSPAFallback covers the fallback + reserved-prefix 404s;
existing invite test updated. Verified end to end in the running hub
(deep-link reload, back/forward, no %2F/# in the bar). Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-09 14:19:07 -07:00
Snow LeeandClaude Fable 5 a7bb790615 feat: multi-project sync hub, bdrive login/init onboarding, .bdrive rename
The web server (bdrive web) becomes a full sync hub, and client devices
get one-command onboarding — without ever seeing storage info or holding
cloud credentials:

- bdrive web -c config.json: server configurable from a JSON file
  (remote/addr/upload/upload_ttl/projects_db); explicit flags win.
- Hub mode: pointing bdrive web at a storage root hosts many projects,
  each under <root>/<project-id>/ (remote.Prefixed). Projects live in a
  file-backed registry (projects.json — loaded at open, rewritten
  atomically per change) with create-or-join-by-name semantics.
- Per-project APIs: /api/projects (list/create/get) and
  /api/p/<id>/{tree,file,render,download,upload/*,store/*}. The web UI
  grows a project list with per-project browsing and hash deep links.
- Browser uploads and a store proxy for syncing devices: presigned
  direct-to-storage PUTs when the backend can sign (S3 presign, GCS V4
  signed URLs; expiring, credential-free), relayed through the server
  otherwise. Journals are never presigned — only immutable blobs.
  Blobs-before-journal and one-writer-per-journal invariants hold.
- https:// remote backend: a device syncs one hub project through
  /api/p/<id>/store/* — mnt/sync/daemon/log all work unchanged.
- bdrive login <url>: verify a hub and remember it as the device default
  (settings.json). bdrive init: create-or-join a project named after the
  folder (--name/--project override), write .bdrive, seed a starter
  .bdriveignore, mount, and start the daemon — one command per project.
- Hard-break rename: .beardrive->.bdrive, .beardriveignore->.bdriveignore,
  ~/.beardrive->~/.bdrive, BEARDRIVE_HOME->BDRIVE_HOME, temp/conflict
  prefixes; old names are no longer read.
- Tests: presigning, project registry persistence, store API validation
  and gating, project isolation over live HTTP, browser upload flows, and
  two-device convergence through a hub (incl. read-only pull-only mode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 07:13:00 -07:00
Snow LeeandClaude Fable 5 1ac128db2c rename: sfs -> BearDrive everywhere; CLI becomes bdrive
Product and project are BearDrive; the CLI binary is bdrive (bdrive mnt,
bdrive sync, ...), the web viewer is bdrive-web. All conventions follow
the full name: .beardrive settings file, .beardriveignore, ~/.beardrive
home, BEARDRIVE_HOME, .beardrive-conflict-* / .beardrive-tmp-* files.
Plugin/skill/marketplace renamed to beardrive (/beardrive:mount).

Module path is now github.com/runbear-io/beardrive — merge only after
renaming the GitHub repo, or go install breaks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
2026-07-07 15:02:40 -07:00
Snow LeeandClaude Fable 5 2d29ff8aac feat: web viewer, project-local config, selective sync, Claude Code plugin
- sfs-web (cmd/sfs-web, internal/webapp): read-only Obsidian-style web UI
  serving a local folder (default) or an sfs remote; markdown rendering
  with [[wikilinks]], task lists and tables, file downloads with ETags,
  per-file provenance from the journals; added to goreleaser builds
- .sfs project file (internal/config): per-folder volume/remote/include
  settings that travel with the folder, win over the global registry, and
  never sync; daemon picks up edits live
- .sfsignore + include lists (internal/syncer): gitignore-style selective
  sync with ! re-includes, applied symmetrically in scan and materialize;
  newly ignored files stop syncing without being deleted anywhere
- Claude Code plugin (plugin/, .claude-plugin/): sfs skill, /sfs:mount and
  /sfs:status commands, turn-boundary sync hooks (blocking pull on prompt,
  async push on stop); installable via the repo's marketplace manifest
- CLAUDE.md and .claude project settings for Claude Code development

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
2026-07-07 14:05:36 -07:00