- #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
- org admin: rename, member roles/removal (self marked), project
rename/delete, invite links (create/copy/revoke, uses + expiry),
org-wide public-share audit with revoke; members get a read-only view
- hub settings: verification/approval policy toggles (verification
disabled without SMTP), read-only domains/self-signup/admins, pending
signup queue with approve/deny (count feeds the admin bar)
- panels replace the content pane without becoming routes (classic-app
parity): Browser takes a panel prop, HubApp owns the state and any
navigation closes it
- e2e: 8 admin specs, all mutations self-reverting (42 total green)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
- project home at /<pid>: connect-an-agent guide (Claude plugin flow,
Hermes/Codex CLI tabs, persisted choice, copy buttons, real hub origin
+ project id), with Insights embedded below for admins/org-owners
- Insights: squarified treemap, reads×freshness scatter with danger
quadrant, hot-path list, agent coverage matrix — classic math ported
verbatim into JSX SVG; dedicated /insights route
- history views: whole-project / subtree / per-file feeds with kind tags,
device attribution, expandable linkified session notes
- REPLACED react-router-dom with src/nav.ts (~40-line synchronous history
router): v7 wraps navigation in startTransition, which left the old
view on screen for seconds after the URL changed (flaky navigations,
35KB heavier bundle). Routing semantics are unchanged.
- e2e: 12 new specs porting the original 17 parity checks (34 total,
~13s, stable across repeated runs)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
- 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
- URL as source of truth: parseRoute/urlForPath/urlForView ported verbatim
(src/router.ts), single catch-all route so encoded slashes survive
- hub shell: project nav with color chips, org bar, admin bar with pending
count, sign-out per session flags; volume shell renders too
- empty-state onboarding (invite paste + create project), /join/<token>
invite accept that survives the login redirect
- toast + modal prompt/confirm primitives (imperative promise API over a
React host, matching the classic behavior)
- e2e: 7 new hub specs (11 total green); harness gains a no-org 'solo'
account to reach the empty state
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
- 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
Clicking a project now lands on an index page (/<project-id>) instead of
redirecting to Insights: a tabbed guide for mounting the project locally
and wiring it to a coding agent, with the Insights dashboard embedded
below for admins/org-owners.
- Claude Code & Cowork share one tab guiding through the plugin
(/plugin marketplace add + /beardrive:install with this hub's URL and
the project id pre-filled); Hermes and Codex get the four-step CLI flow
(install, login, init, hooks install) with real copyable commands.
- /<project-id>/insights stays a dedicated deep-linkable route; the
sidebar project name is now a home link.
- Copy buttons, localStorage-persisted tab choice, scroll/back-forward
behavior consistent with the other views.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
- No-flicker polling: tree and heat refreshes compare fetched data and skip
DOM rebuilds when nothing changed; in-place listing re-renders preserve
scroll.
- RESTful routes: /<project>/insights and /<project>/history[/<path>]
replace query-param views; the reserved first segments survive refresh,
deep links, and back/forward. Per-route scroll restore (manual
scrollRestoration) returns the reader to where they were.
- History rows unified: every entry (recent changes and full history) is a
clickable row that opens the file — view/download version links removed;
old versions remain reachable via the blob API until the time-travel
viewer lands. Folder tree loses the per-dir history icon.
- Hot path: danger rows use the amber accent, ⚠ trails the filename, and a
tooltip explains the flag (reads/30d, days unchanged).
- Frontend responses send Cache-Control: no-cache so upgrades never leave
browsers on a stale embedded UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
History notes still clamp to one line so the feed stays scannable, but
clicking (or Enter on) the note expands the full text and back — links
inside keep working. Selecting a project now lands admins/org owners on
the Insights dashboard instead of the empty "select a file" pane;
members keep the placeholder, and routes that carry a path still open
that file or folder.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
AgentHeat aggregates agent-kind buckets per device per top-level folder;
the handler joins the device registry (name/OS) and sorts by total. Human
and share buckets are never consulted, so human actor identities cannot
appear in the response — asserted by test, along with registry join, root
folder bucketing, sort order, and invalid-by rejection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Insights (admin/org-owner) becomes a stacked dashboard driven by one heat
fetch plus the tree, with the all/human/agent lens applied throughout: a
dependency-free squarified treemap of every file (cell size = reads in the
window, color = staleness, ⚠ on hot+stale, one delegated click handler —
readable at 500+ files, group labels open folders, cells open files); the
reads×freshness scatter demoted to drill-down with density handling
(translucent dots, radius = agent share); and a hot-path top-20 list with
stacked agent/human bars replacing the plain danger list. The agent
coverage matrix section renders when the server provides the by=device
breakdown. Design addendum recorded in docs/design/read-heatmap.md;
calendar/streamgraph explicitly deferred.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Admin/org-owner Insights view (⋯ menu): dependency-free SVG scatter of
every file by 30-day reads × days since last change, log scales, with the
hot-but-stale danger quadrant shaded and a ranked fix-these-first list;
lens toggle for all/human/agent reads. The Claude Code plugin gains a
PostToolUse(Read) hook so plugin users feed agent-read telemetry without
project-level hook registration. Docs synced: README, SKILL.md,
plugin install/init commands, CLAUDE.md, design doc marked implemented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Read telemetry per docs/design/read-heatmap.md: a ReadLedger over a new
batch-oriented MetaStore ReadRepo (file reads.json + SQL read_stats)
aggregates viewer and share reads into daily per-actor buckets, debounced
to visits, folded into all-time rows past retention. GET /api/p/<id>/heat
serves per-path counts (human/agent/share, distinct readers, last read) —
never identities. /store sync traffic and history blob views are not reads.
The viewer shows heat dots and read counts on folder listings and the file
meta line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Folders in the file tree are now selectable: the row opens the folder
(chevron still folds) and the main pane lists its contents, with clickable
breadcrumbs, folder URLs/deep links, palette entries, and uploads landing
inside the selected folder. In hub mode the listing includes a Recent
changes feed for the subtree; the history API now classifies puts as
add|edit (replayed over all ops before filtering) so entries are badged
added/edited/deleted, and op notes render under entries with URLs
linkified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Add a database/sql MetaStore (db_sql.go) targeting SQLite locally and
Postgres/Supabase in production through one portable schema: real tables
(accounts, tokens, auth_policy, projects, orgs, org_members, invites,
shares, devices), idempotent CREATE TABLE IF NOT EXISTS migrations at Open,
?→$N placeholder rebinding for Postgres, times as RFC3339 text, and
transactional multi-row writes (an org and its members). Pure-Go drivers
only (modernc.org/sqlite, jackc/pgx v5) so CGO-free builds keep working.
One shared conformance suite (db_conformance_test.go) runs the same
service-level operations — accounts+tokens, policy, pending/approve,
projects create-or-join/rename/delete, org roles, invite create/redeem/
uses/validity, share create/revoke/expiry, devices — then reopens the store
and asserts everything persisted. Runs against file AND sqlite always, and
postgres when BDRIVE_TEST_POSTGRES is set. Verified green on all three
against a real local Postgres.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Introduce a pluggable metadata persistence layer: a MetaStore of typed
repositories (AccountRepo, ProjectRepo, OrgRepo, ShareRepo, DeviceRepo).
The five registries keep their in-memory maps + logic and now persist each
change as one record through a repo, instead of rewriting a whole JSON file
inline. The `file` backend (db_file.go) reproduces the exact on-disk JSON
formats, so a running hub loads unchanged. Open*(path) constructors stay as
thin wrappers over the file backend.
Zero behavior change: full webapp suite green; the example hub boots and
existing auth.json/orgs.json/projects.json load and sign-in works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
A hub on a public URL could be signed up to with any fake email. Make the
safe posture the default and turn the dangerous ones into startup errors.
Three supported postures:
- invite-only (NEW default): allow_signup defaults false. A valid org
invite link bootstraps an account even when self-signup is closed — the
only way in. pageLogin/pageSignup detect a /join/<token> target via
BuiltinAuth.InviteValid (wired to OrgDB.ValidInvite) and offer account
creation; signupInvited skips the domain/approval/verification gates and
activates immediately (the owner's invite is the vetting).
- approval-gated: allow_signup + require_approval (no SMTP needed).
- domain-restricted + verified: allow_signup + allowed_domains +
require_verification (needs smtp).
Hardening:
- BuiltinAuth.ValidateSignupPolicy (run at hub startup in web.go) refuses to
boot an open hub with no gate, or require_verification without a mailer
(the link would otherwise only reach the server log).
- handleAdminPolicy rejects enabling verification without SMTP; the UI
toggle is disabled with clearer copy in that case.
Tests: TestValidateSignupPolicy, TestInviteBootstrapsAccountWhenSignupClosed,
TestPolicyVerificationNeedsMailer. Verified end to end in the running hub
(outside-domain invitee onboards via a link; direct signup stays closed).
Docs updated (README, CLAUDE.md, SKILL.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
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
Rework the bdrive web UI away from the violet Obsidian-ish theme to a
near-black, honey-accented, hairline-crafted look in the spirit of Linear:
- Replace the mixed emoji glyphs (chevrons, clocks, palette icons, admin
badge, history markers, share dialog) with one coherent SVG line-icon
sprite + a svgIcon() helper; colored letter-marks for projects.
- Repoint the design tokens and rewrite style.css: near-black grounds,
7% hairline borders, tightened type, honey spent only on active state,
the ⌘K selection, and brand marks.
- Restyle the server-rendered auth pages and the public /s/ share shell
to the same tokens so sign-in and the app read as one product.
File-tree behavior:
- Folders start collapsed; a lone root folder opens on load.
- Opening a file (search, wikilink, deep link) unfolds the path to it and
scrolls the row into view. State now tracks open dirs (not collapsed),
so it survives the periodic tree refresh.
- Plain-text/JSONL views wrap long unbreakable lines instead of
overflowing the reading column.
Verified in the running example hub across desktop + mobile; go build,
go vet, go test ./... all green; zero horizontal page overflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Flips the final objective gate (contrast_aa) true; design-only.
- Palette match-highlight on a SELECTED row: recolor .plabel b to
--accent-bright (#c9b3ff) → 6.19:1 on the tinted row (was accent 3.98:1).
- Auth-page secondary text .alt #8a8a8a→#969696 → 5.12:1 on the card
(was 4.38:1).
- Polish: active-project marker uses --accent-bright to match its label;
Upload/Download join Share/History as uniform ghost buttons so no
secondary action shouts louder than the rest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Closes the remaining design-review gaps (design-only; no flow changes).
Accessibility (the failing objective gate + residuals):
- Mobile 44px now reaches the PRIMARY tap targets: file tree rows,
project rows, the org "Manage" button, new-project +, and folder-history
all sized to 44px min (round 1 had only reached the header/sidebar icons).
- Residual sub-AA small text fixed: --text-faint #8a8a8a→#969696 (AA on the
#262626 sidebar, measured 5.1); selected palette row's kind/icon lifted
to #c4c4c4 (6.6); filled-button hover moved to --accent-press #5a3bc9 so
white labels stay ≥4.5 on hover (7.2, was 2.9).
- Directory rows expose aria-expanded, updated on collapse toggle.
UX (the main remaining seam):
- Native prompt()/confirm() replaced with in-app modal components
(modalPrompt / modalConfirm, destructive variant) for new project,
rename, delete, remove member, and revoke invite/share — so every
decision shares one visual language with the share modal and toasts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Addresses the baseline design review's failing objective gates and layout
defects; no backend or flow changes.
Accessibility (was the ceiling — 3 of 4 objective gates failed):
- Contrast to AA: lifted --text-faint (#6e6e6e→#8a8a8a) and --text-dim;
darkened --accent-dim (#7c5cd6→#6a48e0) so white button labels reach
4.5:1; new --accent-bright (#c9b3ff) for accent text on the tinted
active background (tree/project active rows, admin badge, invite/ghost
buttons all lifted off sub-AA pairings). Same fix applied to the
server-rendered auth pages, which now share the app's token values.
- Keyboard + focus: file tree, project, and org-name rows are now
focusable (tabindex/role + Enter/Space) with a global :focus-visible
ring; restored input focus rings on app and auth pages.
- Touch targets: every header + sidebar control is a 44x44 hit area on
mobile; secondary file actions (History/Upload/Download) collapse under
a "⋯ More" menu so the row still fits with zero horizontal overflow.
Layout defects:
- #meta no longer wraps to 5 lines / shoves the action buttons — single
truncating line; #crumb truncates too.
- Long tree filenames ellipsize (label span flex:1 min-width:0) instead
of hard-clipping.
Consistency:
- Fixed the .markdown specificity leak: admin/history/onboarding views no
longer inherit markdown type rules (content class toggles per view), so
the admin type scale renders as declared.
- Recolored the off-system gold Admin badge to the accent family; added a
3-step radius token scale; swapped the mojibake-ish ▣/⛛ markers.
- Added a global [hidden] guard so explicit display rules can't override
the hidden attribute.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Both usability personas cleared the bar this round (admin 4.8, user 4.8;
every dimension >=4, zero blocker/major). Final touches from their notes:
copyText() guards navigator.clipboard so invite/share copy degrades to
"copy it manually" on a non-HTTPS origin instead of throwing; the join
toast now says "joined the <name> team, opening its projects" to
distinguish org from project.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Fixes the two round-2 majors.
User: on mobile the per-file actions (Share/History/Upload/Download) are
now icon buttons in the header — reachable again (round 2 wrongly hid
them). Search tolerates simple English plurals (ideas→idea), the
no-matches state states what search covers, the sidebar shows the hub
brand instead of the raw device name, and history-row downloads carry a
download attribute. Logged-out loads redirect to sign-in from /api/config
instead of firing 401-ing API calls.
Admin: a hub-admin "Signup & access" settings screen (⚙ Admin in the
sidebar) toggles email verification and admin approval live — persisted to
auth.json and surviving restart — while the domain allowlist and admin
list are shown read-only (deliberately server-config-owned so a browser
session can't widen access). Pending approvals live on the same screen.
Config toggles are now *bool so an explicit config value pins the setting
each boot, else the UI-saved policy stands. Invite revoke confirms; role
change re-renders the panel.
Tests: policy persistence + reload, policy API admin-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Admin: top-level ⚙ settings entry (owners/admins), signup form states the
domain restriction up front, invite list shows creator + join count,
org-wide share audit shows creator/date and confirms before revoke,
self-row role/remove controls disabled to avoid footguns.
User: mobile header no longer overflows (per-file actions move to the ⌘K
palette on narrow viewports; tables/pre scroll in their own container),
empty-state copy works on mobile, share confirmation is now an explicit
"anyone with this link can view" dialog with copy/open/revoke, invite
links carry a "you've been invited" banner through login, joining opens
the joined project, brand shown as the title, logout labeled, palette
placeholder clarified to "file names".
Tests: invite use-counter + creator in the owner list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Makes a self-hosted hub safe to expose on a public URL and operable
without hand-editing JSON — addressing the blocker/major findings from
the persona usability evaluations.
Signup gating (config auth block, all optional):
- allowed_domains: signup email must match (e.g. only @runbear.io)
- require_verification: email-link activation before sign-in (reuses mailer)
- require_approval: hub admins approve new accounts (admins list)
- brand shown on the sign-in page; allow_signup:false already hid Sign up
Accounts carry a Status (active/unverified/pending); non-active accounts
cannot authenticate.
Admin lifecycle (endpoints + web UI):
- org: rename, member role change, member remove (last-owner guarded),
invite list + revoke
- project: create (web), rename, delete (from the org panel)
- hub admins: approve/deny pending signups (sidebar bell + panel)
- org-wide public-share audit with revoke
UX: onboarding empty-state (explains invites, paste-invite + create-project)
instead of a blank sidebar; visible "Search ⌘K" button; toasts replace
blocking alert(); responsive layout with an off-canvas sidebar; joining
via #join now survives a logged-out click (token carried through login).
Web uploads are attributed to the signed-in account, not the server.
Login/signup are rate-limited per IP.
Tests: domain/verification/approval gates, auth rate limit, org+project
lifecycle, owner-only guards, invite→join→role→remove over HTTP.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Rendered-markdown share pages get a small "Shared with BearDrive" footer
linking to the repo — the shell template only, so shared raw HTML is
still served byte-for-byte and the /s/ sandbox CSP is unchanged. All
/s/* responses now sit behind a per-IP token bucket (default 120 req/min
sustained + burst, tunable via "share_rpm" in the web config) so the one
unauthenticated surface of a hub can't be scraped into a free CDN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
The quota counterpart of AuthProvider: CheckWrite(org, addedBytes) runs
before every write path (browser upload via server or presign/commit,
the device-sync store proxy's sign and put), RecordUsage after success,
and CheckSeat before an invite adds a member. Denials surface as 403.
The open-source server ships only UnlimitedQuota and a nil provider
means unlimited — zero behavior change here; a managed deployment
enforces plans by swapping the provider. Unit test asserts the hooks
fire with the correct org and byte counts and that denials block
without recording usage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Every hub project now belongs to exactly one org (file-backed orgs.json,
same load-at-open + atomic-rewrite discipline as the other registries).
Membership (email -> owner|member) gates every per-project route: the
viewer APIs, uploads, history, blobs, shares management, and the device
sync store proxy; /api/projects lists only your orgs' projects, and
project names are now scoped per org. Public share links (/s/) stay
public by design.
Design choices, per the simplest-consistent rule:
- Migration: a pre-org hub sweeps all org-less projects into a "default"
org at startup; ALL existing accounts join it (they could all see every
project before, so anything narrower would lock someone out), oldest
account as owner. Zero manual steps.
- An account in no org that creates a project gets a fresh org named
after itself, so nobody is ever blocked from starting to sync.
- Invites are expiring multi-use links (default 7 days): an owner mints
/#join/<token>, any signed-in account that opens it joins as member.
The web UI shows the org in a sidebar footer (members on click,
Invite button for owners).
bdrive init needed no changes: its connect-existing flow lists projects
through the now-filtered API.
Tests: OrgDB + migration units; a 403/access matrix over every
per-project route; invite mint/redeem flow; and a multi-device syncer
test proving a device holding an org-B token can neither pull org A's
files nor push into its store (sync degrades to Offline, never partial).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Cmd/Ctrl+K opens a palette over the viewer: fuzzy-match any synced file
(word-start and streak-aware scoring, matched letters highlighted),
switch projects, and run context actions (share/history/download of the
open file, project history, upload, sign out). Esc, backdrop click, or
Cmd+K again closes it; arrows + Enter drive it. The overlay CSS now
re-asserts [hidden] since its display:flex otherwise overrides the UA
rule and the palette could never dismiss.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Share links (bdrive share <file>, or the web UI's Share button):
- Mint an unguessable public URL (/s/<token>) anyone can open — no
account. HTML renders as a page, markdown gets a standalone shell,
PDFs open inline; ?download=1 attaches.
- Sandboxed: /s/* responses carry CSP `sandbox allow-scripts` + nosniff
and never see auth cookies, so shared content's scripts run in an
opaque origin and can't touch hub sessions.
- Links serve the file's LATEST synced content and live until revoked;
--expires makes self-destructing ones; --list/--revoke manage them.
Re-sharing a file returns the same link. File-backed shares.json.
- CLI resolves the project by walking up to .bdrive/ from the shared
file, warns when the hub address is private (LAN-only links), and
hints when the file hasn't synced yet.
/beardrive:install (plugin command) — team onboarding driven by Claude:
- Ensures the bdrive binary, signs in (bdrive login), runs bdrive init
(whole folder or a shared subfolder like wiki/).
- Asks before appending a CLAUDE.md section that teaches agents to put
shareable artifacts in the shared folder and mint URLs with bdrive
share; asks before registering project-level hooks in
.claude/settings.json: blocking pull at UserPromptSubmit, async push
on PostToolUse Write|Edit — teammates sync with or without the plugin.
- Fix: the plugin hook script still checked for the old `.bdrive` file
and was a silent no-op since the directory change; now checks -d.
Tests: share creation gating, public access + sandbox headers, dedupe,
latest-content semantics, revoke, expiry, markdown/download variants,
list filtering, registry persistence. Docs updated (README sharing +
Claude Code sections, SKILL.md, CLAUDE.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Authentication (previous phase, now landed together with its follow-ups):
- Email+password+name accounts behind an AuthProvider interface; the OSS
server ships BuiltinAuth only (file-backed auth.json: bcrypt password
hashes + SHA-256 token digests, plaintext never stored; server-owned
/auth/* pages; managed deployments can swap in another provider).
- bdrive login: loopback-callback browser flow (sign-up on the page, the
terminal finishes itself) with a device-code fallback for headless
machines; long-lived revocable device tokens in settings.json.
- Password reset via plain SMTP (stdlib) with a log-link fallback when no
SMTP is configured.
Move-proof projects:
- .bdrive is now a directory; config.json carries a stable mount id.
The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed
by that id — never the folder path — so renames/moves are free.
- The daemon re-reads the project config each tick and exits cleanly
(propagating nothing) when its folder vanishes; the registry self-heals
and the next bdrive command at the new location resumes with zero
spurious changes.
bdrive init is the front door (mnt/umnt removed; bdrive stop pauses):
- Interactive on a TTY (create new / connect existing project from the
server's list; whole folder / shared subfolder via the include list),
full flag bypass (--name/--project/--shared/--yes), never prompts
without a TTY. Runs the login flow first when there is no session.
Default server: beardrive.ai (config.DefaultServer).
Web history (revert-ready):
- Hubs now always require auth; journal ops carry the signed-in account
(user/user_name) alongside the git/OS fallback author.
- File-backed device registry: per-device name, OS, account, and the
public IP the server observed, joined into history at read time.
- GET /api/p/<id>/history?path=|prefix= (newest first) and
GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained
forever, so the next phase's revert is re-putting an old blob.
- UI: History button (file versions or project feed), per-folder history
shortcut, view/download of any past version.
Tests: auth flows (callback, device-code, reset single-use, persistence,
gating), history API + device registry, folder-move survival, registry
self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
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
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
BearDrive is the product and repo brand (open source + future managed
cloud); sfs stays the CLI command, and .sfs/.sfsignore/~/.sfs conventions
are unchanged, so nothing breaks. Brew formula becomes
runbear-io/tap/beardrive (still installs the sfs binary).
Not included here (need the GitHub repo rename first): the Go module
path and repo URLs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
- 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