Spec — Event site templates¶
Status: implemented. Built and in use.
Create two reusable event-site templates in the sage-match-control.github.io
repo, so a new tournament site is a copy-and-fill job rather than a
copy-an-old-event-and-hunt-for-hardcoded-strings job.
| Template | Based on | For |
|---|---|---|
standard-tournament-template |
events/archives/bkl-cup-2026/ |
Multi-day, multi-venue, open-entry tournaments |
dual-meet-template |
bkl structure + events/archives/ppa-x-club-2600-dual-meet.html event model |
Two clubs facing off, usually one day, one venue |
The deliverable is the templates, not an event. No real event is created by this task. §9 is the runbook a later session follows to stand one up.
Everything below was derived by reading the three source pages
(bkl-cup-2026/index.html 2752 lines, match-control.html 3167,
ppa-x-club-2600-dual-meet.html 1590). Line references are to those archived
copies; script-relative line numbers are offsets within the <script> block.
Settled up front (§3): data comes from the Cloud Run snapshot pipeline, not
a direct browser fetch of Google Sheets; snapshots go to one generic data repo
with a folder per event; bracket-generator.html ships in both templates
(superseded — see D3 below).
1. Placement¶
_templates/
CLAUDE.md <- how to instantiate (§9)
standard-tournament-template/
index.html
match-control.html
dual-meet-template/
index.html
match-control.html
_templates/ at the repo root, not under events/.
Templates must not be served as pages. This repo has no .nojekyll, so GitHub
Pages runs Jekyll over it, and Jekyll excludes top-level directories beginning
with _ from the published site. That is the mechanism keeping _templates/
unpublished.
- Do not add a
.nojekyllto this repo without also finding another way to exclude_templates/. (The data repo needs.nojekyll; this one must not have one. Different repos — don't cross the wires.) - Templates contain only placeholders and example values, never secrets, so if
they do leak into the published site nothing is exposed — it is a tidiness
boundary, not a security one. State that in
_templates/CLAUDE.md.
2. Asset paths — fix the bug the originals have¶
The bkl and PPA pages were authored at events/<slug>/ and later moved into
events/archives/ as pure renames (commit 8e7d516, all R100). Their
relative asset paths were never updated, so in their current location they are
broken:
../favicons/…→events/archives/favicons/(does not exist)../../assets/logo.png→events/assets/logo.png(does not exist)
Templates must not inherit this. Use root-absolute paths in both templates:
<link rel="icon" href="/events/favicons/favicon-32x32.png">
<meta property="og:image" content="/assets/logo.png">
This works because sage-match-control.github.io is a user/org Pages site
served at the domain root. (If the site is ever moved to a project-pages repo
served under /<repo>/, root-absolute paths break and every template needs a
prefix — note this in _templates/CLAUDE.md.)
Payoff: templates preview correctly in place, instantiation is
location-independent, and moving a finished event into events/archives/ no
longer breaks its icons — the exact bug that already bit bkl, PPA, and boc.
Leave the archived originals alone; fixing them is a separate cleanup (§11).
3. Settled decisions¶
D1 — Data source: Cloud Run snapshot pipeline.
Both templates read a published JSON snapshot from GitHub Pages; Cloud Run
fetches the sheets and publishes it. PPA's direct browser-side gviz fetch is
not carried into either template. Mission Control, the partial-sync status
line, and the "last synced" timestamp all depend on this and are in scope for
both templates.
D2 — Data repo: one generic repo named event-data, a folder per event.
Not a repo per event. Snapshots live at
sage-match-control/event-data → <event-key>/data/<day-key>.json, served over
GitHub Pages at https://sage-match-control.github.io/event-data/…. Layout and
the SyncConfig restructure it implies are in §8.
Consequences: GITHUB_REPO stays a single env var, its value becoming
event-data; GitHubPublisher needs no change; day keys become globally unique
across events; a template's EVENT_KEY must equal its folder name in that repo.
Because the name is now fixed, GHPAGES_REPO is a hardcoded constant in both
templates rather than a per-event token — one less thing to fill in at
instantiation, and one less way for a new event to point at nothing.
D3 — bracket-generator.html ships in both templates.
It is fully event-agnostic (no config block, no sheet access — pairs are pasted
in by hand), so it is copied verbatim from bkl and needs no genericisation.
Superseded by the bracket generator spec. The per-template copy this decision describes was correct at the time — the duplication cost (§1) was real but small next to genericising a template contract from scratch. Once the tool had shipped and drifted (one copy fell behind the S.A.G.E. theme with no mechanism catching it), that trade flipped: the tool moved to one evergreen copy at
tools/bracket-generator.html, with an optional Event name field replacing the token-based branding it loses. Seesage-docs/docs/specs/implemented/bracket-generator-spec.md.
D4 — Organizer page stays unlinked. Follow bkl: match-control.html is not
linked from index.html; the URL is handed out directly.
4. The shared template contract¶
Both templates must expose the same configuration surface, so a session that has instantiated one can instantiate the other without relearning anything. The only intentional difference is the club dimension (§6).
4.1 Placeholder convention¶
Two kinds of value, handled differently on purpose:
- Identity strings that must never survive instantiation — event name, dates,
venue, sheet IDs, URLs, club names — are
{{DOUBLE_BRACE}}tokens. They are greppable, and "no{{remains" is a hard acceptance check. - Structural config that demonstrates the shape —
DIVISIONS,EVENTS,FACILITIES,DAYS— ships with working example values under a// EXAMPLE — replace with this event's owncomment, so the template renders and the expected shape is self-evident.
Never leave a real bkl or PPA value as an example: no bkl, BKL,
bklCup2026, Pickleball Central PH, Pampanga Paddle Aces, Club 2600, and
no real sheet IDs or Cloud Run URLs anywhere in _templates/.
4.2 Token list¶
| Token | Used in |
|---|---|
{{EVENT_KEY}} |
config; folder name under events/ and in the data repo; storage namespace |
{{EVENT_TITLE}} |
<title>, hero <h1>, footer |
{{EVENT_TAGLINE}} |
hero subtitle |
{{EVENT_HEADLINE}} |
bkl's .domination-line slot — optional, blank is fine |
{{EVENT_DATE_RANGE}} |
hero eyebrow, footer, meta description |
{{VENUE}} |
hero eyebrow, footer, FACILITIES |
{{QR_IMAGE}} / {{QR_URL}} |
QR panel image + printed short link |
{{CLOUD_RUN_BASE_URL}} |
match-control.html only |
{{CLUB_A_CODE}} … {{CLUB_B_NAME}} |
dual-meet template only |
4.3 Config block¶
Both templates keep bkl's convention: one commented CONFIGURATION region at
the top of the <script>, closed by an explicit END CONFIGURATION marker,
with nothing event-specific below it. An organizer must never need to read past
the marker.
// ---- Event identity -------------------------------------------------------
// Does triple duty: browser-storage namespace, this event's folder in the
// shared data repo, and its folder under events/ in this repo. Keep all three
// identical.
const EVENT_KEY = '{{EVENT_KEY}}';
// ---- Days -----------------------------------------------------------------
// One entry per tournament day. Length drives the UI: see §7.1. Day keys must
// be globally unique across all events in the data repo — prefix them with
// something event-specific.
const DAYS = [ /* EXAMPLE — replace */ ];
const LIVE_GO_LIVE_HOUR_PH = 7;
// ---- Venues / courts ------------------------------------------------------
// Length drives the UI: see §7.2.
const FACILITIES = [ /* EXAMPLE — replace */ ];
// ---- Categories -----------------------------------------------------------
const DIVISIONS = { /* code -> { name, full } */ };
const EVENTS = { /* code -> label */ };
// Optional overrides. Omit or leave null to fall back to the key order of
// DIVISIONS / EVENTS above, which is the normal case (§7.3).
const DIVISION_ORDER = null;
const EVENT_ORDER = null;
// ---- Clubs ----------------------------------------------------------------
// DUAL-MEET TEMPLATE ONLY. Absent entirely from the standard template.
const CLUBS = { /* code -> { name, full } */ };
// ---- Data source ----------------------------------------------------------
// Fixed for every event — one shared repo, one folder per event, keyed by
// EVENT_KEY above. Nothing here is filled in at instantiation; if a page ever
// needs a different repo, that is a platform change, not an event setting.
const GHPAGES_OWNER = 'sage-match-control';
const GHPAGES_REPO = 'event-data';
const GHPAGES_BRANCH = 'main';
const snapshotUrlFor = dayKey =>
`https://${GHPAGES_OWNER}.github.io/${GHPAGES_REPO}/${EVENT_KEY}/data/${dayKey}.json?t=${Date.now()}`;
// ---- Network --------------------------------------------------------------
const FETCH_TIMEOUT_MS = 8000;
const POLL_INTERVAL_MS = 10000;
// ---- Storage keys ---------------------------------------------------------
const STORAGE_DAY_KEY = `${EVENT_KEY}.dayIndex`;
const STORAGE_SEARCH_KEY = `${EVENT_KEY}.search`;
const ORG_SECRET_STORAGE_KEY = `${EVENT_KEY}.orgSecret`; // match-control.html only
STAGE_META / STAGE_ORDER / BADGE_CLASSES carry over from bkl unchanged in
both templates.
4.4 Theme¶
Collect every event-specific colour into the single :root block at the top of
<style>, under a /* ---------- THEME ---------- */ banner, and state in a
comment that this block is the only place to restyle an event. bkl's palette
(--court:#C6FF00, --court-dark:#060B16, …) is a reasonable neutral default
for both templates; do not carry PPA's palette into the dual-meet template
purely because that was a dual meet.
4.5 Required spreadsheet columns¶
Identical for both templates. Exact, case-sensitive:
- Matches tab:
matchNumber,teamCode1,team1Player1,team1Player2,teamCode2,team2Player1,team2Player2,Schedule,team1Score,team2Score,CourtAssignment,court.courtis the live court (distinct from the scheduledCourtAssignment) and is what drives the Live Matches board. The PPA sheet did not have it; without it the Live tab renders "No matches are currently on court." forever. - Standings tab:
teamCode,player1,player2,wins,loss,quotient,bracket.
Document this list in _templates/CLAUDE.md — it is the most common cause of a
new event silently rendering empty.
5. Template A — standard-tournament-template¶
Copy events/archives/bkl-cup-2026/ and genericise per §7. Everything in the
table below is inherited behaviour to keep; it is what makes this worth
templating rather than rebuilding.
| Feature | Where it lives now (index.html, script-relative) |
|---|---|
| CONFIGURATION block with explicit END marker | L1–L168 |
Snapshot fetch (fetchDaySnapshot, snapshotUrlFor) |
L286–L307 |
FETCH_TIMEOUT_MS + AbortController timeout |
L73 |
| Polling, paused while the tab is hidden | L78 |
Stale-response guard (dayIndexAtStart) in loadLiveData |
L318+ |
7 AM PH go-live gating (computeDayIsLive, isLive: 'auto') |
L42–L52 |
Partial-sync reporting (snapshot.failedFacilities) |
in loadLiveData |
| Live Matches court board | L1318–L1414 |
| Standings: category toggle bar (desktop) + category autocomplete (mobile) | L855–L924 |
| Standings: scroll capture/restore across re-render | L771–L808 |
Standings: matchup pairing (pairUpMatchups, renderMatchup) |
L655–L741 |
Twice-to-beat (1)/(2) handling (matchInstanceOf) |
L482–L490 |
parseCode memo cache + regex derived from DIVISIONS/EVENTS |
L147, L448–L463 |
| Autocomplete with clear (×) button | L1118–L1192 |
| QR panel | body, .qr-panel |
| Day picker gating the tabs until a day is chosen | L1035–L1117 |
match-control.html additionally keeps: the Mission Control tab (resync,
shared secret in sessionStorage, connection check, per-facility buttons,
CSV-fallback checkbox — body + script L1541–L1740), and its override of
computeDayIsLive() to always return true (script L54–L56) so the organizer
page is never gated. Keep both, and keep the comment explaining why.
Team code format: <DIVISION><EVENT>_<REST> (e.g. B18MD_1, HI40XD_SF_2,
B35XD_F_1_(2)).
6. Template B — dual-meet-template¶
Start from the same genericised bkl base as Template A — not from the PPA file — then port the dual-meet model onto it. The PPA page is the reference for what a dual meet is, not the code to build on: it lacks the Live board, Mission Control, polling, the stale-response guard, and the snapshot pipeline.
6.1 Port from PPA¶
| Feature | Where it lives now (ppa-x-club-2600-dual-meet.html) |
|---|---|
CLUBS registry, club-prefixed team codes |
L~960 |
parseCode with a club segment |
L~1010 |
Club win-totals summary bar (clubWins, VS separator, leader highlight) |
in renderStandings |
Desktop standings: category column with clubs stacked inside (renderCategoryColumn, renderClubSubsection) |
L~1160–1185 |
| Mobile standings: one column per club, categories inside | renderStandings else-branch |
divisionLabel() including the club's full name |
L~1030 |
| Hero copy pattern: "Club A × Club B · date · venue" eyebrow | body header |
The club-aware standings rendering replaces bkl's club-less category card.
6.2 Team code format — the fork point¶
- Standard:
<DIVISION><EVENT>_<REST> - Dual meet:
<CLUB>_<DIVISION><EVENT>_<REST>
Build the regex the way bkl does, with the club segment added:
const CODE_REGEX = new RegExp(
`^(${Object.keys(CLUBS).join('|')})_(${Object.keys(DIVISIONS).join('|')})(${Object.keys(EVENTS).join('|')})_(.+)$`
);
parseCode(code) returns { club, division, event, rest }, keeps bkl's
parseCodeCache memoisation, and on no match returns
{ club: null, division: null, event: null, rest: code }.
The added club is additive, so audit — don't rewrite — these call sites:
divisionLabel, divisionEventLabel, categorySortKey, standingsStageKey,
roundKeyword, pairKey, renderStandings, renderCategoryColumn,
renderClubSubsection.
pairKey() must include the club (PPA's already does) so two pairs with the
same names on opposite clubs never merge in the autocomplete index.
6.3 Take bkl's round parsing, not PPA's¶
Use bkl's roundKeyword() with anchored regexes ((^|_)SF(_|\d|$)), not
PPA's roundLabel() which uses rest.includes('SF'). bkl's version survives
twice-to-beat suffixes and won't false-positive on a bracket label.
7. Genericisation work¶
This is the substance of the task. Every item is something currently hardcoded that must become config-driven, so that "regardless of the categories or the number of days" actually holds.
7.1 Any number of days¶
DAYS.length drives the UI at runtime. The instantiator sets the array; they
never delete UI code.
length > 1— day picker visible, tabs gated until a day is chosen,STORAGE_DAY_KEYremembered across reloads, "Jump to today" (getTodayOrNextDayIndex(),match-control.html) available.length === 1— picker hidden, day0auto-selected on load so tabs and data appear immediately,STORAGE_DAY_KEYunused, "Jump to today" hidden.
Both templates carry both paths. The dual-meet template merely ships with a
one-entry example DAYS; it is not restricted to one day.
Keep computeDayIsLive() and isLive: 'auto' in both — the 7 AM PH gate is
what hides scores and the Live/Standings tabs before an event starts, and it is
just as wanted for a one-day meet.
Also drop bkl's STORAGE_DAY_KEY comment about the cancelled Aug 14 day and the
.v2 suffix — that is bkl history, meaningless in a template.
7.2 Any number of venues¶
- Replace bkl's hardcoded
facilityForCourt()(courts 1–4 → Main, 5–9 → Annex, 10–15 → Dreamcourts) with a lookup driven byFACILITIES. - Keep
groupLiveCourtsByFacility()and the'Other Courts'fallback bucket for unmapped courts. FACILITIES.length === 1— suppress the facility title row on the live board (a redundant venue name above a single table) and hide the per-facility resync row inmatch-control.html, which duplicates "Resync this day now".match-control.html— generate the per-facility buttons fromFACILITIESinstead of the hardcodedMain/Annex/Dreamcourtsmarkup.
7.3 Any set of categories¶
CODE_REGEXalready derives fromObject.keys(DIVISIONS)/EVENTSin bkl. Keep that, and keep the "don't hand-edit this regex" comment.DIVISION_ORDER/EVENT_ORDERbecome optional: when null, fall back to the key insertion order ofDIVISIONS/EVENTS. Today bkl requires a hand-maintained parallel list of full names that silently mis-sorts if it drifts fromDIVISIONS. Removing that duplication is the single biggest reduction in per-event setup work.- Delete bkl's
divisionMeta()special case (/^B(\d+)$/→ "Beginner N+"). It is a bkl-specific naming convention; unknown codes should fall back to{ name: code, full: code }only. - PPA's division chips (
chipDefs = ['IMD','IWD','IXD','HIMD','HIXD'], L~1290) are hardcoded. If the chips row is kept, derive it from the division/event combinations actually present in the loaded data. Otherwise drop the chips from both templates.
7.4 Strings and branding¶
- Every event-specific string in the HTML body —
<title>, meta description, allog:tags, hero eyebrow / title / tagline / headline, footer, QR caption and link text — becomes a{{TOKEN}}from §4.2. - Keep the S.A.G.E. crown/shield SVG mark and the "Powered by S.A.G.E. Match Control Experts" footer line in both templates: those are the operator's brand, not the event's.
- Storage keys are namespaced off
EVENT_KEY(§4.3), so a new event can never read or clobber another event's saved state in a shared browser.
8. Backend: multi-event SyncConfig (one-time)¶
Templates are only instantiable if the API can serve more than one event. This work happens once, not per event.
Superseded — see
sync-config-runtime-spec.md. This section was implemented as described, then replaced:src/registry/SyncConfig.mjsno longer exists. The event/day/facility registry now lives in theevent-datarepo atconfig/events.json, fetched and cached at runtime bySyncConfigStore.mjs, so adding an event is a commit to that repo rather than a Cloud Run redeploy. The data-repo layout (§8.1), the<event>/data/<day>.jsonpath, the globally-unique day-key rule, and the decision to keep the flat/sync/:dayroute all still hold exactly as written below. What changed is only where the registry lives — §8.2'sEVENTSconst and its module-loadDAY_INDEXare historical.
8.1 Data repo layout¶
Keeping data/ inside each event folder makes the new layout a pure prefix of
today's data/<day>.json, so repoPathFor grows one path segment and nothing
else about publishing changes.
Because there is still exactly one repo, GITHUB_REPO stays a single env var —
it just points at event-data. No per-day repo override, no second Cloud Run
service.
8.2 src/registry/SyncConfig.mjs¶
Restructure the flat day2..day6 DAYS map into an event-keyed registry, and
derive a flat day index at module load:
const EVENTS = {
'bkl-cup-2026': { days: { day2: { label: '…', facilities: [ /* unchanged */ ] }, /* … */ } },
// new events appended here at instantiation time
};
// dayKey -> { event, entry }, built once at load. Throw on a duplicate day key
// across events rather than letting one event publish into another's folder —
// this guard is the only thing keeping the flat /sync/:day route safe.
const DAY_INDEX = /* … */;
getDay(day)returns{ day, event, label, facilities }—eventis additive, so existing callers keep working.repoPathFor(event, day)returns`${event}/data/${day}.json`.knownDays()still returns every day key across all events, soUnknownSyncDayErrorkeeps listing something useful.SyncService.syncDay()already callsgetDay(day)— takeeventoff that result and pass it torepoPathFor. That is its only change.GitHubPublisherneeds none.
Keep the route as /sync/:day. Day keys are globally unique, so the event is
derivable from the key. This keeps the 15 Apps Script installs already on the
bkl spreadsheets working untouched — they build their URL as
${CLOUD_RUN_BASE_URL}/sync/${DAY_KEY}. A /sync/:event/:day route would be
marginally tidier but breaks every installed script for nothing the duplicate-key
guard doesn't already provide.
8.3 Sheet tab names / GIDs¶
SyncConfig assumes every facility sheet uses tabs named CSV (matches) and
STANDINGSCSV (standings), with GIDs 2136121736 / 1507029786 for the
?method=csv fallback. These are module constants today. Move them to per-day
config (defaulting to the current values) — templating means new events will
arrive with sheets that don't match. The PPA sheet already used a different
standings GID (327842042), so this is a real variation, not a hypothetical.
8.4 Apps Script¶
Rename scripts/bkl-sheets-sync.gs to apps-script/sheets-sync.gs now that it
serves more than one event, and generalise its header comment (it currently
documents installing across "15 facility spreadsheets — 5 days x 3 facilities").
Its CONFIG block already parameterises DAY_KEY / FACILITY_NAME /
CLOUD_RUN_BASE_URL; the watched tab GIDs (SCHEDULE 331216315, COURT CONTROL
359580789) are also per-spreadsheet and must be called out as such.
8.5 Known consequence for bkl¶
Once GITHUB_REPO points at event-data, a resync of a bkl day publishes to
event-data/bkl-cup-2026/data/…, which is not where the archived bkl
pages read from — their live data would stop updating. Harmless (the event ended
in Aug 2026 and nothing triggers a resync), but leave a comment in SyncConfig
saying so rather than leaving a trap for whoever revives that event. Migrating
the old published data is out of scope (§11).
9. _templates/CLAUDE.md — the instantiation runbook¶
Write this file as part of the task. It is what makes the templates usable
without re-reading this spec, and Claude Code picks it up automatically when
working under _templates/.
It must cover:
- Choosing a template — clubs facing each other →
dual-meet-template; otherwisestandard-tournament-template. Day count and category count do not affect the choice; both templates handle any number of each. - The steps, in order:
cp -r _templates/<template> events/<event-key>/- replace every
{{TOKEN}}(§4.2) — thengrep -r '{{' events/<event-key>/must come back empty - replace the
// EXAMPLE — replaceconfig values:DAYS,FACILITIES,DIVISIONS,EVENTS, andCLUBSfor a dual meet - set the
:roottheme block if the event wants its own palette - add the event + its days to
event-data'sconfig/events.json(see the §8 note — this wasSyncConfig.mjswhen this spec was written), using day keys prefixed with the event key - install
apps-script/sheets-sync.gson each facility spreadsheet - create the
<event-key>/data/folder inevent-data - The required spreadsheet columns (§4.5), stated as the first thing to verify — it is the most common cause of a new event rendering empty.
- The team code format for each template (§5, §6.2), with examples.
- Root-absolute asset paths (§2) and why not to "fix" them to relative.
- Archiving, later: move the folder into
events/archives/; because paths are root-absolute nothing needs re-prefixing. Note that this is the step the old events got wrong. - The
_templates/publication boundary (§1): no.nojekyllin this repo.
10. Acceptance checklist¶
Both templates
-
grep -rniE 'bkl|bklCup2026|Pickleball Central PH|Pampanga Paddle Aces|Club 2600|tinyurl' _templates/returns nothing. - No real sheet ID, Cloud Run URL, or QR short link anywhere in
_templates/. - Every
{{TOKEN}}in §4.2 appears in the template that needs it, and every{{in the templates is listed in §4.2. - Config block ends with an explicit
END CONFIGURATIONmarker and nothing event-specific appears below it. - Asset paths are root-absolute and resolve when the template is opened in place (§2).
- Setting
DAYSto 1 entry hides the picker and auto-loads; setting it to 3 shows the picker and gates the tabs — without editing anything but the array (§7.1). - Setting
FACILITIESto 1 entry suppresses facility headings and the per-facility resync row; 2+ restores both (§7.2). - Adding a division/event to
DIVISIONS/EVENTSflows through parsing, sorting, and display withDIVISION_ORDER/EVENT_ORDERleftnull(§7.3). - All three tabs render against a hand-made snapshot JSON.
- Before the configured date at 07:00 PH,
index.htmlhides scores and the Live/Standings tabs;match-control.htmlshows them regardless. - Mission Control: connection check hits
/ping, resync sendsX-Sync-Secret, secret lives insessionStorageonly and is never written into the file. - Polling stops while the tab is backgrounded and resumes on focus.
- Standings and live board keep scroll position across a poll re-render.
- Mobile (≤980px) and desktop standings layouts both render.
dual-meet-template only
- Standings show the club-vs-club win summary with the leader highlighted.
- A pair with the same names on both clubs is not merged in the autocomplete.
- Desktop shows category columns with clubs stacked inside; mobile shows one column per club.
Backend
-
snapshotUrlFor()produces…/event-data/<event-key>/data/<day>.json. -
SyncConfigthrows at module load if two events declare the same day key. - Regression:
getDay('day2')still resolves to thebkl-cup-2026event andrepoPathForyieldsbkl-cup-2026/data/day2.json— the restructure must not change what existing day keys mean. - Sheet tab names/GIDs are per-day config with the current values as defaults (§8.3).
Runbook
-
_templates/CLAUDE.mdexists and covers all seven points in §9. - Following it end-to-end against a scratch event produces a working page without opening this spec.
11. Out of scope¶
- Creating any real event. §9 is the runbook; running it is a later task.
- Migrating the published
bkl-cup-2026data intoevent-data(§8.5). - Fixing the broken relative asset paths in the existing archived pages (§2). Templates avoid the bug; retrofitting the old pages is separate.
- Generating QR PNGs or short links.
- Any change to the scoresheet half of
sage-tools-api. - Retiring
events/archives/ppa-x-club-2600-dual-meet.htmlor the bkl folder. Templates are copies; the originals stay exactly as they are.