Skip to content

Cloud Team Collections (S3 + Supabase backend)#8048

Closed
JohnThomson wants to merge 220 commits into
masterfrom
cloud-collections
Closed

Cloud Team Collections (S3 + Supabase backend)#8048
JohnThomson wants to merge 220 commits into
masterfrom
cloud-collections

Conversation

@JohnThomson

@JohnThomson JohnThomson commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Cloud-hosted Team Collections: a new TeamCollection backend storing book content in S3 and coordination state (locks, versions, membership) in Supabase (Postgres RLS + edge functions), with sign-in, sharing/membership UI, progressive join, automatic remote-update application, account-switch handling, and a Playwright-over-CDP E2E harness driving real two-instance Bloom sessions against a local dev stack.

Design docs: Design/CloudTeamCollections/ (architecture, CONTRACTS.md, GOING-LIVE.md). Durable work-state and outstanding items: Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md.

Known open item at draft time: item 9's same-machine lock takeover fires across separate collection folders (batch doc OUTSTANDING BUGS #0, decision pending) - e2e-4 fails its final assertion on this; all other E2E singles and the full C# suite are green.

🤖 Generated with Claude Code

Devin review


This change is Reviewable

hatton and others added 30 commits July 7, 2026 22:28
Design for replacing the Dropbox-folder Team Collections backend with a
transactional S3 + Supabase backend ("Cloud Team Collections"):

- Design/CloudTeamCollections.md: requirements and decisions (approved-accounts
  sharing, Send/Receive modal model, versioned-mirror persistence with no
  content retention, .bloomSource Lost & Found recovery with server-side
  incident events, coexistence with folder TCs, no auto-migration in v1),
  architecture, base-class seams, UI changes, test plan, roadmap.
- Design/CloudTeamCollections/CONTRACTS.md: frozen v1 API contracts (RPCs,
  edge functions with exact request/response shapes, link-file format,
  S3 layout, realtime message shape).
- Design/CloudTeamCollections/IMPLEMENTATION.md: master checklist - five
  build waves, branching strategy, shared-file schedule, merge protocol.
- Design/CloudTeamCollections/tasks/00..10: per-task child plans (goal,
  dependencies, file ownership, steps, acceptance criteria).

Full interactive review document (diagrams, ~135-case edge-case matrix,
auth decision brief): https://claude.ai/code/artifact/14157962-d8e4-4c3f-afa7-34c49ed29e1a

Design only - no product code changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All development and testing now targets a fully local stack (local
Supabase via CLI, MinIO as a filesystem-backed S3 substitute, local
GoTrue email/password auth that accepts any login) so implementation
can start before the real S3 bucket, hosted Supabase, or
Firebase/BloomLibrary auth changes exist. Adds task 11
(local-dev-stack), makes the auth Option A/B/C decision non-blocking,
and tracks real-infrastructure cutover as a deferred config-swap list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ms, capability flags, feature flag)

- New TeamCollectionLink.cs: parses/writes folder-path and cloud://sil.bloom/collection/<id>
  forms of TeamCollectionLink.txt; throws InvalidTeamCollectionLinkException on bad content
- TeamCollectionManager: replace 3x hardcoded `new FolderTeamCollection(...)` with a
  CreateTeamCollectionFromLink factory; add ConnectToCloudCollection (throws NotImplemented);
  add ConnectToCloudCollection to ITeamCollectionManager interface
- TeamCollection: add TryLockInRepo/UnlockInRepo virtual seams; route AttemptLock,
  UnlockBook, ForceUnlock through them; folder default keeps exact read-modify-write behavior
- TeamCollection: add SupportsVersionHistory, SupportsSharingUi, RequiresSignIn capability
  flags (all false on the folder backend)
- ExperimentalFeatures: add kCloudTeamCollections token
- FeatureRegistry: add CloudTeamCollection feature entry (LocalCommunity tier, gated behind
  kCloudTeamCollections experimental flag)
- New TeamCollectionLinkTests: 24 tests covering parse/write round-trips, folder form, cloud
  form, garbage content, missing file, both-forms-present error
- New Design/CloudTeamCollections/notes/write-book-status-audit.md: 7-caller audit of
  WriteBookStatus for task 05 diff-dispatch design
- Design/CloudTeamCollections/tasks/00-enablers.md: tick all 6 step checkboxes

All 208 existing TeamCollection tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creates the complete Supabase server-side implementation for Cloud Team
Collections (Wave 1, task 01-server-schema):

supabase/config.toml
  Standard Supabase CLI local-dev layout (as supabase init would produce).
  auth.email.enable_confirmations = false for local GoTrue auto-confirm
  (task 11 dev stack). Schema 'tc' exposed via PostgREST extra_search_path.

