OpenPairings

All notable changes to OpenPairings are documented here. Format loosely follows Keep a Changelog.

Each entry is tagged so a version can be skimmed:

tagmeaning
Featuresomething new you can do
Fixsomething that was broken
Changeexisting behaviour works differently
Removedsomething is gone
Securitya vulnerability closed, or judged not to apply
Verifiedchecked against a reference, no code change
Performancethe same result, sooner

[Unreleased]

[0.111.0] - 2026-10-30

  • Security Two dependency advisories closed. Phoenix HTML 4.4.0 (advisory EEF-CVE-2026-117645) and Bandit, the web server, in its patched release (EEF-CVE-2026-116078, memory amplification from HTTP/2 bodies sent one byte at a time). Nothing to do on your side.

  • Verified Ainalrami 0.48.0, which changes nothing you will see. The engine learned to write a proof file for each round it pairs, checkable by a separate verifier that owes it nothing. OpenPairings does not ask for one, so the pairings, the speed and the screens are exactly what they were; the proofs are for people who want to audit the engine from the command line.

  • Performance Pairing the rounds that need the whole field is about a fifth faster. Ainalrami 0.47.0 builds the graph over every pair of the field in under half the time; the slowest known position, 600 players in round 9, drops from 0.91 to 0.73 s. Same pairings to the board: the two builds were run side by side on 39,753 rounds and agreed on every one, which is the only kind of speed-up worth having.

[0.110.0] - 2026-10-27

  • Feature Right-click a player for their Players Card, on Pairings and on Standings. On the Pairings page it is the third item of the seat menu (and in the menu of the Not playing list); on the Standings page a player's name now opens a small menu of its own, with the same keyboard keys and tap as the Pairings seats, and a way to the Players list. It is the card the Players page already had, now one shared piece rather than a copy per page. A team standings table has no card to open, so it keeps no menu; the individual ranking of a team tournament does.
  • Feature A tie-break of your own, as a formula. For regulations that want something FIDE never wrote - "forfeits lost, the more the worse" being the classic - the tie-break builder has CUSTOM - Custom formula (not FIDE) under Your own: rows of sign, weight and figure (wins, draws and losses over the board, games played, forfeit wins and losses, full-, half- and zero-point byes, pairing-allocated byes, games played and won with Black, unplayed rounds, points, Buchholz, Sonneborn-Berger), and one switch for whether the higher or the lower total ranks first. It starts from Forfeits lost (fewer is better), −1 × forfeit losses, higher is better; Forfeits won count for you turns it round and Your own formula starts empty, each with its explanation on the button. It multiplies and adds; nothing typed is ever run, which is more than can be said for most spreadsheets. It ranks where the list puts it, shows each player's working on the Standings page, carries a custom label on the page, the prints and the results site, and travels with the tournament export. It stays out of the TRF tie-break line like EXT, no invented code, so the file for rating has the same lines it always had. It does not leave FIDE mode - C.07 lets the regulations choose - but the builder says Not a FIDE tie-break in so many words, warns when it sits above a FIDE tie-break, and after round 1 the formula is locked with the list. Individual standings only.

[0.109.0] - 2026-10-26

  • Fix A team's edit form keeps what you type, and stays open. The same lapse the Add a team form had: a redraw of the page put every field back to the saved team and folded the form shut, mid-sentence. Each team now keeps its own draft until you save it.

  • Feature The ITDX file says how each round was paired, in the engine's own words. It now ends, after the last player, with ### OP trace R<n> | ... lines: per round, what Ainalrami prints at its normal level - the boards with their colours, the float cascade, every bracket with who moved down, who was paired, who floated and the criteria that scored. The engine's library calls print nothing, so the trace is its command line run on the very field it paired, written out with the organiser's wishes, exclusions and bye preferences; that is a second run, so it happens in the background job that works out the round's account, not in the Pair round click (on a 450-player field the trace took 65-80 ms against 15-20 ms for the pairing and came to about 20 KB a round: cheap, but a second opinion is no business of the click). Each trace says whether it ends with the boards that were stored, and says so loudly if it does not. A team Swiss round gets the engine's report of the result it already returned, taken in the click because there is no second run to speak of. Rounds from before get their stored account instead, labelled as an account; a round robin, Keizer, a match's second leg and a round made by hand say there was no engine run; boards changed after the engine paired them are pointed out. Players are starting ranks: the engine's roster lines, which carry names, are dropped, the 001 records having made the introductions already. The trace is kept per round (rounds.engine_trace, never loaded unless asked for by name, not exported), the importer ignores it, and the file for rating, its copy and the engine-dialect download never see it. Pairing is untouched; the engine is merely asked to explain itself afterwards, which is more than most people manage.

  • Fix The manual's contents list stays where you left it. Every chapter you picked sent the list back to the top, so reaching chapter 15 meant scrolling down to it again each time. Worse, the pointer was then resting over some other chapter, whose hover colour made two chapters look selected at once. The list now keeps its place from chapter to chapter, and a chapter opened from a link scrolls the list far enough to show it.

  • Change Setting up boards tells the flow as it is. Pairing a round is all it takes for the box: it gets every paired round, released or not, and says Waiting for OpenPairings to pair round N while it waits; its Start round N button is for a box with no link. The manual had still sent arbiters to that button, and the other programs' pages are refreshed from their latest releases.

  • Security Bandit 1.14.0. The web server had eleven advisories published against 1.13.1 on one day, mostly HTTP/2 frames and chunked bodies made to eat memory or CPU; 1.14.0 fixes them. Nothing in the app changed, which is the best kind of release note.

  • Change Two buttons on the Pairings page retired for having nothing to do. Standings, beside "On to round N" and as the main button after the last round, repeated the menu entry one click away; after the last round the main button is now Send for rating, which is what is actually left to do. More, Edit pairings by hand opened a hand-edit session with no edit in it - the first right-click on a player has always opened one by itself, and that is now the only way in.

  • Change The two publishing switches stop pretending to be one. A round's Spectators see: level and the tournament's results-site switch are separate things: the level also decides what the hall screen and the phone app show, so it could be set to Results on a tournament that was not on the results site at all, which looked like a switch wired to nothing. The tournament's card is now called On the results site (it was Publish this tournament, which the manual had already stopped calling it), the round's control is captioned Released: instead of Spectators see: - how much of the round you have let out, wherever it goes - its hover text names all three places, and while the tournament is off the site the round says so under the level.

  • Fix Adding a team no longer forgets its name. The Add a team form kept what you typed in the browser alone, so any redraw of the page - a change made in another tab will do - emptied every field except the one you were typing in. Type the name, move on to the captain, and the name was gone; saving the name first and editing the rest afterwards was the workaround nobody should have had to find. The page now keeps the draft as you type and draws it back.

[0.108.0] - 2026-10-26

  • Feature A hand edit can say why. The confirmation of a swap, an emptied seat, a moved bye, a filled seat, a pool pairing or a deleted board, the dialog that finishes the hand edits, the one that creates a round by hand and the one that swaps two rounds of a round robin each have a Reason (optional) line, 200 characters at most. It is never required and never in the way; a blank reason is simply no reason. The reason is kept in the edit's audit row (reason in its details, control characters and line breaks turned into spaces), spoken on the audit page, listed above the boards while you are on that round, and accepted by the Manubrium edit and finish routes as an optional reason. The ITDX file writes it beside the edit in the ### OP block as reason "...", with accents stripped and anything else outside ASCII dropped; the original stays in the app. Nothing reads it back, which is how free text should be treated by anyone who has met free text.

  • Feature Every round knows when it was paired. Rounds have a paired_at (UTC), set by every way a round gets its boards: Swiss, round robin, Keizer, team rounds, rounds made by hand and the mirrored second leg. Pairing a round again after unpairing it replaces the time, as the round is a new round. Rounds that existed before get the time of their newest round paired audit row, and none when the trail has none. The Pairings page shows it under the round bar in your own time zone, and the ITDX file's OP round line says paired 2026-10-26 09:14 UTC instead of leaning on the audit rows, which are now only the fallback for a round without a time. An export file carries it and an import restores it.

  • Fix The file sent for rating stops doing the pairing program's homework. It carried the virtual points of Baku and acceleration (250), the players kept apart (260) and the extra points (299): records that decide who meets whom next, read by nobody at the rating server. They are gone from it, and so from the copy, which is that file plus its marks. National rating (172) and the team records stay, being about ratings and games. The ITDX file and the engine's spelling keep everything, since another pairing program needs it. The manual had also gone on listing the 240 the previous fix removed; it lists neither now.

  • Fix The file sent for rating no longer announces a round nobody has played. A seven-round event sent after round four ended with 240 Z 007 because a player had been marked absent for round seven in advance. That is a note for whoever pairs round seven, and the rating file reports games: the bye is gone from it, with the empty columns that held its place and the point it was credited in the total. The copy is that file plus its marks, so it lost the bye too; the ITDX file, the plain download and the engine's spelling keep it, being the ones another pairing program reads. A test now builds the file for a dozen kinds of tournament (byes ahead, withdrawals, late entrants, Baku, 3-1-0, prohibitions, a postponed game, a team Swiss) and checks that nothing in it names a later round, and that each points column is what its own columns add up to.

  • Feature Players go onto their team when they are entered. In a team event the add-player form and the registration dialog have a Team select: No team or one of the teams. A player lands at the bottom of that team's line-up, through the same function the Teams page uses, so the locked line-ups of FIDE mode, the foreign-team check and the audit row (team.player_assigned, or team.player_removed for No team) are the ones that already existed, not a second copy that would drift. If the team refuses, the player is still entered and the reason is shown. The Teams page's Add button now goes through the same function.

  • Feature The players list shows the team. A team event's Players list has a Team column, on by default: the team and, after a dot, the board in its line-up. It sorts by team and then board, with players on no team last. The printed player list has a Team column too, on by default for a team event and unavailable for any other.

  • Feature A club rule that does not apply says so. With a Teams of the same club rule, the Forbidden pairings page counts the teams that have no club, because the rule skips them, and gives each an input to fill one in on the spot. Where at least two players with a club are on a team and three quarters of them name the same one (trimmed, capitals ignored), that club is offered as a button, on that page and on the team's edit form. Pressing it is the only way it gets filled in. The Teams page's create and edit forms say the club is needed for that rule; it stays optional. A Same club rule for players gets the same notice for players without a club, with a link to the Players list filtered to them (?club=none).

  • Feature Three TRF files, one job each. Transfer to another program (ITDX) on the Export page is the third: the partial TRF26 FIDE's TEC manual draft calls In-Tournament Data Exchange, for handing a running event to another pairing program. It writes ? for every game without a result (an open postponed game and a board paired and not yet played), and X in record 162 as the draft says: the draw for postponed games, 0 for a newly paired round, the draw with the unplayed boards' players carrying it in their points when both are in the file. It keeps the national rating records and all the ### notes, says ITDX - NOT FOR RATING inside and _ITDX-NOT-FOR-RATING in its name, and is made in FIDE mode even with a postponed game open, because it is partial by definition. It also carries a write-only block of ### OP lines (version, settings, tie-breaks, prohibitions, round events, per-player absences and bye wishes, plain ASCII, no names or contact data) that nothing reads back. Importing the file gives back the same pairings, colours and scores, with the unplayed boards still unplayed.

  • Change The copy has its notes back. Download a copy is still Send's records and legend line, 0000 - Z for an open postponed game and no ?, but it carries the ### notes again (FIDE mode, imports, MPAs, byes, corrections, prohibitions), plus one new line per game written as not played, ### Postponed @ Round 3: 5-12. The file Send hands out is unchanged. The rating inbox's rebuilt copy follows.

  • Change A copy is a copy. Download a copy on the Export page, the postponed-games copy and the rating inbox's rebuilt copy used to be a second opinion on the file Send hands out: open postponed games written as ? (which FIDE's checker rejects as not valid characters), a column ruler, and a handful of ### notes about leaving FIDE mode, imports, MPAs, byes, corrections and prohibitions. They are now the file Send hands out, byte for byte, plus only the ### COPY - NOT FOR RATING line and the receipt lines that stop a copy being sent twice. An open postponed game is 0000 - Z in both; the legend line stays. The notes that vanish from downloads are still built by the plain TRF26 report, which nothing links to, so the FIDE mode refusal on open postponed games and the engine-dialect download are untouched. The manual and the Export page say the same.

  • Change Tie-breaks have a page of their own. Settings has a Tie-breaks entry in the Set-up group, right after Scoring, and the list, the presets, the builder and Reset to FIDE default moved there from the Tournament page, along with the two advanced settings about unrated players and shared places. The page has its own Save settings, so rearranging tie-breaks no longer rides along with a changed venue, and saving it writes the same audit entry as before and nothing else of the tournament. FIDE mode still freezes the list once round 1 is paired and now freezes a page instead of a card. The Tournament page keeps no signpost to it; the rail is the signpost, and it has been there all along.

[0.107.0] - 2026-10-25

  • Fix The tie-break builder's buttons press again. Cut, median, score basis, "then" and Koya limit looked clickable and did nothing in a browser: each button sent its choice under the one name the browser overwrites with the button's own (empty) value, so every click chose the first option. Tests click without a browser and saw no problem, which is the kind of alibi only a test can have. The choice now travels under a name nobody else wants.
  • Feature Game points can come first. Settings, Scoring has Ranked and paired by: Match points first (the FIDE default) or Game points (board points) first, for leagues that care more about boards than matches. The choice reaches everything that ranks a team: the team standings and their printout, OpenResults, the tie-break defaults (Reset to FIDE default then starts GP, MP), and a team Swiss's score groups, floaters and colours, which Ainalrami pairs with score_mode: :game_points. C.04.6 Art. 1.2.1 leaves the choice to the competition's rules, so neither value leaves FIDE mode; like the points themselves, it locks once round 1 is paired. The team Swiss TRF writes FIDE_TEAM_TYPEA_GP_MP in 192 and an import reads it back. A team round robin's Berger code has nowhere to say it, so a team round robin read from a TRF comes back on match points.
  • Feature The round that freezes team numbers is a setting. Options, Teams (advanced, team Swiss only) has Team numbers can be corrected until round, 4 by default as C.04.2 2.3 has it. An earlier round only locks them sooner. A later one asks the two Leave FIDE mode? questions, is recorded for the report and lists on Settings, FIDE, because a rule made for the whole event is not a Chief Arbiter's one-off decision under C.04.6 1.1.3, however much it would like to be. Late teams are numbered after the others whatever it says.
  • Feature Team numbers can be drawn by lot. The Teams page has Draw the order by lot until round 1 is paired, for competitions whose rules say so (C.04.6 1.1.2). The shuffle uses the system's strong random source, saves a restore point first and asks before replacing an order somebody set by hand. The page notes when the draw happened and the audit trail lists the order drawn; moving a team afterwards removes the note, since the order is no longer the draw. It is offered only when the team rating is Typed in for each team: with a rating method, Order by rating and the regeneration after round 1 would undo it, and a button that quietly loses arguments is worse than no button.

[0.106.0] - 2026-10-24

  • Feature Clubmates can be kept apart in a team Swiss. Settings, Forbidden pairings now speaks of teams when the event is one: Teams of the same club (from the club on each team's card, compared trimmed and without regard to capitals, a team with no club being no club's) and Teams I choose, each a rule or a wish, for every round or a window of them, exactly as the players' rules are. A rule is handed to Ainalrami 0.46.0 as absolute; a wish only breaks ties between pairings C.04.6 ranks equal and never costs a colour, a floater or the bye. When the groups leave a round that would pair without them no pairing, the Pairings page says which groups are in force and which of them, lifted alone, would let it through, instead of blaming Article 3. Not FIDE, and the documents say so.

  • Change FIDE mode counts team rules as it counts the players'. Rules set before round 1 change nothing; a wish that actually moves a match asks the two questions first and is recorded for the report, as does any rule added, changed or removed once round 1 is paired. A team Swiss paired through the page now asks too, which it had no way to do before.

  • Change A team TRF never writes XXP or 260. Ainalrami 0.46.0 refuses them in a team Swiss, since they name players and a team event has none to name. The engine dialect writes XXO team-forbidden 3 11 16 @1-2 and XXO team-soft-pairs ... instead, ainalrami -c replays the file with them, and importing it brings them back as rules for the teams named. The TRF26 report keeps to FIDE's records. Ainalrami is pinned to v0.46.0.

  • Feature Match sheets and blank line-up forms. The match result sheet now shows each player's FIDE ID and the average rating each team fielded. New on the Print page: Line-up forms (blank), one page per team and round for the captains to fill in - team, opponent and board colours once the round is paired, a row per board, reserves, signature. Paper has not been replaced by tablets, only supplemented.

  • Feature The rating of who actually played. Each match on the Pairings page shows the average rating both teams fielded (Avg. Elo), as do the printed team pairings and match sheets, and the team standings show a team's latest. Unrated players are left out of the mean rather than counted as zero. Display only; the seeding reads what it always read.

  • Feature Team numbers can be corrected until round 4, as the players' can. On the Teams page Team numbers… exchanges two teams of equal team rating or regenerates every number from the ratings (the Team rating for the order of the teams option), with a confirmation listing whose number changes, an audit trail entry, and no departure from FIDE mode. The Pairings page warns above Pair round N when the numbers no longer follow the ratings. From round 5 nothing renumbers, and a team round robin never does.

  • Feature Board order, chosen for each round. Options, Teams has a new Board order: Fixed by the roster (the default and the Olympiad procedure, where nobody sits above a player listed higher) or Chosen for each round, where any player of the roster, reserves included, may sit on any board of a line-up. Some leagues let the captain decide the order, and arguing with a league is a hobby for people with more time. The pairing still seats a team in roster order; the match's Line-ups page changes it. Every line of the TRF26 already names the opponent and colour actually played, so the line-ups as played are what the file carries. In FIDE mode the setting is locked once round 1 is paired, since it is competition regulation.

  • Feature A series and a club on the team. The team card takes an optional Series label ("Reeks 1") and an optional Club. A series is a label, not a group: teams of every series are still paired together. When any team has a series, the team standings and their printout gain a Series column. The club is stored and shown on the card; it waits for a pairing rule that wants to know who shares a clubhouse.

  • Feature An individual ranking for team tournaments. The Standings page has a second view, Individual ranking: every player who played, with rank, name, rating, team, series, games, points and percentage, filtered by series when there are any, and a printout that follows the filter. The order is points, then percentage, then rating; players level on points and percentage share a rank. Points are the board statistics' own, and Games counts only games actually played, so a forfeit adds a point but not a game.

  • Change The printout footer thanks a tester again, next to the signature. Some debts go on every page.

  • Change The tie-break builder says what it is for before asking you to choose anything: an "Add a tie-break" heading, and a line saying where the new one lands.

[0.105.0] - 2026-10-24

  • Change The printouts are signed by the author, not by the testers. The footer on every printed sheet now says who made the program and where it lives; the thanks to the people whose feedback shaped it moved to Settings, About, where credits are read rather than photocopied. About gains the author's line above them.

  • Change Adding a tie-break is a builder, not a list of two hundred. Settings, Tie-breaks now asks in two steps: the tie-break itself from a short list of base ones, then only the adjustments it accepts - cut or median, forfeits counted, fore, Koya limit, match or game points for teams, and SSSC's normalising factor as a free whole number from 1. A preview shows the code and its name before you add it, and a combination already in the list says so instead of quietly doing it twice. Combinations that have an old spelling (BH/C1 is BHC1, BH/M1 is MBH, ARO/C1 is AROC1, team BH:MP is BH) are stored under it, so tournaments saved before keep reading the same.

  • Security plug_crypto 2.2.0 to 2.2.1, which closes EEF-CVE-2026-105963. The audit step was right to complain.

  • Feature Every code in FIDE's Mandatory Tie-Breaks table can be picked. The TRF26 table lists 158 codes (counting the Koya limits and SSSC factors with two values each); the picker was missing 112 of them. Individual events gain /P - forfeits count as games against the scheduled opponent - on Buchholz, Fore Buchholz and Sonneborn-Berger with all their cuts and medians. Team events gain the match-point and game-point forms of the same family (BH:GP/C1, FB:MP/P, PS:GP/C2, KS:MP/L1, AOB:GP/F, WIN:MP, WON:MP, MPvGP), the extended Sonneborn-Bergers with their cuts and /P (EMMSB is new, the other three are completed), the extended direct encounters (EDEBT, EDEBB, EDET, EDEB, /P on each) and SSSC with /F, /P and a normalising factor /K<n> of any whole number from 1. They are stored under the table's own spelling, which is also what the TRF 202/212 lines carry, so export and import agree without a translation list to forget. Values are Ainalrami's 0.45.0; a test carries the whole table and checks it code by code.

[0.104.0] - 2026-10-23

  • Feature A round robin prints its whole schedule, the future included. Print, Schedule (all rounds) lists every round with its date from Settings, Dates, its boards, pairing numbers and ratings, and its bye - also the rounds nobody has paired yet. Those are not guessed: they come from the same functions that will pair them, and a test pairs every round and compares it with the paper, board by board. Before round 1 the page says the table is still provisional; after it, it says what can still move a round (swapping rounds, pairing by hand), which is the arbiter, not the arithmetic. Tick with results off for a blank schedule for the wall, and with postponed dates puts "postponed to" the agreed date and venue where a * used to sit. Double round robins print both cycles.
  • Feature Player cards come filled in. The cards on the Print page now list every round with its date, board, colour, opponent, result and running score. A round robin's card is complete before a single move is made; a Swiss card fills in what has been played and leaves the rest to the pencil, which is the honest version of predicting a Swiss. The Players Card of a round robin prints the same schedule for one player. A blank tick keeps the empty grid for anyone who liked it.
  • Feature Team round robins get the same treatment. Schedule (all rounds) prints every match of every round with its board lines, the rounds to come seated from the rosters as they stand now (and the page says so, because a reserve has opinions). Team schedules prints one sheet per team: opponent, date, colour on board 1, match score and running match points. A player's card shows the team their team meets, the board, the colour and the opponent.

[0.103.0] - 2026-10-22

  • Change The tournament format stops pretending it can change after round 1. Once round 1 is paired, Swiss, round robin, individual or team is fixed: the select is disabled with a sentence saying why, there is no Unlock, and the server refuses the change from anywhere else too. The rounds already played were built for one shape, and no amount of reinterpretation turns a Swiss into a round robin after the fact. Start a new tournament.

  • Change New points after round 1 come with a warning, not a padlock. Outside FIDE mode the win, draw, loss and bye values can still be fixed after rounds are paired, because a club that typed 3-1-0 by mistake deserves a way out. Changing them now says, before the save, that every round already played is re-scored and the standings and anything published move with it.

  • Change Round dates that run backwards are refused. Settings, Dates no longer saves round 5 on the day before round 4; it names both rounds, in Dutch too. Two rounds on one Saturday are fine, a blank is not out of order, and an imported file keeps whatever schedule it arrived with.

  • Feature Officials without a FIDE list. On a computer with no FIDE list, or for an arbiter newer than the list, the ID box offers to take a FIDE ID as typed. It saves like any other, the report forms use it, and the page keeps calling it unverified until somebody picks the official from a real list.

  • Change The organiser lives on Settings, Tournament, FIDE ID included. It used to be editable on two pages, which is how a name and an ID drift apart. The Tournament page now has the same lookup as the officials; Officials and norms shows the organiser read-only with a link. Renaming the organiser without picking again warns that the stored FIDE ID belongs to the old name, and the save clears it rather than handing someone else's ID to the new one.

  • Feature Collaborators: change the role, resend the invitation. Share / Team has a role select beside each collaborator (Editor or Arbiter, pending or accepted) and a Resend invitation button for anyone who has not accepted yet, at most once a minute so an impatient owner cannot turn an inbox into a mailing list. Both land in the audit trail. An accepted invitation link stays single-use and says so when opened again.

  • Feature Settings pages ask before throwing your edits away. Type something on a Settings page with a Save button and then click elsewhere, use the menu or close the tab, and the page asks first. One shared mechanism, on every such page; controls that save as they change do not count.

  • Fix The last English on the Settings pages. The page titles on Rooms and FIDE, the engine line on About, the status badge beside every Settings heading, the format names in the Tournament select, and the archived, handed-off and phones-cannot-connect refusals that pairing, unpairing, importing results and swapping rounds answer with now arrive in Dutch for a Dutch reader.

  • Feature The phone can take its hand edits back. POST /rounds/:n/edits/discard in the Manubrium app's API is the Pairings page's Discard hand edits, not a cousin of it: the same restore record, the same refusals (nothing to discard, a result already in, a session that began on a round with results), the same audit line, now marked as from a phone. The round's hand_edits says whether it can (discardable). It goes back to where the hand edits began, not one step: there is one way back, and now two places to reach it from. The contract gains the request and the field and stays manubrium-v1.

  • Feature Hand edits can be taken back. Opening a round's hand edits now takes a restore point first, and the banner gains Discard hand edits: after a confirmation the boards and the not-playing list go back to exactly where the session found them, ids and game identities included, and the session closes without the check. Only while nothing in the round is decided; once a result is in, the boards it was played on are not something to quietly rewind. The way back is one documented function, so the next thing that wants an undo does not get to write a second one. Audit-logged, in both languages.

  • Change A Chess960 position can be drawn again, in front of witnesses. "It cannot be drawn again" was true and occasionally unhelpful, such as when the draw went to the wrong round. Draw position again now replaces a round's position while the round has no result, after a confirmation naming the one it replaces; the audit trail records old and new, so anyone redrawing until the knights look nice will be doing it on the record.

  • Feature A round robin's schedule can be unpaired in one go. A schedule paired before the field was settled used to come apart one Unpair round at a time, from the last, as a test of patience. More, Unpair the whole schedule removes every paired round with its boards and byes, after a confirmation and a restore point, while no game anywhere in it has a result. Pairing numbers stay, so the table pairs the same way next time. Audit-logged.

  • Fix A refused round swap no longer leaves a restore point behind. Swap rounds took its restore point before asking whether the swap was allowed, so a refusal left a souvenir of a moment in which nothing happened. It now asks first.

  • Fix The live round lights the Pairings tab. The projected round page highlighted nothing in the top bar, as if it belonged to no one. It is the pairings, shown large, and now says so.

  • Feature An incident logged from the phone can be put right from the phone. The Manubrium app's API takes PATCH /incidents/:id (a new type, a new note, or both) and DELETE /incidents/:id (the photo goes with it). Whoever may log an incident may amend or delete one, and only for a type their phone code may log, so a code that can only call the arbiter cannot rewrite an illegal-move report through the back door. Both land in the audit trail under the account: the amendment with what it said before, the deletion with the note it took along. The report is corrected; the history of it having been wrong stays. The contract gains the request and the 204 and stays manubrium-v1: nothing that was there changed.

  • Fix A Swar TRF's absentees import as absent. Swar lists the players missing from the next round on an XXZ line, which the engine underneath (Ainalrami) used to file under codes it does not read; the import then brought them in as present, ready to be paired. Ainalrami 0.44.1 reads the line, so they now arrive as a zero-point bye for that round, exactly as a 240 Z record would. Pairing inside OpenPairings was never affected: it tells the engine about absences with a Z in the column, which was always read.

  • Change Ainalrami pinned to v0.44.1 (from v0.44.0): XXZ read, other unknown XX lines warned about on stderr, and the command line no longer trips over a Windows-1252 name.

  • Change Times are shown in the clock on your wall. History, the audit trail, the Admin activity list and the incident list stored UTC and printed it bare, so an arbiter in Brussels read 14:05 for something done at 16:05 and picked restore points by mistake. They now show your browser's time zone, keep the UTC time in the tooltip, and "Today" turns at your midnight rather than Greenwich's. Without JavaScript you get UTC, labelled as such.

  • Change The tournaments list is cards on a phone. Seven columns at 375 pixels had pushed Status and the More menu off the edge of a card that cannot scroll. Below 720 pixels each tournament is a card with the name and More on top, the facts on one line and the status under them; wider screens keep the table.

  • Feature The badge editor says when it saved. It has always saved as you type and never told anyone, which is fine until somebody wonders. A "Saved" tick now fades in beside the heading after each save, a failed save says so, and a line of hint explains there is no Save button.

  • Feature Rating lists and categories can be renamed. Renaming a rating list changes its label only; sequences hold the list's number, so every tournament keeps reading it. Renaming a category rewrites the list, its rule, its prize count and every player's tags, after a pinned restore point, and refuses a name that already exists.

  • Change Odd player data is warned about, not refused. A rating below 0 or above 3000, a birth year before 1900 or in the future, and a federation that is not three letters get a note in the add and edit forms and a list above the grid. The tournament rating no longer refuses 10,000 either. Nothing is rejected, and imports keep importing.

  • Fix The Entries page remembers when it last checked. "Last checked" lived in the page's memory, so every visit began with no idea. It is stored per tournament now and counts the timer's checks too; a tournament that was never checked says so.

  • Feature Incidents can be handled and removed. An arbiter call stayed highlighted for the rest of the event. Each incident now has Mark handled (it stays, unhighlighted, and can be reopened) and Remove (behind a confirm, with the note and photo, leaving an audit line that it existed). The phones' incident list is unchanged.

  • Change The publishing pill speaks plainly about errors it has no words for. An unknown reason used to print the raw Erlang term in the top bar. It now gives a plain sentence and keeps the term in a Technical detail disclosure for whoever has to report it.

  • Change Dutch picks one word and sticks to it. The Dutch catalogue and manual had been hedging: team and ploeg, bewaren and opslaan, rondes and ronden, e-mail and email, sometimes in the same paragraph. It is now team, bewaren, rondes and e-mail everywhere in the Dutch interface, the Dutch manual and the program chapters, with the articles, relative pronouns and possessives that follow a team (het team dat, elk team, zijn teamlijst) fixed by hand rather than by a find-and-replace that would have turned afronden into something Belgian. Compounds followed: teamtoernooi, teamlijst, teamrangschikking. Informal je stays.

  • Removed The Screenshot to come placeholders. Twenty-three manual figures promised a screenshot and delivered a checkered box saying so, for months, in two languages. The empty slots are gone from every English and Dutch chapter, and SCREENSHOTS.md is an empty list waiting for pictures that exist. The renderer still knows how to show a real image, should one ever be taken.

[0.102.0] - 2026-10-22

  • Fix Error pages are pages now, and a dead link says why it is dead. A missing page or tournament used to answer with two words of plain text and nothing to click; it now says it does not exist, was deleted or is no longer yours, and offers the way back to Tournaments, as does the server-error page. Refusals in the print tabs (a round not paired yet, a preview nobody worked out) get the same treatment with a link back to Print documents, the old public links answer with the same page, and the rating inbox and backup downloads send a vanished file back to the page it came from with a sentence, not a line of text in an empty tab. Statuses are unchanged; only the dead end is gone.

  • Fix A button for something that is already gone no longer crashes the page. Handing off, returning, deleting, restoring or purging a tournament purged in another tab, a badge or badge event deleted elsewhere, and a restore point that vanished all said nothing useful, or said 404. They now say what happened and refresh the list. An invitation to a tournament its owner deleted is no longer listed, cannot be accepted, and its link says "This tournament was deleted by its owner." instead of crashing. Accepting or declining an invitation that is no longer valid says so; the wrong-account page offers to switch account; the invitation names the role it is for (an arbiter is told they get results on the phone, not full editing access); and declining asks first.

  • Change Destructive buttons ask, and actions that worked say so. Removing the publishing token, clearing the new-tournament defaults, removing a badge photo or logo and declining an invitation now confirm, saying what goes and what stays; the rating-list confirm names the list. Moving a tournament to the Recycle bin, restoring it, deleting it for good, leaving, archiving and copying one each show a notice naming it, and the dates in the Archived and Recycle bin tables are dates instead of raw timestamps. The role-change confirmation on the Admin page is a dialog instead of a card below a long table, and a role change that fails says so instead of crashing the page.

  • Fix Failures no longer show the program's internals. A hand-off file, SWAR file, rating-inbox check, restore or backup that failed used to print an inspect dump or a server path; it now logs the detail and shows a sentence. A registration whose email could not be sent no longer tells the person to check an SMTP configuration they do not have, a password change outside sudo mode redirects instead of crashing, and "name can't be blank" is a translated "Naam mag niet leeg zijn".

  • Change More of the program speaks Dutch. The ten print documents, the History page (headlines, "Today", "System", the restore buttons), page titles, the sign-in and single sign-on messages, the server-update banner, and the labels the manual adds (Note, Warning, Figure, Enlarge) were English in the Dutch interface. So were about forty changeset messages ("must be a valid email address", "this pair is already forbidden", ...), which now have errors.po entries, and a handful of untranslated leftovers ("unrated", "Deputy"). magic link, bye and Helper stay as the loanwords the rest of the catalogue already uses.

  • Change Navigation fills in the gaps. The Settings menu in the top bar lists every page the Settings sub-nav does (Group, Forbidden pairings, Live boards and Officials & norms were missing), in the same order; the Help button opens the chapter for History, the audit trail, Connections, Tools and Features instead of the contents page; the Audit page keeps its category and page in the address; the Print page follows the tournament while it is open and says why Next round preview is disabled; and the empty Standings and team tables link to where their data comes from.

  • Fix A change staged on a round that another tab unpaired no longer takes the page down. The Pairings page kept a half-finished hand edit open across a remote unpair, on purpose, and then handed the confirmed change a round that was no longer there, which it took personally. The dialog now closes when its round disappears, and applying one anyway says This round was unpaired elsewhere; nothing was changed. Acknowledging an announcement another tab had withdrawn crashed the same way and now says it was withdrawn; a game page whose game was unpaired goes back to its round with a reason instead of teleporting home; and a hand-typed round in the pairing-rationale address sends you back to the Pairings page instead of a crash.

  • Change Unpairing says what it takes, and what it leaves. The confirmation used to mention the results and stop there. It now names the other round of the match in a match format, the pairing numbers when round 1 goes, and the restore point under Advanced, History; afterwards the page says the round was unpaired and lands on the last round still paired. The round robin confirmation no longer claims the schedule can never change - unpairing from the last round back always could. Draw Chess960 position and a phone code's Revoke now ask first: one cannot be drawn twice, the other signs a phone out mid-round. A revoked code says so, and a code already gone says that instead of nothing.

  • Fix Refused hand edits explain themselves. Could not apply that change: not in round was an atom wearing a sentence's clothes. Each refusal now says what happened (the board or player has gone, the seat is taken, only the last board can be deleted) and the page reloads to show where things went. Hiding a board somebody still sits at used to fail silently and now says why; hiding and showing a board is announced like the other edits. The hand-edit banner finally says how to make one: right-click, or tap, a player.

  • Fix The Pairings page, live round, phone results and CSV import speak Dutch all the way through. The pair button, the hand-edit dialogs, the CSV import's line-by-line complaints, the incidents table (now in a card that scrolls sideways on a phone, its note wrapping) and a dozen flashes were English whatever the language setting. They are translated now, and the CSV import's problems are worded on the page rather than in the importer. A link to a postponed game lands on its board, not just its round.

  • Fix Removing a player, or changing everyone at once, asks first now. Remove on a player who had played emptied their seat in every one of those games, changed the opponents' results, and did it on one click with nothing to go back to. It now says how many games are involved, points at withdrawing instead, and keeps a restore point. The header menus that set everybody absent, paid or in a category rewrote the whole roster the same way; they ask, say how many players they changed, and the absent and paid ones keep a restore point too.

  • Fix The player form stops throwing away what you typed. A click beside it closed it, discarding the edits; it now closes with Escape or Cancel only. A FIDE lookup by a name that matches several players used to take the first one without a word, and answering No to the changed-name question left the FIDE ID, title and rating in the form. It now asks for the FIDE ID, and No puts the form back as it was. Saving a player who was removed in another window closes the form and says so instead of crashing, and a row that is no longer there is called gone rather than ignored.

  • Fix Categories, extra points and teams answer when you act. Remove on a category asks, naming what goes with it; Save rules says Rules saved.; a rule whose from is not below its below is refused; Assign categories waits for a save instead of quietly using the old rules; and typing in the rules table survives somebody else touching the tournament. Remove half a point and Apply bands ask first and keep a restore point, and Save first now notices bands you have typed and not saved. The add forms are empty again after an add, and the teams page answers with a floating message beside the buttons you pressed rather than a line at the top of a long page.

  • Fix More buttons that cannot be undone ask first. Removing a forbidden-pairing rule, group or pair, removing a collaborator, Not played in this event, and Clear all round dates now say what they are about to do. The saved note goes away as soon as you edit again, a failed date save says why, and a failed invitation e-mail shows a link you can actually copy. In Officials & norms, text typed since the last save survives adding, removing or picking an arbiter, and each additional arbiter has a Remove of its own. A search that finds nobody says so, and says when the FIDE list has not been downloaded.

  • Change The grid, the menus and the error sentences speak Dutch. The player grid's column names and tooltips, its right-click menus, the restore-point labels, the tournament setup, dates, scoring and logo messages, the registration queue's refusals, and the checks on a changeset now follow the language instead of falling back to English, and a refusal no longer prints a raw reason such as not in round or an inspected tuple. A norms form that cannot be generated redirects with a message instead of a plain-text 500. The manual follows, with the Group page in the settings table and the Dutch names aligned with the screen's own.

  • Verified Publishing to OpenResults is checked against OpenResults' own contract. The results site now publishes a schema for everything it accepts and answers; this app keeps a copy, checks every snapshot it builds (team events, byes and forfeits included) and every answer it reads against it on every test run, and asks GitHub once a day whether the copy is still current. Nothing was wrong, which is the least interesting and most reassuring thing a test can find. The next renamed error code will not get to be quietly unhandled.

  • Verified The Manubrium API and the board link now have machine-readable contracts. priv/contracts/manubrium-v1/ and priv/contracts/alnasl-link-v1/ hold a JSON Schema for every request and answer, with examples taken from what the server really sends, and the test suite checks the server against them on every run. The phone app and the hall box keep a copy and check it daily, so the next time one side renames a status or counts moves differently, a build goes red instead of a board in a playing hall going quiet. Board statuses are a closed list (waiting, live, finished) and moves_count is written down as full moves, which is what it was all along; it just was not written down where anyone would trip over it. The three calls that answer with nothing at all (unpairing a round, withdrawing a hall-box code, signing a phone out) are in it too, since "nothing" turned out to be the one answer the first draft forgot to write down.

  • Fix A hall-box code made from the phone keeps the name you gave it. The Manubrium app has always asked for a name when making a code for a box, sent it, and watched the server ignore it in favour of the code's last four characters. The contract noticed in about four seconds. The name is now kept and listed.

  • Fix The time a phone's result was entered carries its time zone. It was the one timestamp in the Manubrium API sent without a Z, which a phone reads as its own local time: an hour or two out anywhere east of Greenwich, and quietly so.

[0.101.3] - 2026-10-21

  • Removed Plug-ins can no longer draw over every page. The one plug-in that did used it for a single face in a single theme for two accounts, and that face has retired. A hook with no callers is a door left open onto nothing, so it went with it; the Tom W theme stays, for everyone, as before.

  • Feature A group archives in one go. A youth championship is five tournaments and one event, and closing it meant five trips through five More menus, each asking the same question. The group's row on the Tournaments page now has Archive group: one question (with the postponed games still unplayed, counted across the group), every tournament on your list archived. A sibling somebody else runs is not on your list and stays theirs to close; one handed off to another copy of the app is left alone, as the single Archive already did.

  • Fix The Help scrolls as one page again. The contents column outgrew the screen when it went from three parts to seven, and it held on to the mouse wheel: with the pointer over it the page refused to move, and with the pointer elsewhere the list stayed half-scrolled. It now hands the scroll on to the page when it reaches its end, like everything else on the web.

[0.101.2] - 2026-10-21

  • Change The manual's contents come in seven parts instead of three. "Reports, teams and sharing" held eleven chapters, which is not a part but a drawer. It is now Reports and exports (printing moved in), Team tournaments, Publishing and sharing, Live boards and Manubrium, the phone app. A few chapters traded numbers so that each part reads in order; every /help/... address stayed exactly where it was, so the links people saved still work.
  • Change The other programs in the Help introduce themselves. Each page opens with the program's mark, its job in a few words, and three paragraphs in the same order for every program, in English and Dutch: what it is, when you need it, and where it fits, with links to the chapters it belongs with. A download button and a source button replace the link that read like a footnote, and a program with several pages gets a row of them to move between. The READMEs' tests, build steps and code layout stay on GitHub, where the people who want them already look; so do the plugin internals of Polis and Nunki. Alnasl's pages are brought up to date, fair-play chapter included.
  • Feature Ainalrami has a page under Other programs. The engine that pairs every Swiss round was the one program the Help never introduced. Now it has the command line, the TRF extension lines, the organiser deviations and the questions of the rules that are still open.
  • Fix OpenResults' page no longer offers its latest release. It has never published one; the link led, with great confidence, to an empty page.
  • Fix A phone incident with no board is refused by name. Manubrium's round-level Log an incident sent entries with no board, which every incident needs, and they came back as "Board ?: not saved. Check with the arbiter." - on the arbiter's own phone, next to a result that had in fact been saved, so the blame landed on the wrong entry. It is now refused as no_board, with a sentence saying what is missing.
  • Change The app's refusals are worded for the person holding it. Signed in to an account, the phone belongs to the organiser, an editor or an arbiter, and telling them to check with the arbiter was sending them to look for themselves. Conflicts and unreadable entries now say what to do instead; they are still refused and still shown, and Manubrium Lite's helpers keep the old wording.
  • Change The app's standings say where the tie-breaks stop. Points count every result entered; tie-breaks only count complete rounds, on the Standings page and in the app alike, so mid-round the leaders sit on a Buchholz of 0 and look broken. The standings answer now carries tiebreaks_through and rounds_paired so the phone can say so. The numbers were right all along, which is not the same as looking right.

[0.101.1] - 2026-10-21

  • Feature Manubrium's privacy policy has an address, /help/manubrium-privacy, in English and Dutch. The App Store wanted a URL, and a Markdown file in a repository did not count. The short version: the app talks only to the servers you add, keeps a token and an outbox, and has nobody else to tell.

[0.101.0] - 2026-10-21

  • Feature Manubrium can start a tournament, not just run one. New tournament on the phone offers the systems the desk's form offers and starts from the account's own defaults, then creates through the very function the form calls: same derived type, same seeded round dates, same hosted limit, same audit row. Team events are still made at the desk, which has room for the line-ups.
  • Feature Players from the rating lists, from the phone. The app searches the FIDE list, and the Belgian member list where it is switched on and actually has members, with the Players page's own search; picking a hit fills the player exactly as the add form would, main-list rating included. Somebody already in the tournament is marked as such and refused if added anyway, since one of them is plenty.
  • Feature Check-in and requested byes in the app. Present is the Players page's Pr. column, so ticking off arrivals at the door means the same thing on both. A player's requested byes are listed per round with their kind, and the app can replace the ones for rounds not yet paired, through the player dialog's code and its confirmations; a round already paired is refused, because rewriting history is what hand edits are for.
  • Feature The results-site link and the hall boxes, on the phone. A published tournament hands the app its OpenResults address for sharing. Each board in a round carries what a hall box saw - status, moves, result - with the disagreement flagged; the box still enters nothing, it only testifies. The organiser and editors can list the boxes' codes, make one (shown once, as on the Boards page) and withdraw one.

[0.100.0] - 2026-10-21

  • Feature The phone pages have a name now: Manubrium Lite. Still a page in the phone's browser, served by the program itself, no install, no store, no account: the same code, the same helper and deputy rules. The full Manubrium - an actual app that runs the whole tournament - is a separate thing, and Lite is not going to pretend to be it by asking for a spot on the home screen.
  • Feature Manubrium Lite keeps results when the hall wifi does not, as long as the page stays open. A tapped result goes into an outbox on the phone and is sent from there, in order, when there is a connection; the header says how many are waiting and each board says Waiting, Sent or Not accepted. The server judges every result when it arrives, not when it was tapped: if the code was revoked meanwhile, the round moved on for a helper, the players on the board changed or someone entered a different result, the phone's entry is refused per board, with the reason, and stays on screen until dismissed. Nothing is dropped quietly and nothing overwrites a result entered at the desk while the phone sat in a basement. A resend after a lost reply is recognised and saved once. Clearing a result still needs a live connection; un-entering things on a delay is how one gets stories.
  • Feature Incidents from the boards. A deputy phone logs an illegal move, a clock problem, a phone or electronic device, a complaint, time added, a game rewound, an arbiter call or something else, with a short note; the board, round, players and time come along without anybody typing a name. A helper phone gets one button, Call arbiter, on the theory that a helper watching an argument needs the arbiter, not a form. The Pairings page says how many a round has, and More, Incidents lists them per round, newest first, updating as they come in. Rate limits and a cap per code keep a stuck thumb from writing a novel. A printable report comes later.
  • Feature Arbiters get accounts, and only the part of the tournament they need. Invite someone as an Arbiter instead of an Editor and they can enter results and log incidents from their phone, under their own name in the audit trail, and do nothing else: no desk pages, no pairing, no quietly fixing a player's rating on the way past. They connect at /m/connect, one tap for the phone in their hand or a QR for another one. Remove them, or let them leave, and their phones stop working in the same breath. Helpers keep their no-account codes; not everybody who carries scoresheets wants a login.
  • Feature Phones can reach the desktop program over the hall network. Switch on Let phones on this network connect in Phone QR, pick the computer's address, and phones on the same wifi scan the QR and are in. The program itself still listens only to its own computer; a second door opens beside it that serves the phone pages and nothing else, to private addresses only, and only while a phone code is active. It is plain http, like the board link, because nobody in a school gym is issuing certificates. Off by default, because the safest port is the one that is not open.
  • Feature Arbiters' boards. The Phone QR dialog gives each arbiter a range of boards, and My boards on their phone shows exactly those. It is a view, not a fence: an arbiter can still walk to board 40 when board 40 is on fire.
  • Feature The board's own result beside the entered one. With live boards in the hall, every board on the phone shows what the board saw, and turns red when it disagrees with what was entered. It never enters anything itself: a sensor that saw a king knocked over has opinions, not authority.
  • Feature Two arbiters, two results, one board. The second entry was already refused; now both phones are told, in red, on that board, until someone settles it. Nothing is overwritten, which is the whole point of having two arbiters.
  • Feature Incident photos. A deputy can attach a photo taken on the spot. The phone shrinks it first, the server checks that it is a JPEG of sane size, and it lives with the tournament: visible on the desk's incidents page, nowhere public, not to arbiter accounts, not in exports, and gone when the tournament is. Every incident also lands in the audit trail now, where incidents belong.
  • Feature The hall view grew up. Each board says Playing or Finished; the round shows how long it has been going, counted from publication or else the first board report, and it says which instead of inventing a start time it does not have; a search finds a player or a board; Keep the screen on stops the phone dozing off mid-round where the browser allows; and a one-handed layout puts everything within reach of a thumb, since the other hand is usually holding a scoresheet.
  • Fix Picking a theme closes the theme menu. It used to stay open over the page after the choice was made, which on a phone looked exactly like a picker that had jammed. Keep the screen on now lets go of its button when the browser refuses, instead of looking pressed while the screen sleeps anyway.
  • Feature The board list on a phone can be narrowed to a range. With more than six boards, Boards from - to shows only that part of the room; the code's own range still decides what may be entered. The buttons are bigger, too, for thumbs in a dim hall.
  • Feature Manubrium, the app: run the tournament from the hall, not just its results. A native phone app, separate from the browser pages, that holds several servers at once and opens on the current round. The organiser and editors can pair the next round, undo the latest one, publish, read the standings with the real tie-breaks, take a late entry, withdraw and re-enter players and give them rounds off; arbiters enter results and log incidents, as everywhere else. Results tapped without a signal wait on the phone and are judged by the same rules as the browser pages' when they arrive. Every write goes through the function the desk already calls, so the phone has no rules of its own to get wrong; where the desk would stop and ask a question - pairing numbers, leaving FIDE mode, bye exclusions, hand edits, round robins, team events - the phone refuses and points at the desk, on the grounds that a dialog nobody reads is worse than none.
  • Feature Connect a phone from the account page. Account, Manubrium app, Connect a phone shows a QR code that works once, for ten minutes, and turns into a token for that one phone; the server keeps only hashes. The page lists the connected phones and signs any of them out on the spot, which also cuts off the results it was entering. A desktop copy hands out its hall-network address instead of its loopback one, provided Let phones on this network connect is on; otherwise it says so instead of printing a code nobody could use.
  • Feature Hand edits from the hall. The Manubrium app swaps two players, takes one off a board, gives a lone player the bye, fills an empty seat, pairs two latecomers on a new board and removes an empty last board - the Pairings page's edits, through the Pairings page's code. Where the desk would ask first (an old round, a round already sent for rating, a rematch, a second half-point bye), the phone asks the same question in the same words and does nothing until answered. Finish hand edits runs the pairing checker and keeps boards it disagrees with only on request, recorded as the round's MPA PIBE. Team events stay at the desk, which can at least see the matches it is breaking.
  • Feature Mid-event settings from the app. Time control, arbiters, venue, rounds, the bye's value, whether the bye type is asked, shared places and tie-breaks - the things that change once the first clock is running, and nothing structural. FIDE mode's locks hold on a phone exactly as on the Settings page; a locked field is greyed out and, if somebody insists, refused with the same sentence.
  • Feature A Lite phone's result waits for the arbiter's word. A result entered on a Manubrium Lite phone shows in the app's round marked Phone, with a count above the boards. Confirm keeps it; Reject clears the board and the audit trail names the phone that was overruled. It is the same audit row the desk already kept, read a second time - there is no new flag to fall out of step with the first one. Results entered from the app itself are the arbiter's own and need nobody's blessing.
  • Feature Incident photos from the app. Take a photo while logging an incident and it waits in the outbox with the incident; add one to an incident logged earlier from More. The server takes a JPEG by its first bytes, not by what the phone called it, at most one per incident and nothing large; only the organiser and editors can look at it. An arbiter's phone can send a photo and cannot open anybody's, which is a policy, not an oversight.

[0.99.1] - 2026-10-21

  • Change The Mac app is signed with a Developer ID and notarised by Apple. Until now it was signed with nothing but good intentions, and every Mac that downloaded it asked the arbiter to override Gatekeeper by hand, which is a strange way to greet someone who just installed your software. The release workflow now signs every binary inside OpenPairings.app, hardened runtime and BEAM entitlements included, sends the bundle to Apple, staples the ticket and checks the verdict with spctl before the zip exists. A fork without the secrets still builds, with a warning, ad-hoc signed. The workflow can also be started by hand with mac_only to test all this without publishing anything.
  • Change Releases carry signed build provenance. Every file of a release, SHA256SUMS included, is now attested through GitHub and Sigstore, keyless, so gh attestation verify <file> --repo teapot-chess/openpairings can say who built it and from what. Until now the checksums vouched for a download only against the release they sat in, which is a witness who was hired by the defendant.

[0.99.0] - 2026-10-20

  • Security One account can no longer keep the whole server busy pairing. On a hosted server every "Pair round" was its own engine run with nobody counting: the guard against a second click lived in one browser tab, so ten tabs on a 400-team Swiss were ten searches on a two-core machine, each carrying on after its tab was closed and none with any time limit. Pairing now goes through a small number of slots for the whole server (two by default, PAIRINGS_ENGINE_SLOTS), one per tournament however many tabs ask, and a run stops after three minutes (PAIRINGS_PAIRING_TIMEOUT_S) with nothing written. A team round that has to wait says it is queued instead of looking hung; an individual round that cannot start within a few seconds says the engine is busy; a round already being paired elsewhere is not paired again. The next-round preview runs one at a time and the explanation's background work two at a time, for the same reason. The desktop build keeps no count: the only person it could starve is the one who pressed the button.
  • Security Accounts on a hosted server have ceilings. At most 200 tournaments per account (PAIRINGS_MAX_TOURNAMENTS; the recycle bin does not count), checked on every way a tournament arrives - new, duplicated, imported from a backup, TRF, SWAR or a hand-off - and at most 20 files read per account per hour, since each one is up to ten megabytes or twenty thousand records parsed on the spot. Nobody running an actual tournament will meet either; they exist for the account that is not running an actual tournament. The desktop build has neither.
  • Security Redeeming your own phone code no longer resets the guessing limit. A correct code on /m used to wipe the address's count of wrong ones, so an account that minted its own codes could redeem one every seven guesses and walk the entire eight-digit space at leisure. Wrong guesses now simply age out, and an account mints at most 40 phone codes an hour - every active code being one more right answer for whoever is guessing.
  • Security A removed collaborator's hall boxes stop working. A board-link code made for one tournament checked only that the tournament still existed, so a collaborator removed from it kept a box with write access to its results for as long as the code lived. Such a code now works only while its maker can still open the tournament, checked on every request as group and account codes already were, and removing a collaborator - or a collaborator leaving - withdraws the codes they made for it.
  • Security Single sign-on no longer couples to an account by an address nobody verified. Signing in through 02cloud matched an existing account by the email in the directory profile and stamped its own identity on it, without asking whether that email had ever been verified - so anyone able to type an arbiter's address into their own profile could sign in as that arbiter. Now only a verified address couples; an unverified one is ignored and the sign-in carries on under the directory username's SSO-domain address, as it already did for profiles with no address at all. An account already coupled to a different directory identity is never re-coupled: the sign-in is refused with a message, and the log notes it without the address.
  • Security Nobody can lock somebody else out of password sign-in any more. Wrong passwords and requested log-in links were counted together per address and checked before the password, so five tries at anybody's address - from anywhere - shut that person out for a quarter of an hour, correct password and all. Wrong passwords now count per address and client together, so a guesser runs out of tries where they sit and the owner, signing in from elsewhere, notices nothing. Log-in links and sign-up mails keep their per-recipient limit, because they send mail, but it no longer refuses a password.
  • Security A demoted administrator stops administering on the next click, not the next reload. The Admin page checked the role when it opened and then trusted it for as long as the tab stayed open. Every action now re-reads the acting account first, and a role change sends the affected person's open pages back through the door, so they come back with whatever role they now have.
  • Change The toggle that was called "Live link on the results site" now says what it does. It is "Show the live boards on the results site", with a hint that says the same. Under the old name, a reasonable arbiter could take it for a link to something, or a link between things, or a mood.
  • Feature The results-site settings page opens with a link to the tournament's page on the results site. Once the tournament is published, a link at the top opens it in a new tab, and the address in "Connected to ..." on the Live boards page is a link too, instead of text one has to copy into a browser like it is 1996.

[0.98.3] - 2026-10-20

  • Fix Stopping from Ascella, an update or a logout no longer hangs on "Stopping". On a Mac, the window waited for its server to end inside the very queue that was to bring the news, so the server ended and the window kept saying "Stopping" until somebody force-quit it. Quit from the menu never had the problem, which is how it survived this long. Any stop that hangs anyway now ends after forty seconds.

[0.98.2] - 2026-10-20

  • Feature Intel Macs, and macOS back to 10.13, get their own app. openpairings_app_macos_x86_64.zip sits beside the Apple Silicon one on the release page. Intel Macs were dropped on 2026-10-14 because GitHub's Intel runner added thirteen minutes to every push; it now runs only when a release is about to go out (or when someone starts the workflow by hand), so ordinary pushes cost what they cost yesterday. The Intel app is compiled on a real Intel machine for macOS 10.13 and newer; the Apple Silicon one still asks for 11. Not tried on a Mac that old: CI proves that nothing in the bundle asks for more than 10.13 and that the app starts on a current macOS, and a High Sierra owner is invited to prove the rest. A release that cannot build for Intel is not published.

[0.98.1] - 2026-10-19

  • Change Published tournaments tell OpenResults how long the clocks start at. The time control already travelled, as prose the results site had to read like tea leaves; the first period's minutes now go along as a plain number, so the live boards can show the starting clock before the first move. A time control nobody can parse sends no number rather than a guess.

[0.98.0] - 2026-10-19

  • Fix A tournament the results site deleted is no longer asked about forever. When the site's operator deleted a tournament, every publish came back refused and the queue kept trying, politely and indefinitely, like a door-to-door salesman at a house that has been demolished. Now the first refusal switches publishing off for that tournament, drops it from the queue and says on its OpenResults settings page that the results site removed it. Switching publishing on again clears the notice; whether the site takes it back is up to its operator.
  • Security Redirects from the results site are no longer followed. A publish or a connection check that was answered with a redirect went along with it, taking the token and the tournament key to whatever address the answer named. Now nothing is sent anywhere it was not configured to go: the connection card says the results site moved, shows where it claims to have gone, and leaves changing the address to you.
  • Security Taking over a tournament from a backup is checked with the results site first, on a hosted server. One token serves every arbiter on a hosted server, so the tournament key in the file was the only thing telling one arbiter's tournament from another's, and a backup is a file anyone holding it can edit. Taking over now asks the results site whether that address really is claimed with that key, and refuses if it is not - or if the site is too old to answer. Desktop installs are unchanged: their own key is checked on every publish anyway.
  • Security A plug-in's player list is for administrators. The Teams page offers "fill from the list" from whatever a plug-in has (roster_candidates), and unlike the plug-in's menu and Settings entries that hook was not held to administrators, so a collaborator on a tournament could pour a federation's player list into it. It is held to them now.
  • Fix Withdrawing a double forfeit the pairing wrote no longer hands out a draw. When both teams of a match were out of the round, the pairing wrote it as a double forfeit with no boards. Withdraw the decision then had nothing to give back, cleared the flag anyway and left an empty match, which scored as a 0-0 draw: a draw's match points to two teams that never turned up, and in a Swiss the next round was paired on them. With required line-ups there was no way back either. Withdrawing such a decision now seats the match again, exactly as the pairing would seat it today, so the way back is: mark the team present, withdraw the decision. While nobody could be seated it is refused with that advice. A decision taken by hand still gives the boards back what they held, and the button's question now says which of the two it is about to do.
  • Fix Two withdrawn teams no longer leave one of them a forfeit win. In a team round robin paired in advance, when both teams of a future match withdrew, the first withdrawal forfeited the match to the second team and the second withdrawal politely left it alone: one team kept a win for a match nobody would play, and which one depended on the order of the clicks. The second withdrawal now turns that forfeit into a double forfeit, the same thing a round paired after both withdrawals writes, and reinstating either team gives it the forfeit win back.
  • Fix A decided match's boards cannot be edited underneath the decision. A board inside a double forfeit or a match forfeited by decision used to accept a result, leaving the match decided one way and its boards saying another - game points moving, the teams counting as having met, the TRF still writing the forfeit. Such a board is now refused with a message to withdraw the decision first, and the Pairings page shows the decision's result there instead of offering a picker it would only refuse.
  • Fix A match awarded to a team that seated nobody counts as won. With required line-ups such a match is 0-0 on the boards and a win in match points; the standings counted it as drawn in won-drawn-lost, and the tie-breaks were told it was a forfeit loss for the team that won it, which C.07 Art. 16 treats rather differently. Both now read the match points.
  • Fix The BB column counts a team Swiss bye. The pairing-allocated bye has no boards, so the column added nothing for it, while the Board Count that actually ranks the teams counts it as a win on every board (C.07 Art. 12). The column now agrees with the order it sits next to.
  • Fix Direct encounter shows the number that ranked the players. C.07 Art. 6.1.1 leaves forfeits out unless Art. 15.2 makes them games, which it does in a round robin and only there. The order already read it that way; the DE column did not: for teams it counted a forfeited match in a Swiss, and for players it left a forfeit out of a round robin. Same rule in both places now, so the number on the page is the one that decided.
  • Fix Swapping two rounds of a team round robin no longer seats somebody in a round they cannot play. A round's line-ups are built for its own number, absences and late starts included, and the swap moved them wholesale: a player marked absent for round 4 could find themselves on a board in round 4, with the reserve playing round 5 in their place. The swap is now refused while a seated player cannot play the round the boards would move to, and the message names them. Rebuilding the line-ups instead would quietly change who plays, which is the arbiter's call, not the swap's.
  • Fix Moving a player or team that has just left the list no longer crashes. The arrows worked out the neighbour before checking there was anything to have a neighbour, so a player a co-arbiter had moved to another team raised an arithmetic error instead of doing nothing.
  • Verified Team match points are fixed in FIDE mode once round 1 is paired. C.04.6 Art. 1.2.1 has the competition's rules state the scoring in advance. They already were held, like the bye's value, with the field shown locked on the Scoring page; this is now written down in the manual and pinned by tests, so it stays that way.

[0.97.2] - 2026-10-18

  • Fix Ticking "Tournament lasts more than 30 days" shows what it opens straight away. Reporting to FIDE and the rating options for tie-breaks used to appear only after Save, which is a strange way to ask somebody a question: tick, save, scroll back, discover there was more. Now they appear with the tick and disappear with it; nothing is stored until Save, as before.
  • Fix Stopping no longer ends in a red stack trace. On the way out the launcher-details process tidied up its secret by asking the application controller, which at that point is busy dying and answers everyone with an exit. The secret goes with the VM anyway, so now it lets the question go unanswered and leaves quietly.

[0.97.1] - 2026-10-18

  • Fix Old public links work again. When the public pages moved to the results site on 29 August, their old addresses here (/p/<code>/standings, /pairings, /register) were simply deleted, so every link already on a wall chart, in a club newsletter or in someone's bookmarks answered "not found" for seven weeks while the tournament sat one domain over under the same code. They now forward permanently to that page on the results site. Removing the pages was the right call; making the links die with them was not.

[0.97.0] - 2026-10-18

  • Fix The results site's new name no longer leaves your old pages behind. A desktop copy that publishes to OpenResults without a configured address followed the default from openresults.zerotwo.cloud to openresults.cloud. The installation key and every page address are bound to the server that issued them, and the program took the new name for a new server: it registered again, gave every tournament a new address, and left the old pages up and frozen, looking perfectly current to anyone holding the old link. The two names are now one server. Copies that have not published since the move keep their key and their addresses; copies that already moved keep the new ones, and the frozen old pages are for the site's operator to take down.
  • Feature A long event can be reported once, at the end. Under "Tournament lasts more than 30 days" a new choice, Reporting to FIDE: monthly reports (what it did before, and still the default) or one report at the end, as most club championships are actually registered. In the second mode the rating inbox counts every round from the last day like any short tournament, so October stops being "overdue" in November for a file nobody meant to send until April. Send warns, without refusing, when the file holds fewer rounds than the tournament, because FIDE takes a report only against a registration with the same number of rounds; monthly mode gets a one-line reminder of the same rule. A postponed game played before the final report is just a result in it, and the Export page now says so instead of letting you build a separate file for it.

[0.96.0] - 2026-10-18

  • Fix Refresh ratings no longer overwrites the starting rating in a tournament lasting more than 30 days. There the starting FIDE rating is what round 1, the pairing numbers and the default tie-breaks stand on, and the button replaced it with whatever the newest list said. It now adds each changed rating as a later rating from a round (by default the first round dated on or after the list, else the next one to be paired; one box changes it for the whole batch), replaces one already given from that round, and leaves the starting rating and every pairing number alone. The automatic notice, which compared the wrong month for such an event anyway, stays quiet there.
  • Feature A "?" beside "Tournament lasts more than 30 days". The paragraph of hint that sat under the tick is now a small popover: hover, focus or tap, Escape to close. It is a reusable component, so the next setting that needs an essay can have one too.

[0.95.0] - 2026-10-18

  • Change Ainalrami v0.44.0. The engine now takes a ? result for what TRF26 says it is, a played game worth the file's X, instead of refusing it. OpenPairings never hands it one, so nothing here pairs differently; the standalone engine just stopped falling over on files this app writes.

  • Fix An empty board no longer stops the tournament. Mark both players of a board absent and the board stays behind with nobody at it and no result. Since 0.93.0 the next round waits for every earlier round's results, and it waited for this one too: a result nobody could enter, for a game nobody was playing. Round 3 then refused to pair with "Round 2 still has missing results", and hiding the board did not help, because hiding is cosmetic. A board with nobody at it now holds nothing up; a board with one player left still does, because that player is owed a bye or an opponent. The Hide this board button also learned Dutch.

  • Feature Rounds changed elsewhere now say so. The Pairings and Players pages already reloaded when a colleague paired, unpaired, re-paired or published a round, which left you wondering why round 4 had appeared out of nowhere. A short notice now names the round and says "elsewhere" (the data keeps no author, and inventing one would be worse). The session that did the pairing hears nothing, and a result entered by someone else stays a quiet reload.

  • Feature Restore points before bulk changes. One click that rewrites a whole roster now leaves a restore point behind, labelled with what was about to happen: giving everyone a category, Assign categories, re-seeding the pairing numbers or the starting numbers (rating, entry order, lots), Refresh ratings and the player-data refresh (only when something is actually applied), the extra-points bands, re-seeding the manual ranking, and ordering teams by rating. The audit trail records what changed per field, which is a poor way to un-change forty players. Automatic points are still capped at the newest fifty per tournament; the ones you save by hand are now exempt from that pruning and no longer count towards it, since a busy bulk-editing afternoon would otherwise evict End of day 1.

  • Feature The entry form states its fee. Settings, Results site, Entry form gains an optional entry fee: an amount, a currency and a free-text note ("free for juniors"), shown on the public form. It is a sentence, not a till: nothing is paid through it, and nothing sends the entrants an e-mail either, which is now the written-down decision rather than an omission.

  • Feature Entries waiting for you can be listed by name, marked unconfirmed. With the public entry list on, the entries you have not decided on appear on it by name only, under an "unconfirmed" badge, unless you switch that half off. Discarding an entry takes its name off the next copy the results site receives, names are cleaned and capped before they travel, and the page escapes whatever a stranger decided to call themselves.

  • Change Officials & norms is its own Settings page, under Reporting. The chief arbiter, deputies, further arbiters, e-mail addresses and IT3/IT4 extras moved there from the Norms page, where nobody looking for "chief arbiter" thought to look, and the event type for title norms moved there from the FIDE page. The Tournament page's card explaining where the officials really were is gone, its job done by being unnecessary. The Norms page keeps the reports and the judgment, and links to the page when something is missing.

  • Feature The big open Swiss clause (B.01 1.4.3 d) is spotted on its own. An individual Swiss with at least 20 FIDE-rated players from outside the host federation, from 3 federations or more, 10 of them GM/IM/WGM/WIM, each missing at most one round (a pairing-allocated bye is not missing), lifts the federation mix for everyone, and the norm judgment now notices without being told. Nothing to set; the new page says whether it applies and with what counts. Players under the FIDE flag are not counted, and a tournament with no federation set never claims it.

  • Verified The event type travels in the JSON export and comes back on import; a file from before it existed imports as an ordinary event, which judges exactly as before. The TRF has no field for it (record 092 is the pairing system), so the rating report is unchanged.

  • Feature Uploading a SWAR file you already have: compare and merge. Instead of choosing between a second copy and doing nothing, the panel now lays the file beside the tournament - settings, players, results, byes, pairings, rounds - and lets you take from it what you want: a section at a time, a game at a time for results. A result the file adds where there was none is taken; one that disagrees with a result entered here is a conflict, and stays as it is unless you say so. Played rounds are never re-paired, nobody is renumbered, and the file's later rounds can be added. A pinned restore point is saved first, because everybody trusts a merge right up until they read it.

  • Fix A TRF's unknown result (?) is worth what the file says it is. TRF26's record 162 can value an unknown result with X; the import read the X and then counted every ? as a draw anyway, complained that the file's points did not add up, and judged the later rounds on scores the file never had. A ? now counts as the file's X - a win, draw or loss in the file's own points, a draw when there is no X - for both players, and an X other than the draw becomes the tournament's postponed-game valuation, which FIDE mode notices. An X that is none of the three is refused with the value named, rather than rounded into something the file did not say.

  • Fix The X survives the trip back out. The TRF26 download wrote X at a draw whatever the boards counted; when every ? in the file counts as the same win or loss, X is now that value, so the points column matches the standings and importing the file again gives the same tournament. Mixed valuations still get the draw and a file ranked by its own points. The export page's note says which of the two you are looking at.

  • Change An imported ? explains itself on the Pairings page. The board still shows * postponed, but its result menu now says, on hover, that this may be a TRF's unknown result and that the real one goes right there. You still cannot type ?: "nobody knows" is not a result, it is an apology.

[0.94.0] - 2026-10-18

  • Feature Rooms. Settings, Rooms gives a hall of more than one room its names and the boards each holds (1-20, 35, 40-45), checked as you type: no name twice, no board in two rooms, and a board in none is its own business. The Pairings page then names the room above its first board, and both printed pairing lists grow a Room column, so the player looking for board 37 learns it is in the annexe before walking the main hall twice. A tournament without rooms looks exactly as it did. Rooms travel in the JSON export and come back on import.

[0.93.0] - 2026-10-18

  • Change New address. The repository moved to github.com/teapot-chess; old links still redirect, out of politeness rather than obligation.
  • Fix A TRF saved mid-round comes back mid-round. A board with both players and no result yet used to be dropped on import, quietly, which made the round look finished and let the next one be paired over games nobody had played, this program's own export included. Such boards now come in as games still being played, the import report names their round, and the next round waits for them.
  • Fix Every round's results count, not only the last one's. Clearing a result in an earlier round and forgetting to re-enter it used to let the next round be paired anyway, and the engine, reading that game as never played, was free to pair the same two again. The Swiss match format had the same blind spot for the first leg of a match. Now any board without a result, in any round, holds the next round up and the refusal names the round.
  • Fix Baku's virtual points stay what they were when a round was paired. They used to be worked out again on every pairing from the number of rounds and the acceleration setting as they stood that day, so shortening the event or switching Baku off rewrote the scores rounds already played had been paired on. Each round now records its own, and later rounds read them back. Older rounds, with nothing recorded, are worked out as before.
  • Fix Correcting pairing numbers no longer moves somebody across the Baku line. An exchange or a regeneration after round 1 kept Group A's boundary at the same number while the players under it changed; the last player of Group A now stays the last player of Group A (C.04.7 1.3.2).
  • Fix A category of one no longer collects the bye every round. Pairing by category handed its only player the pairing-allocated bye in round 1, round 2, and on, without consulting C.04.3 C2. Once that player has had it (or a win without playing), the round is refused as having no legal pairing, with the offer to pair it by hand.
  • Fix Two Pair clicks at once no longer number a late entrant twice. The numbers are now worked out from the rows as they are, under the write lock, so the second run finds nobody left to number instead of shifting the field again and leaving a gap.
  • Fix A round being filled in by hand can no longer be paired over by anything other than the Pairings page, which was the only thing that checked. The check now lives where the pairing does, and the empty round and its hand-edit session are created in one go.

[0.92.0] - 2026-10-18

  • Change The local view is now only the boards. The QR codes it used to carry were one more thing on a screen meant for a projector. The Pairings page's More menu has Local view (the page, minus the codes) and a new Phone QR, which opens a large dialog over the Pairings page: the results-page code when the tournament is public, and the phone result-entry codes, each with its address in words. It closes on the button, Escape or a click outside it.
  • Fix The players page's (?) and (i) now stand level. Same size, same centre line, which is the least a pair of icons can do.
  • Change The Plug-ins menu is one line per plug-in. The name only; the description moved to the tooltip, and the version stays on the Installed plug-ins page.
  • Change Group is its own Settings page, in the Set-up list right after Tournament. It was a card at the bottom of the Tournament page that most tournaments scrolled past; the Tournament page keeps no trace of it. The rail already lists every page, so the Options page's card that only said Forbidden pairings has its own page is gone too.
  • Change On the Scoring page the whole Byes and absences card is advanced. Its points and limits count in the N advanced settings are not at their defaults link too, so a changed value no longer hides without a word.
  • Fix A Settings page with nothing advanced on it no longer offers the Show advanced settings switch. Each page now says whether it has advanced content for this tournament as it stands, and a test walks every page for Swiss, round robin and team tournaments to keep the switch honest.
  • Feature A plug-in can put a page in a tournament's Settings (hosted edition only), in the rail beside the core's own: under Reporting unless it asks for Set-up or Publishing, administrators only, like every plug-in entry. Things configured once per tournament belong there, not in the top bar every day.
  • Feature The Help now also holds the manuals of the other programs in the family: Alnasl, Arkab, Ascella, Polis, Nunki and OpenResults, under Other programs at the bottom of the contents. Their text is copied in on purpose, so an arbiter with six programs in one hall can read all of it in one place, and the search looks through it as well. Alnasl's manual is available in Dutch; the others show their English text on the Dutch side and say so. Pictures are not copied, links between pages of one program stay in the Help, and anything else points at the program's GitHub. mix help.sync_family refreshes the copies from the sibling checkouts and records the commit each came from, because the public builds cannot see those checkouts and somebody has to carry the text across.

[0.91.0] - 2026-10-18

  • Feature A plug-in can do work when the program starts (hosted edition only), once the database and the rest are up, instead of waiting for somebody to open a page. One that fails is logged and the program starts regardless, because a plug-in is a guest.
  • Feature A plug-in can ask for other programs to be started with the core (hosted edition only). Its manifest may list applications:, and they run beside OpenPairings in development, in tests and in the release, which now also packs them. Until now a plug-in's own dependencies were on the shelf and in nobody's startup list, so the plug-in had to start them by hand and hope. A list that is not a list of names stops the build and says which manifest to blame.
  • Fix A round published after a delay now tells the open Pairings page the moment it goes public. The publish itself always happened on time; the page just kept saying scheduled until somebody reloaded it, which is the kind of patience nobody asked for.
  • Feature Plug-ins can talk to devices in the playing hall (hosted edition only). A plug-in may answer plain requests under its own /p/<id> address, without a sign-in, so a printer PC or a laptop by the speakers can join a tournament with a one-time link and keep a line open, outbound, with no port forwarding. The plug-in checks who is calling; the core only points the way. Every round going public now reaches the plug-ins too, whichever way it got there.
  • Change The Archived table on the Tournaments list has the same More menu as the rows above it, instead of three buttons in a row: Export, Unarchive and, last in red, Delete or Leave. The previous release did the active rows and left these to their own devices.
  • Removed The single-file Mac download is gone. It carried its own copy of Erlang that insisted on macOS 15, which made it a very small download for very few Macs. The Mac app (openpairings_app_macos_aarch64.zip) and the portable zip stay, and both run on macOS 11 and newer. Nothing in the program looked for the old file, so installed copies are unaffected.

[0.90.0] - 2026-10-17

  • Fix The Mac app opens on macOS 11 and newer, not only on 15. The Erlang inside is compiled for 11 now, SQLite too, and the build fails, naming the file, if anything in the app asks for a newer macOS. The ready-made runtime had demanded the latest macOS all along; it just never said so to anyone who had it.
  • Change The Tournaments list and the Players page, less of everything at once. Each tournament row has one More menu instead of a row of buttons that ran off the right edge, and the top bar is New tournament, an Import menu and Export all. The More buttons show the arrow and no ellipsis, because one hint that a menu opens was plenty. On the Players page a long club name is cut with an ellipsis (hover for the rest), the grid scrolls inside its frame instead of the page, and the paragraph of instructions waits behind an information button until someone asks. The status badges read opstart, bezig and afgelopen in Dutch, which they should have done all along.
  • Feature Print one category at a time. Tournaments with categories get a Print for selector on the Print page, and the player list, cards, pairings, result slips, score sheets, standings and cross table follow it. Board numbers stay where they were, so the U1800 sheet no longer needs a pair of scissors and a steady hand.
  • Change The manual now calls them what they are to the person using them: live boards.
  • Change The Pairings page's round bar shows what the round needs, not everything it could ever need. One main button per stage: Pair round N before pairing, nothing while the results come in (they are the work), On to round N once they are in, Standings after the last round, with Send for rating beside it. The Print menu, Swap rounds… and the rest went into one More menu, grouped, with unpairing at the bottom in red where it can't be hit by accident. Nothing was removed. It just stopped shouting.
  • Change Tournament settings, calmer. The Settings pages now sit in the account page's frame: a rail of pages on the left, the settings most tournaments touch up front, the rest behind Show advanced settings. The switch is remembered on your account. Nothing was removed, and an advanced setting you changed is never hidden quietly: a note counts them and opens them. Features the tournament doesn't use fold down to their on/off switch, because nobody needs the homologation fields of a tournament nobody is homologating.
  • Change Place cards and the next round preview are on the Print page now, with the category selector like the rest. They used to live only behind other pages' buttons, which is a fine place to hide things you don't want found. The preview stays unlinked until you have worked it out on the Pairings page.
  • Feature A hosted installation can now answer on a second web address while the first keeps working, and the old address can be set to forward visitors to the new one. Off until it is turned on; nothing changes for anyone before then.
  • Change The address of the public results site that a desktop copy publishes to, and that the publishing form suggests, can be set by the operator instead of being fixed in the build.
  • Feature A banner for the domain move. With PHX_MOVED_TO set, every page opened anywhere but the new address says where the app went and links there. It can't be closed, because a banner you can close is a banner you never read again. The new address is spared, and so is paper.
  • Change The Players page has three buttons instead of seven. Add player stays the main one, Print player list stays beside it, and the place cards, Pairing numbers, Starting numbers, Refresh ratings and Update player data moved into a grouped More menu that the arrow keys can walk. All of them are still two clicks away. Registering players is the job; the toolbar no longer pretends otherwise.
  • Change A desktop copy now publishes to openresults.cloud unless told otherwise. Copies from before this release keep using the old address, which keeps answering, so nobody's tournament goes anywhere it wasn't sent.

[0.89.7] - 2026-10-17

  • Change Adding a player with no sex now asks first. The form used to save "unknown" without a murmur, and the murmur turned up later in the FIDE report. It now says what it needs the answer for and offers Choose sex or Save without. Editing, imports and lookups stay quiet.

[0.89.6] - 2026-10-17

  • Fix The KBSB list fetches its own sexes. 0.89.3 started keeping each member's sex, but only for rows imported after it, and a list synced earlier stayed sexless until somebody pressed Sync from KBSB, which nobody did. A list with rows and not one sex among them now re-syncs itself once, a minute after the server starts. Update player data then has something to offer.

[0.89.5] - 2026-10-17

  • Fix A name saved untidy before names were tidied is now offered the list's spelling. Update player data compared names after tidying both, so "Holovatyi ,Toma" and "Holovatyi, Toma" were the same name to it and the comma stayed where it was pasted.

[0.89.4] - 2026-10-17

  • Change Performance sits with Sex, under About each player. It had been filed under Columns, a row away from the only other tick it resembles, where nobody would look for it.

[0.89.3] - 2026-10-17

  • Removed The round's right-click menu on the Pairings page. Its only content was the "what spectators see" switch, which already sits at the top of the page; a second copy under the mouse only hid the board you had clicked. A right-click away from a seat now gets the browser's own menu.
  • Fix The KBSB list's sex is no longer read and thrown away. The import checked that KBSB's file had a Sex column, nodded, and then discarded it, so anyone picked from the national search without a FIDE record arrived sexless, which at a youth event is most of the room. It is now stored as the app's own "m"/"w", and a KBSB pick fills it in when the FIDE list has nothing to say. Rows already on this machine learn it at the next Sync from KBSB on the Connections page; the upgrade forgets the file's stored ETag so that sync really downloads, instead of being told politely that nothing changed.
  • Change "Update clubs" is now "Update player data" (Dutch: "Spelersgegevens bijwerken"). Same dry run, same preview, now with a tick per change. Besides the club and club number, which still share one tick because half a club is not a club, it proposes the name as the KBSB list spells it, sex, birth year, federation, affiliation, a missing FIDE or national ID, and the FIDE title. Sex, birth year and federation come from the FIDE list first and the KBSB list when FIDE is silent, since FIDE is who reads the rating report. It still never blanks a field a list merely leaves empty, a full date of birth survives a list that agrees with its year, and ratings stay with Refresh ratings, where they always lived. New audit entries say "Updated player data"; the old ones keep their old sentence.

[0.89.2] - 2026-10-17

  • Feature Performance on the public standings, off unless you ask. The Results site settings have a new "Performance" tick under the columns. Ticked, each standings row carries the same performance rating the Players page already shows, worked out on the games up to that round; unticked, which is where every tournament starts, the key is simply not sent. A player with no finished game has no number and no key, rather than a zero that would look like a very bad tournament. One formula, one function, so the two screens can no longer disagree out of spite.
  • Fix A pasted player is no longer a stranger to Update clubs. A row copied from a spreadsheet kept its tab at the end of the national ID, which looks like nothing on screen and matches nothing in the KBSB list, so the club refresh walked past it every time and the club stayed as pasted. Names, IDs, clubs, federations and titles are trimmed on save, a name's comma gets one space after it and none before, the IDs already stored with a tab are cleaned by a migration, and the KBSB lookup trims as well, for good measure.
  • Feature Sex on the public pages, off unless you ask. The Results site settings have a new "Sex" tick under the player details. Ticked, the published players carry an M or F and the standings and starting list gain a column for it; unticked, which is where every tournament starts, old ones included, the key is not sent at all rather than sent blank. It is personal data, and nobody had published it before, so nobody gets surprised by it now. Like the rounds-present column, it is the exception to "absent means shown".

[0.89.1] - 2026-10-17

  • Fix A deputy's phone can now clear a result, and says so. The only way to get a board back to blank was a small unlabelled arrow, which sat on a board that vanished from the list a second and a half after its result went in; the screen's own advice was to tap the chosen result again, which re-saved the same result and wrote "changed from 1-0 to 1-0" into the audit log. Tapping the chosen result is now a non-event, as on the Pairings page. Under every board with a result, a deputy's phone shows a Clear result button (Dutch: Wissen); the first tap turns it into Tap again to clear board N for four seconds, the second clears and puts the board back among the unfinished. It is only shown where the clear would be accepted: not on a helper's phone, not outside the phone's boards, not on an archived or handed-off tournament, not on a closed FIDE-mode round or a result already sent in a finalised TRF.
  • Fix The audit trail now fills in while it is open. The page read the log once when it opened and never again, so anyone who had it open since before their colleague's, or their own, last action was looking at a photograph and calling it a log. The query, the permissions and the actor names were checked and are the same for an owner and an accepted collaborator; the page was the only thing out of date. Every row written for a tournament is now announced on a topic of its own, and an open audit page reloads a moment later, keeping the filter and page it was on. A burst of rows costs one reload.
  • Fix An interrupted FIDE list update no longer leaves half a list behind. The import emptied the player table and refilled it, so a restart in the middle (a deploy, for instance) left whatever had been refilled so far: in production 894,000 players against 1.9 million entries in the name index, for weeks, with arbiters searching for a player whose row was gone. The new list is now built in a scratch table beside the old one, and rows and name index trade places in a single short transaction at the end; until then every search sees the old, complete list, and a run that dies leaves it untouched. At start-up the program looks for the signs of an earlier interrupted run (a leftover scratch table, or far fewer players than index entries), logs a warning, shows it on the Connections page with a Sync again button, and on a server that downloads by itself starts that sync without being asked.
  • Fix Revoking a phone no longer crashes the Pairings page, or twenty others. Revoking a mobile enrollment tells every open page of the tournament, and no page but the phone's own knew what to do with the news, so each of them died on the spot. They now ignore it, and the page that lists the enrolled phones drops the revoked one from its list.
  • Change A tournament's Live boards page now admits that account-wide links exist. The box that reaches a tournament through somebody's whole account used to be invisible exactly where an arbiter goes to ask who is reading the boards. The page lists them under Account-wide links: the box, the account, when it was made, when it last called, and a link to the account page where the owner of the link can withdraw it. Read-only, shown to the people who may edit the tournament, and absent when there is nothing to show. The network listener still takes no account links, so there is nothing to list about them there either.

[0.89.0] - 2026-10-16

  • Change A round robin asks how many times everyone plays everyone, instead of offering "Single" or "Double". The field is a number now, 1 to 10: somebody asked for triple, and a list that grows by one entry per request is a list that gets asked again. Every cycle is the Berger table once more with the colours reversed from the cycle before, standings and the cross table carry every cycle (a cell reads 1 ½ 0 for three), and the TRF writes and reads BERGER_ROUNDROBIN_G3 and onwards. FIDE's reversed last two rounds stay what FIDE made them: a double round robin's option, so a double whose swapped rounds are already paired can no longer quietly change length and disown them. SWAR knows two cycles at most, so a third and beyond go there as its double round robin with every round played, and the export screen says so rather than letting SWAR discover it. The untranslated "Single" and "Double" are gone, which is one way to translate them.
  • Feature Two rounds of a round robin can trade places. The Berger table assumes the hall is free on the evenings it picked; Swap rounds… on the Pairings page is for when it is not. The two rounds exchange their boards, the round numbers keep meaning the order you actually play in, each game keeps the colours its table round gives it, and the dates stay put because the evening did not move, the games did. It works for team round robins, across cycles and on top of FIDE's reversed last two rounds without reversing them twice; swapping the same two again undoes it. A round with a result, a forfeit, a postponed game or a date played is refused by name, since rewriting history is a different feature. FIDE mode allows it with a reminder that the published schedule just changed, the audit trail records which table rounds went where, the explanation page says which table round a swapped round really is, and the TRF calls the result CUSTOM_ROUNDROBIN, because no Berger code admits to it.
  • Feature A board box can be connected to your whole account, without a code. Alnasl's "Connect an OpenPairings account" sends your browser to a page here that names the box and, in large letters, where it will be sent back to; Allow, and the box sees every tournament you may edit that is current - not finished, or finished within the last 14 days - including the ones you make next week. Lose access to one and it is off the box at its next request. The boxes connected this way are on the account page under Live boards, with a Withdraw button each and room for eight. The secret still never passes through anyone's clipboard: the browser carries a grant that is good once, for two minutes, and is traded for the code by the box itself. Not offered on a desktop copy's network listener, which takes network codes and nothing else. API: a third code kind, oplu_, and POST /api/boards/v1/grant, both in docs/board-link-api.md.
  • Fix The TRF sent for rating has its legend line back. The DDD SSSS sTTT NNNN... IIIIIIIIIII BBBB/BB/BB PPPP RRRR 1111 1 1 line Swiss-Manager puts above the players, the one a rating officer reads the columns by, went missing on 3 October when the file was trimmed to look like SWAR's. It now sits above the first player again, in round reports and in the postponed-games file. No rulers: Swiss-Manager does not send those either. Downloads and copies keep both.
  • Fix An absence entered several rounds ahead no longer writes empty rounds into the TRF. A player marked absent for round 6 while round 2 was the last one paired got 240 H 006, correctly, and three 0000 - columns on their 001 line for rounds 3 to 5, which had not happened and now never will in that file. The padding that held the bye's place leaves with the bye (Ainalrami v0.43.2).

[0.88.1] - 2026-10-16

  • Security Bandit 1.12.5 → 1.13.1 (EEF-CVE-2026-105660, EEF-CVE-2026-105659) and decimal 3.1.1 → 3.2.0 (EEF-CVE-2026-97853, a Decimal.round/3 that would allocate whatever it was asked to). The web server and the arithmetic were both willing to be talked into things; neither is now. No behaviour changes, which is the point.

[0.88.0] - 2026-10-15

  • Change One unplayed game no longer freezes a round robin's public standings for the season. Standings used to stop at the last round with every result in, which is honest, and in a club championship with one game rescheduled in round 3 meant "standings after round 2" until spring, which is honest and useless. Now a round-robin board nobody has entered, in a round a later complete round has moved past, is read as a postponed game nobody asked for: it counts as the Scoring page says a postponed game counts for the opponent (a draw, unless you said otherwise), the standings go through the latest round, and every place that shows them says how many games are still open and marks the players who have one. Nothing is written on the board, so the TRF, the send check and the rating report see exactly the blank they always saw, and no game can end up rated twice. A game played early does not make its round complete; the round has to be in. Swiss, Keizer and team events are unchanged. The reasoning, the alternatives and the C.07 tie-break arithmetic are in docs/postponed-rethink.md.
  • Change A round robin always offers "postponed by White/Black". Its schedule never depended on anyone's score, so the switch that kept provisional scores out of Swiss pairings had nothing to protect there. On the Scoring page it shows ticked and locked; the values beside it still apply.
  • Feature The snapshot says which games are pending. boards[].pending on such a board, with the points it counts for, and standings.pending_games with the total; postponed_games keeps meaning postponed boards only and stays away at zero. The results site words it "Result not yet in", because it may be a game that was played and simply not typed in.
  • Fix The tick boxes under Settings - Results site (labels, hints and the group headings above them) were English in the Dutch interface, a language the rest of the page had already learned. They are translated now. The keys they travel under in the snapshot are the same as before; only what the arbiter reads changed.
  • Fix The update notice no longer warns about a round robin or a Keizer tournament in progress. The warning exists because an update can change the Swiss engine between two rounds of an event; a round robin comes from OpenPairings' own Berger table and Keizer from its own ladder, neither of which an update can re-pair, so naming them was a smoke alarm for a toaster. Swiss tournaments, team Swiss included, still count.
  • Feature The results site's two screen cards (projector view, hall display) can be switched off: a new tick, "Projector and hall display links", under Settings - Results site - Pages. It hides the cards and nothing else, because the screens are addresses, not decorations, and a hall that already has one open on a TV should not lose it to a tidy-up. Ticked unless you say otherwise, so every tournament already published keeps its cards. Travels in the snapshot as display.screen_links.
  • Feature The small window has a Details button. It grows the window downwards with the program's own output (the end of launcher.log, refreshed every second, left alone while you scroll up to read) and a few numbers in words: version, uptime, processor, memory, how long the database takes to answer SELECT 1, the last slow queries, pages open, tournaments, board links online. Copy puts the numbers and the last 200 log lines on the clipboard; Open log folder shows the file. "It is slow" becomes something that can be mailed. Collapsed by default, and asks nothing while it is. The Mac app has the same under a Details button, in a window of its own that follows the Mac's language, with the log at logs/server.log. Manual chapter 1, English and Dutch.
  • Feature ./openpairings.sh details for a machine without that window. The same numbers and the last 20 log lines in a terminal, redrawn every two seconds until q; --once prints one snapshot for pasting, --json the raw answer. Works over SSH, which is where Linux problems tend to be described from.
  • Security The numbers have a door, and it is narrow. GET /_launcher/stats answers a desktop copy only, a request from this machine only (the socket's address, never a forwarded header - and a request that carries one is refused outright, so a desktop copy somebody put behind a tunnel does not turn its public traffic into "this machine"), and only with a secret made at every start - handed over by the launcher, or written by the server to <data>/run/launcher.token, readable by its owner and removed on a clean stop. Everything else is a 404, the hosted service and the board network listener included; tests say so. The contract is docs/launcher-details.md, so a Mac launcher can read the same thing later.

[0.87.0] - 2026-10-15

  • Feature A Mac app again, and this time an app. Every push builds OpenPairings.app for Apple Silicon (M1 and later) only, and a release attaches it as openpairings_app_macos_aarch64.zip - a fixed name, so a link to the latest one stays right. Double-click it and a small window says OpenPairings is running, the browser opens once the server answers, a second double-click opens a tab instead of a second server, and Stop OpenPairings stops it, the way closing the window does on Windows. The data stays in ~/Library/Application Support/OpenPairings, where the Mac builds always kept it, so replacing or binning the app touches nothing. Not signed by Apple yet: the first start needs Open Anyway in Privacy & Security, which docs/macos.md and the manual's chapter 1 (English and Dutch) walk through. Updates on a Mac are a link to the download, not a button. Nobody on the project owns a Mac, so the build proves it starts on GitHub's; whether it starts on a person's is the test list at the end of docs/macos.md.

  • Change The .dmg is gone, replaced by the zip. It carried a version in its name, which made "the latest download" a moving target, and GitHub's runners refused to make it often enough to need five retries. The single-file binary and the portable zip are built for macOS again too.

  • Feature Developer ID signing and notarisation, written and switched off. The step is in the workflow and skips itself until the Apple account's five secrets exist (docs/macos.md names them); then the first-start detour disappears. Until then the app is ad-hoc signed, which Apple Silicon requires and Gatekeeper does not trust.

  • Security No data folder inside an app bundle. config/runtime.exs refuses an OPENPAIRINGS_DATA_DIR that points into any .app, before creating it: a bundle is replaced by the next version and binned whole. The Windows lesson, learned this time on paper.

  • Feature Theme packs. A theme can now arrive as a folder - theme.json, theme.css, optionally its own .woff2 fonts - instead of a change to the program. The desktop build reads them from themes in the data folder, a server from PAIRINGS_THEMES_DIR; none is installed by default. Installed packs sit in the theme picker after the built-in ones with a swatch of their colours, and their stylesheet is loaded only by whoever chose them. Packs are not trusted for being pretty: a stylesheet that imports, fetches anything but its own fonts, revives expression() and friends, or styles anything outside its own [data-theme] is refused, and the picker says why. The Content-Security-Policy did not need to loosen for any of this. mix pairings.theme_check PATH runs the same check plus the contrast rules the built-in themes answer to. Remove a pack and its users land on System without a word. Manual chapter 1 (English and Dutch) has a Custom themes section; docs/theme-packs.md is for whoever writes one.

  • Removed The Jorian theme left the building and came back as a cartoon. It is no longer built in; it is the first theme pack, in a separate collection, redrawn as a toon: stickers with ink outlines, hard shadows, a battlemented top bar, a pennant for the current tab, result chips that stamp themselves on. Same website colours, considerably less restraint. Standings and number tables do not dance, and nothing moves for anyone who asked their computer for less motion. Whoever had it chosen gets the pack if it is installed and System if it is not.

  • Fix "Are you sure?" boxes open in the middle of the screen again. Every one of them - unpair a round, take a round's results off the public page, draw lots for the manual ranking - opened in the top-left corner, in every theme. The CSS reset everybody uses removes the one rule a browser centres a dialog with; it is back.

  • Fix The pairing rationale's tally is a row. Boards, byes, floaters and rematches were stacked one per line, a short list pretending to be a long one.

  • Feature A theme pack can be kept for particular accounts. A users list of email addresses in theme.json, and the pack exists for those accounts and nobody else: not in anyone else's picker, not storable on anyone else's account, never offered to someone who is not signed in. Somebody whose browser still remembers it lands on System, like any theme that went missing. An empty list is refused rather than read as "everybody", because that is the reading nobody meant.

  • Fix "Before round 1, spectators see the starting ranking" no longer sits locked on "On". Switched on after a round was already public, it stayed on and greyed out, and the one person who tried it concluded the tournament was ruined. Once a round is public the setting now says it no longer applies; if it is on, it can still be switched off, and it can no longer be switched on at that point. The Publishing chapter of the manual says so too.

  • Change The taskbar and the Dock get their own icon. Nine squares on a white tile, seven of them a blue arrow pointing at the top-left corner with a yellow tip - Alnasl's grid, facing the other way, so the two sit together without being mistaken for each other. The winged orb was three pixels of wing at 16 pixels. Only the program's icon changed; the site, the favicon and the installer still wear the orb. Drawn by rel/build_taskbar_icon.py.

  • Feature SHA256SUMS on every release. One file with the checksum of every download, for anybody asked to trust an unsigned program by hand.

  • Change The Mac app lives in the Dock now. It used to be a shell script that hid from the Dock on principle (LSUIElement), which made Keep in Dock a feature of every app on the Mac except this one. The app's executable is now a small native program (rel/macos/Launcher.swift, compiled by the build): a Dock icon with its running dot, a place in Cmd-Tab, Options > Keep in Dock, a window that closes without stopping anything and comes back when you click the icon, Open in browser on the icon's menu, and English or Dutch to match the Mac. Quitting - Stop OpenPairings, Cmd-Q, the Dock's Quit, logging out - stops the server cleanly first, and a force-quit takes the server along instead of leaving it running where nobody can find it. The server's output moves to logs/server.log in the data folder, one run per file. Written without a Mac in the building, so the first compile is GitHub's and the first click is yours: docs/macos.md has the list.

  • Fix The network listener's port check speaks each system's dialect. Asking a second time for a second was not enough: on Linux a port this program closed itself stays "in use" for up to a minute unless the check says it may reuse it, and on Windows saying so would let it take another program's port. The check now asks the question each system understands. Found because the new release gate did its job and refused 0.86.0 over exactly this test.

[0.86.0] - 2026-10-15

  • Change The Dutch says "je" everywhere, and one word for one thing. The live-boards pages and the Dutch manual still said "u", which made the program formal in one room and on first-name terms in the next. Also settled: resultaat for a game's result (the site stays the uitslagensite), verstuurd for verzonden, paring for the pairing that "koppeling" used to name, arbiter for scheidsrechter, and chapter 18 quotes Alnasl's labels as Alnasl now writes them.
  • Feature A round robin asks how to number the players before it pairs, and says what each answer gives. The Pair the whole tournament (Berger) button used to ask "Continue?", which is an answer to a question nobody had put. The numbers the Berger table pairs on were whatever the Players page held, or the rating order if it held nothing, and the pairing step did not mention them. It now opens a dialog with four ways to number: By rating (highest first; equal ratings by title, then the tie-break from the options, then name, then entry order, so the same field gives the same list every time), As entered (the order the players were added to the tournament, which is their id order, the only entry stamp that cannot tie), By lot (the program draws when you press Draw lots; you see the result, can draw again, and nothing is fixed until you confirm), and As set on the Players page (offered when numbers exist already, with a note if they happen to be the rating order or the entry order). The current choice is preselected, the resulting numbered list is shown before anything is written, and the dialog says that the numbers cannot change after round 1. The same question sits in the Pair round 1 by hand… dialog, because that also fixes the numbers. A team round robin keeps ordering its teams on the Teams page; it is a different code path with its own arrows, and was left alone.
  • Feature The numbering choice is in the audit trail, and a draw can be repeated from it. The entry records how the players were numbered and the resulting order (number, player, name). A draw also records its seed: the same seed applied to the players in entry order gives the same order again, which is what "reproducible" means when somebody asks afterwards. The Players page's Starting numbers dialog gets Order as entered beside Draw lots and Order by rating, so both places offer the same three, and its draw is now seeded and recorded the same way.
  • Change Manual chapters 4 and 6 describe the choice (English and Dutch). Chapter 6 has a new section on numbering a round robin, including what "as entered" means after an export file is read back, and a note for arbiters coming from SWAR: its three ways of ordering correspond to these three, and an imported SWAR file carries only the resulting order, not which way produced it.
  • Fix The network listener no longer calls its own port taken. Switched off and on again within the second, it found the port "in use" - by itself, a moment ago. The check now asks again for up to a second before believing it. Another program's port is still taken after a second, so that diagnosis stands. Found by the test run on GitHub, which is slower to let go of a port than a desk PC and therefore more honest.
  • Fix Extra points in acceleration mode survive a trip through a TRF. Export the tournament, import the file, pair the next round: the copy paired as if the acceleration had been switched off, because the file held the virtual points every past round was paired with and not the ones the next round would be, and the import threw even the past away for not being Baku. The diagnosis on file said "the format cannot carry it". The format was fine; it was the writer. TRF26's 250 names a round range and Baku's later rounds already use it, so a full export with rounds still to pair now adds the next round's points (the XXA line in the older spelling gets one more value). On import, virtual points that are not Baku's become acceleration mode: each round keeps what it was paired with, each player holds what the file gives for the next round, a 299 at round 999 keeps them in the final standings, and the review says so as an adjustment. A file whose 192 says Dutch without _BAKU is no longer made Baku because its first rounds happen to look like it. The file sent for rating is unchanged: no round it does not report, and still no extra point dressed up as a game. A counted handicap already came back and still does, now under test in both spellings.
  • Fix A TRF16 file accelerated by Baku is Baku again on import. Without a 192 to say so, the import recognised Baku from the numbers and then asked permission to switch it on from a lock that refuses changing acceleration once a round exists, which the import had just written. The lock said no, the import said nothing, and the tournament came in without acceleration. It is now set while the tournament is being built, and if the tournament's other settings refuse Baku the review says that instead of nothing.
  • Change The Export page says when players hold extra points a TRF cannot carry. Points that neither count in the standings nor reach the pairing (a handicap that is not counted, a penalty in acceleration mode that is not kept) have no record in any TRF, so a tournament imported from the file has none. A line under the TRF downloads now gives the number of players affected, before anyone finds out the other way. Manual chapter 10, English and Dutch, says what goes in the file and what does not.
  • Fix The sign-in screen's panel follows the theme's accent instead of one fixed green. It was green on every theme, beside a primary button that is blue on Tom W, rose on a rose accent and whatever the arbiter picked otherwise: two apps sharing a card. The panel now starts as the accent itself (light themes) or a deep step of it (dark and Slate, where a pale slab on a near-black page is a searchlight), and everything laid over it - the glow, the grid, the pill behind the name, the sign-up button - is mixed away from the text colour, so an overlay can only add contrast. The contrast test now measures every line on it in all 72 theme-and-accent palettes, with and without the glow, and a fixed colour sneaking back into the fill fails a test of its own.
  • Fix The sign-in screen's layout, four small things that each took longer to notice than to fix. The intro paragraph wrapped at about half the panel's width (a leftover 34ch cap) while the list under it used all of it; it now uses all of it. The form sat dead centre in a tall card, leaving the top half looking abandoned; it now sits at the optical centre, above the arithmetic one, with a larger title. The logo mark's wing tips were shaved by the svg's own clip (the stroke is wider than the half unit of margin it was given) in the top bar and on the panel: overflow: visible. And a flash can no longer be wider than the window: its fixed 320 or 384 pixels are now capped at the viewport less a margin, with long unbroken text wrapping instead of pushing the message off the screen.
  • Change A red test run now blocks a release. 0.85.0 was published while its own "Elixir CI" run was failing on a flaky test, because the release workflow and the test workflow fire on the same push and never speak. The release job now asks GitHub how Elixir CI ended for that exact commit and publishes only on a pass; a failure, no run at all after ten minutes, or a run still going after forty-five stops it, and re-running the job after fixing or re-running the tests asks again. Pushes that do not publish anything build as before. Not testable from a desk: the first real release is the experiment, and docs/binaries.md says what to do if it goes wrong.

[0.85.0] - 2026-10-13

  • Feature A desktop copy can let a board box on another machine in the hall connect. Until now the answer to "the box is a small computer beside the boards and OpenPairings is on my laptop" was "then run both on the laptop", which is an answer, the way amputation is a treatment. Settings, Live boards has a new card on the desktop edition, Boxes elsewhere on this network, with one switch: Let boards on this network connect. Off by default. On, it opens a second listener on the network address you pick (port 4747 unless you say otherwise) that serves the board link's four requests and returns 404 for everything else: no pages, no websocket, no static files, no login screen to skip, because none of that is on it. The listener the pages are on stays on 127.0.0.1, untouched. The page then offers two codes and says in words where each leads: for a box on this computer (127.0.0.1, as before) and for a box on the network (the computer's address). On a computer with a single network the same code is also shown with the computer's name. The setting is the computer's, not a tournament's; it survives a restart; Connections shows where it stands and can switch it off. Switching is a process starting or stopping under the supervision tree, not a restart. The hosted edition has no such switch, no such listener, and codes that carry its public address, exactly as before.

  • Security What that listener will and will not do, since it is plain http and pretending otherwise would be malpractice. The code of a network box crosses the hall readable, and the page says so before you switch it on, with the difference between a cable and a wifi the players share spelled out. What was cheap to do about it is done: the listener binds one address, not all of them; it answers callers on private address ranges only; it accepts only codes made for the network (a code for a box on the same PC is refused there), and those work until withdrawn, like every other code; it is closed whenever no such code exists, so withdrawing the last one closes the port; X-Forwarded-For and its relatives are dropped on it, so refused codes are counted against the address the connection really came from even on a copy configured as if behind a proxy; errors are a code and a sentence, with no version and no path. No TLS. A port already in use is reported on the page, tested on the address and on all addresses because Windows will happily let two programs share one, and takes nothing else down.

  • Feature An address that changed is noticed, and not followed. A hall network hands out addresses, and tomorrow's may differ. The codes then point at an empty room. The program says so at start-up (in the log) and on Settings, Live boards: This computer's address changed from … to …: boxes with the old code cannot find it, and each network code in the list shows the address it was made for, in red once that is no longer where the computer answers. It does not move the listener to the new address by itself: a laptop that is suddenly on a different network is the one moment nobody wants a port opened unasked. Pick the new address, make a new code; or give the computer a fixed address and never read this paragraph again.

  • Change Windows will ask a question the program cannot answer for you, and the page now says which one. The first time something listens on the network, Windows Security asks whether to allow it. The card says what the question looks like, what to tick (Private networks, Allow), and where to go if the answer was Cancel.

  • Change A game a board reported no longer outlives its pairing. Unpair a round and pair it again, and the games the boards had reported for the old pairings stayed in the database: shown nowhere, exported nowhere, kept until the tournament was deleted. A patient with no pulse and a bed. They are now removed at the moment the pairing goes, inside the same transaction where there is one: a round unpaired, a board deleted, a team line-up or team import that rewrites the boards, a restore point restored, a hand-off file coming back. The audit trail gets one line with the count and none of the moves. A game whose pairing survives (another board number, another player on it) is kept. A restore point from before a re-pairing brings the old pairings back with their games as the point held them; the box keeps its own copy throughout and can send a fuller one again under the same id.

  • Change Manual chapters 17 and 18 no longer say a box elsewhere on the network cannot reach a desktop copy (English and Dutch). Chapter 17 has a new section on the switch, the warning, the Windows question, which address, and what happens when the address changes; chapter 18 has the set-up for Alnasl on another machine as six steps, four new rows in the symptom table, and a limits list that says what the link is (plain http, tried on one computer against its own network address, not yet between two machines in a hall). docs/board-link-api.md describes the listener for whoever builds the other end, and corrects what it said about games of vanished pairings.

  • Change The manual's chapter on electronic boards describes Alnasl's installers, not a build from source. Chapter 18, English and Dutch, now gives the Windows installer and the Linux install line, where Alnasl keeps its data, and what the hardware runs have and have not shown.

  • Fix A results site that was "too old" for ninety seconds is no longer too old for six hours. A deploy restarts this program a moment before the results site, the first request for the box's permission lands on the old site, gets a 404, and the tournament was filed under "older version, paste a key" until somebody pressed Try again or six hours passed. That one answer is now asked about again every five minutes for the first hour, then on the slow schedule like every other refusal. Diagnosis by exclusion: it was never the site, it was the order of two restarts.

  • Feature A second theme named after the maintainer, copied from the project's own website, under Tom W in the theme picker. The website is a star atlas printed as a blueprint, so this is pale blue-white paper, navy ink, a royal-blue accent and a yellow highlighter that survives only as the text selection. Every token is defined and the contrast test measures it like the other seven, in all nine accents, so a button colour of your own still works. It is a plain theme, in every edition, for every account; nothing hides behind it. The website's lightest grey-blue text was under 4.5:1 on tinted rows and was darkened to the next ink up. Manual chapter 1, English and Dutch.

  • Change The thanks name a second person. The About page and the footer of every printed document now thank two people for their feedback instead of one (the program only had room in its heart for one arbiter, and its heart has been enlarged). The sentence is plural, in English and Dutch.

  • Fix A Belgian player no longer arrives with the federation "V". The Belgian members file has a column called Fed. It is not a country; it is the member's wing inside Belgium, one letter: V, F or D. Since 2026-09-30 the national-ID lookup in the player dialog, the add and refresh buttons on the Players page, the picks from the KBSB search and the registration review copied that letter into the player's federation, which everywhere else is a FIDE country code: no flag, a column of single letters on lists and on the results site, players of different countries treated as compatriots by the federation exclusion rule, and a letter where FIDE expects BEL in the TRF. These now take the federation from the FIDE list through the member's FIDE ID, and leave it blank when there is no FIDE ID or the FIDE list does not know it (a member can be a foreigner, so nobody is assumed Belgian). A migration repairs what is stored: players and pending registrations whose federation is exactly V, F or D and who have a national ID get the FIDE list's federation, or blank. Importing an older backup does the same, so it cannot bring the letter back. A TRF that already went out with the letter is not re-flagged as changed: what a receipt compares is the games and the players, not the federation, so the next send simply carries the right code.

  • Change The hall box gets its permission for the results site without anybody asking anybody. To send moves to the results site a box needs a relay key, and the procedure for obtaining one was: find an administrator of the results site, have them make a key in a panel only they can open, receive it by some channel nobody specified, and paste it into a password field. On a Saturday morning, with the round starting. The program already holds the credentials that publish the tournament, and the results site now hands a relay key to exactly those; so it asks. It asks when the tournament is on the results site and there is a reason to think a box wants it - a connect code reaches the tournament (its own or its group's), a board has reported a game, or the live link is on - in whatever order those things happened, from the publishing queue's own process and never from a page. Settings, Live boards, card The results site says where it stands in words: Getting permission from the results site…, Yes. The box may send moves there., or Not yet, with the site's reason and Try again. No network: it waits and tries again by itself, silently, like publishing does. Replace the box's permission fetches a new one; the site stops accepting the old one, and the box picks the new one up at its next poll because the link's version covers it. The box is still never given the publishing credentials - it can be left in a cupboard, they can rewrite a tournament. A takedown drops the key on both sides. Nothing travels in an export, and the audit trail gets the key's last four characters and no more. The desktop edition does the same under its own installation key.

  • Change The Relay key field is gone from the normal page. It appears only when the results site is too old to hand a key out, under a sentence that says so, which is the one case where pasting is still the answer. The words "relay key" are small print now; an arbiter should not have to learn plumbing vocabulary to show a chess game.

  • Change The manual's set-up for electronic boards is three steps in OpenPairings instead of five. Chapters 17 and 18 (English and Dutch) lose "ask an administrator" and "paste the relay key"; the steps are renumbered, the cross-references follow, and the troubleshooting table says what "no relay key" means today: the tournament is not on the results site, or the site is unreachable, refused, or too old.

  • Security The relay key no longer reaches a log line by way of the form it used to be pasted into: relay is on Phoenix's parameter filter, and the field is redacted when a tournament is inspected.

  • Change The manual has a set-up guide for electronic boards. Chapter 17 said what live boards are, and still told the reader that Alnasl's side of the link was coming in a later version, which stopped being true some commits ago. A new chapter 18, Setting up electronic boards (English and Dutch), walks an arbiter from the boards and cables to the finished round, once for a hosted OpenPairings and once for the desktop edition on the same PC as Alnasl, with what works without internet in a table, several tournaments in one hall, a symptom table written from the messages the two programs actually print, and the limits as they stand (no installer for Alnasl, team events as flat boards, one game per board leaves the box, no Chess960, a desktop copy only reaches a box on its own PC). Chapter 17 lost its out-of-date warning and its out-of-date round robin sentence, the results, publishing, OpenResults and import chapters point at the new one, and the question mark on Settings, Live boards opens it.

  • Fix The board link no longer tells a hall box that an earlier round is "on". The round a box should have on its boards was the first paired round with a result missing. Sound for a round robin, and a trap in a Swiss: clear a result of round 2 to correct it while round 3 is being played, and for as long as the field stayed empty the link said round 2 was the current one; leave a game of round 2 without any result and round 3 never became it. A box that believed the link would have walked its boards back in the middle of a game, or not forward at all. The rule is now: of the rounds that were paired last together (one round in a Swiss or a Keizer, both legs of a two-leg match, every round of a round robin), the first that still has a game without a result, and the last of them when none has. In a Swiss that is simply the round paired last. A round robin behaves as it did, including that a postponed game (*) is a result and lets the schedule move on. docs/board-link-api.md says so in full.

  • Feature The board's page counts the moves a box rewrote. A box could already say how many moves it had worked out after the fact and how many it had guessed. There is a third kind: a move it read, and later had to admit was another move of the same piece (a pawn put down between two squares is the classic). The link takes rewritten beside reconstructed and guessed, stores it, carries it in exports and restore points, and the game's page says all three. A box that sends no such number is taken to have rewritten nothing, which is at least polite.

  • Change The board link's document says what the code always did. The limits table now has every bound the server enforces (clock values, the three counts, the start position, the game id, the secret) and the ten-minute Retry-After a hall gets for guessing codes; a field the contract does not name is ignored, not refused, in both directions; and the listing's version is said to change with every result entered, so nobody mistakes a box refetching a few kilobytes per game for a bug.

  • Change The player dialog says what its ticks do, and asks one question about the bye. The row of ticks read Absent, Not eligible for half-point byes, Expelled and Forfeit, and an arbiter reading them in Dutch could not tell which one took a player out of the standings (Forfait sounds like a game that was forfeited; it is how a player is withdrawn here). They now read Absent, No half-point byes for this player, Withdrawn from the tournament and Expelled by the arbiter, each with a line underneath saying what it does, and the combinations that mean nothing are greyed out: Expelled outranks Withdrawn outranks Absent, so ticking the first dims the other two and ticking Withdrawn dims Absent, with what they held left untouched. The stored fields and the export keys are unchanged, and the old Forfeit wording stays findable in the manual. The bye had a tick (Exclude from the pairing-allocated bye) beside a select (Pairing-allocated bye preference) that you could set against each other: tick, then no preference, or want the bye while barred from it. That is now one list, Pairing-allocated bye: FIDE rules decide, never gets it, not in certain rounds (with, if you like, a preference for the other rounds), rather not, rather gets it, must get it. It lists only the choices whose switch is on, keeps showing a stored value whose switch is off, and keeps both warnings on the choices they belong to. The same five fields are stored as before. A stored player who is never to get the bye and also carries a preference is shown as the exclusion, which is what the engine always did with them, and saving the dialog drops the preference; the changeset drops a redundant rather not beside never the same way and still refuses wants it where the bye is excluded. Chapters 4, 5 and 8 of the manual.

  • Feature The boards in the hall report to the pairing program, which listens and does not take their word for it. A box that reads electronic boards (Alnasl) used to learn the pairings by reading the public results page, and told this program nothing. There is now a link between the two, and the box does the calling: Settings, Live boards, New code gives a line of text and a QR code, the box scans it, and that is the setup. Over the link the box is told the rounds, who sits where with which colour, the rate of play, which round is on, and - when the tournament is published and you have pasted a relay key - where on the results site to send the moves. Pair a round and the box has it within about five seconds; swap a board afterwards and it has that too. Back come the moves, the clocks, the PGN, the result the board saw, and how many moves the box had to work out or guess. A board never enters a result. On the Pairings page a tournament with boards gets a column, Board saw, to the right of the pairings: quiet when the board agrees with you or said nothing, red when it does not. Red blocks nothing - the next round pairs, the file sends - because a sensor board is a witness and you are the arbiter. The games are kept with the tournament: each has a page (reached from the column) and a PGN, and there is a PGN per round and for the whole tournament, with the tournament's headers and your result, * until you have entered one. A tournament in a group can make a group code: one scan covers every tournament of the group its maker may open, including one added next week. A code is random, shown once and stored as a fingerprint; it can be withdrawn, is rate limited, and reaches a spectator's worth of data plus the board list - no birth dates, no clubs, no addresses. Making and withdrawing one, the relay key and a box's first call are in the audit trail. The games travel in an export and a restore point; the codes and the relay key do not. A desktop copy answers on its own computer only, as it always has, so its code says 127.0.0.1 and works for a box program on the same PC; a box elsewhere on the hall's network cannot reach a desktop copy yet. The moves are not checked for legality here - this program has no chess in it - only for being shaped like moves. The box's half of the link is still to be built, against docs/board-link-api.md, which is the contract. Manual chapters 10 and 17.

  • Change The desktop program asks before it downloads the FIDE list. Forty-five seconds after start-up it used to fetch 41 MB without a word, and the Players page did the same when an administrator asked for a rating check, which on a phone hotspot in a tournament hall is a generous way to spend somebody else's data. On a desktop install the daily look at FIDE (one small request) stays, but a missing or newer list now only raises a question: a banner on every page, for administrators, with Download now, Later (gone for a day) and Don't ask again. The Connections tick becomes a choice of download automatically, ask me first (the desktop default) and never; a stored off stays never, and an install with nothing stored gets ask. The Players page raises the same question instead of starting the download. A hosted server is untouched and downloads as before. Checked and left alone: the Belgian list sync and the FIDE badge photo only ever run from a button, the update check reads a few kilobytes of release list, and no flags or fonts are fetched at all. Manual chapter 4.

  • Feature A team forfeit can pay the full board count. A match awarded as a whole paid the winner one game point per board it had bothered to seat: 7-0 for a team that seated seven of eight, and 0-0 for a team awarded the match before it entered a line-up. That is C.07 Art. 11.1.2 to the letter, and not what leagues write in their regulations, which tend to say 8-0 and mean it. Settings, Scoring, A match forfeited as a whole now offers The full board count: a match forfeited by decision before any game was played, or against a team out of the round as a team, scores the full count to the team it went to. The default stays One per board it seated, for every existing and new tournament. A match forfeited after play keeps the sum of its boards, a single forfeited board is still one board's point, no board's result changes and no player's statistics gain a game. Standings, tie-breaks (the board-weighted one included), the results site and the TRF team totals read the same figure; the setting is in the tournament export and the audit trail. In FIDE mode it is scoring: locked once round 1 is paired, not a departure. Manual chapters 3 and 12.

  • Fix The Federation flags tick did nothing with Federations off, and was offered anyway. A flag is drawn for a federation, so with federations hidden there was nothing to draw it for; the page let you tick the box regardless and left you to look for the flags. It is a sub-option of Federations now: attached to it on Settings, Results site, only there while Federations is ticked, and never sent as on while it is not. Your choice is kept underneath, so ticking Federations again brings the flags tick back as you left it. N of M hidden counts the one box you unticked, not two. The results site, from its next version, also draws the flag beside the player's name - on the standings, the pairings and the cross-table, which is where people were looking for it.

[0.84.0] - 2026-10-11

  • Change Send says what it does, which is download. The confirmation used to promise that the round would be sent, and at least one arbiter took the program at its word and waited for the rating officer to receive something. Nothing was ever going to arrive: Send… downloads the rating file and records that it was handed over, and the e-mail is yours to write. The confirmation (rounds and postponed-games file) now says downloading is not sending, a line under both Send buttons says it permanently, and the Export page says it a third time after the download, with the receipt code. The Dutch confirmation was also rewritten into Dutch. Manual chapter 11.

  • Fix The rating inbox lists tournaments that are somebody's rating business, and nothing else. A tournament is listed when a file was sent from it or when it is marked FIDE-rated (Settings, FIDE - the only "this is rated" setting the program has; there is none for a national rating). Not sent yet: round N has finished and has no receipt is said only of a tournament marked FIDE-rated; one that is not but sent a round once keeps that receipt, gets the label not marked FIDE-rated, and is no longer told its other rounds are missing. Manual chapter 11.

  • Fix The rating inbox gives a long tournament a monthly deadline, and names a list by its date. A club championship running from October to April was told its October rounds were for "the list of April: send it before 30-04", counted from the last round of the season. FIDE B.02 Art. 4.1 has a tournament of more than 30 days report every month, so each month's rounds now count from the last of them (October rounds: the list of 1 November), and only the month the tournament ends in is the final report under Art. 9.1. Lists are named by the day FIDE publishes them (the rating list of 1 November 2026) instead of the list of October, which read either way. The "not rated" limit is the third list after the tournament's last day, as the article says, where it used to be counted from the list the report was due in - a month too generous for an event ending in the last five days of a month. An overdue monthly report is called overdue, never unrated.

  • Fix The rating inbox fits on a phone, and looks like a page. Each tournament was a table dropped into a card: at phone width the table kept its width, the page scrolled sideways to 854px and took the Download and Check buttons with it; tournament names were body-sized, so nothing showed where one stopped; the Changed since sent badge was a sentence that could not wrap; the amber and red states were inline colours. Receipts are now rows that wrap, tournaments are ruled blocks, the states are classes that pass the contrast test in every theme, and a receipt whose file was never kept says so instead of offering nothing.

  • Change The rating inbox has a door where administrators look for one. It is the rating officer's page and was reached through a button on one tournament's Export page (and a bare link on Admin). It is now in the Account menu on every page and a card at the top of Admin, for administrators only - the same accounts the route lets in. On the Export page it is a small link under the Send button for administrators and nothing at all for everybody else. It is also just called Rating inbox now.

  • Fix Keizer: expelling a player no longer fines everybody who beat them. A win on the ladder pays the opponent's value, and an expelled player was taken off the ladder - so their value became nothing, retroactively, and every win over them with it. The manual promised that opponents "keep the points those games gave them"; in Keizer they kept the memory. The expelled player now keeps a rung for the games already played against them, valued like anyone else's as the ladder moves; only their own row leaves the table, and they are not paired again. Somebody expelled before ever being paired is not on the ladder, as before.

  • Feature Results site: a round-robin row that is not counted is marked. A player who leaves a round robin with under half their games played keeps the games in the table and counts for nobody (C.05 6.6). The arbiter's page has said so with a dash and a note; the snapshot sent the row with a place and with more points than the row above it, and left the spectators to form theories. The row now carries "uncounted": true (absent everywhere else, so an older results site sees what it saw), and OpenResults shows it without a place, muted, with a footnote. The published tie-break working no longer lists games against that player either - it explained a Sonneborn-Berger of 4.5 with parts adding up to 8.

  • Fix Results site, Keizer: the rounds add up to the score. With Points for a round sat out set, a Keizer tournament published each absence at that value, while the Keizer table's Score beside it counted the round as nothing - Keizer pays an absence on the ladder, by its own rule. The per-round figures now say what the score is made of. A Swiss with the same setting publishes what it always did.

  • Fix Title norms, round robin: the unrated player who scored nothing is left out (B.01 1.4.2). Games against "unrated players who score zero" in a round robin are not norm games. The rule was not implemented, and including such a game is the generous mistake: a point against an opponent raised to the rating floor. "Zero" is read as the points from games actually played - a bye or a win by forfeit is a point on the table, not a score. One draw over the board and the games count.

  • Fix A TRF26 with ? in it ranks on its own points. A postponed game is written ? and is worth a draw in the file. A tournament that counts it as a win or a loss meanwhile (outside FIDE mode, by definition) got a file whose place column came from the standings and whose points did not: first place on one point, second on three. Such a file now takes its places from itself, as the file for rating already did. With the default valuation - a draw each - nothing changes.

  • Fix A result is refused when somebody else has changed who sits at the board. The write went to the board by its id, whoever sat there by then: between a co-arbiter's swap and your page's refresh, "1-0 on the board I am looking at" was recorded for two other people, silently. The players are now compared with the board as stored; if they differ nothing is written and the page says this board changed and asks for a reload. The phone says the same. Two arbiters entering different results on the same unchanged board is still last-one-wins - that is a disagreement, and software does not settle those.

  • Verified C.05 6.6 applies to a round robin imported from SWAR as well. A file with a forfait général player who played under half the games is read the way a tournament made here is: the importer sets the Forfeit tick and the rule reads it. No code change; a test, and a sentence in the manual, so the question stays answered.

  • Fix A round-robin player withdrawn with the Forfeit tick is now withdrawn as far as C.05 6.6 is concerned. The rule that takes a player's results out of the final standings when they leave having played under half their games has been in for some time, tested, and in the manual. It looked at one field, the player's status. The Players page withdraws a player with Forfeit, which is a different field; the only thing on that page that changes the status is Expelled. So the rule worked for expulsions and for nobody an arbiter had simply withdrawn, and such a round robin's final standings counted games the regulations say are not counted. It now reads both. Nothing changes for a Swiss, for a team event, or for a player who merely lost a game by forfeit.

  • Fix A helper's phone can no longer write over a result the arbiter has just entered. A helper fills blank boards and nothing else. "Blank" was judged on the copy of the board the phone's page was holding, which stays blank for as long as the page is behind - a second or two on good wifi, rather longer on the wifi chess clubs actually have. The check now reads the board as stored. A deputy corrects results as before.

  • Fix A title norm no longer counts the FIDE flag as a foreign federation. B.01 1.4.2: players under FID "are accepted, but do not count as a foreign player". The federation check counted FID as one of the two federations other than the applicant's that 1.4.3 asks for, so one real foreign federation plus somebody under the FIDE flag passed. It was the one place the norm judgment could say yes where FIDE says no, which is the wrong direction to be wrong in.

  • Fix Results site: a round robin with a Berger table per category no longer prints a dash for the smaller category. When the categories are not the same size the shorter table finishes first, and its players had nothing at all in the later rounds - which the results site, adding the rounds up, reads as "unknown" and shows as a dash from there on. Those rounds are now published as not paired, 0 points, as they already were for an individual Swiss. A round robin over one table publishes exactly what it did.

  • Fix SWAR export: an expelled player is written as SWAR's Forfeit, not as Present. The export read the Forfeit and Absent ticks and never the status, so an expelled player left for SWAR as somebody to pair in the next round - and was paired in the next round here too when the file came back. The line under the export button says what the file makes of it (SWAR has no expulsion; imported back, the player is one who forfeited the event).

  • Fix SWAR export says so when an absence is worth something SWAR cannot hold. SWAR's Pt ABSENT is a tick box: half a point, or nothing. A tournament paying a full point for an absence went out as half a point without a word, and came back as half a point. The note under the export button now lists it. The file is the same; it is just no longer a surprise.

  • Verified App-path audit, part 2 (docs/audit-app-path-2026-10-part2.md): round robin, Keizer, team events, norms, the Belgian file, postponed games, the phone. The six fixes above, six findings documented and left for a decision, and what agreed: the Berger tables against C.05 Annex 1 for every size it prints, team Swiss rounds (same after unpair, same from a JSON copy, per-round sums equal to the standings), and 50 tournaments with postponed games in the fuzz harness - valued as configured, flagged as provisional exactly while open, and every game rated once.

  • Feature An equally good alternative is shown whole. Equally good alternatives on the pairing explanation put the boards as played beside ONE board of the alternative and the remark that the rest "was not recorded" - true, and no use to anybody trying to see the other pairing. The engine (Ainalrami 0.43.0) now hands back the whole round it found, so an item has two full columns: As played, every board the alternative would not have, and In the alternative, every board it would have instead, White first, with who goes down from each score group where that differs and a count of the boards that are the same in both. Where the record alone could not say which step of the fixed order separates the two, the engine's reading does: a later arrangement of the bottom half, an exchange between the halves, or a different player sent on from a group where moved-down players are paired first. An alternative that changes more than 40 boards is not listed, and says how many. Answers already stored keep the old one-board rendering; such a round gets a notice at the top with the existing Recompute button, which works the opened answers out again (accounts are now version 5) and changes no pairing. A round changed by hand after pairing is left alone, as before. Cost of keeping the round: nothing measurable - the search already had it. Manual chapter 6.

  • Feature The TRF import asks about players who look withdrawn. A TRF has no way to say "withdrew": export a tournament with a withdrawn player, import it, and the next round paired them (audit finding F6). The file cannot be fixed, so the import's review step now lists every player who played or had a bye earlier and has no game and no bye in the last round of the file, or the last several, under Looks withdrawn - will be paired in the next round unless you mark them withdrawn, with a tick per player. Ticked already when they have nothing in two rounds or more; not after one, because one missed round is usually one missed round. Not listed: late entrants, players with nothing at all, anyone with a paid bye in the last round or a bye granted for the next, a file whose rounds are all played, round robins and team events. What was chosen is kept in the import's record and the audit log. A file with nothing else to report now stops at the review for this alone. Manual chapter 10.

  • Fix In a Baku round the explanation page agrees with the pairing about who floated. The page built its score groups on game points, said so in a note, and then called every board between a Group A loser and a Group B winner a float - two players the engine had put in one bracket, for the good reason that they were in one (audit finding F8). The page now adds the round's virtual points as the pairing did: the groups and the float count are the engine's, a player with virtual points reads 1.0 (+1.0 virtual) so nobody takes a bracket for a standing, and the note says what the page shows instead of apologising for it. Manual chapter 6.

  • Change "The transposition order chose what was played" now says what it means. The pairing rationale's why this player floated and not another answered a tie with a sentence that was correct, complete, and understood by roughly nobody who had not written a pairing engine. It now reads equally good by every rule; the played pairing comes first in the fixed order, so the pairing numbers decided, and links to a new block under the score group, Equally good alternatives, where each alternative is one item instead of a loose line: the boards as played beside the board the alternative would have had, and why it lost. The why is said once, in plain words - when every criterion is equal the system takes the first possibility in its fixed order (top half against bottom half in pairing-number order, then the bottom half rearranged, then players exchanged between the halves) - and, where the stored record shows it beyond doubt, concretely: which bottom-half player is left over in each version, or that the alternative needs an exchange between the halves and the played pairing does not. The regulation's own terms are one click away under The FIDE wording (C.04.3 Article 3.8.1, with 4.2 and 4.3). The same candidate under two floaters' questions of one score group is the same forced pairing and is now shown once; more than three alternatives fold behind and N more. The "what if" verdict for a tie uses the same words. Nothing about pairing changed, nothing is recomputed, and rounds explained before today group the same way, because the grouping happens when the page is drawn. One limit, stated on the page: the record keeps only the alternative's deciding board, so the rest of that pairing is not shown.

  • Fix A tournament that stops publishing stops waiting to be published. Switch publishing off, bin a tournament or hand it off while a send was still queued, and the queued send stayed: refused on every pass (or never tried at all), counted every time, and shown in the top bar as Sending for as long as anyone cared to look. Each of those now takes its queue entry with it, and the queue drops any it finds for a tournament that is off, binned or away rather than arguing with it. Nothing is lost by this: taking a tournament back, or out of the bin, queues it again and sends it whatever it holds.

  • Fix "Regenerate from ratings" on numbers that already follow the ratings does nothing, and says so. It used to rewrite every pairing number with the number it already had, announce the change to every open page, and queue the tournament for the results site - which is how the top bar came to say Sending about a tournament in which nothing had happened. Now nothing is written, nothing is broadcast, nothing goes in the audit trail, and the dialog reports The numbers already follow the ratings. Nothing changed. A file's numbers stay marked as the file's, since nobody regenerated them.

  • Change The results site is not sent a tournament it already has. A queued publish is still built in full when its turn comes, and then compared with the document the results site last accepted: identical apart from the time it was built, and it is not posted. A setting saved as it was, a result typed and taken back before the send - the queue empties, the top bar goes back to Live, and the server's history stops filling up with copies of itself. The comparison is rigged to lose: it is only made by the queue, only against a document the server answered yes to during this run of the program (the memory is not kept in the database, so a restore cannot bring an old one back), only for the same server, key, credential and program version, never for five minutes after any failed send, and never more than an hour after the acceptance. A first publish, Try again, publishing switched off and on, a hand-off coming back, a restart and an upgrade all send as before. The one thing that changes for an arbiter: a results site that has started refusing a tournament (hidden by its operator, moved to another installation) is found out at the next real change, not at the next click that changed nothing.

  • Feature A switch that says "this tournament has live boards". Settings, Results site, Publishing each round has a new switch, Live boards. Off for every tournament, old and new, and only offered while the tournament is published. On, the snapshot carries tournament.live_boards: true, the tournament is published again at once, and the results site puts a Live boards link beside the rounds - which it has been ready to do for weeks, waiting for somebody to ask. Off, the key is not sent at all, so a tournament that never touches the switch publishes exactly what it did before. It is in the JSON backup, it is in the audit log, and it does not start a relay or check that one exists: tick it with nothing in the hall sending moves and spectators get a link to thirty-two pieces that never move.

  • Feature A tournament group is one event on the results site. The group was a switcher in this app and nothing anywhere else, which is an odd way to run a festival with an Open, a U20 and a U12. A published tournament's snapshot now carries a group block - the event's name and a random public id, this tournament's label and place, and its siblings by address, label and name - and OpenResults turns that into tabs at the top of every page and an event page listing the sections. The rule that kept this out of 0.80 is the rule it ships with: only siblings that are themselves on the results site are named. Not published, in the recycle bin, handed off, or set to publish and never actually sent: not its name, not its label, not a gap in the numbering where it would be, and a tournament with nothing to show beside it carries no block at all. A sibling that is published but not listed on the front page is named only on the other unlisted ones - a listed page does not point at a link-only one. The group's name does become public as the event's name; the Group card now says so, and links the event page once there is one.

  • Feature The other sections re-send themselves. Publish or withdraw a section, rename it, change a label, the order or the group's name, bin it, take it out of the group - every other published member whose page would now read differently is queued again, without anyone visiting it. Each tournament remembers a fingerprint of the block it last sent, so a result entered in the Open does not re-publish the U12 forty times an evening; only a change a sibling's page would actually show does. A takedown clears the fingerprint with the key.

  • Feature Federation flags, as a display option. Settings, Results site has a new tick beside Federations: Federation flags. On unless you untick it, for new and existing tournaments alike - a flag is the federation code drawn, not a new fact about anybody, and it shows nothing where Federations is off. The results site draws it only for snapshots that ask, so nothing changes there until this version publishes. A player under the FIDE flag gets the code and no picture. It is stored, exported, imported and audited with the other display ticks, because it is one. The app's own player lists stay flagless: they are for working, and the code column already says it in three letters.

  • Fix The explanation page no longer calls a legal pairing a rematch. Two players whose first board ended in a forfeit have not played each other (C.04.2 3.5), and the engine pairs them again when it suits the round. The page counted any board with two names on it, so it stamped its own engine's pairing REMATCH, in red, at the top of the round. It now counts games. The colours on that page had the same complaint last week; this was the sibling nobody visited.

  • Fix "This round cannot be paired" on the Forbidden pairings page means it. The check counted a forfeited board as a game played, declared the field unpairable, and the Pair button then paired it. A forfeit leaves the two free to meet; the check now knows.

  • Fix A restriction added after a round, then that round unpaired: it applies to the round you pair again. Forbid a pair after seeing it on the sheet, unpair, pair again - the forbidden pair was kept apart, but the record (and the TRF's 260) went on saying "from the next round", and a club or federation rule added the same way sat the round out entirely. Unpairing now moves such a restriction to the first round still to be paired, and to the whole event when no round is left.

  • Fix A player who withdrew and came back no longer gets a dash on the results site. OpenResults adds each round's figures up into the score a player brings to the next round, and a round with no figure is an unknown - a dash from there to the end of the event. Late entrants were cured of this in 0.83.0; a player who withdraws for a round or two, one who stays withdrawn, and one taken off a board by hand were left with it. Every such round is now published as not paired, worth nothing, which is what the standings already count. Until the results site learns the word it prints it as sent.

  • Fix A paired round is checked under its own pairing rules, not the next round's. The checker behind a manual pairing alteration, "Explain again" and the explanation page's "why not this pairing" answers rebuild the round's field - and built it for the round after the last one paired. A club rule for "the first three rounds" was gone from round 3's field the moment round 3 existed; a rule for the last rounds, or one added later, was read back into a round that never had it. The pairing was right. The verdict on it - the one that goes into the TRF as a PIBE line - was a verdict on a different round.

  • Fix The TRF's place column counts only the players in the file. A late entrant waiting for a first round, already holding the points their missed rounds pay, has a place in the standings and no line in the TRF. The place was handed out anyway and the file went from 3 to 5.

  • Fix An absence paid a full point is a full-point bye in the tie-breaks. C.07 16.1.1 keeps "requested bye" for half a point or none; an unplayed round worth a win is 16.2.1's full-point bye and is never re-read as a draw. The TRF has always written it F. The standings filed it under requested byes, so in the last round it counted as a draw in the opponents' Buchholz and the rank column of the app's own file did not follow from the file. Only tournaments that pay an absence what they pay a win are affected, which is not many, and now none.

  • Fix A backup from before "absences count as voluntary" existed is restored the way its tournament was left. The setting arrived switched off for every tournament already there and became on-by-default later; a file with no such key was given the later default, so the same event broke ties one way where it stood and another way out of its own backup.

  • Verified The app around the engine, 800 random tournaments. A new harness drives individual Swiss events through the app's own functions - late entrants, withdrawals and returns, paid and capped absences, forfeits, 3-1-0, Baku, forbidden pairs, rounds unpaired and paired again, results corrected later, seats vacated by hand - and asks the same questions several ways: the score the engine was handed against the standings, the standings against their own rounds, the TRF's points column, the pairing numbers against the initial order, the results-site document round by round, a JSON and a TRF round trip paired against the original, and the engine's own checker on the exported file. It found the fixes above that are not about forfeits or pairing rules; reading found those. Two things it found are not fixed, and are in the report: docs/audit-app-path-2026-10.md.

[0.83.0] - 2026-10-10

  • Feature "Pair round N" now stops when the pairing numbers do not follow the ratings - while that can still be fixed. A Swiss could reach round 6 with its strongest player seeded forty-third, and the only thing that said so was a yellow line nobody is obliged to read. For rounds 2, 3 and 4 - the last moment C.04.2 2.3 allows a correction - the pair button now opens a dialog instead: who is out of place (name, rating, number, the number their rating earns; the first twelve, then "and N more"), why it matters, and three answers. Renumber by rating and pair runs the Players page's regeneration and then pairs. Pair anyway pairs on the numbers as they are, writes the decision to the audit trail, and does not ask again unless a different set of players is out of place. Cancel does neither. It counts late entrants numbered after the field too, and looks one pairing ahead, so the question comes before the round that would append them rather than after. Numbers that came with a TRF or SWAR file get the same question, worded to say where they came from. Equal ratings in any order are still nobody's business; an exchange between them never triggers it. Neither answer leaves FIDE mode. Round robin, Keizer and team events are not asked. The yellow list stays, and now lists the same players the dialog would.

  • Feature After round 4, a note instead of a silence. Once round 4 is paired the numbers are final, so there is no dialog and no button - but if they are out of rating order, the Pairings page keeps a folded line saying so, with the players and the rule that closed the door. For the arbiter who gets asked in round 6.

  • Fix Unpairing the last remaining round takes its pairing numbers back. An individual Swiss whose round 1 was paired and unpaired with thirteen players in - to see what the button does, months before the event - kept numbers 1 to 13, and whatever entered afterwards was seated around them. Recent versions at least placed newcomers by rating; none of them forgot the rehearsal. Unpairing back to no rounds at all now clears the numbers the pairing issued, in the same transaction, and the next round 1 numbers whoever is entered by then, by rating. Numbers somebody chose are left alone: a TPN exchange, and an imported file's own numbers (a newcomer is still placed among those by rating). The audit entry for the unpairing says how many numbers went. Round robin (whose numbers are its schedule, and may have been drawn by lot), Keizer and team events keep theirs.

  • Feature Tournaments that number late entrants at the end are told, once. A Swiss older than the By rating default still appends late entrants after the field, because a migration said so and no arbiter did. Until round 4 is paired, the Pairings page and Settings, Options now say it - "FIDE (C.04.2 2.4) numbers them by rating. Switch?" - with Switch to by rating and Keep. Either answer is remembered per tournament and audited. Switching renumbers nobody already numbered; the dialog above deals with those.

  • Fix On a phone, Advanced and Settings open again - and you can see they exist. The tab bar scrolled sideways, which hid both menus off the right edge with nothing to hint at them, and made the bar a scrolling strip that touch browsers clip menus out of: you tapped Advanced, it "opened", and Pairing rationale was nowhere. The tabs now wrap onto two rows, every one on screen, and the menus drop below the bar across the full width.

[0.82.0] - 2026-10-10

  • Fix The pairing explanation no longer gives a forfeit a colour. It counted the seat of every forfeited game as a colour played, so a player with White, then a forfeit at Black, read as balanced and "due White" - and the page printed "✗ against due colour" beside the Black the engine had correctly given. FIDE C.04.2 3.4 counts played games only, and so does the page now: forfeits show as dashed "not played" squares, a counts as strip shows the history the way the rule reads it (unplayed rounds first), the due colour follows the same ladder Ainalrami uses, and a verdict that hinges on a skipped round names it. The team account's colour column puts its unplayed rounds first too. The pairings themselves were never wrong; only the commentary was.

[0.81.0] - 2026-10-10

  • Fix A late entrant's running score on the results site no longer turns into a dash. In an event that pays nothing for an absence, the rounds before a player joined were simply missing from what OpenResults received, and it read "missing" as "unknown" - so "points before this round" showed a dash beside every late entrant for the rest of the event. Those rounds now travel as "not yet joined", worth zero, which is what the standings had counted all along.

  • Change The sign-in page says what the app does now, not what it did in the summer. Event groups, FIDE mode and the Dutch manual made the list; "validated against 2.5 billion pairings" became "checked against billions", which stays true without a recount. "Welcome back" and "Confirm it's you" finally speak Dutch too.

  • Removed JaVaFo. Ainalrami - the default since August, and the one that knows the rules changed in February - is now the only Swiss engine. Gone with it: the Swiss engine choice on Settings, Options (the page now just names Ainalrami), the "Switch to JaVaFo?" dialog, every "not with JaVaFo" note on the Players, Pairings and Forbidden pairings pages, and any reason to have Java installed. A tournament still set to JaVaFo when this version first starts is moved to Ainalrami, and the upgrade writes one line in its audit trail saying so and how many rounds were already paired; its rounds stay as played, and its next round is paired by the 2026 rules instead of the 2017 ones. That is the upgrade's doing, not the arbiter's, and it is not marked as a FIDE-mode departure: the rules it moves to are the ones in force. Old JSON exports that name an engine still import - the key is ignored - and a TRF whose type code says FIDE_DUTCH_2017 imports as a Swiss, with a line in the review saying the next rounds follow the current edition. The older TRF spelling is ?dialect=engine now; ?dialect=javafo still works for anyone who bookmarked it.

[0.80.0] - 2026-10-09

  • Feature Tournament groups: one event, several tournaments, one click between them (Settings, Tournament, Group). The Open, the U20 and the rapid on the side were separate islands in the list; put them in a group and every page of each gets a switcher at the top - "Open | U20 | U12" - that opens the same page of the sibling: Standings to Standings, Players to Players, and the Players page when the sibling has no such page. A dropdown on a phone. Each tournament keeps its own players, rounds and pairings; this is not player categories. Create a group from a tournament, add one to a group you can already edit, give it a short label, reorder, take it out - the last one out takes the group with it. Owners and collaborators alike, on the tournaments they may edit. The switcher lists only the tournaments the viewer may open, so sharing one section of an event does not announce the others. The home list gathers a group under its name, foldable. The JSON export names the group for whoever reads the file; the import ignores it and the copy arrives ungrouped, because the file does not get to decide what this machine's tournaments belong to. Not a FIDE matter, and not published to OpenResults yet.

  • Feature Rating tie-breaks can count each game at the rating of its round (Settings, Tournament, Rating-based tie-breaks use each round's rating, shown once the tournament lasts more than 30 days). On, each opponent counts at the rating they held in the round the game was played: ARO and its cuts, TPR, PTP, APRO and APPO, and the per-round working the public standings show. RTNG still sorts by the first rating. Off by default, because C.07 Article 10 makes the first rating the rule unless the regulations say otherwise; this is for the regulations that do. The standings now say which rating the tie-breaks used, which beats leaving people to reverse-engineer it from the ARO column. Carried by the JSON export and import; TRF has one rating per player and stays that way. Needs Ainalrami with per-round tie-break ratings (VCL4THP Q214).

[0.79.0] - 2026-10-09

  • Feature Round-1 absentees as late entries can be switched per tournament (Settings, Options, next to the late-entrant numbering). New tournaments have it on; an older or imported one can switch it on - or off - until round 1 is paired, then the checkbox greys out with the reason and the server refuses the change too, since flipping it would renumber players in rounds already played. Not shown for Baku (always on), round robin or Keizer. Moving toward C.04.2 2.4 is not a FIDE departure; the change goes in the audit log.
  • Feature Tom W, a pale sky-blue theme with a royal-blue accent, under Board in the theme picker. Named for someone whose help shaped a good deal of this app; he gets a colour scheme, which is more than arbiters usually get. A light theme, so the accent picker works on it exactly as on Light: red means red.
  • Removed Mocha. It did not earn its place. If it was your theme you are back on System, on every device, until you pick another.
  • Fix Dropdown arrows stay inside their box. Every dropdown draws its own arrow now, in the theme's colour, with room kept for it - the browser's own sat on the edge and, in some browsers, half outside it. Long options end in "..." instead of running under the arrow.
  • Change "Preview next round" is in the Belgian pack (Federation features, BEL), off until you switch it on. The "Optional tools" group it sat in alone is gone - a group of one is a shelf, not a category.
  • Change The rating inbox counts deadlines the way FIDE does (B.02 Art. 9.1). A report goes to the list of the month its tournament ended in, or the next month's when five days or fewer were left of that month. Past that list it is amber ("goes to a later list", with the list it will now make); past the third list it is red ("will not be rated"). Postponed-games files count from the last game's date. Nothing is refused: the inbox tells you which colour you earned, it does not take the file away.
  • Feature A receipt keeps the file that was sent. The exact bytes, the name it went out under and its size, in the database beside the hash (a TRF is kilobytes; anything over 2 MB is not kept, the hash still is). The inbox now downloads that file and Check checks it, a postponed-games file included. A receipt from before this release has no file: the inbox keeps offering a rebuilt copy for it, and says that is what it is. The file travels with the tournament export, but only if it still matches its hash.
  • Feature Receipt (JSON) replaces the "Rating validator (YAML) - Soon" button on the Export page, which had been soon for long enough. One download per sent file: code, fingerprint, kind, round or period, sender, time, the games as sent, the file's SHA-256 and its name. For the tournament's owner, its collaborators and administrators.
  • Fix Settings > FIDE: "Saved." beside "Save FIDE settings" no longer answers a click in the Rating lists card, and a click in that card no longer throws away the FIDE-ID range row you were still typing. The two shared one note, one error and one "nothing unsaved" flag, so each spoke for the other.
  • Fix A TRF with an unknown result (?) has its later rounds checked. The engine will not score a ?, so the import's round check crashed on the first round after one and checked nothing at all - a thorough way of finding no problems. It now counts each ? game as the draw the import makes of it (a postponed game, the FIDE value), checks every round, and the review step says which games were assumed, what as, and which rounds rest on it. A symbol the import does not know (rewritten to ?) gets the same treatment.
  • Feature Ask the bye type for each absence. A new switch on Settings - Scoring, under Byes and absences, off by default and offered in an individual Swiss. On, the player dialog asks for every round in "Absent at the rounds" not yet paired whether it is a half-point, zero-point or full-point bye, with the one the absence points would give already picked - so Save is usually the only click, and the other answer is there for the player who did not ask in time. The pick is stored as the bye itself: it survives an unpairing, goes into the TRF (240 while the round is unpaired) and comes back on import. The half-point bye rules count a picked one (a second asks for confirmation, an ineligible player cannot have one and is offered the zero instead), and a picked full-point bye gets the same notice the Pairings page gives one. The limits on paid absences still decide what is pre-picked - an earlier half-point or full-point bye counts toward "only the first N are paid" exactly as an absence would - but the bye picked is scored as picked. Pick past the limit and the form says so, then lets you, which is the point of asking.
  • Feature "Mark absent for this round" asks too. With the switch on, emptying a seat in a paired round on the Pairings page asks the same three-way question in its confirmation, picked the same way, and stores the answer as the bye. The half-point rules come along: refused for a player marked not eligible, a second one needs its own tick, and a full-point bye gets its Level-2 notice. Off, it is the plain absence it always was.
  • Fix A half-point bye count no longer mistakes a typed bye for the absence value. A round with a zero-point or full-point bye row (from an import) in a tournament paying half a point for an absence was counted as a half-point bye by the second-bye warning and the not-eligible check.
  • Change A player absent from round 1 is a late entrant in every new Swiss, not only in Baku (C.04.2 2.4): no pairing number at round 1, numbered on arrival by Late entrants' pairing numbers. A round 1 unpaired and paired again numbers whoever is now present by rating, and takes the number back from whoever is now absent. Tournaments created before this version, and those imported from a TRF or SWAR file, keep numbering round-1 absentees with the field - their numbers were already somebody's; a backup keeps what its tournament had.
  • Fix Round 1 paired again by hand places a player entered since the first pairing by rating, as the engine's pairing does, instead of last.
  • Feature Pairing numbers out of rating order are said out loud. Until round 4 is paired, the Pairings page lists the players whose number no longer matches their rating - with the number they should have - above Pair round N, and links to Regenerate from ratings. Equal ratings in any order and late entrants numbered after the field are left alone. It warns, it does not block; a 2090 sitting at number 17 had been quiet about it long enough.
  • Feature Forbidden pairings get a page of their own, and rules. Settings, Forbidden pairings: "same club" and "same federation" are rules now, any number of them, each a rule or an "if possible" wish, for every round, the first or last N rounds, or rounds X to Y - C.05 5.2's own "compatriots, if possible, not in the last rounds" is two clicks. A rule follows the roster as it is when the round is paired, so the late entrant from the same club is covered without anybody remembering to. Tick any number of players and keep them apart in one go (two make a pair, more make a group), edit and remove in place, and see what it all costs the next round before pressing Pair: games ruled out, players left with nobody, and a red line when the round cannot be paired at all. Picking players two dropdowns at a time is over.
  • Change The club and federation exclusions and "keep clubmates apart for the first N rounds" became rules, with exactly the meaning they had; existing tournaments and older backups are converted on the way in. The TRF report writes a club rule as one 260 group line instead of every pair in it. A SWAR export says when a rule limited to some rounds cannot go along.
  • Change In FIDE mode, prohibitions are announced before round 1 (VCL4THP Q195/Q196, C.05 5.2). Setting them then is fine, hard or soft. Adding, changing or removing one after round 1 is paired - or changing how hard the wishes are tried - asks twice and leaves FIDE mode, and the TRF copies say what changed in a ### Prohibition @ Round r line. A late entrant falling under a rule is not a change. Tournaments that added a prohibition mid-event before this version keep their FIDE mode: the past is not retried under the new law. The checklist path now has no failures.
  • Fix Boards are numbered in C.04.2 Art. 3.6's order (Ainalrami 0.41.0). The engine handed the boards back in the order it found them, bracket by bracket, which is not 3.6's order once a float or a mixed bracket is involved: in about one round in thirteen of a small event a board sat above one it belongs below. Now it is the higher-ranked player's score, then the pair's sum, then that player's pairing number, the pairing-allocated bye last, with Baku's virtual points counted. Same pairs, same colours, same bye; only the numbers on the tables move.
  • Change The manual catches up: the rating period inbox has a section of its own, the board order is written down, the next-round preview is where the Belgian pack put it, and the theme list names Tom W and no longer promises only light and dark.

[0.78.0] - 2026-10-08

  • Performance Ainalrami 0.40.0. Pairing 40 to 80 players takes 1.6 to 1.7 times less time: the engine's certified shortcuts now start at 30 players, not at a 100 that someone once guessed and nobody measured. Same pairings - 2.16 million rounds paired both ways, not one difference.

  • Change FIDE mode makes nothing final while a postponed game is open. No TRF that holds the open game's round - copy, engine spelling or the file Send… hands out - and no final standings, on screen or on paper, until every postponed game has a result; the refusal lists the open games. A file of only the rounds before it (the rating inbox's copy of a round sent last week, say) is made as usual: it says nothing about a game that had not been paired yet. OpenResults keeps showing the standings, marked not final, because that is what they are. The other way on is Not played in this event beside each game on Settings, Export: it asks twice, takes the tournament out of FIDE mode for good, writes the game as 0000 - Z in the file for rating and names it in a ### Not played @ Round r line in the copies. The game itself stays postponed, so if it is played after all it still goes in the postponed-games file. Outside FIDE mode nothing changed: the old way was always right for a club that never asked FIDE's opinion (VCL4THP Q169).

  • Feature Result symbols that are not result codes import as unknown. A TRF file with a 5, an x or a % in a result column used to be refused outright. The import now reads such a symbol as a game with an unknown result, imports it as a postponed game, and lists every one on the review step - player, starting rank, round and the symbol found - for you to confirm; Cancel imports nothing. The opponent's result in that round is set to unknown too, and said so, because both sides of a game have to agree. A symbol beside no opponent (0000) is still refused: a game needs two people, even a lost one. The refusal names the player, their starting rank, the round and the symbol, rather than leaving you to find it in three hundred lines.

  • Change Late entrants are numbered by rating by default. A new Swiss tournament gives a player who joins late the pairing number their rating earns and moves everybody below down one (C.04.2 2.4); the rounds already played keep their boards. Every tournament that already exists keeps numbering them after the field, as it did, and stays in FIDE mode - nobody chose that value, a migration did, and an upgrade that evicted every event from FIDE mode overnight would be a strange way to announce a default. A backup file from before the setting existed imports the same way; a TRF or SWAR import, which has no say in the matter, is a new tournament and gets the new default.

  • Change Choosing "After the field" leaves FIDE mode. The FIDE rules give a late entrant the number their rating earns, so picking the next free number instead is a departure, asked about twice like the others, recorded for the round it happened in and written into the TRF26 report. The grandfathered value shows up on the Options page only for the tournaments that already have it.

  • Change Printed pairings and standings show the tournament rating. The rating beside each name on the pairing lists, standings, cross table, cards, score sheets and the next-round preview is now the one the tournament's Tournament rating setting ranks by, not FIDE-else-national regardless. The column says Elo when the FIDE rating leads, Nat. when the national one does and Rtg otherwise - five of the six methods had been printing a number the pairing never looked at.

  • Fix A bye entered on the player for a round not yet paired is in the TRF. "Absent at the rounds" for a coming round went nowhere near the export; only a bye that had arrived in an imported file did. It is now written like one - a 240 record in TRF26, the next round's column in the engines' spelling - lettered by what the tournament pays an absence (H for half a point, F for a win's worth, Z otherwise), and reads back in as that bye. Not in an individual round robin, which pairs everybody anyway.

  • Fix Unpairing a round no longer forgets a granted bye. A half-point, zero-point or full-point bye for a player still absent from that round was deleted with the round, and the next pairing recorded a plain absence at the tournament's absence value instead. It now stays, and the same bye comes back, team events included. The other way round as well: taking the round out of a player's absences before it is paired drops such a bye, rather than leaving it to score the player for a game and a bye.

  • Feature The manual in Dutch. /help follows the language you picked: all fifteen chapters, search included. Every section keeps the English one's address, so the "?" links land in the same place in both languages - a translation that moves the furniture is just a different manual. A chapter not yet translated would show in English rather than not at all.

  • Fix Baku: a round-1 absentee is not in Group A's count. A player on a requested bye, absent, or starting later than round 1 used to be numbered with the field and counted in N, so eight players and one no-show got a Group A of six instead of four. FIDE C.04.7 1.2 forms Group A from the players paired in round 1, and C.04.2 2.4 numbers a late entry "only when they actually arrive"; now they get their number on the round they turn up - after the field, or by rating if Late entrants' pairing numbers says so - and Group A keeps its last player (C.04.7 1.3.2). A number handed out before round 1 to somebody who then does not play it is taken back and the field closes ranks, and a TPN regeneration or exchange in the meantime no longer hands one out early. Events already under way keep the Group A they were paired with. (VCL4THP Q111, which cost 35%.)

  • Feature A round robin can be paired by hand. Pair round N by hand… sits beside the Berger button: the round starts empty, you pair it from the not-playing list, and whoever you leave off the boards sits out with the zero-point bye. It is the Swiss hand-edit session, with the Berger table's round as the checker - a round that differs from it needs a tick and goes in the TRF as a ### MPA line. Individual round robins over one table only; team, per-category and match-format ones keep their table (FIDE VCL4THP Q100).

  • Feature Hand-made round-robin rounds are held to the round-robin rules. A pair meeting twice in one cycle, or a player on the same colour three rounds running, needs its own tick as you make the board; finishing also checks that the rounds left in the cycle can still pair everyone exactly once - an actual search, not a count, because six players can fit every count and still be stuck with two triangles. Edits to rounds the table paired are judged the same way (VCL4THP Q101, Q102).

  • Change After a round made by hand, the Berger button pairs the remaining rounds - and refuses, naming the two players, when the table's next round would pair a couple who already met in that cycle. Round-robin rounds no longer wait for the previous round's results before the table is paired; they never depended on them.

[0.77.0] - 2026-10-08

  • Change Help opens in a new tab. The top bar's Help and every "?" beside a page title open the manual in a tab of their own. Before, reading how the Pairings page works cost you the Pairings page - help with a sense of irony. The manual's subtitle no longer says "The arbiter's handbook": FIDE already has an Arbiters' Manual, and two books with nearly one name is one too many.
  • Feature A findings list worth reading (the component behind the Interclubs plug-in's season check): findings grouped by kind, coloured by how much they matter, filtered with a click, and each one opening to say what it means and what to do - instead of a bullet list that said all of it at once.
  • Performance The next-round preview asks the engine once per batch, not once per outcome. The outcomes still to pair go to Ainalrami's pair_variants in chunks: the field is built once and each outcome is just the players of the open games with their result and score. Same boards, colours, byes and refusals as pairing each outcome alone - a test compares the two on 2,367 outcomes of 1 to 6 open games, refusals included. Six open games (729 outcomes), one core: 7.0 s to 5.0 s at 40 players, 3.0 s to 2.0 s at 150; four cores: 2.1 s to 1.6 s and 1.0 s to 0.7 s. With bye preferences in play, or pairing by category, the preview pairs one outcome at a time as before - the batch does not take those, and guessing is not faster, only wrong sooner.
  • Change Ainalrami v0.39.0 (from v0.38.0): adds pair_variants, the batch above. Pairings are unchanged.

[0.76.0] - 2026-10-07

  • Change Plug-ins are for administrators only. The Plug-ins menu, the installed plug-ins page, every plug-in page, a plug-in's tournament menu entries and its feature switches show only for an administrator.
  • Feature Announced boards. The next-round preview can record that the name cards of its fixed boards are going out: Announce fixed boards, or print them with "these cards go out now" ticked. The Pairings page shows the announced boards of the coming round, can withdraw them, warns when a result, a player or a setting changes in a way that could break them, and Check again lists the ones no longer certain. When the round is paired, any announced board whose opponent, colours or board number differ is shown in a warning that cannot be missed, announced next to paired, until you confirm the cards are re-printed; when they all hold, a short line says so. The pairing is never changed to match an announcement. Everything goes into the audit trail.
  • Performance The next-round preview remembers what it has paired. Entering a result for one of the open games updates the preview at once, without pairing anything again; clearing a result pairs only the combinations that are new. Any other change works it out in full, as before.
  • Fix A player listed twice in "Add player". A KBSB member whose entry on the national list has no FIDE id showed up a second time as the matching Belgian FIDE entry. Same name and birth year now make one line, carrying the FIDE id, and picking it fills the FIDE details in.
  • Feature "Run again" on the next-round preview, to work it out afresh at any time - after entering results, or after a run that failed.
  • Feature The Chess960 starting position as a board. Hovering or clicking the round's position on the Pairings page shows it on a diagram, and the printed pairings carry the same diagram beside the position.
  • Change "Preview next round" is an optional tool now, off until you switch it on (account settings, "Federation features", "Optional tools"). The group that held the general switches is renamed from "Pairing options" to "Optional tools".
  • Change Checkboxes drawn by the app. Every checkbox (settings, forms, filters) is a clean, sharp box in the theme's accent colour, in every theme, instead of the browser's own thin one.
  • Feature Plug-ins, for the hosted server only. A server build can now include extra modules that the published application and the desktop downloads do not contain; the desktop application is unchanged and has no trace of them. Where a plug-in is installed, the home screen gets a "Plug-ins" menu with each one's name, version and description, and an "Installed plug-ins" page. A plug-in can add its own pages, a switch under Features, players for a team's roster on the Teams page, and checks on a team match's line-ups, shown on the match page while the line-ups are entered and before they are saved. The checks only report: they never block a save or change a pairing or a result. A plug-in that sets the board order of a league's team matches lets a line-up out of roster order be saved and reports on it instead. A plug-in can also number a league's teams on a larger Berger table, where an empty number gives its opponent the bye; when that table is not the one FIDE's tables give the field, the first round paired from it takes the tournament out of FIDE mode, the same way a soft rule does.
  • Change The user manual is redesigned as an arbiter's handbook. Help (/help) now has a chapter list grouped in three parts that stays beside the text (folded behind a button on a phone), an "On this page" outline that follows your reading, a breadcrumb, previous and next chapter cards, and a reading column with a proper type scale. Every heading has a link you can copy. Rules and warnings stand out as callouts (FIDE rule, warning, tip, note), procedures are numbered steps, keys are shown as keys, and tables scroll on a narrow screen. The text itself says the same as before.
  • Feature Search the manual. A search box above the chapters (press / or Ctrl+K) finds every section that contains all the words you type, with the matches marked, and opens the chapter at that section.
  • Feature Screenshots in the manual. Each chapter has room for one or two numbered figures that open larger on a click. Until a picture is added the figure shows which screen it will show.
  • Feature A "?" beside the title of the Players, Pairings, Standings, Teams, FIDE and Export settings pages, and the TRF import, opens the manual at the section about that page.
  • Change Printing a manual chapter prints the chapter alone: no menus, no chapter list, callouts and figures kept whole.

[0.75.0] - 2026-10-07

  • Change Ainalrami 0.38.0. The same pairings in the program; the engine's standalone checker and command line now also take the Swiss and round robin match formats, pairing by category and soft pairs, so a file from a tournament using them can be checked outside OpenPairings too.

  • Fix A bye a TRF file records for a round not yet paired keeps the player out of that round. A half-point, zero-point or full-point bye written ahead in a file (in the next round's column, or as a TRF26 240 record) was lost (the column) or did not stop the player being paired (the 240), who was then scored for the bye and for the game. It is now kept as a requested bye for that round, as one entered on the player is, and the import's confirm step lists it. A round robin, which pairs everybody every round, does not import such a bye and says so. A TRF26 export of a tournament with a bye granted ahead no longer takes that bye off the player's total (it could show a negative score).

  • Feature User manual in the program. A Help link in the top bar of every page opens a manual for arbiters, fifteen chapters from installing and FIDE mode through pairing, results, tie-breaks, printing, import and export, the FIDE report, teams, publishing and hand-off. It needs no account, and inside a tournament it opens at the chapter for the page you are on.

  • Fix Leaving FIDE mode always asks twice. A settings save that would take the tournament out of FIDE mode, the pairing by category, and a pairing that a bye exclusion, a bye preference, an "only if possible" wish or extra points would move now show the same two-step confirmation as the explicit "Leave FIDE mode" button, naming what departs. Cancelling at either step changes nothing.

  • Feature Hand edits to a round are a checked manual pairing alteration. Editing a round by hand opens a session that ends with "Finish hand edits"; the next round waits for it. An edit that makes a rematch, a prohibited pair, a second pairing-allocated bye, three same colours running or a colour difference over two, or gives both players the wrong colour, names the problem and needs its own tick. Finishing compares the round with the pairing checker's own pairing; keeping a different one is recorded and written in the TRF26 report as a ### MPA line.

  • Feature A round with no legal pairing can be paired by hand. When the engine finds no legal pairing, the Pairings page offers to create the round empty and pair it from the not-playing list, with a new "Give the pairing-allocated bye" action.

  • Fix A TRF import shows what it changes before it imports. The file's version (TRF06, TRF16 or TRF26) is named and every adjustment is listed for Confirm or Cancel before the tournament exists. A file whose rounds break a pairing rule asks for "Import anyway", the finding is kept with the tournament and its TRF26 reports carry it as a ### Import line. A round check that fails is reported instead of passing silently.

  • Feature Every FIDE tie-break can be selected. All of C.07's Swiss and round-robin tie-breaks are in the list, grouped by kind, and written to and read from the TRF in C.07's spelling. A tournament can say how an unrated player counts in the rating tie-breaks (a fixed rating, the lowest in the field, or the average of the rated players), let players still level share a place (2=), draw lots for ties into the manual ranking, and use an externally calculated value as a tie-break.

  • Feature Tournament rating method. Settings, Options chooses the rating that gives the pairing numbers and that the rating tie-breaks read: FIDE, national, FIDE else national (the default, as before), national else FIDE, the highest of FIDE, national and a typed rating, or a typed rating alone. Players level on rating are ordered by FIDE title, then by an announced criterion, and a Swiss can number late entrants by rating.

  • Feature Pairing numbers by hand. Until round 4 a Swiss's players of equal rating can exchange pairing numbers, or the numbers can be regenerated from the ratings, each confirmed first. Before round 1 a round robin's starting numbers can be set by hand or drawn by lot.

  • Fix Round-robin standings leave out a player who withdrew early. A player who withdraws from a round robin, or is expelled, having played under half of their games no longer counts in the standings (C.05 6.6): their games count for nobody's score or tie-breaks. The cross table and the rating report keep every game.

  • Feature Rating lists kept current and checked against the right month. The FIDE list is checked once a day and downloaded when FIDE has a newer one; each rating keeps the list and month it came from; the rating check uses the list valid at the tournament's start, verifies the list first, shows a notice on the Players and Pairings pages when ratings differ (switchable per tournament), and applies only the changes you tick.

  • Feature Rating-list sequence and your own lists. Each tournament reads ratings from an ordered sequence of lists, with FIDE's defaults for standard, rapid and blitz, and a rating from another list of the sequence can be picked instead. Administrators can load rating lists of their own from a CSV file.

  • Feature Byes and player status. A full-point bye can be given to a player sitting a paired round out (written F, with a ### line); a second half-point bye asks for confirmation; a player can be marked not eligible for half-point byes, or expelled (not paired further, left out of the standings). A withdrawn player is marked in the standings.

  • Feature Corrections recorded as the rules want. A result corrected after a later round was paired is confirmed first, takes a restore point, is logged and gets a ### Correction line in the TRF26 report. A result found wrong after the next round is over can be corrected for the rating report only, while the pairings and standings keep the result they used.

  • Feature Tournaments lasting more than 30 days. A tournament can hold a later rating per player with the round it applies from; expected scores use each game's ratings and the rating tie-breaks use the first rating or a chosen round.

  • Fix The TRF26 report leaves less out. National ratings that rank a player are written as National Rating Support records with their 172, extra points carry their round in 299, and a prohibition added mid-event is a 260 from the round it was added for.

  • Feature Chess960. A tournament can be played as Chess960; a starting position is drawn per round on the Pairings page and printed with the pairings.

  • Feature A warning before round 1 when the rounds cannot all be paired. A Swiss with more rounds than its players allow, or a round robin whose rounds do not match its schedule, is flagged on the Pairings page.

  • Feature Rating period inbox (administrators only, Admin page and a link on Settings, Export). Per FIDE rating period it lists every tournament's sent rounds and postponed-games files with their receipts, the rounds of FIDE-rated tournaments that are finished but have no receipt, the postponed games still open, and the deadline (shown in red once it has passed with something still to send). A sent round can be downloaded as a TRF copy and checked with Ainalrami's checker. The file that was sent is not kept, so the copy is rebuilt from the tournament as it is now; a round that changed since it was sent is flagged.

  • Feature Title norms know the kind of event. The FIDE settings page has an "Event type for title norms" choice, and the Norms tab judges with it, per the FIDE Title Regulations (B.01). A World or Continental Team or Club Championship of 7, 8 or 9 rounds needs 7 games for a norm instead of 9 (1.4.1 b), and the World Cup 8. A national team championship (1.4.3 b) or the final stage of a national championship (1.4.3 a) lifts the federation-mix requirement for players of the federation registering the event, a Zonal or Sub-zonal (1.4.3 c) for everyone; each exempted check says so and repeats 1.4.3 e. Only the kinds that fit the tournament (team or individual) are offered. An ordinary event, the default for every existing tournament, is judged exactly as before.

  • Security The "remember me" cookie is marked Secure and HttpOnly explicitly in production, like the session cookie, instead of relying on the request being recognised as https behind the proxy.

[0.74.4] - 2026-10-06

  • Feature SWAR 3-2-1 tournaments import. A .swar file of SWAR's "3-2-1" type was refused; it now imports, scored exactly as SWAR scores it, by rules read off SWAR's own source: the club's win, draw and loss values, a presence point for every round the player was there (a game, a 0-0, a ½-0, a forfeit win - not a forfeit loss, a double forfeit or an absence), every bye worth the club's bye value plus, when the club ticks it, a presence point, and an absence nothing. A pairing bye is worth 2 points under the usual settings, where the earlier, switched-off mapping paid 1. The few codes SWAR's own 3-2-1 dialog never writes (a bye scored as a win or a draw, a draw by forfeit) import with a warning naming the rounds. Exporting a 3-2-1 tournament keeps its values, and a Swiss with its own point values, which goes to SWAR as a 3-2-1 file, now imports back. One difference stays: SWAR computes some tie-breaks (Koya among them) on the 1, ½, 0 scale in a 3-2-1 event, where OpenPairings uses the tournament's own points. Tournaments created here score as before.

  • Fix A player's bye controls no longer vanish without a word. With "Bye preferences" switched on, a tournament where they cannot apply (a FIDE-rated one, JaVaFo as the engine, a round robin, Keizer or a team event) showed nothing at all on the player, which looked like the switch not working. It now shows a one-line note saying why, and what to change.

[0.74.3] - 2026-10-04

  • Change "Bye preferences" sits last in the Belgian pack on the Features page, below the federation's own switches, instead of in a group of its own above them. Same switch, same key: nothing is switched on or off by the move.

  • Feature "Add player" searches the KBSB list too. With the KBSB player lookup switched on, the search box finds members of the national list by name or national id (G-licences included), listed before the FIDE hits. Picking one fills in the national id, rating, club and birth year, and - when the member has a FIDE id - the FIDE title and rating for the tournament's rate of play. A FIDE hit for a member already listed is not shown twice. Without the lookup the box searches FIDE only, as before.

[0.74.2] - 2026-10-04

  • Change Confirmation questions in the app's own look. "Hide the public standings after round 1?" and every other "are you sure" question was the browser's grey system box. It is now a dialog in the app's colours (light and dark), with Cancel focused first; Escape cancels. The questions and what OK does are unchanged.

  • Change Teams page: a team's absent rounds are coloured. Ticking round 2 under "Absent as a team in round" fills that button in, instead of only adding it to a list below. The line "Absent in rounds: ..." now only lists rounds already paired, which no longer have a button.

  • Change One switch for the pairing-allocated bye. "No pairing-allocated bye for chosen players" (in the Belgian pack) and "Bye preferences" (on its own) were two switches for one thing: never getting the bye is the strongest of the same preferences, and no more Belgian. The single "Bye preferences" switch in Settings - Features now shows both controls on a player. An account that had the exclusion switched on keeps it: it now has "Bye preferences" on. Stored exclusions and preferences pair exactly as before.

  • Change Up to 99 rounds. The maximum was 30, which ruled out a weekly club event over a whole season, which can need 40 or more. New tournament, Settings and the account defaults now accept 1 to 99; a round robin that needs more than 99 rounds is still refused with the reason. The TRF and SWAR files have room for it (a SWAR round count is a 32-bit field; the longest TRF line at 99 rounds is 1,081 bytes, under the import's 2,048-byte line bound).

  • Verified G-licence players import like any other member. KBSB's players file lists them with a negative id and no club. They are imported, found by name and by id, and keep their id through SWAR and TRF export; a test now holds this.

[0.74.1] - 2026-10-04

  • Change An unrated player in a team's rating counts 1400, not 0, and the value is a setting. The team rating that seeds the teams (Olympiad, first boards and whole-roster methods) gave a player without a rating 0, and a board nobody sits at too. Both now count Rating of an unrated player (Settings - Options - Teams, team_unrated_rating, 1400 by default, 1400 for every existing tournament): the FIDE rating floor (Rating Regulations 2024 Art. 7.1.4 and 7.2.1; the World University Team Championship regulations Art. 5.2.4; the Olympiad Pairing Rules of 2012 Art. 7). A short roster is thus not ranked below a team with an unrated player. The Olympiad tie-break on the fifth player's rating uses the same value; a typed team rating is unchanged; a team with no players at all still rates 0. Seeds already stored never change by themselves: only Order by rating and the round-1 auto seeding use it.

[0.74.0] - 2026-10-03

  • Fix A team file sent for rating: the 310 match and game points follow the file's own games. A postponed board is written 0000 - Z in the file sent for rating, but the team record still counted it provisionally (a draw), so its match and game points disagreed with the 001 lines. The 310 now counts only the boards the file holds: a match with a postponed board is decided by the other boards, and a match with every board postponed counts for nothing (it is rated in the postponed-games file); the team's rank is then left blank, as for a file of chosen rounds. The copy, the engine's input and the TRF26 download are unchanged.

  • Feature Team events without players. Settings - Options - Teams - Line-ups: Optional pairs teams whether or not they have players on their rosters (the default, Required, is how every team event worked before and stays FIDE's). Every match gets all its boards, the empty seats with no result; a result goes on a board nobody sits at, or on the match as one score from its page (2½-1½ - written onto the boards as wins on the top boards and draws, so the standings, the tie-breaks, the TRF and OpenResults read it like any other match). A team sits a round out only when it is absent or withdrawn as a team. A board without two players is no rated game: the TRF leaves it off the 001 lines, the 310 still carries the match and game points, and a match won because the other team was absent is a 330 record. In FIDE mode the Export page warns before such rounds are sent; it does not block them. Not a FIDE procedure. The setting locks when round 1 is paired.

  • Feature Teams page: a team can be marked absent as a team for any round not yet paired - a team Swiss leaves it out, a team round robin gives its boards to the opponent - with either line-up setting.

  • Fix Team round robin: a match neither team can field a single player for (everybody absent on both sides) is a double forfeit - both teams lose it, as when the arbiter records one - instead of scoring as a drawn match with no boards. The TRF writes its 330 --.

  • Fix Team events: a match forfeited by decision to a team that had nobody seated at any of its boards is that team's win. Its forfeit wins sat on empty seats, which score nothing, so the match came out a drawn 0-0. The TRF names such a match in a 330 record, the only place it can show.

  • Change Team rating for the order of the teams: the Olympiad's rule by default - the average of a team's highest-rated players, one per board, whatever their board order, then the next player's rating, then the name (FIDE Olympiad Pairing Rules, Art. 3.1). Settings - Options - Teams offers the first boards in board order (the rule until now), the whole roster, or a rating typed for each team; a rating typed for a team on the Teams page is always used, so a team with no players can be seeded too. The Teams page shows each team's rating, OpenResults gets it (teams[].rating), and the TRF writes it in the 310 record's strength factor. Pairing round 1 now orders teams nobody moved by hand by rating first; an order set with the arrows, and every tournament that already had teams, keeps its order.

  • Fix A round without a date keeps its place in the schedule. A blank or null entry in a tournament's round dates was dropped when they were saved, so every later date moved up a round: a 7-round event known only by its first and last day (["2026-10-18", null, null, null, null, null, "2026-10-25"]) came back with 2026-10-25 on round 2 and rounds 3 to 7 blank. Ecto removes blank strings from inside a list field it casts, and every writer of the round dates went through that cast: the JSON import (an OpenPairings backup with a gap in its dates, or an envelope from another program such as the KBSB dataplatform's registration export), the SWAR import, and saving the Dates page with a round left empty. A blank now means "no date for that round" and stays on its own round; a null is stored as a blank. The start and end dates are still the earliest and latest dated rounds. A schedule without gaps saves and imports exactly as before. Dates already shifted by an earlier save or import are not moved back; correct them on the Dates page.

  • Change The TRF sent for rating holds only records, and never an unknown result. The files "Send…" hands out (a round's report and the postponed-games file) now look like the FIDE files SWAR sends, which the FRBE→FIDE path accepts: no column ruler, no DDD legend line and no comment line - no ### receipt line, no "FIDE mode exited" note. The receipt's code and the file's hash stay in the app. A postponed game whose result is not known when its round is sent is written as not played for both players, 0000 - Z (TRF26's zero-point bye, "Not rated"; SWAR writes the same for a round a player was not paired in), instead of ? with X in a 162 record: it counts zero in the points column, the rank column follows the file's own games, and the game is rated once, with its result, in the postponed-games file after it is played. Nothing then depends on how FIDE's rating server would read a ?. Copies, the TRF26 and engine downloads and the file the pairing engine and checker read are unchanged (they keep ? and X); ½-0, a played 0-0 and a double forfeit are written as before.

[0.73.1] - 2026-10-03

  • Fix Teams: any team can be deleted again while it is in no round. A pairing that numbered the teams and then refused (too few teams, a team with nobody to field) kept the numbers with no round on the board, which froze the order of the teams and left the Delete button only on teams added afterwards; the numbers are now given back, and an upgrade gives back the ones such a refusal left. Every team shows Delete: a team in a match already paired (or in a team round robin's schedule) is not deleted, and the page says why and that it can be withdrawn instead. A team Swiss team that never played - it sat every round out, or came later - can be deleted after round 1.
  • Fix Round robins: two cycles can be chosen after the first cycle has started, up to the start of the second one (outside FIDE mode, where the number of rounds is fixed before the start). The number of rounds now follows at once, so the Pairings page offers the second cycle - the same pairings with colours reversed (FIDE C.05) - instead of reporting every round paired. A two-team match of one round could not be extended at all. Going back to one cycle is refused once the second cycle has a round.
  • Fix Team Swiss: a round that cannot be paired because every team has already met every other says so - a Swiss with N teams has at most N-1 rounds (N with an odd number) - and points to a round robin with two cycles, instead of the general "every pairing breaks an absolute criterion". Settings - Options says the Cycles setting is for round robins only; a Swiss never used it.
  • Feature Double round robins: the last two rounds of the first cycle can be played in reverse order, FIDE C.05 Annex 1's recommendation, so nobody has the same colour three times running where the cycles meet. On for a new individual double round robin, and when an event switches to two cycles before those rounds are paired; a setting under Settings - Options. The TRF names it FIDE_DOUBLEROUNDROBIN, and a TRF with that code imports with it on. Existing tournaments, imports without the code and team round robins keep the plain Berger order.
  • Fix OpenResults: teams are published under their full name. The results site and its hall screen printed the short name whenever a team had one; the short name now stays in OpenPairings, where it labels the team tie-break working on the Standings page.
  • Fix History page: opening the changes under a restore point crashed the page (it had to reconnect) when one of them was a change to the FIDE tournament ID ranges (Settings > FIDE). The ranges are a list of entries, and the diff printed the list by converting each entry to text, which an entry cannot be. It now reads like 498788: rounds 1-2, and any other list of structured values (officials, category rules) prints too instead of crashing; an emptied list reads "not set". The Audit page already coped with these values.

[0.73.0] - 2026-10-03

  • Fix Baku acceleration with a late entrant: Group A (the players who get virtual points) is now the top half of the starting list as it stood when round 1 was paired, for the whole event, as FIDE C.04.7 says. It used to be recounted every round over everybody with a pairing number, so a player who joined later made Group A bigger part-way through - 36 players in round 1 have a Group A of 18, and a 37th joining in round 4 made it 20, the two extra players even getting virtual points for rounds already played. Every TRF exported afterwards wrote that bigger Group A into every round, so a pairing checker replaying rounds 1 to 3 found pairings the file could not explain. Only Baku tournaments that took a late entrant were affected. For one that is running: rounds already paired never change; the next round, and every export from now on, uses the round-1 Group A - including a re-export of rounds already sent. Group A also travels with a TRF26 import (its 250 records), a JSON backup or restore and a hand-off. Unpairing round 1 lets the next round 1 decide it again.
  • Feature Team events print more: a cross table, match result sheets, rosters and board prizes. The Print page of a team round robin or team Swiss now also offers a Team cross table (a round robin as a team-by-team grid of game points with MP and GP; a Swiss as one row per team with, per round, the opponent's number, the colour of board 1, the match score and the running match points), Match result sheets (one A4 page per match: both line-ups with colours and ratings, result boxes, the match score, and signature lines for the captains and the arbiter, filled in where results are entered), Team rosters (each team's board order with ratings, FIDE IDs and federations) and Board prizes (per board number, ranked by percentage, points and performance). The same four are tabs beside Standings (Cross table, Match sheets, Rosters, Board prizes), each with its own Print button. Nothing is scored differently: the numbers come from the team standings.
  • Fix New tournament: ticking "Team tournament" with Swiss no longer says "Reporting only ... still paired player by player". That note dated from before 0.62.0; Swiss plus Team has paired team against team by FIDE's team Swiss (C.04.6) since then, and the hint now says so. Keizer, which has no team system, is refused for a new team tournament (below).
  • Feature Team events: line-ups per round, entered after the pairing is published. Each match's page (the "Line-ups" link in the Pairings page's match list) has one select per board for each team, starting from the roster minus anyone unavailable that round. Players can be left out and reserves moved up, but a line-up keeps the roster's board order (the FIDE team events' fixed board order). Saving rewrites the match's boards; it is allowed until the match's first result, and recorded in the audit trail.
  • Change Team events in FIDE mode: once round 1 is paired the rosters and board orders are fixed - nobody moves up or down, changes team or leaves a team they played for. A new player can still be added at the bottom of a team, as a reserve. Outside FIDE mode every change is still allowed, with a warning.
  • Fix TRF: a player moved to another team after playing (outside FIDE mode) was listed in the 310 record under their new team, also for the rounds they played for the old one. The move is now recorded, and the report lists the player under the team they played for.
  • Feature Team events: a board-colours setting (Teams, Matches). FIDE's rule - the team named first has White on the odd boards - stays the default; it is now cited from the Olympiad 2026 regulations instead of memory. "League" makes the first-named team the home team, and home and away can be swapped on a match's page before it starts. Existing tournaments keep exactly their colours.
  • Feature Team events: withdraw a team in one action (Teams page): all its players are withdrawn, and its matches already paired for later rounds are forfeited to the opponents. Reinstate undoes it. A team round robin can take a team that played fewer than half its matches out of the team standings (Settings, Scoring; FIDE General Regulations for Competitions 6.6); off by default.
  • Feature Team events: a double forfeit ("Neither team" in the match list): both teams lose the match by forfeit. Written to the TRF as a 330 record of type --, and read back from one on import.
  • Feature Team tie-breaks: Buchholz cuts and median, Buchholz on game points, the extended Sonneborn-Bergers on game points (EGMSB, EGGSB), Extended Direct Encounter, Top Board Results, Bottom Board Elimination and SSSC (C.07 Articles 12-13). The FIDE default set is unchanged.
  • Feature Team Swiss: the pairing-allocated bye's match and game points can be set (Settings, Scoring) when the regulations say other than a draw (C.04.6 Art. 1.4). Written to the TRF (362 P, 320) and read back on import.
  • Change New tournament: a team tournament can no longer be Keizer, which has no team system and paired it player by player. Existing ones keep working.
  • Fix Team standings counted game points from rounds not yet played: a team round robin's "Pair all" writes the forfeit on a short team's empty seat in every round at once. Standings, board statistics, the TRF and OpenResults now count a round once it is the current round or earlier.
  • Fix TRF of a team event: the 202 tie-breaks are written in C.07's spelling (MPTS, GPTS, BC, SB:MP ...) so a checker such as ainalrami -c can check the team standings; and the 310 match and game points are those of the file's own rounds (a file from round 1: the standings after its last round, with the rank; a round sent on its own: what that round earned, no rank), not the whole event's.
  • Fix TRF: the initial colour drawn by lot is now written (152, or XXC in the engine file) whenever one is on record - for individual events too. Apart from that new line, individual reports are unchanged.
  • Change Publishing a team event to OpenResults adds the boards per match, each board's place in its match and a double-forfeit flag (additive).
  • Feature Settings, Export: the TRF section announces a "Rating validator (YAML)" export, marked Soon. The button is a placeholder and does nothing yet.

[0.72.1] - 2026-10-02

  • Fix Imports were stuck at 0% since 0.70.0 - every upload: JSON backup, SWAR, TRF, hand-off, results CSV, logo, tools - fixed. Since 0.70.0 every upload box - JSON backup, SWAR, TRF, hand-off, hand-off return, results CSV, tournament logo and the arbiter tools' file parser - kept its button disabled until the file had arrived, but the file only started to travel when that button was pressed, so the box showed "0%" forever and nothing ever reached the server. Files now upload as soon as they are chosen, and the button waits only for a file that is really on its way.
  • Fix A refused file says why. A file of the wrong type or over the size limit showed a stuck "0%" and a disabled button with no reason; it now shows the reason in words beside the box (for example Only .json files are accepted here.), leaves the button pressable, and closing the dialog forgets the file.
  • Fix An import that fails on the server shows an error in the dialog instead of silently reloading the page. A backup whose players (or rounds, teams, ...) list holds something other than an object used to crash the page; it is now refused with the entry it is in, and a refused field names its record (Could not import player entry 2 (id 2): birth_date is invalid). The minimum a hand-written version 1 backup needs is now documented in docs/import-export.md.
  • Fix The Advanced and Settings menus work on a phone - they sat inside the sideways-scrolling tab strip with a fixed-position panel, which touch browsers clip or misplace, and tapping the page did not close them on iOS. The panel now hangs from the bar itself and scrolls inside when long, a tap anywhere outside closes it (also on the round menus on the Pairings page), and choosing an item closes it.

[0.72.0] - 2026-10-02

Arbiters, read this before upgrading. Four fixes below change how the next round is paired in some tournaments. None of them changes a saved round: boards, colours, results and standings already on record stay as they are, and nothing re-pairs a round by itself.

  • Baku acceleration - affected: Swiss tournaments with Baku on, rounds 2 up to the last accelerated round. A round paired before the upgrade will not match a FIDE checker replaying the report.
  • Pairing numbers as starting ranks - affected: any Swiss tournament from round 2 on with a late entrant, or with two players who have not played yet meeting on one board.
  • Unplayed rounds written with the right letter - affected: tournaments that pay points for an absence or for a loss.
  • Rounds before joining no longer losses - affected: tournaments where a loss pays points, with a late entrant or a withdrawal.

In an affected tournament, a round that is paired but not yet played can be deleted and paired again to get the corrected pairing - it may come out different from the one on the wall. For the last two, the FIDE/TRF export of an affected tournament changes as their entries describe. The details of each are in the first four entries.

  • Fix Baku acceleration: accelerated rounds are paired in the right order within each score group. The engine was handed the players ordered by game points instead of game points plus the round's virtual points (FIDE C.04.7), and it takes that order as the ranking inside a score group. So whenever a Group-A player and a Group-B player ended up on the same pairing score with different game points, the Group-B player was treated as the higher-ranked one, and the group's halves, floaters and colours could come out different from what FIDE's rules give. Affected: Swiss tournaments with Baku acceleration on, rounds 2 up to the last accelerated round (the first half of the rounds, rounded up); round 1 and every round after the accelerated ones were right, and tournaments without Baku (extra points included) were not affected. Both engines were affected. Checked on 60 random Baku tournaments: 124 of their 396 rounds were paired differently from bbpPairings and Ainalrami given the same games and virtual points; with the fix, 0 of 396, and 0 of 884 rounds in 140 more. Rounds already paired are not changed - nothing re-pairs a saved round, and the next round is paired from the games as they were played. In a running tournament that is still in its accelerated rounds, the next round is paired correctly; a round paired but not yet played can be deleted and paired again to get the correct pairing (it may come out different). A round paired before the upgrade will not match a FIDE checker replaying the tournament report, and its "what if?" and rebuilt explanations now judge it against the correct order, so they may say the engine would have paired it differently. Virtual points still never count in the standings, the tie-breaks or the scores of any export (the tournament report carries them only as its acceleration records).
  • Fix The pairing engine is told each player's pairing number as their starting rank. Since 0.18.0 the file "Pair round" hands the engine numbered the players by the current standings (score, then rating) instead of by pairing number, which the Dutch rules order a score group by and take 5.2.5's colour parity from. Two things came out wrong. A late entrant - numbered after the field - was placed in their score group by rating, so the boards and the bye around them could differ (found on rounds 2 and 3 of test tournaments). And on a board of two players with no game yet (both had a bye or an absence, or joined late), the colours could be the other way round, in any Swiss tournament from round 2 on. Baku acceleration stays fixed: Group A is the top pairing numbers and its virtual points are in the file. After upgrading: no saved round changes - rounds already paired keep their boards and colours. From the next round on the pairing follows the pairing numbers, so a round deleted and paired again can come out differently, and "why not me" on an old round is now judged on the pairing numbers.
  • Fix An unplayed round is written with the letter for what it is worth. Since 0.18.0 every unplayed round went to the engine as Z while the engine was told a Z is worth the absence value: a round before a player joined or after they withdrew, an absence paying half a point or a full one and a capped absence were all one letter at one value. The file contradicted its own score column, and the engine could give the pairing-allocated bye to the wrong player (it read a late entrant on zero as having scored a point without playing). Now an absence worth a draw is H, one worth a win is F, and a round worth nothing - before joining, after withdrawing, an unpaid absence - is Z worth 0, as in TRF. Only tournaments that pay points for an absence (abs_value, e.g. "absent = ½") or for a loss were affected; standings and tie-breaks were always right. After upgrading: saved rounds do not change. The FIDE/TRF export now writes such absences as H or F and declares a zero-point bye worth 0 (it was the absence value), and pre-announced absences in the export follow the same rule. An absence paying a value that is neither a draw's nor a win's (half a point in a 3-1-0 event, a 3-2-1 presence point) still has no letter of its own and is written Z; the score column stays exact.
  • Fix With a loss worth points, the rounds before a player joined no longer count as losses. In a tournament where a loss pays something (3-2-1 and similar point systems), a late entrant's rounds before joining - and a withdrawn player's rounds after leaving - were scored as losses in the file sent to the engine and in the TRF export's score column, while the standings rightly gave 0. A late entrant was paired in a score group above their own (a player joining in round 3 entered it with two points the standings did not give them). They now score 0, or what the late-entry rule awards when "rounds before joining count as absences" is on (late_entry_absences, unchanged). After upgrading: saved rounds do not change; standings do not change; the TRF export's totals for those players drop to what the standings show.
  • Performance Ainalrami 0.36.0: the same pairings, its matcher and its file writer faster. The weighted matcher a hard bracket falls back to is 1.24-1.32x faster per call, and the TRF writer about five times faster on large fields; every call and every file is held identical to 0.35.0's. It also checks a pairing handed to its explanation calls (the fix above about boards that are not a round). Checked before release against Gacrux and bbpPairings on about 11.6 million rounds; every disagreement judged against the rules found this engine's pairing legal (Ainalrami's docs/validation.md, "Release check for 0.36.0").
  • Feature One-tap results on a phone. On a phone or any touch screen, every board on the Pairings page carries three large buttons - 1-0, ½-½, 0-1 - above its result select, the result on file shown pressed. A tap writes the result in place, through the same checks as the select (clearing still asks first, a postponed game's non-draw still asks first); tapping the pressed one again writes nothing and logs nothing. The select stays for forfeits and the rest. A desk with a mouse sees no change.
  • Change A board is a card on a phone held upright. Below 640px the pairing list shows each board as its number, White and Black on their own lines with a light or dark square, and the result buttons across the width - the four-column table put the names and the result off the screen. The standings keep the rank and name pinned while the other columns scroll sideways.
  • Fix Swapping, marking absent and awarding a bye work on an iPhone. The seat menu opened only on a right-click or a long press, and iOS sends neither, so on an iPhone or iPad there was no way to reach it. On a touch screen a tap on a seat now opens its menu; a mouse click is unchanged.
  • Fix Typing in a field no longer zooms the page on an iPhone. Every text field and dropdown was 15px, under the 16px below which iOS zooms in on focus and stays zoomed; on a touch screen they are 16px.
  • Change Touch targets of at least 44px on a touch screen: buttons (the registrations queue's Discard was about 30px, the standings' move buttons about 26px), menu items, the top bar's menus and pickers, and selects.
  • Fix Wide tables scroll instead of being clipped on a landscape phone and in the next-round preview. A table card only scrolled below 768px, and a landscape phone is wider than that; the preview's tables were in plain cards that never scrolled. The page body hides sideways overflow, so the far columns were simply not there.
  • Change Notches and rounded corners: the page and the top bar keep clear of a phone's safe area (viewport-fit=cover). Zoom is never disabled.
  • Feature A sent receipt for every round sent for rating. Each "Send…" now stores a receipt: a fingerprint of exactly what went out (every game's identity, both players, the colours, the result as the file wrote it, the round, and the file's own bytes), who sent it and when, and a short code to quote - R5·7F2A for round 5, P·9C01 for a postponed-games file. The round shows it on the Pairings page ("Sent 03-10-2026 14:02 UTC · R5·7F2A") and on Settings, Export; the audit trail names it; and the file sent carries it in a ### comment line, which TRF readers skip - no record of the file is added or changed. Every copy says whose copy it is ("copy of R5-7F2A, not for rating") or that the round was never sent. Backups, imports and hand-offs carry the receipts like the record of sent games. It only shows: whether a game may be sent is still decided by that record and the locked send, as in 0.71.0.
  • Feature "Changed since sent": a round that no longer matches what went to the rating officer is shown in red. After a result correction, a postponed game played but not yet in a postponed-games file, a player's FIDE ID or name changed, colours swapped, or a game removed or added, the round on the Pairings page and on Settings, Export reads "Changed since sent (R5·7F2A) — the rating body has the old version" and lists each change. Nothing is ever sent again on its own. Rounds sent before this version get a receipt marked "sent before receipts" - no code, because the file was not kept - and their changes are still detected from the record of sent games. See docs/import-export.md, "The sent receipt".
  • Change The results snapshot now carries the point system and the points of every game. tournament.scoring holds the win, draw, loss, bye and forfeit values and the presence point; each board carries points (what each seat scored, as the standings score it) and a postponed board also postponed_as (how each seat's game counts until it is played). The results site used to read points off the result token as 1, a half and 0, which disagreed with this app under a 3-1-0 system, a presence point or a postponed game valued as a win or a loss. Old readers ignore the new keys.
  • Fix A TRF file with one unreadable round no longer loses the warnings of the others. The import checks each round of a Dutch-system file against the FIDE pairing rules. A round whose game entries do not add up to a pairing (Alpha's line says it played Bravo, Bravo's says Charlie, so Bravo is seated twice) is now reported by itself - "round 1 could not be checked against the FIDE pairing rules" - and every other round is still checked. Before, an engine that refuses such a pairing took the whole pass down with it, so a rematch in round 2 went unreported because round 1 was malformed. A player whose line simply ends before a round is treated as not playing it, rather than as a board missing from it.
  • Fix Nothing is asked of the engine about boards that are not a round. Ainalrami's explanation calls now refuse a pairing that leaves a player out, names one twice or names one the round does not have (they used to read a missing player as a bye). So the explanation page's "what if" and "no-show" questions only accept players seated in the round (a rostered player whose board was removed by hand used to crash the page), and a hand-edited round whose boards are no longer a complete pairing has no account worked out for it. A round's account asked for in the instant the round finishes is dropped quietly instead of logging a failure. Team Swiss rounds, decided team against team, were never given an individual account and still are not.
  • Change In FIDE mode, the event's terms are fixed once the first round is paired. The number of rounds (a round robin's cycles), the scoring, the pairing-allocated bye's value, the acceleration, the pairing system and the tie-break list can no longer be changed, and there is no Unlock for them: FIDE's draft checklist for pairing programs (VCL4THP) fails a program that allows it. The Settings pages show them disabled and say why. A tournament that has left FIDE mode keeps the old round-1 lock with its Unlock.
  • Change In FIDE mode, only the last two rounds played can be corrected (C.04.2:4.3): with round 7 played, rounds 6 and 7 and any round in progress. A result, a hand edit or a team forfeit decision in an earlier round is refused and says why, and a player who played in such a round is withdrawn rather than deleted. The result of a postponed game can still be entered in any round.
  • Change Scores no game can give take a tournament out of FIDE mode: two draws worth more than a win and a loss, a bye worth more than a win, or, with 1-½-0 scoring, a bye of anything but 1, ½ or 0. 3-1-0, a bye worth a draw or nothing, and a bye of 2 under 3-1-0 stay in. The Scoring page names the setting, as it does for the others.
  • Feature Leave FIDE mode, on purpose. Settings, FIDE has a "Leave FIDE mode" button behind two confirmations, the second listing what it costs: it is for good, the report says from which round, and the locked settings and earlier rounds open up. It is the way to change a setting FIDE mode holds.
  • Feature The FIDE report says when a tournament left FIDE mode. The TRF26 export carries a ### FIDE mode exited @ Round N comment line, or "before Round 1 was paired", whichever way it left.
  • Feature Every page of a tournament not in FIDE mode says so, in one quiet line linking to Settings, FIDE.
  • Fix Standings with games still without a result say they are not final, on the page and on paper, as they already did for an open postponed game; before, "Standings after round N" printed with no warning while boards were blank.
  • Fix The FIDE report's rank column is the player's place. The TRF26 export wrote the starting rank in the 001 record's rank column (86-89) as well, so the pairing checker FIDE's testers run (ainalrami -c) read nearly every place as one the tie-breaks do not give. It now carries the place in the standings after the file's last round. A file of chosen rounds that does not start at round 1 keeps the old column.
  • Fix Tie-breaks in the FIDE report are in C.07's spelling: 202 says BH/C1, BH/C2, BH/M1 and ARO/C1, not this app's BHC1, BHC2, MBH and AROC1, which a checker could not read - so the standings of most files could not be checked at all. Both spellings are read on import.
  • Fix The KBSB player search on the FIDE page said "matricule" in English; it says "national ID".
  • Verified The VCL4THP tracker, second pass. Every answer on the path checked against the code, with file and line in the notes: mix pairings.vcl, docs/vcl4thp-tracker.md.
  • Performance "Pair round" comes back sooner on large fields, and the page is free again the moment it does. On one core (the two-core server busy elsewhere), a 1,000-player round 9 went from about 2.7 seconds of server work before the boards appeared, and 0.7 seconds more after, to about 1.1 seconds and nothing after; a 200-player round 9 from 0.6 seconds to 0.2. Where the time went: every reader of a round decoded its stored engine account - the largest thing in the database - although only the explanation page looks at it (the standings, the pairing history, the backup taken before pairing, the round's publish control, which ran on every render); the boards were written one row and one label update at a time; each player's history was found by scanning every board of every round; the page then reloaded everything a second time on its own pairing's broadcast; and the result menus sent the same sixteen labels again for every board. All of that is gone. Nothing the click writes or shows changed: a test pairs three tournaments through the page and compares the boards, labels, byes, standings, TRF, backup, public snapshot, audit trail and board list with what 0.71.0 produced, field for field. Storing a round's explanation after the click no longer counts as a change to the tournament's data, so it no longer throws away the standings just worked out. PAIR_TIMING=1 logs how long each stage of a click took (see docs/deployment.md).

[0.71.0] - 2026-10-01

  • Feature Bye preferences: "must get", "rather gets" and "rather not" the pairing-allocated bye, per player, for every round or chosen rounds (the fourth, "must not get it", is the existing bye exclusion). Behind its own switch, "Bye preferences", under the account's features (off by default, in no federation's pack). An organiser's wish, not a FIDE rule, so every time one is chosen the player form says so. "Must get it" gives the player the bye whenever the round has one and a legal pairing allows it - otherwise the round is paired normally and the Pairings page says why. The two soft ones decide who on the score that gets the bye takes it, above the quality criteria and below everything FIDE ranks higher: they never lift the bye to a higher score or leave a round unpairable. Wanting the bye where the player is excluded from it is refused with the rounds named. A second pairing-allocated bye (FIDE rule C2) is never given: "must get it" for rounds after the player's bye is refused when saved, and a round a stored one would apply to is not paired - the Pairings page names the player and the round of their earlier bye. In "why the bye went to X", a player a preference kept from the bye reads "bye preference", not "organiser exclusion". A round they change is explained on its explanation page and under the boards, recorded in the audit trail and on the tournament's FIDE record, and listed on the Export page's TRF section; a round they did not change records nothing. Swiss with Ainalrami only. See docs/pairing-systems.md, "Bye preferences".
  • Feature Not on a FIDE-rated tournament. A FIDE-homologated tournament does not offer bye preferences; one that becomes FIDE-rated keeps the stored ones but ignores them when pairing, and the Players and Pairings pages say whose are being ignored. The bye exclusion is unchanged.
  • Performance Ainalrami 0.35.0: the slow rounds on large fields are gone. Rounds of big opens that took seconds - a 600-player round 2 took 2.9 s, a 1,001-player round 2 over 30 s - now pair in about a tenth of a second or less, and so do the rare slow last rounds: the 30 slowest of 3,000 sampled positions went from 22.5 s in all to 4.2 s on one core (the worst from 5.2 s to 1.0 s). A median round of 100-1,000 players pairs in under 50 ms on one core. Every pairing is identical to 0.34.0's on 447,152 compared rounds. It also carries the engine side of the bye preferences above.
  • Feature Next-round preview: which boards are already certain while the last games are still being played. With six or fewer games of a Swiss round still open, the Pairings page offers "Preview next round". It pairs the next round for every possible result of those games (1-0, ½-½ and 0-1 each - 729 outcomes for six) exactly as "Pair round" will, with the same requested byes, withdrawals, late entrants, forbidden pairs, acceleration, bye exclusions and preferences, soft rules and board numbering, and saves nothing. It then shows the boards that come out the same in every outcome (same players, same colours, same board: their name cards can go out), the pairs that are certain but may still move board or swap colours, and for everybody else who they could meet and which open games decide it, and whether the bye is decided. It runs in the background with a progress bar ("Paired 212 of 729 variants", and the time left), works itself out again a moment after a result is entered, and "Print fixed boards" prints them with a list by name for the name cards. Above six open games it says how many are still open, and with JaVaFo that the preview needs the built-in engine. The round itself is paired as usual once the last result is in. The field the engine reads is built once and only re-ranked per outcome, so the engine is most of the work: six open games on 600 players take about 14 s on one core with Ainalrami 0.35.0 (the server uses one), 6 s on four.
  • Feature Hall display settings and an announcement for the screen in the playing hall. Settings -> OpenResults has a new "Hall display" card for the results site's full-screen page for a TV or projector: choose which views it cycles through (the round's pairings, a list to find your board by name, the results as they come in, the top of the standings), how many seconds each page stays up and how many places of the standings it shows, and whether it holds on the pairings until the first result of a new round is in. Type an announcement ("Round 5 starts at 14:00", up to 500 characters, line breaks kept) and press Save: it reaches the hall screen with the publish that follows at once. Once the tournament is published the card shows the hall display's address and an "Open the hall display" button. These only change what the hall screen cycles through - what spectators may see is still decided by "What the public page shows" and each round's level. The settings travel in backups and restore points.
  • Feature Entries from the results site wait on the Players page. While the entry form is open, or anything is still waiting, the Players page shows a card with every new entry - within a minute of it being sent - and Accept and Discard on each, so the registration desk no longer has to go looking under Settings. Each entry is shown as the player it would become: the rating, title and federation from the FIDE list when the entry's FIDE ID is on it (with a note when the entry claimed a different rating, or when the list has that FIDE ID under another name), and - with the Belgian player lookup on - the member number, national rating and club from the KBSB list. Accept creates exactly that player, marked not yet arrived. The Registrations page shows the same queue.
  • Feature Possible duplicates are flagged before you accept. An entry with the same FIDE ID, national ID or name (ignoring order, case and accents) as a player already entered, or the same email address as another waiting entry, says so on the entry. A duplicate FIDE ID still cannot be accepted; the rest is for you to judge.
  • Feature The entry form can open and close on time, and cap the field. Settings > Results site > Entry form takes an opening time, a closing time (both in your computer's time zone) and a maximum number of players; the results site applies them by its own clock, even while this computer is off, and counts the entries waiting for you towards the maximum. A switch lets the form list who has entered - your entry list, with the columns the public page shows; undecided entries only as a count and email addresses never. None of these opens the form; "Open it" still does. Changes are in the Audit log.
  • Feature National ID on entries. The results site's entry form asks for the member number at the player's own federation, and its FIDE search now also finds Belgian club players who are not on the FIDE list, filling in their member number and club from this computer's KBSB list.
  • Change The entry form's settings are not carried by an export, like "Open it" itself: a copy of a tournament starts with its entry form closed and without last edition's dates.
  • Fix Two "Send…" clicks for the same round can no longer both send it. The check "already sent?" and the marking were two steps, so a double click, a second tab or a co-arbiter could each get a file for the same round. Sending now builds the file and records every game in one write transaction, and the database keeps one record per game sent: the second request gets "Not sent, and no file". The same holds for the postponed-games file. Records already in the database are kept as they are; a game recorded as sent twice before this release stays on record (the upgrade logs it) and keeps blocking.
  • Fix A copy handed off to another machine can no longer send. "Send…" now refuses on a locked (handed-off or archived) copy, for rounds and for the postponed-games file. And a game sent as ? from one copy and with its result from the other is never offered for the postponed-games file afterwards: that would have been its third send.
  • Fix A restore no longer makes a sent game sendable again after a player's FIDE ID or name changed. Each game now keeps an identity of its own through restores, backups and hand-offs, and the record of what was sent follows it. The restore warning no longer says a sent game would be taken away when it is still there, and a game sent as ? keeps its ? - so it is still offered for the postponed-games file once played.
  • Fix A round restored to before it was paired, and paired again with other games, can be sent. It used to count as sent forever, so its new games could never be reported. Sending it now warns first that the round goes to the rating officer a second time; paired again into the very same games, it still counts as sent.
  • Fix A copy of a tournament remembers what was sent. The JSON backup now carries the record of sent games, and an import keeps it. A tournament imported from a backup, TRF or .swar file of an event that may already have been reported (a FIDE tournament ID, something sent, or results in a TRF/.swar) sends nothing until an arbiter confirms on Settings, Export that this copy is the one that reports.
  • Fix Correcting the result of a late game no longer moves the date it was played to today. The date - which decides the FIDE rating period - is set when the game is first played and kept; changing it is the separate "Save date" on Settings, Export, now recorded in the audit trail.
  • Change Copies are named and marked as copies. "Download a copy" and "All rounds" end in COPY-NOT-FOR-RATING (KOPIE-NIET-VOOR-RATING in Dutch) and say so in a ### comment line inside, which TRF readers skip; only "Send…" hands out the file for rating. The older TRF spelling (?dialect=javafo) refuses a round with an open postponed game instead of writing it as a draw.
  • Change The postponed-games file is reported as a tournament of its own. Late games are reported to FIDE as a separate tournament, so the file has its own name (by default the event's name + "postponed games" / "uitgestelde partijen", editable), its own FIDE tournament ID, start and end dates from the days its games were played, only those games, and one file per FIDE rating period (month). The main round reports still write an open postponed game as ?; docs/import-export.md documents why that is assumed not to be rated.

[0.70.0] - 2026-09-28

  • Security Mint 1.11.0. The HTTP client underneath the app's outbound requests (FIDE, the rating lists, publishing) had three advisories (EEF-CVE-2026-91043, -92103, -94194); 1.11.0 fixes them.

  • Performance Ainalrami 0.34.0: large rounds pair 2.5-2.8x faster on a two-core server, with output identical to 0.33.0 on 445,172 compared rounds.

  • Performance A round's explanation is ready within a second of pairing; each "why him and not me" is worked out when you open it. The explanation page's alternatives - why this player floated and not another, why the bye went where it went - each cost one full re-pairing of the round per candidate, and were all worked out after every pairing although most are never read. Now only the brackets and their criteria are worked out after pairing (on a 450-player event, well under half a second), and each question is a button: open it and the page says "Working it out…" until the answer is in - a few seconds per candidate on a large field - then keeps it, so it is worked out once per round and shows at once for anybody who opens it later, including on other screens already open. If it cannot be worked out the page says so and offers "Try again". An answer belongs to the pairing it was worked out for: unpair and pair again and the questions start afresh. Nothing that decides or records the round moved: the checks whether a bye exclusion or an "only if possible" wish changed it still run in the click. Rounds paired before this keep the alternatives they stored.

  • Change "Why did he float and not me" for one player is now Ainalrami's own answer (Alternatives.float_alternative/5) instead of a copy of the engine's steps kept in this app; the answers are the same, and the candidates of one question are now worked out side by side on a machine with more than one core.

  • Change Settings → OpenResults: three sliders instead of buttons, a select and a Save button. They are built like the "Spectators see:" control on the Pairings page, and everything saves as it is chosen. On the results site: Off · Link only · Listed replaces the "Published" and "Listed on the front page" buttons: Link only publishes the tournament at its own address, Listed also puts it on the results site's front page. Going down to Off asks first; everything turning publishing on or off did before still happens (the consent question on a public results site, the first copy queued, the audit rows), and a copy already on the site still stays there until it is removed under "The address". Automatically: By hand · Pairings once paired · + results live · + standings when the round is finished replaces "Publish each round": the app moves each round up the per-round ladder by itself, as far as chosen - a round's pairings once it is paired (after an optional "after N minutes", inline), its results as they are entered, and the standings after it once it and every round before it are finished.

  • Change A round can always be taken down by hand, and it stays down. The old "Immediately" mode locked the per-round control; the automation never does. A level chosen below what the automation would give the round is kept for that round, so the automation does not push it back up when the next result comes in. Turning the automation down keeps every round at the level it has; turning it up (asked first when rounds are already public) applies to the rounds already public too.

  • Change The starting ranking is a setting, off by default. "Before round 1, spectators see the starting ranking" on Settings → OpenResults replaces the "Initial standings" switch on the Standings page, which now says what it is set to and links there. A new tournament shows spectators nothing until its first round is public; a tournament that had published its initial standings keeps them on.

  • Change "Standings" and "Round pairings" are gone from "What the public page shows". Whether a round's pairings and the standings after it are public is that round's level now. Every other box stays.

  • Change How existing tournaments are carried over. Nothing anybody could see changes on the upgrade. "Manually" becomes By hand; "After a delay" becomes Pairings once paired, with its delay; "Immediately" becomes + standings when the round is finished - its rounds, results and finished standings, which it showed without ever writing down, are written down as public; "On the round's own date" becomes By hand, and rounds already paired still go public on their date. A tournament that had the "Standings" page switched off gets an automation without the standings step, one that had "Round pairings" off gets By hand, and that page stays hidden until the arbiter shows it again from a note on the settings page. An account's default for new tournaments, and a backup file written before this version, are converted the same way.

  • Performance Standings are worked out once per change, not once per page. Every page that shows standings - Standings, Players, Pairings, the printed standings and crosstable, the public results page, the projector - used to replay every game of the tournament on its own, and again for every arbiter and every refresh. They now share one replay until something that can change the standings is written: a result, a bye, a player, a round paired or unpaired, an import or a restore. The replay itself is unchanged and still never trusts a stored total; the database marks every such write, whichever part of the program made it, so a page can never be shown a table from before the change. A tie-break or scoring setting is part of what is remembered, so changing one shows the new table at once.

  • Performance "Pair round" shows the round as soon as the engine has paired it. The engine's explanation of the round - the brackets, and for every float and the bye what each other candidate would have cost - used to be worked out inside the click, one full re-pairing per candidate: on a 450-player event on the two-core server that was about four of the click's four and a quarter minutes. It is now worked out afterwards, in the background and at low priority, and the explanation page says "Working out the explanation…" until it arrives, then fills in by itself; the Pairings page shows the same note beside the round's status meanwhile. Everything that decides or records the round stays in the click: the pairing, the check whether a bye exclusion or an "only if possible" wish moved it (and so the "no longer FIDE-compliant" mark and its audit rows), the audit entry. If working the explanation out fails, the page says so and offers "Try again"; if the server restarted meanwhile, it is worked out again when somebody opens the page. Unpairing the round stops it. Explanations of rounds paired before this read exactly as they did.

  • Change The Pairings page has two rows above the table. The tournament's name, then one bar for the round: the round buttons and the round's status ("playing", "finished", ...) on the left, and on the right what spectators see, Print and More - or the "Pair round" button while the round is not paired yet. On a narrow window the right-hand group moves onto its own row. The "Pairings & results" subtitle and the separate "Round N" heading are gone (the selected round button says it; screen readers still get the heading). "Local view & phone QR" and "Public page" moved into More, which is there even before a round is paired. A missing setup item is now one line with each item linked to where it is filled in, and "Initial colour: drawn by lot" is a small note under round 1's table only.

  • Change The round's publishing control now reads "Spectators see: Nothing · Pairings · Results · Standings". It said "Public" and "Hidden" before (0.69.1). Same four levels, same behaviour; the label now says who it is about, with "What spectators see on the results site" on hover. The copy in the round's right-click menu is headed "Round N: what spectators see".

  • Change One place to choose what spectators see of a round. The Standings page no longer has its own "Standings after round N" switch, which did the same as the Standings level on the Pairings page. Instead it says what spectators see of the round the standings are after - for instance "Round 3 · Spectators see: pairings, results and standings" - with a "Change on the Pairings page" link that opens that round. The "Initial standings" switch stays on the Standings page until round 1 is finished, since no round's level covers the entry list.

  • Change The version is back on the top bar. It had moved into the account menu; it now sits, small and grey, after the account menu (or after "Log in"), linking to the changelog. Below 1400px wide it shows the release alone ("v0.69.1"), with the full build in the tooltip, and the bar still fits on one row from 769px up. On the phone layout it is in the account menu, as before.

  • Change A new account page. Account → Settings is one calm page with a card per section and a list of sections beside it (a row of buttons on a phone): Profile, Sign-in & security, Preferences, New tournaments, Federation features, Your data and Delete account. The Federation features page is now a section of it; its old address opens the page there.

  • Change Only the risky parts ask you to sign in again. The account page used to ask for your password again before showing anything if you had signed in more than ten minutes ago. Now it always opens, and only changing your address or password, signing other browsers out, downloading everything and deleting the account show a "Confirm it's you" button until you have signed in within the last twenty minutes.

  • Feature Display name. Set one under Profile and it is shown instead of your email address in the audit log, the history, the list of people a tournament is shared with, invitations and the invitation email (with the address beside it or on hover, since two people can share a name).

  • Feature See where you are signed in, and sign out other browsers. Sign-in & security lists every browser signed in to your account ("Safari on iPhone, signed in 3 days ago"), marks this one, and signs out any other one, or all of them at once. Addresses on the 02cloud domain see that 02cloud manages their address and password instead of the two forms. Changing your address is now limited to five confirmation emails an hour.

  • Feature Language, theme and accent can follow you. The language you pick is kept on your account and used on every computer you sign in on. A theme or accent chosen on the account page is used everywhere too, and the top-bar pickers then change it everywhere; "This device decides", the default, keeps each browser's own choice as before.

  • Feature Defaults for new tournaments. Set the pairing system, rounds, format, rate of play, place, federation, organiser and how rounds are published once, and "New tournament" starts from them (and says so). Anything you change in the form applies to that tournament only; existing tournaments and imports are not affected.

  • Feature Download everything. Your data gives you one zip with your account settings and a backup of every tournament you can open, including archived ones and your recycle bin. Each backup is the same file a tournament's Export gives and can be imported the same way.

  • Feature Delete your account. Type your email address to confirm. It is refused while the account still owns tournaments (including ones in the recycle bin), because deleting it would delete them too, and for the only administrator. Tournaments shared with you stay with their owners, their audit logs record that you left, and what you changed in them stays attributed to your address.

  • Fix A SWAR file's empty rounds are no longer read as paid absences. Only SWAR's absence table makes an absence - it is the only one SWAR pays the absence points for and counts towards the number of absences paid. A round a player was not in (SWAR's "forfait" table for a withdrawn player, or the empty round OpenPairings writes for a round before a late entrant joined, when those do not count, or after a withdrawal) used to import as an absence. So exporting to SWAR and importing the file again gave a withdrawn player half a point for every round after he withdrew, and a late entrant half a point for every round before he joined, using up his paid absences with them. Those rounds now import as nothing, and a file written with "Rounds before a late entrant joins count as absences" off is imported with it off.

  • Fix A TRF of chosen rounds scores each absence as the standings do. When a tournament pays for only a limited number of absences, a TRF exported for, say, rounds 4-6 counted the absences from round 4 again, so an absence the standings no longer paid (the fourth, with three paid) was paid in the file. Every absence now counts the ones before it, including those in rounds the file leaves out.

  • Verified A late entrant's rounds before joining use up the paid absences first. With half a point per absence, three paid, a player who joins before round 4 has rounds 1-3 as his three paid absences, and an absence in round 4 pays nothing - in the standings, the cross table, the player card, the tie-breaks, the score the next round is paired on, the TRF report, the SWAR file and the results site alike, whether his join round was set or worked out, and however the round-4 absence was entered. A requested half-point bye is not an absence: it still pays half a point and uses none. Every case checked is listed in docs/swar-import.md.

  • Fix A delayed "Pairings once paired" publish now actually fires on its own. With the automation's pairings step set to a delay (an optional "after N minutes"), a round became due for OpenResults at that moment but nothing re-published it if nothing else happened to touch the tournament in the meantime - the post-pairing publish had already gone out and been sent (excluding the still-delayed round) long before the delay ran out, and the public page only caught up whenever some unrelated write next enqueued a publish. Pairing a round under a real delay now records its own due time (rounds.publish_due_at), and Publishing.Drain's existing 30-second timer and boot-time sweep pick it up and publish it once it arrives - reusing the same queue a manual "Publish now" uses, so a restart or deploy between pairing and the due time cannot lose it. Publishing the round early or hiding it again by hand cancels the wait.

  • Fix A backup's public-page settings (listed, which columns show, which tie-breaks are hidden) are read back on import and restore. public_listed, public_display and public_hidden_tiebreaks were written to every export but never applied when importing that file or restoring a tournament to an earlier point - an arbiter's public-page picks silently reset to "list it, show everything" on every restore, and an imported copy of a published tournament came back listable and fully shown regardless of what the original had chosen. A backup written before these settings existed keeps their ordinary defaults, exactly as it always has.

  • Fix Importing a file no longer crashes if Import is pressed a beat before the upload has actually arrived. Production hit this on an ordinary 20 KB SWAR file over Cloudflare: the "import_swar" submit reached the server while the entry was still uploading, and Phoenix's consume_uploaded_entries/3 raises on that rather than waiting, which took the whole page down to a reload. Every upload-consuming submit in the app (SWAR, TRF, JSON backup, hand-off, hand-off return, results CSV import, tournament logo, and the public arbiter tools' SWAR/TRF uploads) now checks first, keeps the dialog open with a calm "Still uploading" message instead of crashing, and finishes the import itself the moment the upload catches up - no second click needed. A file that failed on its own (too large, wrong type) is reported with its existing message rather than being called "still uploading". The Import/Upload/Parse button is also now disabled with a progress percentage while a file is arriving, so the race is unlikely to begin with.

[0.69.1] - 2026-09-28

  • Fix The top bar stays on one row on narrower windows. Between the phone layout and about 1200px wide, the account menu and the publishing status wrapped onto a second row. The bar now tightens its spacing there, shows the logo without the word, the publishing status as its coloured dot, and, just above the phone layout, a person icon for the account menu. Everything that is hidden still has its full name for screen readers, and the publishing panel still says everything.

  • Change One "Public" control per round instead of three switches. The Pairings page used to show a separate Pairings, Standings and Results switch for each round, which allowed combinations that make no sense on the results site (results public while the pairings are hidden) or leak (standings public while the results are "hidden" - the table already contains them). Each round now has one choice, and each step includes the ones before it: Hidden, Pairings, + Results, + Standings. Choosing a level publishes or withdraws exactly what is needed, in the right order, as one change. Going down still asks first and names everything that disappears, including later rounds that have to go with it. Standings cannot be chosen until the round and every round before it are finished, and the control says why. In "immediate" publish mode it shows everything public and locked, as before. The same control is in the round's right-click menu. Nothing stored changes on upgrade: a round left with an odd mix - for instance its results switched on while its pairings are hidden - shows the level it actually reaches and a short note, and the next level you choose tidies it up. The audit log records the same entries as before.

[0.69.0] - 2026-09-27

  • Change Rounds before a late entrant joins count as absences. In a tournament that pays points for a round sat out (Settings, Scoring), a player who joins in round 4 is now scored for rounds 1-3 exactly as if they had been registered and marked absent: the absence points, within the "last round" and "first N" limits, and those rounds use up the absences allowed. With half a point for the first three absences, a player added before round 4 has 1.5 points after round 3 and a later absence pays nothing. This is what SWAR does. It applies to tournaments already running too, including players added before this version, whose join round is worked out (below): their late entrants' scores, places and tie-breaks can move. A new setting beside the absence points turns it off; it is on for every tournament still being played, and off for one that is finished or archived (and for a finished one restored from a backup made before this version), so upgrading does not rewrite final standings - switch it on there if you want the new count. Tournaments without absence points - FIDE events - are unchanged: the rounds before joining still count as nothing. The standings, crosstable, player card, printed lists, the score the next round is paired on, the TRF report, the SWAR file and the results site all count these rounds the same way.

  • Feature "Joins in round" on the Players page. Adding a player after rounds were paired offers the round after the last one paired; the add form and the player dialog let you change it, and say what the rounds before it count as - "Rounds 1-3 count as absences: 1.5 points, no absences left." An absence typed in for a round before the player joins is said to be covered, or not counted, instead of silently doing nothing.

  • Feature The join round is worked out when it was never set. A player added before this version, or through an accepted registration, has no "Joins in round". For them it is read off their rounds: the first round they have anything in (a game, a bye, an absence), or - with nothing yet

    • the next round to be paired. Anyone with something in round 1 was there from the start and nothing changes for them; a withdrawn player who never played stays as they were. Nothing is saved: unpairing and pairing a round again gives the same answer. The player dialog shows it - "Joins in round 4 (worked out from their first game)" - and typing another round sets it; a round you set always wins. The pairing is unaffected: such a player is paired in the next round, as before.
  • Fix The SWAR file keeps absences the way SWAR does. A declared absence is written as SWAR's own absence record, so SWAR pays it the absence points and counts it towards the limit - it went out as a round worth nothing. A round before a player joined is no longer left out of the file, which made SWAR read that player's later rounds one round early.

  • Feature Two kinds of extra points: handicap and acceleration. Settings, Extra points now asks what the extra points are for. Handicap is what they always were here - a head start for players below a rating (bands like "1400:1, 1600:0.5"). Acceleration is what SWAR does with its XtraPoints - the stronger players get them (bands pay players at or above a rating, "1800:0.5, 2000:1") so they meet each other from round 1. The page explains each, with an example, before you save.

  • Change A counted handicap now pairs on the total. With "Count extra points" on, the pairing puts players in score groups by points plus extra points - the same total the standings rank on - so the leader on handicap meets the players chasing them. Before, the standings counted the handicap and the pairing ignored it. With counting off nothing changes. The FIDE TRF report still gives game points; the handicap stays in its 299 records.

  • Feature Acceleration points go to the pairing every round. They are handed to the pairing engine as virtual points (JaVaFo and Ainalrami both), round by round, as SWAR does. "Keep acceleration points in the final standings" decides the standings: on counts them, as SWAR does; off leaves game points only, like Baku. "Remove half a point" takes half a point off everyone in a rating range, as SWAR's "Remove" does - the next round is paired with what is left, and rounds already paired keep what they were paired with. Baku and extra points in the pairing cannot be on together.

  • Change SWAR files pair here as they paired in SWAR. A SWAR file imports in acceleration mode: its players' XtraPoints go to the pairing, each round keeps the extra points SWAR paired it with, and SWAR's band table becomes the bands. The .swar export writes all three back. The import no longer warns that further rounds will differ from SWAR's.

  • Change Existing tournaments. A tournament that came from SWAR and uses extra points (and has no bands set here) is now in acceleration mode, which is what SWAR did with them - from its next round on, its extra points are part of the pairing. Every other tournament is in handicap mode, as before; one that counts its extra points pairs on the total from its next round on.

  • Feature Extra points wherever the score is shown. Printed standings and cross tables show XtPts and Total when the standings rank on them, the standings sent to the results site carry both (it does not show them yet), and a round's pairing explanation says when it was paired with virtual points (extra points or Baku).

  • Change Extra points in the pairing are on the tournament's FIDE record. Pairing with a counted handicap or with acceleration points is not what the FIDE rules pair, so it is marked the way a bye exclusion that moves the bye is: the first round in which extra points actually reach the pairing - somebody in the round holds some - is recorded as the round the tournament stopped matching the FIDE rules, and the audit trail says so. With nobody holding extra points nothing is recorded, and Baku, FIDE's own acceleration, never is. The Extra points page says this before you save, and more strongly on a FIDE-homologated tournament.

  • Change So are "only if possible" wishes that move a board. A soft forbidden pairing or "keep clubmates apart" changes the Dutch system's own choice, even at the weakest setting, so the first round in which a wish changes who plays whom is recorded the same way; a wish the pairing already honoured records nothing. The round is paired a second time without the wishes to tell, only when there are any. The exclusion rules page says so beside the wishes.

  • Feature "No pairing-allocated bye" for chosen players - an organiser's rule, not a FIDE one. A new switch in the Belgian pack (Features page) adds "Exclude from the pairing-allocated bye" to a player's details: for all rounds, or for certain rounds typed exactly like absences. The Swiss engine then treats the player as one who already had the bye, and nothing else changes. Swiss with the Ainalrami engine only: with JaVaFo the form says it is not available, and round robin and Keizer do not show it. The form warns every time it is ticked that the pairings will differ from FIDE-endorsed programs and a FIDE checker cannot replay those rounds, and more strongly on a FIDE-homologated tournament. The players list marks such a player "no bye".

  • Feature When the exclusions leave no legal round, the Pairings page names the excluded players and offers "Pair anyway, ignoring the exclusion for X": the round is paired with that one player's exclusion lifted for this round only, and the audit trail records it.

  • Feature On record. The round's explanation says "X was passed over for the bye: organiser exclusion"; the audit trail records it; the first round in which an exclusion moves the bye is recorded as the round the tournament stopped matching the FIDE rules; and the Export page's TRF section lists those rounds, since the TRF cannot carry the exclusion. Backups, snapshots, hand-offs and Duplicate carry the setting.

  • Change Ainalrami 0.33.0. It adds the bye exclusions above; pairing without an exclusion is unchanged, byte for byte. Its TRF table now has FIDE's own FIDE_DUTCH_2025 code, which the TRF export writes (below).

  • Feature An agreed date for each postponed game, with its history. The list of open postponed games on the Pairings page has a date field per game: the date the two players agreed to play it on. It is optional and never a deadline - nothing becomes overdue. Every change is kept (old date → new date, when, and by whom) and shown under the game, and the audit trail records it too.

  • Feature A notice and a calendar file for the players. Each open postponed game has a printable notice - one slip per player with round, board, opponent, colour, the agreed date and the venue - and, once a date is agreed, a calendar file (.ics) to add it to a phone. "Print notices" prints them all; the Print page lists them too while a game is open.

  • Feature The standings show who still has a game to play. A player with a postponed game still to be played is marked "1 pending" on the Standings page and on the printed standings, so a provisional place is not read as a final one. In a team event the team is marked "1 board pending".

  • Change Pairing the next round names the players of every open postponed game. The confirmation lists each game still to be played and says its players are paired on a provisional score. It asks; it never blocks.

  • Feature A warning before anything reads as the end of the event. With a postponed game still unplayed, the Print page says "1 postponed game still unplayed" above the standings, cross table and prize lists, the Standings page lists the games under its "not final" note, and archiving the tournament says so in its confirmation. The tournaments list marks such a tournament "1 pending". Each game links to the round where its result is entered.

  • Feature Postponed games on the tournament's main page. The Players page shows a small card with every postponed game still to be played - round, board, players and the agreed date - each opening its round on the Pairings page.

  • Feature Which FIDE rating period a late game belongs to. On Settings, Export, the postponed-games file says in plain words which month's rating period each played game falls in, and to send it before the end of that month to have it rated then. It is guidance; your federation may want it sooner.

  • Change A team match with a postponed board says so. On the Pairings page and the printed team pairings its score reads "2.5 - 1.5, 1 board pending", and its match points are marked provisional. They still count in the team standings, as before.

  • Feature The results site can show when a postponed game will be played. The agreed date is sent to OpenResults with the board, so spectators see "Postponed, to be played …" instead of an empty result.

  • Fix A SWAR round robin imports as a round robin. It came in as a Swiss in everything but its name, so its standings used the Swiss rules of the tie-break regulations: a forfeit between two tied players left direct encounter undecided, the free round counted in Sonneborn-Berger, and Buchholz was allowed. SWAR's own copy of FIDE's round-robin tie-break exercise came out with three places reversed; it now matches SWAR. A SWAR "double rounds" event imports as match format, "aller-retour" as a double round robin.

  • Fix A SWAR file saved just before pairing no longer adds a round. SWAR saves the round it is about to pair with nobody in it yet; the import turned that into a finished round in which every player was absent, and every Buchholz and Sonneborn-Berger moved. That round is now left out, and the import says so - pair it here.

  • Fix Some SWAR v6.50 files would not import at all. A v6.50 file can carry SWAR's older, shorter category list; the import now reads either.

  • Verified Standings re-ranked against SWAR's own. Every SWAR file at hand was imported and ranked here, and compared with the places and tie-break values SWAR stored in it (tools/swar_rerank.exs). Every remaining difference in a finished event is SWAR applying the 2024 tie-break text, a Belgian convention, or a SWAR defect - listed in docs/swar-import.md.

  • Change A SWAR file in SWAR's team mode asks before it imports. SWAR has no team tournaments: its "team" mode (a Swiss named "... - team") keeps only the individual games, while the teams, matches and match points live outside the file. Importing one now says so and offers to import the games as an individual tournament, instead of doing that without a word.

  • Change SWAR files unlike any seen so far are refused, or flagged. A tournament type SWAR's own nine do not include used to import as an ordinary Swiss; it is now refused with a sentence saying why. Data after the player list - where a newer SWAR would put something new, teams for instance - used to be ignored without a word; the tournament now imports without it and the import says so, with the size and the SWAR version, and asks for the file.

  • Fix SWAR's pairing exclusions are imported. A SWAR file that keeps clubmates or compatriots apart - SWAR's way of running school and interclub-style events - or that forbids certain pairs of players, lost the rule on import, so the next round paired here could seat teammates against each other. The club and federation rules and the forbidden pairings now come across, with a notice when SWAR's club numbers and the clubs' names do not group the players the same way.

  • Feature Rank each category separately. A new switch on the Categories page gives every category its own standings, numbered from 1, with ties broken among the tied players of that category only - so direct encounter looks at the games within the category. The standings table, its print and the category filter all follow it. This is SWAR's "separate categories", and a SWAR file with that setting imports with it on, and with each category paired on its own.

  • Feature A round robin can pair each category on its own. With "Pair each category independently" on, every category gets its own Berger table, all in one round with the boards running on - a club event of several round-robin groups is one tournament.

  • Fix A SWAR round robin continued here plays SWAR's own table. The import numbered players by their SWAR registration number, so a round robin saved before pairing - or a Swiss seeded for round 1 - was paired in registration order. Players are now numbered in SWAR's own seed order, which is how SWAR numbers its Berger tables: for every single-group round robin among the SWAR files checked, each round comes out exactly as SWAR paired it. Its free rounds stay worth the full point SWAR gives them.

  • Fix SWAR's extra points count in the standings, as they do in SWAR. A SWAR Swiss whose players have extra points imports with Extra points switched on; a round robin or 3-2-1 file has none, as SWAR drops them.

  • Feature SWAR's player lists import. A SWAR "base" file - a club's players, saved once to start every tournament from - was refused for its empty round 0. It now imports as a tournament with those players and no rounds.

  • Feature More of a SWAR file comes across. Categories arrive switched on; a "double rounds" Swiss is match format; FIDE homologation and SWAR's per-round FIDE tournament ids fill in the FIDE settings; SWAR's colour for the top seed in round 1 becomes the initial colour. An accelerated Swiss says that SWAR's acceleration is not FIDE's Baku method and is imported without it. SWAR's own settings OpenPairings has no place for - the rating it seeds by, the first table number, the rating-report rounds, its XtraPoints table and more - are kept, for the export.

  • Feature The .swar export writes everything SWAR can hold. Exclusion rules and forbidden pairs, extra points (while the tournament counts them), separate categories, round robins double and in match format, Swiss match format, every tie-break SWAR has (six were written before), FIDE homologation and tournament ids, the initial colour, and everything a SWAR file brought with it. Dates go in SWAR's day/month/year, the cadence of a rapid or blitz event is found in SWAR's list, and categories go as names. The Export page lists, for the tournament at hand, what the file cannot hold and what SWAR will do instead.

  • Fix A backup of a SWAR tournament restores as that tournament. A JSON backup, a restore point or a hand-off left out the tournament's SWAR identity and the SWAR settings it was imported with, so the restored copy exported a different .swar file, under a new identity on the federation's results site. They now travel, together with "Rank each category separately", and the restored copy exports the same file byte for byte and continues a SWAR round robin with SWAR's full-point free rounds. Older backups still restore. A copy imported beside a tournament that already has that identity - "Duplicate", or a backup imported on the machine the original is on - gets its own instead, and the import says so, so two tournaments never upload as one event.

  • Verified Import, export, import gives the same tournament. Checked for a synthetic file per feature and for every real SWAR file at hand: each comes back as the same tournament, and exporting it again gives the same file. The one thing that does not survive is a national rating that differs from the FIDE one, which a SWAR 7 file has no place for. Re-ranked against SWAR's own standings, finished events now agree on 436 of 456 places (was 424) and 1890 of 1995 tie-break values (was 1848); the rest is listed in docs/swar-import.md.

  • Feature A team Swiss file's 320 and 330 records are trusted, not guessed. Importing a TRF-2026 file that says which team had the pairing-allocated bye each round (320), or names a match forfeited by decision with no boards for it at all (330), uses what the file says instead of assuming the one boardless team had the bye. A file's own 362 match-point values (win/draw/loss) are imported too. Files without these records keep today's behaviour.

  • Feature Team TRF exports carry FIDE's 310/362/320 team records. A team tournament's export now writes each team's number, match points, game points and final rank (310), the tournament's match points (362), and a team Swiss's pairing-allocated bye each round (320) - so a file this app writes now round-trips through another program's team-rank check, not just its own.

  • Fix 192 (which system paired the boards) named team events and round robins by the wrong FIDE code. A team event exported FIDE_TEAM, which the table only defaults to FIDE_TEAM_TYPEA_MP_GP - now written outright - and a team round robin of more than one cycle exported FIDE_TEAM_ROUNDROBIN (one cycle) regardless, now BERGER_TEAM_ROUNDROBIN_G2. Import now also recognises a team file's own FIDE_TEAM_DOUBLEROUNDROBIN/BERGER_TEAM_DOUBLEROUNDROBIN as two cycles, and an individual file's FIDE_DUTCH_2025 (FIDE's real spelling for the system in force since 1 July 2025) as Ainalrami.

  • Fix 192 now names Ainalrami's Dutch system by FIDE's own code. A tournament paired by Ainalrami exported FIDE_DUTCH_2026, a code FIDE's table does not have; it now writes FIDE_DUTCH_2025 (and FIDE_DUTCH_2025_BAKU under Baku acceleration). This waited for Ainalrami 0.33.0, whose own table had the same wrong spelling. Import still reads FIDE_DUTCH_2026, so files exported before come back as Ainalrami.

[0.68.0] - 2026-09-27

  • Change Ainalrami 0.32.0. Its TRF checker now also checks team events: the team ranks in a TRF-2026 file, a match forfeited on every board, and team byes, all as FIDE's tie-break rules set out. OpenPairings' own team standings are unchanged.
  • Feature Team TRF files in the 2026 format import their teams. A file whose team section uses the newer 310 records, instead of the older 013, now brings in each team's name and players.

[0.67.0] - 2026-09-26

  • Change Postponed games moved to Settings, Export. Everything the separate Postponed games page had - every postponed game with its state and the date it was played, and the postponed-games TRF - is now in the TRF section, beside the round reports. The Pairings page keeps only the reminder of games still to be played, where their results are entered; its "Postponed games" button is gone, and an old link to that page lands on Export.
  • Feature Choose what goes in a postponed-games file, and its dates. Played postponed games that went out as ? in their round's report are listed with tick boxes: send some now, keep the rest for a later file. The ticked games are packed into as few extra rounds as possible, nobody twice in a round, and each extra round has a date you can set - by default the latest date its games were played. "Download a copy" marks nothing; "Send…" asks first and marks only the ticked games as sent.

[0.66.1] - 2026-09-26

  • Fix The top bar stays one row. Your email, account settings, Features, the version and Log out are now one account menu at the end of the bar, instead of a row of links that pushed the bar onto a second line on a laptop-width window.
  • Fix Sending a TRF updates the Export page straight away. A round sent from the TRF section now shows "Sent" without a reload.
  • Fix Confirming a postponed or already-sent result no longer stretches the pairings table. The explanation wraps inside the result cell, with its buttons underneath.
  • Feature PGN for a range of boards. More, PGN: type the boards (for example 1-4, as printed on the pairing sheet) and download just those games of the round - the top boards for a broadcast.
  • Removed The "Keyboard and mouse tips" line under the round header.

[0.66.0] - 2026-09-26

  • Fix Team Swiss picks the pairing C.04.6 defines on large fields too. With Ainalrami 0.31.0, choosing among a score group's pairings (Article 3.6) is exact instead of a search with a budget, which on very large early-round brackets could settle for a legal but different round. Rounds are also faster.

  • Fix A team tie-break list that starts with game points uses game points throughout. MPvGP, SSSC and extended direct encounter now follow the score the list starts with (Ainalrami 0.31.0).

  • Verified Tie-breaks checked two more ways: against an independent second implementation of C.07 (15,000 events, no disagreement) and under random tie-break lists against FIDE's TieBreakServer (22,000 events, no unexplained difference).

  • Security Two dependency advisories closed. mint 1.10.0 -> 1.10.1 fixes EEF-CVE-2026-82672 (MEDIUM): its HTTP/1 client did not validate the tail of a chunk-size line, allowing response smuggling on pooled connections. lazy_html 0.1.12 -> 0.1.13 (test-only) fixes EEF-CVE-2026-92106 (LOW): SVG and MathML style/script text serialised unescaped. typed_struct 0.3.0, websock 0.5.3 and websock_adapter 0.6.0 were checked too: mix hex.audit and osv.dev list no advisory for them.

[0.65.2] - 2026-09-25

  • Change The TRF section shows where every round stands. One row per round: sent (with the date), ready to send, ready with a postponed game that goes out as unknown, or still being played with how many results are missing - each with its own mark, not only a colour, and a summary line above ("2 rounds sent · 1 ready to send"). Tick the rounds instead of typing "1-5": the ready ones start ticked. "Download a copy" takes any ticked round and marks nothing; "Send…" asks first, marks the results as sent, and says why it cannot go ahead when a ticked round was already sent or still has boards without a result. "All rounds (TRF)" stays as a quick full download.

  • Fix The round's Print and More menus open onto the page instead of running off its right edge.

  • Fix The top bar no longer jumps to two rows while publishing. The results-site indicator keeps one width, whether it says "Live 42 ms" or "Sending".

  • Removed The spectators' QR block on the local view. The Pairings page's "Public page" button already opens the same page.

  • Change About is the last item in the Settings menu, after Export.

[0.65.1] - 2026-09-25

  • Change A tidier round header on the Pairings page. The three publish switches sit together in one "Public" group, named Pairings, Standings and Results (the heading already says which round). Printing is one "Print" menu: pairings, pairings with the absentees section, result cards, a test print and the stack-cut order. The rest is under "More": the PGN downloads, "Import results (CSV)", and "Unpair round", last and in red, apart from everything else. The long tip under the round is folded into one "Keyboard and mouse tips" line.

  • Change The TRF downloads moved to Settings, Export. "Export TRF (all rounds)", the rounds you choose, and "Export TRF for sending" with its "Finalise" tick are now together in a TRF section there, with the notes that go with them (rounds already sent, postponed games still open, players the sent-games record cannot tell apart). A TRF is made after a round or at the end, and on the Pairings page, used while a round is played, they were in the way.

[0.65.0] - 2026-09-25

  • Change Tie-breaks now come from Ainalrami, the same code that checks them. Every tie-break value, the order inside a tied group, and the per-round working on the public page are now computed by Ainalrami's C.07 (2026) implementation. It was checked against FIDE's TieBreakServer on more than seven million values. Before the switch, the two were compared on 1,000 tournaments (mix pairings.tiebreak_gate, docs/tiebreak-gate-2026-09.md), and every difference was traced to a cause.
  • Fix Forfeits no longer count as played games in Buchholz and Sonneborn-Berger. C.07 2026 treats a forfeit as an unplayed round, which is scored against a dummy opponent. The old code counted the forfeit opponent's score instead.
  • Fix Direct encounter works when not everyone in the group has met. Before, it only ordered a score group in which everybody had played everybody. It now follows all of Article 6, including re-applying it to a smaller group and the Swiss rule.
  • Fix A player's own missing rounds count in their Buchholz. A round with no game on record (after a withdrawal, for example) scores against a dummy at the player's own score, as Article 16.4 says. It used to add nothing.
  • Change Buchholz is not used in round robins. C.07 Article 8 forbids it. The standings page drops it and says why. This applies to team round robins too.
  • Change Team tie-breaks come from Ainalrami as well. Team Buchholz, Sonneborn-Berger, EMGSB and the order among tied teams are now Ainalrami's C.07 tie-breaks. Ainalrami's team tie-breaks were compared game by game with FIDE's TieBreakServer. The values OpenPairings already gave are unchanged.
  • Fix Team direct encounter works when not every tied team has met. Before, it only ordered a group in which everybody had played everybody. Now all of Article 6 applies to teams too.
  • Change A round still in progress no longer moves the team tie-breaks. They count the rounds whose every match is finished. Match points, which rank first, still count every finished match.
  • Feature Postponed games, per tournament. A game that is not played in its round - moved to a later evening by agreement, or adjourned - can now be recorded as postponed. Turn it on under Settings, Scoring ("Allow postponed games"); it is off by default, and off means no postponed option is offered anywhere. When on, the result list on the Pairings page (and on a phone) offers "postponed by White" and "postponed by Black". The tournament goes on. Every postponed game still to be played is listed at the top of the Pairings page, with a button to its round, so its real result can be entered at any time, however many rounds later.
  • Feature What a postponed game counts as, until it is played. Also under Settings, Scoring: one value for the player who postponed it and one for the opponent, each a draw, a win or a loss. The default is a draw for both, which is what FIDE allows; anything else (a win for the player who postponed, so they are paired higher up, for example) takes the tournament out of FIDE mode, and the page says so. The value counts everywhere - standings, tie-breaks and the pairing of the next rounds - and is stored on the game when it is postponed, so changing the setting later only affects games postponed after the change.
  • Feature Postponed games page. With postponed games on, a page lists every game ever postponed: who postponed it, what it counts as, when it was played (a date, today unless you change it) and what has been sent of it.
  • Feature Finalise results for TRF sending. The TRF export for sending on the Pairings page has a box, "Finalise results for TRF sending". Ticked, the download marks every result of the exported rounds as sent. A sent result can still be changed, but only after a confirmation, and a round that was sent cannot be finalised a second time or unpaired, so no game goes to FIDE twice. The rounds already sent are shown beside the button. A copy can always be downloaded with the box unticked. The whole file is refused while an exported round has a board with no result at all.
  • Feature What was sent survives a restore. Every game sent in a finalised TRF is also kept in a record that a restore point or a hand-off return does not replace, and the "sent" marks are put back on every game still there afterwards. Going back to before a round was sent, even to before it was paired, cannot make that round sendable again. A restore that would take away a sent game, or give it another result, shows a warning listing those games and needs its own tick besides the typed word.
  • Feature A round that was sent is protected from hand edits. Swapping players, marking one absent, filling a seat, awarding a bye or pairing from the pool in a round already sent to FIDE shows a large warning and needs its own tick before it goes through. The round stays marked as sent. The audit trail says the warning was confirmed.
  • Feature Absences in a sent round are protected too. Changing a player's "Absent at the rounds" in the player dialog on the Players page so that it adds or removes a round already sent shows the same large warning, and Save waits for its own tick. The round stays marked as sent, and the audit trail says the warning was confirmed.
  • Feature A warning when the sent-games record cannot tell two players apart. The record names a player with no FIDE ID by name, so two such players with the same name (ignoring case and spaces) are one player to it. The Pairings page says so beside the TRF for sending, naming them; sending still works, and the audit trail notes the warning. A restore or a hand-off return that puts the sent marks back says so too when one of those players has a sent game.
  • Feature A postponed-games TRF. A game still open when its round was sent goes in that report as ?, and every later report writes it as ? again, so a file that was sent never changes. A game played before its round is sent simply goes in that round with its result. A game played after goes in a separate TRF on the Postponed games page instead: extra rounds, as few as possible, with nobody playing twice in a round, and only the players of those games in it. Finalising that file marks its games as sent, so each goes to FIDE exactly once.
  • Feature Pairing with a result missing records it as postponed. If the last round still has boards without a result, a second button beside "Pair round" offers to record them as postponed games and pair the next round. It says what that does and asks first; the page then says which boards it recorded, and so does the audit trail. The ordinary button still refuses, as before.
  • Feature Warnings where a postponed game matters. Entering a result that is not a draw for a postponed game asks for confirmation first, because the rounds paired since counted it provisionally and stay as they are; the question says what it counted as. Pairing a round while a postponed game from an older round is still open asks too. A postponed game in the last round is simply noted beside the button. A results CSV cannot give a postponed game a result that is not a draw, or change a result already sent in a finalised TRF: the import names the board and says to enter it on the Pairings page, where the confirmation is.
  • Feature Standings say "not final" while a game is postponed. The Standings page, the printed standings and team standings and the cross tables carry a banner as long as a postponed game is still to be played, and the tournament stays "running" rather than "finished" even after the last round, until it is played.
  • Feature The TRF marks a postponed game as an unknown result. The TRF26 export writes ? for both players, and the 162 record gives it the value of a draw (X), the value FIDE's format gives an unknown result, so the file's points column adds up from the file itself. The older spelling writes the draw and scores it as one too. The Pairings page says the export is not final while one is open, and, when a postponed game counts as something other than a draw, that the file's points can differ from the standings until it is played, and that an outside pairing program or checker (JaVaFo, a FIDE pairing checker) cannot reproduce the rounds paired since from either download. Only the file the app hands its own pairing engine, which is never sent anywhere, carries the value the tournament counts the game as.
  • Feature The KBSB upload says "Voorlopige stand" instead of "Eindstand" above the standings while a postponed game is still to be played.
  • Change A ? in an imported TRF becomes a postponed game. It used to be imported as a blank result, which stopped the next round from being paired. It is now kept unknown - counted as a draw until its result is entered - and the import says which rounds carry one. Importing such a file turns postponed games on for the tournament.
  • Fix Importing a TRF with a ? result no longer crashes the page. The notice about it was the one import notice the page could not read.
  • Change Norms, performance ratings and expected scores leave a postponed game out until it is played, and the SWAR export writes it as not played yet. PGN exports it as *.
  • Change A team match with a postponed board is not complete. Its score is shown as provisional, with the number of boards postponed, and the next round is paired with that provisional score.
  • Fix Badges: the KBSB logo is back as the default right-hand logo on both sides, and the stray black bar on the front's right edge is gone. The logos, QR code and room numbers sit lower, as on the original design, and the logos on the back are centred in the space under the rooms.
  • Change Badges: a photo from FIDE also works when the server cannot reach FIDE. ratings.fide.com does not answer requests from a datacenter, so on the server "Fetch from FIDE" can fail. "Open FIDE profile" opens the player's page in your browser; copy the photo there and paste it into the badge with Ctrl+V, or save it and upload it. An installation can also send "Fetch from FIDE" through a relay it controls (FIDE_PHOTO_PROXY_URL and FIDE_PHOTO_PROXY_TOKEN), which makes the button work on such a server.
  • Change Tools: the badge maker is listed after the FIDE report forms, instead of between the page's introduction and the upload it describes.

[0.64.0] - 2026-09-24

  • Feature Accreditation badges. A badge maker for the arbiters' room and the press desk: A6 badges with a photo, name, title, federation, FIDE ID, a coloured role (Chief Arbiter, Arbiter, Player, Press, VIP, ...) and up to twelve numbered rooms or zones on the front, the event's usage conditions on the back, printed two to an A4 sheet with fold and cut lines. Link a badge event to a tournament and "Import players" and "Import officials" fill it in; running an import again updates those badges instead of adding new ones, and never touches badges you added by hand or fields you changed yourself. Photos can be uploaded, or fetched one at a time from the player's FIDE profile with "Fetch from FIDE". Role names, room names and the conditions are edited per event, so a bilingual event prints in both languages. Find it under Tools, in a tournament's Advanced menu, or on its Print page. Signed-in only.

[0.63.6] - 2026-09-23

  • Fix Ticking a box in the player dialog no longer jumps back to the top. In Safari (and Firefox on a Mac) clicking a checkbox does not focus the checkbox, so focus lands on the dialog itself. When the tick re-rendered the dialog, the code that moves focus into a freshly opened dialog read that as "nobody has started yet" and moved focus to the Name field - scrolling a long form back to the top mid-edit. Once you have clicked or typed in a dialog, it now leaves your focus alone.

[0.63.5] - 2026-09-21

  • Fix The edit dialog stopped growing nameless "(not in list)" category boxes. Ticking or unticking a category added one more empty checkbox each time. The checkbox group posts a hidden empty value on every change - that is what makes unticking the last category reach the server at all - and the row that shows categories the tournament no longer lists was drawing a ticked box for that blank, which then posted its own blank back. Blanks are now dropped where the dialog reads the list, by the same rule the database stores by, so the row shows only real names. Nothing was ever saved wrong: the schema already discarded the blanks.

[0.63.4] - 2026-09-21

  • Fix The Pr./Paid/Cat. cell menus stopped flickering. Clicking the cell a menu came from closed it on the way down and re-opened it on the way up, so the little box blinked instead of going away; a double-click (the gesture for the edit dialog) left it floating over the dialog; and on a tournament with no categories defined yet, the Cat. cell opened an empty box. A second click now puts the menu away, a double-click opens the editor with nothing left behind, and a menu with nothing to offer does not open at all.
  • Change The Cat. menu stays open while you pick. A player is usually in more than one category, and the menu closed after every single toggle, so three categories meant opening it three times. It now stays up and re-labels itself ("Add Junior" becomes "Remove Junior") as the row is patched. The single-valued menus - Pr. and Paid - still close on the one pick that finishes them.

[0.63.3] - 2026-09-19

  • Change Publishing the "Rds" column is opt-in. 0.63.2 sent it for every published tournament, which put a new number on public pages - some of them mid-event - that nobody had asked for. It is now off until you tick "Rounds-present column" under Settings - Results site: the only public display tick that starts off, and deliberately the exception (see PairingsEngine.PublicDisplay). Your own screens and printouts are unaffected - those follow the "Rds" column tick, as before.

[0.63.2] - 2026-09-19

  • Change "Rds" is now an ordinary column tick, in the Players page's Display panel beside Cl, Nr, Rnk and Ga - not a setting and not a switch. Tick it and it shows on the players grid and on the standings table, and the Print button takes it with it. The tournament setting it briefly had is gone; nothing you can enter was stored in it.
  • Change The results site gets the column by default. Publishing it is the ordinary display tick under Settings - Results site, which is on unless you turn it off - so a published tournament simply lists it, the way it lists ratings and clubs.

[0.63.1] - 2026-09-19

  • Change The "Rounds present" switch moved to the Standings page, beside Print, where the table it changes actually is - it was under Settings - Scoring, a page about what a round is worth. It reads On/Off rather than Public/Not public, because it also changes your own screen and the printed sheet.
  • Feature Publishing that column is now its own tick, with the other public ticks under Settings - Results site. Off keeps the column on your screen and your printout without sending it to the results site.

[0.63.0] - 2026-09-19

  • Feature A "Rds" column: how many rounds each player was there for. Asked for by a club whose championship gives a prize to everyone who turns up for every round - they were counting it off the crosstable by hand. The switch sits on the Standings page itself, beside Print ("Rounds present", On/Off); it then shows there and on the printed standings (main and per-category tables, Swiss, round robin and Keizer). Whether the results site carries it too is its own tick, with the other public ticks under Settings - Results site, and readers there can sort by it. It counts games played, whatever the result, plus a bye given because the field was odd and a win by forfeit; it does not count a half-point or zero bye the player arranged in advance, an absence, or a loss by forfeit. Off by default, and it changes no scoring, no ranking and no tiebreak.

[0.62.11] - 2026-09-18

  • Fix An upgrade now refreshes what is already published. What a published copy contains is decided by this application's code, so a fix to the snapshot used to reach spectators only for tournaments that happened to be edited again afterwards - 0.62.10's fix left a live event showing the old document. The first start on a new version re-sends every published tournament once. Tournaments taken down or binned stay down.

[0.62.10] - 2026-09-18

  • Fix Players who register after the first pairing now reach the results site. A player added once start numbers had been issued had no number yet, and the published roster kept only numbered players - so a 21-player event with 3 absences showed 16 instead of 18 on OpenResults. Such a player is now published with the number the next pairing will give them, the same way the whole field is numbered before round 1. Team reserves who have not played yet appear in their squad for the same reason.

[0.62.9] - 2026-09-18

  • Fix Large team Swiss events pair reliably and quickly. With Ainalrami 0.29.0, events of 200-500 teams no longer hit "couldn't finish within the search limit", and the slowest round measured went from over a minute to about 5 seconds. Every pairing is the same as before.

[0.62.8] - 2026-09-16

  • Fix The print and PGN menus on the Pairings page open into the menu from the keyboard in every browser. The context-menu key or Shift+F10 on a print link now always moves focus to the first option, the link says whether its menu is open, and closing the menu puts focus back on the link.

  • Fix The phone results page no longer moves focus by itself after another phone enters a result. It only hands focus on when the board you were on leaves the list, not when you had already moved elsewhere.

  • Change The login and sign-up pages describe what OpenPairings does now. TRF-2026 is no longer listed as coming soon (it shipped in 0.47.0), team tournaments, the pairing explanation and publishing to OpenResults are listed, and the Ainalrami line says what it was validated against.

[0.62.7] - 2026-09-15

  • Change The hosted server has its own logo. The web version shows a new "aurora" mark in the header and the browser tab; a desktop install keeps the green one, so it is easy to tell which copy you have open.

  • Fix The Options page no longer shows "Match format" twice with the same name. One setting is for round robin and one for Swiss; they are now labelled "Round robin match format" and "Swiss match format", each says which system it applies to, and the Keizer top value says it is for Keizer only.

[0.62.6] - 2026-09-15

  • Feature SWAR files whose [CATEGORIES] block defines a two-axis category (age-then-rating or rating-then-age) now import both axes: a player gets tagged with their age band and their rating band, the same way any player already carries a set of categories. Pairing settles on the first axis deterministically. Exporting a two-axis-imported tournament writes the same two-column [CATEGORIES] shape back out, and a re-import gives back identical categories and player assignments. See "Categories: two axes, two tag sets" in docs/swar-import.md.
  • Fix SwarExport's own category round trip silently lost every player's category on export-then-reimport: value1 was written with a leading blank slot that no longer lined up with how the per-player index is decoded. Fixed alongside the two-axis work above; a plain single-axis category list now round-trips correctly too.
  • Security Following the 0.62.5 KBSB roster leak (a rescue that quoted the whole downloaded roster - names and birth years - in the page's error text and the server log), the same class of bug is now closed everywhere else it could reach personal data: the FIDE rating-list sync's own crash handler and its :DOWN task-crash handler, the SWAR importer's parse-failure handler, and the TRF importer's parse and post-import verification handlers. A new shared PairingsEngine.SafeError module (reused by the KBSB sync too, so there is one fix instead of four) turns any unexpected exception or crash reason into the exception's TYPE only - never the term it failed on - both on the page and in the log; the SWAR import panel's flash text is built the same safe way instead of inspect/1-ing the raw parse-error reason.

[0.62.5] - 2026-09-15

  • Fix When team Swiss pairing gives up or breaks, the arbiter now sees a clear, translated notice instead of a raw refusal string or a crashed page: the search hit its limit, no legal pairing/bye exists under FIDE's rules, or an unexpected crash - each says what happened, that nothing changed, and what to do next (try again, pair by hand, or for a very large event contact support). Every refusal or crash is caught before anything is written, so the round is left unpaired and the tournament otherwise untouched; a crash is logged for the admin without any player or team data. A team Swiss round now pairs in a supervised background task instead of the LiveView process itself - large events (300-500 teams) can take 10-50 seconds, and the Pairings page now shows "Pairing round N… this can take up to a minute for large events" with the button disabled, rather than looking frozen; a second click while it runs is refused rather than starting a second search. The individual (JaVaFo/Ainalrami) path's own pairing crash is now caught the same way, with the same generic message.
  • Feature A snapshot published from a hosted instance now names its publisher: the tournament owner's account email and this instance's public host, in a new publisher field. OpenResults uses it to show which hosted account published a tournament in its admin panel only - it never appears on a public page. Additive and hosted-only: absent on a local or desktop install (there are no accounts to name), and absent whenever the tournament's owner is not known. See docs/snapshot-schema.md in the OpenResults repo.

[0.62.4] - 2026-09-14

  • Verified No change to what the app does. A release to test updating from 0.62.3: the update banner, "Check for updates now" and the in-app update itself. The builds behind it now reuse compiled dependencies, and a test's note about a JaVaFo/bbpPairings disagreement is corrected - it no longer reproduces.

[0.62.3] - 2026-09-14

  • Fix A second "Sync from KBSB" press no longer seems to do nothing. When KBSB's file has not changed since the last sync, nothing is downloaded - correctly - but the page said nothing, so the button looked broken. It now says "Already up to date", and a sync that did import says how many players it imported. English and Dutch.
  • Removed Releases no longer include macOS downloads. Only Windows and Linux are built; the macOS builds took the longest by far and nobody used them. They can still be built from source.
  • Fix The update banner can now be dismissed per version, and a dismissal never hides a later release. Investigating a report of never seeing the banner found the auto-check itself, its placement on every arbiter page, and desktop detection all already correct - but there was no way to dismiss the banner at all, and the actual GitHub release for the latest tag on the day of the report had not been published, so "no update" was the honest answer regardless of the code. The dismiss control this adds closes the gap that would have made a real update look silently suppressed once an arbiter learned to ignore it.
  • Feature "Check for updates now", on the Admin page's "This installation" card, desktop only. Reports "You're on the latest version", the newer version (with the existing banner's install/release-page actions appearing alongside it), or "Couldn't reach GitHub" - only ever shown after an explicit click, never for the silent background check. Rate-limited to once per 30 seconds machine-wide, well inside GitHub's 60/hour unauthenticated budget.
  • Change The background check now runs a few seconds after the app opens (was 10 seconds) and every 30 minutes after that (was every 6 hours), so an update shows up the same time an arbiter opens the app, not up to half a working day later. It also sends GitHub's ETag as If-None-Match on every request now and treats a 304 as "nothing changed" rather than a fresh check - free against the 60/hour unauthenticated budget, so the shorter interval costs the same request budget a single uncached check every few hours used to.

[0.62.2] - 2026-09-14

  • Fix Syncing the Belgian rating list from the default address failed every time with "no case clause matching". Only an address with a month placeholder named the month it found, and the sync expected one. It now imports from the fixed address too.
  • Security A failed Belgian list sync no longer prints player data. The error shown on the page, and written to the log, quoted whatever the import failed on - in this case the downloaded roster, names and birth years. It now names only the kind of failure.

[0.62.1] - 2026-09-14

  • Fix The Belgian rating list sync failed with :not_sqlite. It asked for the current month's file (players_202609.zip), which KBSB never creates: that address answers with a redirect to KBSB's blog, the sync followed it and tried to read a web page as the rating list, instead of falling back to an earlier month. KBSB keeps one file current, players_202608.zip, so that is now the default address. Redirects are no longer followed blindly: one to another data file is followed once, and any other redirect counts as "not published", so a month template still falls back as intended.

[0.62.0] - 2026-09-14

  • Fix Windows: uninstalling or upgrading OpenPairings could delete every tournament and every local backup. Two installers could wipe %LOCALAPPDATA%\OpenPairings whole: a source-built Setup.exe for 0.53.0-0.53.2 (never offered on the release page, but how one arbiter lost everything), and every .msi from 0.58.1 to 0.61.0, whose Velopack clean-up step deletes that folder on uninstall or when a newer .msi replaces it - "just for me" installs are the ones known to be affected. Your data now lives at %LOCALAPPDATA%\OpenPairingsData, backups at their own %LOCALAPPDATA%\OpenPairingsBackups, so removing one cannot remove the other; existing installs move automatically on first start of this version, before any old clean-up step can run. If you still have an old OpenPairings installed, start this version once before you uninstall anything. Installed apps is also cleaned up: duplicate and stale entries for the same install are removed. See docs/binaries.md, "Windows: where the data lives".
  • Fix Windows: the .msi and Setup.exe no longer leave two entries in Installed apps. Installing one over the other now ends with one entry at the new version. The .msi refuses to install a second copy "for everyone" beside one installed "just for me" (and the other way round), and says which choice updates the copy you have.
  • Change The Belgian rating list now syncs from KBSB's own public monthly file (players_YYYYMM.zip at frbe-kbsb.be), instead of the KBSB data-platform API or the OpenResults relay - both removed. No API key is used any more; hosted and desktop installs work identically. Club names, when KBSB doesn't bundle them, can come from a second optional "Belgian club names URL". The manual file upload still works, and now also accepts the zip or the players.sqlite inside it. A list with no national rating on any row - KBSB's July and August 2026 files - keeps each player's stored rating instead of wiping every rating. See docs/kbsb-sync.md.
  • Feature Team Swiss: teams play teams. A Swiss created with Team tournament ticked now pairs team against team under FIDE's Swiss Team Pairing System (C.04.6, February 2026), with Ainalrami's team engine: matches seated board by board as a team round robin's, White on board 1 and every odd board for the team the colour rules give White, match points from the score and game points as the colour tie-break, FIDE's Type A colour preferences. The pairing-allocated bye scores a drawn match. An event already paired player by player before this stays on that path unless every round is unpaired. See docs/team-tournaments.md.
  • Feature Team round robin: teams play teams. A round robin created with Team tournament ticked pairs team against team over the whole Berger schedule. A new Teams tab (team tournaments only) adds teams, sets their board order and boards per match, and orders them by hand or by rating for pairing numbers. Each pairing is a match played board by board; an absent player's reserve moves up, an unfillable board is a forfeit win, and board numbers run on across the round so result entry, slips and score sheets are unchanged. English and Dutch. See docs/team-tournaments.md.
  • Feature Team standings, team tie-breaks and board statistics. The Standings page of a team round robin ranks teams by match points, then game points, direct encounter, Buchholz, Sonneborn-Berger and weighted board points, with a Working disclosure; below it, every player's boards, games and performance grouped by board for board prizes. FIDE's team default tie-break set (MP GP DE BB SB) is now calculated instead of dropped.
  • Feature Team tie-breaks follow C.07 Article 16 in a team Swiss. Buchholz, Sonneborn-Berger and EMGSB count a bye, a forfeited match or an unplayed round against a dummy opponent correctly, and a withdrawn team's remaining rounds count as draws for its opponents. A team round robin is unchanged.
  • Feature Initial colour: drawn by lot, or set by the arbiter. FIDE draws the colour that decides round 1's boards by lot (C.04.3 5.1, C.04.6 4.1). Settings - Options now has Initial colour: Drawn by lot (the default), White or Black, for individual and team Swiss. It locks once round 1 is paired; tournaments that paired round 1 before this keep what they had.
  • Fix Round 1's colours were never really drawn. Ainalrami gave the first board White in every tournament, and JaVaFo drew its own lot on every run, so the same round paired twice with JaVaFo could come out with every colour reversed, and nothing recorded which it had been. Both engines are now told the stored colour.
  • Feature A team Swiss round's pairing rationale. The Pairing rationale page now shows the team engine's account instead of the individual analysis: why each bracket took the upfloaters it did, which sets could not be paired and why, the bye's reasoning and tie-break, and the Article 4.2/4.3 rule behind each match's colours. Rounds paired before this have no account, and the page says so.
  • Feature A team tournament imported from TRF gets its matches back. TRF files record teams and games but not which boards formed which match; the import now works the matches out from the boards - the same two teams board by board, board orders rising together, White on the first team's odd boards, a lone forfeit board placed by board order - and creates them, so the team standings, printing and publishing work straight away and a team Swiss pairs on by teams. A file exported by OpenPairings comes back with identical matches. A round whose boards do not form clean matches is not guessed at: the notice names the round and the reason; in a team round robin that round's games count for no team (and are marked on the Pairings page), and a team Swiss with such a round carries on player by player.
  • Feature Forfeit a team match by decision. The Pairings page's match list has a Forfeit by decision column: To Team A / To Team B makes every board of the match a forfeit win for that team and records the decision, with a restore point taken first and a row in the audit trail. Withdraw the decision gives the boards back the results they had. In a team Swiss the team the match was awarded to cannot then take the pairing-allocated bye (C.04.6 [C2]); if games were played before the decision, the teams still count as having met and played, for colours, repeat pairings and tie-breaks. Backups and restore points carry the decision. See docs/team-tournaments.md.
  • Feature A board added by hand joins its teams' match when it fits. Pairing two players from the not-playing list in a team event now offers the board where it fits their teams' match - a free board of it, the colours the match needs, board orders in line - and it counts for both teams. A board that fits no match is marked no team on the Pairings page, listed with the reason it counts for no team, and offered Make it board n of match m once it does fit. Before this such a board silently counted for neither team.
  • Change A team Swiss already paired player by player stays that way. An event part-way through is not converted when this version arrives: its rounds carry on on the individual path, and the Teams page says so. Unpair every round and it pairs team against team from round 1. A team Swiss imported from a TRF file whose rounds cannot all be rebuilt as matches counts as paired player by player too.
  • Feature Team pairing sheet and team standings print, on the Print page of a team round robin: one table per match with its boards, and the team table after any round.
  • Feature The TRF file carries the team section. A team tournament's export writes a 013 record per team with its players in board order; the individual games on the 001 lines are unchanged, and an individual tournament's file is byte for byte what it was. Importing a TRF with teams now creates the teams and their board orders (its games still come back as individual games: TRF does not record which boards formed which match).
  • Feature Team tournaments publish to OpenResults. The refusal that blocked the publish switch for a team tournament is lifted, now that the results site has team pages: the snapshot carries tournament.team_event, teams (name, short name, captain, roster in board order), each round's matches (the two teams, board 1's colour, its board numbers, and the game and match points OpenPairings computed), team_standings (rank, match points, game points, each tie-break with its Working parts) and board_stats (per board, each player's games, points, percentage and performance). Every field is additive and gated by the same withholding rules as the rest of the snapshot: an unpublished round's matches never leave, a match whose round has not switched results on shows its two teams with no points, and the team standings and board statistics stop at whatever round the arbiter has published standings through, exactly like the individual table. An individual tournament's snapshot is unchanged. The team data travels for a team round robin and for a team Swiss paired by teams; a team Swiss that was already paired player by player before this version publishes as an individual event, with its individual standings. Settings - OpenResults now explains what a team event sends instead of refusing it. See docs/snapshot-schema.md on the OpenResults side.
  • Feature A match forfeited by decision is published as one. The OpenResults snapshot names the team the arbiter awarded a match to (matches[].forfeit_decision), so the results site can say so rather than show only a row of forfeit results. Withheld exactly like the match's points: not sent while the round's results are private or the match is incomplete.
  • Change Backups and restore points carry team matches, the teams' seeding order and pairing numbers, and boards per match and match points, so a restored team tournament has the same standings.

[0.61.0] - 2026-09-13

  • Security A crafted hook payload could crash the Pairings or Players page for everyone connected. The Players grid's sort, category, card and cell-menu events, and the hand-edit menu's open_menu, are pushed by page scripts, so anyone holding the socket could send an unexpected value
    • an unknown sort key, a non-numeric id, a scope the menu has no branch for - and crash the LiveView. They are now no-ops instead. Found in a full audit of the browser-side JavaScript and LiveView hooks.
  • Change Dutch says "FIDE-rated" and "unrated", not "gerateerd" and "ongerateerd". The FIDE settings, the IT3 report description and the bonus points help use the words Dutch-speaking arbiters actually say; "lower-rated players" reads "spelers met een lagere rating".
  • Change Live results are now a choice per round, and off by default. The Pairings page has a third switch next to "Pairings round N" and "Standings after round N": Results round N. Red, which is what every new round starts as, means the round's pairings still go to the results site but its results do not - every board shows no result there, and nothing worked out from a result travels either. Green sends the results as you enter them, which is what happened for every round until now. So a typo, or a result on the wrong board, no longer goes public the moment it is typed, and players cannot follow their rivals' games on the site mid-round unless you switch it on. Rounds that are already public when you update keep their results public; so does a published round in a backup made before this version. The switch is locked green once the standings after that round are public (they already contain every result in it), and always in the "immediate" publish mode. Taking a round's pairings off the public page turns its results off too, and the confirmation says so; taking standings down only unlocks the switch. In the timed and scheduled modes the switch can be turned on before the round's pairings publish themselves. Requested, absent and pairing-allocated byes still show with their points; a forfeit recorded against an emptied seat is a result and waits with the rest. The Live page follows the same switch, since what is on the wall in the hall is public too: while a round's results are held back its boards show no results, a line says so, and the standings below stop at the round before. Switching results on or off is recorded in the audit trail. English and Dutch.
  • Feature The snapshot carries the tournament's category vocabulary, for a results-site filter bar. tournament.categories (the tournament's category names, in its own order) and players[].categories (gated the same way, but already added in 0.53.0) let OpenResults build "Best U1800" or "all Belgian players" style filters without guessing at an order or re-deriving the vocabulary from the roster. Both are additive and both are OMITTED - not sent empty - while the arbiter has the "Categories" display setting off, the same setting that already gates the columns on the arbiter's own public-display page: a tournament that does not group players by category in public should not hand a results site the means to do it anyway. players[].category, the single pairing category, is unchanged and still travels regardless of the setting.
  • Feature The Players grid works from the keyboard. The grid is one stop in the Tab order: the arrow keys move between cells, Home and End go to the ends of a row, Ctrl+Home and Ctrl+End to the ends of the grid, Page Up and Page Down ten rows. On a player's Pr., Paid or Cat. cell, Enter, Space, the context-menu key or Shift+F10 opens that cell's menu with focus in it; Escape returns to the cell. Your place survives live changes: when a result entered elsewhere re-ranks the grid, focus stays on the same player's cell, and if that player leaves the grid a screen reader is told and focus moves to the next row. Screen readers hear the grid as a grid, the player's name as each row's header, and a menu cell in words ("Paid, Anna Peeters: yes"). Clicking, right-clicking and sorting are unchanged.
  • Feature Pairings can be hand-edited from the keyboard. Every player on a board, every empty seat and every name in the not-playing list can be reached with Tab and is named for its board and colour ("White on board 3: ..."). Enter, the context-menu key or Shift+F10 opens the same menu a right-click does, with focus in it. "Swap with..." then says "Swap armed" to a screen reader, shows the word "swapping" on the armed seat, and Enter on the second seat completes the swap; Escape cancels and says so. Pairing two players from the not-playing list and choosing an empty seat work the same way. A confirmation you cancel returns focus to the seat you started from; one you apply leaves focus on the board you edited. What each edit does is unchanged. English and Dutch.
  • Fix Ctrl+1, Ctrl+2 and Ctrl+3 no longer enter a result. With a board's result box focused on the Pairings page, the quick-entry keys 1, 2 and 3 also answered to Ctrl, Alt or Cmd held down: pressing Ctrl+2 to switch browser tabs recorded a draw on that board and kept the tab from switching. The keys now only count on their own (or with Shift, which AZERTY keyboards need).
  • Fix The Players grid and the standings keep your chosen columns after a reconnect. When the connection dropped and came back - a server update, a network hiccup, a laptop waking from sleep - the Players grid went back to the default columns and the standings to all of them, until you moved to another page. The columns you picked in the Display panel now come back with the connection.
  • Fix The projector view keeps fitting the screen after a reconnect. A hall screen that lost its connection and got it back went back to twelve boards a page until the window was resized. It now measures itself again when it reconnects.
  • Fix A screen reader hears a pending server update once, not on every page. During the countdown before an update, moving to another page read the whole "Server update" sentence out again. It is said when it appears and when it gets closer, as intended.

[0.60.2] - 2026-09-13

  • Fix The Players grid's right-click menu no longer opens off the edge of the window. Right-clicking a cell or column header near the right or bottom edge put part of the menu out of reach. The menu now measures itself once it is on the page and opens leftward or upward from the pointer when it would not fit, and a menu taller than the window (a long category list) scrolls inside itself.
  • Change A desktop copy that has not published anything yet says it is ready, not that it is "Not publishing". The Connections card and the top bar used to greet a fresh install with "Not publishing" and a sentence about nothing being sent, which read as "not set up". They now say "Ready to publish" and tell the arbiter where to start: a tournament's Settings, then OpenResults. Other not-configured states keep their wording.
  • Fix Changing the backup passphrase no longer strands the backups written under the old one. Verify and restore tried only the current PAIRINGS_BACKUP_PASSPHRASE, so a rotation made every existing backup unreadable. They now also try each passphrase in the new PAIRINGS_BACKUP_PASSPHRASE_PREVIOUS (comma-separated), current one first; new backups are still encrypted with the current one only. Backups written before any passphrase was set keep verifying and restoring once one is - that already worked, and is now tested. docs/deployment.md explains the rotation, and that a lost passphrase means unreadable backups.
  • Feature Entrants' email addresses are cleared once their tournament is over. A daily job removes the email from pulled registrations of any tournament whose end date is at least PAIRINGS_REGISTRATION_RETENTION_DAYS (default 30) days past; name, decision and the rest of the entry stay. Tournaments with no end date, or one still to come, are left alone. docs/privacy-retention.md lists what personal data is kept and for how long.
  • Feature A results site low on storage is a wait, not a failure. The results site now refuses public-mode publishes and new tournaments with storage_low while its disk is nearly full. OpenPairings keeps everything queued, backs off at least as long as the site asks (five minutes), asks once for the whole queue rather than once per tournament, and says so in amber on the Connections panel, the top bar ("Waiting") and each tournament's Results site settings: "Results site low on storage - The results site is low on storage. Everything waiting is sent when it has room again." The first send that succeeds clears it. In Dutch too.

[0.60.1] - 2026-09-13

  • Fix After a restore, a refused publish says what probably happened instead of blaming "a different machine". A tournament first published after a backup comes back from the restore with no key; publishing it again mints a new one, and the results site refuses every update because it bound the address to the old one. The message said a different machine had published the tournament - in the restore drill it was this machine, before the restore. On a restored database, for a tournament that was already in the backup, publishing and taking down now say the tournament was probably published after the backup this installation was restored from, and that the results site's operator can move or remove it.

  • Fix A restore no longer puts a withdrawn tournament back online. A tournament taken off the results site after a backup still had its key, its address and publishing switched on in that backup, so after a restore its next change published it again - the restore drill measured 404 before and 200 after, and nobody was told. Every takedown - the Take down button, moving to a new address, deleting a tournament for good - now also appends a line to a journal kept beside the database and outside it (pairings_engine-takedowns.jsonl next to pairings_engine.db, openpairings-takedowns.jsonl in a desktop copy's data folder), which no backup contains and no restore touches. At every start, before anything can publish, a tournament that still holds exactly the claim a journalled takedown retired has publishing switched off again, its key and queued publish dropped, and an audit row ("Kept this tournament off the results site after a restore"); a tournament that was moved also gets a new address, so the old one cannot come back. A tournament its arbiter deliberately published again after the takedown is never touched. The journal holds tournament ids, withdrawn addresses, times and a short fingerprint of each retired key - never a key. It does not travel with a backup to another machine; copy it along if the whole installation moves.

  • Security A restore signs everybody out. A backup holds the sign-in sessions that existed when it was written, including ones that were ended since - signed out, or cut off by a password change - and the restore drill found such a session valid again in the restored database, for any browser still holding its cookie. Restoring a backup now empties every session, unused sign-in link and email-change link in the recovered copy before it can go live, so everyone signs in again. Passwords and roles are still the backup's: a password changed or a role removed after the backup is back as it was, and has to be applied again by hand - "What a restore undoes" in docs/deployment.md says how to find them. The recovered copy is also marked with when its backup was written and when it was restored.

  • Security Backups no longer carry the OpenResults operator token - and a restore now ends by setting it again. The token is the results site's master key: it publishes to, overwrites and deletes any tournament there, break-glass included. The restore drill found it in plain text in every production backup (the deploy sets no backup passphrase), and Connections hands a backup to any administrator who asks. It is now stripped from the copy before it is written, as a desktop installation's own results-site key already was; the publishing address stays in. This changes the restore procedure: until the token is set again a restored server publishes nothing. mix pairings.backup --restore prints the step - read OPENRESULTS_INGEST_TOKEN from the results site's unit into DEPLOY_PUBLISH_TOKEN and run mix pairings.publishing --ensure, as the deploy does - and a desktop copy that published with a token has it typed in again on Connections. mix pairings.publishing also stops logging its SQL, which showed that token in clear on the terminal of whoever ran it by hand.

  • Fix A server run refuses to start on a database that is behind the code, instead of serving pages that fail. The restore drill restored a backup one migration old, and the app booted, answered / with a 302 and the login page with a 200 - a health check would have passed - while every tournament page failed with no such column. The reason: migrations run at boot only in a release (the desktop and portable builds), and the hosted service is mix phx.server, where migrating is the deploy's own step. A production run that does not migrate now checks first, and when migrations are pending it logs how many and which, says to run mix ecto.migrate, and stops. A release still migrates at boot, so a restored backup there simply migrates. A database NEWER than the code (a rollback) is logged, not refused.

  • Change BACKUP_RETENTION is now a number of DAYS, not a number of files - and a restart no longer spends a backup. It was a count, every boot wrote a backup five minutes in, and every "take one now" another, so the default "30" was a month only on a box nobody restarted: a week of a few deploys a day was a week of backups. Now a backup is kept while it is younger than that many days (by the time in its own header), the newest is always kept whatever its age - a machine that was switched off for a season comes back with its last one - and the scheduler skips the boot-time backup when the newest on disk is less than a day old, scheduling the next one for when that one comes due. If you set BACKUP_RETENTION, read it again as days. A value that is not a whole number of at least 1 now stops the boot: 0 used to delete every backup, the one just written included, and a negative value deleted the newest and kept the oldest. Listing backups also reads each file's header instead of the whole file.

  • Fix mix pairings.backup --restore prints a swap that moves the WAL with the database, --verify reads every page, and neither leaves a decrypted copy of the database behind. What the restore drill found in the backup code itself, fixed the way OpenResults fixed its copy:

    • the printed swap renames pairings_engine.db, its -wal and its -shm together to ...before-restore-<time>, restores ownership and migrates before the start. The two mvs it printed before moved the database alone, and after a crash or a kill SQLite read the old WAL into the restored file: test/pairings_engine/backup_swap_test.exs runs the printed commands against a killed database's files, and against the old two mvs SQLite served the post-backup tournament under the restored file's name.
    • --verify (and so --restore) runs PRAGMA integrity_check and refuses a backup whose database has a damaged table, which it used to pass, and says what SQLite said in words rather than <<109, 97, ...>>.
    • verifying no longer leaves its staging copy - the whole database, decrypted for an encrypted backup - in the temp directory. On Windows it did after every verify (977 copies, 303 MB, on the drill's workstation), and on every system after a refusal.
    • the recovered file comes back already in WAL mode, so the first boot on it no longer logs database is locked.
    • --verify and --restore take a backup's name exactly as --list prints it.
  • Verified A backup has now been restored, end to end - and restoring one has a written procedure, which it never had. The first restore drill (docs/restore-drill-2026-09-13.md) rebuilt a working installation from a backup: every table back except the rating lists, which come back empty by design, the app booting on it, a round paired, and a tournament published before the backup withdrawn from the results site with the key the backup kept - the reason backups exist. It also found that following mix pairings.backup --restore's own printed commands could put the OLD database back live without a word: they move the database without its -wal file, which after a crash or a kill holds the newest writes and gets read into the restored file (fixed, above). docs/deployment.md now has "Backups", "Restoring a backup" (tested step by step: move the WAL with the database, restore ownership, migrate - a backup one migration old booted and failed every tournament page without it, and now refuses to start), "What a restore undoes", and "Restoring on a desktop install", which had no way at all.

  • Change What a restore undoes, written down. As the drill found it, a tournament taken off the results site after the backup was published again on its next change, and signed-out sessions were valid again - both fixed above. Still true after the fixes, and in the guide: a tournament first published after the backup can no longer be updated from here, passwords and roles go back to what they were, and the rating lists are empty until a sync. The guide carries a tested script that carries each tournament's publishing state forward from the database the restore replaced, when that database still opens, and a report of everything else to re-apply.

  • Fix Pulling entry-form registrations words a refusal by the server's own error code, like publishing already does. PairingsEngine. Registrations still dispatched on the HTTP status alone - a 403 with installation_suspended, installation_revoked or not_owner all fell back to a bare "the server answered 403: <code>" - even though PairingsEngine.Publishing was refactored two days ago to read the JSON error code the same way PairingsEngineWeb.Components.ConnectionStatus does. Now reuses Publishing.rejection_of/1 and Failure.effective_code/1 to decode the body rather than re-parsing it, and gets its own sentence for each: not_owner names the other installation (the same words Publishing.take_down_words/2 already uses for it), installation_ suspended/installation_revoked say the key was suspended or is no longer accepted, and key_mismatch/key_required/a bare 403 keep the "a different machine published it" sentence they already had. Pulled in public mode too (a per-installation key rather than a token), which gets its own "does not recognise this computer's key" wording for a rejected credential, same as publishing's.

  • Fix The Connections panel's headline no longer contradicts the sentence under it. headline/1 picked "Token refused" for almost every refusal (only "Publishing paused" had its own wording), whatever the server actually said - so a 200 from some other website read "Token refused" above "which is not an OpenResults server", and a suspended key, a hidden tournament, a blocked address or a different installation owning the tournament all read "Token refused" too, in operator mode, or a blanket "Refused by the results site" in public mode. Every code reason_sentence/1 already has a sentence for now gets its own matching headline instead ("Owned by another installation", "Key suspended", "Key revoked", "Tournament limit reached", "Tournament too large", "Tournament hidden", "Registration closed", "Address blocked", "Waiting a moment", and "Key not recognised" for public mode's own unrecognised-key wording); "Not an OpenResults server" for a server that answered with no error code at all, which was the reported case. New Dutch msgstrs throughout. Found by checking every {state, reason} pairing, as asked - one more of the same bug was already pinned as expected behaviour in public_publishing_live_test.exs (an installation-revoked key), fixed alongside it.

  • Security The public /tools/norms page no longer re-serialises every uploaded file on every officials-form keystroke. update_fields and its siblings (picking an arbiter from search results, adding or removing one, choosing an FA1/IA1 candidate) synced the entire session into PairingsEngine.Tools.Session on each call - every uploaded file's parsed tournament and full player list included, not just the small overlay/candidate map that actually changed - on a page with no login and no rate limit on this path. Session.put/3 runs :erlang.external_size/1 over the whole term and deep-copies it into a shared ETS table, so the cost scaled with everything uploaded so far, on every keystroke. Measured on a real 5,000-player upload (5.48 MB session payload), averaged over 200 calls: re-syncing the whole session this way cost ~7.5 ms; the fields that actually change now sync through a new small "patch" entry (Session.put_patch/2) that Session.get/1 merges back on for the reader (the download route included), costing ~8.5 µs - about 880x less, and no longer proportional to what has been uploaded. The officials form also gained the same 300ms phx-debounce the signed-in Norms page's already has, so a keystroke does not even reach the LiveView until typing pauses. A reload or reconnect still restores exactly what it restored before - the merge is covered by its own test, alongside one pinning that N field edits never rewrite the stored file payload.

  • Fix A production server without PHX_HOST now refuses to start instead of quietly using example.com for every emailed link - login, invitations, email changes - and the Keycloak callback. Guarded like the SMTP check: only the running server (PHX_SERVER) enforces it, so mix ecto.migrate and other build tasks still run without it, and the desktop app keeps localhost.

  • Fix A collaborator invite that failed to send was still reported to the tournament owner as sent. add_collaborator/3 called the mailer and discarded what it returned, always setting mail_status: :sent - so the Team panel's own "share this link manually" fallback for a failed send could never actually show, because an SMTP failure comes back from Swoosh as {:error, reason}, not an exception, and nothing looked at it. It now does, so a real send failure shows the manual-share notice instead of a silent, false "sent".

  • Fix Account emails were always sent in English, regardless of the arbiter's chosen language. The magic-link, account-confirmation, email-change, and collaborator-invitation emails had no translation in them at all. They now render in whichever locale the person taking the action - logging in, registering, changing their email, or inviting a collaborator - already has selected for their own screen. An invitation is worded in the INVITING arbiter's language, since the invitee may not have an OpenPairings account yet to have a language of their own.

  • Fix A failed magic-link resend or email-change confirmation vanished without a trace. Both call sites sent the mail and ignored what came back, so an SMTP hiccup left the person waiting for an email that was never coming, with no log line and no different flash to say so (registration's own equivalent already logged this correctly). Both now log the failure; the email-change confirmation, which is already the logged-in user's own account, now tells them too. The log-in form's resend keeps its one message either way on purpose - it must not become a second, quieter way to learn whether an address exists.

  • Fix A local/desktop build (OPENPAIRINGS_LOCAL=1, or the Burrito app) gave no sign that a login or invite email had gone anywhere. That build prints the email to its own terminal instead of sending it - there is no mail server on a single-user machine - but the log-in page's notice about local mail only recognised the dev preview mailbox, not this adapter, so it said nothing for a second account on the same machine, an invited collaborator, or a password reset. It now names the terminal instead.

  • Feature The desktop app publishes to openresults.zerotwo.cloud without a token. On your own computer, with no token configured, the results site's address now defaults to https://openresults.zerotwo.cloud, and turning publishing on for a tournament is all it takes. The first time, the app asks the results site who runs it and shows you once: who operates the site, a link to its terms when it has them, and what will be published. Say no and nothing is sent and publishing stays off; say yes and this computer gets a key of its own from the results site, which creates the tournament's address, and the tournament is published as before. Nothing at all is sent to the results site - no connection check, no question - until you turn publishing on for a tournament. The hosted service is unchanged and never does this: it still needs a token from its operator, and a token you enter on the desktop takes over from the computer's own key the moment it is saved.

  • Feature No link or QR code until the first copy has arrived on the results site. In this mode the results site picks each tournament's address, and until a publish under it has succeeded the site answers that address like one that does not exist. So the share link, the "Public page" buttons on Pairings and Standings, the entry-form link and the spectator QR code on the local view all stay hidden until the first copy has arrived - not merely until the address is created, because a first publish refused in between (too large, paused, the connection dropping) would have left a dead link on screen or on paper. An open page shows the link the moment it arrives. The Results site settings page says what the tournament is waiting for instead: your go-ahead, registering this computer, the first copy, or changes being sent. "Move to a new address" asks the results site for a new address and removes the copy at the old one. An address that never received a copy and that the results site has since let go (it does after 30 days) is replaced with a new one without interrupting you - nobody was ever given a link to it. An address belongs to the results site that created it: pointing this computer at a different one gives a tournament a new address there, after you agree to publish on that site, with no link in the meantime. A tournament already published with a token keeps its address and its link if the token is later removed, rather than being moved and leaving the old copy up with nothing here able to take it down.

  • Feature The results site's refusals are explained, in English and Dutch. Paused publishing (amber), a suspended or revoked key, a blocked network address, the limit on tournaments (with the number), a tournament too large to accept (with the size), a tournament owned by another installation or hidden by the site's operator (red) each get their own sentence on Connections, in the top-bar indicator and on the tournament's Results site settings page. A rate limit is honoured quietly, waiting at least as long as the site asks, and a refusal that applies to every tournament - offline, paused, rate limited, not registering - is asked once per round of sending rather than once per waiting tournament. When a pause ends or registration reopens, what was waiting goes out without waiting for its next retry. What only the arbiter can resolve stops instead of retrying: "Try again" sends it again, and a key the site no longer accepts offers "Register again", which asks you again first - the app never registers again on its own. A tournament owned by another installation (after a hand-off, or a rebuilt laptop) names its address and this computer's installation, which is what the site's operator needs to move it. Fetching entries from the results site is refused in the same terms: a 403 now says which refusal it was instead of always blaming "a different machine", and a blocked network address can still fetch.

  • Security This computer's publishing key never leaves it. The key the results site gives a desktop copy is left out of every backup - deleted from the copy before it is written, so a backup restored on another computer does not make that computer this installation - and it is in no export, hand-off file, restore point or TRF, is never shown after it is stored, and never reaches a log line or the audit trail. It is also only ever sent to the results site that issued it: pointing the app at another address stops using it. Tournament keys are still carried in backups as before, so a rebuilt laptop can still manage what it published; after any restore - onto the same computer too - this computer registers again, and the site's operator moves all its tournaments across in one step.

  • Verified The one 2026-09-05 audit finding that was never judged ("the public officials form re-serialises the whole session on every keystroke") turned out to be two pages. The signed-in Norms page's officials form was already fixed three days before the audit ran - 632d77a and 319b0e8 (2026-09-02) memoised the roster sort/IT3 counts out of the template and debounced the form, so officials_change is now assign(socket, dirty: true) on an assign the template never reads. Re-verified directly: a 200-player tournament costs the same 397 VM reductions per keystroke as a 5-player one, touches no database query, and broadcasts nothing - no code change, one more test pinning it. The actual public, no-login /tools/norms page has its own, still-open version of the same shape: its officials card carries no debounce at all, and every keystroke re-syncs the entire upload session (every parsed file, not just the changed field) into PairingsEngine.Tools.Session, measured to cost linearly in the total uploaded data (10 µs empty, 5.9 ms at 4.6 MB uploaded). Real, and out of scope for this pass - see docs/audit-2026-09-05.md's "One finding was never judged" for the numbers and a follow-up flagged separately.

  • Fix The audit trail is in Dutch for a Dutch arbiter - the rows, not just the page around them. Every line on the Audit page ("Registered player…", "Entered result 1-0 on board 4 (round 2)…", every settings change, restore, import and role change), and the same lines on the Admin page's recent activity and the History timeline, stayed English whatever language was picked, because they were assembled by hand where no translation pass could see them. They are now worded when the page is shown, so rows written by earlier versions read in Dutch too. Shown as stored, in either language: action codes, the field names in a settings diff (rounds_count), result notation, engine names, a restore point's own name and the SWAR upload's error message. The category filter buttons, the "System" actor and the tab title are translated as well. The English wording is unchanged except where a sentence had to be rebuilt to translate: a phone's result now ends "Via the phone "Tafel 3" (Deputy)." instead of "(via phone, "Tafel 3", deputy)", and a new tournament names its system as "(Swiss)" rather than "(swiss)".

  • Fix Saving the officials on the Norms page no longer makes the Audit page crash. The settings diff printed each old and new value with to_string/1, which raises for a map, so one officials save (or any other map-valued setting) left that tournament's audit trail unopenable. The same formatter printed a tie-break list as "BHSB" and a switch as "false → true"; they now read "BH, SB" and "Off → On". Two older row shapes read better too: a result blanked before 2026-08-03 said "changed from 1-0 to" and now reads as a cleared result, and a role change names the roles as the Admin page does ("from Account owner to Administrator").

  • Fix Thirty-four kinds of audit row said what they were instead of what happened. Hand edits to a paired round, publishing and taking down pairings and standings, hiding an empty board, all four hand-off steps, the results site settings, the bulk edits on the Players grid, the club refresh, category rules and assignment, and recomputing the pairing rationale all showed an internal code - pairing.players_swapped, handoff.returned - on the Audit page, the History page and the Admin page's activity list, in English and Dutch alike. Each now reads as a sentence in the arbiter's language, rows written by earlier versions included, and says what an incident needs: "Took the pairings of round 4 and every later round off the public page. Public standings now go no further than after round 3." Round 0 reads as the initial standings. A hand edit names its round, but those rows never stored which players moved - only the confirmation's own English line - so that line follows the sentence in quotes: Recorded as "Chris Maes takes Bram Claes's place". The rows 0.59.0 wrote for its "Publish the starting rank before round 1" switch read as a sentence too.

  • Fix The Audit page's category filters missed every one of the 35 codes above, so they only ever showed up under "All". Filtering by "Pairings" hid hand edits to a paired round, board hiding, and publishing or unpublishing pairings and standings - exactly what an arbiter filters for after an incident. Pairing edits, board visibility, pairings/standings publishing and the pairing-rationale actions now join Pairings; standings publishing and the 0.59.0 starting-rank switch join Standings; the results site (openresults.*, the SWAR publish actions, the public link rotation, the public registration toggle) and the categories feature's toggles, rules save and auto-assign join Settings, next to the category codes already there; the bulk Players-grid edits join Players; the four hand-off steps join Tournament, next to created/deleted/restored. A restore point still fits none of them - it can touch anything a row in any other bucket can - and stays reachable only under "All".

  • Fix The publishing connection panel no longer prints an English sentence under a Dutch heading. The panel on Connections, the status pill's panel and tooltip in the top bar, and the answer to Test connection all showed the connection check's own English sentence - "Verbonden" above "Connected. The address and token are both accepted." - because the check returned a finished sentence and the panel could only strip the headline off the front of it in English. The check now returns what happened as data, and the panel words it in the arbiter's language. That also closes a quieter fault behind it: whether a failure showed amber ("the server answered, but refused the token") or red ("cannot reach the results site") was decided by whether the English sentence began with "Reached", so translating it would have turned every wrong token into a red light. The English wording is unchanged apart from three details: a server that is not OpenResults now reads "Reached the server and it answered 200, which is not an OpenResults server." instead of naming the internal probe address; the network failures are full sentences ("The connection timed out."); and an answer from OpenResults carrying an error code this version has no words for shows that code ("Could not confirm the connection (403 installation_revoked).") rather than calling it "not an OpenResults server". The top-bar panel also updates when the reason changes but the colour does not - it kept saying "No address is set." after the address was saved and the token turned out to be missing too.

  • Change A refusal from the results site is worded by the error code it sends, not by its HTTP status alone. OpenResults names every refusal (key_mismatch, unauthorized, ...) and two answers with the same status can mean different things. Publishing and taking a tournament down still say "a different machine published this tournament" for key_mismatch, key_required, and a bare 403 from an older server; a 403 carrying any other code now says "the server answered 403:" and the code, instead of sending the arbiter to look for another machine.

  • Fix Pairing a whole round-robin schedule recognised "finished" by the first word of an English sentence. The loop behind the pair-everything button stopped successfully on any error that began with "All " - so a future refusal worded that way would have been reported as a fully paired schedule, and translating the real one would have turned finishing into an error. It now stops on the reason itself. The sentence an arbiter sees when every round is already paired, from any pairing system, is now translated, and a one-round event says "The only round has already been paired" rather than "All 1 rounds have already been paired".

  • Change The mobile enrolment pages are English, like the result entry they lead to. /m, a scanned QR link and "leave" rendered their card in English with the error line in the arbiter's language - "Die code is onjuist of is verlopen." inside an otherwise English page - and then handed the helper to an all-English result entry page. Player-facing pages are English; these are now too. The arbiter's own language choice is left exactly as it was, so a phone that is also the arbiter's still shows their admin screens in Dutch.

  • Fix A printed Keizer standings table has its "Keizer points" header in Dutch too. It was the one English word in "Waarde | Keizer pts | Score", in the main table and in every category table; the on-screen standings already said "Keizer-ptn".

  • Fix Remembered column choices on the Players grid and the Standings page could silently fail to load or save, in a way that could also break the rest of the hook. ColumnPrefs (assets/js/app.js) read and wrote localStorage directly; every other localStorage access in the app (the theme bootstrap, the version toast) already wraps it in try/catch because getItem/setItem throw, not return null, when a browser blocks or partitions storage - a private window in older Safari, an embedded frame, or site data disabled outright. ColumnPrefs is the one spot that was not guarded; both calls now are.

  • Fix "roughly 4%% of rounds" now reads "roughly 4%", in English as well as in Dutch. The engine comparison under Settings > Options and the body of the "Switch to JaVaFo?" confirmation both wrote the percent sign doubled, on the assumption that gettext undoes it the way a printf format does. It does not: Gettext.Interpolation.Default replaces %{name} and passes every other byte through, so both sentences showed two percent signs to everyone, in the one paragraph an arbiter reads while deciding which edition of the pairing rules their tournament follows. The test suite now refuses %% anywhere in the catalogues.

  • Fix Thirteen Dutch translation defects, from the first audit of the catalogue as prose (docs/translations-audit-2026-09-12.md). The ones that misinformed rather than merely read oddly: the Norms page's red bar told the arbiter to fill in a card called "Functionarissen & FIDE-rapport- gegevens", while the card is headed "Officials & FIDE-rapportgegevens"; the Players page's live-rank tooltip said "zelfde als Pl" about a column headed Cl; the standings header W-We - FIDE's own symbol for score minus expected score - was translated to W-Wv, right beside a We column that is not translated at all; a pairing sheet's absentee list said "bye van een halve punt" where a point is neuter in Dutch ("een half punt"); and one sentence on Settings > FIDE had its subject and object swapped, so it read "a rated event may run its arbiter". Also: "resultatensite" in four places where the other thirty-four say "uitslagensite", "ronde-robin" where the rest say "rondetoernooi", three different Dutch names for the starting-rank number, and eleven strings that addressed the arbiter as "u" in a catalogue that says "je" everywhere else.

  • Fix A first-boot migration could still crash the app on the slowest CI runner, even after the earlier "two connections race for the WAL lock" fix. That fix (pool_size: 1 for the throwaway migration connection) closed the multi-connection race, but left a second, subtler one: on a loaded macOS runner, that single connection's first-ever connect/1 could simply take longer than DBConnection's default queue patience (queue_target/queue_interval, 50ms/2000ms), which then dropped the very first migration query - "connection not available and request was dropped from queue after 4000ms" - before the connection had even finished connecting. PairingsEngine.Repo now gives that connection the same patience busy_timeout already does (config/runtime.exs, config/dev.exs), and first boot retries a connection-availability failure up to three times with a fresh connection before giving up - a genuine migration error still crashes immediately, on the first attempt, with its own message.

  • Fix The category selector on the standings page did nothing, and so did the arbiter picker on the norms page. Both were a drop-down that announced its own changes, which a browser never sends: the change has to be announced by the form around it. Every test passed because a test raises the event directly on the drop-down. There is now a check that fails the build if any drop-down is wired that way again.

  • Change "Standings after round 0" is now called "Initial standings". The control publishes the field as entered, before a game has been played, and naming that "after round 0" read like a bug report.

  • Change The FIDE and KBSB sync buttons no longer warn just because a tournament has an unfinished round. "Paired, not yet fully scored" is true for most of a season on a real club installation, so the confirmation fired on almost every press and trained the maintainer to click through it without reading it. It now asks only when a result was actually entered, changed or cleared somewhere in the last two minutes - measured locally, a FIDE sync's write lock is a genuine multi-second hold (the FTS index's bulk delete and rebuild alone measured 9.3s and 11.5s against a synthetic 1.9M-row table, since FTS5 has no fast "drop everything" path the plain table's own delete does), so the risk is real; "this tournament has a round in progress somewhere" was simply never the right way to detect it. The warning, when it does appear, now names the tournament and says why: "Bruges Open just had a result entered. A sync can lock the database for several seconds, so the next one could be delayed. Press again to sync anyway." Nothing was ever actually lost either way - PairingsEngine. BusyWrite already turns a lock collision into a clean, retryable error - this only changes when the person is asked to decide.

  • Fix A version bump no longer dirties the OpenResults checkout. The contract test that regenerates test/fixtures/snapshot_swiss.json and snapshot_keizer.json in the sibling repository stamped both with THIS checkout's own dev version (source.version, from Build.id/0), so every release rewrote both files whether or not the snapshot contract itself had changed - which left the sibling checkout dirty right after a deploy stamped its own build -dirty from exactly that, and twice left a stale hardcoded assertion failing over there instead. The fixture writer now pins source.version to a fixed placeholder (0.0.0-fixture) before writing - the same treatment published_at already got, and for the same reason. The real value Snapshot.build/1 sends in production is unaffected, and the envelope test's is_binary/1 assertion does not care what a placeholder says.

  • Fix A FIDE sync no longer holds the database's one write lock for several seconds. do_import_list/4's replace of the ~1.9M-row rating list already committed in chunks, but the search index (fide_players_fts) was rebuilt with two single, unchunked statements - DELETE FROM fide_players_fts and the INSERT ... SELECT that refilled it - measured at 9.3s and 11.5s against a synthetic 1.9M-row table, ~21s combined, because FTS5 has no fast "drop everything" path the plain table's own DELETE does. That was the actual multi-second stall a sync confirmation on this page existed to warn an arbiter about. It now builds a fresh index in the same 2000-row chunks as the player rows themselves, while the live index keeps answering searches unchanged throughout, and swaps the new one in with an atomic rename once it's complete - measured locally at the same 1.9M-row scale, every chunk stayed under 100ms, the rename swap itself took 13ms, and the longest single lock left anywhere in a FIDE sync is now the same sub-250ms DELETE FROM fide_players that was already there. A run killed mid-build or mid-cleanup leaves at most an inert leftover table, cleaned up by the next sync - never a broken or half-built search index. With the real stall gone, neither the FIDE sync nor the KBSB one (already sub-second) asks for confirmation before starting any more.

  • Fix Entering results with the arrow keys no longer records the results you pass on the way. On Windows a closed result dropdown changed - and saved - on every arrow press, so going from 1-0 to 0-1 recorded ½-½ in between, with an audit row, and passing the empty option opened "clear this result?" under the keyboard. The choice is now sent once, on Enter or when you move on; Escape puts the recorded result back. The 1/2/3 keys and the mouse are unchanged. Clearing a result puts focus on Cancel, and closing the question either way puts focus back on that board, where it used to drop to the top of the page. Each result box is named for its board and players for a screen reader. First accessibility pass, 2026-09-13 (docs/accessibility-2026-09-13.md).

  • Fix Every dialog takes focus, keeps it, and gives it back. The Players Card, player registration, rating refresh, club update, hand-edit confirmation, category preview, JaVaFo switch, norm judgment, restore point, hand-off, delete and publishing consent now move the keyboard into themselves, keep Tab inside, and return focus to what opened them when they close. A screen reader hears each as a named dialog.

  • Fix A screen reader hears what the page says. Flash messages - including the one that arrives with a new page, like "You now have access to ..." - the "Saved." and "could not save" notes on every settings page, the server-update banner (when it appears and when it escalates, not every second), the site notice and "Updated to" are announced, politely. On the phone, "Board 3: 1-0 saved" is.

  • Feature The right-click menus have a keyboard way in. On the Players page the column headers are buttons (Enter sorts) and the context-menu key or Shift+F10 on Pr., Paid or Cat. opens that column's menu with focus in it; Enter or Space on a player's name opens their registration, the context-menu key their Players Card. On the Pairings page the context-menu key on a print link opens its variants. The per-player cell menus and the hand-edit menu still need a mouse (see the report's recommendations).

  • Feature "Skip to content", the first thing a Tab reaches on every page, and a visible focus ring on everything the keyboard reaches, in every theme. The current tab, round, theme, accent and language say so to a screen reader, not only by colour.

  • Fix Legible in every theme. Secondary text in Slate was 2-3:1 and is 4.5:1 now; Mocha's secondary text and six of its accents, the warning colour in Light, Paper and Board, Board's success and secondary text, the blue and teal accents on light themes, the account pages' button and flash colours, the version number, and two lines on the sign-in page were all short of 4.5:1. Each colour moved only as far as needed, hue kept - every change is listed with its ratios in the report. Text boxes and dropdowns have a visible edge (they had a near-invisible hairline), and a field with an error is edged in red.

  • Fix Colour is no longer the only signal. Below 860px the publishing indicator keeps a shape per state and its word for a screen reader; prize places, the rationale page's colour-history squares and a locked publish switch's reason are also in words.

  • Fix The phone keeps your place. When an entered board leaves the list, focus moves to the board that takes its place instead of the top of the page, and the chosen result says "pressed".

  • Feature The projector view has a Pause button. The boards paged on their own and only a tap stopped them; "Pause cycling" does it from the keyboard.

  • Change Theme and accent names are translated, and the scrolls the page makes for you - on the bracket map, to the next board after a result - jump instead of gliding when the system asks for less motion.

[0.60.0] - 2026-09-12

  • Change The installer file names now carry the version. A download sitting in someone's Downloads folder used to say nothing about which release it was: OpenPairings-win-Setup.msi, OpenPairings-win-Setup.exe, openpairings_macos_aarch64.dmg and openpairings_macos_x86_64.dmg are now OpenPairings-<version>-win-Setup.msi, OpenPairings-<version>-win-Setup.exe, OpenPairings-<version>-macos-aarch64.dmg and OpenPairings-<version>-macos-x86_64.dmg (e.g. OpenPairings-0.59.0-win-Setup.msi). The portable zips and the bare Linux/macOS binaries are unaffected - only the four installers above changed. The in-app updater's own files did NOT change and deliberately keep the Velopack pack id instead of this friendly name: releases.win.json, RELEASES and every OpenPairingsApp-<version>-full/delta.nupkg keep the exact names an already-installed copy looks for, because Velopack's updater finds its updates by those names, not by browsing a folder - renaming them would break every install already out there. One consequence: a download link that wants "always the newest installer" can no longer point at a fixed file name, because that name now changes every release. Point it at https://github.com/AuroraRyunix/openpairings/releases/latest instead, or look the asset up through the GitHub releases API.
  • Fix A TRF-2026 file whose points table declares X imports instead of being refused. FIDE's 2026 report format lets record 162 say what an unknown result is worth (X); the engine used to reject any file that did. Ainalrami v0.26.1 reads it, and scores an unknown result (?) with it when
    • and only when - the file itself declares it.
  • Fix Before round 1, the public standings page lists the players instead of staying blank. The public site knows every player by their start number, and start numbers are only handed out when round 1 is paired - so until then there was nobody it could show, whatever the "before round 1" setting said. The field is now numbered provisionally, in the order pairing round 1 will number it, so the numbers normally stay the same afterwards.
  • Feature Pairings and standings now publish on two independent, cumulative tracks, each with its own green/red toggle. The Pairings page shows "Pairings round N" and "Standings after round N" for the round being viewed (in both the page header and the round's right-click menu); the Standings page shows "Standings after round K" beside Public page, for the latest complete round K (K = 0 is the entry list, before round 1 has results). Green means public - click to unpublish, with a confirmation naming what else that hides; red means not yet public - click to publish, disabled with a reason when the round isn't ready (not yet paired, not yet complete, or its own pairings aren't public yet). In "immediate" publish mode both controls are shown locked (green, permanently on), since every round - and the standings behind it - is already public the instant it's paired.
  • Change Publishing a round's pairings can now put its own before-it standings out ahead of any explicit click. A pairing sheet for round N shows every player's score BEFORE round N, so publishing round N always means standings through at least round N-1 are already public, whether or not anyone published them on purpose - no separate step, and no background job, for a "timed" or "scheduled" round crossing its own publish instant either. Unpublishing either pairings or standings cascades the other way: taking a round's pairings back down also pulls public standings back to before it, and pulling standings back also hides any later round whose sheet would otherwise disclose them right back.
  • Change The "Before round 1: public" flag is gone, replaced by the "Standings after round K" control's round 0. It was a single before/after on-off switch that vanished the moment round 1 was paired at all - the new control instead tracks whichever round is actually the latest complete one, works the same way before and after round 1, and every existing tournament keeps showing the public exactly what it showed before this shipped.

[0.59.0] - 2026-09-11

  • Change Public standings now wait for a complete round, not just a published one. Publishing round 1's pairings used to be enough on its own for the public page to say "Standings after round 1" with every player on zero - a published round counted toward the standings even with no results in it. The public after_round bound is now the longest contiguous prefix of rounds that are both published AND complete (every board has a result): the results site shows the starting rank until round 1's results are all in, and while a later round is still being entered the public standings stay at the last fully-entered one instead of jumping ahead with holes in it. Published rounds themselves are unaffected - a round with results still coming in still shows its pairings, exactly as before.
  • Feature An arbiter can now choose whether the starting rank publishes before round 1. OpenResults shows the entry list, ordered by start number, on a tournament's public page whenever there are no standings to show yet. "Publish the starting rank before round 1", on the Standings page while the tournament has not paired a round, turns that off - the roster stays off the public page until round 1 has results, the same way a hidden round or a hidden board already stays off it. Defaults to on, so every existing tournament keeps behaving exactly as it does today; the moment any round publishes, the roster travels regardless of this setting, because by then the same players are already named on the public boards.
  • Feature A category can now combine rating, age and sex instead of picking one SWAR-style threshold. Each category's rule is any combination of five conditions - rating from, rating below, age from, under age, women only - and a player qualifies whenever every condition set is true for them. That makes nested prizes (U1800 and U1600 both), bands (set both a rating floor and ceiling on one category), and combined categories (rating below 1800 AND age 45+) all expressible directly, where SWAR's non-overlapping single-threshold categories could not express any of the three. Age is counted the FIDE way - on 1 January of the tournament's year, not "years since birth" - and the Categories editor shows the equivalent birth-year range live next to every age input so the arbiter can see exactly who qualifies before saving. Existing tournaments' categories are converted to the new shape automatically, on upgrade and on importing an older backup, with the exact same auto-assign results they had before.
  • Feature An optional prize count per category now highlights the places that are actually prizes. Set how many prizes a category has on the Categories page, and the standings page's Category column shows each player's place within every category they're in, with prize places picked out visibly. A category selector above the table narrows the view to one category at a time - "U1800 - 3 prizes" - numbered 1..n in that category alone, with the choice kept in the page's link so it can be shared or bookmarked. The printed per-category standings now number players the same way, in place of the overall rank they used to show.
  • Feature The OpenPairings window says which version it is running, and which Ainalrami. The small window that starts OpenPairings on Windows now shows both versions under its buttons, and the OpenPairings version in its title bar - read from the installed files, so they are there even when the server does not start. On macOS and Linux the same versions are printed when OpenPairings starts.
  • Fix The Windows installer's wizard looks the way it was drawn. The picture meant for the welcome and finish pages was squashed into the strip across the top of every other page, and that strip was stretched down the welcome page: Velopack fills those two slots the other way round from what its own help says. The welcome and finish text also broke mid-sentence and ran out of room; it is now plain paragraphs the page wraps itself. The build now checks that each picture is in its own slot and stops if not.

[0.58.2] - 2026-09-11

  • Verified Install and restart, tried for real. Nothing in the program changed. 0.58.1 is the first release with every installer and update package on its release page; this release exists to give an installed 0.58.1 a genuine update to offer, so "Install and restart" can be tried end to end from the user's side.

[0.58.1] - 2026-09-11

  • Fix The release 0.58.0 should have been. 0.58.0's release page went out without its Windows installers (the MSI and Setup.exe), without the update package that "Install and restart" updates from, and without any of the portable zips. The steps that build them ran only for tagged builds, and 0.58.0 was released automatically from main. Those steps now run for every build that can publish a release. Nothing in the program itself changed. 0.58.0 is marked as a pre-release, so no installed copy is told about it.

[0.58.0] - 2026-09-11

  • Fix A fresh install's first launch no longer races itself for the database. On the very first boot there is no database file yet, and every connection in the app's pool tried to switch the new file into SQLite's WAL mode at once - a change that needs an exclusive lock, attempted before the library had been told to wait for one. All but one connection were refused on the spot, the setup that runs before the app starts waited on a pool with nothing to give, and after four seconds the program gave up without opening. Reproduced on an ordinary SSD in 2 fresh boots of 5; a slower machine makes it likelier. Setup now runs through one throwaway connection before the pool exists, so the file is created and switched exactly once, and the pool only ever confirms a mode already set.

  • Feature A per-user Velopack install can now apply an update from inside the app - "Install and restart", confirmed, not a follow-up anymore. 0.57.0's notice offered every install kind a link and nothing else; a :velopack_per_user install (%LOCALAPPDATA%\OpenPairingsApp) now gets a real button too, when rel/windows/launcher.c says it can.

    Neither side crosses into the other's territory to make this work. This application still never touches Velopack: clicking the button and confirming (a plain browser confirm, repeating the in-progress-round caveat when one applies) calls PairingsEngine.Updates.request_install_and_restart/0, which shuts the BEAM down cleanly - System.stop/1, the same graceful shutdown an ordinary stop already uses, so every Ecto/SQLite connection is closed before anything else happens - with a dedicated exit code. OpenPairings.exe, which has watched its child process for the server's whole lifetime since this release (not only during startup, as before), sees that exact code and knows this is the update signal rather than a crash. Only then does it load velopack_libc.dll with LoadLibrary/GetProcAddress - never linked, and never touched at ordinary startup, so an arbiter who never clicks the button never pays for it - check for an update against this same repository, download it, and hand off to Velopack's own apply-and-restart, which relaunches OpenPairings.exe fresh once it is done.

    Every failure - offline, nothing newer, a download error, an apply error, or the DLL missing or blocked (antivirus quarantine is the expected case) - is treated identically: say so in the launcher's small window, then start the server again on the version already on disk. An arbiter must never be left without a running program because an update attempt did not work out, and the button only ever appears at all when both :velopack_per_user and the launcher's own DLL-presence check agree - a per-machine install gets the same DLL in its payload today but never sees the button, because updating there needs an administrator regardless (see 0.57.0's entry).

    Packaging: velopack_libc.dll travels in the portable payload next to OpenPairings.exe, fetched and SHA256-checksummed by CI from Velopack's own pinned 1.2.0 release asset (matching the vpk pin already in place) rather than committed - see docs/binaries.md's "Updates" section for the manual test plan, and its "The Windows installers and the update feed" section for the packaging detail and a CI trap this also fixed: a re-run of an already-published tag's release build no longer fails packing over its own delta-diff download.

    An antivirus re-test is due on this release - 0.57.0's entry noted Symantec Endpoint Protection passes the portable release and an unsigned Velopack Setup.exe; this is the first release to ship a native DLL beside the launcher, and that shape has not been checked against a scanner yet.

[0.57.0] - 2026-09-11

  • Feature Desktop builds check for a newer OpenPairings release and say so - never anything more. Once a day at most (a check on start, then every six hours), PairingsEngine.Updates asks GitHub Releases for AuroraRyunix/openpairings's newest non-draft, non-prerelease tag and compares it to this build's own version. Finding a newer one puts a non-modal notice at the top of every page - the version, a link to the release, and, if any tournament currently has a round paired but unfinished, a plain warning next to the install action saying so. Nothing is ever downloaded or applied automatically: an update can change Ainalrami's version, and a tournament locks the engine's name, not its version, so applying one under a running event could change the pairing algorithm mid-tournament. Applying is always the arbiter's own act, from the release page this links to.

    Never on the hosted server, by construction rather than by a flag left off: eligibility reads PairingsEngine.Authz.local_mode?/0, the same signal the rest of the app already uses to tell a desktop install from openpairings.zerotwo.cloud, checked once before the checker's timer is even scheduled, again before every tick, and a third time by the notice itself - so a hosted server never so much as sends itself a message that could reach GitHub. A setting to turn the check off entirely (default on) lives on the Connections page, next to everything else this machine talks to - because it contacts a third party, and an arbiter must be able to stop that without needing a reason.

    The notice's install action depends on how the running copy got there (PairingsEngine.Updates.InstallKind, pure filesystem detection - no Velopack library touched to answer it): a per-user Velopack install (%LOCALAPPDATA%\OpenPairingsApp) is told the installer updates it in place; a per-machine one (Program Files\OpenPairingsApp) is told plainly that an administrator is needed; the portable zip, the single-file Burrito binary, macOS and Linux all get a plain link to the release page. An actual in-app "Install and restart" was researched rather than assumed away: Velopack's native C ABI (velopack_libc) was confirmed, during this work, to link and run correctly against this project's own toolchain (zig cc -target x86_64-windows-gnu) - so the mechanism is not the obstacle. What blocks it is that the call has to be made by the process Velopack's own apply-and-restart is built around, OpenPairings.exe (rel/windows/launcher.c), not by the Phoenix/LiveView process a browser click reaches - which is a child of that launcher's job object. Wiring a signal across that boundary, with no second real release yet to update FROM and no way to exercise a live Windows install end to end here, was judged exactly the kind of Velopack-internals guesswork this feature was asked not to do speculatively - so it stops at notify-and-link for every install type, and a real "Install and restart" is left as a follow-up for rel/windows/launcher.c once there is a release to test it against.

  • Fix The macOS application reports the version it actually is. The .app bundle's Info.plist carried a hardcoded 0.18.0, so Finder's Get Info, Spotlight and any crash report described this 0.56 application as 0.18.0 - on the one platform where the version is not written anywhere else the user can see. The build now reads it from mix.exs, the same single source of truth the release job and the Windows installer already read, because a second copy of a number is the whole bug: nothing in a build reads Info.plist back, so no step could ever have noticed it was stale.

    mix pairings.version_check now guards the class rather than the instance. For the three prose documents it still asserts they name mix.exs's version; for a file that is supposed to derive it, the test is inverted - finding a literal version in the workflow's Info.plist block is itself the failure, because finding one means somebody hardcoded it again. That check runs in mix precommit, so the next attempt fails before it can ship 37 more releases telling macOS the wrong number.

  • Change A Windows launcher that fails to build no longer takes the whole release with it. mix release pairings_engine_portable used to raise when Zig was present and rel/windows/build_launcher.ps1 failed. It now prints a loud warning that names the file that will not exist (OpenPairings.exe) and what will refuse it, and finishes the release. The reason is a version fork rather than a broken build: CI pins Zig 0.15.2 because Burrito requires exactly that, a developer machine carries whatever is current, and zig rc's argument handling is exactly the kind of surface that moves between the two - so a build could die over a 100 KB C stub while the portable release it belongs to was complete and working through OpenPairings.bat. Nothing ships half-made: rel/windows/build_installer.ps1 still hard-stops on a payload with no OpenPairings.exe, and CI still fails the Windows target when the file is missing. Read those two as this change's other half - it is only safe to warn here because they refuse there.

  • Fix JaVaFo implements the 2017 edition of FIDE Handbook C.04.3, not a "2022 edition" - there has never been one. That wrong year had spread through the Settings page (the engine picker, its explanatory hints, and both halves of the switch-to-JaVaFo confirmation dialog), several code comments, the cross-program test suite, and docs/conformance-evidence.md, the evidence document written for FIDE's Technical Commission. FIDE's handbook page for the version JaVaFo runs (handbook.fide.com/chapter/C0403Till2026) names it plainly: approved at the 87th Congress in Baku 2016, with the Terms and Definitions and the Pairing Guidelines For Programmers added at the 88th in Goynuk 2017 - which is why FIDE's own 2026 report format calls it, in the ETT26 Tournament Type Code Table, the 2017 edition, and carries no FIDE_DUTCH_2022 code at all, only FIDE_DUTCH_2017, FIDE_DUTCH_2026 and FIDE_DUTCH. TrfExport has written FIDE_DUTCH_2017 for a JaVaFo-paired tournament since TRF26 support shipped in 0.47.0 - the code has been right the whole time, and only the prose describing it was wrong. Where "2022" actually came from: Gacrux, the FIDE Tie Break Server, keys its own ruleset selection on DUTCH_RULES = { 0: "2022-01-01", 1: "2026-02-01" }, an internal effective-from date meaning "the ruleset before the 2026 one" rather than an edition year, and reading that label as an edition is what put the wrong year everywhere else in this project. Corrected throughout; which rules either engine runs has not changed.

  • Feature OpenPairings gets a real Windows installer wizard - a .msi alongside Setup.exe, and it is the recommended download. vpk pack --msi (WiX 5 underneath) now runs inside rel/windows/build_installer.ps1 and CI packs both on every tagged release, feeding it the same Welcome, Licence and Conclusion content that was wired in but unreachable before this: Setup.exe is Velopack's "one-click" installer by design and was never going to show a wizard, no matter what content it was handed - only a .msi does. Two new WiX dialog images (rel/windows/msi_banner.bmp/msi_logo.bmp, exactly 493x58 and 493x312 - WiX "Bitmap" dialog controls, so .bmp, not .png) are drawn by an extended build_brand_assets.py from the same site mark as the icon and splash - no new artwork. The .msi is renamed OpenPairings-win-Setup.msi the same way Setup.exe already is. Both images keep clear of where Windows draws each page's own text. The wizard's "just me / everyone" page defaults to just me: vpk leaves it on "everyone", and a machine-wide copy needs an administrator for every update, so the build sets the default in the finished .msi and stops if that does not take.

    Verified without installing it (per-machine needs admin, and even per-user installs software on whoever runs it): opened with the Windows Installer COM API and its Property, Directory and ControlEvent tables read directly. That is how a real, load-bearing discrepancy with Velopack's own docs turned up - docs.velopack.io/packaging/installer describes a per-machine install as going to Program Files\{publisher}\{packTitle}, but the .msi this project actually builds (vpk 1.2.0) uses the pack id for both install roots: %LOCALAPPDATA%\OpenPairingsApp per-user (identical to Setup.exe's existing root) and Program Files\OpenPairingsApp per-machine - one path segment, not two. Assert-DataDirectorySafe, the guard that refuses to pack an installer whose uninstall could reach the database, was checking the documented (wrong) per-machine path; it now checks the real one. The database directory (%LOCALAPPDATA%\OpenPairings) was never at risk on the true path either, since it does not depend on packAuthors/packTitle - but the guard is only worth having if it checks the path Velopack actually uses, not the one its docs say it does.

    Per-machine installs cannot update without an admin prompt, checked before shipping this per the maintainer's own condition for doing so: Velopack's docs confirm updates go through the same Update.exe either way, and a per-machine install puts that, like everything else, under Program Files, which an ordinary process cannot write to without elevation. Per-user has no such problem, which is why it stays the install this project actually recommends; per-machine exists because the .msi's wizard genuinely offers the choice. The in-app update check itself is still not built - a different, later change - so today this only affects manually re-running a newer .msi, but it will affect the automatic path the same way once that exists.

    CI (.github/workflows/binaries.yml) now packs both installers on tag builds and attaches exactly OpenPairings-win-Setup.msi, OpenPairings-win-Setup.exe, releases.win.json and the .nupkg - the files Velopack's GitHub update source actually reads, per its docs. Neither RELEASES (legacy Squirrel-migration file; OpenPairings never shipped on Squirrel) nor assets.win.json (vpk's own build manifest, read by nothing in this repository) is attached. vpk is pinned in CI to 1.2.0, the exact version this was built and verified against locally, and a best-effort vpk download github step runs first so delta packages get generated once a previous release exists to diff against - the very first tag to carry this feed has nothing to diff against yet, which is expected, not a failure.

    Also fixed in passing: assets.win.json used to be left naming the two files this script renames by their pre-rename names, i.e. files that no longer existed on disk after a build - it is now rewritten to match. build_installer.ps1's -OutputDir default used to read $PSScriptRoot from inside the parameter block, where it is not yet populated for a top-level script - it silently resolved to a bare, drive-root-relative path and made every run that did not pass -OutputDir explicitly fail outright; the default is now computed in the script body instead, where $PSScriptRoot is reliable.

  • Fix An imported round robin's odd-player bye is worth a full point, matching what SWAR itself shows for the same file. SWAR forces ByeValue to a full point the instant a round-robin file is opened, unconditionally, regardless of what byte the file actually stores there - confirmed against SWAR's own source (docs/swar-source-audit-pass2-2026-09-09.md §3). The importer was reading the file's stored value instead, so an imported round robin's crosstable could differ from SWAR's own by a point per bye. scoring_attrs/1 now mirrors SWAR's forcing for a SWAR-imported round robin, and a new warning names it whenever the file actually has a bye to score. OpenPairings' own, natively-built round robin is untouched - its structural bye still scores zero, which stays correct, since FIDE does not award a point for one either.

  • Fix A player's category survives an old SWAR file instead of quietly going blank. Old SWAR files stored a player's CatIndex as a small ordinal; current ones store it pre-multiplied by 100, and SWAR itself normalises the old encoding on load, unconditionally. category_name/2 only had the multiplied reading, so a legacy file's small index fell through the "this is a second-axis component, not modelled" branch and the player lost their category outright, where SWAR still shows one. Mirrored now, agreeing with the off-by-one fix that already lives in that function.

  • Fix Importing a SWAR file that used manual acceleration now says so. SWAR's XtraPoints are not only a display column - they reach SWAR's own pairing engine as extra score for whichever round is paired next, and OpenPairings has no equivalent feature and said nothing about the gap. A new import warning fires on an ordinary Swiss file that carries a non-zero per-player XtraPoints value or a populated acceleration-band table, saying plainly that manual acceleration is not reproduced here and that a further round may pair differently. Manual acceleration itself remains unimplemented - this is the warning the audit's own "minimum action" asked for, not a step toward building it. Silent for a round robin or a 3-2-1 tournament, where SWAR itself discards XtraPoints before its own pairing engine would ever see them.

  • Fix A SWAR import's warnings other than the points-mismatch one now reach the page instead of a crash. category_warnings/1 and tiebreak_warnings/1 already returned a plain sentence rather than the points-mismatch shape, and the two above join them - but the LiveView that turns an import's warnings into a flash message assumed every entry carried a player name, which raises on a plain string. Latent until this release, since nothing had produced a sentence-shaped SWAR warning that actually reached that code path before.

  • Verified The SWAR tiebreaks September's audit found dropped on import were already fixed, in 0.54.0. docs/swar-source-audit-pass2-2026-09-09.md §2 was written against the importer's mapping table before that release landed. Re-checked against the current one: @tiebreak_codes already carries every mappable SWAR ordinal, and tiebreak_warnings/1 already names the three it genuinely cannot compute rather than dropping them. No code change; the audit document now says so instead of reading as still open.

[0.56.0] - 2026-09-10

  • Feature A tournament tells you when its settings stop describing a FIDE-handled event, and records the round it happened in. FIDE Mode is the default and there is no switch for it: the settings a new tournament gets are the compliant ones, and only changing one can take that away. So the state is computed, not set. Three settings can take a tournament out of it, and all three change who plays whom: a Keizer ladder (FIDE defines the Swiss and the round-robin Berger tables and nothing else), pairing each category as its own separate tournament, and the immediate two-game Swiss rematch, whose second leg is a colour-reversed copy with no pairing decision behind it. Nothing is blocked - an arbiter running a club evening that will never be rated has every right to any of it - and the pages that host those settings say what it means, once, with a link to the setting that did it.
  • Feature The round in which that first happened is kept, and survives a restore, a hand-off and a backup. It is the one half of this that cannot be recomputed: put the setting back and the tournament is compliant again, but nothing left in the data could say which round it stopped. FIDE asks for that round by name. It is recorded in the same write as the setting change that causes it, is never cleared, and only ever moves earlier - so rolling back to a snapshot from before it cannot un-record it, and a tournament handed to another laptop comes home carrying what happened there.
  • Change The audit trail names the setting that did it, on its own line. tournament.fide_compliance_lost, beside the existing round-1-freeze override rows rather than buried inside the bulk settings diff - it is the entry an arbiter may have to point at later.
  • Verified Most settings are not a compliance question, and the reasons are written down. Non-standard scoring, half-point byes and the pairing-allocated bye value are all things FIDE's own checklist requires to be configurable; administrative extra points have a dedicated record in FIDE's own 2026 report format; a hand-set standings order is how an arbiter records a play-off, which the tie-break regulations end in. None of them raises a warning, and PairingsEngine.Compliance says why for each.

[0.55.0] - 2026-09-10

  • Security The other three launchers start with Erlang distribution switched off too, so no way of double-clicking OpenPairings leaves a node on the network. 0.54.0 closed this for OpenPairings.exe and said in as many words that OpenPairings.bat, openpairings.sh and the macOS .app still started with distribution on. They no longer do.

    The exposure was identical on all four, and it was measured rather than inferred: a portable release starts epmd on 0.0.0.0:4369 and the node itself on 0.0.0.0:<ephemeral>, both on every interface, while the web port is correctly pinned to loopback. What turns that from untidy into a door is the cookie: releases/COOKIE ships inside the download, so it is byte-identical on every copy anybody installs, and a reachable Erlang node plus a publicly known cookie is someone else running code on your machine. An arbiter's laptop on club or hotel wifi is exactly where that gets used.

    Nothing in the application needed distribution, which was checked before it was switched off rather than after. There is no clustering - the dns_cluster dependency is configured :ignore unless DNS_CLUSTER_QUERY is set, which nothing local sets - and nothing calls Node.connect/1, Node.list/0 or :rpc. mix pairings.role and the app-role wrapper that is the documented recovery path for an installation with no administrators start the Repo themselves and never reach a running node. The deploy script's restart warning goes over HTTP to loopback for the same reason, and says so. Each launcher's stop was already its own - closing the window, Ctrl-C, or a Windows job object - and never bin/... stop, which is an RPC.

    What it costs is exactly that: bin/pairings_engine_portable stop, restart, pid, remote and rpc are RPC to a named node, so against an instance a launcher started they have nothing to talk to. The three script launchers let a pre-set value through, so RELEASE_DISTRIBUTION=sname ./openpairings.sh still gives you a node to attach to when you want one. OpenPairings.exe forces none and ignores the variable, deliberately: it is the front door an arbiter double-clicks, and an escape hatch on the front door is a hole with a label on it. OpenPairings.bat is the diagnostic launcher, and that is where the escape hatch belongs.

    The single-file Burrito binary never had the problem and needed no change. Its launcher builds the erl command line itself and passes -setcookie without -name or -sname; distribution only starts when the VM is given a node name, so it opens no epmd and no listener.

    Verified on Windows against a real portable release. Before: epmd on 0.0.0.0:4369 and the node on 0.0.0.0:56324. After: no epmd process at all, and the BEAM holding exactly one listener, the web port on 127.0.0.1. The application is unaffected - a round pairs, the served page reads it back out of the database, and closing the window still stops the server. openpairings.sh and the macOS bundle cannot be run on a Windows machine and were changed by reading; both were syntax-checked, and the bundle's launcher was rendered out of the workflow heredoc and checked as the script CI will actually write.

[0.54.0] - 2026-09-10

  • Fix A SWAR file's tie-breaks are imported in full, and a criterion this app cannot hold is named rather than dropped. The importer mapped six of SWAR's sixteen ranking methods; Buchholz Cut-2, Koya, ARO, ARO Cut-1, median Buchholz and games-played-with-Black were all added to this app's tie-break catalogue afterwards and the importer was never told. It is not a blank column: the list is compacted, so a tournament SWAR ranked on Cut-2 and then Buchholz arrived ranked on Buchholz, with the second criterion silently promoted to first and the standings order changed.

    Twelve of the sixteen now map. The other four - median-2, performance rating, games won with Black - have no honest counterpart here and produce a warning naming which criterion was lost and that everything after it moved up a place. Found by reading SWAR's own source rather than from a bug report.

  • Feature Windows has a real application to double-click. The portable release now carries OpenPairings.exe beside OpenPairings.bat, and it is what the Windows installer installs. Double-clicking it shows a small window that says OpenPairings is starting, waits until the server actually answers, and only then opens your browser - no console window, no black rectangle full of Erlang, nothing to type.

    The window is deliberate rather than an oversight. Taking the console away takes the stop button away with it, and the console window was the stop button: "close this window to stop it" was the whole model. So the model is kept and the window says so - closing it stops OpenPairings, and it stops it for real. The server runs inside a Windows job object, which means it is terminated with the launcher even if the launcher is killed from Task Manager rather than closed. A launcher that leaves a server running after you close it is the reason people end up with three copies of a program and no idea which one holds their data.

    Double-clicking a second time opens a browser tab at the copy already running instead of starting another one. If something else is already on port 4000, it says so and offers to open it anyway, rather than dying silently. If the app fails to start it says that too, and offers to open the log - which is now written to launcher.log next to your database, so a problem report has something in it.

    OpenPairings.bat is untouched and still there. It is now the diagnostic route rather than the front door: it shows the output live, in a console, which is what you want when something is wrong and not what you want when it is fine.

  • Security The Windows launcher starts the app with Erlang distribution switched off. A release defaults to starting epmd on 0.0.0.0:4369 plus a distribution listener on another all-interfaces port. Measured, not assumed - the web port was already correctly pinned to loopback, these were not.

    That matters more than it looks, because the Erlang cookie that authorises a connection to those ports lives in releases/COOKIE inside the download, so it is the same on every copy of a given release. Nothing in local mode needs distribution: it is one person on one computer, and the new stop button is a job object rather than a remote call. Turning it off also removes a Windows Firewall prompt on first run, which is an alarming thing for a chess program to show a club arbiter.

    OpenPairings.bat, openpairings.sh and the macOS .app still start with distribution on - they are unchanged, and on a machine without an inbound firewall that exposure is real. Closing it there is a separate change.

[0.53.2] - 2026-09-09

  • Fix A file holding two tournaments is refused instead of imported as one. TRF has no end marker, no length and no envelope, so two files joined end to end are one syntactically valid file - which cat a.trf b.trf, a Windows copy /b, and a script with one >> too many all produce.

    The existing duplicate-rank check already caught the usual version of this, because a second document normally restarts its numbering and every rank then appears twice. It did not catch the quiet one: two tournaments whose starting ranks happen not to overlap imported cleanly, as a single tournament holding everybody's players, named after whichever 012 line was read last, with no warning. A file is now checked for having exactly one tournament-name line before anything is parsed, so this is refused by structure rather than by the coincidence of a collision.

    Uploading the same file twice is deliberately still allowed and still makes two separate tournaments. A TRF carries no identity to compare against, and two clubs can legitimately run events with the same name on the same dates - refusing would block a real case to prevent a visible one. Both behaviours are now pinned by tests, so the difference is written down rather than assumed.

[0.53.1] - 2026-09-09

  • Security The publishing task can take its token from the environment, so the deploy stops putting it on a command line. A secret passed as an argument is in that process's argv, and /proc/<pid>/cmdline is readable by every local account - so on a box that hosts other applications the ingest token was legible for as long as the task ran. --token still works and still wins when both are given: a person typing it has decided. The deploy script closed its own half of this at the same time; this is the half that had to live here, because OptionParser reads argv and nothing else.

[0.53.0] - 2026-09-09

  • Feature A player can be in several categories at once. The same person is a junior AND a woman AND a club member, and each of those is a prize list somebody has to produce - but a player had exactly one category, so the arbiter had to choose which prize the software would help with. Now they carry a set. Prize lists, the printed per-category standings, the Players grid and the published results all read all of them.

    Pairing cannot work that way, and that is the shape of the whole feature. With "pair by category" on, each category is paired as an independent tournament - its own engine call, its own bye - so a player in three categories would be given three opponents in one round. Exactly one has to win, and one function decides it, in one place, for the pairing partition and every label that has to agree with it.

    Nothing already recorded changes: every existing player's set is exactly the category they had, backfilled in the same migration that adds the column, so a round paired after the upgrade is the round that would have been paired before it.

  • Feature Sorting and filtering the Players grid by category. Sorting a multi-valued column has no honest answer, so the column does not pretend to have one: clicking sorts by the pairing category, and right-clicking offers one category at a time - "show only the U16s" - as a filter, which changes which rows exist rather than their order. The right-click menu toggles a single name and leaves the rest of a player's set alone.

  • Fix An imported SWAR category was one too strong, and the last category never received anybody. SWAR stores a player's category index one-based, with the first slot written as 100 (Categories.cpp:737); this import divided by a hundred and stopped, which is one higher than the slot it wanted. Found by reading SWAR's own source rather than by a bug report, and invisible until now because every .swar fixture this project has defines no categories at all, and because our own exporter writes a leading blank that makes an export-import round trip agree with itself while both halves are wrong. With "pair by category" on this was not a label but a pairing pool.

  • Fix The pairing-explanation page could name a category the pairing did not use. It labelled a board with whatever the player's category field said, with no check that the tournament still lists it, while the pairing itself pools an unlisted category as "Uncategorized". Both now ask the same function, so the explanation and the pairing cannot disagree.

  • Verified What SWAR's second category list means, settled from its source. value2 is the second axis - age beside rating, or the reverse - and for all four of SWAR's category types both lists hold numeric bounds rather than names, which none of the three meanings previously guessed at got right. The import still reads the first axis only, because SWAR renders the pair as one label and a player here carries a name; the warning that says so stays. Also confirmed correct as built: presence points on a pairing-allocated bye, and the divide-by-four scale of the "3-2-1" point values.

[0.52.1] - 2026-09-09

  • Fix The forced-unlock message named "the other machine" instead of the machine. Breaking a hand-off open says which copy is now dead - the one the arbiter carried to the club laptop, the tournament hall PC, the secretary's desktop - and it is the only place the app says it. The message was built from the row the unlock returns, and clearing handed_off_to is part of what the unlock does, so the destination was always gone by the time it was read and the placeholder won every time. Taken from the staged row instead, which still carries it. The audit entry the same function writes had this right all along.

[0.52.0] - 2026-09-09

  • Feature ?, the ITDX unknown-result code, is read on import. The letter sent to FIDE TEC on 2026-09-08 makes exactly one commitment - "We will implement ?" - and this is the application half of it; Ainalrami v0.25.0 is the other. A file that records a game as played with its result lost now imports as a real board with both players on it and no result, and the import warns which rounds those were. It is read but never written: a code you can type is a placeholder waiting to be used for "not entered yet", which is what a blank already says.

    The interesting part is what it would have done instead. ? is deliberately absent from the engine's playing_codes/0, so the importer's game test would not have matched it and the entry would have fallen through to the single-sided path - which has three clauses and no fallback, and raises on an arbiter's file mid-import. That is the same drift that once turned W/D/L games into byes, recorded in that file's own comments. The test for this reverts the guard and watches it happen.

Fourteen findings from the 2026-09-05 audit, closed together: the log that held live login tokens, three crashes that share one root cause, and a page anybody can reach that cost the server a copy of itself per upload.

  • Security A login link is no longer written into the server log. The request log recorded the full path of every request, and five routes carry a bearer token in the path rather than in a header or a cookie: the magic-link log-in, the confirm-email link, a collaborator invite, a public-tools download and a phone enrolment. For all five, holding the URL is the whole of the authentication - and a log-in token stayed live for fifteen minutes after it landed in a file that is shipped, tailed and kept for months. Those paths are now logged with the secret replaced (GET /users/log-in/[FILTERED]); every other request is logged exactly as before, because going quiet would have traded a leak for a blind spot.
  • Fix The arbiter tools page no longer copies its whole upload store on every upload. Enforcing the store's memory budget read every parsed tournament currently held into the writing process, so one upload cost a copy of all the others - and the page needs no account, so a handful of simultaneous connections multiplied that by however many somebody opened. The budget and what it evicts are unchanged; only the cost of checking it is gone.
  • Fix A .swar file with many nationalities no longer scans the FIDE list once per country. Matching players SWAR left without a FIDE id looked the 1.9-million-row rating list up per distinct country string in the file, against a column with no index - and the file decides how many distinct strings there are. It is one indexed query now.
  • Fix The projector view survives its round being withheld. With nothing published, the page held a value that every reader of it was written for the absence of, and the hall screen's own page-turn timer raised on the difference - taking the LiveView down in front of the room. The same mistake hid the "no round has been paired yet" card, so a brand-new tournament's Live page explained nothing at all; it now says so.
  • Fix The break-glass hand-off unlock cannot crash the tournament list. The confirm event killed the page when no return was staged, and the word an arbiter has to type is now required by the server rather than only by the button.
  • Fix A phone is told when a tournament has been handed to another machine. The result buttons stayed enabled - the write was refused, but only after the tap, and only as "Could not save that result." The buttons now follow the same gate every other write does, and a tap that lands in the moment between the hand-off and the phone hearing about it says what actually happened.
  • Fix A results CSV can no longer kill the Pairings page. A board number of a million digits was converted before it was checked and raised where the parser promises never to raise. It is refused as a board number now, which is what it is.
  • Fix A tournament's rate of play survives a TRF round trip. FIDE's 122 (222 in TRF26) was written out of the rate-of-play field and read back into the free-text field it replaced, so an export-import loop emptied it: the setup checklist then reported it missing and a second export carried no rate of play at all.
  • Fix Accepting an entry tells open pages about the new player after the write, not during it. The broadcast went out from inside the still-open transaction, so a page could reload and render a roster without the player it had just been told about.
  • Fix A backup empties the Belgian rating index as well as the FIDE one. It was emptying the players table and shipping the search index over it intact, so a restore came up with no KBSB players and 36,000 of them still in the index - a lookup offering names that are not there. Restore it, and the next sync repairs it either way.
  • Fix A double quote typed into the Belgian player search no longer breaks it. The character ended up inside the search index's own syntax, which either errored - dropping every keystroke to a full scan of the mirror - or quietly searched for something else. The two sibling searches already stripped it.
  • Change A SWAR tournament id containing a quote, a slash or a control character is refused on the way in. It is used as a download filename verbatim, and one of those made that download fail for good rather than once. Real ids, braces and all, are unaffected.

[0.51.0] - 2026-09-08

Three things the players grid got wrong, all reported from real use.

  • Fix Sorting on Presence now reverses when you click it again. The column pinned the absentees to the top whichever way it was sorted. A present player was reported to the sorter as blank, and blanks are held last in both directions - a rule that is right for a missing rating and wrong here, because "present" is the commonest answer to the question this column asks, not a missing one. Presence now sorts as a real order: present, sat out earlier rounds, sitting out this one, absent, forfeit - and clicking twice reads it back to front.
  • Change A player you add by hand starts as not paid. The fee status defaulted to paid, so every new entry arrived already ticked and the only way to keep the column honest was to un-tick people. It fills in as the money comes in, which is the direction the work actually runs. Nothing already recorded changes, and an import still says what its file says.

[0.50.0] - 2026-09-08

The 2026-09-05 audit's three worst groups, closed together: what a stranger can do to the server, what reaches the public site, and what decides who wins.

  • Fix A seat the arbiter vacated is published as what was recorded on it, not as a full-point bye. A board with one empty seat and a real result - a forfeit, or anything a SWAR or TRF import carries - travelled to the results site as a pairing-allocated bye worth the tournament's bye value, whatever was actually on it. The same defect was fixed in the federation's page first; this is the second copy, in the snapshot. It was not confined to one row: the public site rebuilds each player's running total by adding these per-round figures up, so an invented point moved every later total on that player's card, while the standings in the same document said something else. The figure now comes from the same function the crosstable is computed with, and the row is labelled "vacated seat" and carries the result, because it is not a bye.

  • Fix A tournament handed to another arbiter can no longer be republished from the copy that released it. Hand-off froze every local write and nothing at all on the way out to the results site - and worse, handing a tournament off queued a publish of it, because that is what every write in the app does. If the receiving arbiter adopted the publishing key out of the hand-off file, a send from here overwrote their live results with a frozen copy from before the trip. Refused now at the queue, at the send, and in what the drain considers due; a queued publish that predates the hand-off is kept rather than dropped, and goes out when the tournament comes back. An archived tournament still publishes, deliberately: it is finished on this machine, there is nobody to overwrite, and refusing would leave the public page one publish short of the final standings.

  • Fix A result entered while a publish is in flight is no longer lost. The queue row is the only record that a tournament has unsent changes, and the drain deleted it after a successful send. Between the moment the payload was built and that delete sat a whole HTTP round trip - up to fifteen seconds on venue wifi - and a result typed inside it found the row already there and wrote nothing, by design, so a burst would not restart the backoff. Deleting then threw away the only trace. The result was not published late; it was never published, until some unrelated later write happened to queue the tournament again. A counter that only queueing moves now guards the delete: if anything arrived while the payload was away, the row stays and the next drain sends what is true then.

  • Fix Binning a published tournament takes it out of publishing instead of publishing it. Moving a tournament to the recycle bin broadcasts, and broadcasting is what queues a publish - so a fresh snapshot of the event the arbiter had just withdrawn went out about two seconds later. Binning does not withdraw the published copy, deliberately: the bin is reversible for 90 days and a takedown is not, so that stays a deliberate action.

  • Fix Deleting a published tournament for good takes it off the results site first, and refuses to delete anything if it cannot. The key that can withdraw a published tournament exists on one machine, in the tournament's own row, and a permanent delete took it with the row - so purging while the page was up did not leave a stale page, it left a permanent one, carrying every published player's name, rating, club and federation, with no way to remove it from this app at all. The recycle bin's 90-day sweep did exactly that, unattended, on a page load. The sweep now leaves a still-published tournament in the bin instead, and "Delete permanently" withdraws it properly or tells you why it could not.

  • Fix Under Belgian 3-2-1 scoring, a draw is no longer counted as a win. The "Number of wins" tie-break (FIDE C.07 Art. 7.1) asks how many rounds a player took as many points from as a win awards - and compared what the round actually paid, presence point included, against a figure that left the presence point out. In the Belgian club scheme (win 2 / draw 1 / loss 0 with a point for turning up) a draw pays exactly what a win pays before that point, so every draw in the event counted as a win, and so did a bye worth a draw. It is in FIDE's default Swiss set, so it was deciding places by default. Both sides of the comparison now measure the same thing.

  • Fix Koya's 50% line is measured against what a round can really pay. C.07 Art. 9.2 counts the points scored against opponents on at least half the maximum possible tournament score, and the maximum was worked out without the 3-2-1 presence point: 2 a round where a round pays 3. The line landed at 1 point a round, which is exactly what a player who loses every game and turns up to every round scores - so the whole field cleared it and Koya separated nobody, returning each player their own score back. An ordinary 1/half/0 tournament is unaffected by either fix; the numbers only move where a tournament pays a presence point.

  • Fix The working shown behind a tie-break agrees with the column again. The panel that explains how "Number of wins" and Koya were arrived at carried its own copy of both calculations, so it confirmed the wrong figure instead of exposing it. Both now read the one function the standings read.

  • Fix The enrolment screen says eight digits, because the code is eight digits. It had said six since the day it shipped, in English and in Dutch - the number a helper counts to before deciding they have mistyped.

  • Fix The public norms tool has a rate limit, like every other page a stranger can reach. It was the last one without: no account, ten files of five megabytes accepted on each press, and every one of them parsed. That made it the cheapest way to spend this machine's processor. Sixty files in ten minutes from one address now, counted per file because that is where the cost is, and charged only for what was actually parsed - so a refused press does not dig the caller deeper, and nothing already parsed is lost.

  • Change Pinned to Ainalrami 0.24.0, which reads a TRF in linear time and measures a line in bytes rather than characters. That is the engine half of the upload findings below: a 5 MB file of player records took over an hour to parse and a single very long line about forty-five minutes, and separately a team name carrying accents lost one player from its roster per accent. The bounds this release adds are now policy rather than a guard against the parser.

  • Fix A TRF file is now measured before it is read. Two of the TRF parser's loops cost the square of what they are given, and nothing in the format bounded either: 8,000 player records took 280 ms and a million took over an hour, while a single 5 MB 132 or 013 line - a two-line file - cost about three quarters of an hour on its own. Both belong in the pairing engine and are reported there; what the app can do is refuse the input, which it now does in one pass over the raw bytes before anything is decoded. The limits are 5 MB, 20,000 records and a 2,048-byte line, each derived from what TRF16 can actually express - the starting rank has four columns, so no file names more than 9,999 players, and the longest record is 91 + 10 per round, which at 2,048 covers a 195-round event.

  • Fix A .swar file can no longer stop the whole application with one round number. A [RONDE] record's round number is a raw 32-bit integer straight off the disk, and the importer turned the highest one it found into a loop from 1 - inside the import transaction, holding the database's single write lock. One record saying 2,000,000,000 froze every other writer behind it. Round numbers outside 1 to 30 (Tournament.max_rounds/0, the longest event this app will hold) are refused by the parser now, before a row is written. Round 0 goes with them: it used to produce a Round row numbered 0, a round before the first.

  • Fix A bad results CSV no longer answers with a page bigger than the file. The importer returned one error line per bad row and the Pairings page rendered every one of them: 200 KB of junk produced 99,999 errors and 4.5 MB of message text, so the 2 MB the upload allows became a ~45 MB page pushed down the arbiter's own websocket. A file with more lines than a round can have boards is now refused with one sentence, and a long list of problems is cut to fifty with a count of the rest - enough to show whether it is one mistyped board or the wrong file entirely.

  • Change A tournament export is read at 10 MB rather than 25. The size was checked by the upload box and nowhere else, so 25 MB of JSON became up to ~285 MB of memory before the format tag had been looked at. The limit now lives in the importer, is re-checked on disk before the file is read at all, and is set from what an export weighs: a 400-player, 13-round tournament with a 2,000-row audit trail measures 220 KB, and the largest Swiss ever played scales to about 5 MB. A file holding a dozen such events at once is refused with a note to export in batches; the machine backup remains the tool for a whole archive.

[0.49.0] - 2026-09-08

  • Feature An imported TRF file is now checked against the pairing rules, round by round. The importer recreated whatever the file said, board for board, and never asked whether the file said anything legal - so an event carrying a rematch in round 5 imported clean and the app went on pairing round 6 from a position the rules do not allow. Every round the file records is now scored against the absolute criteria of the Dutch system, and the arbiter is told about a rematch, a board where both players were due the same colour, a pair the file's own 260 prohibited, or a second pairing-allocated bye for a player C.2 had already ruled out - naming the round, the players and the rule.
  • Change A file with an illegal round still imports, in full. The check reports; it never refuses. An arbiter recovering a historical event needs the tournament far more than they need our opinion of it, which is how the points cross-check has always behaved. The verification is also rescued as a whole: an engine that trips over some field shape nobody has met yet cannot take a tournament down with it.
  • Change Only the absolute criteria, and only where they apply. "We would have paired this differently" is not a finding - two conforming programs pick different rounds from the same position all the time, and a notice an arbiter learns to dismiss costs them the one that matters. A round robin's schedule is fixed before a move is played and Keizer is not the Dutch system, so a file that says (192, or the 092 label) it is not a Dutch Swiss is passed over in silence rather than reported as broken.

[0.48.0] - 2026-09-07

The other half of TRF26: 0.47.0 taught the app to write the file, and this teaches it to read one back. Every setting below was already parsed and then dropped on the floor, which is the quiet kind of wrong - the imported tournament looked complete and was configured differently from the one that left.

  • Fix An imported tournament keeps its scoring. A 3-1-0 file came back scored 1 / half / 0, so every total disagreed with its own games and the next round would have been paired from the wrong brackets. The point system (162, or the engines' BB* lines) now lands on the tournament's win, draw, loss and bye values.
  • Fix An imported tournament keeps the pairs its arbiter separated. Prohibited pairings (260, or XXP) were read and discarded, so a re-imported event would happily seat two players it had been told never to pair. A rule the file limits to some rounds is widened to the whole event, which is the safe direction, and the import says so rather than absorbing the change silently.
  • Fix An imported tournament keeps its length. A nine-round event imported after round three became a three-round event, which moves the engine's final-round colour rule four rounds early. The file's own 142 is read now.
  • Feature What else comes back: which edition of the rules paired the boards (192, so a JaVaFo tournament stays a JaVaFo tournament and a round robin stays a round robin), the tie-breaks (202, filtered to the ones this installation can compute), byes granted for a round nobody has paired yet (240), administrative extra points with the flag that makes them count (299), and Baku acceleration (250 / XXA) - the last applied only when FIDE's method actually reproduces the file's own numbers for that field, so a different method is reported rather than mislabelled.
  • Feature A bye granted for the next round is in the export. The report was a record of rounds played and this is the one forward-looking thing in it: whoever pairs the next round from this file needs to know who is not playing. Carried as a 240 record, or as the column an engine reads in the older spelling. Only on a full export - a round slice is a historical excerpt.
  • Feature Extra points are in the export. An administrative bonus or penalty had no home in the file at all, since TRF's points column is game points by definition, so a FIDE report and a re-import both lost it.
  • Change 142 is the tournament's length, not the number of rounds the file happens to carry. A pairing engine applies the final-round colour exception by that number, so a three-round slice of a five-round event that claimed three rounds made round three the last one.

[0.47.0] - 2026-09-07

  • Feature The TRF download is a TRF26 file. FIDE's Tournament Report File Format Version 2026 replaced TRF16 on 1 September 2025, and the file an arbiter uploads now speaks it: 192 names the system that paired the boards (Ainalrami is FIDE_DUTCH_2026, JaVaFo FIDE_DUTCH_2017, Baku appends _BAKU, a round robin is BERGER_ROUNDROBIN_Gn, Keizer the CUSTOM_SWISS FIDE has no better code for), 202 lists the tie-breaks, 222 the rate of play encoded where its wording allows (5400+30), and 162 the point system when it is not 1 / half / 0. Prohibited pairings and Baku acceleration are in the file at last, as 260 and 250 records; before this they were only ever in the file the engines read. ?dialect=javafo on the download asks for the older XXR/XXP/XXA/BB* spelling that JaVaFo and older checkers read. Either spelling parses to the same tournament.
  • Feature TRF26 files import. A 240 bye record, a 299 abnormal point assignment, a 260 prohibited pairing and a 162 point system are read alongside the TRF16 spellings, into the same tournament. Team records are not.
  • Change The engines' input is unchanged. JaVaFo reads only the older spelling, and both engines still get identical bytes, so a real tournament can still be diffed between them.

[0.46.0] - 2026-09-07

Feedback from the first day of the rationale page on a real eleven-round tournament, in the order it came.

  • Fix The six empty boxes after a name are colours again. The colour history beside each seat - White, Black, White, Black - drew every round as an empty box: the record spells colours in capitals and the chip matched lowercase, so nothing ever matched. Each chip now says which round and which colour on hover, a round without a game is the dashed one, and the chips are pure white and pure black so they read the same on a light card and a dark one.
  • Change Board by board is closed by default. The cards are read one at a time when a player asks; an arbiter who opened the page for the bracket map scrolled past a hundred of them. The section is a disclosure now, with the board count on it, and a "worth a look" link opens it on its way to the board.
  • Change Article 5.2.5 explained once, not on every card. The note on a board decided by 5.2.5 is a line; the rule, and how this engine reads its parity, is spelled out once under the grid, where it stretches no card into a column of text.
  • Feature "Work it out now." A question the pairing-time cap skipped - why this player floated, or got the bye, and not one of the eighty-eight others - can be worked out on request, every candidate a full pairing of the round. It writes the account only: no board is touched. The cap stays at pairing time, where nobody is waiting on that question yet.
  • Change Recompute runs in the background and says so. Recomputing a tournament's accounts, or working the alternatives out, greys the button out with a note that a long tournament takes a minute or two, keeps the page answering meanwhile, and reloads it when the result lands.

[0.45.0] - 2026-09-07

  • Feature "Rather not, if possible." A forbidden pairing can now be a wish instead of a rule. Tick "Only if possible" when adding one and the Ainalrami engine weighs the pair against the pairing criteria rather than ruling it out: the two are kept apart where the rules allow it and seated when nothing else is legal - where the same pair as a rule would have left the round unpairable. The wish is never written to the TRF; JaVaFo and Keizer have no such option and ignore it, and the Settings page says so beside the control instead of letting a wish be set that nothing reads.
  • Feature Clubmates apart for the first N rounds. The classic request
    • "no club games in rounds one and two" - as one number under the exclusion rules, with the same "if possible" meaning. 0, the default, changes nothing.
  • Feature How hard to try. A wish sits either above the colour and float rules (strong: the engine would rather float a player than seat the pair) or below all of them (weak: a tie-break and nothing more). The absolute rules sit above either - a wish never produces a rematch or an illegal colour. The rationale page shows the wish as a rung of its own, "pairs the arbiter asked to keep apart if possible", and a what-if that seats a soft pair anyway is judged on it.
  • Change Backups carry the distinction. A tournament export, snapshot and hand-off file records whether each forbidden pairing is a rule or a wish, and the two new tournament settings with it. Files written before this release import every pairing as a rule, which is what they were.

[0.44.0] - 2026-09-07

  • Feature "What is the best round in which they play each other?" A third question on the rationale page's "What if?". The first two seat the pair as given and score it; this one has the engine FIND the best legal round containing the pair, then reports what it costs against the round that was played and lists every board it would move to get there. A pair the rules forbid is refused with the rule - met in round 3, both absolutely due White, forbidden by the arbiter - which is checked first: forcing an illegal pair leaves no legal round, and "no legal round" is the wrong answer to "why can't they play" when the true one is "they met in round 3".

  • Feature "Didn't turn up?" Name the missing player and get the least disruptive legal fixes, ranked by who ELSE would have to move: the stranded opponent takes the bye, or plays the bye holder, or takes over one board whose displaced player then takes the bye or the bye holder. Each says who moves and what it costs against pairing the whole round again - and "nobody else moves, costs C7" is an honest answer, because the cheapest fix is rarely the best round. Illegal fixes are dropped; new boards are tried both ways round so the engine's own colour criteria pick the colours. Advice only: the pairings page is where a board actually changes, by hand.

  • Feature The float cascade. The engine's account now opens with one line per bracket, top to bottom: who was there, who arrived from above, what paired, who fell. The per-bracket detail says the same at length; this is the shape of the round at a glance. The CLI's explain mode prints the same cascade.

  • Change Engine updated to Ainalrami 0.20.0, which answers the two questions above and adds soft constraints - club protection and the like - which the app will expose in the next release. Pairing behaviour with none set is unchanged, byte for byte.

[0.43.1] - 2026-09-06

  • Fix The recompute offer was at the foot of the page. It is the one thing on the page that changes what everything else can show, and it sat below the bracket map, the board cards and the criteria - where it was found by exactly one person, laughing. It is now the first thing after the introduction.

[0.43.0] - 2026-09-06

  • Feature JaVaFo-paired rounds get the detailed account too. The recompute no longer asks who paired the round - only that it is a single-pool Swiss. JaVaFo records nothing about its reasoning, so this is the only account such a round can ever have: Ainalrami's reading of the boards JaVaFo produced, on the standings as they stood before the round. The page is explicit about it, in the offer and on the result - "paired by JaVaFo, analysed after the fact by Ainalrami" - and where a line says Ainalrami would have preferred something else, it says so in the warning colour as a disagreement between two engines, never in red as an error in the round. The two agree on all but a fraction of a percent of pairings, so expect that rarely; where it appears, it is worth a look. Nothing about the "never changes a pairing" guarantee changes.

[0.42.0] - 2026-09-06

  • Feature Older rounds can be brought up to the detailed account. A round paired before today has an engine account without the subgroups, colour states, ruled-out pairs and "why him and not me" lines - or no account at all, if it predates the record. The rationale page now says so, in the warning colour, and offers one button: recompute, for every round of the tournament that needs it. It works from the boards as played, on the standings as they stood before each round, and stores the result marked as recomputed. It is not a re-pair - a re-pair could give a different answer, and the question is about the round that was played - and it never changes a pairing: it writes the account and nothing else, and a test holds it to that. A round an arbiter changed by hand after pairing is left exactly as it is, because its stored account is the engine's original decision and the page already says the boards diverged from it; "What if?" still judges such a round live. Pressing the button again does nothing.

  • Change "What if?" no longer needs a stored account. It rebuilds the field itself, so it now appears on every Ainalrami-paired Swiss round, including the ones from before the record existed.

[0.41.0] - 2026-09-06

  • Feature The pairing rationale now answers "why him and not me". For a round Ainalrami paired, every player who floated out of a bracket gets a line per other member of that bracket saying what floating THEM instead would have cost: impossible (no legal pairing has them leave), worse on a named criterion - "floating down again after floating down last round", with the two scores - or equal, in which case the transposition order decided and the line says which way. The same for the pairing-allocated bye, after first saying who was not allowed one at all and why (already had a bye, won by forfeit). These are worked out once, when the round is paired, by actually pairing the alternative and scoring it; they are stored with the round, so what the page shows is what was true at the moment of the decision. Capped at twelve candidates per question - round one's single bracket of everyone is not where this gets asked.

  • Feature "What if?" - name a swap or a pair and get the ruling. On the rationale page, pick two players from the round and ask what would have happened had they swapped seats, or played each other. The engine scores the proposal against what was played, on the field exactly as it stood when the round was paired: it breaks an absolute rule (met in round 3, both absolutely due White, forbidden by the arbiter - named pair by pair), or it scores lower on a named criterion, or it is equal. Nothing changes; the swap itself is still on the pairings page. This is the closest thing to what an arbiter does in their head when a player complains, done by the rules rather than by feel.

  • Change Engine updated to Ainalrami 0.19.0, which judges the alternatives. Pairing behaviour is unchanged.

[0.40.1] - 2026-09-06

  • Security Two denial-of-service advisories in the HTTP client closed. mint - the low-level HTTP library behind every outbound request this app makes (OpenResults publishing, the FIDE sync, the federation upload) - carried CVE-2026-82728 (high: unbounded buffering of a malicious response exhausts memory) and CVE-2026-82729 (medium: quadratic chunk parsing pins the CPU). Both are on the client side, so the attacker has to be a server this app connects to; a compromised or spoofed endpoint could take the app down. Bumped to 1.10.0, which closes both. No code changed. The 219-agent audit of 2026-09-05 never looked at dependencies at all - this came out of the first dependency audit these repositories have had.

[0.40.0] - 2026-09-06

  • Feature The pairing rationale now shows what each bracket was paired FROM, not only what came out of it. For a round Ainalrami paired, every bracket in "What the engine reported" opens with the S1/S2 the engine actually paired off (seeds and names, S1 marked when it is the players moved down), a colour table for every player in it - the run they have had so far, Whites minus Blacks with its sign, what they are due, and FIDE's own classification of that preference: absolute, strong, mild or none - and the pairs inside the bracket that the absolute criteria ruled out, each with its reason: met in round N, both absolutely due the same colour, or forbidden by the arbiter. That list is the answer to "why am I not playing him"; until now the record described only the optimisation and could not say. A bracket where nothing was ruled out says so, because "the criteria chose freely" and "the rules left no choice" are different defences. Rounds paired before this release have a version-1 record and render as before, without these blocks.

  • Change Engine updated to Ainalrami 0.18.0, which is what reports the above. Pairing behaviour is unchanged: the engine's comparison against bbpPairings stayed at 100%.

[0.39.0] - 2026-09-05

  • Feature The player list exports as CSV, shaped the way you need it. Settings - Export now has a picker: choose which fields go in the file, put them in the order you want them (they come out in that order, not in some fixed internal one), sort the rows by seed or by name, and leave out anyone flagged permanently absent - a player who is just missing one round still appears. You can also choose what separates the columns, which matters more than it sounds: Excel splits on the list separator of the machine's locale, so on a Belgian or Dutch Windows a comma-separated file opens as a single column of text and a semicolon-separated one just opens. The whole choice lives in the download link, so it can be bookmarked or passed to somebody else. Names that begin with =, +, - or @ are neutralised on the way out - a spreadsheet would otherwise run them as formulas.

  • Fix Export was unreachable from the Settings menu. The top-bar "Settings" menu listed seven of the ten settings pages: OpenResults, About and Export were missing, so the only way to reach Export was to open some other settings page first and find it in the row of tabs there. Anyone who did not already know it existed could not get to it. Both menus now list the same pages in the same order.

[0.38.0] - 2026-09-05

  • Fix Deleting a player turned their opponents' losses into points. The Remove button on the Players grid nils the seat and leaves the result standing, so every game the deleted player had survived as an opponentless row. Keizer scored that as an unpaired bye worth half a ladder rung - paid to whoever had LOST the game, with the loss dropped from the W/D/L line as well. The result now stands, and the vanished opponent is worth nothing, which is what a Keizer win means: what a win pays IS the opponent's value, and there is no longer an opponent to value. Standings already read the row this way; Keizer no longer diverges from it.

  • Fix An odd round robin lent half a point to everyone else's tiebreaks. The structural bye - the player the Berger schedule sits out against the phantom - was recorded as a "requested-zero" row, a type chosen purely for its point value. That type also carries "the player asked for this", so C.07 Art. 16.3 re-counted the trailing bye as a draw: the byed player's adjusted score rose half a point in every opponent's Buchholz and Sonneborn-Berger. Because SB weights that score by the points scored against them, the error was not even uniform - whoever beat them gained 0.5 and whoever drew gained 0.25 - so it reordered players tied on score. A bye nobody requested is now involuntary, matching the rule already applied to a Swiss pairing-allocated bye. A genuine requested bye in the same tournament is unaffected.

  • Change Engine updated to Ainalrami 0.17.0. Two TRF defects that could change a published round: a round already paired but not yet played was dropped when the file was read back, so the round was paired a second time; and an XXP "these two must never meet" line written with a comma instead of a space was discarded in silence rather than refused.

[0.37.1] - 2026-09-05

  • Fix A hall screen opened by URL never cycled. ?display=1 exists so a machine can be pointed at an address and left alone, and that was the one path that never started the timer - it was reached only from the button, the pause toggle and its own tick. The screen showed the first page of boards for the rest of the tournament.
  • Fix A vacated seat was published as a full-point bye whatever the result said. A board with one seat emptied printed the tournament's bye value unconditionally, so one carrying a recorded forfeit went to the federation's public site as a bye. It now shows what the arbiter entered, and falls back to the bye value only when there is genuinely no result.

[0.37.0] - 2026-09-04

  • Fix A player who was away now appears on the published page. Declared absences are not pairings - they live in their own table - so nothing about them reached the results, and those players were simply missing from the round they had told the arbiter about. They now appear the way the federation's own software writes them: the same row as a bye, carrying the absence's point value and the word "Afwezig". The value is read from the standings code rather than decided again, so the page cannot disagree with the table printed above it.
  • Feature A strip of round links above the results, so a long page can be jumped rather than scrolled. Only the rounds: the federation's own strip also links to sections this file does not contain, and a link to an anchor that is not there is worse than no link.

[0.36.3] - 2026-09-04

  • Fix A bye now sits after the boards, not among them. It was sorted on its board number like any other pairing, so it landed between board three and board four while its own board cell was blank - reading as a gap in the numbering. A published round puts its bye after the last numbered board, and so does this now.
  • Fix The points a bye awards print the way the federation prints them - 1 rather than 1.0, beside the 0.0 in the points column, and a half-point bye shows the same half symbol the results use.
  • Change The version reads as a version in the federation's list, and as itself in the file. That list's "Vers." column takes whatever follows the last hyphen, so a plain v7.00 arrived as 7.00. The default is now OpenPairings <release> -v7.00: the column shows v7.00, and anyone opening the file sees which program wrote it and which release. Still settable.
  • Feature The page carries a footer saying what made it - the same three-cell strip a SWAR file ends with, in the same place, reading the OpenPairings release, the pairing engine's version, and a build stamp. The engine is there because it is the one fact about a published round that is otherwise nowhere in the file, and it is what somebody asks about when a pairing looks wrong. The federation's software signs that strip with its own name and its author's initials; neither is copied onto a page they had no hand in.

[0.36.2] - 2026-09-04

  • Fix Publishing to the federation failed at the indexing step. The guid's curly braces were sent raw, and { and } are not legal in an HTTP request target - the client refused before anything left the machine. SWAR's own notes say to keep the braces and its tool needs a flag to manage it, which reads as though the server demands them; really that flag just makes a tool willing to send an illegal request. The federation's own published addresses percent-encode them, and the server decodes them back before its script runs. Upload was never affected; a tournament already staged can be finished with "Finish indexing".
  • Fix A bye now reads like a bye. It put the word in the result column and left the opponent blank, so the label floated in the wrong place. A published file with byes in it settled the shape: the points awarded go in the result column and the word goes where the opponent's name would be. The tournament's own bye value is shown, rather than assuming one point.

[0.36.1] - 2026-09-04

  • Fix A board with an empty seat took the SWAR page down. Asking for a tournament's federation results page returned a server error whenever any round held a pairing with no white player - a seat an arbiter had vacated rather than a board they had deleted. Every shape now renders: a bye, a board seated on one side only, and a fully vacated board, which renders nothing at all rather than an empty row.

[0.36.0] - 2026-09-04

  • Feature A tournament can be published to the Belgian federation's results site. The sixth Belgian feature: generate the SWAR results page and send it, in the two steps the federation's server expects - upload, then index. Admin only, gated on the pack, and never automatic: no queue, no retry, no publish-on-save. It happens when somebody presses the button, on a confirmation that names the tournament and says where it is going. When the server refuses, its own message is shown - "bad Guid date" tells an arbiter which part is wrong, where "upload failed" would not.
  • Change An upload that stages but does not index can be finished. The two steps are recorded separately, so a tournament whose file reached the server but never got indexed offers to run the second step again instead of making somebody upload the whole thing blindly.
  • Fix A tournament exported to SWAR now keeps its identity. The .swar file fell back to a bare UUID when the tournament had no SWAR id yet - not the shape the results site accepts, so an upload from SWAR would have come back "bad Guid date", and never stored, so exporting twice produced two identities for one event. Exporting now mints the id in the documented shape and keeps it, which is what lets an arbiter carry on in SWAR and upload from there without publishing the same tournament twice.
  • Fix A flaky account test no longer depends on the whole database. It set a password on every user row and asserted exactly one existed, which held only while no other test had left one behind - so it failed on some orderings and passed on others.

[0.35.0] - 2026-09-04

  • Fix An odd-sized round robin locked its cycle count a round too early. The rule that keeps single/double switchable while the rounds paired so far stay inside cycle 1 measured a cycle as "players minus one" - true only for an even count. An odd count plays a full round per player, because the bye is a phantom opponent, so a five-player event locked at round 4 of its own five-round schedule, and a five-player double locked at round 8 of ten. The length now comes from the schedule builder itself rather than a second copy of the arithmetic.
  • Fix A late registration could unlock a cycle count that was correctly locked. The same rule counted every player row, including someone who registered after round 1. A round robin never reschedules around a latecomer - they never receive a pairing number - so counting them implied a longer schedule than exists, and a fully-paired four-player event read as editable again the moment an unrelated fifth player signed up. It now counts the frozen roster the schedule was actually built from.
  • Verified The rest of the round-robin flow was audited and is correct. Schedules, colours across both cycles, the odd-count bye, standings and tie-breaks with unplayed games, withdrawals, the crosstable, and a TRF round trip were all swept for three to ten players and both cycle counts: every pair meets exactly once, the second cycle reverses the first's colours exactly, and nobody receives two byes. The round trip and the property sweep are now permanent tests.

[0.34.0] - 2026-09-04

  • Feature A locked setting is a guard rail now, not a wall. Settings that freeze once a round is paired - the pairing system, the engine, the match formats, category pairing, the absence-point rules - can be unlocked one at a time and changed. The lock still stops the accident; it no longer stops the decision.
  • Change The warning says what it costs, per setting. Clicking a locked control already explained why it was locked; it now offers to unlock, above a sentence naming what that particular change would do to rounds already played. Swapping the pairing engine hands the new one a history it did not produce; changing the absence rules rescores every paired round on the spot. "Are you sure?" would have told an arbiter nothing.
  • Change An unlock lasts one save. It lives in the page, never in the database, and closes again when the save succeeds - so it cannot quietly stay open for the rest of the tournament. A failed save keeps it, rather than making the arbiter click twice for one change.
  • Change Changing an unlocked setting is written to the audit trail as its own entry, with the field and both values, rather than disappearing into the diff of an ordinary settings save. It is the record that makes the decision defensible weeks later.

[0.33.0] - 2026-09-04

  • Change "Live view" is now "Local view". "Live" reads as live to the public, and that page is the opposite of public: it is the screen in your venue and your own tab, and since it began showing only published rounds the old name invited exactly the confusion that gating exists to remove. What is public is OpenResults. The address and the module keep their names; only the label changed.
  • Fix The pairing list reads as a pairing sheet again. White was left-aligned, the result centred in a fixed column, and black left-aligned after it - so a short name drifted away from the result and a long one crowded it, and the result never looked centred between the two players. White is now right-aligned, black left-aligned, and the two names share the width equally, the way a printed pairing sheet reads. The projector view follows, with a narrower result column since it is read from across a room.
  • Change Standings printing lives with the standings. The pairings page no longer carries a Print standings button. Printing standings as they stood after an earlier round is no longer offered in the interface, only by its address - the standings page has no round to pin it to, and pinning one to that page's own Print would have silently dropped the manual-ranking banner, which only appears when no round is requested.

[0.32.0] - 2026-09-04

  • Change A TRF will not leave without its round dates. FIDE requires one per round, and a file that reaches them without it comes back after the event, when fixing it means re-exporting and re-submitting rather than filling in a field. The refusal names which rounds are missing and points at Settings, Dates.
  • Change A registration list still exports, dates or not. It has no rounds to date, and it is the file an arbiter checks a registration list against - refusing it would have made the commonest pre-tournament export impossible. A partial export (?rounds=1-3 of a nine-round event) likewise needs dates only for the rounds it actually contains.

[0.31.0] - 2026-09-04

  • Fix The column ruler now appears on a roster too. The ruler and the field legend were written only when the tournament had at least one round
    • but a registration list is exactly the file somebody holds against the column positions, checking whether a name has overrun its 33 characters. The legend now appears either way, stopping at the rank column rather than inventing game blocks for rounds that do not exist. Engine pin moved to Ainalrami v0.16.0, which carries the fix.

[0.30.0] - 2026-09-04

  • Fix A sync during a live round is a warning, not a wall. The refusal was wrong the moment it met a real installation: "in progress" means paired and not yet fully scored, which for a club championship is true from September to June, so the rating lists could never have been synced at all. The warning also names three and counts the rest, rather than listing a season's worth in one sentence.
  • Change And the risk it guarded is far smaller than it was. the write lock is free between chunks, and what remains long is two single statements - the bulk delete and the full-text index rebuild - measured in seconds. A result entered during one of those waits rather than fails, and if it ever does fail it says so.

[0.29.0] - 2026-09-04

  • Change A round is live, or it is not. Putting a round on a screen in the hall is publishing it - the people it would be withheld from are the ones standing in front of it - so there is no longer a state where a round is on the wall while it is held back from the results site. A round is live, or it is not. When a paired round has not been published, the page says so and links to where you publish it, rather than quietly looking a round behind.
  • Change The standings follow the same round. showing round 4's boards above a table that already counts round 5's results would give the withheld round away just as surely.

[0.28.0] - 2026-09-04

  • Feature A projector view that cycles when the boards do not fit. When there are more boards than the screen can hold it pages through them, about twelve seconds each, and comes back round. A page counter and a progress bar say which page is up and how long is left - somebody looking for board 47 needs to know it is coming and roughly when, or a rotating screen is worse than a still one. Tap to pause and read; tap again to carry on. If every board fits on one screen, none of that appears.
  • Change It fits itself to whatever screen it is plugged into. The browser measures how many rows the glass can hold and tells the server, and re-measures when the window is resized or the screen rotated, so nothing has to be configured for a particular television.
  • Change High contrast by default, because it is aimed at a hall. "Use my theme" switches back for a dark hall or a good screen.
  • Change A URL that opens straight into it, for an unattended screen.

[0.27.0] - 2026-09-04

  • Change One code, one phone. The first device to scan the QR or type the 6-digit code claims it; a second is told the code has already been used and to ask for a new one. Before this, one code enrolled any number of phones until it expired, which meant a printed sheet left on a table was a working credential for a day, and the audit trail - which records the code, not the device - could not tell three helpers sharing one code apart.
  • Change A code can carry a name, so the right one reaches the right person. The list also says whether a code has been scanned yet, which is how you tell that the one you just handed over actually arrived.
  • Change Codes already handed out become single-device too. Unlike the access levels, which were backfilled to keep existing phones exactly as they were, this refusal is visible and recoverable: the second phone is told why, and the arbiter mints another.

[0.26.0] - 2026-09-04

  • Change The phone shows what is left to do, not what is done. A board drops out once its result is in, the header counts boards remaining rather than boards paired, and Show all brings the finished ones back for checking or correcting. At helper level a finished board could not be changed anyway, so leaving it in the list only asked someone to scroll past work they were not allowed to touch.
  • Change A board you just entered waits a moment before it goes. Clearing a result returns the board to the list immediately, with no confirmation of its own, because that is already what clearing looks like.

[0.25.0] - 2026-09-04

  • Feature An enrolled phone gets only the access it needs. A code is minted as a helper by default: it may fill in a result on a board that is still blank, may not change one already entered, and sees only the latest paired round. A deputy keeps the old run of the place - any paired round, corrections included. Either can be limited to a range of boards. The level and range show in the device list and in the audit trail, which matters because one code can be used by any number of phones: the trail records the code, so "Boards 1-10, helper" says far more than a token id when two people disagree about a board.
  • Change Codes already in circulation keep the access they had. The migration backfills every existing enrolment to deputy; only newly minted ones default to helper. A migration should not quietly narrow a credential somebody is already carrying.
  • Change Phone enrolment is refused outright on a local run. A local run has no accounts at all, so an enrolment token would be the one credential that worked from another machine the day such a run could answer one.
  • Fix Messages on the phone's result page were invisible. That page had no flash outlet, so every put_flash on it went nowhere - including the existing "this tournament is archived" refusal, which meant a helper tapping a result on an archived tournament saw nothing happen at all.

[0.24.0] - 2026-09-04

  • Change No results site, no publishing controls. There was a "Published / Not published" state and a dead "Turn on" button on a page that could not publish anything. A tournament that is already publishing keeps its controls even if the connection later goes away - otherwise there would be no way to turn it off.
  • Feature Publish and unpublish a round where the boards are. Both already existed as buttons in the round header, which is easy to miss; they now sit where the boards are. In "publish immediately" mode neither appears, because that mode means every round is public the moment it is paired - a line says so and points at the setting.
  • Change A rating list sync no longer holds the database for the whole import. They used to run the whole replace inside one transaction, and SQLite allows a single writer for the whole database, so every other write queued behind them - which is how entering a result could fail. Each chunk now commits on its own and the lock is free in between. The checks that refuse a corrupt or truncated download moved to before anything is deleted, since there is no longer a transaction to roll back: an import that parses no rows, or fewer than half the current list, still leaves the existing list untouched.
  • Fix An interrupted FIDE sync could break player search for good. The import drops the search index's triggers and recreates them afterwards; with no transaction to undo that, a cancelled run left them gone, and the next run - which recreated only what it had found at its own start - found none and so restored none. From then on every change to a player would have left the index stale, silently. The triggers now have built-in definitions to fall back on, and a sync that finds them missing says so and puts them back.

[0.23.0] - 2026-09-04

  • Fix Entering a result could fail while a rating list was syncing. A sync holds SQLite's single database-wide write lock for its whole run, so a result entered at that moment waited 15 seconds and was then refused - and because a lock arrives as a raised error rather than a returned one, it bypassed the handling every write has and took the page down with a generic "something went wrong". Nothing was ever silently lost: a refused write is never written, and the page re-reads the database on the way back. But the arbiter had no way to know that. It now says, in as many words, that the change was NOT SAVED and nothing was written.
  • Change A sync yields to a round in progress. A rating list can be refreshed at any time; a result cannot wait for one, so the sync is what yields. Both the FIDE and the Belgian sync are covered, and the guard is on the handler rather than the button - a control missing from the page is still an event anyone can send.

[0.22.0] - 2026-09-03

  • Change Every version bump now publishes a release with binaries. Releases used to be gated on a tag alone, and tags were easy to forget: the version moved from 0.18.0 to 0.22.0 with none behind it, so the newest build anyone could download was four releases old. A push to main whose mix.exs version has no release yet now cuts one, which means bumping the version and cutting a release are the same act. The commits after a bump publish nothing until the version moves again.
  • Fix The Belgian rating list sync stopped at “0 of 35849”. Clearing the old roster fired one full-text-index scan per deleted player, so the cost grew with the square of the roster: measured at 141 ms for 1,000 players, 538 ms for 2,000, and 2,136 ms for 4,000. At the current ~36,000 members that came to roughly 171 seconds, just past the 180-second watchdog that gives up on a stalled import - so a sync that had been quietly slowing for weeks began failing outright. The index is now emptied in a single statement first: the same clear-out takes 145 ms. A test guards the cost.
  • Fix Settings cards had no styling at all. .set-card was used in six places across the optional-features and admin pages but never had a CSS rule, so each one rendered as bare text on the page background - no surface, no border, no padding.
  • Change Each federation now reads as a pack you can switch on. Each switch is a full-width row you can click anywhere on, and an enabled one is tinted. The reassurance about tournaments is styled as an aside rather than competing with the cards that carry the switches.
  • Change A deploy no longer slows the running site to a standstill. The build ran unconstrained on a two-core server while the previous version was still serving, taking both cores; it now runs on one, at low priority.

[0.21.0] - 2026-09-02

Added

  • Feature The Belgian features are now yours to switch on or off, one at a time. A new page under your account lists them: the national rating-list sync, the player lookup that reads it, the bulk club update, and SWAR import and export. Off, the buttons are simply not there rather than there and refusing.

    Turning one off never changes your tournaments. It hides an entrance. Everything already imported - players, clubs, scores, categories - stays exactly as it is and scores exactly as it did; there is a test that imports a Belgian club tournament, turns all five off, and checks the standings and every scoring setting are identical afterwards.

    The lookup and the club update read the list the sync fetches, so with the sync off they search whatever was last downloaded. That is a real choice ("last month's list is fine"), so the page explains it rather than the switches enforcing it.

    Nobody loses anything on upgrade: if this machine is set up for the KBSB, or you have ever imported a SWAR tournament, or a member list is already downloaded, all five arrive switched on.

Fixed

  • Fix Buttons could disappear off the edge of the page. The top bar and the tournaments header packed their controls into a row that could not wrap, and the page clips what overflows rather than scrolling it - so a control that did not fit was not merely off-screen, it was gone and unclickable. In Dutch it bit hardest, because the words are longer: "Log out" is "Afmelden", "Tools" is "Hulpmiddelen", and at a 1000-pixel window the log-out link was rendering 25 pixels past the edge. It was already happening in English too, on a part-width laptop window with a tournament open. The rows wrap now, and a test keeps them wrapping.

Changed

  • Change Seven themes, not eleven. Solarized, Solarized Light, Nord and Nocturne are retired; Catppuccin is now called Mocha. If Catppuccin was your choice you keep it under its new name - the browser rewrites the stored value rather than resetting you, which is the difference between a rename and a removal. A retired theme falls back to the system default instead of leaving you on a palette that no longer exists.

Added

  • Feature Three new looks, and two retired. Slate (cool grey-blue, for a screen rather than a page), Paper (warm off-white with a brick accent) and Board (cream and board-green, the colours of the equipment) - the same three rooms the results site and the KBSB database manager already offer, so the suite looks like one thing. Dracula and Tokyo Night are gone.

    If either of those was your choice, your browser will quietly move you to the system theme rather than leaving you on a palette that no longer exists. Board's warning and error colours had never been chosen anywhere, so they are new: a leaf green kept deliberately lighter than the board-green accent so success does not read as "accent", boxwood ochre, and the brick red of a fallen flag.

Changed

  • Change Every theme and accent combination is still checked for readable text on every build - 616 measurements across eleven themes, none below the threshold.

[0.20.0] - 2026-09-02

Added

  • Feature Bringing a tournament back now brings the results with it. Returning the file replaces this copy with what came back and then unlocks, in one go - if any part of it fails, nothing changes and the tournament stays locked. A restore point of the frozen state is kept first, so a wrong file can be undone; if that restore point cannot be written, the return is refused rather than risked.

    Three things must agree before anything is replaced, and one of them binds the contents rather than just the wrapper: the returning file's own history has to contain the moment this copy was handed over. A file that never left here cannot carry it.

    A tournament whose lock was forced open refuses a returning file, because after a forced unlock both copies can have real work in them - it says so, and names the safe route instead of guessing which one wins. What still does not come home is the record of who typed the results over there; the results themselves do.

  • Feature Move a tournament between this server and a copy on your own machine. Hand it off and it locks here - read-only, with a banner on every page saying where it went - and downloads a file. Import that file elsewhere and the tournament is live there. Bring it back with the returning file and this copy unlocks. It is a checked-out library book, not a sync: exactly one copy is live at a time, and nothing is ever merged.

    Handing off carries the audit trail with the tournament, and the collaborator list as invitations to re-accept. It deliberately does not carry helper phones - a code that lets a phone enter results here must not start working somewhere else because a file moved - so those need re-enrolling on the other side. The panel says all of this before you commit to it.

    If the other machine is lost, stolen or wiped, an owner can force the lock open. It is folded away, asks you to type UNLOCK, records itself separately in the audit log, and says the true thing rather than a vague warning: the other copy still exists, this does not close it, and whoever has it must never open it again.

Fixed

  • Fix A checked-out tournament can no longer be binned, purged or archived here. Purging one destroyed the row the returning file needs, stranding the other copy permanently.
  • Fix Taking a tournament off the results site checked whether it was allowed to - after doing it. On a frozen tournament the published copy, its history and its collected entries were destroyed and the key cleared, and only then was the change refused. Rotating a public address hit the same path, so a refusal left the tournament unpublished with its key gone.
  • Fix Two pairing entry points read the archived flag directly instead of asking whether the tournament was writable at all, so they would have paired a checked-out tournament and then failed mid-write. A results import into one crashed outright, and generating a phone QR for one produced a code whose results the server then refused.
  • Fix Nine refusals said "this tournament is archived" whatever the real reason was - sending an arbiter to unarchive a tournament that was not archived.
  • Change The FIDE/KBSB sync line is gone from the top bar. The Connections page still shows both in full; the top bar had too much in it.

[0.19.0] - 2026-09-02

Fixed

  • Fix Buttons you can actually read, on every theme. The main action button filled itself with your accent colour and wrote white on top - hardcoded. Across the ten themes and nine accent choices, 64 of 90 combinations failed the accessibility threshold for readable text, the worst at 1.81:1 against a required 4.5. On Nord the button you are most meant to find scored 2.00. Each theme and accent now names the ink that belongs on it - Nord's is 6.24 - and the delete button, which had the same fault in six themes, is fixed the same way. Eight other places writing white on a coloured fill went with them; no hardcoded white is left in the stylesheet.

  • Change One action per screen actually looks like one. The report downloads on the norms pages and the arbiter tools - IT3, IT4, FA1, IA1 and the combined forms - were each styled as the page's main action, so a page offering six of them shouted six times. They are the quieter tonal style now; saving still stands out, because saving is the thing you cannot get back by clicking again. Same for the "Add" button that builds a forbidden-pairing list, which is a step rather than the commit.

  • Feature A quieter middle button (tonal) for the second action on a busy page: tinted with the accent and lettered in it, rather than shouting or disappearing. Readable by construction on every theme - the worst combination measures 4.65.

  • Fix Two hover states were darker than the thing they hovered over, and one theme's accent hover walked its own contrast down; both now move away from their ink rather than into it.

  • Fix The Save button on the publishing card carried a class that does not exist (pe-btn-primary), so it has been rendering as an ordinary button since it was written.

  • Fix The last yellow boxes, on Nord and every other theme. The earlier fix moved the app's own components onto the theme's colours, but six places were reading daisyUI's --color-warning/--color-success instead - tokens no theme overrides, so they stayed a fixed amber and green while everything around them changed. The changelog's own Fix tags were one of them. Two more surfaced while widening the net: a warning panel tinted with a hardcoded red, and three connection-status rules reading a colour token that does not exist, so their grey never applied in any theme.

  • Fix The theme test now reads the templates as well as the stylesheet, understands oklch()/hsl()/rgb(), and fails on any colour token the themes do not actually override - the three gaps that let the above through.

  • Fix Standings ties are ordered by an explicit rule - rating, then name, then pairing id - instead of whatever order the rows happened to arrive in. On a fresh tournament with no games the previous order could differ between runs; no computed placing changes.

  • Security In production the server now listens on loopback only. The only thing that ever connects to it directly is the tunnel on the same machine; binding every interface left the whole app one firewall rule away from the internet. OPENPAIRINGS_LISTEN_IP widens it deliberately for any deployment that needs that. Local mode was already loopback-only.

  • Security The build pipeline pins every GitHub Action to a commit instead of a movable tag, grants write access only to the tag-gated release step rather than every build, checks the lock file on every run, and lets Dependabot propose action updates.

  • Security The deploy script no longer prints the server's secrets. It read the systemd unit - SECRET_KEY_BASE, the mail password, the tokens - through the helper that echoes output, on every redeploy after the first; that read is silent now, the deploy-notice token travels to curl over a pipe instead of a command line, and anything that still gets printed passes through a redactor. The same script now refuses an unknown SSH host key, prefers key authentication (DEPLOY_SSH_KEY, password only with DEPLOY_ALLOW_PASSWORD=1), runs the service as its own system account with systemd's sandboxing, makes the database directory private, and sets TRUSTED_PROXY_HOPS=1 so rate limits count real visitors. All of it takes effect on the next deploy. The secrets already printed to terminals should still be rotated.

  • Fix The local-mode documentation now says, in one sentence, why the binary must never sit behind a tunnel or reverse proxy.

  • Change Pairing engine Ainalrami v0.15.0. The TRF reader and writer now count columns in bytes like every other implementation, so a file from Swiss-Manager or bbpPairings with an accented name imports with that player's games intact; a byte-order mark no longer swallows the first line; a truncated line no longer yields a wrong opponent number; and the matcher breaks ties on a fixed rule rather than map order (checked byte-identical over 72,140 pairings). Nothing in the pairings this app produces changes.

  • Fix Ten forms can now recover what you typed after a dropped connection. They had no id, so LiveView could not restore them after a reconnect - on venue wifi that meant retyping. The test suite now refuses a form without one, so it cannot come back.

  • Security Two phone-entry codes can no longer be active at once. A unique index now enforces it; generation draws again on a collision instead of giving up after twenty tries and inserting the duplicate; and the public code page answers "wrong code" rather than a 500 if the database ever drifts.

  • Security Password-login lockout counts the real client address, not the tunnel's, once TRUSTED_PROXY_HOPS is set - so thirty wrong passwords from one place lock out that place, not everybody.

  • Security The rate limiter counts atomically. Under a burst of two hundred simultaneous hits it counted 109 to 127; it now counts two hundred.

  • Security A newline in the language switcher's return address no longer causes a 500. The filter is an allowlist now, and anchored so a trailing newline cannot slip past it.

  • Fix Revoking a helper's phone access reaches their open page immediately - it is sent back to the code screen - instead of only when they next try to enter a result.

  • Fix A crafted settings event with an unknown field, or a context-menu position that is not a number, no longer crashes the arbiter's own page.

  • Change The four pages that show bye chips now fetch the absence-cap counts once per render instead of once per chip; the unused second rate-limiter module is removed.

  • Security The public norms tool caps the number of extra arbiters at twenty. The count reached the server unchecked through a hidden form field and drove the spreadsheet expansion; a single request asking for a hundred million meant ten gigabytes of XML. It is clamped at every parse, and the expansion itself refuses anything above the cap as a last line of defence.

  • Fix An out-of-range "master file" choice on the public tools page no longer crashes the session; the index is parsed and clamped, and the combiner reports an invalid choice instead of raising.

  • Fix A search for a number longer than any FIDE id no longer crashes - on the players page, the arbiter picker, or the results site's lookup. Anything outside the real id range is simply "no such player".

  • Change The public tools page computes its player-count explainer once per file change instead of on every render, and the norms page debounces its officials form.

  • Fix A SWAR file with one unusable player no longer crashes the import. A blank name or an out-of-range FIDE id made the whole import screen fall over instead of saying which player was wrong; it now reports the player and the field and writes nothing, as the TRF importer already did.

  • Fix A corrupt SWAR file can no longer pair a player against themselves, or against someone whose own record disagrees. Each side's opponent is now checked to name the other back, the same two guards the TRF importer has had since July.

  • Fix Two SWAR players sharing an internal number are refused up front instead of one of them silently losing every game to the other.

  • Fix Absence caps above 255 no longer wrap when exported to SWAR (300 used to become 44); the export clamps and logs, and the settings form refuses the value in the first place.

  • Security A backup file cannot ask for an absurd key-stretching count. The restore command used whatever iteration count the file's own header claimed; it is now bounded to a sane range.

  • Security The FIDE rating-list download checks the archive's declared size before inflating it, with a generous ceiling, so a hostile zip cannot exhaust memory on its way in.

  • Fix A tournament whose name has no Latin letters exports as tournament-<id>.trf instead of .trf.

  • Fix A substitution dialog left open could seat one player on two boards. "Swap with…" checked the bench player once, when the dialog opened, and the dialog deliberately stays open while another arbiter edits the round. If that player was paired onto another board in the meantime, confirming still went through. The write now re-checks, inside its own transaction, that the player belongs to this tournament and is not already seated in the round, and refuses otherwise. This was the third of three seating writes the August sweep named; the other two were fixed then.

  • Fix Un-pairing a round is one transaction again. It deleted the round, then the byes, as two separate steps; a crash between them left bye rows that a later re-pairing silently kept, and a player seated for real that round was scored for a game and a bye. Both deletes now commit together, and the standings ignore - with a warning in the log - any bye row for a round in which the player has a game.

  • Fix A restore that fails no longer moves the history pointer. The "before restoring to…" snapshot and the pointer move were committed before the restore itself ran; when the restore was rejected, the tree pointed at a state the data was never in. All three now succeed or fail together.

  • Fix Accepting the same registration twice at once created two players. The pending-to-accepted transition is now the guard: whoever claims it first creates the player, the other gets "already decided".

  • Fix A player's team must belong to the same tournament. The edit form could carry a team id from anywhere; nothing displays teams yet, so this closes a door rather than a hole.

  • Fix The external pairing engine now has a deadline. JaVaFo ran with no timeout; a pathological position could hang the arbiter's pairing indefinitely. It is now given sixty seconds and reports an error afterwards.

  • Change Absence-cap counts are fetched in one query per tournament rather than one per rendered row, on the pages that adopt the new call; the four current callers still use the old one and will follow.

  • Fix Amber warning boxes ignored the theme. Archived banners, the "careful, this is hard to undo" notes on the settings pages, the pairing-confirmation warnings, the history rail's tags and both status pills in the top bar were painted with a fixed amber, so on Nord, Dracula, Tokyo, Nocturne and Catppuccin they sat in the middle of the palette looking like something had failed to load.

    Every theme has always defined its own --warn — Nord's is its aurora yellow, Dracula's its pale citrus — these components just never used it. They do now, along with the reds and greens beside them, so a warning looks like a warning in the theme you actually chose.

    Two of them were worse than off-key: the OpenResults visibility toggles and the on/off state pills referenced a colour that does not exist, so every theme, including the two the app ships with, fell through to one hardcoded green.

    A test now reads the stylesheet and fails on a colour written into a component instead of taken from the palette, on a theme that forgets to redefine one, and on a fallback to a colour no theme defines. That last check is what would have caught the missing green.

[0.18.0] - 2026-08-29

Added

  • Feature Where you publish and where spectators go are two settings. They were one field doing both jobs, and it cost something: on a server hosting both applications, every publish left the box, went out to the CDN and came back in through the tunnel to reach a process one hop away — because the single address had to be the public one. Pointing it at localhost would have broken every share link, QR code and printed URL.

    Leave the new "Public address" blank and nothing changes. Set it, and the server can send over loopback while spectators still get the public name. The win is not speed — publishing is queued and nobody waits on it — it is that two applications on one machine no longer need DNS and a CDN to be up to talk to each other.

    On a hosted install the deploy configures all three (send address, public address, token) from values it already has, and reconciles them on every run — so change them in the deploy configuration, not on the Connections page, where the next deploy would undo the edit. The mix pairings.publishing task still defaults to filling blanks and refusing to overwrite; it is the deploy, which owns one known host, that asks for the stronger behaviour.

  • Feature A record of machine-wide actions. Role changes, backup downloads, publishing changes and rating-list syncs are now audit entries rather than log lines, and an administrator can read them on the Admin page. Log files rotate; a record should not. The audit trail holds them alongside per-tournament rows, and a tournament's own trail is unchanged.

  • Feature The standalone build opens your browser. It used to start a server and sit there, leaving you to know that http://localhost:4000 was the thing to type. It now opens your default browser at the right address once the server is actually listening, so double-clicking the binary opens OpenPairings.

    Only on a local run — a hosted server never tries this — and OPENPAIRINGS_NO_BROWSER=1 turns it off for anyone running the binary headless or over SSH. It cannot stop the app starting: if the launch fails the failure is logged and the tournament goes on.

  • Feature An Admin page, next to Connections and only for administrators. It shows who may administer this installation and lets an administrator change a role — grant support to a colleague, or take it back — plus the version, whether this is a local or hosted run, and the database size.

    Three things it refuses, each a way somebody would otherwise be locked out: you cannot change your own role; the last administrator cannot be demoted; and an address declared in the server's configuration is shown but not editable, because a button that appeared to revoke one and was undone by the next restart would be a lie.

    Granting roles from a screen is a change of mind from mix pairings.role, which shipped saying there should be no screen. That argument was about the first administrator and still holds — nothing here can bootstrap one. It does not reach the second: an administrator granting support gains nothing they did not have, since the same session can already repoint publishing and download the whole database.

  • Feature The publishing state is in the top bar, on every page. A dot, a word and the round trip: Live, Sending while the queue is not empty, Refused for a rejected token, Offline when the results site cannot be reached, Not publishing when nothing is set up. It links to Connections, because a pill that says "Offline" and cannot be acted on is a worry rather than information.

    Publishing is deliberately invisible — queued, retried, never in the way of pairing a round — and the cost of that is an arbiter having no way to tell "results are going out" from "nothing has left this laptop since Tuesday". Both look like nothing happening. An arbiter does not visit the settings page mid-round; they pair, enter results, and trust it is going out. That trust is the thing worth instrumenting.

    The word carries the state, not just the colour: colour alone is unreadable to a colourblind arbiter and ambiguous to everyone at a glance. Amber pulses only while sending, which is the one state that is temporary, and the animation stops under prefers-reduced-motion.

    One poller serves the whole installation and everything else reads a cached value, so this costs no network request per page — which matters most when the results site is down, since that is exactly when every page would otherwise be waiting fifteen seconds to say so.

  • Feature Administrator and support roles. Who may change how an installation is wired up is now an explicit role rather than a side effect of how somebody signed in.

    admin may change the publishing connection, take and download backups, and start a rating-list sync. support may look — the connection status, the backup list, the sync state, and the read-only connection check — because "why did publishing stop" is a question worth answering without handing over the ability to cause it. owner is everyone else, and is what every existing account becomes.

    Granted from the command line and nowhere else:

    mix pairings.role                          # who holds one
    mix pairings.role you@example.com admin
    mix pairings.role you@example.com owner    # revoke

    There is no screen for it on purpose. Shell access on the server is already the highest authority there is, so making it the way roles are granted keeps the authority to grant admin from being something admin itself confers.

    A deployment names its administrators once, in DEPLOY_ADMIN_EMAILS, and that drives two independent routes to the same authority. The address reaches the systemd unit as ADMIN_EMAILS and may administer with no database row at all; and the deploy also runs mix pairings.role --ensure after migrating, so it holds the role in the database too. Either alone is enough. The redundancy is the point: an installation nobody can administer is recoverable only over SSH.

    This grants nothing new — the unit is root-only, and root could run the mix task anyway. mix pairings.role with no arguments lists both kinds and marks which is which.

    --ensure re-grants on every deploy, so the deploy configuration is the source of truth: demoting somebody still listed there lasts until the next deploy. Revoking for good means removing the address and running mix pairings.role <email> owner. An address with no account yet is reported and skipped rather than failing, so a first deploy is never aborted by it.

    Your own machine needs none of this and is not affected.

  • Feature Backups can be downloaded. A Backups card on Connections lists what is on the machine, takes one on demand before a risky change, and hands any of them over.

    They are written beside the database, which survives a bad migration, an accidental delete and a botched restore — most of what goes wrong — and does not survive the disk. Copying one off-site needs a destination this app has no business holding credentials for, so instead it gives you the file and says plainly that a backup kept only on the thing it protects is half a backup.

    Administrators only, and the card says why: a backup is every tournament, every player, the email addresses people gave the entry form, and every publishing key. On your own machine there is no gate — the file is sitting beside a database you can already open.

  • Feature A connection indicator for publishing. Green when the results site answers, amber while something is being sent, amber again if the token is refused, red when it cannot be reached, grey when nothing is set up — with the round trip in milliseconds and the reason in words beside it. On Connections in full, and compact on a tournament's OpenResults page.

    Publishing is deliberately invisible: queued, retried, never in the way of pairing a round. That is right and it had a cost — "my results are going out" and "nothing has left this laptop since Tuesday" looked exactly the same, which is to say like nothing happening.

    A refused token and an unreachable server are shown differently on purpose: one is a working network and a wrong secret, the other is a network problem, and they want opposite fixes. And because an empty queue means either "everything has been sent" or "nothing was ever queued", it also says when something last actually went.

    The check runs off the page's own process. Doing it inline would have frozen the settings page for up to fifteen seconds on exactly the machine whose connection somebody had come to check.

  • Feature Far more control over what the public page shows, in far less space. Seventeen switches instead of seven, grouped by what they decide: which pages exist at all (standings, round pairings, player cards, byes), what is shown beside each name, which tournament details ride in the header (city, dates, arbiter, deputy, tempo, FIDE badge), and which extra columns a shown page carries.

    Whole pages can now be withheld — some arbiters hold the standings back until the last round is in, and now they can. What still cannot be hidden is the truth on a page that IS shown: there is no way to publish a standings table with the names removed or a pairing list without results. A page is published honestly or not published.

    A withheld page says so and links back to the tournament, rather than claiming not to exist — the event is right there, and a 404 would send somebody hunting for a link that was never broken.

    The old layout took most of a screen for seven boxes and would have taken three for seventeen. It is a grid of compact toggles now, green when on.

  • Feature There are backups now. There were none. Every tournament, result, registration and publishing key lived on one database file on one machine — and the keys are the only thing that can withdraw a published tournament, so losing the file meant losing the ability to take your own event off the public web.

    One is written a day, and mix pairings.backup takes one on demand before a risky change. --list, --verify and --restore are the other half: a backup is worth exactly what its restore is worth, so the file is a real database that gets opened and checked before anything is recovered.

    The rating lists are left out. They are 207 MB of the 219, they are a downloaded copy of somebody else's data, and a sync rebuilds them — so a backup is about 17 kB and it is worth keeping a month of them. The tables are emptied rather than dropped, because a database missing them would not match its own migration history and would refuse to start.

    Restoring writes the recovered database beside the live one and prints the three commands to swap it in. A SQLite file cannot be replaced underneath an open connection pool without risking the thing being recovered.

    Set PAIRINGS_BACKUP_PASSPHRASE to encrypt them — worth doing, because an unencrypted backup carries the email addresses people gave the entry form.

  • Feature Settings → Results site. One page for everything about a tournament's public existence: publish it, list it or not, choose what the public page shows, open the entry form, get the share link, move it to a new address, take it down.

    These were spread over two pages and read as unrelated — the publish switch under Tournament next to the logo uploader, the entry form under Options next to pairing preferences. They are not unrelated. Every one of them answers part of "what does the public see", and an arbiter putting an event online should answer that on one screen rather than assemble it from two. Reviewing entries stays with the players it creates; there is a link.

  • Feature Choose what the public page shows. Ratings, titles, federations, clubs, categories, tiebreak columns and player cards can each be turned off per tournament. A club evening and an international open have genuinely different answers about whose Elo belongs on the open web, and the arbiter is the one who knows which this is.

    Names, board numbers, results and placings are not on the list: they are the tournament, and an arbiter who does not want them public should not publish. Hiding the tiebreak columns hides the arithmetic, not the result — the order is still exactly the one the arbiter computed.

  • Feature Publishing a tournament no longer advertises it. Publishing gives a tournament a public address; putting it on the results site's front page is a separate, deliberate choice, and it is off by default.

    It shipped defaulting to on, reasoning that this was what publishing had always meant. The reasoning was about not changing behaviour and it produced behaviour nobody chose: sixteen tournaments appeared on the front page at once because a migration had switched publishing on, not because sixteen arbiters had decided to advertise their events. Every existing tournament is reset to unlisted — nothing is taken down, and every link still works.

    The settings page says plainly that this is not privacy: an unlisted tournament is still readable by anyone who has the address, and addresses get forwarded.

  • Feature Entries arrive on their own. The Registrations list now fills itself once a minute instead of waiting for someone to press Pull. That was fine while this app served the entry form — the arbiter was in the app and the entries were on the same machine — and stopped being fine when the form moved to the results site, because the queue then lived somewhere the arbiter had no view of at all. A queue nobody is told about is one discovered on the morning of the tournament.

    Nothing is accepted automatically. Entries land in the review list marked "not yet arrived" and wait for a person, which is the whole model. The Pull button stays: somebody standing at the door with a queue in front of them should not have to wait out a timer.

  • Fix The public page showed the wrong board numbers. A fixed-table player takes board 1001 and the boards after them renumber to close the gap — that is what the arbiter's screen and the printed sheet show. The published page showed the raw column instead, so a game printed as board 12 in the hall appeared online as board 1001, with every board after it off by one. The order was wrong too.

    The snapshot carried only the real board number, on the reasoning that a label is a rendering decision belonging to whoever draws the page. Right in principle, wrong in practice: nobody drew it. Both now travel, in the order the arbiter sees.

  • Fix A hand-set standings order no longer hides that it has gone stale. The arbiter's own page warns when a result has changed since the order was set, or when a player joined afterwards and has not been placed. Neither warning travelled, so the public page said "the arbiter chose this order" while their screen said "…and it may no longer match the real standings". Both are published now, read from the same two functions the arbiter's page calls so the two cannot drift apart.

  • Feature Players can find themselves on the FIDE list again. The entry form offers a search that fills in name, FIDE ID, rating, title, federation and birth year. The old local form did this and the results site could not, because the FIDE list lives on the arbiter's machine and did not move with the form — so players had been typing FIDE IDs from memory, and arbiters correcting them by hand.

    The list still lives here. The results site borrows a search of it over the publishing token, rate-limited, and proxies it server-side so the arbiter's address and the token never reach the page. The rating that comes back is the one for this tournament's tempo: a player entered at their standard rating in a blitz event is seeded wrong.

    Off unless both machines are deployed together. Where they are not, the form asks people to type their own details exactly as it did yesterday — a search box that cannot search is worse than none.

  • Feature Title and birth year on the entry form. Both were collected by the old local form and neither survived the move, so arbiters were typing them. The title is a list of the eight FIDE awards rather than a text box, because free text collects "gm", "Grandmaster" and "GM (inactive)" and somebody normalises all of it by hand. Neither is required.

  • Feature Remove a tournament from the results site. In Settings, under Publish to the results site, once something has actually been published. It deletes the public page, every earlier snapshot in that tournament's history, and any entries collected for it, and it says so before you confirm. Nothing on your machine is touched — the tournament, its players and its results stay exactly as they are.

    A takedown that does not reach the server changes nothing and tells you why. In particular a "not found" reply is reported rather than treated as "already gone": it can equally mean the results site is too old to have a takedown at all, and being told your event was withdrawn while it is still up is worse than being told nothing happened.

  • Feature Entries from the results site. The public form on OpenResults collects sign-ups; a published tournament now has an Entries page (linked from its Settings) where you fetch them and decide on each one. Accept adds the player, Discard adds nothing.

    The results site cannot put anyone in your tournament. It holds requests and this machine comes and asks — which is why there is a Fetch button rather than a list that fills itself in. An accepted entrant lands not yet arrived, exactly like the form on this machine: filling in a web page announces an intention to play, and pairing someone who never turned up hands their opponent a forfeit win.

    Rounds an entrant asked to sit out become their absent rounds, clamped to the rounds your tournament actually has — a request for round 9 of a five-round event is shown to you rather than quietly trimmed.

    A turned-down entry is kept rather than deleted, so fetching again cannot bring it back and you can still reach the person. Their email address is the one personal detail an entry carries; it stays on that page and goes into no snapshot, no TRF and no public page.

  • Feature Publishing has a UI. The Rating lists page is now Connections — everything this machine talks to, the two rating lists it reads and the results site it writes to. The address and token are set once for the machine; each tournament then has its own switch in its Settings, off until you turn it on.

    The token is never rendered back once saved, and an empty token box means "keep the one you have" rather than "clear it" — otherwise editing the address beside it would wipe a working token every time.

    Test connection is a GET against a route that cannot match anything, never a publish. A test button that published would be a trap: you press it to find out whether the settings work and a tournament goes live as a side effect.

  • Feature The results site has themes. Six, chosen from the picker beside its name and remembered per browser: the original (Paper), Night, Board (cream and board-green), Slate, and a High contrast black-on-white for a projector at the back of a hall. "Match device" is there too.

    It opens on light rather than following the device. A spectator opens this page once, often inside a club's own site, and club sites are overwhelmingly light — a dark slab dropped into the middle of one reads as broken rather than as a theme.

  • Feature Right-click a player on the results site for their card. The full opponent table — every round, who they played, their number, federation, title, rating and own total — shown in place, the way right-clicking a row does on the Players page here. Left-click still opens the page, which is what happens with no JavaScript and on a phone.

  • Feature The results site keeps itself current. Standings and pairings refresh themselves every 20 seconds instead of waiting for somebody to pull down on their phone. The pages this replaced were live, and being static was the most visible thing lost in the move.

    It swaps one region rather than reloading, so scroll position, theme and an open card all survive; it stands down on the entry form, where replacing the page under somebody typing would clear it; and it says so in the footer when the connection has gone, rather than showing a page that has quietly stopped being current.

  • Feature Elo and running scores on the published pairings. Each board shows both players' ratings and the points they carried into the round — which is what a pairing list means by score, and what explains why those two are on that board.

Changed

  • Change Settings is arranged around what you are actually doing. "Publish each round" was under Options and is now on OpenResults, with the rest of what reaches the results site. "Export / backup" has its own tab instead of sitting at the bottom of the Tournament page. The Tournament page no longer carries a signpost card pointing at OpenResults, since the tab beside it does that. And the settings tabs no longer offer Changelog, which is not tournament-specific and is already in the top bar.

  • Fix The Explain page described a colour rule the engine no longer follows. Where Article 5.2.5 decides a board, the page told arbiters this engine reads the parity on the tournament pairing number and other programs read it differently. FIDE settled that question on 28 August 2026 — against us — and the engine was updated the same day, but the explanation was not. It now describes what actually happens, and says the ruling settled it rather than presenting a live disagreement. Three other places carrying the old claim were corrected with it.

  • Change New tournaments publish rounds MANUALLY by default. They defaulted to publishing the instant a round was paired — on the public page before the arbiter had looked at it. The first person to see a pairing should be the person responsible for it: a mistake caught in ten seconds is a re-pair, and the same mistake seen by four hundred players is a correction, an announcement and an argument.

    Choosing "Immediately" now says so plainly, where you choose it. Existing tournaments that have never published are moved to manual too; one that is already publishing is left exactly as it is, because silently ceasing to publish mid-event is its own disaster.

  • Change The version number opens the changelog, and the changelog no longer asks you to log in. The version sits in the top right of every page, including the sign-in page, and was not clickable. Making it a link exposed the second half: /changelog had been behind the login only because it was added beside the tournament pages and inherited their pipeline — so a signed-out visitor clicking it was sent to a log-in screen for a document that describes the application and reads nothing but CHANGELOG.md.

  • Feature The panel says how slow the connection has been, in one line. "Slowest in 10 min: 4 ms" — and only that, when there is nothing else to say. A drop earns a second clause, because a drop is worth knowing: "Slowest in 10 min: 4 ms · last drop 2 h ago". If it is dropping now it says so in the same shape: "Drops in 10 min: 2", in bold.

    Deliberately not an uptime percentage. This app cannot see the checks it failed to make while it was down, so any figure it computed would describe how reachable the results site was during the moments the app was alive — flattering by construction. And a percentage averages away the shape that matters: 99.9% over a week is one ten-minute outage, which is nothing on a Tuesday and ruinous in round four. Real uptime needs something outside the machine watching it.

  • Fix The publishing panel said things twice. It read "Connected" and then "Connected. The address and token are both accepted."; and "last sent just now" beside "82.0 KB last sent" — the same words twice on one line, from bolting the new size on as a separate item rather than folding it into the sentence. It now reads "82.0 KB sent just now", the message stops repeating the heading above it, and the three facts on that line are separated instead of running together.

  • Feature The publishing indicator says whether it has been steady, not just whether it works now. A status light answers "right now", and the failure it hides best is the intermittent one: a hall's wifi that drops for fifteen seconds every few minutes reads green almost every time anyone looks, while results arrive late for no visible reason. The panel now keeps the last ten minutes of connection checks and says either "steady for the last 10 minutes, slowest check 180 ms" or "3 checks failed in the last 10 minutes — it is answering now, but worth a look at the network".

    It says nothing at all until there is enough history to mean it. A claim about ten minutes from an app that started ninety seconds ago would be a lie told confidently.

  • Fix The publishing indicator opens instead of sending you to the rating lists. Clicking "Live" in the top bar navigated to the FIDE rating-list page, which has nothing to do with publishing. It now opens the full status in place — the reason in words, the address, the queue, when something last went out and how big it was — and closes when you click away or press Escape. The Advanced and Settings menus gained the same click-away behaviour, which they never had either.

  • Feature How much a round costs to publish. A published snapshot is the whole tournament rather than a change to it, so its size is the one figure that says what publishing actually sends — and it moves: the new tie-break breakdown multiplied it by about 3.4 on a large event. The size of the last document sent is now recorded and shown beside when it went.

  • Feature Every build now says exactly which build it is. The top bar showed v0.18.0, which is a release, not a build — a week of commits and a dozen deploys all report the same string. It now reads v0.18.0+3f2a1c9, with the commit and the build time in the tooltip, and the same identifier travels on every published document so the results site records which build produced it. A build that does not know its own commit says +unknown rather than guessing.

    This came from losing half an hour to it: a deploy went out, the public site still looked wrong, and both sides reported the same version. Only one of the two applications had been deployed.

  • Fix The OpenResults settings page stopped looking like you had left the tournament. It was the one settings page that did not tell the layout which tournament it belonged to, so the top bar dropped every tab — Players, Pairings, Standings, Print — and its Home link turned into "Tournaments".

  • Fix Importing a SWAR file with categories now says what it could not read. SWAR's category block carries two lists of values; a player's category is read from the first, while both end up in the tournament's category list. A file using the second one therefore imported categories with nobody in them, silently. The import warns instead, naming both sets and pointing at Settings.

    What the second list means is genuinely not documented, and the candidates imply different imports — so this warns rather than guesses. One real club file with categories configured would settle it; docs/swar-import.md now says exactly what to look for.

  • Change A local install stops offering a server's controls. Running the standalone binary signs its single owner in automatically, so the account Settings link, the "Share / Team" invitation card and the sign-in/sign-up pages had nothing to do there — the invitation would have been emailed to a terminal window, and a second account created on the sign-up page could never have been signed into. All four are gone on a local install, and a bookmarked sign-in URL goes to the tournament list instead of a page that cannot help. Log out still works, because it is merely inert rather than misleading.

  • Change The version header can no longer drift. It is written in four places and only one of them is read by anything, so the other three went stale repeatedly — twice in the three days a code review spent documenting the first occurrence. mix precommit now refuses when the roadmap, the feature list or the changelog names a version mix.exs does not.

  • Change Seven documentation claims that were not true any more. The roadmap said standalone binaries had no CI smoke test (there are four), that a "paired by someone else" notice had shipped (it was removed), and that the absence-handling setting was off by default while describing its behaviour backwards. The backup document promised "every Tournament field" when fifteen are held back. The endorsement checklist called FIDE Mode and adjourned games "not applicable" without mentioning that both are hard failures under the 2026 draft. A moduledoc argued at length that local mode deliberately does not log you in, next to the code that logs you in. None of it changed what the app does; all of it would have misled the next person to read it.

  • Feature Choose which tie-breaks the public page shows. It was one switch for every column at once. Settings → Results now has a checkbox per tie-break the tournament ranks on, so you can publish Buchholz Cut-1 and keep the rest off the page. A hidden one is not sent at all — no column, no value, no breakdown — rather than sent and hidden at the other end.

    Hiding a column does not stop it deciding the order, so two players can appear one above the other with every published number identical. Both the settings page and the public standings say so rather than leaving it unexplained.

    Publishing the numbers and publishing where they came from are also separate now: "How the tie-breaks were reached" is its own toggle.

  • Feature Published results now carry the working behind each tie-break. The public site could show that your Buchholz was 14.5 and nothing about where it came from, which leaves the one question a player actually has — "why am I fourth?" — unanswerable. Each published standings row now carries, per tie-break, one contribution per round: which opponent it came from, what it was worth, and whether it was discarded by a cut or is one of Article 16's virtual opponents.

    It is sent rather than worked out at the other end, because it cannot honestly be worked out there: Buchholz sums opponents' adjusted scores, not the scores in their standings rows, so a public page adding up the visible numbers would disagree with the arbiter's. Only the tie-breaks that genuinely cannot be re-derived are sent — the Buchholz family, Sonneborn-Berger, Koya and average rating. Wins, games with Black and the running score are already visible in the results.

    Turning off the tie-break columns now withholds this too, rather than sending it and relying on the public site to hide it.

  • Fix A Keizer bye marks a hand-set standings order out of date. Byes award points without going through result entry, so every other pairing path already flagged a manually-ordered standings table as stale when it wrote one. Keizer did not. Nobody could hit it — the manual-ordering card is hidden for Keizer tournaments — but that is a reason it could not be reached, not a reason it was right. Keizer's absentee byes are also written inside the round's own transaction now, as the Swiss ones always were, instead of just after it committed.

  • Change Searching the Belgian rating list works the way the FIDE one does. Both sit side by side in the same dialog, but only the FIDE list had been given a full-text index; the national list was scanning every row on every keystroke, behind an index on last name that could never serve the query it was created for. It is indexed now, matches first names and name tokens in any order, and folds accents, so "muller" finds "Müller". The dead index is dropped.

  • Change A collaborator lookup runs on an index. Sharing a tournament creates an invite against an email address, which only gains an account id once that person signs in, so the lookup matches either — and the email half had no index, making a query that runs on every page load a full table scan of the invite table. Nobody would have noticed at present sizes; it is one line.

  • Change Pairing a round stops rebuilding what it already worked out. The per-category path had always computed the tournament's history once and passed it around; the ordinary single-pool path had not, so it built the same thing twice within one click. Pairing by category also asked for the forbidden-pairing list twice per category, for a list that is the same for all of them, and walked the whole roster's game history once per category on top of that. Measured on a 16-player round: 43 database queries before, 35 after; a four-category round, 96 before and 87, with the per-category cost now flat. The engine call still dominates a pairing click — this is housekeeping, not a speed-up you will feel.

  • Change The pairing-explanation page stopped recomputing the standings once per round. It needs each round's incoming scores, and asked for them one round at a time — a nine-round tournament ran eleven full standings computations, then a twelfth with arguments identical to one it had just made. It reads the rounds once and folds each round's prefix in memory now.

  • Change The pairing sheet asks only for what it prints. Each player's incoming score came from the full standings — every tie-break, the ranking sort, all of it — and then everything but the points was thrown away. That runs on mount, on every round switch and after every result entered. There is a points-only path now.

  • Change Tie-break scoring stopped redoing one calculation thousands of times. The adjusted score FIDE Article 16.3 uses is a property of a player, but was recomputed at every game-encounter, separately inside each of Buchholz, its two cut variants, Median Buchholz and Sonneborn-Berger. A 300-player 11-round grid did it around 9,900 times for 300 distinct answers. Computed once per player now, and carried.

  • Change "Pair the whole tournament" stopped rewriting the same setting once per round. A round robin works out its own round count from the size of the field, and the button that pairs every round at once was re-deriving and re-saving that number on every single round — a 14-player double round robin did it 26 times, each one telling every open page twice that the settings had changed. It happens once now. Nothing visible changes; there is just far less going on behind a button pressed during a live event.

  • Fix The tie-break list stopped offering three that score zero. Match points, Game points and Berlin board points need team standings, which are not built. Adding one to a tournament produced a column of noughts that separated nobody, permanently, with nothing on screen to say why. They are out of the picker now, and a tournament that already stores one — FIDE's own default set for a team event names all three — drops it from the ranking and says on the Standings page which and why. The FIDE defaults themselves are left as FIDE wrote them; hiding our own gap by editing their list would misreport the regulation.

  • Verified Koya is right as it stands. A code review flagged that the Koya tie-break reads an opponent's raw score where Buchholz and Sonneborn-Berger first apply the unplayed-rounds adjustment. Checked against C.07: Article 16 names its own scope in its opening sentence — Buchholz, Sonneborn-Berger and their variants — and Koya (Article 9.2) is not among them. No change; the citation is now in the code so the next reader does not re-open it.

  • Fix A draw stopped being called a win. Under a 3-2-1 club scheme (win 2, draw 1, plus a point for turning up) a draw is worth exactly what a win is worth. Four screens — the player card, the printed crosstable, the pairing-explanation trail and the players grid — worked out whether you had won by comparing your points against the value of a win, so all four showed a "1" where the standings beside them showed a draw. The FIDE tie-break "games won over the board" counted it too.

    The result code always knew: ½-½ is a draw whatever a draw pays. Every screen reads that now, from one table. Its neighbour "rounds worth as many points as a win" is deliberately left comparing points, because that is the tie-break's definition in FIDE's own words.

  • Fix A phone can record a played-but-unrated result. The three codes for a game that happened but does not reach the rating report have been enterable from the Pairings page since they shipped; the mobile screen carried its own copy of the list, made before they existed, so a helper at the board could not record one. The two screens read the same list now, and a build where they disagree fails rather than shipping.

  • Fix A CSV import can express every result a click can. Bulk import rejected those same three codes outright, and separately accepted two spellings it documented nowhere. Both halves are gone: the accepted spellings and the codes they store are one table, and the documentation lists it.

  • Change The new-tournament form is grouped by what it asks. It was one grid holding everything, so the explanations took layout cells of their own: name and system on a row, a paragraph across the next, the team checkbox alone beside two empty cells, another paragraph, and only then the fields that decide the tournament&#39;s shape. It is three small groups now - what it is, how big, where and when - with the notes between them rather than inside them, so choosing an option grows its own section instead of reshuffling everything after it.

  • Change The original attempt at that, kept for the record: Its explanations — the engine note, the "Reporting only" note on team tournaments — were laid out as if they were form fields, so each one took a column beside an unrelated input, and choosing a pairing system reshuffled the rest. They span the full width now and sit under the field they explain, so picking an option adds a row instead of moving three columns. Three hand-tuned margins went with it.

  • Change The downloads are named after the product, and the release page now carries both shapes. The single-file build was published as pairings_engine_<target> — the internal application name, which predates the app being called OpenPairings and means nothing to anybody downloading it. It is openpairings_<target> now.

    More usefully: a tagged release attached only that single file, which is the one docs/binaries.md tells a Windows arbiter not to start with, because antivirus deletes it on sight. The portable release — the recommended one — existed only as a CI artifact you had to dig out of a build page. It is attached to releases now, as one zip rather than 1,800 loose files.

  • Change "Generate a new link" is now a real revocation. It used to change this machine's idea of the address and nothing else. That was genuine revocation while the pages were served from here — they 404'd the instant the slug changed — and it silently stopped being one when the page moved to another server.

    A bare rotation would have done the opposite of what the button promised: the leaked link keeps working, because the copy behind it is still there and this machine has merely stopped pointing at it, and the next publish creates a second copy at the new address. The tournament ends up public twice, and the key that could have withdrawn the first now names an address holding nothing.

    It now takes the old copy down, moves, and publishes again. The takedown goes first, because it is the part actually being asked for: if it fails, nothing else happens, since a failed revocation must never look like a successful one. The button says what it costs — printed QR codes and anything a club has embedded will need the new address.

  • Change A tournament switched on but never actually sent now heals itself. On start-up the app queues a publish for anything marked to publish that has no key — a promise nothing kept, where Settings says "published" and hands out a share link the results site has never heard of.

    Three ways to land there, and none of the ordinary machinery recovered from any of them: today's migration, which is raw SQL and cannot queue anything; a queue write that failed, which is swallowed on purpose so a publish can never take down the write that triggered it; and a database restored from a backup taken between the switch and the send.

  • Change A tournament switched off after publishing still collects its entries. Pulling used to require the publish switch to be on. That switch says whether more will be sent; it has nothing to do with whether there is a queue to collect. A tournament switched off still has its copy on the results site, and that copy's form is whatever the last snapshot said — so entries could arrive at a tournament this machine had stopped publishing to, sit unread, and be deleted unseen by a later takedown.

  • Change Closing the entry form now pushes immediately. The form is on the results site, so the arbiter's switch only reaches it by riding along in the next published snapshot. Closing is the urgent direction — an arbiter shutting entries at the door needs the site to stop taking them — so both opening and closing enqueue a publish rather than waiting for the next result to come in.

    Before this the switch had no effect on the results site at all: it accepted entries for every published tournament regardless.

  • Change A hand-set standings order now says so on the public page. The arbiter's chosen order was already published; the disclosure that a person chose it rather than a tiebreak was not, because it only ever lived on this app's own public standings page. Publishing the order while dropping that fact is the exact failure the feature's own documentation is written to prevent, and removing the page would have caused it silently.

  • Change Saving the publishing settings now tests them. "Saved" on its own answers the wrong question — nobody types an address to find out whether it was stored, and a typo saves perfectly well. It now says whether the results site actually answered, and shows the reason when it did not. The settings are kept either way: a typo you cannot correct because the form discarded it is worse than one that is stored and reported.

  • Change A JSON backup of a published tournament now carries its publishing key, and is therefore as sensitive as a password. That is deliberate: rebuilding a laptop from a backup has to recover the ability to manage what that laptop published, and a key left on the dead disk would strand a tournament in public with nobody able to take it down. Every place the app offers such an export now says what the file can do.

    Importing one does not take the tournament over. The key arrives dormant and the imported copy behaves as a separate tournament: switch publishing on and it gets a new address of its own. Settings then offers the choice in words — take over publishing the tournament already at that address, or start fresh — and doing nothing is starting fresh. If an import adopted the key by itself, two people opening the same file would both believe they owned that tournament, both publish to the same address, and either could delete the other's work.

    Duplicating a tournament never carries the key, even though it uses the same export-and-import machinery: the original is still right there, still publishing.

  • Change All three private copies of the played-code vocabulary are now checked at build time. Ainalrami.Trf.playing_codes/0 and bye_codes/0 are public precisely so nobody writes a fourth, but neither expresses the question these call sites ask - what point value does this code stand for - which is a three-way split running across both engine lists rather than along them. What the engine can police is the domain, and that is the half that broke: W/D/L went missing from one copy and an opponentless unrated result escaped as an exception. A missing code now fails the build naming itself, instead of surfacing mid-import on an arbiter's file.

    Neither remaining copy was mis-scoring anything today; each was complete as of v0.14.0. The exposure was drift, which is now not possible silently.

  • Change The public standings page now respects the round publish gate. It showed results for rounds the public pairings page one link away deliberately hides, so withholding a round hid who played whom and published the results anyway.

    Both public surfaces now compute standings through the same bound: the longest run of rounds 1..n that are all published. The contiguous prefix rather than the highest published number, because with round 3 published and round 2 held back, standings through 3 carry round 2's results anyway.

    That bound lived privately inside the OpenResults snapshot first, which meant the published site enforced the gate and the page served from this app did not — two publics disagreeing about what "not published yet" means. One definition now, in Tournaments.published_through_round/1.

  • Change Colour allocation follows a FIDE ruling that went against the engine. The pairing engine is pinned forward to Ainalrami v0.14.0, which changes how Article 5.2.5 decides who gets White.

    5.2.5 is the last resort: when neither player of a pair has any colour preference, the higher ranked of them takes the initial colour if their number is odd. The question was which number. Ainalrami used the tournament pairing number as the handbook defines it; both reference engines used a numbering that skips players who have never been paired. On 2026-08-27 the FIDE Systems of Pairings and Programs Commission ruled that the references were right.

    What an arbiter sees. On a board where 5.2.5 decides and somebody in the tournament has been registered without ever being paired - a no-show, a late entry who has not arrived, anyone who has only taken byes - White and Black may now be allocated the other way round from before. Nothing else moves: who plays whom is decided before colours are allocated, so pairings, byes, floats and standings are untouched. A tournament already in progress is unaffected for rounds already paired.

    This brings us into agreement with bbpPairings and Gacrux on a class of boards where we previously and deliberately differed. See the engine's docs/dispute-initial-colour.md for the full record, including two mistakes of our own that the ruling exposed.

  • Change One TRF16 implementation, not two. The app carried its own FIDE TRF16 reader/writer alongside the pairing engine's, which was photocopied from it and then kept growing - so a pairing input was written by one implementation and read by the other, and the app's copy knew nothing of XXR, XXP, XXA, 250, 260, BB* or 162. Every caller now uses the engine's, and the app's is gone. Both were measured against each other on 249 serialize cases and 137 parse cases first: every remaining difference was the engine's reading or writing something the app's copy could not.

  • Change The extension lines are written, not glued on. The XXR, XXA and XXP lines a pairing input carries - the round count, the Baku virtual points, and the arbiter's forbidden pairings and club/federation exclusions - used to be built as text and concatenated onto the finished TRF, which put exactly the lines carrying the rules outside the writer and outside every check it makes. They are fields of the tournament now and the writer emits them. This is the class of defect that gap hid: an XXA line one column too wide, which JaVaFo tolerated and bbpPairings rejected outright, so every accelerated tournament this app exported was unreadable by anything but JaVaFo.

  • Fix A pairing file for a round still being played is readable again. A player row whose last round has no result yet lost the two columns that round's blank result occupies, because trailing whitespace was trimmed off the row. bbpPairings rejects the whole FILE for that, not just the round - it stops reading round blocks two columns early and then finds characters left over. This was already fixed in the engine's writer and arrives with the consolidation above; the app's own writer, which every export went through, still trimmed. Files where every round has a result are unchanged byte for byte.

  • Change The unsupported-extension guard now looks at every extension. Before pairing with Ainalrami, the app scans the file it generated and refuses to pair - writing nothing - if it finds an extension line the engine will not act on, because an ignored rule yields a complete, legal-looking round that quietly breaks it. That scan only looked at lines beginning XX, which was the whole vocabulary while the app hand-built its own extension lines. The writer emits the numeric and BB* spellings too, so the scan now covers every code that is not TRF16's own.

Removed

  • Removed This app no longer serves public pages. /p/:slug/pairings, /p/:slug/standings and /p/:slug/register are gone. A tournament becomes readable by the public in exactly one way now: it is published to the results site.

    There were two public surfaces for one thing, and the default was the wrong one. A link to the machine running the round, printed on a wall chart and handed to a hall full of spectators, is precisely what the results site exists to prevent — and the fix earlier today only stopped the app advertising those links, it did not stop them working. A second correct-looking answer is worse than none, so they were removed rather than de-emphasised.

    The two switches are now one. "Public pages" and "Publish to the results site" asked different questions — may anyone read this here, versus does a copy leave at all — and the first has no meaning without a "here". Publishing is the whole of it. Every tournament whose public pages were switched on is now marked to publish, because its arbiter had already said "this is public" and the only thing that changed is where the public reads it.

    Club embeds move too. A club site that put the pairing list on its own front page embeds it from the results site now. The framing exception this app carried for those three pages is gone, along with the cookie-free websocket and the iframe height-reporting script that served them, and PUBLIC_FRAME_ANCESTORS with it. Nothing this app serves can be framed any more, which is one fewer argument to get right every time a route is added.

Fixed

  • Fix The phone-enrolment QR could not work on your own computer, and did not say so. Running OpenPairings locally, the QR encoded http://localhost:4000/... — which a phone resolves to itself — and the app only accepts connections from the machine it runs on, so even the right address would have been refused.

    That second part is deliberate and is what makes a build with no login safe to ship: it prints sign-in links to a terminal and signs in whoever reaches it. Opening it up so phones could connect would hand that to everyone on the venue wifi, so this is a property of running locally rather than a missing feature.

    The panel now says that, instead of offering a code that leads nowhere. Helpers can still enter results from a phone when a tournament runs on a shared server.

  • Fix A shared tournament went stale on the other person's screen. Deleting, archiving or restoring a tournament told only its owner, so a collaborator kept seeing a row that was gone — or missed one that was back — until they happened to reload. Sharing worked in both directions already; it was the tournament's own lifecycle that forgot the other party existed.

  • Fix A player's card showed their opponent's score wrong. It counted the opponent's administrative extra points, which a tournament only ranks on if you asked it to — and it does not by default. Right-clicking a player showed a total the standings table would deny.

  • Fix A crafted language-switch link returned a server error. A redirect_to containing a backslash or a tab reached Phoenix's redirect, which refuses those by raising, so anyone could produce a 500 from the public log-in page. It sends you home instead. Never an open redirect — Phoenix stopped that part — but a language switch is not a place to find a stack trace.

  • Fix ARO and AROC1 counted an unrated opponent as rating zero, and it moved prizes. A player who faced 2200-rated opponents and one unrated scored around 1956 instead of 2200, and dropped below anyone who happened to draw a full rated field — in a tie-break that decides who takes a trophy. One unrated entrant in the whole event was enough.

    The cause is honest enough on its own: an unrated player's rating is stored as 0, which is right everywhere else and is a 2000-point vote inside an average.

    C.07 Article 10 does not offer a number to use instead. It says these tie-breaks "must be dropped from the tournament tie-break list when unrated players are present, unless detailed rules on the handling of unrated players are included in the tournament regulations or established and published by the Chief Arbiter before the start of the tournament." So excluding unrated opponents from the average, or substituting a floor rating, would both be inventing a rule FIDE declined to write.

    Rating-based tie-breaks are therefore dropped — not computed, not ranked on, and the column is gone — in any tournament with an unrated player, and the standings page says which and why, including the escape hatch: publish a rule in your regulations and use a different tie-break here. It re-checks as players are added, so a late unrated entrant drops it without anyone reloading.

  • Fix A change took up to half a minute to reach the results site. The publish queue was swept on a 30-second timer and nothing else, so unlisting a tournament sat there until the next sweep — invisible until the new top-bar indicator gave it something to sit amber on. An enqueue now nudges the queue, debounced so that pairing a round does not fire one HTTP round trip per tournament, with the timer kept as the backstop that retries anything still in backoff.

  • Fix "Publish each round" described a page that no longer exists. Its help text pointed at /p/<slug>/pairings — a route removed with the local public pages — and told you the switch was gated on a setting under Settings → Tournament, which had also moved. It now names the real address on the results site and the switch that actually gates it, which is on the same page.

  • Fix Twelve Dutch strings were wrong, including two buttons that meant the opposite of what they did. The engine-switch confirmation offered "JaVaFo behouden" (keep JaVaFo) on the button that switches to JaVaFo, and "Ainalrami gebruiken" (use Ainalrami) on the one that keeps it — so a Dutch arbiter choosing a pairing engine was reading each button backwards. "Backups" read as "Terug" (Back), the publish indicator's "Sending" read as "Stand" (Standings), and a note about a round a tournament does not have read as "this tournament is archived".

    All of them were fuzzy entries: gettext's own guesses, produced by matching a new or reworded string against the most similar existing one. Dutch was reported complete at 918 of 918 and the count was true — what it did not say was that eleven of those were machine guesses nobody had read, and Elixir's Gettext (unlike GNU msgfmt, which drops them) puts fuzzy translations on the screen. A twelfth string, the publish queue's "1 tournament waiting to send", had no Dutch at all.

    The catalogue is now checked by tests rather than by counting: nothing untranslated, nothing fuzzy, and no translation interpolating a placeholder that will not be bound — that last one raises at runtime, so it would take the page down in Dutch and in no other language.

  • Fix On your own machine, the whole Connections page refused to do anything. Setting where the results site is, taking a backup, and updating the FIDE and KBSB rating lists were all gated on having signed in through 02cloud SSO. A local install has no accounts at all — it signs itself in as an owner named after the machine — so it failed that check, and there was no way to pass it.

    Which meant the one build that most needs the rating list could not download it, and an arbiter could not point their own laptop at the results site.

    The gate is a role now, and a local run needs none: the listener is on loopback, sign-in re-checks that the request came from this machine, and whoever is at the keyboard already holds the database and the binary. Being able to run it is the credential — the same reasoning that already skipped email confirmation there.

  • Fix Every public link pointed back at this machine, even for a tournament being published. The share links, the projector's QR code, the registration link — all of them handed out a /p/:slug address served by the very computer running the round.

    That is not a broken link, which is what makes it bad: it works perfectly and quietly undoes the split. Moving spectators off the arbiter's machine is the entire reason the results site exists, and the app was sending them straight back.

    There is now one function that answers where the public reads a tournament, and every link goes through it. (The local pages it fell back to were removed later the same day — see below — so that function now has one answer rather than two.)

    A tournament marked to publish on a machine that has not been told an address has no public link at all, rather than a link built from a blank address. The share card names the host a spectator will land on, so nobody has to read a URL to work out which site they just copied.

    The links are absolute now rather than relative — half of them end up on a QR code, in a printed footer or pasted into an email.

  • Fix The navigation still said "Rating lists". The page became Connections when the OpenResults settings landed on it; the nav label was missed, so the button and the heading it led to disagreed.

  • Fix A backup no longer loses an accessible table, relabels a played round, or un-hides a hidden board. Three separate ways a restore came back wrong.

    SWAR's HandyTable is a table number, not a flag — its own 1001+ handicap numbering lives in that field — and the exporter was writing 1 or 0 into it. So a wheelchair-accessible board survived a backup and restore as nothing at all, and nobody was told. It now carries the real number. The two cases that genuinely cannot (a legacy row that only ever held the flag, and a number past 32767 that would silently wrap negative) are explicit, and the second logs a warning naming the player.

    Restoring re-froze every round's board labels from each player's fixed table as it stands at restore time, so a backup taken after a mid-tournament pin renumbered rounds people had already sat down at. The labels are now carried in the payload. The excluded-field comment argued that recomputing was "more correct than carrying a stale label across" - a frozen label is not stale, it is the record of what the sheets on the tables said.

    And hidden was passed to Pairing.changeset/2, which does not cast it, so a restore put every hidden board back into public view. Version 0.14.x fixed the export half of that and not this one.

  • Fix Vacating a seat paid the player left on the board. Keizer scored any pairing with no opponent as a pairing-allocated bye worth half the player's own ladder value, and vacating a seat writes exactly that shape. The remaining player was paid the moment an arbiter marked someone absent. A vacancy and a bye differ only by their result code, which is now what tells them apart. award_bye_for_vacancy/2 still pays, because that is an arbiter deliberately choosing to.

  • Fix Byes and absences now sort below the special boards. A fixed table is a seat somebody is playing at, so it belongs with the games; the two groups where nobody is playing belong together at the bottom.

    Worth recording: 0.14.7's entry claimed to have made exactly this change. The intent was right and the concatenation was backwards, so two comments elsewhere describing the intended order only become true now.

  • Fix A place card and the pairing sheet named different tables. The sheet prints the displayed label; place cards, result cards and score sheets printed the engine's own board number. With any fixed table in the tournament those differ, so the card in a player's hand contradicted the sheet on the wall - and the number it showed appears on no document anybody in the hall can read. The cards now follow the sheet.

  • Fix A round's PGN could contain two games tagged [Board "1"]. The tag now carries the engine's board number, and this is the one board-numbered surface that deliberately disagrees with the pairing sheet. A PGN tag is never read by somebody standing at a board; it is how a database tells one game of a round from another, keyed on (Event, Round, Board), which needs a uniqueness the label loses as soon as a fixed table collides with an ordinary one. Same number, two documents, two different right answers.

  • Fix A pairing created from the pool printed outside the round's numbering. It was the one pairing-creating path that never froze its label, so it fell back to the engine's board number and left a visible gap in the printed sequence. Frozen narrowly rather than by re-freezing the round: a full re-freeze recomputes from current fixed tables and would renumber boards under players already seated.

  • Fix fixed_board accepted 0 and negative numbers, which reached the label, the PGN and every printed document. A value that collides with an ordinary board is still allowed - that one is deliberate.

  • Fix Every rendered bye row fired its own COUNT query, an N+1 inside four render loops. The query now runs only where its answer can change the result, which for any tournament that is not a SWAR import configured with a cap is never. No number it returns has moved, and there is a test counting queries alongside the ones pinning values.

  • Fix Queuing a publish can no longer break a write. It hangs off the funnel every write in the app goes through, which is what stops any call site forgetting to publish — and equally meant one bad query there broke all of them. A publishing queue is a courtesy and must never stop an arbiter entering a result.

  • Feature A plain announcement banner, for telling people something without restarting anything. scripts/notice.py "Big server maintenance in 12 hours." --hours 12 puts one sentence on every open page until it is withdrawn or its time passes.

    Deliberately not the deploy warning, which could not have done this. That one is a restart countdown: capped at two hours, escalating through three tiers to red, saying things about unsaved work, and held in memory so it dies with the release it was warning about. All of which is right for a restart minutes away and wrong for "we are pushing the new system tomorrow".

    So the differences are the feature. It says whatever it is given, for up to two weeks, it never escalates, and it is persisted — a notice about maintenance twelve hours out has to outlive every restart in those twelve hours, and one that vanished at the first hiccup would look exactly like one that was never set.

    It restarts nothing and schedules nothing; the banner is the entire effect. Same DEPLOY_NOTICE_TOKEN, because it is the same privilege — both can put a banner on every screen — and the endpoint fails closed when that variable is unset.

  • Feature Tournaments can be published to OpenResults. The public half of this app is a separate service, and until now nothing sent it anything: the snapshot builder existed and had no caller. It does now.

    Opt-in per tournament, off by default, and toggled by its own action rather than by the ordinary settings form — the same guarantee public_pages_enabled and registration_open already have, for a sharper reason. Those two decide whether somebody holding a link may read an event here; this one decides whether a copy of it leaves the machine at all, and a stray form field must not be able to start that. It is also excluded from the tournament export, so importing a file cannot make this machine start sending a copy of it to whatever server this machine is pointed at.

    A publish never blocks and never fails loudly. That is the entire point of the split: a chess venue's wifi — school gyms, hotel basements — is exactly where an arbiter is standing when they pair round 5. A write records an intent and returns; a background drain sends what is due every thirty seconds; a send that does not land keeps its place in the queue with a longer backoff and an error written in words rather than in a struct ("the connection was refused — is the server running?").

    The queue holds intents, not payloads. One row per tournament, enforced by a unique index. A snapshot is a whole document rather than a delta, so five publishes stacked up behind a dead connection are not five things to send — they are one send of the current state, rebuilt when it actually goes out. Eight results entered in a minute are one publish, and a publish delayed twenty minutes carries what is true when it leaves rather than what was true when it was queued. That is what an arbiter wants: the page catches up to the hall, not to a moment in the past.

    Publishing hangs off broadcast_tournament_change/2, the one funnel every write in the app already goes through, so no call site has to remember to publish and none can forget. It costs one primary-key lookup for a tournament that has not opted in.

  • Fix A single reported board moved everybody else's tiebreaks. While a round was being played, the moment the first result was typed in, every player who had not yet reported gained a phantom draw on their opponents' Buchholz, BHC1, BHC2, MBH and Sonneborn-Berger — and Koya's 50% threshold jumped a full win at the same instant.

    Two quantities were being subtracted from each other that are not the same kind of thing. rounds_played_count/1 returned the largest number of game records any player held, and it was compared against a round number. An unreported pairing produces no record at all, so the count rose as soon as one game finished, while every other player's last record still pointed at the previous round. The difference read as "this player missed a round", which Article 16.3 scores as a draw.

    Both readers now ask the rounds how many of them are complete — the highest round in which no pairing is still unreported — rather than asking the players how many records they hold. A round holding only byes counts as played; a round with no pairings and no byes does not, so creating a round in advance no longer awards anyone anything.

    This was the most ordinary situation there is: a round in progress, results coming in one board at a time, standings on a projector.

  • Fix Importing a round's results from CSV wrote them to the wrong games. The importer matched each line against the board number the pairing engine assigned. Every document an arbiter can read - the printed pairing sheet, the Pairings page, the public page, the projector - prints the displayed board number instead.

    Those two are the same for almost every tournament, and come apart as soon as one player has a fixed table (Players -> Fixed table). That pairing is labelled with its table number and printed last, and the ordinary boards close the gap it leaves - so a fixed table on real board 3 of 5 makes the sheet read 1, 2, 3, 4 against real boards 1, 2, 4, 5.

    An arbiter transcribing that sheet typed four boards, got "4 results imported", and three of them landed on the wrong games. The standings were then wrong, and nothing anywhere said so.

    The importer now matches the number the sheet prints. Nothing changes for a tournament with no fixed table in it.

    Two things worth knowing about the edge:

    • A fixed table outside the ordinary range - SWAR's own starts at 1001 - now imports like any other line. It could not be entered from a CSV at all before, because 1001 is never a real board number and the importer rejected it as unknown.
    • A fixed table deliberately set to a number an ordinary board already uses prints two rows with the same label. That number is taken to mean the ordinary board, and the fixed table is entered from the Pairings page. The alternative - refusing the whole file as ambiguous - would block the four boards that are not ambiguous at all.

    Found while verifying something else, and it was not caused by it: the damage reproduces identically with a fixed table at 1001, so it has been live for as long as the feature has.

  • Fix Club sites could not embed the results pages at all. They were reported as embeddable when the local public pages were removed; they never were. Phoenix sets frame-ancestors 'self' by default, and the claim came from searching the results site's own code for a policy, finding none, and never looking at an actual response.

    Every page there permits framing now, and that is safe for the whole site rather than for a list of routes: there is no session and no login, so a framing page gains nothing it could not get by fetching the URL itself. PUBLIC_FRAME_ANCESTORS restricts or disables it.

  • Fix A Keizer standings row could count a board nobody had scored yet as a played loss. The instant a round was paired, classify_result/2's catch-all sent the still-blank result into the same bucket as a genuinely played "0-0" - one more game Played, one more Loss - instead of treating it as not yet played. Played/W/D/L are not shown on any Keizer screen today, and the ladder itself was never wrong either way (a blank result scored 0 ladder points regardless of the bucket), so this stayed invisible with the app's own defaults. What it did inflate is raw_points, the ordinary-scoring column printed beside the ladder, by one points_loss per unscored board - real for any tournament with a non-zero loss value, zero everywhere else since points_loss defaults to 0.0. A blank result now scores nothing, the rule Standings.pairing_records/4 has always applied to the same state on the FIDE path.

Security

  • Security Password sign-in had no rate limit. Every other unauthenticated route in the app was given one deliberately — mobile enrolment, magic link, registration, the FIDE lookup — and this one was missed, leaving nothing between an attacker and a password list but bcrypt's own cost.

    It now shares the magic-link form's two buckets: per address, so a list cannot be walked against one account, and per client, so one client cannot walk a list of addresses. Refused before the password is checked, so a throttled attempt costs no bcrypt round and cannot be timed to tell a real account from a missing one. Only failures count — signing in and out during your own tournament must not lock you out of it.

  • Security The KBSB rating import was the one control on Connections with no role check. Every sibling handler on that page checks; this one did not, and the page became readable by support the same day. It pulls the whole Belgian roster over somebody else's API and rewrites the local rating table — an act, not a look — so it is an administrator's now, on the handler as well as the button.

  • Security Connections is no longer readable by an ordinary account. Its buttons were role-gated, and that was half the job: any signed-in user could still open the page and read the address this installation publishes to, the backup filenames and their sizes, when each rating list last synced, and whether the results site was answering.

    None of that is a password and all of it is the operator's business rather than every arbiter's. The route refuses now, not only the buttons, and the nav link is not offered — a link is a courtesy, but a bookmark or a typed URL is not, so the markup gate alone would have left the page serving to anyone who guessed it.

    support may still open it, which is the point of that role: the person answering "why did publishing stop" can see the diagnostics without the authority to change them.

  • Security Only an administrator can change where this machine publishes. The OpenResults address and token were editable by any signed-in user, including a self-registered one. Repointing them at another server would quietly send every published tournament's player names, ratings and clubs there.

    It is now behind the same gate as the FIDE rating-list download and the backup download — an explicit admin role — and on the event handlers rather than only the markup, since a hidden button still accepts a crafted event.

    Signing in through 02cloud is deliberately not enough. How somebody authenticated and what they may do are different questions, and answering the second with the first would make every account in a federated directory an administrator of this installation's wiring.

    Deliberately not extended to a tournament's own publish switch. An operator decides where this machine publishes; the arbiter running an event decides whether theirs goes. Gating the second would stop an arbiter publishing their own tournament on a machine somebody else set up, which is the ordinary case rather than the dangerous one.

  • Security A published tournament belongs to the machine that published it, and can now be taken down. Until now there was one ingest token for a whole results site, and it answered only "may this machine talk to this server". Anything holding it could overwrite any tournament there, and nothing could withdraw one at all: turning a tournament's publish switch off stopped further updates and left the page — player names, ratings, clubs and federations — public forever. The only remedy was SSH and SQLite.

    Each tournament now gets its own key, generated on your machine the first time it publishes and never regenerated. The results site requires it for every later update and for deletion, so a second machine holding the shared token can no longer touch your event.

[0.17.1] - 2026-08-26

There is no 0.17.0 release. The version existed for a day and binaries were built from it, none of which worked - migrations were skipped, so every one served a 500 on its first page. A Burrito binary caches its unpacked runtime under its own version number, so a machine that ran one of those would be handed the broken extraction back for any later build calling itself 0.17.0. Skipping the number is cheaper than explaining that.

Added

  • Feature Dutch. The arbiter interface now speaks Nederlands - 548 interface strings plus the 24 validation messages, picked from the language menu or taken from the browser's own accept-language. nl-BE and nl-NL both resolve to it; there is one catalogue, because chess vocabulary does not differ enough between Flanders and the Netherlands to justify two.

    Terminology follows Belgian (KBSB/FRBE) usage rather than literal translation: paring rather than the Dutch indeling, scoregroep for a score bracket, doorschuiver for a floater, and stamnummer for a national ID. bye, tiebreak and Elo stay untranslated because that is what arbiters actually say. Product and format names - JaVaFo, Ainalrami, SWAR, TRF16, IT3/FA1/IA1 - are never translated.

    The public pages stay English, deliberately and unchanged: an open draws players from many federations, and the language an arbiter picked for their own screens is not one to impose on a visiting player reading the standings.

  • Feature The Dutch is finished: the sentences with links in them, and everything that prints. 840 interface strings now, up from 548.

    Two gaps closed. First, the sentences a previous pass deliberately left in English because they wrap around a link or a value: those needed the whole sentence to stay one translatable unit with the markup movable inside it, which is what <.rich_text> now does - the msgid carries a %[name] placeholder and the translator puts it wherever Dutch wants it. 54 sentences that read half in each language, or not at all, now read as one.

    Second, everything you print. Pairing sheets, standings, cross tables, score sheets, place cards, result cards and player lists came out in English whatever language the arbiter had picked, because PrintController assembles its HTML as strings instead of through a template and so was invisible to every pass that walked templates. Printed documents now also carry the right lang, as does the app itself.

    Along the way, a wrapping script's "this is a code, not prose" filter turned out to be matching case-insensitively, which had silently skipped every one-word label in the app: eighteen Cancel buttons, and most column headers. Those are translated now too.

    Column headers that are codes (Pr., Nr, Fed, Elo, Pts, W/B, 1-0) stay English on purpose - they match the FIDE forms and the on-screen grid.

  • Feature Another 80 interface strings translated, and the half-Dutch sentences fixed. The first pass wrapped 468 strings but only matched text sitting against a tag boundary on a single line, so longer sentences were left bare - and where it did fire mid-sentence it produced FRAGMENTS. The Players page read "Double-click a row to edit the player ... or right-click the Kolomtitel Pr. om het voor iedereen tegelijk in te stellen": half English, half Dutch, in one sentence.

    Sentences are now the unit. A fragment cannot be translated properly anyway - Dutch puts the verb where English does not, so a translator handed "to set it for everyone at once." on its own has nowhere to put it. Where a sentence wraps around an inline value it is left in English rather than split, which is untranslated but at least coherent; those are listed in docs/i18n.md as the remaining work.

  • Feature Players can request byes when they register. The public form now shows one tickbox per round, so somebody who already knows they will miss round 1 says so while signing up instead of e-mailing the arbiter about it. They still land not yet arrived, exactly as before - requesting a bye is an announcement, not an arrival.

    Marking them present later does not cancel the byes. The two are separate fields and always have been; this is the first time anything filled the second one in from the player's side.

    The form states what a requested bye is actually worth in that tournament, read from its settings rather than assumed - a player deciding whether to ask for one is exactly who a hardcoded answer misleads.

  • Feature One value and one allowance for every round a player sits out. Settings -> Scoring's "Genuine absences" card is now "Byes and absences": what a sat-out round pays, for the first N of them, up to round Y. It already existed - it just had almost nothing feeding it.

    A player asking for a specific round off and a player marked absent outright are now the same thing, because they are: the only way you know before the round is paired is that the player told you. Somebody who was paired and then didn't turn up is a forfeit on their board, which is a different event entirely and always was.

    FIDE's default is zero, so nothing changes until an arbiter sets a value.

  • Feature Played-but-unrated results (TRF W / D / L). A game that was contested over the board but does not reach the rating report - typically one that ended before the minimum number of moves. Three new results on the Pairings page: "1-0 (played, not rated)" and its draw and loss counterparts.

    They score exactly what their rated twins score and count as PLAYED for every FIDE Art. 16 purpose, because the game happened. The only thing that differs is the letter in the TRF, which is how FIDE is told not to rate it. Required by the draft VCL4THP (Q185).

    Wired through the result table, standings, Keizer, TRF import and export, PGN and SWAR export. The last two mattered more than they look: PGN would have written * (its unknown-result marker) for a game that plainly had a winner, and SWAR export's catch-all returned bitmask 0 and 0.0 points, dropping the result and the score together.

  • Feature The pairing rationale explains Article 5.2.5, and says where we differ. When neither player held a colour preference, 5.2.5 decided the board - the higher ranked player takes the initial colour if their tournament pairing number is odd. The board now says so.

    On a tournament paired by Ainalrami it adds the part an arbiter actually needs: other programs may seat that board the other way round. They take 5.2.5's parity on the player's position within the bracket; we take it on the tournament pairing number, which is what Article 1.1 points to. The pairing is identical either way - only who holds White differs.

    This is the one rule where the engine knowingly differs from both reference implementations, and until now the only way to find that out was to ask us. Someone cross-checking a board against SWAR should be able to read the answer off the screen.

  • Fix Every standalone binary ever built skipped its own migrations and served a 500 on the first page. no such table: users. Downloading a release, running it and opening the page did not work, and never had.

    Migrations run at boot when the app detects it is a release, and the check for that was RELEASE_NAME != nil - the line mix phx.gen.release generates. It is true of a release started through its own bin/<name> script and false in a Burrito binary, which is every executable this project ships: Burrito's launcher execs erl directly rather than going through that script, and sets RELEASE_ROOT but not RELEASE_NAME. So the guard said "not a release", migrations were skipped, and the app started against an empty database.

    It went unnoticed because nothing ever ran one. The binaries workflow built five executables and tested none of them, and the guide told you to run a migration step by hand first - through a PairingsEngine.Release.migrate that has never existed in this codebase. Two pieces of documentation and a CI job all agreeing about a step that could not be performed is what a gap in coverage looks like from the outside.

    Now keyed on RELEASE_ROOT, which both kinds of release set. Found by the new smoke test below, on its first run.

  • Change The binaries are built on every push to main, and each one is started before the build is called a success. They were built on tags and manual dispatch only; the last build before this was a month old, on a branch, from before the engine switch. A shipped artifact built only at release time is tested only at release time.

    Each target now boots in local mode and is asked for a page, and the run fails unless the page comes back carrying the auto-signed-in owner's address. That is a thing no unit test can cover: the release evaluates config/runtime.exs for real, inside a self-extracting wrapper, and the migration bug above lived precisely in that gap. Concurrent runs on a branch cancel each other so a burst of commits does not queue five runners deep; tag builds never cancel.

    Deliberately no paths-ignore for documentation: CHANGELOG.md is compiled into the binary, so a docs-only change is not docs-only here.

  • Fix A first local run could hit "database is locked" while creating its own database. Five pool connections racing to create the same new file and set WAL on it. Locally the pool is two now - one person cannot use five

    • which is both faster to start and a smaller race. It recovered either way, since Ecto retries, but a fresh install should not have to.
  • Feature A second download shape, because antivirus deletes the first one. openpairings_portable_<target> is the same application as an ordinary Erlang release: a folder you unzip, runtime inside, with a launcher beside it - OpenPairings.bat on Windows, openpairings.sh elsewhere. Unzip, run the launcher, same localhost, same no-login, same everything.

    The single-file binary is nicer to hand somebody and it is not always possible to hand it to them. Symantec removed one from disk before it ran once - not a warning, not a quarantine prompt, deleted. Nothing is wrong with the binary: an unsigned executable carrying a compressed payload, which unpacks a runtime into AppData and spawns processes, is byte-for-byte what a dropper looks like, and a heuristic engine cannot tell them apart.

    Code signing is the real answer on Windows and needs a certificate on a hardware token or a cloud HSM. This needs neither, and a directory of DLLs with a .bat beside it is not a shape anything hunts for. Both shapes are built by the same CI run, both bundle the Erlang runtime, and both are now started and asked for a page before the build is called a success - the portable one through its launcher rather than around it, since the launcher is the part that is new.

  • Feature Local mode: the standalone binary runs on your own machine with no setup at all.

    ./pairings_engine_linux_x86_64 start

    It is the default in a binary, and it took two goes to get there. The first version made you pass OPENPAIRINGS_LOCAL=1, so running the file the way anyone actually runs a file - by running it - produced environment variable DATABASE_PATH is missing and a 2.8 MB erl_crash_dump. That is a server's error message shown to somebody who is not running a server.

    Nobody deploys a self-extracting single-file executable to production; a server gets a real release and a service unit. So a binary IS the local case, and it now knows it without being told (Burrito's launcher exports __BURRITO). OPENPAIRINGS_LOCAL still overrides both ways: =1 for a plain mix release or a dev run, =0 to make a binary behave like a server anyway.

    The CI smoke test was complicit and has been fixed too: it passed OPENPAIRINGS_LOCAL=1, which proved the flag worked and said nothing about the case that broke. It now starts each binary with no configuration whatsoever.

    A release is built for a server, and refused to start without SMTP credentials, a DATABASE_PATH and a SECRET_KEY_BASE - all correct for a server, all nonsense for one person who downloaded one file. Local mode generates the secret once and keeps it, puts the database in the OS's own per-user data directory, and serves http://localhost:4000.

    There is no login. A local install has one user by definition - the person at the keyboard - so it does not ask who you are. The first request signs you in as an account named after your OS user and hostname (ann@her-laptop.local), created on first start and reused after that, so your tournaments are still there next time. The account exists at all only because tournament ownership, the audit trail and invitations are keyed on a user; it is cheaper to give local mode a real account than to teach all of that about a user who is not there. The log-out button is hidden, since there is nothing to log out of.

    This is a session, not a bypass: a genuine token is issued the same way a magic link would issue one, so scopes, sudo mode, expiry and LiveView all behave exactly as they always did. Nothing else in the app knows local mode exists.

    Two independent conditions gate it, and both must hold. The setting has to be on, and the request has to have physically come from this machine - checked per request against the peer address, ignoring X-Forwarded-For, which the client controls. The listener is also pinned to loopback and will not be talked out of it, not by PHX_HOST or anything else. The per-request check is not redundant with the pin: a reverse proxy in front of the app, or a later change to how the endpoint is built, breaks the pin's guarantee and leaves the other one standing.

    What made this worth doing now is the engine switch: pairing no longer needs a JVM. Ainalrami is Elixir and is inside the binary, so a local install has no external dependency at all. Migrations already ran at boot in any release, so first start creates and migrates the database itself - the binaries guide said to run an eval step first, and that step referenced a module that has never existed.

Security

  • Security The retired markdown renderer is gone. The changelog page was rendered by earmark, which carries CVE-2026-48591 - a stored XSS through unescaped HTML attribute values - and is retired in every release, so there was no version to upgrade to and never will be.

    The old reasoning for keeping it was sound as far as it went: the only markdown it ever saw was this repo's own CHANGELOG.md, read at compile time, so the hole was unreachable. That is a property of today's call sites, not of the code, and an abandoned package with a permanent advisory reopens the same argument at every audit.

    The suggested replacement, MDEx, is a Rust NIF, and this app cross-builds standalone binaries for five OS/arch targets - each would then need a Rust toolchain. So the parse now uses earmark_parser (maintained, pure Elixir, and the half that never had the flaw, since the flaw is in HTML generation) and the generation is ours, in PairingsEngine.Markdown, where escaping is not optional, the tag set is an allowlist, and a javascript: link is not something the renderer can emit.

    Verified as a swap rather than a rewrite: rendering the whole changelog through both produces identical counts for all 20 tags it uses and identical text, but for the apostrophe - earmark applied smartypants, which earmark_parser has deprecated. mix hex.audit now reports nothing.

  • Security A player could be seated on a board of a tournament they were not in. Two of the ways an empty seat is resolved on the Pairings page - seating somebody from the pool, and pairing two players out of it - took the player id straight from the browser's message and checked only that the arbiter owned the tournament, which proves nothing about the row. The board's foreign key points at the players table and cannot say "and in this event", so any player id in the database could be put on any board of any tournament, under any account. Both now ask the ownership question the rest of the module already asks before accepting an id from the page.

Verified

  • Verified An unrated game counts toward a title norm; a forfeit on the same board does not. B.01 1.4.2 excludes a game "decided by forfeit, adjudication or any means other than over the board play" - it says nothing about rating, so a game recorded unrated still counts, at full value.

    That was already the behaviour, but by accident: the norms module reads a played flag and has never heard of unrated result codes, so the right answer fell out of another module's bookkeeping. No code changed; the rule is now stated where it applies and asserted both ways, because only the contrast shows the rule is being applied rather than everything being counted.

Fixed

These first four came out of a deliberate sweep of the codebase for inconsistencies rather than from a bug report, prompted by the engine switch: if Ainalrami is what pairs every tournament now, the input it is handed had better be right. The sweep looked for one shape in particular - the same rule written down in two places, where the two can drift apart without anything failing. It found five, four of them live.

  • Fix A game that was played but not rated was handed to the pairing engine as a loss. The crosstable scored it correctly - an unrated win pays what a rated win pays - but the score written into the pairing file came from a second, hand-written mapping that listed six result codes and sent everything else to points_loss. W and D were not on the list.

    So a player who won an unrated game was given to the engine a full point light. That is not a display problem: score is what decides which bracket you are paired in, so they were bracketed too low and played the wrong opponents, while the standings on the next screen showed the point they had actually earned.

  • Fix An absence worth half a point was worth nothing to the pairing engine. Same cause, bigger blast radius. A bye was scored from its letter in the file, and the letter for an absence is Z, read as a loss. But Z is also written for a requested zero-point bye, and an absence may be worth abs_value - half a point, in most clubs that set it, capped by round and by count.

    Exactly the tournaments that configure a paid absence are the ones it went wrong for: the arbiter sets "half a point for a round sat out", the standings honour it, and the file says zero. Byes are now scored by the same function the crosstable calls, so there is one answer to the question instead of two.

  • Fix A pairing-allocated bye in a SWAR 3-2-1 event dropped its presence points on the way to the engine. The crosstable pays bye_value plus presence points; the engine was told bye_value alone.

  • Fix The explanation on the pairing-rationale page could have described a different pairing than the one on the board. The options handed to the engine and the options handed to its explain pass were two separately written lists that happened to contain the same three keys. They now are the same list. Nothing was wrong yet - the next key added to one of them would have been.

  • Change The "absent counts as a voluntary unplayed round" setting is on by default, and now says so. The field's own comment read "never on by default" directly above default: true. The default is correct - an absence and a requested bye are one event under two names, and splitting them for tie-break purposes split one thing in half - so the comment was the thing that was wrong, along with two test names describing the old default and no test asserting the real one. There is one now.

    Behaviour is unchanged; this entry exists because the documentation said the opposite of the code, which is worse than saying nothing.

  • Fix A backup restored every hidden board back into public view. An arbiter can hide an individual pairing from the public page; that flag was not written into the backup file, so restoring an event un-hid all of them. That is a disclosure rather than a lost preference - the board the arbiter deliberately withheld comes back visible, with nothing to say it happened.

    Restoring a backup written before this fix leaves nothing hidden, which is the same state as before and the safe direction.

    The stored pairing rationale (the "why did the engine pair it this way" panel) is still dropped by a backup, and now says so in the code rather than going quietly. It holds internal player references that a restore cannot reconnect, so carrying it across would attribute every bracket to the wrong players while looking entirely convincing. Losing an explanation is better than showing a false one; carrying it properly needs work the file format already has a version field for.

  • Fix A 3-2-1 event was scored one way on screen and another in the file the engine brackets from. SWAR's 3-2-1 system pays a point for turning up on top of the result, and the standings pay it - but the score column written into the pairing file carried no such point, for any game. So a Belgian club event bracketed its players by a number that was not the one on their own crosstable row. SWAR sends the sum of both to its engine, so this was also handing over a different column than the program the format was read off.

  • Fix A player marked absent in a Swiss tournament now scores their absence award. They used to score nothing at all, whatever the tournament paid. Players absent for the whole event were filtered out of the round before the point where absences get recorded, so no record was written - no board, no forfeit, no row - and the award had nothing to attach to. Keizer had always recorded them; Swiss never did. Invisible while the award was zero, which is the default, and silently wrong for anyone who had set it.

    Round robin is unaffected and was never wrong: its schedule is fixed, so an absent player still has an opponent and the round is scored as a forfeit on that board rather than as an absence.

  • Fix A round robin too long for the app's own round limit crashed instead of refusing. 32 players over two cycles is a 62-round schedule, and this app supports 30 rounds - the length a Berger table needs and the ceiling the tournament validation enforces were two numbers written independently, so asking for the next round threw an exception rather than saying anything. It now refuses in words, naming how many rounds that field would need and the maximum, and the maximum in the message comes from the same place the validation reads it. It is not quietly capped: capping would pair a schedule the arbiter did not ask for and drop the end of it.

  • Fix A half-point absence is no longer labelled "0 bye" on player cards and printed lists. It reads from what the round actually paid, which could not vary until now.

  • Change Absences count as voluntary unplayed rounds by default. FIDE's C.07 treats a voluntarily unplayed round differently in Buchholz/Sonneborn-Berger, and every absence the pairing knows about was announced in advance - so that is what it is. Existing tournaments keep whatever they have; only new ones start with it on.

    The warning attached to this setting now fires when you CHANGE it rather than whenever it is on. Tied to the state, it would have greeted every arbiter opening a fresh tournament with a red box about a setting they never touched.

  • Fix Every title norm in a 3-2-1 event was judged one band out. The norm calculation bands each game's stored points against the tournament's own win/draw values to get FIDE's 1/½/0 - and on a SWAR 3-2-1 event those stored points already carry the point for turning up. So every played game shifted one band UP: a win read as a loss, a draw read as a win, a loss read as a draw. Not an occasional misread but a uniform inversion, so a player's norm score and every check derived from it were wrong for everyone in the event. The same nine games score 7.5/9 normally and scored 1.5/9 under 3-2-1.

  • Fix A club that pays for absences no longer exports as one that pays for none. Leaving the absence limits blank means "no cap", which is what the settings screen tells you to do; the .swar exporter wrote a blank as 0, and 0 in that format means "cap at zero - pay nothing". Re-importing the file made it true. Blank now exports as the round count, which is the largest honest number for a tournament that long.

  • Fix A player whose games were all unrated exported as "0 games played". The .swar game counter used its own four-code list of what counts as played, against the nine the standings use - so the unrated W/D/L results and the asymmetric ones were invisible to it, and the file said zero games beside nonzero points and three round records. A file that contradicts itself, and one that re-imports as fact.

  • Fix Two forfeits exported to PGN as "unknown result". +/- and -/+, the legacy spelling of a single-sided forfeit, fell through to * where their FF twins export as a decisive result. A double forfeit still exports as *, deliberately: PGN has no way to say it.

  • Fix Entering "1/2-0" in a Keizer tournament took the standings page down. The asymmetric VCL.13 results have been storable, and scored, since they were added; Keizer's per-round statistics were the one reader of the same vocabulary never extended to them, and had no fallback, so the page raised the moment such a result was entered. They now count what the crosstable counts - the side on the half as a draw, the side on zero as a loss, both as games played. An unknown class is now a loud failure naming itself rather than a silently ignored one, because the alternative to a crash here is wrong standings.

  • Fix A withdrawn player's progressive-score tie-break stopped when they left. Progressive score sums the running total round by round, and it was folding over the rounds a player had a record for rather than over the rounds of the event. FIDE's C.07 Art. 16.1.1 settles it - "any round after a participant withdraws is a zero-point-bye" - so those rounds exist and add nothing, which means carrying the total forward. A player who left after round 1 on 1.0 scored 1.0 where 3.0 is correct, and was placed below players they should have tied with.

  • Fix Importing a TRF with a blank round in the middle of it shifted every later round up one. A round with no results is legal input - a late entrant's earlier rounds look exactly like that - but the import skipped it, so rounds came out numbered 1 and 3 with no 2 and the games from round 3 landed in round 2's columns, dates and all. Trailing blank rounds are still skipped: inventing rows for rounds that have not happened would claim the event is further along than it is.

  • Fix A TRF this app exported could not be read back into it: every played-but-unrated game returned as two byes. The importer decided whether a round entry was a game at all from its own private list of result codes, written before W, D and L existed and never extended to them, so an unrated game failed to resolve to a board and each side came back as a bye with the opponent gone. The proof it was drift and not a decision sat thirty lines below the list, in code that could never run. There is one canonical list of playing codes now and the last two private copies of it are deleted - a private copy cannot be fixed by correcting the real one, which is how this kept happening.

  • Fix A played-but-unrated result recorded against a bye crashed both pairing and the FIDE download. The guard that exists to stop a bye ever carrying a playing code had the same gap, so W, D and L stayed playing codes on a row with no opponent - the exact combination it is there to prevent. Building the FIDE file then raised, and raised early enough that nothing caught it: pairing the next round threw an exception where every other refusal returns a message, and the download went with it.

  • Fix A player marked as needing an accessible table lost that marking when the tournament was restored. SWAR records the marking on its own, with no table number attached, and a .swar import stores it that way. Restoring a JSON backup or a snapshot re-derived it from whether a table number was present, so a player who had the marking and no number came back without it - and exporting that tournament to .swar again then told the next program the player did not need one. It is carried across as stored now.

  • Fix The categories "Turn off" button could not turn categories off once a round was paired. Pairing by category locks after round 1, and the off-switch sent that locked setting along with the one it meant to change, so the whole write was refused - with a message naming a setting the arbiter had not touched. The button now sends only what it needs to, and is disabled with a sentence saying which setting is in the way.

  • Fix A late entrant marked absent was paid for rounds before they joined. A player who is both absent and joining in a later round was written an absence row for every round of the event, including the ones the tournament did not have them for. Those rows score: on an event that pays for absences they collect real points and burn the per-player allowance doing it, and they go into the .swar export as well.

  • Fix Two players could be handed the same pairing number. Mark the top-numbered player absent, then register a walk-in, and the walk-in was issued a number that was already on somebody's row: the next number was taken as the highest among players currently eligible, and an absent player keeps the number they were given. Nothing downstream objected. The exported FIDE file then carried two rows with the same starting rank and opponent references that could mean either of them, and the tie-break that exists so the pairing order is reproducible had nothing left to break the tie with. Which numbers have been issued is a question about the whole roster, and is now asked of the whole roster.

  • Fix An archived tournament still accepted three edits to a vacant board. An archive is read-only, and seating somebody from the pool, awarding a bye for the vacancy and pairing two players out of the pool were the three members of that family that never asked whether the tournament was writable. Their five siblings all did, which is why the Pairings page carried a comment saying these writes were refused server-side regardless - and why the suite that proves it names the five and not the three.

  • Fix A FIDE ID with a letter in it crashed the add-player form. Anything that is not a clean number - letters, a dash, a pasted value with a stray space - raised out of the duplicate check before the form could say "is invalid". A 40-digit number got further and raised from the database driver instead; IDs are now bounded to nine digits, which is more than FIDE issues.

  • Fix Picking a language on the log-in page and then signing in put you back in English. Signing in clears the session, which is where the chosen language is kept, and nothing put the language back afterwards. The picker sits in the log-in page's own bar, so "choose Nederlands, then sign in" is the obvious order to do it in and it was the one order that lost the setting. The end-to-end language tests all signed in first and switched afterwards, so none of them was ever in a position to see it.

  • Fix Switching language sent you to the home page instead of back to what you were reading. The picker is meant to return you to the page you were on and nothing in the app ever told it which page that was, so it always took its fallback. On the public pages that is worse than an inconvenience: a visitor reading the standings who switched language was bounced to the log-in screen, because the home page needs an account. The page is now passed through, query string included, so a switch on a filtered page comes back filtered.

  • Fix A browser asking for Dutch with a broken quality value now gets Dutch. accept-language: nl;q=banana resolved to English, as did a dangling nl-. The resolver was already right - a malformed weighting sorts last rather than raising - but its test asserted the result was English, which was only ever true because Dutch did not exist yet. Worth recording as a fix rather than a test change: the behaviour a user gets really did change.

Changed

  • Fix A Swiss late entrant was paired before the round they joined in. A player with a start_round of 3 played rounds 1 and 2. The check existed and was correct - it is what decides whether there are enough players to pair at all - but the roster handed to the engine was rebuilt from scratch a few lines later without it. Keizer read start_round properly all along; only the Swiss path did not.

    They are now left out of the round completely: no board, and no absentee bye either, because a round before you join is not a round you were absent from. Reachable in practice by restoring a tournament from a backup, or by importing one, since nothing in the Swiss interface sets start_round by hand.

  • Change The new-tournament form names the engine. Picking "Swiss" now says, underneath, that Ainalrami will pair it and which edition of the rules that means. The engine was previously only visible in Settings, which is not where the choice is being made.

  • Change The interface no longer claims a FIDE endorsement. Four places said it - the engine picker, the note beside it on a homologated tournament, and both halves of the switch dialog. FIDE has announced that existing endorsements are revoked in the coming Acceptance Cycle, so it was a claim with a shelf life, and an app should not be trading on one.

    What replaced it is the thing that is both true and actually decides the question: Ainalrami implements C.04.3 as it stands from 1 February 2026, JaVaFo implements the 2017 edition it was last built for, and on a homologated event that difference is what you would be defending. The reasons to prefer JaVaFo are stated without the badge - most tournament software ships it, so its boards are the ones that reconcile.

  • Fix The engine that pairs every tournament now was pinned to a build from before two of its own fixes. The pin moves to Ainalrami v0.11.1.

    Both fixes came from reading bbpPairings 6.0.0's source rather than inferring behaviour from its output. The final-round topscorer threshold used pointsForWin where the reference uses max(pointsForWin, pointsForDraw) - which only matters in a point system where a draw outscores a win, something FIDE would never publish but a TRF can state outright, since the values are free-form numbers in the file.

    It was measured rather than argued: a new corpus axis with a draw worth more than a win, run in two arms over identical seeds. With the fix, 3,775,174 rounds and 31,184,698 pairings at 100.00% agreement. With that one line reverted, 8,181 rounds paired differently from the reference - and zero illegal rounds in either arm, which is the point. The broken arm does not fail loudly. It quietly pairs a different tournament.

    The second fix: the engine recognised only 1, = and 0 as "a game was played". Correct for anything that came through its own TRF parser, which normalises the letter spellings W, D and L on the way in - and wrong for a caller building player maps directly, which is exactly what a host application does. A played unrated game read as unplayed carries no colour and no float history.

  • Change Ainalrami is the default Swiss engine now. A new Swiss tournament is paired by our own engine unless you go and pick JaVaFo; it used to be the other way round. The confirmation dialog flipped with it - it now asks before switching a running tournament to JaVaFo, since that is the change that swaps a maintained engine for a third-party one frozen on the 2017 rules.

    The reason for the flip is that Ainalrami is no longer the riskier choice. It implements the 2026 handbook text where JaVaFo 2.2 implements 2017, it agrees with bbpPairings 6.0.0 across two independent corpora of roughly 488 million pairings each, and the second corpus - run after the optimisation work - found zero disagreements. Every claim the settings screen used to make about the engines has been rewritten to match: the old copy called JaVaFo "FIDE-endorsed" and Ainalrami "experimental", and neither is a fair description any more. Nothing on the FIDE side is endorsed for the 2026 rules at all, which is the whole point.

    Existing tournaments are untouched. Whichever engine paired round 1 keeps pairing the rest.

  • Change The tournament's point system now reaches the pairing engine. An event scored 3/1/0, or one with a half-point loss, or a pairing-allocated bye worth something other than a full point, was pairing as though it were 1/½/0. The engine was reading its own defaults because nothing ever handed it the tournament's values. It does now - win, draw, loss, the pairing-allocated bye, the forfeit loss and the zero-point bye all travel with the pairing request.

    This matters most for downfloat history. A player whose "loss" is worth half a point was being recorded as having scored, and therefore as having floated down, which then steered later rounds. Tournaments on a standard 1/½/0 scoring see no change.

  • Change The About screen names the engine that is actually pairing. It showed the pairing system - "Swiss" - which stopped identifying anything once the system label and the engine name came apart. A Swiss tournament now reads "Swiss - FIDE Dutch (Ainalrami)", matching how round robin has always read "Round robin (Berger)". The printed pairing sheet credits the engine by name for the same reason.

[0.16.1] - 2026-08-23

Changed

  • Feature The arbiter UI is now translatable: 469 strings in one file per language. priv/gettext/nl/LC_MESSAGES/default.po would be Dutch - one entry per string, each recording the file and line it came from so a translator has context. Fill in the right-hand sides and the app speaks Dutch; nothing else needs touching.

    The player-facing pages stay English on purpose. Public pairings, standings, registration and mobile result entry are pinned via EnglishHook regardless of the chosen language. An open draws players from a dozen federations, and the language a Belgian arbiter picked for their own admin screens is not one to impose on a visiting player reading the standings.

    Nothing changes visually today: gettext returns the original text when no translation exists, so a wrapped app is byte-identical until a catalogue arrives.

  • Feature Language scaffolding, English only for now. Adding a language is now translation work rather than architecture work: add it to PairingsEngineWeb.Locale, run mix gettext.extract --merge, translate the .po, done. The picker appears by itself once there is a second language - it stays hidden while there is one, because a picker offering a single option is furniture.

    A locale comes from the session first, then the browser's accept-language, then English. The header matters more here than in most apps: the people reading the public pages are players with no account and no settings screen, so their browser is the only thing that can speak for them. nl-BE, nl-NL and nl all collapse to one catalogue.

    Two things this had to get right, both documented in docs/i18n.md. A LiveView does not run in the process that served the request, so a locale set only by a plug would be lost the moment the socket connects - the page would render translated and then silently revert. And the resolved locale is written back into the session, because a LiveView cannot read headers, and without it the dead render and the live render disagree.

    Almost nothing is translated yet; a few strings are wrapped to prove the pipeline end to end.

  • Feature Four new themes: Solarized Light, High Contrast, Tokyo Night and Nocturne.

    The set was five dark themes and one light, which is backwards for the work: an arbiter enters results in a brightly lit hall at two in the afternoon, not in a dark room at midnight. Three of these four address that.

    • Solarized Light - the palette already here, read the other way up.
    • High Contrast - pure black on white, heavy borders, no tinted surfaces. Not a flavour but an accessibility option, for reading a laptop across a hall; every other theme here trades contrast for calm.
    • Tokyo Night - deep indigo, distinct from a set that skewed grey (Nord) or purple-on-charcoal (Dracula, Catppuccin).
    • Nocturne - muted rose and gold over deep plum-grey. The only one that is not somebody else's palette.
  • Fix A categories test asserted a bare word against the whole page. It creates a player called "High" and then did refute html =~ "High", which broke the moment unrelated chrome contained that substring - the "High Contrast" theme in the topbar picker. The claim is about the preview modal's contents, so it now asks the modal rather than the document.

  • Removed The orange accent and the Gruvbox theme are gone too. Orange carried the same translucent-warm --accent-soft as amber, so removing only amber left the identical trap one click away in the picker. Gruvbox showed it regardless of accent, because its own signature colour is orange - that one was the theme working as intended rather than a defect, and it goes because it is not wanted.

  • Removed The amber accent is gone. In every dark theme its --accent-soft was a translucent orange, and that value is painted behind the active topbar tab and as the focus ring on selects - so a semi-transparent orange box followed you onto every page and around every dropdown you touched. All seven themes were swept against the accents to confirm: amber was the only choice that produced it. Gruvbox still does, but its accent is orange by definition, which is the theme rather than a bug.

  • Fix The stranded-page watchdog never fired. It tested liveSocket.isConnected(), which is transport-level - and the websocket can be perfectly connected while the LiveView on the page has failed to rejoin and is dead. That is exactly the state a restart produces, so the check reported "connected", cleared itself, and the reload it existed to perform never happened. It now requires the main view to carry phx-connected, which LiveView sets only while genuinely joined.

  • Change The restart banner tells you to reload if the page is stuck. It said "this page comes back on its own, no need to touch anything", which was not reliably true. A promise that does not arrive leaves somebody staring at a bar; "reload this page if it has not come back in a minute" costs one click and always works.

  • Change The history tree reads as a tree now. Three things it was missing:

    • Branches had no rail. The vertical line was drawn once for the trunk, so a branch appeared as offset diamonds with nothing joining them - and that a branch is a path is the one thing this view exists to say. Each lane now draws its own rail, fainter than the trunk, because the leftmost line is where the tournament is now and should stay the line the eye follows.
    • Cards ignored the pointer despite carrying buttons. Border and a whisper of shadow on hover, nothing more: a row is a record, and lifting each one off the page would turn a history into a deck of cards.
    • "Where the tournament is" could only be found by reading captions. That card now carries an accent left edge and a faint tint, so it is findable at a glance down a long list. The badge still says it in words.
  • Change A collapsed restore point now shows WHAT changed under it, not just how many. The row said "4 changes after this point", which is the one thing you cannot act on - it reads identically whether they were four rating refreshes or four re-pairings. Each point now carries a pip per kind of change folded beneath it, in the same hue the audit timeline and the expanded rows already use for that kind, so the colour language is one language rather than two. The count is still there, and screen readers get the kinds as text.

Fixed

  • Fix -fast on the deploy script did nothing. The flag was matched against --fast and -f only, so the single-dash spelling - the one actually typed - fell through and the deploy waited the full countdown with no complaint. A mistyped flag that looks exactly like a working one is the worst kind. --fast, -fast and -f are all accepted now, and anything unrecognised is called out instead of ignored.

Changed

  • Change The two-minute restart tier is quieter again. It was briefly given a tinted amber bar to make the state change harder to miss. Red at thirty seconds is where the volume belongs.

[0.16.0] - 2026-08-22

Security

  • Security Bandit upgraded to 1.12.5, closing a HIGH and a MEDIUM. Bandit is the HTTP server, so both were reachable from the internet: CVE-2026-74836 (HIGH) let HTTP/2 connection-window starvation pin Plug processes indefinitely - a denial of service - and CVE-2026-75484 (MEDIUM) passed header values containing CR, LF or NUL through to the application unvalidated.

  • Security earmark's CVE-2026-48591 is reported but not reachable here, and it stays for now. The advisory is a stored XSS through unescaped HTML attribute values, which needs attacker-controlled markdown. There is none: the only thing earmark ever renders is this repo's own CHANGELOG.md, at compile time, baked into a constant. Nothing user-supplied reaches it at runtime.

    The suggested replacement is a Rust NIF, and the release workflow cross-builds Burrito executables for five OS/arch targets - taking that on to close a hole that cannot be reached is the worse trade. The reasoning is written down beside the code, and a test now fails if earmark is ever called from anywhere else, because a second call site is exactly what would make the CVE live.

Changed

  • Fix The app was shipping a four-day-old Ainalrami, and every claim made about the engine was measured on a different build. The dependency is pinned to an exact commit on purpose - an engine that changes under you is the one thing a tournament manager can never allow - but the pin had not been moved since 17 August, leaving it 79 commits behind.

    What that build was missing is not cosmetic:

    • Article 5.2.5 initial-colour handling, added the day after the pin. This is the exact behaviour the settings screen cites when it says Ainalrami follows the current handbook text, so the app was making a claim its own bundled engine could not honour.
    • The entire matching-performance rewrite. Measured here on a round 6 after five played rounds: 100 players 4.35 s → 0.11 s, 200 players 82.7 s → 0.19 s, and 400 players did not finish in seven and a half minutes → 0.65 s. A 200-player event was effectively unusable and a 400-player one would have looked like a hang.
    • The validation the UI quotes. The zero-disagreements-over-488-million figure was measured against the current engine, not the pinned one.

    Pin moved to 6d739bd, and the full suite - including the JaVaFo-versus-Ainalrami differential tests - passes against it.

  • Fix The restart banner can no longer get stuck on screen forever. It was cleared only by a server push - fine, until the very thing it warns about is what stops those pushes arriving. If the socket did not come back, the page never heard the expiry and sat on "back shortly" indefinitely; reported after half an hour of it. It now times out on the client too, three minutes past the deadline, with no server involved.

  • Fix A page left stranded by a restart now comes back on its own. The banner promised "this page reconnects on its own" and it did not always: LiveView retries the rejoin every five seconds forever, so anything that makes the rejoin fail permanently leaves the page looking alive and doing nothing until somebody hits reload.

    The known cause was the SECRET_KEY_BASE rotation above - the signed session token baked into the page could no longer be verified, so every retry failed for the same reason as the last. That is fixed at the root, but a retry loop that can never succeed is a bad enough failure mode to guard against on its own terms. If a restart is well past due and the socket is still down, the page reloads itself, which always works.

    Only ever armed by an announced restart. Reloading on any long disconnection would catch people on flaky phone signal in a tournament hall, which is an easier way to lose someone's work than the problem it fixes.

  • Fix Updates no longer log everyone out. The deploy script has always meant to reuse SECRET_KEY_BASE across deploys, so sessions survive a restart - the code and the docs both said so. It never worked. The systemd unit is written as Environment="SECRET_KEY_BASE=..." and was read back with a pattern expecting no quotes, so the regex could not match the file the same script had just written. The reuse branch never ran, every deploy minted a fresh key, and every signed session cookie was invalidated.

    The script lives outside this repo and has been fixed there; both quoted and unquoted units are accepted now. Sessions survive from the next deploy onwards.

  • Feature "Updated to v0.16.0" appears once after an update, bottom right, dismissible, gone by itself after twelve seconds. Only when the version actually changed: a crash-restart or a config reload is not news, and a toast that fires for non-events is one people stop reading. The comparison happens in the browser, because only it remembers what was running before the restart - the server that knew has been replaced.

  • Feature --fast on the deploy script warns for 30 seconds instead of ten minutes, for a hotfix where the thing being fixed is worse than the interruption. The banner opens straight on its red tier, since there is no gentle phase to escalate from in half a minute.

    A flag rather than a smaller default: ten minutes is the right answer during a live tournament, and the short one should have to be asked for each time rather than inherited from a config file edited months ago. The notice endpoint accepts seconds as well as minutes now, floored at ten

    • a countdown nobody can finish reading is a flicker, not a warning.
  • Feature A deploy now warns everyone with a page open, before the server restarts. The deploy script announces the restart, waits, and only then restarts; every connected page shows a banner that counts down and escalates in three steps: "we will be away for about 30 seconds, results save as you enter them, and you stay logged in", then at two minutes "good moment to finish anything you are halfway through", then red at thirty seconds with "go and grab a coffee, we will be back in about 30 seconds".

    The countdown runs in the browser from a single timestamp, so this costs one message per page rather than one per second per socket. Someone who opens a page halfway through the countdown still sees it, because the deadline is held server-side and read on mount rather than only broadcast - that person is the whole point of the feature.

    The banner also says how long the outage lasts (about 30 seconds), that the page reconnects by itself, and that you stay logged in - the three things somebody actually wants to know when a bar appears telling them the server is going away.

    It deliberately says neither of the two obvious things, because both are false. You are not logged out - the deploy reuses SECRET_KEY_BASE, so sessions survive. And results are not lost: they write straight through as you enter them, so they are already saved. What a reconnect actually costs is server-side state rebuilt on mount - an open dialog, a half-filled registration form, a settings page with unsaved edits - and that is what it names. A banner that overstates its case is one people learn to ignore.

    Off unless DEPLOY_NOTICE_TOKEN is set: the endpoint fails closed, so an unset variable means no banner rather than an open route.

  • Fix Hovering a player at the bottom of the bracket map cut the popover off. The chart clips vertically on purpose (letting it overflow makes the mouse wheel scroll the graph instead of the page), so the canvas grows while a popover is open to give it room. That reserve was measured against a typical popover - and the tallest one belongs to precisely the player most likely to be at the bottom of the chart.

    A pairing-allocated bye goes to the LOWEST score group, and that player's popover carries two rows an ordinary one does not: the repeat-bye warning and the bye-detail footer. Deepest dot, tallest box, reserve sized for neither - about 52px of it was cut off below the graph, which is why it only ever happened on players with the fewest points.

    The reserve now covers the tallest popover rather than the average one. It costs nothing to be generous: the canvas only grows while a popover is actually open, so an over-estimate is a slightly larger transient grow, while an under-estimate silently clips content. There is a test that fails at the old value.

  • Fix The registration form's name matches are a dropdown now. They rendered as a list underneath the form, so every keystroke reflowed the page and pushed the birth-year and federation fields down, out from under the cursor of anyone halfway through filling them in. The matches now hang under the field as an overlay - the same panel the arbiter-side FIDE lookup already used. Clicking away closes it and keeps what you typed, which matters for anyone not on the FIDE list, who never picks from it.

    The matches are real buttons rather than an ARIA listbox, on purpose: those roles promise arrow-key navigation, and a screen reader announcing a listbox whose arrows do nothing is worse than plain controls. Tab reaches them, Enter picks, and a live region says how many appeared, since a panel opening is otherwise silent.

  • Feature The registration form can be embedded in a club's own site, like the public pairings and standings pages already could. Same <iframe>, same cookie-free socket.

    It was excluded under a blanket "no forms in a third-party frame" rule. That is the right default and was the wrong call here: clickjacking steals authority a victim already holds, and this form holds none - it needs a name, birth year and federation typed in, runs under an anonymous scope, and posts to this server rather than to whoever framed it, so it hands an embedding page nothing that fetching the URL would not.

    Nothing about who may enter has changed: registration_open is still checked twice, and entries are still rate-limited per IP.

  • Change "What the engine reported" moved to the foot of the rationale page. It was sitting above the bracket map and the board-by-board cards, which are what an arbiter actually opens the page for. It is reference detail for the moment somebody queries one board, not something to read past every time; the intro now links down to it.

  • Feature The rationale now shows, board by board, what the pairing gave up - and shows the colour record that explains it. One row per board: both seats with their assigned colour, each player's last six colours as a strip you can read at a glance, and a flag for anything sacrificed to pair the bracket at all ("colour preference denied", "upfloated again, having upfloated last round"). A board that gave up nothing says so, dimmed - checked and fine is information too.

    This replaces a first attempt that was, frankly, unusable: it printed every criterion that scored on every board, around 200 tags for a 40-player round, and - worse - it had the polarity backwards. These criteria are phrased so that HIGHER is better, because the engine maximises them. A board scoring 1 on "colour preference" is the board where the preference was honoured. The board an arbiter is asked about scores 0, and zeros were exactly what the old view filtered out, so the one fact worth having was rendered as nothing at all.

    Six criteria can be read as a verdict about a single board (the four colour ones, and the two repeat-upfloat ones). The rest deliberately are not shown per board: C7, C8 and C19-C21 are score-scale magnitudes used to rank candidates, and C14/C16 REWARD pairing a recent downfloater rather than penalising anything, so a zero there is not a compromise and must never be drawn as one. Float boards are exempt entirely - those criteria are gated on the pair being inside the bracket, so a float scores zero on all six by construction, and reading that as sacrifice would paint every floating board as a disaster.

    The full ladder is still there, one disclosure away, with the bracket totals. The boards still sum to those totals exactly, float boards included, and a test asserts it because the page says so.

  • Change The Ainalrami dependency is pinned to a version tag rather than a bare commit. The pin had sat 79 commits and four days stale without anyone noticing, because one commit hash looks exactly as current as another - there is nothing in a SHA to look old. v0.10.0 against a newer upstream is legible at a glance, in a diff and in review. mix.lock still records the exact resolved commit, so the pin is no less precise than before, only readable.

  • Feature The pairing rationale now quotes the engine instead of inferring it - for Ainalrami rounds. That page has always RECONSTRUCTED its brackets from a round's inputs and outputs, because JaVaFo hands back nothing but pairs and its reasoning cannot be extracted. Ainalrami can report its own working, so a round it pairs now stores that account on the round itself, at the moment it is paired.

    A new "What the engine reported" panel shows, per score bracket: who was moved down into it, who floated onward, the pairs it kept, and the FIDE C.04.3 criteria that actually scored, by name and value - "C6 pairs in bracket", "C12 colour preference" and the rest. Criteria that scored zero are not listed, because they did not come into the decision. Nothing in the app could show any of this before.

    It is stored rather than recomputed on demand for a reason: a round can be edited by hand after the engine paired it, and re-deriving the reasoning afterwards would explain a pairing the engine never produced. If boards are swapped or substituted later, the panel says so plainly rather than quietly presenting a record that no longer matches the boards.

    JaVaFo rounds, and every round paired before this release, keep the reconstruction exactly as before, and the page still says so.

  • Fix The app still said "JaVaFo" in places where Ainalrami had done the pairing. The rationale page titled every Swiss round "Swiss (FIDE Dutch / JaVaFo)" whichever engine ran, and - the one that actually matters - the IT3 report's B22 "Program used" was hardcoded to "OpenPairings (With JaVaFo)". That is a statement to FIDE about which program produced the pairings, so it was filing something untrue for any tournament paired by Ainalrami, and for round robin and Keizer events, which consult no Swiss engine at all. B22 now names the engine that actually paired, and reads plain "OpenPairings" where no external engine was involved. It is still hand-overridable, as before.

    The engine name now comes from one function on the tournament rather than being spelled out on each page, because this is the second time these labels have drifted apart.

    Remaining mentions of JaVaFo are the accurate ones: it is still the default engine, still credited in the README and licence notes, and the hints that described "XXP" as a JaVaFo rule now call it a TRF rule, which is what it is - both engines read it.

  • Change Every em dash in the codebase is gone, in comments, UI copy, docs and this changelog alike - 4,261 of them, plus 19 en dashes, replaced with the plain hyphen the code already used elsewhere. No wording changed, so nothing can have shifted meaning.

  • Change This changelog's tags are labels now, not emoji. A sparkle and a bug read as decoration and rendered at a different size in every theme font. They are coloured pills outlined in their own colour, which stays legible in both themes. The markdown source writes them as plain Fix so the file still reads on GitHub.

  • Change Settings → Options saves per subject instead of all at once. The page was one long form with a single "Save settings" button beneath everything, so changing the pairing system at the top meant scrolling past the engine notes, the rate of play and the public-pairings card to commit it - and the "Saved." that came back sat at the bottom, telling you nothing about which of those settings had just been written.

    It is now three: Pairing, Tournament type & rate of play, and Public pairings, each with its own Save and its own confirmation beside that button. Saving one leaves the others untouched, including any edits left unsaved in them. Forbidden pairings and club/federation exclusions already worked this way and are unchanged.

  • Fix Two lines on that screen still said Ainalrami was not allowed for a FIDE-rated event. They survived the change that permitted it and directly contradicted the warning printed a few lines below them. Both now say JaVaFo is the default and the safe choice, which is what is actually true.

  • Change The landing page leads with Ainalrami now, and no longer names JaVaFo. The hero read "Swiss pairings via JaVaFo" and the feature list offered "JaVaFo Dutch, or Ainalrami in beta" - advertising a third-party dependency as the headline capability, and the in-house engine as a hedge. Ainalrami gets its own line: our own Swiss engine, built in Elixir. JaVaFo is still the default engine and is still credited where the credit is owed - in the README, the licence notes and the cross-program-agreement documentation.

[0.15.1] - 2026-08-21

Changed

  • Change Choosing the Ainalrami engine now asks first, and the description finally tells the truth about it. The old copy said a handful of known disagreements remained and that forbidden pairings, club/federation exclusions and acceleration were unimplemented. None of that had been true for some time - the most recent measurement against bbpPairings found zero disagreements across ~488 million pairings, and all three features are supported.

    Understating an engine misleads an arbiter exactly as badly as overselling one, so the wording now says what it is: experimental meaning not FIDE-endorsed, which is a paperwork status rather than a measured quality one. Switching to it opens a confirmation dialog explaining that; switching back to JaVaFo needs no ceremony.

  • Change Ainalrami may now be used on a FIDE-homologated tournament. It was refused outright before. That refusal asserted a quality judgement the measurements do not support: the engine agrees with bbpPairings across ~488 million pairings, and the one article where it differs from JaVaFo is 5.2.5, where it follows the current handbook text and JaVaFo carries pre-2026 behaviour.

    What remains true is the paperwork: FIDE endorses engines, not pairings, so a rated round paired by Ainalrami was not produced by the engine the endorsement names. That is now stated rather than enforced - the setting carries the warning, and confirming the switch on a homologated tournament shows it a second time. The default is still JaVaFo, and nothing changes for a tournament that leaves it alone.

  • Fix The Pair button named the wrong engine. A tournament using Ainalrami still read "Pair round 5 (JaVaFo)", and the pairing sheet described pairings JaVaFo had not produced - the one place the engine choice became invisible after making it.

  • Change SWAR 3-2-1 tournaments are refused on import for now, with an explanation rather than a silent failure. Their scoring works differently - a presence point for turning up, on top of the result - and while that part is now understood and implemented, what a BYE is worth under the scheme is still unresolved. Importing anyway would produce a standings table that looks right and is wrong, which is worse than not importing. Every other SWAR tournament type is unaffected.

Fixed

  • Fix FA1 and IA1 arbiter norm forms had two values in the wrong boxes. FIDE revised those templates and the bottom block moved: the old "Recommendation: (please tick…)" section was replaced by a confirmation-and-signature block. Our field mapping already matched the NEW layout, but the templates shipped in the app were the OLD ones - so the tournament federation and the chief arbiter's date were written into the recommendation rows instead of beside "Federation" and "Date".

    Templates updated, and verified by filling a form and reading each value back beside its own label rather than by trusting the cell numbers.

    IT3 was unaffected - its layout is unchanged; the revision only adds a "Pairing System" label above a value that was already there.

  • Fix SWAR 3-2-1 tournaments were scored 2-1-0. The Belgian club scheme is Win 2 / Draw 1 / Loss 0 plus a presence point for turning up - which is what makes it 3-2-1. That presence point was only ever applied to byes, never to a played game, so every played round came out one point short.

    It is not a uniform shift: presence is paid per round ATTENDED, so a player who misses rounds falls one point further behind for each one, and the ORDER of the standings changed too, not just the totals.

    Found by reading SWAR's own source rather than by a bug report: GetPresentPtsUntilRound keeps presence in a separate accumulator and the ranking adds Points + ExtraPts + SpecialPts. A forfeit pays the winner only - the player who did not turn up does not collect a point for turning up - and a double forfeit pays neither.

    Only affects tournaments imported from a SWAR 3-2-1 file. Every other tournament carries no presence value and is untouched.

  • Fix "Team tournament" no longer looks like it does something it doesn't. Ticking it sets the FIDE classification on the report and nothing else - pairing has never branched on team type, so players are paired individually either way. Unlabelled, that was a silent trap: the round it produces looks like a perfectly good pairing, so there is nothing to notice until someone checks the boards against the teams. The box now says "Reporting only" and spells out that team pairing, team standings and team tie-breaks are not built yet.

[0.15.0] - 2026-08-20

The version in mix.exs had run ahead of this file: 0.14.32, 0.14.33 and 0.14.34 were tagged in code but never cut a section here, so everything since 0.14.31 had been accumulating under Unreleased. This release closes that gap - it covers those three versions as well as the work below, which is why it opens a new minor line rather than continuing 0.14.x.

Fixed

  • Fix Searching the local KBSB database returned nothing, always. The search box carried its phx-change on a bare <input> with no wrapping form, so LiveView sent %{"value" => …} where the handler expected %{"q" => …}; nothing matched, the LiveView crashed and silently reconnected, and the box looked inert. It had been broken from the start and nobody could see it, because the KBSB database was never populated until the data-platform sync arrived.

    The test that covered this synthesised the event directly, which proved the handler worked but not that the page could reach it. It now drives the real form element.

  • Fix Registering a player now fills in the club number, not just the club name. Both autofill paths - picking from the FIDE list, and typing a National id - set the name only, and the add dialog had no field to submit the number with. A player registered that way then showed up immediately as a pending club change, because "Update clubs" treats name and number as a pair.

  • Fix An accented player name no longer corrupts the FIDE TRF export. TRF16 addresses its fields by column, and every reader outside this codebase - FIDE's rating server, SWAR, Swiss-Manager - counts one column as one byte. Names were padded by character, so a single accented letter made the row one byte too long and shifted every field after the name. For a row like Hendricks, Björn that meant the FIDE id 1001 was read as 100 - a different player - the rating 2400 as 240, and the entire round history as blank.

    Exported names are now folded to ASCII (é → e, ß → ss), which is what FIDE itself does, so characters and bytes are the same thing again. Non-Latin scripts with no one-to-one Latin form become ? rather than being dropped silently, so an arbiter can see and fix them before submitting.

    Pairing was never affected: JaVaFo decodes UTF-8 and produced byte-identical pairings either way, verified against both JaVaFo and bbpPairings. The pairing input is deliberately left untouched.

Changed

  • Change "Refresh ratings" no longer touches national ratings. It refreshes the FIDE rating and title, and nothing else. A national rating is an import/manual-entry artifact - SWAR's own ELO lands there when you import a .swar file, the KBSB search fills it in when you register a player off the list, and you can type it - so a bulk button quietly rewriting it afterwards made OpenPairings look like it maintains a live national-rating system, which it does not. National ratings still work exactly as before as the pairing fallback when a player has no FIDE rating. The part of the KBSB list that is worth refreshing in bulk is the club, which has its own "Update clubs" button.

Removed

  • Removed The manual KBSB rating-list file upload. The Belgian roster now comes from the data platform, which has the club names the file never carried and needs nobody to remember to do it. The rating-lists page offers the sync and nothing else; with no API configured it names the settings to set rather than falling back to a file picker.

  • Removed The "Pair by: FIDE rating / National rating" setting, which never did anything. It was stored, validated and exported, but no code ever read it - pairing order comes from Player.rating/1, which is unconditionally FIDE-rating-first with the national rating as a fallback and never consulted the setting. Choosing "National rating" changed no pairing, which is worse than not offering the choice. National ratings still work exactly as they did as that fallback.

Added

  • Feature "Sync from data platform" on the rating-lists page. The Belgian roster can now be pulled straight from the KBSB data platform's Odoo-synced database instead of hunting down a rating-list file and uploading it by hand - including each player's club name and number, which is what "Update clubs" needs and which no earlier source provided. Appears once KBSB_API_URL and KBSB_API_KEY are set on the server; without them the page is unchanged and the file upload remains the only option.

    It fills the same local copy the file upload always filled, so nothing downstream changes: club refresh, registration autofill and name matching all keep reading it locally and keep working with the internet down, which is the point - rounds get paired in playing halls.

    Deceased members are now stored and handled explicitly: an exact National-id or FIDE-id lookup still resolves one, but a name match never will, so a living player cannot inherit a dead namesake's club.

  • Feature The Paid column is editable straight from the players table - click or right-click a player's Paid cell for "Paid" / "Not paid" / "Gratis", the same gesture the Pr. column already had for Present/Absent. Right-clicking the Paid column header applies one of the three to every player at once, matching Pr.'s "All Absent" / "All Present". The fee status was previously reachable only by opening a player for editing, which is several clicks each on a registration desk.

    Paid is not one of the columns shown by default - switch it on from the column picker first, as before.

  • Feature "Update clubs" also matches on name + birth year. The two id lookups (National id, then FIDE id) are exact but only help players registered off one of the databases. A player typed in at the desk with neither id - the common case at a club event - now resolves by name, with accents folded so "Bjorn" finds "Björn".

    It refuses to guess: a name that matches two people is only resolved if the birth year singles out exactly one, and a lone namesake whose birth year disagrees is treated as a different person. The preview gained a "Matched by" column so you can see which proposals rest on a name rather than an id.

  • "Update clubs" on the Players page. Beside "Refresh ratings", and the same gesture: it compares every registered player against the locally-synced KBSB list and shows a preview of the club changes before anything is written. Matches by National id, falling back to FIDE id when no matricule is on file, and never clears a club the list has no entry for - an arbiter who typed a club for an unaffiliated guest keeps it. Club name and club number always move together.

  • Feature Embedded pages default to the light theme. A frame sits inside somebody else's page, which is overwhelmingly light, so following the visitor's OS dark-mode setting dropped a dark slab into the middle of a white club site. Embedded pages now default to light regardless of the OS, and stay light when it flips at sunset. Only the DEFAULT changes -- picking a theme from the switch still works inside the frame, still wins, and still persists.

  • Feature Embedded public pages size themselves, and no longer reload-loop. Two follow-ups to the embedding support below, both found by actually putting it on a club site.

    The embedded page now posts its height to the host page whenever the content changes, so the iframe can be sized to fit rather than guessing a pixel value - a guess leaves the table scrolling in a small box or a slab of empty space under a short tournament. docs/public-pages.md has the snippet to receive it.

    And the embeddable pages now use a websocket that does not require the session cookie. A SameSite=Lax cookie is deliberately not sent to a cross-site iframe, so the normal socket connected with no session, LiveView rejected it, and the client retried forever - the page appeared to reload constantly on scroll and hover. Fixed with a second, cookie-free socket for those two pages, rather than by loosening the cookie for the entire app.

  • Feature The public pairing and standings pages can be embedded in another site. They previously sent frame-ancestors 'none' like every other page, so a club website putting one in an <iframe> got a blank frame and a console error. Those two now allow it:

    <iframe src="https://your-host/p/SLUG/pairings"
            style="width:100%;height:600px;border:0"></iframe>

    Nothing else does - not the public registration form, the arbiter tools, the mobile result entry, or any logged-in page, where being frameable is a clickjacking risk rather than a feature. Set PUBLIC_FRAME_ANCESTORS to a space-separated origin list to allow only particular sites, or to 'none' to switch embedding off entirely. See docs/public-pages.md.

  • Feature A second Swiss pairing engine: Ainalrami, opt-in and beta - the "multi-engine" item the login page has been advertising as coming soon. Settings → Options gains a "Swiss engine" control alongside the pairing system: JaVaFo stays the default and is unchanged, and Ainalrami - a from-scratch Dutch-system engine in pure Elixir, pinned as a github: dependency at an exact commit - is available as an alternative for club and non-rated events. It runs inside this app's own BEAM: no JVM, no jar, nothing to install alongside a release or a Burrito binary, which also means its tests are the first Swiss pairing tests in this suite that run on a bare checkout instead of being @tag :javafo-excluded.

    Both engines are handed the byte-identical TRF Pairing.javafo_input/4 already built - the engine choice only decides what turns those bytes into pairs, and everything downstream (create_round/5, board freezing, absentee byes, standings) is shared and cannot tell which one answered. That keeps the two directly comparable on real tournament data rather than only on synthetic input, which is the whole point of having a second implementation.

    Three guards, all in the data layer rather than the UI, because "the UI hides the control" has never been enforcement here:

    • Never on a FIDE-homologated tournament. OpenPairings' entire FIDE story is FE1's "Internal engine: NO - thru JaVaFo", exactly as Vega, Swiss Manager and TournamentService answer it, and JaVaFo's own endorsement is what then covers pairing legality. The changeset refuses Ainalrami on a homologated tournament and refuses ticking "FIDE-homologated" on a tournament already running Ainalrami - the reverse direction matters just as much, and checking only the changed field would have let it through.

    • Locked after round 1, via Tournaments.locked_fields/1 (so update_tournament/2 refuses it, not just the disabled select), same rule and same reason as pairing_system one level up: two independent Dutch implementations won't always agree, and swapping mid-event hands the new engine a history it didn't produce.

    • Refuses rather than silently ignores anything it can't do. Ainalrami reads every TRF extension this app emits - XXR (rounds), XXP (forbidden pairings and club/federation exclusions) and XXA (Baku acceleration) - so forbidden pairings and accelerated events work on either engine. That's new: Ainalrami's parser used to discard everything but XXR, which for this input is the worst way to fail, because an engine that ignores an XXP line still returns a complete, perfectly legal-looking round that just happens to seat two players who must never meet, and nothing downstream can tell. Integrating it here is what surfaced that; both extensions were implemented upstream rather than worked around, and the round is still refused outright - nothing written, reason named - if the generated TRF ever carries an extension the engine doesn't know. The check reads the generated file rather than the tournament's settings, so a future extension is caught by default instead of being quietly ignored.

      A late catch on that last point, found by a source sweep before any of this shipped: the integration parsed the XXP lines and then dropped them. Ainalrami.Pairing.pair_next_round/2 takes forbidden pairs as an OPTION rather than reading them off the parsed file, and the call passed only the round count - so every explicit forbidden pairing and every club/federation exclusion was silently ignored, which is precisely the failure the guard above exists to prevent. The test that was supposed to cover it could not fail: it asserted that ranks 1 and 2 were not paired in round 1 of a six-player field, where the Dutch system pairs top half against bottom half and those two could never have met anyway. Both are fixed, and the replacement is four players - where the forbidden pair IS the natural pairing - with a control test pinning that.

    Round robin and Keizer are completely unaffected - they compute their own pairings and never consult the setting. See docs/pairing-systems.md.

  • Feature "Save a restore point" on the History page - restore points could only ever be created as a side effect of something else. Snapshots.capture/4 was called from exactly four handlers, all of them in PairingsLive: pairing a round, unpairing one, pairing a whole round-robin schedule, and importing results from a CSV. Run a tournament the way small clubs actually do - register players by hand, tune the settings, type results in one board at a time, never touch an import - and the app took no snapshots at all. The History page then had nothing to offer a "Go back to here" button on, so the restore machinery that has been shipped for a while (branch tree, "Switch to this branch", the type-to-confirm modal) was invisible and the whole page read as a list you could only look at. That was the actual complaint behind "History is just a read-only thing".

    So: a button that takes one on demand, with an optional short label ("End of day 1", "before the appeal") that becomes the point's caption on the timeline. It goes through the same Snapshots.capture/4 as the automatic ones - same payload, same retention, same branch tree, same restore path - under its own snapshot.manual action code so hand-saved points show a "saved by hand" badge and read apart from the ones the app took to protect an irreversible action. It's a write like any other: refused on an archived tournament, and logged to the audit trail.

    The empty state is honest now too. A tournament with no restore points yet says so, explains that hand-editing doesn't take one, and points at the button - instead of silently rendering a timeline with no actions on it, which is what made this look broken rather than merely unused.

    The automatic triggers are deliberately unchanged: these are full tournament copies, so adding more of them shifts storage behaviour for every existing tournament and deserves its own decision (candidates noted in TODO.md).

  • Feature "Assign categories" now shows a dry-run preview before it writes anything - clicking the button used to reassign every player's category instantly, no warning. It now computes the same rule decisions (Tournaments.preview_auto_assign_categories/1, the single source of truth the real write path now reuses too, so preview and apply can never drift apart) and shows a Player/From/To diff in a confirm modal, filtered to only the players who'd actually move. Cancel discards it with zero side effects; confirming re-checks current DB state rather than trusting the staged preview verbatim, so a concurrent edit mid-modal can't write stale decisions. A no-op roster (nothing would change) says so plainly instead of opening an empty diff.

  • Feature "Substitute player" now shows both halves of the move, with real in/out arrows - previously the confirm modal only showed the board seat changing, with no visual for where the outgoing player went or the incoming player came from. A new "Not playing list" row appears alongside the board row (the outgoing seated player arriving there, the incoming pool player leaving it), and the existing .SwapArrows journey-arrow mechanism - which matches identical names appearing once on the "before" side and once anywhere on the "after" side, across the whole modal, not just within one row - draws the two arrows automatically from that shape alone. No changes needed to the arrow-drawing code itself. This also fixes a real bug the old single-row layout had: the unchanged seat (say, White, when Black was the one substituted) still showed the same name on both its own before/after sides, so the old name-matcher drew a spurious arrow between White's two seats instead of showing anything for the actual substitution - misleading, since it looked like White had somehow moved. That seat still gets its own short "stayed put" arrow today (the same thing an unmoved partner in a two-board swap already got), but it's no longer the only, unexplained arrow on screen.

  • Feature Hide a fully-vacated board, or delete the round's actual last one - two ways to clean up a pairing row that's left over from marking both players on it absent, without ever risking the 0.14.6 board-renumbering bug class again. "Hide" (right-click the empty board, or from the new "Hidden boards" panel) is a display-only, fully reversible toggle - it's skipped by PairingsLive, LiveRoundLive, PublicPairingsLive and print docs, but the row, its frozen display_board, audit history and TRF/export data are completely untouched, and never interacts with PairingDisplay's freeze mechanism at all. "Delete" is a real, permanent removal, but is only ever offered - and only ever accepted server-side - on the round's own current highest real board number, since deleting the trailing row is the one deletion that can never require renumbering anything after it. It goes through the same confirm-modal pattern as every other hand-edit gesture on this page.

Changed

  • Change The login/registration hero no longer lists "multi-engine" as coming soon - it ships (above), so the Swiss line now names both engines and the coming-soon row is down to TRF26 alone.
  • Change Result cards redesigned - each player's name, rating/starting-№, and signature line used to be crammed onto one small row alongside a "Sign _" blank; the name is now on its own, larger row, with the signature on a separate row below it instead of competing for space on the same line. The result row (1-0 / ½-½ / 0-1) is now genuinely centred - the trailing "other: ...." option previously used margin-left: auto to push itself right, which dragged the three main results left of true centre as a side effect.
  • Change Removed the "Print player cards" button from the Players page - kept causing confusion with "Print place cards" right next to it and wasn't something arbiters actually used; the print route itself is untouched.
  • Change The swap-confirm modal (swap/vacate/fill/substitute/pair) is wider (720px instead of 560px, the same .pe-modal-wide variant the categories preview above already uses) so names have more room before truncating.
  • Change Removed the "⇄ Board N" chip from a cross-board swap's changed seat - confusing, and redundant with the journey arrows the modal already draws to show where a moved player came from. Also removed its now-fully-dead plumbing (related_board, threaded through three functions purely to feed this one chip) and the flex-wrap machinery that existed only to give it its own line.

Changed

  • Change The History page is the restore-point tree now, not a merged event feed

    • it used to interleave audit rows and restore points as peers, which broke down in three ways once it was actually used. Saving a point by hand wrote both a snapshot and an audit row, so it showed up twice. Audit rows carry no branch information, so after switching to a branch a result change still rendered at the top of the trunk, which is not where the tournament was. And the page carried filters for "players", "settings" and so on - questions about changes, which is the Audit trail's subject, not this page's.

    So the points are the timeline, and everything that changed between two of them is folded under the earlier one, collapsed until you open it. A branch can be collapsed to a single line. The kind filters are gone; the Audit trail keeps the full searchable log, and this page links to it. Day headings went too - one collapsed branch can stand in for points spanning several days, which headings couldn't survive - so each point carries its own date.

    The save box got a real label and an example instead of the placeholder "What is this point?", which read as a search field. And having exactly one restore point no longer looks broken: it is the point the tournament is already on, so it offers no "go back", and the page now says why instead of showing a save button and nothing else.

Fixed

  • Fix Settings now says where the chief arbiter lives - the Officials fields sit on the Norms page, because that is what builds the IT3/FA1/IA1 forms from them, but an arbiter looking for "chief arbiter" looks under Settings, and Settings said nothing at all. Reported by someone who could not find the field while holding a direct link to it. Settings → Tournament grew an "Officials" card that links to Norms and shows the current chief arbiter, or says it is not set and that this does not block pairing (it is a recommended field, not a required one). It edits nothing - moving the fields back is a separate question about what the Norms page is then for.

  • Fix The accelerated TRF this app exports was malformed, and only JaVaFo would read it - XXA lines carry Baku acceleration's virtual points, and the writer padded each player's starting rank to five columns instead of four. That put a single-digit rank in column 9 and left columns 5-8 blank, shifting every value on the line one place right of where the format puts it. JaVaFo accepts the malformed form, which is exactly why this survived: JaVaFo has been the only thing that ever read these files. Any stricter reader rejects the line outright, so an accelerated tournament exported for a rating officer, an archive, or a second engine was silently unusable. Confirmed against the real bbpPairings binary, which reads the rank from columns 5-8 and each value four wide on a five-column stride, and proven by generating one accelerated TRF that differs only in this layout: the old form is refused as an invalid line, the new one parses - and JaVaFo pairs both identically, so no existing pairing changes.

  • Fix The bracket map's variable dead space, under the graph - on "Pre-round score brackets" there was sometimes a sizeable empty gap between the graph and the minimap strip below it, and sometimes none. Reported as intermittent; it was entirely deterministic. The canvas's inline min-height permanently reserved the room a hover popover needs below the lowest dot that opens downward, and because the above/below flip deliberately tests against the much taller pinned popover's room, on every round past the first essentially every dot opens downward - so the reservation cleared the graph's own floor by about 110px on most rounds and by nothing on the few whose bracket happened to be tall enough. Which round you looked at decided whether you saw a gap.

    Now both reservations are deferred, the same way the pinned one already was: the canvas rests at exactly the graph's height and grows via :has() only while a wrap is actually hovered, focused or pinned (--pe-hover-min / --pe-pinned-min). The popover can cover what sits under the chart while it's open, which is the deliberate trade - a tooltip overlapping its surroundings for as long as you hover is better than a permanent empty band under every chart. Nothing about the flip itself changed, so a pinned popover still always opens onto the side it reserved room on, and neither popover is clipped by the strip's overflow-y: hidden.

  • Fix Something covering the corner of the "Everything" button on the History page - the timeline's day headings paint an opaque 10px-wide mask over the rail so the date sits in a clean gap, and that mask deliberately overhangs 6px above the label to cover the rail's own inset. Above the first heading there is no rail to cover - the rail lives inside the <ul> below it - so the overhang only reached up into the filter row, which sits directly above with no margin between them, and landed exactly on the bottom-left corner of the first filter button. The first day group is now marked server-side (.tl-first-day) and drops the upward overhang; later headings, which genuinely do have rail above them, are unchanged. The marker has to come from the server because each heading is the first child of its own day group, so .tl-day:first-child matches every one of them rather than the first.

  • Fix Three real "vacated seat" crashes in the "Explain this round" page and its underlying analysis, found and fixed over one afternoon as each one surfaced the next. Tournaments.vacate_seat/3 can empty EITHER colour's seat (not just the one a bye already leaves empty), which turned out to be a state nothing downstream actually expected:

    1. PairingRationale.board_context/7's rematch check assumed White is always present whenever the board isn't a formal bye - crashed with key :id not found in: nil reading white.id when only Black remained seated.
    2. A bye's own recipient can be vacated too (a bye pairing already has Black's seat empty; vacating White leaves nobody at all, but is_bye
      • which only checks Black's seat - still reports the board as a bye). annotate_bye/3 then crashed reading the vanished recipient's name. Fixed alongside a real, quieter second bug in the same function: an empty "ghost" bye board sorting before a real one would have silently reported the empty ghost as the round's allocated bye instead of the actual recipient.
    3. Once the analysis itself stopped crashing, PairingExplainLive's own rendering turned out to make the identical assumption in three separate places - the score-bracket graph, the "Board by board" card list, and the "Pairing numbers" table - each reading a player's name unconditionally with no guard for a missing seat. Boards missing a side are now excluded from the score-bracket graph entirely (there's no player to plot), and the other two now show a plain "Seat vacant" note instead of crashing.
  • Fix The round-dates setup checklist could stay stuck on "missing" forever even after every date was filled in - round_dates was only padded or truncated to match rounds_count on the Dates settings page's own save, not wherever else rounds_count can change (e.g. round-robin silently correcting it to match the real Berger schedule on freeze). Moved into the shared tournament changeset so it happens on every save, from any page.

  • Fix The "Delay (minutes)" field showed even when "Publish each round" was set to "Immediately" - it now only appears for "After a delay", live, as the dropdown changes.

  • Fix A print doc could show a stale fixed-board note after a round was already paired and frozen - fixed_board_note/1 (score sheets, result cards) read Player.fixed_board live, bypassing the freeze mechanism that already exists for exactly this (see the 0.14.6-class fix below) and contradicting its own module doc's claim to be the only such reader. A mid-round edit to a player's fixed board could make a reprinted document disagree with the already-frozen main pairing sheet for the same board. Now reads the pairing's own frozen data instead.

  • Fix The swap-confirm modal's before/after cards could end up different heights, making the connecting journey arrows look crooked - caused by a long name wrapping onto a second line in one card but not its partner. Names no longer wrap (truncate with an ellipsis instead, full name still available on hover); cards now size to their own content instead of a forced equal split, so a short name's card isn't stretched to match a long one.

    • A related, more specific wrap bug survived that first fix: the W/B colour badge could end up alone on its own line, above the name, on any "Swap colours" confirm with two ordinary-length names - not just a pathological long one. Root cause: with flex-wrap: wrap on the seat row, the browser decides which line an item lands on using its un-shrunk preferred size, not its post-ellipsis size - so a name "too wide" to fit next to the badge got bumped to a new line before the shrinking/ellipsis logic ever got a chance to run.
    • "Pair these two" (pool-pair) could show a large, visibly empty gap between its two cards - a CSS Grid trap: the arrow column's auto track sizing doesn't mean "size to content" the way it does for a width property, it means "absorb the grid's leftover space" when nothing else claims it, and two narrow cards left a lot of it. Changed to max-content, which never grows past the arrow glyph itself.

Fixed

  • Fix An archived tournament's Pairings page let you enter/change results - the underlying write was always correctly refused (Tournaments.ensure_writable/1, in place since archiving shipped), but the result <select> had no disabled state tied to archived_at, and a refused write showed no error at all. Worse: because a refused write leaves every assign byte-identical to before, LiveView sends no patch for that board's <select> - so the browser's own "user just picked this" native state was left uncorrected, visually looking like the change had taken even though nothing was written. Fixed by disabling the select outright while archived, surfacing a clear "This tournament is archived" error on any write that's still refused, and forcing a patch on refusal (a small nonce threaded into the element) so the existing data-result-resync hook actually runs and snaps the value back. The right-click board-editing menu (swap/mark absent/award bye/fill seat/pool pair) now also refuses to open at all on an archived round, and CSV results import is hidden.
  • Fix Two more instances of the "crashes instead of showing an error" bug class found while fixing the above: the Players page had its own broken local error_text/1 (same shape as an earlier-fixed crash in Settings) that raised trying to read .errors off the bare atom :archived instead of a real changeset - editing, deleting, or bulk actioning a player on an archived tournament crashed the LiveView. CSV results import had the identical bug one level down, in ResultsImport.write_all/2. Both fixed; audited and fixed the same "write refused but the LiveView shows nothing" gap (no crash, just silence) across the Pairings, Players, Categories, Standings, and Tournament/Options settings pages too, so every one of them now shows a clear error instead of quietly no-oping.
  • Fix A production migration wrote display_special (the fixed-board freeze from the fix below) as the literal text "true"/"false" instead of the integer 0/1 - Repo.update_all against the raw "pairings" table name has no :boolean type info to encode through, so SQLite stored the Elixir atoms' literal text. Every page that loaded a pairing then crashed with cannot load "false" as type :boolean, a full outage confirmed and fixed live in production (data repaired, migration source corrected so a fresh deploy can't repeat it).
  • Fix Only a tournament's owner could archive it - a co-arbiter it was shared with could not, even though sharing otherwise grants "exactly like you" access. archive_tournament/unarchive_tournament now go through the same owner-or-accepted-collaborator check every other shared action uses; an archived shared tournament also now shows up in a collaborator's own Archived panel (marked "shared"), not just the owner's - previously it would have simply vanished from their dashboard the moment anyone archived it. Delete stays owner-only, same as the main list.
  • Fix Giving a player a fixed (accessible) board after their round was already paired silently renumbered every board after theirs - PairingDisplay read Player.fixed_board live on every render, so an arbiter editing a player's fixed board mid-round (e.g. on the Players page) retroactively moved that player's pairing to the special/end-of-list group and closed the gap in the ordinary sequence, shifting every board number after it, while people were already seated. The special/ordinary classification and board label are now computed exactly once, at the moment a round is (re-)paired - ordinary pairing, round-robin, Keizer, or an import/restore - and frozen onto the pairing (display_board/display_special). Editing a player's fixed board no longer has any effect on an already-paired round; it only takes effect the next time that round is paired (a fresh round, or an explicit unpair-and-re-pair). The narrower survivor of the 0.14.6 board-renumbering bug class.
  • Fix The top bar overflowed the page horizontally at laptop widths (roughly 769-1280px, i.e. most laptops with the window not maximised) - the auth cluster (FIDE/KBSB sync status, email, Settings, Log out, version) never shrank, so it pushed the whole page wider than the viewport. There were no responsive rules at all between the 1280px desktop layout and the 768px tablet one. Two new breakpoints now drop the sync strip, then the email and version, before anything is forced to wrap.
  • Fix The "Advanced" and "Settings" top-bar dropdowns, and the accent/theme pickers, were always laid out and hit-testable, not just when opened - .topbar-menu-panel/.accent-picker-panel/.theme-picker-panel each set display: flex (or grid) unconditionally, relying entirely on <details>'s own native collapse to hide the closed panel. In Chromium that collapse uses content-visibility: hidden, which still reports the panel's full-size box for layout purposes while correctly skipping paint/hit-testing - so a closed dropdown was invisible and non-interactive, but its ~180px-wide box still counted toward the page's scrollable width. This was most of what was actually causing the top-bar overflow above (a closed multi-item "Settings" panel poking out past the viewport with no visible cause). All three panels are now display: none unless their <details> is [open], which is also simply correct regardless of any one engine's collapse behaviour.
  • Fix Norms was missing from the "Advanced" sub-nav strip - the top-bar dropdown menu listed four pages (Norms, History, Audit trail, Pairing rationale) but the strip rendered on each of those pages only showed three boxes, because NormsLive never rendered the shared sub-nav at all. It does now, and the sub-nav component covers all four consistently.
  • Fix Settings that decide the shape of already-paired rounds were locked only in the Settings LiveViews, not in the data layer - pairing_system, rr_cycles, rr_match_format, swiss_match_format, pair_by_category and the "Pt ABSENT" trio (abs_value/abs_jusque/abs_nbfois) were disabled inputs plus a strip of the submitted params in SettingsOptionsLive/SettingsScoringLive - enforced nowhere else. Confirmed exploitable: calling Tournaments.update_tournament/2 directly with pairing_system: "keizer" on a tournament with a paired Swiss round silently succeeded, reinterpreting every round already on the board under different pairing/scoring rules. Tournaments.locked_fields/1 is now the single source of truth - update_tournament/2 itself refuses a change to any locked field once round 1 is paired, and both Settings pages render their disabled state from the same function, so the two cannot drift apart again. This is the same shape of bug the archive guard (ensure_writable/1) exists to prevent, just for a different property - "the UI hides the button" is not enforcement.

Added

  • Feature Archive a tournament - a new frozen read-only state, separate from the Recycle bin. An archived tournament stays fully readable: its own pages, public link, prints and exports all keep working exactly as before. It just refuses every change until you unarchive it. Unlike the bin, nothing archived is ever purged automatically - it's "done with this, keep it forever", not "on its way out".

    Archived tournaments leave the main list and get their own Archived panel on the Tournaments page, with an amber archived badge, and every page inside one carries a persistent read-only banner.

    The read-only part is enforced in the context layer (Tournaments.ensure_writable/1, called by all ~30 write paths plus the pairing engines), not by hiding buttons - a stale open tab, a queued LiveView event or a direct script call are all refused too. archived_at is a dedicated column rather than a status value (status is derived and would be recomputed away) and is deliberately not cast by the ordinary changeset, so no settings save can archive or silently unarchive anything.

Fixed

  • Fix SettingsSupport.error_text/1 crashed the LiveView on any non-changeset error. It called changeset.errors unconditionally, so a bare reason atom parsed as a remote call (:archived.errors/0) and took the whole page down. Found by the archive tests - a settings save on an archived tournament hit it immediately. Now handles changesets, atoms and strings.

[0.14.31] - 2026-08-13

Changed

  • Change Public pages are now off by default for a new tournament, instead of on - player names, ratings and clubs no longer become reachable by anyone who finds the link until an arbiter deliberately turns it on (Settings → Tournament → Public pages), same "opt-in per event" reasoning self-registration already used.
  • Change Scoring moved to its own Settings tab - points per win/draw/loss, the pairing-allocated bye value, and SWAR's "Pt ABSENT" genuine-absence rule used to live at the bottom of the Options page; they're on Settings → Scoring now, since "how many points" is a different concern from "how the tournament is paired" (Options).
  • Change Categories on/off moved onto the Categories page itself - it used to be a checkbox buried in the Options form (labeled "Enable the Categories tab", even though the tab was already always shown). It's now a plain Turn on/Turn off button right on the Categories page, taking effect immediately - no more Save-then-navigate-over round trip. "Pair each category independently" moved there too, next to the switch it depends on, marked (beta), and turning categories off now also turns this off in the same write (previously conflicted with the changeset's own validation if attempted directly).
  • Change Clearer genuine-absence field labels, on the new Scoring page: "Points for a genuine absence (SWAR's 'Pt ABSENT')" → "Points awarded for a genuine absence", with a fuller explanation of what "genuine absence" actually means (a plain no-show, distinct from a requested bye or a forfeit) above the fields. "...pays through round (inclusive)" → "Last round this still applies to"; "...for up to this many absences (cumulative)" → "Cap: only a player's first N genuine absences pay" - both with longer, more concrete hints.
  • Change A prominent warning now accompanies "Treat a genuine absence as a voluntary unplayed round for tiebreaks" - this option changes FIDE tiebreak results (Buchholz/Sonneborn-Berger), not just scoring, and FIDE's own C.07 has no such concept at all. The warning appears the instant the checkbox is ticked, before the page is even saved.

Fixed

  • Fix The Save-settings button on the Tournament and Options pages sat crammed against whatever card came right after it (Tiebreaks → Save → Public pages / Share-Team, and Options → Save → Forbidden pairings)
    • a bare .actions row only ever had margin above it, none below, so it visually ran into the next section. Now has proper spacing on both.

[0.14.30] - 2026-08-13

Added

  • Feature Public pairings can now be published on a delay instead of the instant they're paired - a new "Publish each round" setting (Settings → Options → Public pairings) picks between four modes: Immediately (the old, still-default behavior), Manually (a round stays hidden from /p/:slug/pairings until you publish it by hand), After a delay (a fixed number of minutes after pairing), and On the round's own date (midnight UTC of that round's date on the Dates page). Whichever mode is active, the Pairings page also gets a standalone Publish now / Unpublish button and a public/not-public badge for the round you're viewing - a manual override that works regardless of mode, so an arbiter can always release a round early or pull it back, even under a timed or scheduled setting. Rounds can be published out of order (round 3 public while round 2 still isn't); the public page always shows exactly the round it was asked for, or a placeholder if that one isn't out yet.

[0.14.29] - 2026-08-13

Changed

  • Change Start/end date are no longer entered separately from round dates - they used to be their own fields on the Tournament settings page, independent of the per-round dates on the Dates page (two places that could say different things about what's really one piece of information). They're derived now: the earliest and latest non-blank entry in round_dates, recomputed on every save regardless of which settings page triggered it. The Dates page shows the derived result live as a read-only preview, updating as you type before you even save. The "New tournament" form keeps its one "Date from" field (it only ever seeded every round to the same date); "Date to" is gone - it would have been silently ignored under the new model.
  • Change A tournament's setup no longer independently requires a start date - requiring round_dates already covers it now that start date is derived from that list, not a second, separate requirement to satisfy.

[0.14.28] - 2026-08-13

Fixed

  • Fix The swap confirm modal's card pairs still ended up visibly uneven with real (longer) names, even after 0.14.27's top-alignment fix - the "⇄ Board N" chip sat on the same line as the name, so it ate into the name's own available width on only the "after" card (the one side that ever gets a chip), making the exact same name wrap harder there than its own "before" counterpart managed without a chip competing for room. The chip now forces onto its own line below the name, so both sides of a pair get identical width to wrap into. Verified against the exact case that surfaced it (Burssens, Ruben (1502), a name long enough to wrap under a chip but not without one): the name itself no longer wraps at all now, on either side.

[0.14.27] - 2026-08-13

Fixed

  • Fix The swap confirm modal's two board-diff cards could end up visibly misaligned when a long name wrapped to two lines in one card but not its mirror (e.g. on the White row of one card, the Black row of the other) - centering each card vertically meant the taller one grew upward as well as downward, so its own White row landed at a different height than the other card's White row despite White not having changed. Both cards now align to the same top edge instead.

Changed

  • Change Every player shown in a swap now gets a journey arrow, not only the ones who moved - a two-board player swap shows 4 people (the 2 who traded boards plus whoever they left behind on each board); all 4 now get one, in their own colour: the 2 travellers get a real crossing journey, the 2 who stayed put get a short arrow back to their own seat, so nobody shown reads as forgotten next to the two who visibly moved. A same-board colour swap still shows exactly 2 (there's nobody else on that one board to draw) - unchanged. Verified live: 4 paths, 4 dots, one per player's own colour; the two stationary players' paths measure as flat, near-zero-length arrows back to themselves.

[0.14.26] - 2026-08-13

Security

  • Security phoenix_live_view 1.2.7 → 1.2.9, clearing a LOW-severity open redirect (EEF-CVE-2026-64941 / GHSA-36m4-rm57-3prf) in LiveView's own internal validate_local_url!/2 check - an ASCII tab/LF/CR in a URL could pass that check as "local" while a real browser strips those characters and follows the link elsewhere. Pulled phoenix along with it (1.8.9 → 1.8.11, phoenix_live_view's own dependency floor) - both already inside this project's existing mix.exs version constraints, so only mix.lock changed.

    Not obviously reachable from this app's own code today - the one place that stores a redirect target (user_return_to) uses the current request's own path, not an attacker-supplied parameter - but the vulnerable check sits inside every redirect/push_navigate/ push_patch LiveView does internally, so worth clearing regardless.

[0.14.25] - 2026-08-13

Added

  • Feature Every player shown in the swap confirm modal gets their own colour
    • not just the two who moved. A two-board swap shows up to four people (the two travellers plus whoever stayed put on each board); each now has a distinct colour (from a fixed 6-colour palette, independent of the tournament's own accent) applied to their name everywhere it appears, so identity is legible at a glance instead of only "changed vs. unchanged". Each traveller's journey arrow, dot, arrowhead and "⇄ Board N" chip now match their own colour too - read straight off the seat elements the arrows hook already found, so there's no separate colour list to keep in sync between Elixir and JS. A same-board colour swap gets two colours; a non-swap confirm (mark absent, award bye, fill a seat, pool-pair) gets none, same as before.

[0.14.24] - 2026-08-13

Fixed

  • Fix Swap journey arrows: the two curves crossed below the middle rather than at it, which read as a mistake rather than a swap. Each curve was built with one shared control x ("a lane per arrow"), which is not symmetric; with +k out of the start and −k into the end, both curves pass through the exact centre of the channel at their own half-way point and meet there.
  • Fix The arrowhead was still turning as it landed. Each curve now ends (and starts) with a short straight run, so the head sits on a level segment.

[0.14.23] - 2026-08-13

Added

  • Feature The swap confirm modal now draws each moving player's journey - a curved arrow from where they sit now to where they land, with a dot at the start and an arrowhead at the finish. Two boards trading players gives two curves crossing in the middle, which is the swap itself; a same-board colour swap crosses inside the one row. The curves route through the centre column (the static "→" steps aside and widens into a channel) so they never cross the opaque board cards.

    Which seats get joined is worked out by matching a name that changed seats on both sides, so mark-absent, award-bye, fill-seat, pool-pair and substitute-from-pool draw nothing at all - nobody travels between two shown boards there - with no extra server-side state to keep in sync. Purely additive: with no JS, or if measurement fails, the plain "→" layout and the "⇄ Board N" chips stand exactly as before. Honours prefers-reduced-motion (no draw-in animation), and redraws on window resize and on re-render.

[0.14.22] - 2026-08-13

Fixed

  • Fix The new W/B swap-modal badges (0.14.21) were inverted on a dark theme - White rendered as a dark box, Black as a light one. The fix used var(--surface)/var(--text) for an "inverted pair," not noticing those tokens themselves flip between light and dark themes. Now fixed literal colours (the same cream/charcoal pair the brand mark's own chess pieces use), so White is always the light box and Black always the dark one, independent of theme.

[0.14.21] - 2026-08-13

Changed

  • Change The swap confirm modal's board-seat colour indicator is no longer a fixed-red dot - replaced with theme-aware W/B letter badges (White: bordered, page-surface background; Black: inverted, same pair either way), so it no longer reads as an alarm colour in the one place - mid-swap - nothing is actually wrong. A genuine two-board swap's changed seat also gets a small "⇄ Board N" chip pointing at the other board involved, so the two independent before/after cards no longer need to be mentally cross-referenced by hand.

[0.14.20] - 2026-08-13

Added

  • Feature PGN export can include board numbers - right-click "Export PGN" on the Pairings page (same .PrintMenu pattern as "Print pairings"/"Print result cards") for three more variants: this round with board numbers, every round, and every round with board numbers. ?board=1 adds a [Board "N"] tag to every game, using the same DISPLAY board number every other view shows (fixed-table boards relabeled/moved), not the raw stored board.
  • Feature Editing a past (non-latest) round's pairings now asks you to confirm it's not a mistake - swap, mark absent, pool-pair, fill a seat, award a bye: any pairing-altering action staged on a round that isn't the tournament's current latest paired round now shows a clear warning in the confirm modal ("You're changing round N, not the current round"), with the primary button disabled until an explicit "I understand, apply anyway" checkbox is ticked - both client-side and re-checked server-side. Entering/editing a result is unaffected; this only gates changes to who's paired with whom.

Fixed

  • Fix Public pairings round-picker/table sat flush against each other with no gap.

[0.14.19] - 2026-08-13

Fixed

  • Fix SWAR export produced a file real SWAR misreads for a player with many rounds absent and no per-round "byes" row (globally absent: true, never explicitly marked absent round-by-round). A round with neither a Pairing nor a byes row used to be silently omitted from that player's [RONDE] array - internally consistent for our own reader, but not a shape real, hand-run SWAR tournaments ever produce, and real SWAR was confirmed (against an actual production export) to desync on it: "???" opponent names and phantom results on later rounds for exactly that player, once their block ran out. Every round from the player's start_round onward now gets an explicit zero-point "absent" record instead of being skipped - a round before start_round (not registered yet) is still correctly omitted. No change to OpenPairings' own scoring/standings; only what gets written into the exported .swar file.

[0.14.18] - 2026-08-13

Added

  • Feature Public pairings page now shows round history, not just the latest round - /p/:slug/pairings gained the same round-picker the authenticated Pairings page has (?round=N, bookmarkable/shareable), so anyone with the public link can look back at earlier rounds instead of only ever seeing whatever round is currently being paired. The public standings and pairings pages now cross-link to each other.

[0.14.17] - 2026-08-12

Fixed

  • Fix "Pair with another player who isn't playing…" confirm dialog got silently closed by an unrelated remote broadcast - an arbiter mid-way through that gesture, with someone ELSE entering a totally unrelated result elsewhere in the round, got bounced out of it as if they'd hit Escape themselves. A remote broadcast now leaves any in-progress menu/swap/confirm gesture alone (the round data underneath it still refreshes fully either way); only the arbiter's own completed action clears it. Hardened Tournaments.pair_from_pool/4 to re-check the target board number is still free right before writing, since the confirm dialog can now sit open across a remote update.
  • Fix The Players page's own "Player data was just updated by another arbiter" popup, removed - same call as the Pairings page's identical notice (0.14.12): it kept surprising people mid-click regardless of where it sat. Player data still refreshes live underneath; only the popup announcing it is gone.
  • Fix Assigning categories skipped unrated players for an elo_below threshold bracket (e.g. "U1800") - an unrated player's rating is effectively 0, which genuinely is under any positive ceiling; the rating > 0 guard had it backwards. elo_above keeps requiring a real rating, unchanged - an unrated player has no proven rating to be above anything.

Added

  • Feature Tiebreak values on the Players Card (right-click a player) - a compact strip of the tournament's own configured tiebreaks and this player's value for each, same as the Standings page shows per column. Also added to the card's print version.

[0.14.16] - 2026-08-12

Added

  • Feature Print button on the Players Card popup (right-click a player on the Players page) - opens a new tab with that one player's round-by-round history (opponent, colour, result, running score) as a print-ready document, same data and table shape as the popup itself. New route: GET /t/:id/print/card/:player_id.

[0.14.15] - 2026-08-12

Added

  • Feature Public standings link (/p/:slug/standings) now also shows the Category column whenever the tournament has 1+ categories - it was added to the authenticated Standings page and print in 0.14.14, but missed the public page, which has no column-preference system at all and just needed the column added outright. Same "-" for an unassigned player, on both the FIDE-tiebreak table and the Keizer ladder.

[0.14.14] - 2026-08-12

Added

  • Feature Standings page now shows a Category column whenever the tournament has 1+ categories defined - printed standings already did this (Category column plus one sub-table per category); the live Standings page never had a Category column at all. Shows unconditionally once the tournament has any categories - not gated by the Players page's column-visibility tickbox, matching print's own always-on behavior. Covers both the FIDE-tiebreak table and the Keizer ladder.

[0.14.13] - 2026-08-12

Fixed

  • Fix Changing an already-set result didn't show up for another arbiter viewing the same round, if they had that board's result dropdown focused - confirmed by hand: the write and the live broadcast both worked correctly (the database and every other part of the page updated), but that one <select>'s displayed value stayed stuck on the old result, even after clicking away from it. Root cause is a genuine Phoenix LiveView behavior (protecting a focused form control from a server-pushed value change, so it doesn't clobber in-progress typing) that doesn't fit a discrete-choice dropdown like this one. The real result is now also mirrored into a plain data-result attribute, which always patches through regardless of focus; the result select's hook resyncs its value from that on every update, closing the gap without disturbing whatever the arbiter is doing.

[0.14.12] - 2026-08-12

Removed

  • Removed The "Round N was just updated by another arbiter" notice, entirely
    • repositioning it as a fixed toast (0.14.11) didn't fix the underlying complaint: it still popped up and grabbed attention mid-click regardless of where on screen it sat. The round data itself keeps refreshing live via the existing broadcast the moment anyone enters a result elsewhere - that always worked and still does; only the popup announcing it is gone.

[0.14.11] - 2026-08-12

Fixed

  • Fix "Round was just updated by another arbiter" notice pushed the whole board list down for everyone else viewing the round - it sat inline above the pairings table with a plain margin, so it appearing the instant someone else entered a result visibly shifted every board underneath it, right as another arbiter might be reading or clicking one. It's now a fixed-position toast that overlays instead of pushing layout - same idea the existing flash-message toast already uses, just anchored top-center. Also gave each board row a stable DOM id so a remote update always patches rows in place rather than risking a full node swap that could drop focus from an open result dropdown.

[0.14.10] - 2026-08-12

Added

  • Feature Each player's score coming into the round now shows next to their name on the pairing list - the authenticated Pairings page, the public pairings page, the projector/live view, and the printed pairing list all show it now, e.g. "Alice (2400, 2.5)" (rating and score together where a rating exists, just the score otherwise). This is the score BEFORE the round shown, not after - the same figure a real printed pairing sheet has always shown, computed fresh per round so it's correct even when browsing a past round.

[0.14.9] - 2026-08-12

Fixed

  • Fix The public pairings page (/p/:slug/pairings) and the projector/live view (/t/:id/live) never used the same board display logic as the authenticated Pairings page - both just sorted by raw pairing.board with no relabeling at all, so the moment a tournament had a fixed-table ("special") board, a bye, or an absence, they'd silently disagree with the arbiter's own Pairings page and with print: a real board 10 showed "10" there, "1001" everywhere else, for the exact same game. Both now go through the same PairingDisplay module as the Pairings page and print, so every surface shows identical board numbers and row order.

[0.14.8] - 2026-08-12

Fixed

  • Fix Marking a player absent (or otherwise changing a bye) renumbered every other board in the round - a real regression from 0.14.6's "byes/absences sort below the special boards" change: pulling a bye or vacant seat out of the ordinary numbering sequence to move it visually to the bottom also shrank that sequence, shifting every later board's displayed number. Board numbers are now computed once, together, from real board order regardless of bye/vacant/normal status - stable no matter what an arbiter does to any other board mid-round - while the ROW ORDER (byes/vacant sorted below the special boards, unchanged from 0.14.6) stays exactly as intended.

[0.14.7] - 2026-08-12

Fixed

  • Fix FIDE lookup silently rewrote an already-filled name when the only difference was formatting (case, punctuation, word order - e.g. an arbiter's own "Tom van 't Hoff" against FIDE's "Van 't Hoff, Tom"), with no confirmation prompt, even though a genuinely different name correctly asked first. The "don't bother asking about a pure reformat" shortcut turned out to be the wrong default for identity data - real report: a player's hand-typed name got rewritten with no warning. Any real change to an already-filled Name/Sex/Birth year now always asks first; only a byte-for-byte-already-correct value (re-running the lookup for no reason) still applies without a prompt, since there's nothing to actually change.

Changed

  • Change Pairings table (and matching print documents) now order byes and vacant/absent seats below the special (fixed-table) boards - previously a bye or an absence-vacated seat just sat wherever its real board number happened to fall among the ordinary boards. Order is now: ordinary boards, then byes, then vacant seats, then special boards (unchanged). A fixed-table player's own bye still sorts and labels as a special board, not as a bye row - the fixed-table label always wins.

[0.14.5] - 2026-08-12

Changed

  • Change Players page Pr. cell menu: clearer wording, and works on a plain click too - a single player's Present/Absent toggle used to say "All Absent"/"All Present", easily misread as touching every player; it now just says "Absent"/"Present" for one row (the column header's genuinely-bulk menu keeps the "All" wording - right-click it to set everyone at once). Right-click wasn't discoverable to begin with, so a plain left-click on the cell opens the same menu now too.

Verified

  • Verified Fixed-board results and same-round byes don't cross-contaminate - investigated a report that a fixed-board win looked mis-credited (it wasn't: a different player's matching score came from their own bye that round, entirely independent). Added a regression test (PairingsEngine.StandingsTest) that enters a result through the exact write path the UI/CSV import use and asserts every player's score end to end, confirming standings are keyed by player id start to finish - PairingDisplay's board relabeling can't affect it.

[0.14.4] - 2026-08-12

Added

  • Feature Sex (M/F) column on the Standings page and printed standings - follows the same "sex" tickbox preference as the Players page on the live Standings view (shown by default until you touch the Display panel); always shown on the printed standings document (main table, per-category tables, and the Keizer ladder alike). Shows FIDE's own letters, same as the recent Players table fix.

[0.14.3] - 2026-08-12

Changed

  • Change "Norms" moved into the "Advanced" top-bar dropdown, alongside Audit trail and Pairing rationale, instead of sitting as its own tab.
  • Change Cleaned up the "Advanced"/"Settings" dropdown chevron - it now sits inline with the label (proper vertical alignment instead of a stray floating triangle) and flips to point up while the menu is open.

[0.14.2] - 2026-08-12

Fixed

  • Fix Sex column showed the raw internal code ("M"/"W") instead of FIDE's own letters ("M"/"F") - both on the Players table and on printed player lists. Storage is unchanged (still "m"/"w" internally, matching Trf.trf_sex/1's export convention); only the display was wrong.

[0.14.1] - 2026-08-12

Changed

  • Change FIDE lookup now asks before overwriting a Name, Sex, or Birth year that's already on file and disagrees with FIDE's own record - previously only a fuzzy name-search match staged its name behind a confirmation; an exact FIDE-ID match applied everything, including identity fields, immediately. Filling in a currently-blank value still applies right away either way (nothing to conflict with), and a name that's the same identity just reformatted (case, punctuation, word order - e.g. "tijl de moyer" vs FIDE's own "De Moyer, Tijl") still applies immediately too, not treated as a real change. Operational data (title, rating, federation) keeps applying directly on every refresh, since that's the expected, routine outcome of the button - FIDE publishes a new list monthly and this is how ratings actually get updated. Multiple conflicting fields on the same lookup are shown together in one prompt.

[0.14.0] - 2026-08-12

Changed

  • Change Round-robin tournaments now pair their whole Berger schedule in one action instead of one round at a time. A round-robin round's pairings never depend on prior results - the whole schedule is fixed the moment the player list is frozen - so there was never a reason to require a click per round the way Swiss does. "Pair round 1" is now "Pair the whole tournament," confirm-gated (it locks in who's playing; anyone added afterward isn't in the schedule, and it can't be changed once it exists). This also closes "I don't see all rounds in advance" - the whole schedule exists and is browsable right after that one click.
  • Change Round-robin's declared round count now always matches what the Berger schedule for the actual roster needs. It used to be a free-typed number on the tournament-creation form (defaulting to a generic "9", the same default Swiss uses) with nothing tying it to the real math - "I get too many rounds" was a direct symptom of that mismatch, showing round tabs the schedule would never actually reach. rounds_count is now corrected to the real total (a pure function of player count plus cycles/match-format) the moment the round-robin pairing pass first freezes the player list, not a value the arbiter needs to compute by hand.

[0.13.1] - 2026-08-12

Fixed

  • Fix The KBSB lookup button could find a player's FIDE ID but never used it - if the player's FIDE ID was blank, "KBSB lookup" would fill it in from KBSB's own cross-reference, but the arbiter then had to click "FIDE lookup" a second time by hand to actually pull that player's real FIDE data (title, rating, federation, birth year). Now the FIDE data comes in automatically the moment KBSB supplies the ID that unlocks it. If the FIDE ID KBSB found isn't in the local FIDE copy (e.g. not synced recently), KBSB's own fields are still applied rather than discarded.

[0.13.0] - 2026-08-12

A batch of smaller fixes and features from one round of feedback.

Added

  • Feature "Leave" a shared tournament. A collaborator could never remove themselves from a tournament someone else shared with them - only the owner could remove a collaborator. Leaving is now self-service, right next to the (still owner-only) Delete button on the tournament list.
  • Feature "Copy" (duplicate) a tournament, named "Copy of <original>" - available to the owner and any collaborator, reusing the same export/ import round trip Settings > Export/backup already relies on. The copy is owned by whoever clicked it; no collaborators carry over.
  • Feature Fixed-table ("special board") pairings are now really renumbered, not just annotated. A player with a Fixed table set (Players page) used to just get a "(table N)" note next to their ordinary board number, leaving no visible gap. Now the ordinary boards close the gap a pulled- out fixed-table board leaves (board 10 -> fixed_board 1001 means whoever was board 11 becomes displayed board 10), and fixed-table pairings move to the end of the Pairings page and the printed board-order pairing list, sorted by their own table number. Two fixed-table players paired against each other show as one row. The player edit modal now warns (doesn't block - sharing a table is valid when paired together) when a Fixed table value is already used by someone else.
  • Feature Standings columns follow the Players page's Display panel. We/W-We, tiebreak columns, and XtPts/Total used to always show on Standings; now unticking a column on Players hides it here too (shared localStorage preference, defaults to "show everything" for anyone who's never touched the Display panel).
  • Feature A top-level Changelog page (/changelog), next to Tournaments and Tools - it used to be buried at Settings > Changelog, needing a specific tournament in context even though the content is entirely app-wide.

Changed

  • Change The top-left nav link reads "Home" instead of "Tournaments" once you're inside a tournament - same destination, clearer label for leaving the tournament context.
  • Change Tournament list: a single-day tournament shows just the one date instead of "date -> date"; finished tournaments get their own badge colour instead of looking identical to running ones.
  • Change FIDE rating-list downloads (~41 MB, network fetch) are now limited to SSO-signed-in accounts. Local self-registration is open to anyone, so without this, anyone could sign up and repeatedly trigger the full download.

Fixed

  • Fix FIDE lookup didn't fill in Sex (M/F) - added, normalized from FIDE's raw "M"/"F" to the app's own "m"/"w" convention.
  • Fix The topbar accent-picker/theme-picker (and the Advanced/Settings dropdowns) sometimes didn't open on the first click - an open popover's panel could cover the next trigger over. All four now share an exclusive <details> group, the browser-native fix.
  • Fix The pairing-rationale "Pre-round score brackets" chart ignored the theme system - every colour (band backgrounds, player dots, floater/ rematch connector lines, the fairness sparkline) was fixed light-theme hex, unreadable under every other theme.
  • Fix A tournament's uploaded logo only ever showed on place cards - every other print document (pairing list, standings, player list/ cards, crosstable, result cards) never rendered it at all.

[0.12.3] - 2026-08-12

Fixed

  • Fix The player edit modal's fields weren't live-synced to the server at all - typing a corrected name or FIDE ID only changed the browser's copy; the server-side form stayed exactly as it was when the modal opened until Save was clicked. Clicking "FIDE lookup" or "KBSB lookup" right after typing something new silently searched on the old value instead (or, if a FIDE ID was already on file from before, used that instead of a freshly-typed name - which is also why the name-correction confirmation could seem to not fire: it was really taking the exact-ID path on stale data, not skipping the check). The whole modal now syncs on every field change, matching what Save would actually submit.

[0.12.2] - 2026-08-12

Changed

  • Change FIDE lookup now corrects the player's name, not just the ID/rating/federation/birth year. By FIDE ID (an exact, unambiguous match) the name is corrected immediately, same as every other field. By name only (a fuzzy best guess), everything else still fills in right away, but the name goes behind a "FIDE has this player's name as '…' - correct it? Yes/No" prompt instead of silently overwriting whatever was typed by hand.

[0.12.1] - 2026-08-12

Fixed

  • Fix A player unrated in a tournament's own tempo (Rapid/Blitz) showed as a literal 0 Elo instead of falling back to their Standard rating. FIDE's list uses 0, not a blank field, for "no rating in this list" - the fallback logic used ||, which doesn't treat 0 as absent in Elixir, so it never fired. Affects the tempo-aware FIDE lookup added in 0.12.0; also fixed the raw Standard/Rapid/Blitz hint on the player dialog, which had the identical bug.

[0.12.0] - 2026-08-11

The version number sat at 0.11.1 for five weeks while the app kept shipping - this release is everything that landed in that gap, in one batch rather than the many small ones it should have been. Going forward this file gets an entry every time the version bumps.

Added

  • Feature SWAR .swar v7 export - a real, opinionated SWAR file OpenPairings can write from any tournament, not just import. Settings → Export / backup.
  • Feature Hand-editing a paired round - right-click any player on the Pairings page to swap them with someone else, mark them absent, award a bye, or fill an empty seat from the round's "not playing" pool. Two people sitting the round out can be paired straight into a new board.
  • Feature Threshold prize categories - a category can now be "below/above this Elo" or "below/above this age" instead of only a hand-picked name, with a one-click "Assign categories" button that fills in every player at once.
  • Feature Public self-registration - players can register themselves for a tournament without an arbiter account (off by default, rate-limited).
  • Feature Mobile "enrol a phone" - no-account result entry from a phone at the board, for arbiters or helpers without a login.
  • Feature Automatic FIDE title-norm judgment (B.01) on the Norms page.
  • Feature TRF06 import support, and VCL.13's asymmetric ½-0 / 0-½ result code across the whole pipeline.
  • Feature Standalone single-file binaries - no separate Elixir/Erlang install needed to run OpenPairings.
  • Feature Alphabetical pairing list and printable score sheets.
  • Feature Settings → About page: which pairing engine a tournament is using, and a credits line.
  • Feature 02cloud SSO (Keycloak) as an optional login method; configurable FIDE rating-list source URL.
  • Feature Light/dark theming and an accent-colour picker - now with 4 more accent colours (Indigo, Cyan, Orange, Fuchsia, 11 total) and 5 more full themes (Solarized Dark, Nord, Dracula, Catppuccin Mocha, Gruvbox Dark) alongside System/Light/Dark, 8 total. The theme switch is now a popover instead of an inline button row, to fit them all.
  • Feature Tempo-aware FIDE ratings - a player's FIDE lookup/refresh now pulls the Standard, Rapid, or Blitz rating matching the tournament's own cadence (Settings → Options → Type), falling back to Standard when the player has no rating in that specific list yet. The player registration dialog shows all three alongside "Elo used" (whichever one the pairing engine actually reads). Changing a tournament's Type after players are already registered doesn't retroactively re-fetch anyone's rating - a save note points arbiters to the Players page refresh instead.

Changed

  • Change The Players page's right-click "Absent" now has a real bulk mode (the Pr. column header) that touches every player at once without disturbing anyone's individually-set absent rounds.
  • Change Printed player lists follow exactly the columns ticked in the Display panel, instead of a fixed five.
  • Change FIDE report generation (IT3/FA1/IA1/IT4): arbiters are no longer capped at two, the organizer is a searchable FIDE person instead of free text, and arbiter/organizer e-mail became mandatory for a download.
  • Change JaVaFo's pairing input for round 2 onward is built in current- standings order rather than fixed pairing_number order - this is what a real SWAR-vs-OpenPairings pairing mismatch on the same data turned out to be.
  • Change The bracket-map pairing-rationale view: unpin no longer stray-scrolls the hover panel, plus a head-to-head duo view.
  • Change The "updated by another arbiter" toast (already on Pairings) now also shows on the Players page.
  • Change Mobile result entry: forfeit and asymmetric-disciplinary result codes (1-0 FF, 0-0 FF, ½-0, etc.) are now reachable from a phone, behind a "More…" toggle per board rather than crowding the three main buttons - chosen over a long-press gesture, which has no visible affordance and behaves inconsistently across mobile browsers.

Fixed

  • Fix Results entered from a phone weren't being written to the audit trail at all - every other way of entering a result was, mobile was simply never wired up. Now logs the same pairing.result_entered/ _changed/_cleared actions the desktop flow does, attributed to "System" (no user account exists for an enrolled phone) with the enrollment's own label/id recorded so an arbiter can still tell which phone made the change.

  • Fix Round 2+ pairing input had no tie-break for players equal on both score and rating - JaVaFo's Dutch-system engine falls back to input order when a bracket has more than one structurally-equal pairing, and that fallback order was effectively arbitrary (unordered map enumeration), not a real rule. Found via the same real SWAR-vs- OpenPairings comparison above; fixed by adding pairing_number (FIDE Art. 1.14's starting-rank fallback) as the third sort key.

  • Fix SW321_PreBye presence points on pairing-allocated byes were not modeled at all - a real scoring gap for clubs running SWAR's 3-2-1 point scale.

  • Fix FIDE C.07's Cut-1 Exception (Art. 16.5.1) was missing, and a trailing pairing-allocated bye was scored as a draw instead of a bye.

  • Fix Article 16.4 unplayed-round tiebreak scoring (Buchholz / BHC1 / Sonneborn-Berger) was wrong for a specific unplayed-round shape.

  • Fix SWAR AbsValue mapping was backwards - a checked "pay ½ point for absence" box imported as 0 points, not 0.5.

  • Fix SWAR handicap-table boards were misplacing pairings in board order.

  • Fix The registration form's FIDE-id dropdown could silently fail to appear at all.

Security

  • Security Content-Security-Policy with a per-response nonce.
  • Security Rate-limiting keyed on the real client IP; throttled magic-link sends and registration-form submissions.
  • Security Mobile enrollment codes moved to a CSPRNG, 8 digits over one global space instead of per-tournament.
  • Security Closed an /invites link enumeration issue; tournament owners can now disable or rotate a public link.
  • Security Bumped bandit for a HIGH-severity WebSocket DoS advisory.

Versions before 0.11.1 are not itemized here - see the git history.