feat(board): the boards come from the files that record them - #949
Merged
Conversation
| } | ||
| } | ||
|
|
||
| if (attached > 0) log.info("[boards-seed] {} seats found the account they were recorded under", attached) |
This was referenced Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice 2 of #922, the backend half. Nothing about the board page's shape changes here — that is #930. What changes is where the association's history lives and what a board is allowed to carry.
The migration is unreleased and is rewritten in place
V87__board_history.sqlhas never run in production. The newest tag,v1.6.0, predates it, no tag contains it, and this repo deploys on release. So it is rewritten in place per the house rule rather than followed by aV103, and its data-loading SQL is deleted outright — no backfill, no regex archaeology onRoos “SkyeWolf” Kruk. The rewritten migration is schema only.Every developer database and every staging environment that has run the old V87 must be dropped and reseeded. Flyway will refuse to start otherwise: the checksum has moved, and
clean-on-validation-errordoes not rescue a mismatch on a versioned migration. This is not a suggestion — I hit it myself mid-review and the cure wasDROP DATABASE. The seed files are what makes that cheap: a fresh database comes up with the whole history.What a board carries now
The identity moves from the name and the start date to the number, because a name is a thing a board chose and may never have been written down, while its place in the line always is.
accentmirrorsgame.accentexactly —VARCHAR(32), nullable, free string, blank meaning the association's blue.One statement in there is not schema:
UPDATE boards SET number = id WHERE number = 0. Adding aNOT NULLcolumn fills existing rows with 0, and the unique key cannot then be built if a database holds more than one live board. Numbering them by their own key is deterministic, needs no ordering logic, and is not an attempt to recover history — the history comes from the files.imageandcandidatedeliberately surviveboards.imageandboard_members.imagehold asset file names likeboard9/board9.jpg— paths into the frontend's own source tree. They are what/boarddraws today, and they must keep working: uploaded pictures arrive in A board and a seat carry an uploaded photograph #929 and these columns are dropped in The asset file names go #935. So they stay, and the CSV files carry them and the seed writes them. This is the expand half of an expand–contract.boards.candidatestaysNOT NULLand stays a dead copy ofname, by decision (Boards on the island: a timeline, edited in place, seeded from a CSV #922, Out of Scope). Sincenameis now nullable, everything that writes a board fills it: the board's own name, orBoard <number>where the name is blank. That rule is in one place,BoardUseCases.candidateFor, and in the seed's board upsert. It is asserted on create, on update and in the seed.The history becomes files
services/api/src/main/resources/db/seed/boards/boards.csvandseats.csv, read through the sharedSeedCsvbound once byBoardSeed— the same arrangementEsportsSeeduses. No second CSV reader.Ten boards and forty-six seats. Dates, roles, blurbs and image names come from the old V87 verbatim, extracted mechanically rather than retyped. The board names, the cheers and the nicknames are what the author supplied;
accentanddescriptionare blank for all ten and are the author's to fill in later — one cell each, an edit and a deploy.The names are split. V87 stored
Roos “SkyeWolf” Krukas one string, in two different kinds of quote. The files recordRoos KrukandSkyeWolfseparately, the way a roster entry's handle already sits beside a member's name. Thirty-one of the forty-six carried a quoted nickname; boards 8 and 9 have ten more recovered from what their blurbs state; board 7's five are unknown and blank.Board.vuecomposes the quoted form back for display, so nothing a reader sees changes.The tenth board is a candidate board with no seats.
Rainbow road, from 2026-09-17 — the day after board 9's handover, so the line has no gap and no overlap. Nobody has taken a seat on it, so it has no rows inseats.csvat all, and both the seed and the tests handle that without complaint. Whether a board is a candidate or in office is read off its dates and never stored, so no column can disagree with them.Two dates are worth naming:
boards.csvwhen it is.The repeatable seed
R__Boards_seed, modelled onR__Esports_seed.getChecksum()hashes the two files' contents, so correcting a row is an edit and a deploy rather than another migration.NOT (a <=> ? AND b <=> ? …)guard so a row that already agrees is not even written.What the auto-link does and does not do
It attaches a seat to an account once, as the seat is created, and never again.
TRIM(CONCAT_WS(' ', first_name, prefix, last_name))— which is why this is possible at all. The old V87 had a name-match step, but thirty-one of the recorded names had a nickname in quotes in the middle of them, so it could never match, which is why no seat before the seventh board is linked to anybody today.user_idonly in theINSERT; the update path does not touch it.There is no backfill of existing links and no attempt to attach a seat that already exists.
Encoding
Two names carry characters that are not the ASCII letters they resemble, and one board's name carries an apostrophe, so this is asserted rather than eyeballed:
İlayda Hotamiş—İis U+0130 (Turkish capital I with dot),şis U+015F. Corrected from V87'sÌlayda "Vriendelijke kebab" Hotamis, which had U+00CC and a plains.Kimberly Evertsz— corrected from V87'sEvertz.Don't starve together— an apostrophe needs no quoting in a comma-separated field, so it is not quoted; over-quoting it would show up as a quote inside the name.Blueshell, always ahead,RNG, Be With Me!) and are quoted, which is the exact failure modeSeedCsv's own doc comment warns about.Checked, and asserted at two seams:
SeedCsvdecodes them as UTF-8BoardSeedParsingTest— asserts the header is the first thing in the file, and the exact string both literally and asİlayda HotamişBoardSeedLoadIT.a name written outside ASCII arrives byte for byteboardsandboard_membersareutf8mb4utf8mb4_unicode_cion both tables and on all nine text columns involved, including the four new ones.BoardSeedLoadITasserts it frominformation_schemaso a column that drifted fails with a reason rather than as a mystery?if it did notThe byte-exact assertion looks up its row by role, not by name: these columns collate accent- and case-insensitively, so
display_name = 'İlayda Hotamiş'would happily match a row that had lost both characters and the assertion would pass against corrupted data.One thing found while looking: U+0130 will not render in the site's display face — the shipped
.ttfcovers 226 codepoints and lacksİ ı ş Ş. That is a frontend concern and is not touched here; what matters on this side is that the real character is in the database.Api and client
number,cheer,accentanddescriptiononBoardResponse,CreateBoardRequestandUpdateBoardRequest;nicknameonBoardMemberResponse,AddBoardMemberRequestandUpdateBoardMemberRequest;namenullable on the response. Blank strings normalise to absent, so a cleared colour means the association's blue rather than an empty string.numberis validated as at least 1 and refused with a 409 and the number in the message when another board holds it —DuplicateBoardException(number)in place of the old name-and-start-date one, which nothing threw. A board updating to its own number is not a conflict. No arbitrary upper bound: a cap on how many boards the association may ever have is not a rule.The generated client is regenerated —
./gradlew :services:api:dumpOpenApiSpec, normalise,yarn gen:blueshell,yarn lint:gen, which is what CI's Validate OpenAPI client generation job runs. The diff isservices/api/openapi.jsonplustypes.gen.ts, and every changed line intypes.gen.tsis a board type. Nothing else moved.The existing page keeps working, minimally
/boardand/management/boardsare #930's to redesign, so they are touched only as far as the schema forces:Board.vuenames a board with no recorded name from its number (Board 6; Roman numerals are The board page reads as a timeline #930's) and composes a seat's nickname back into the quoted form.BoardManager.vuegains the number, cheer, colour, description and nickname fields. Not decoration: the api writes what a request carries, so a dialog that did not send them would silently clear a board's cheer the first time somebody corrected its dates.boards.tsgrowsboardTitleandseatTitle, which is where the display strings belong.tests/e2e/boards.spec.tsstill passes, with four cases added.Architecture
shared/seedwas pinned inSharedFanInArchitectureTest.BELOW_THRESHOLDat one consumer module with the note "until the board seed lands". It has landed,boardis the second, and the pin is lifted — that test fails when a pinned package's fan-in recovers, which is what made this a checkable claim rather than a note. NoModuleMetadatais touched:boardalready namesshared.Nothing reads "the current board"
BoardRepository.findActiveBoardhas no callers at all — I looked for every consumer of the idea, since a future-dated board 10 must never answer as the board in office. It could not anyway: the query isstartDate <= :date AND (endDate IS NULL OR endDate >= :date), and board 10 starts in the future.BoardSeedLoadITnow pins that, asserting it answers with board 9 today.The cohort module reaches the board module through
BoardMemberService.serversBetween/servedBetween, which are seat-based and windowed. Board 10 has no seats, so it contributes nothing to any cohort. Nothing else in the api readsboardsorboard_members.Tests
Run locally, exit codes checked rather than output grepped. The integration suite ran against a throwaway MariaDB 10.11.10 container on port 3309 (3306 was taken by another stack) with
blueshell-testcreated fromservices/api/0_init.sqland CI's ownMYSQL_*env,cwebppresent for the shipped art../gradlew :services:api:compileKotlin :services:api:compileTestKotlin :services:api:compileIntegrationTestKotlin./gradlew :services:api:test./gradlew :services:api:integrationTest(full suite)yarn typecheckyarn lint --max-warnings=0yarn test:unityarn test:e2e --grep "board"./gradlew :services:api:dumpOpenApiSpec+yarn gen:blueshellNew tests, at the seams #922 settled on:
BoardSeedLoadIT— a Spring integration test on what the files do to the database. Ten boards and forty-six seats land; a board with no seats lands; a board's name, cheer and candidate are what the file says; a blank cheer or colour is absent rather than empty; the split name and its nickname; a nickname recovered from a blurb; the five seats with no nickname; the asset file names surviving; a seat's dates coming from its board; a blurb carrying a comma, a quote and a line of its own; a cheer carrying a comma; a name outside ASCII, byte for byte; the columns' character set; a corrected row applied; a deleted board and a deleted seat left deleted; a second run changing nothing; the auto-link attaching an exactly-one match, leaving a no-match and a two-match alone, and never re-attaching after a detach; and the in-office rule read off the dates.BoardSeedParsingTest— a pure unit test on the files themselves: the ten numbers in order, forty-six seats, no byte-order mark, the non-ASCII name literally and as escapes, the apostrophe, both comma-bearing cheers, no quote left inside any name, every seat naming a board the boards file lists, no two seats on a board sharing a name, and the terms running one after another with no gap and no overlap.BoardControllerIT— extended: every new field round-tripping through create, read and update; a board with no name; blanks normalising to absent; a nickname round-tripping through create, update and read; the number conflict refused with 409; a board keeping its own number.BoardUseCasesTest— the candidate fallback, the number conflict, the nickname.BoardControllerSecurityTest— unchanged in what it asserts, so every endpoint's permissions are still covered by exactly the cases that covered them before.What I did not verify
How many seats auto-link in production. The test database holds no accounts beyond the ones a test creates, so nothing attaches there by default and the mechanism is what is asserted: one exact match attaches, no match and two matches do not. Against the real
userstable the number is whatever it is — most of the people in this history never had an account here, and the seats that find one will say so in the log line the seed writes.Closes #927
Diff breakdown —
█added░removed, scaled to the largest row.