generated from davenh99/solidpb
Movie selector: groups, join codes and coin-flip bracket tournaments #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "movie-selector-app"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Builds the movie selector described in
PLAN.md.What it does
Sign up with an email and password, create a group, and you get a permanent
join code to share. Everyone in the group adds movies (plain text names, no
lookups). The group owner starts a tournament, which locks the movie list, and
then plays the bracket one round at a time. Each matchup is a 50/50 coin flip
decided on the server; an odd number of movies gives one film a random bye.
Non-owners refresh to follow along, as agreed — no live updates. When the
tournament ends the list unlocks, so a group can run as many as it likes,
reusing the same movies or a fresh set.
Decisions taken
Geoff chose these when I asked:
resolving the whole bracket in one click.
Template bugs fixed along the way
These were pre-existing, not introduced here. Two of them blocked the app
outright, so they are separate commits and can be reviewed independently:
record whose
changedByrelation is required, but self-registration has noauthenticated user. The hook failed after the account was committed, so
signup returned 400 while the account existed, and the natural retry hit
"email already in use". The plugin and its two collections are removed.
make devserved a blank page.@solidpb/ui-kitis excluded fromoptimizeDeps, which also stops Vite crawling its imports, so the CJS-onlybind-event-listenerwas never converted to ESM. Fixed viaoptimizeDeps.include. Production builds were always fine.main.goembedsui/dist, which isgitignored, so
make devon a clean checkout started Vite but never builtthe API.
buildnow bootstraps the frontend when it is missing.make typeswas broken.include .envonly creates make variables, andthe recipe runs from
dist/where godotenv cannot find the file either, soENVfell back toproductionandgen-typeswas never registered.There is also a
GOEXPERIMENT=nojsonv2pin in the Makefile: Go 1.27 enablesthe rewritten
encoding/json, which sends pocketbase v0.39.11'sCollection.UnmarshalJSONinto infinite recursion. v0.39.11 is the latestrelease, so there is nothing upstream to bump to. Go 1.26.5 is unaffected and
ignores the flag, so the same commands work on either toolchain.
One permission was deliberately loosened: the template locks
userto"you can only see yourself", which left group pages unable to name their
members or say who added a movie.
migrations/4widens list/view to accountsyou already share a group with. Email stays hidden regardless.
Structure
Bracket logic lives in
core/bracket.gowith no pocketbase dependency, so itis testable on its own; randomness comes in through a
Flipperinterface thattests seed. Tournament writes go through
core/routesbecausetournamentand
matchuphave no write rules andgroupMemberhas no create rule — aclient cannot forge a bracket result or a membership.
Testing
44 tests,
make test. Byes appear exactly once and only on odd counts, theshuffle never drops or duplicates an entrant, the coin is fair over 10k flips,
and every field size from 2 to 33 reduces to exactly one winner. Endpoints are
covered by pocketbase API scenarios against a fixture built fresh from the
migrations, including the permission cases and the pool lock.
Verified by hand too: a non-member gets 404/403 on every group, movie,
tournament and matchup, and cannot fake a membership. The full journey was
clicked through in a browser on a clean database, desktop and mobile, as both
owner and plain member — and re-run from a fresh clone following only the
README.
Running it
Then http://localhost:3000. You will start with an empty database.
Written by Claude (Claude Code) on Geoff's behalf. Happy to adjust anything
here — leave a comment and Geoff can point me at it.
— Claude
PLAN.md calls for removing the template's roles and permissions setup. Drops the `role` and `permission` collections from the snapshot, the `role` relation on `user`, their seed migrations, the roles plugin, and the frontend guards that consumed them. Also fixes template bugs uncovered on the way: - Provider/hooks referenced a "users" collection that is named "user", and imported types from modules that don't exist in this repo. - The changelog plugin was configured against the same wrong name. - Auth is email/password only now, so the OAuth path is gone. The Makefile pins GOEXPERIMENT=nojsonv2: Go 1.27 turns on the rewritten encoding/json, and pocketbase v0.39.11's Collection.UnmarshalJSON then recurses until the stack overflows, so migrate/serve die on startup. v0.39.11 is the newest release, so there is nothing upstream to bump to. skipLibCheck lets tsc run at all (a @kobalte/core .d.ts does not compile), so there is now a `make typecheck-ui` that passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Five collections covering the whole feature set: group name, owner, and a joinCode that is fixed for the life of the group groupMember join table; the owner gets a row too, so one membership check covers everyone movie a plain text name scoped to a group tournament status, the round waiting to be played, and the winner matchup one bracket pairing; empty movieB is a bye, empty winner means not played yet Rules are applied in a second pass because group's own rules reference groupMember, which does not exist yet while group is being created. tournament and matchup have nil write rules and groupMember has no create rule, so the coin flips and the join-by-code check can only happen in server-side code -- a client cannot forge a bracket result or fabricate a membership by guessing at collection endpoints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>make devrender instead of a blank page 6fec92d6a3make types9f9a3dbe04Drops one ball per movie through a staggered peg board and records every position at 60fps. The whole race runs on the server: physics in each browser would give every viewer a different winner, since floating point and frame timing drift apart, and would put the result within reach of whoever is watching. Getting it fair took three goes, each caught by the slot-distribution test: - Pegs originally ran the full width, so one sat on each wall and balls wedged in the corner and never finished. - Laying rows out left-to-right left the leftover space as a wider gap on the right; balls poured through it and the rightmost lane won 55% of races. Rows are now built outwards from the centre. - Starting the balls in lanes across the width handed the outer lanes the wall channel, which is wider than the gaps between pegs, and they took 83% between them. They now drop together from a central hopper, with hopper slots shuffled so a ball's start does not depend on its index. Wins by slot over 600 races went from [251 31 19 10 42 247] to [113 90 97 97 102 101]. Also: a floor, so finishers pile up instead of falling out of the world; wall friction, because a frictionless wall was a free slide past every peg; and a nudge off peg apexes, where a balanced ball would otherwise sit forever. Recording stops about a second after the winner lands, though the simulation runs on to place everyone -- that keeps the watch around 8 seconds and the payload between 26KB and 210KB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Already merged into main
Pull request closed