supabase/migrations/20260706000001_tc_schema.sql
  tc schema + all tables: collections, members (with last-admin guard
  trigger and NULLS NOT DISTINCT unique constraint for claimed users),
  books (with partial unique index on live lower(NFC(name)), NFC-normalize
  trigger, lock columns, deleted_at tombstone), versions, version_files,
  collection_file_groups, collection_group_files, color_palette_entries,
  events (with realtime pg_notify broadcast trigger; type values mirroring
  C# BookHistoryEventType 0-9, plus incident type 100=WorkPreservedLocally),
  checkin_transactions.

  Helper functions: tc.jwt_email_verified() — the ONE place that reads
  email verification from the JWT (Firebase email_verified claim OR local
  GoTrue role=authenticated); tc.current_user_id(); tc.current_user_email().
  User ids are TEXT throughout (Firebase UIDs ~28 chars; local-GoTrue UUIDs).
  All timestamps via now(). NFC-normalize applied to names and paths via triggers.

supabase/migrations/20260706000002_tc_rls.sql
  RLS enabled on all tables. Policies: members SELECT/INSERT/UPDATE/DELETE
  gated on is_member/is_admin helpers; books/versions SELECT only (no direct
  writes); events SELECT + INSERT (own events); color_palette_entries SELECT
  + INSERT. is_member() and is_admin() SECURITY DEFINER helpers. GRANTs for
  authenticated role; anon role gets nothing.

supabase/migrations/20260706000003_tc_rpcs.sql
  All CONTRACTS.md RPCs as SECURITY DEFINER Postgres functions:
  create_collection, my_collections, claim_memberships (guarded by
  jwt_email_verified()), get_collection_state (full/delta), get_changes
  (cursor-based), checkout_book (race-free conditional UPDATE), unlock_book,
  force_unlock (audited, emits type=5), delete_book (lock required, emits
  type=8), undelete_book (name-uniqueness enforced), rename_check (advisory),
  members_list/add/remove/set_role (remove force-unlocks + emits events;
  last-admin guard via trigger), add_palette_colors (insert-on-conflict-do-
  nothing), log_event. EXECUTE grants to authenticated role.

supabase/tests/01_tc_schema_test.sql
  pgTAP suite (authored, unrun — Docker not available on this machine).
  Covers all acceptance criteria: schema/table existence, jwt_email_verified
  (Firebase + GoTrue paths), RLS matrix (non-member blocked), claim_memberships
  requires verified email, checkout concurrency (Alice wins / Bob loses / one
  event emitted), last-admin guard (delete + demote), get_changes cursor,
  tombstone/undelete, live-name uniqueness (tombstoned names reusable).

Design/CloudTeamCollections/tasks/01-server-schema.md
  Tick all Step checkboxes; note Acceptance tests authored but unrun.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delivers all authored artifacts for Cloud TC task 11 (local dev stack).
Nothing in this commit touches Bloom application code.

Files added under server/dev/:
- docker-compose.yml: MinIO SNSD (object versioning ON) + minio-init job
  that creates bloom-teams-local bucket with versioning + 7-day lifecycle.
  Console at :9001, S3 API at :9000; fixed dev root credentials.
- .gitignore: excludes minio-data/ from version control.
- seed.sql: three dev users (admin/alice/bob @dev.local, password BloomDev123!)
  inserted directly into auth.users + auth.identities with email_verified=true
  so tc.jwt_email_verified() (task 01) is satisfied immediately.
- config.auth.toml.snippet: auth settings (enable_signup=true,
  enable_confirmations=false) for orchestrator to fold into supabase/config.toml
  at merge (task 01 owns that file per coordination rule).
- README.md: full bring-up/teardown/reset instructions, all BLOOM_CLOUDTC_*
  env var definitions, two-instances-on-one-machine recipe, prerequisite
  install commands for Docker Desktop / Supabase CLI / Deno on Windows.
- DEV-CREDENTIALS.md: edge-function dev credential mode spec - when in dev
  mode functions return static MinIO root credentials in the IDENTICAL STS
  response shape (accessKeyId/secretAccessKey/sessionToken/expiration), so
  the C# client cannot tell the difference and CONTRACTS.md stays frozen.
  Documents all known MinIO/AWS parity gaps.
- parity-check/ParityCheck.csproj + Program.cs: standalone .NET 10 console
  project (NOT in Bloom.sln) that verifies MinIO supports (a) PUT with
  x-amz-checksum-sha256, (b) server-side checksum readback, (c) version-id
  capture on PUT, (d) GET by (key, versionId). BUILD VERIFIED (dotnet build
  clean). UNRUN - requires Docker Desktop.
- smoke.ps1: stack smoke test (GoTrue signup, MinIO versioned PUT via
  parity-check, download-start edge function call). AUTHORED-UNRUN.

Also adds the Design/CloudTeamCollections/ files (from cloudTC branch)
that provide context for this and all Wave 1 tasks.

Task 11 checklist: all authored items ticked; smoke.ps1 and parity-check
runtime marked AUTHORED-BUT-UNRUN pending Docker Desktop installation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewed: lock seams preserve folder behavior verbatim; 208/208
TeamCollection tests pass against fresh binaries (verified Bloom.dll
in test output contains the new code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Authored-but-unrun (Docker not yet installed); review fixes follow
in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- checkout_book: GET DIAGNOSTICS ROW_COUNT must land in an integer, not
  a boolean (boolean > integer has no operator; every checkout would
  have failed at runtime).
- pgTAP: claim_memberships unverified-email test now expects ERRCODE
  28000, matching the function.
- Documented that pg_notify does not reach Supabase Realtime; the
  realtime.send swap is deferred to the wave-4 realtime work per the
  polling-first plan.
- config.toml: wire server/dev/seed.sql (task 11 dev users) via [db.seed].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ity tools

Authored; runtime verification pending Docker Desktop install.
Seed-hash fix follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-computed hash in task 11 did not match the documented password
(verified with bcryptjs: compareSync returned false). Replaced all
three copies with a generated, self-verified bcrypt(cost=10) hash for
BloomDev123!.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contract clarifications (wire format only): RPC JSON keys use the p_
parameter prefix; tc-schema RPC calls need the Content-Profile: tc
header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live run of everything tasks 01 + 11 authored, on Podman 5.8.3
(rootful WSL machine, Docker-compat pipe) with Supabase CLI 2.109.0:

- pgTAP 42/42 GREEN. Fixes: plan count (60 -> 42 actual assertions);
  RLS-matrix assertions now run under SET LOCAL ROLE authenticated,
  because the suite's postgres superuser bypasses row security and made
  those tests pass vacuously.
- S3 parity spike 4/4 PASS (sha256 checksum PUT + server-side readback,
  version-id capture, GET by version). Fix: use BasicAWSCredentials --
  MinIO validates session tokens, so the fabricated devmode token was
  rejected. DEV-CREDENTIALS.md spec corrected: dev-mode edge functions
  must mint real temporary creds via MinIO AssumeRole.
- smoke.ps1 GREEN (signup + MinIO + functions-endpoint reachability).
  Fixes: PS 5.1 parse errors (invalid ${()} interpolation; em-dashes in
  a BOM-less UTF-8 file decode as CP1252 smart quotes), native-stderr
  handling, and JSON output from newer supabase status.
- MinIO lifecycle now set via mc ilm rule add (JSON import was mangled
  by compose quoting); idempotent via shell case (mc image has no grep).
- Committed .gitkeep in every bind-mounted dir (minio-data, supabase
  snippets/functions): Podman does not auto-create missing bind-mount
  directories. Added supabase/.gitignore for CLI state dirs.
- README prerequisites rewritten around the verified Podman recipe
  (rootful requirement, compat pipe, npm-based CLI installs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, CloudCollectionClient)

Implements the dev-provider-first design from Design/CloudTeamCollections/tasks/03-auth.md:
- CloudEnvironment resolves Supabase/S3/auth config from BLOOM_CLOUDTC_* env vars over
  compiled defaults that point at the local dev stack.
- CloudAuth is a provider-agnostic session core (token store, proactive refresh at ~80% TTL,
  refresh-on-401, sign-out, account-switch detection, env-override-wins-over-stored-session
  startup path) plus CloudLoginState groundwork for the future sharing/loginState endpoint.
- DevCloudAuthProvider signs in against local GoTrue (sign-up-then-sign-in for unknown
  emails); RealCloudAuthProvider is an inert "not yet available" stub pending the Option
  A/B/C decision.
- CloudCollectionClient is a RestSharp client (modeled on BloomLibraryBookApiClient) for
  tc-schema RPCs and edge functions: bearer injection, Content-Profile: tc header, one
  refresh-and-retry on 401 then abort with NotSignedIn, and typed CloudErrorCode mapping
  for the CONTRACTS.md error shapes (LockHeldByOther, ClientOutOfDate, etc.).

Live-verified against the local Supabase stack: dev sign-in/sign-up, RPC calls with the
Content-Profile: tc header, and the {code,message} error shape PostgREST returns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Durable-state protocol: each task lives on its own branch with
per-step commits, checkbox ticks, and a progress log in the task file,
so work survives AI-session interruption and any future session can
resume from Design/CloudTeamCollections/orchestration/RESUME.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds CloudAuthTests, CloudCollectionClientTests, CloudEnvironmentTests under
src/BloomTests/TeamCollection/Cloud/, covering every item in 03-auth.md's Acceptance
section against fakes (no network): proactive refresh timer, refresh-on-401 success and
clean-abort-on-failure ("please sign in"), account-switch detection, env-override winning
over a stored session, bearer/apikey/Content-Profile header injection, and CloudErrorCode
mapping for every CONTRACTS.md error shape (LockHeldByOther, BaseVersionSuperseded,
NameConflict, MissingOrBadUploads, VersionConflict, ClientOutOfDate, plus the generic
Postgres {code,message} shape live-verified earlier).

Also adds one [Explicit] test, LiveDevProvider_TwoUsersSignInConcurrently_HoldDistinctSessions,
that exercises the real DevCloudAuthProvider against the running local Supabase stack (no
mocks) and confirms two dev users get independent, independently-refreshable sessions -
verified passing against the live stack in this session.

CloudCollectionClient gained a small test seam (IRestExecutor/RestSharpExecutor,
SetRestClientForTests) so the header-injection and error-mapping logic can be exercised
without a live server; production behavior (a real RestSharp RestClient) is unchanged.

FullyQualifiedName~Cloud: 46/46 green. FullyQualifiedName~TeamCollection regression
filter: 244/244 green (no folder-TC behavior change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…view)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…all 6 edge functions

New migration 20260706000004_tc_checkin_txn_functions.sql adds the SECURITY DEFINER
transaction functions (checkin_start_tx/finish_tx/abort_tx, collection_files_start_tx/
finish_tx, download_start_check) that back the edge functions, plus a checkin_transactions
table extension (proposed_files/checksum/result_version_id) and a new
collection_file_transactions table. Uses PostgREST's PT### errcode convention so SQL
exceptions map straight to HTTP status with a JSON body the edge functions unwrap.

Authors all 6 edge functions under supabase/functions/ (checkin-start, checkin-finish,
checkin-abort, download-start, collection-files-start, collection-files-finish) plus
_shared/ helpers (env config incl. dev/prod S3 credential seam, PostgREST RPC client
forwarding the caller's own JWT, error/response helpers). Applied cleanly via
`supabase db reset --local`; `deno check` passes on all functions. Not yet exercised
against the live stack — see the task file's Progress log for next steps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s found

Ran a full live-integration check against the real local stack (Supabase +
MinIO via `supabase functions serve --env-file server/dev/functions.env`)
covering every item in the task's acceptance checklist except a real 48h
expiry wait: happy-path checkin-start -> MinIO AssumeRole PUT -> checkin-finish
-> download-start round-trip, idempotent finish retry, resume (new-book and
existing-book), lock-held, base-version-superseded, checksum failure
(MissingOrBadUploads), new-book invisibility until commit, and the
collection-files two-phase commit + VersionConflict path. 26/26 checks passed
after fixing:

1. `host.containers.internal`/`host.docker.internal` DNS-resolves fine but the
   traffic HANGS INDEFINITELY for any Deno/edge-runtime HTTP call through
   Podman's gvproxy host-gateway hop on this machine (verified with raw
   Deno.connect, native fetch, and the AWS SDK; plain curl over the identical
   URL succeeds instantly) -- likely what stalled the previous interrupted
   attempt at this task. Fixed by joining MinIO to the Supabase CLI's own
   project Docker network (server/dev/docker-compose.yml) and addressing it by
   container name (http://bloom-minio:9000) instead of the host gateway.
2. supabase/config.toml's generated `[edge_runtime].policy = "oneshot"`
   re-transpiles the whole module graph -- including the heavy
   npm:@aws-sdk/client-s3+client-sts imports -- on every request, reliably
   exceeding the ~10s worker-boot timeout. Switched to `per_worker`.
3. New-book checkin-start resume was unreachable: the SQL always took the
   insert-a-new-row path when bookId was null, so a client resuming an
   interrupted new-book Send (which has no bookId to send back, by design)
   always tripped its own prior row's instance-id-conflict check. Fixed to
   recognize "already exists, not yet committed, still locked to me" as a
   resume.
4. get_collection_state's full-snapshot branch (task 01's file) leaked
   uncommitted new books to every collection member; the delta branch was
   already safe. Fixed by excluding current_version_id IS NULL rows unless
   locked to the caller.

Both networking/config fixes and both SQL fixes are documented in
server/dev/README.md's new "Known gotchas" section and inline SQL comments.
Remaining for this task: Deno unit tests per function, the transaction-
lifetime-vs-noncurrent-expiry invariant test, and server/provision-aws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each index.ts previously passed its request handler straight into
serveJsonPost(...), which calls Deno.serve at module scope -- meaning simply
importing the module (e.g. from a Deno test) would start a real server as a
side effect, with no way to invoke the handler logic directly or mock its
Request. Refactored all 6 functions to `export const handler = ...` plus
`if (import.meta.main) { serveJsonPost(handler); }`, so a test can import just
the handler and call it with a mocked Request/mocked fetch, while
`supabase functions serve` (which runs index.ts as the entry point) is
unaffected. Verified empirically against the running local stack that the
real supabase-edge-runtime does set import.meta.main = true for the served
module -- re-ran the full 26-check live-integration suite post-refactor,
still 26/26. `deno check` clean on all 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds hermetic, mocked-dependency Deno tests (no live stack required) to
complement the live-integration spike already recorded in the Progress log:

- _shared/test_support.ts: setTestEnv/mockRequest/withMockFetch/
  routedFetchStub/callHandler test helpers. callHandler mirrors
  serveJsonPost's HttpError-to-Response translation so tests can call each
  handler directly.
- _shared/s3.test.ts (8 tests): hexToBase64, getScopedCredentials (dev-mode
  MinIO AssumeRole shape, no session Policy in dev, missing-credentials
  failure), verifyUploadedObject (checksum match/mismatch/missing-object/
  missing-VersionId), writeManifestBackup (never throws). S3/STS mocked via
  aws-sdk-client-mock.
- One index.test.ts per function (22 tests total): happy path, required-field
  validation, and RPC error passthrough (409/404/426), asserting S3
  credentials are never issued on an error path. PostgREST calls mocked via
  a fetch stub.
- _shared/invariants.test.ts (2 tests): re-parses the actual migration/
  compose source text (not hardcoded twice) to assert the 48h transaction-
  expiry interval is consistent everywhere and strictly less than the dev
  MinIO noncurrent-version-expiry floor (7d).

32/32 pass: `deno test --allow-net --allow-env --allow-sys --allow-read supabase/functions/`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the last remaining step of task 02. PowerShell, matching the only
existing server/ script convention (server/dev/smoke.ps1). Idempotent
(checks-then-creates everywhere): S3 buckets bloom-teams-production/sandbox
with versioning ON, all public access blocked, and lifecycle rules
(noncurrent-version-expiry 7d + abort-incomplete-multipart-upload 7d under
tc/, mirroring server/dev/docker-compose.yml's MinIO init job); an IAM role
bloom-teams-broker whose permission policy is the ceiling the edge functions'
per-request AssumeRole session Policy narrows from; an assume-only IAM user
bloom-teams-broker-caller (sole permission: sts:AssumeRole on that role) that
becomes AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY; an admin IAM user
bloom-teams-admin with direct S3 permissions backing adminS3Client(). Ends by
printing the exact `supabase secrets set` command block for every env var
_shared/env.ts's prodBrokerConfig/adminS3Credentials read.

Per the task file, this is authored and reviewed only -- no AWS account was
available to run it against (verified only via PowerShell's AST parser for
syntax correctness). This completes all 5 steps + both acceptance items in
Design/CloudTeamCollections/tasks/02-edge-functions.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Committed by orchestrator on resume to preserve in-flight work; the
resumed agent continues from the task-file progress log. Pre-commit
hook unavailable in worktree (husky-run); prettier pass owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add useIsCloudTeamCollectionsExperimentalFeatureEnabled() to sharingApi.ts,
reading app/enabledExperimentalFeatures for the cloud-team-collections token
(matches ExperimentalFeatures.kCloudTeamCollections in C#). Wire a new "Share
this collection on the Bloom sharing server (experimental)" button into
TeamCollectionSettingsPanel.tsx's not-yet-a-TC branch; disabled with an
explanatory tooltip until the flag is on, otherwise posts
teamCollection/showCreateCloudTeamCollectionDialog (Wave-3 endpoint, mocked
for now). Also add createCloudTeamCollection() for the next step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add CreateCloudTeamCollectionBody (presentational; sign-in ->
immutable-name-acknowledgement -> initial Send progress -> done/error, all
gated on props) and CreateCloudTeamCollectionDialog (container) to
CreateTeamCollection.tsx. Dev-auth mode shows a plain email/password form;
cloud mode shows a placeholder "Sign in with your Bloom account" button
(real BloomLibrary browser flow lands in a later task). No folder chooser,
no Dropbox checkboxes, no restart.

Fix createCloudTeamCollection() to use postJson instead of post, since post()
is fire-and-forget and does not return a promise.

Add CreateCloudTeamCollection.test.tsx (10 tests) covering the step gating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add MyCloudCollectionsSection.tsx: a presentational sidebar for the
collection chooser listing the signed-in user's cloud Team Collections
(claimed or not), with a signed-out sign-in prompt, loading, and empty
states, plus a per-row pull-down button. Wire it into CollectionChooser.tsx
next to the existing recent-collections grid, backed by
useSharingLoginState/useMyCloudCollections/pullDownCollection.

Add MyCloudCollectionsSection.test.tsx (4 tests).

The pull-down-join dialog (JoinCloudCollectionDialog, six folder-TC
scenarios + NotSignedIn + ApprovalRemoved) is the remaining half of this
step and follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add JoinCloudCollectionDialog.tsx: eight-state pull-down-join dialog,
structurally mirroring the folder-TC JoinTeamCollectionDialog's six
scenarios (create/merge/open/switch/conflict/incomplete-local-copy) plus two
states unique to cloud collections: NotSignedIn (action button becomes
"Sign In") and ApprovalRemoved (action disabled, explains the user isn't on
the approved list). The join action calls pullDownCollection from
sharingApi.ts.

Add JoinCloudCollectionDialog.test.tsx (8 tests) covering all eight states.

Not yet wired into CollectionChooser.tsx's onPullDown: picking the right
state requires backend six-scenario matching logic that doesn't exist until
a later task, so the chooser keeps calling pullDownCollection directly for
now (Wave-1 shell, mocked endpoints).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add cloudAccountEmail to IRegistrationContentsProps/IRegistrationDialogProps,
threaded through RegistrationDialog and both launchers. When set, the
registration email field is force-synced to it and always locked (identity
= the signed-in Bloom account, not free text), with a distinct label/note
explaining why.

Wire this into CreateCloudTeamCollectionDialog: startSend now checks
registration/userInfo first (mirroring the folder dialog's tryToCreate) and
shows the registration dialog with cloudAccountEmail set if not yet
registered, before proceeding to the actual send.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Orchestrator-preserved in-flight work; 29 component tests were passing
at interruption. Remaining per the agent: lint pass, progress log,
final commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ress log

- Ran yarn prettier --write on all 16 files this branch touched; already
  clean, so the two WIP commits that skipped the pre-commit hook needed no
  reformatting.
- Re-ran the component test suite in single-run mode (yarn vitest run
  --pool=threads) covering SharingPanel, CreateCloudTeamCollection,
  MyCloudCollectionsSection, and JoinCloudCollectionDialog: 4 files, 29
  tests, all passing.
- Ran yarn lint project-wide: 0 errors; confirmed the handful of warnings
  that land in files this branch touched (CreateTeamCollection.tsx,
  vitest.setup.ts) sit on pre-existing lines outside this branch's diff.
- Verified all new user-facing strings from this task go through XLF
  (DistFiles/localization/en only) per .github/skills/xlf-strings/SKILL.md:
  correct priority file, translate="no", ID notes, and translator-context
  notes where needed; no existing entries changed.
- Ticked the remaining checkbox and closed out Design/CloudTeamCollections/
  tasks/07-ui-setup.md's progress log; task 07 (Wave-1 shells) is complete.
…hestrator review)

MyCloudCollectionsSection rendered (and queried) for every user; the
plan keeps all cloud UI behind the cloud-team-collections experimental
flag until GA. With the flag off, the chooser renders exactly its
pre-cloud content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JohnThomson and others added 10 commits July 10, 2026 14:18
…point

The endpoint was registered per-project (TeamCollectionApi), but callers
legitimately probe it when no project is open: the E2E harness polls it as
a readiness signal (one such probe landed before project registration in
Bob's e2e-3 relaunch log, 10 Jul 10:11:41 - BEFORE any WebView2 existed),
and a closing collection tab can get a late request in while the chooser is
on screen (John's on-screen sighting). Every such call raised the
'Cannot Find API Endpoint' NonFatalProblem toast.

Moved the registration to SharingApi (application level, same file that
already reaches the current project via TeamCollectionApi.TheOneInstance
for members/history/etc.), answering all-false whenever no project/TC is
current - which is the truthful answer at chooser time.

TeamCollectionApiCloudTests' two capabilities tests now register SharingApi
on the test server as well, keeping end-to-end coverage of the endpoint;
fixture green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…claimed membership + modal report)

Root cause, proven by a live dotnet-stack dump of the hung instance plus a
rerun with better reporting: opening a collection under an APPROVED account
that never ran the join flow (batch item 9's shared-computer scenario)
passed CheckConnection's member check - my_collections matches by EMAIL,
approved-or-claimed - and then threw not_a_member from get_collection_state
during the constructor's first sync, because every data RPC's RLS gate
matches by user_id and only the join flow (CloudJoinFlow) ever called
claim_memberships. The generic catch in TeamCollectionManager's constructor
then showed a MODAL MessageBox, which in --automation mode has no human to
dismiss it: the instance sat blocked before any window or server existed,
with completely empty stdout (e2e-10's bob-takeover 90s-timeout signature).

Two fixes:
- CloudTeamCollection.CheckConnection now calls ClaimMemberships (idempotent,
  once per session) as soon as membership is confirmed, so user_id-gated
  RPCs work no matter which account originally joined the local folder.
- NonFatalProblem.Report in --automation mode writes the problem + stack to
  stdout (BLOOM_AUTOMATION_NONFATAL_PROBLEM, the harness's per-instance log)
  and returns instead of blocking on a MessageBox, mirroring the existing
  RunningInConsoleMode guard. Any future startup-path report is now a
  readable log line instead of a silent hang.

New test: CheckConnection_SignedInAndAMember_ClaimsMembershipsOncePerSession.
C# cloud filter 418/418 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Defect 3's fix made teamCollection/capabilities an application-level
endpoint, so it now answers (truthfully all-false) while a project is still
opening instead of erroring - and BLOOM_AUTOMATION_READY fires when the
server starts listening, which can be before the Team Collection connects.
The two post-relaunch checks that single-shot-asserted supportsSharingUi
=== true (twoInstanceSetup's bob-joined and e2e-2's equivalent) now use the
same 20s expect.poll every other true-asserting site already used. The
false-asserting pre-create checks (e2e-1, e2e-7, join-auto-open) are
unchanged: all-false is exactly what they expect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-broad lock skip), outstanding-bugs list

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d for

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wn-locked books

Two bugs diagnosed from e2e-4's fully-logged failure (see the batch doc's
OUTSTANDING BUGS #1):

- CloudBookTransfer staged every download into the FIXED temp path
  %TEMP%\BloomCloudTCDownload, shared by every download in every Bloom
  process; TemporaryFolder.Dispose deletes the whole folder, so two
  concurrent instances (shared-machine TC use, and every two-instance E2E
  scenario) clobbered each other mid-copy ('Could not find file ...
  A5 Portrait.htm'). The staging folder name is now unique per call.

- QueueMissingRepoBooksForBackgroundDownload skipped books locked by
  ANYONE, so a teammate checking a book out seconds after one transient
  download failure left it missing for the whole lock duration. The skip
  now only covers books locked by the CURRENT user (the genuine
  rename-mid-checkin edge); a teammate's lock does not block downloading
  committed content (same semantics as Receive).

Tests: locked-by-me still skips; locked-by-teammate now downloads.
Cloud filter 420/420 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bug documented for John's decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ock skip

Two adjacent holes found by the preflight light-review pass over today's
defect fixes:

- The claim_memberships guard was a once-per-instance bool; an in-session
  sign-out + sign-in as a different approved member (nothing disposes the
  CloudTeamCollection on CloudAuth.AccountSwitched) would skip claiming for
  the new account and resurrect the not_a_member startup failure. Now keyed
  by email: each distinct account claims once.

- QueueMissingRepoBooksForBackgroundDownload skipped books locked by the
  current user on ANY machine; the rename-mid-checkin edge it protects is
  machine-local ('checked out here' everywhere else in the file means
  lockedBy AND lockedWhere match), and SyncAtStartup fetches such a book on
  restart anyway. The skip now also requires lockedWhere == CurrentMachine,
  so a book you checked out on another computer still self-heals here.

Also: batch doc updated with e2e-5/e2e-8 PASS results (transient-infra
theory confirmed) and single-spec scoreboard.

New tests: CheckConnection_AccountSwitchMidSession_ClaimsAgainForTheNewAccount,
QueueMissingRepoBooks_BookLockedByCurrentUserOnAnotherMachine_StillDownloadsIt.
Cloud filter 422/422 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…b half needs gh auth)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JohnThomson and others added 3 commits July 10, 2026 16:18
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (SQUASH-PLAN.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…meouts, resume runbook); bug-0 option-a sketch preserved

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JohnThomson

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

(Claude Fable 5 agent: bypassing the 100-file limit per the bot's own instructions — this draft PR is the full Cloud Team Collections feature, 237 files.)

…losed (Devin diff-too-large, Greptile bypass triggered, CodeRabbit not installed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This re-review (triggered after commit b93d0c9) verifies that all three findings from the prior pass are correctly resolved. No new blocking issues were found.

  • S3 prefix fix (checkin-start/index.ts): The edge function now reads the DB-canonical instance_id back from the books row via selectTcRow and scopes the STS credentials to that value, never the caller-supplied bookInstanceId. A dedicated Deno test pins the mismatch case.
  • Reap counter fix (20260711000001_tc_reap_counter_fix.sql): reap_expired_checkin_transactions now accumulates the collection-file sweep's ROW_COUNT into a separate v_updated variable and adds it to v_count.
  • Takeover error codes (20260711000002_tc_takeover_error_codes.sql): checkout_book_takeover now raises PT404/PT403 with {\"error\":\"...\"} JSON bodies; the pgTAP test for test 4a was updated to assert PT403.

Important Files Changed

Filename Overview
supabase/functions/checkin-start/index.ts S3 prefix now scoped to DB-canonical instance_id read back from books table; fixes the prior finding where caller-supplied bookInstanceId could steer write credentials to an arbitrary prefix.
supabase/functions/checkin-start/index.test.ts New test pins the mismatch case (caller sends one instance id, DB has another); all error passthrough paths also covered.
supabase/migrations/20260711000001_tc_reap_counter_fix.sql Fixes reap_expired_checkin_transactions counter bug — introduces v_updated so the collection-file sweep ROW_COUNT no longer clobbers the book-sweep accumulation.
supabase/migrations/20260711000002_tc_takeover_error_codes.sql Re-creates checkout_book_takeover with PT404/PT403 passthrough codes and JSON error bodies, matching the schema-wide convention; takeover logic is untouched.
supabase/tests/02_tc_checkout_takeover_test.sql Test 4a updated to assert PT403 (was 42501), matching the fixed error code; full takeover scenario coverage included.
src/BloomExe/TeamCollection/Cloud/CloudTeamCollection.cs New cloud-backed TeamCollection implementation; account-switch takeover wired correctly before check-in when the repo shows a same-machine/different-account lock.
supabase/functions/_shared/rpc.ts callTcRpc and selectTcRow both forward the caller's own JWT; parsePostgrestErrorBody unwraps the PT### JSON message convention before surfacing errors.

Reviews (2): Last reviewed commit: "Batch log: full-matrix attempt invalid (..." | Re-trigger Greptile

Comment thread supabase/functions/checkin-start/index.ts Outdated
Comment thread supabase/migrations/20260706000004_tc_checkin_txn_functions.sql
Comment thread supabase/migrations/20260709000007_tc_checkout_takeover.sql
JohnThomson and others added 2 commits July 10, 2026 20:04
E2E: since progressive join (batch item 7), books new to an instance arrive
via the background download queue AFTER a sync poll returns. e2e-6 read
Bob's v1 baseline immediately after the poll (failed standalone; the file
appeared on disk moments later) - now an expect.poll with the harness's
90s convention. The two 20s ceilings on queue-driven arrivals (e2e-6 v2,
e2e-9 first test) also raised to 90s. e2e-3/6/9 all green standalone.

Greptile (via the 100-file-limit bypass on PR 8048) found:
- P1 security: checkin-start scoped S3 write credentials to the
  caller-supplied bookInstanceId, which checkin_start_tx never validates
  for existing books - any member could obtain write creds for any book
  prefix in their collection. Now reads the DB-canonical instance_id back
  (same pattern as checkin-finish); new deno test pins that a mismatched
  client value cannot steer the prefix.
- P2: reap_expired_checkin_transactions returned only the collection-file
  sweep count (GET DIAGNOSTICS clobbered the loop total) - fixed in new
  migration 20260711000001.
- P2: checkout_book_takeover raised P0002/42501 bare strings instead of
  the schema-wide PT404/PT403 JSON convention (C# client would map both
  to CloudErrorCode.Unknown) - fixed in new migration 20260711000002,
  logic untouched; pgTAP 4a expectation updated to PT403.
- Style: JoinCloudCollectionDialog.tsx nested ternaries -> if/else chains.

Suites: pgTAP 55/55, deno 33/33, dialog vitest 12/12, lint+prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… CDP failures); rerun queued on unlock

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JohnThomson

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

(Claude Fable 5 agent: re-triggering after b93d0c9, which fixes all three of your findings — please verify.)

…ONTRACTS v1.5)

Item 9's same-machine takeover gated only on machine name, so any
same-machine account could silently steal any same-machine lock - even
across two separate local copies of the collection (e2e-4's scenario, and
any shared lab machine). John's ruling: editing/takeover of a checkout is
only legitimate in the local copy where the book is checked out.

Server (migration 20260711000003, additive):
- tc.books.locked_seat: client-computed stable hash of the local
  collection folder path (never the raw path), recorded by
  checkout_book/checkout_book_takeover (new optional p_seat).
- Takeover now requires machine AND seat match; a NULL stored seat never
  matches (fail-safe for pre-seat locks and checkin_start_tx's
  take-if-free path, which records no seat).
- A BEFORE UPDATE trigger clears locked_seat whenever locked_by clears,
  covering every unlock path without recreating those functions.
- get_collection_state/get_changes book rows carry locked_seat.

Client:
- CloudTeamCollection.SeatId (first 16 hex of SHA256 of the normalized
  folder path); passed on checkout and takeover.
- IsEditableHere/CanTakeOverLockOnThisMachine seams now take bookName so
  the cloud override can consult the cache's LockedSeat: another
  account's lock is editable/takeoverable only in the same seat; the
  current user's own pre-seat (null-seat) locks are grandfathered.
- CloudRepoCache rows/snapshots carry LockedSeat.

Also: e2e-7's 20s first-commit poll raised to the 90s harness convention
(its matrix failure was a load flake; standalone 2/2).

Verified: pgTAP 65/65 (10 new seat cases), C# cloud filter 428/428
(6 new), e2e-4 PASS (cross-seat takeover refused), e2e-10 PASS
(same-seat takeover intact).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (240 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

JohnThomson and others added 3 commits July 10, 2026 23:28
The initial book commit runs asynchronously after createCloudTeamCollection;
under matrix load it can still be in flight when the spec reaches section 4,
and killing Alice mid-first-Send leaves no book row ever (11 Jul matrix,
sole failure of 14; passed standalone). Also: batch log for the 13/14
post-seat-fix matrix (e2e-4 passed under full load).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…SS export fixes; no Cloud TC overlap)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Building the cloud-tc-for-review packaging branch re-ran the pre-commit
formatter against origin/master as the base, exposing formatting drift in 14
edge-function files that had bypassed it (committed from subagent worktrees).
This applies the same output here so the two branches are byte-identical.
deno tests 33/33 after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JohnThomson

Copy link
Copy Markdown
Contributor Author

Superseded by #8052, which carries the identical tree (verified byte-identical against cloud-collections) repackaged as 9 review-grained commits per Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md. The Greptile review history here (3 findings, all fixed in b93d0c9 and verified resolved by its re-review) applies to that PR's content verbatim. cloud-collections remains the working branch; the packaging branch is regenerated from it whenever it advances.

(Claude Fable 5 agent)

JohnThomson added a commit that referenced this pull request Jul 11, 2026
…mits, byte-identical), #8048 closed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants