Skip to content

feat(game): WhoCards sole controller + 90-day retention script - #340

Merged
acharlop merged 2 commits into
mainfrom
fix/sdd-privacy-retention
Oct 14, 2026
Merged

acharlop merged 2 commits into
mainfrom
fix/sdd-privacy-retention

Conversation

@acharlop

@acharlop acharlop commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Privacy screen: WhoCards = sole controller, no Unloop, no joint controllers. Host sees who joined, not answers. Answers + recap emails deleted 90 days after workshop.

Retention script: pnpm --filter website game:retention (dry-run), -- --apply to delete. Run 2027-01-13.

#317 📋 · Closes #317

For AI

Copy (packages/game/src/player-copy.ts, EN only, TODO removed):

  1. WhoCards is responsible for your data.
  2. The workshop host sees only who joined, not your answers.
  3. If you record, OpenAI turns your audio into text for us. The audio is not kept.
  4. An answer is stored only when you tap Save. Delete it any time in My answers.
  5. Your email is used only for the recap.
  6. Answers and recap emails are deleted 90 days after the workshop.
  7. Questions or deletion requests: hello@whocards.cc

Nowhere else repeated the joint-controller/Unloop wording (checked emails, website, app). apps/website/src/assets/legal/pp.md untouched (out of scope).

Plan doc docs/plans/2026-10-10-sdd-workshop-game.md: fixed lines 147 and 261. Also changed the T9 title "(joint controllers)" to "(single controller)", dropped the joint-controller risk row, and the GDPR retention bullet now says 90 days and names the script.

Retention

  • apps/website/src/server/game/retention.ts: findExpiredGames(db, now) picks games whose last activity (greatest(created_at, max(round.started_at), max(round.ends_at))) is more than 90 days old and that still have responses or emails. It ignores status, so a game nobody ended still expires. purgeGames(db, ids) deletes all game_response rows for those games' players and nulls game_player.email, in one transaction.
  • apps/website/scripts/game-retention.ts: sits next to reconcile-resend.ts. Gets DB_URL from with-env + @whocards/env/db. Prints code, last-activity date, counts, title. On error it prints only the message.
  • Added the tsx devDep to website. It was missing, and reconcile:resend needed it too. Same version as apps/emails, already in the lockfile.
  • Not touched: game_feedback text and player names. Say if those should go too.

Verified

  • pnpm check green (22/22 turbo tasks; website 369 tests, including 2 new PGlite retention tests).
  • Ran the script against a throwaway local Postgres 15 seeded with 1 old and 1 new game. Dry-run listed only OLD1 (responses=1 emails=1). --apply deleted it. A rerun found 0 games, and NEW1 was untouched. Never run against prod.
  • No screenshot: website astro dev exits before it's ready in the agent worktree, with no output. The copy is static data rendered as a list by Privacy in sdd-2026/_components/join.tsx.

RetriggerConfidence Score: 4/5

Do not merge until cleanup locks games and rechecks current eligibility before deleting retained data. The abandoned-game test gap is non-blocking.

Findings

  1. P1 Resumed games lose answers ▶
  2. P2 Abandoned games lack coverage ▶

T-Rex evidence

Isolated mutation validation script

  • Runs the original tests and both comparisons using disposable copied sources, captures executed source and output, and verifies cleanup without modifying tracked retention files.

Abandoned no-round game regression test

  • Seeds an old lobby game with an email and no rounds, then asserts selection and email purge, providing the missing coverage.

Original retention tests with current code

  • Executed the original retention suite and captured its source and output; both tests pass.

Existing retention tests with no-round fallback disabled

  • Executed unchanged copied tests against the mutated copied module; both tests still pass, confirming the coverage gap.

Abandoned-game selection and purge with current code

  • Executed the supplemental test against current code; the old no-round lobby game is selected and its email is cleared.

Abandoned-game regression test with fallback disabled

  • Executed the same supplemental test against the mutation; selection returns no games and the assertion fails, proving the added test detects the regression.

Complete validation run and cleanup output

  • Captured the validation driver's four test runs and successful cleanup checks; the expected pass/pass/pass/fail comparison completed without tracked source edits.

Evidence from the check

  • Runs the original tests and both comparisons using disposable copied sources, captures executed source and output, and verifies cleanup without modifying tracked retention files.

Evidence from the check

  • Seeds an old lobby game with an email and no rounds, then asserts selection and email purge, providing the missing coverage.

Command output from the check

  • Executed the original retention suite and captured its source and output; both tests pass.

Command output from the check

  • Executed unchanged copied tests against the mutated copied module; both tests still pass, confirming the coverage gap.

Command output from the check

  • Executed the supplemental test against current code; the old no-round lobby game is selected and its email is cleared.

Command output from the check

  • Executed the same supplemental test against the mutation; selection returns no games and the assertion fails, proving the added test detects the regression.

