Skip to content

feat(club): share a card set with the club - #128

Merged
ShuhaoZQGG merged 5 commits into
devfrom
feat/club-card-sets
Nov 13, 2026
Merged

ShuhaoZQGG merged 5 commits into
devfrom
feat/club-card-sets

Conversation

@ShuhaoZQGG

Copy link
Copy Markdown
Contributor

Why

Custom card images followed a person between their own computers (personal settings sync). The timer role rotates every week, so the person who uploaded the club's artwork was rarely the one timing with it. This is Phase 1 of moving Pro to "one Pro account for your whole club": card artwork, the one personal-sync setting that can't be redone in a minute, now travels with the club code instead. Personal sync itself is untouched here; removing it is Phase 2.

What changes

  • Share with my club: in Card Images, admins and editors get a button on each of their own custom sets. Members and guest devices don't.
  • Every club device gets the set. It's listed at the top of the picker. A device that never chose cards of its own opens on it; a device that did keeps its own choice, with the club's one tap away (toastmaster_card_source, the same rule as club presets).
  • Lapse or leaving drops the set from the picker, and the device's own choice shows again. Nothing is deleted.
  • Console: /club/admin has a Card set section with the four cards and Remove card set (admin only).
  • Worker (worker/club-cards.js):
    • PUT /api/club/cards/<hash> uploads one image (admin or editor, device not revoked, club entitled).
    • PUT /api/club/cards publishes the set and bumps ver. Every hash must already be uploaded (409 missing_images otherwise; the client re-uploads once and retries).
    • GET /api/club/cards/<hash> serves an image to any club device, or to an admin's console session. Private cache.
    • DELETE /api/club/cards removes the set (admin).
    • The set is stored in club-cards:<clubId>, its own key, because a presets publish rebuilds club-presets: from scratch.
  • Device: packages/shared/clubCards.js downloads missing images into IndexedDB at start-up and on every club change. Both apps re-show the current card when the set arrives or goes away.

Security

  • Images live at club-card/<clubId>/<hash>, not under club/<clubId>/, which /api/club-assets/* serves to anyone. A test checks the public route can't reach them.
  • Uploads must match their SHA-256 and are sniffed as JPEG, PNG or WebP from their bytes (no SVG). 2 MB per image, 20 MB per club.
  • Publishing prunes images no set names any more, sparing uploads under 15 minutes old so a second publisher's in-flight uploads survive.
  • The device keeps a download only if it is typed as an image. A browser check against the Vite dev server showed its SPA fallback (HTML with status 200) being stored as a card; a captive portal would do the same.

Production impact

None expected yet. Joining a club goes through the pro flag, which is archived in PostHog, and production has no clubs. The new routes are not flag-gated, the same as presets. Their Worker route is reachable, but every write needs an admin or editor of an existing club.

Testing

  • npm test: 2234 passed. New: worker/club-cards.test.js (25), packages/shared/__tests__/clubCards.test.js (18), 7 in the Zoom CardImagesModal.test.jsx.
  • npm run test:e2e: 41 passed. npm run build:web and the Zoom app build succeed.
  • npm run test:specs: new spec e2e/zoom-client/club-card-sets.md (draft, not run in a real Zoom client). timer-core.md and timer-sounds.md were reviewed against the LiveTab.jsx change, and only their "Not covered" lists changed.
  • Checked by hand in the web app on the Vite dev server with a club seeded into localStorage: the club row is listed, it's selected on a device with no cards of its own, it isn't selected on a device that has its own, and no console errors. Uploading, publishing and downloading against a real Worker were not exercised by hand.
  • Running the spec on dev needs FLAGS_FORCE back at "1" (it is "0" since 4a526d0).

An admin or editor presses "Share with my club" on one of their own custom
card sets in Card Images. The images go to R2 under club-card/<clubId>/ and
the set to club-cards:<clubId>, and the club's ver moves, so every club device
downloads it into IndexedDB and lists it in the picker.

A device that never chose cards of its own opens on the club's set; one that
did keeps its own, with the club's one tap away (toastmaster_card_source). A
lapsed or left club drops the set and the device's own choice shows again.
Admins can remove the set from /club/admin.

The images are private: they live outside club/<clubId>/, which
/api/club-assets serves to anyone. Uploads are checked against their SHA-256,
sniffed as JPEG/PNG/WebP, capped at 2 MB each and 20 MB per club. Publishing
prunes images no set names, sparing uploads under 15 minutes old.

Phase 1 of docs/plans/10-pro-is-the-club-plan.md (local plan, not tracked).
New Zoom-client spec: e2e/zoom-client/club-card-sets.md (draft, not yet run).
Conflicts kept both sides: LiveTab.jsx imports (dev's getZoomParticipants
plus this branch's card-sync helpers) and the 'Not covered' lists in
timer-core.md and timer-sounds.md.
Share with my club sent the row's own label, "Custom 1", as the club
set's label. Every other club device has its own "Custom 1", and the
publisher's picker showed two rows with the same name. Sending no label
lets the set read "<club> cards" in the picker and in the console.
CC-01 to CC-07 ran in the real Zoom client (B = the web app). Nothing
failed. CC-07 now says the set leaves B on its next club refresh, which
is at most daily, and B shows its own choice rather than Classic. New
CC-09 covers a republish pruning the old image. CC-02 expects the
'<club> cards' label from the label fix and is marked stale until it is
rerun; CC-08 (club lapse) is not run.
@ShuhaoZQGG
ShuhaoZQGG merged commit a4439d6 into dev Nov 13, 2026
1 check passed
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.

1 participant