Skip to content

Move user-uploaded files from browser IndexedDB to R2/D1#165

Draft
pokle wants to merge 3 commits into
masterfrom
claude/this-plan-gRKHe
Draft

Move user-uploaded files from browser IndexedDB to R2/D1#165
pokle wants to merge 3 commits into
masterfrom
claude/this-plan-gRKHe

Conversation

@pokle
Copy link
Copy Markdown
Owner

@pokle pokle commented May 11, 2026

Summary

Replaces the browser-only IndexedDB storage for user IGC tracks, XCTSK tasks, and map annotations with server-side storage scoped per user. Account deletion now wipes the user's R2 prefix as well as their D1 rows.

  • Tracks → R2 at u/{userId}/track/{sha256}.igc.gz, metadata in new user_track table (D1)
  • Tasks → JSON in new user_task.xctsk_json (no R2 — same shape as comp task.xctsk)
  • Annotations → new user_annotation table, scoped to (user, track); public-by-link readable so anyone viewing a track sees the owner's strokes
  • Account deletion → auth-api now lists+deletes everything under u/{userId}/ in R2, then deletes the user row; FK cascade handles the D1 rows

Files are public-by-link by default — anyone with a share link can view, but there is no /api/u/:username/tracks (or …/tasks) listing endpoint, so libraries aren't discoverable. The dashboard surfaces this with a permanent banner above the tabs.

Per-user quotas: 500 tracks, 200 tasks, 200 MB total. Quota errors surface in the UI as toasts (analysis page) / alert (dashboard).

One-time client-side migration (web/frontend/src/auth/user-files-migration.ts) uploads any leftover IndexedDB tracks/tasks the first time the user signs in after this rollout; orphaned legacy annotations are dropped with a console warning (they had no track association in the old schema).

Public share-link routing

Analysis page now accepts ?u={username}&track={sha256} and ?u={username}&task={code}. Storage layer flips into "public namespace" mode and resolves reads against /api/u/:username/…; writes are blocked while in public mode. Annotation layer is set readonly for non-owners.

Anonymous fallback

Anonymous users keep getting in-memory analysis. storage.isAvailable() short-circuits to false when signed out, so loadIGCFile/loadTask skip persistence; the annotation layer goes in-memory only. Drawing still works; nothing persists.

Test plan

  • bun run typecheck:all — all packages green
  • bun run --filter competition-api test — 248/248 pass (16 new user-files cases covering upload, get, list, delete, quotas, ownership, public reads, annotation cascade)
  • bun run --filter auth-api test — 3 new delete-account tests pass; the pre-existing two users do not see each other's preferences test occasionally times out at the harness default 5 s on slower runners (unrelated to this change, reproduces on baseline)
  • bun test --cwd . ./web/engine — 406/406 pass
  • Manual: drop an IGC on /analysis.html signed in → uploaded, dashboard lists it, URL becomes ?storedTrack=<id>
  • Manual: draw strokes, reload → strokes reappear from server
  • Manual: share ?u={username}&track={id} link → loads in another browser, no draw UI
  • Manual: delete account → R2 u/{userId}/ empty, D1 rows gone

https://claude.ai/code/session_01ASJpfj483tfd8XjkQ5MZxX


Generated by Claude Code

claude and others added 3 commits May 11, 2026 11:34
…ser to R2/D1

Tracks now live in R2 under u/{userId}/track/{sha256}.igc.gz with metadata in
the new user_track D1 table. Tasks live in user_task as JSON. Annotations live
in user_annotation scoped to (user, track) so they're visible to anyone viewing
the track via /analysis.html?u={username}&track={track_id}.

Files are public-by-link by default; the dashboard surfaces this with a
permanent banner. There is no listing endpoint under /api/u/:username/ — files
are not discoverable. Per-user quotas: 500 tracks, 200 tasks, 200 MB total.

Account deletion via auth-api now lists + deletes every R2 object under
u/{userId}/ before removing the user row; the FK cascade then wipes the D1
metadata. A one-time client-side migration uploads any leftover IndexedDB
items on the next signed-in page load.

https://claude.ai/code/session_01ASJpfj483tfd8XjkQ5MZxX
The new user-file download responses set Content-Disposition,
X-Filename, and X-Display-Name from user-supplied filenames and
IGC pilot names. WHATWG Headers values must be ASCII, so an IGC
with a non-ASCII pilot ("François") would 500 the download.
Add asciiHeaderSafe() and an RFC 5987 filename*=UTF-8'' builder;
also cap x-filename at 255 chars.

Vite's dev proxy only forwarded /api/auth and /api/comp; the new
/api/user and /api/u/ routes 404'd under `bun run dev` because
the Pages Functions only run in production. Add proxy entries
so manual testing works end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Preview Deployment
https://1df40e56.glidecomp.pages.dev
Commit: dc71bac

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