Command output from the check

  • Captured the validation driver's four test runs and successful cleanup checks; the expected pass/pass/pass/fail comparison completed without tracked source edits.

View artifacts

Summary

Updates the privacy notice to name WhoCards as the sole controller and promise deletion after 90 days.

  • Operators can preview and run 90-day cleanup for workshop answers.
  • Players see who handles workshop data and when answers are deleted.

Author acharlop explicitly leaves player names and game_feedback text outside this change. Author acharlop intentionally plans a manual retention run for 2027-01-13.

Reviews (1) · Last reviewed commit: "feat(game): WhoCards as sole controller ..." · Reviewed by Greptile

Privacy screen names WhoCards as controller, says the host sees only who
joined, and promises answers + recap emails go 90 days after the workshop.
Adds pnpm --filter website game:retention (dry-run unless --apply).

Closes #317
@netlify

netlify Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for whocards-app canceled.

Name Link
🔨 Latest commit 359e471
🔍 Latest deploy log https://app.netlify.com/projects/whocards-app/deploys/6acf7559cae3f43cd1b64a37

@netlify

netlify Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for whocards-calmly ready!

Name Link
🔨 Latest commit 359e471
🔍 Latest deploy log https://app.netlify.com/projects/whocards-calmly/deploys/6acf7559a7cdf62a90ad5e66
😎 Deploy Preview https://deploy-preview-340--whocards-calmly.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: whocards/whocards-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 130f0a7a-2991-4f17-a1f1-8ff3668a7ec1
📥 Commits

Reviewing files that changed from the base of the PR and between 47f55d3 and 95859a7.

⛔ Files ignored due to path filters: 1
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing: 6
  • apps/website/package.json
  • apps/website/scripts/game-retention.ts
  • apps/website/src/server/game/retention.test.ts
  • apps/website/src/server/game/retention.ts
  • docs/plans/2026-10-10-sdd-workshop-game.md
  • packages/game/src/player-copy.ts
 _____________________________________________________________________________
< Rabbit check: I hopped through the diff and found carrots... and landmines. >
 -----------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@acharlop
acharlop merged commit b945643 into main Oct 14, 2026
11 checks passed
@acharlop
acharlop deleted the fix/sdd-privacy-retention branch October 14, 2026 12:33
Comment on lines +59 to +65
await db.transaction(async (tx) => {
const players = tx
.select({id: gamePlayer.id})
.from(gamePlayer)
.where(inArray(gamePlayer.gameId, gameIds))
await tx.delete(gameResponse).where(inArray(gameResponse.playerId, players))
await tx.update(gamePlayer).set({email: null}).where(inArray(gamePlayer.gameId, gameIds))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Resumed games lose answers

purgeGames trusts IDs selected earlier without checking whether those games still qualify. A host can start a new round in an old game left live after findExpiredGames returns. Although its activity date is now recent, cleanup still deletes all its saved answers and clears its recap email addresses. This was reproduced with a successful round start followed by cleanup using the earlier selection.

Lock each game using the same lock as startRound, then recheck its activity date before deleting. This data-loss issue must be fixed before merging.

T-Rex Ran code and verified through T-Rex

await seedGame('OLD', daysAgo(120), daysAgo(91))
await seedGame('EDGE', daysAgo(120), daysAgo(89))
await seedGame('NEW', daysAgo(10), daysAgo(10))
await seedGame('EMPTY', daysAgo(200), null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Abandoned games lack coverage

seedGame always creates ended games, and its no-round case returns before adding any players or emails. The existing tests still pass when the creation-date fallback for games without rounds is disabled, so they do not protect cleanup of email addresses from abandoned games. Current cleanup works for that scenario; this is a non-blocking regression-coverage gap.

Add an old, non-ended game with an email but no rounds. Verify that selection finds it and cleanup clears it.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Artifacts

Isolated mutation validation script

  • Runs the original tests and both comparisons using disposable copied sources, captures executed source and output, and verifies cleanup without modifying tracked retention files.

Abandoned no-round game regression test

  • Seeds an old lobby game with an email and no rounds, then asserts selection and email purge, providing the missing coverage.

Original retention tests with current code

  • Executed the original retention suite and captured its source and output; both tests pass.

Existing retention tests with no-round fallback disabled

  • Executed unchanged copied tests against the mutated copied module; both tests still pass, confirming the coverage gap.

Abandoned-game selection and purge with current code

  • Executed the supplemental test against current code; the old no-round lobby game is selected and its email is cleared.

Abandoned-game regression test with fallback disabled

  • Executed the same supplemental test against the mutation; selection returns no games and the assertion fails, proving the added test detects the regression.

Complete validation run and cleanup output

  • Captured the validation driver's four test runs and successful cleanup checks; the expected pass/pass/pass/fail comparison completed without tracked source edits.

View artifacts

T-Rex Ran code and verified through T-Rex

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.

SDD game: consent, privacy notice, My answers

1 participant