Add a dedicated Mt. Fuji Hill Climb analysis page (/stats/fujihill)
New /stats/fujihill page: a dedicated Mt. Fuji Hill Climb analysis with 10 charts (ring-acquisition share/count over time, finishers vs. unique athletes, gender ratio, top times, finish-time distribution, ring rate by gender, ring rate by age group, age composition over time, and repeater/growth analysis). Aggregates are precomputed and cached in Redis
Define ring tiers by finish time (Platinum <60min / Gold <65 / Silver <75 / Bronze <90); exclude implausibly fast times (faster than the ~46min course record) as data noise so the fastest-time and ring stats are not skewed
Unify the Dockerfile Node base image to 26 across all stages (package.json engines and .node-version set to 26 too)
v3.7.0
2026-06-10
Event-page trend charts, dashboard/archive redesign, accurate age-group labels
Integrate EventTrendCharts on the event hub page: four per-distance yearly trend charts (participation, finish rate, median finish time, and more) with a distance selector. Add the getEventYearStats query (with unit tests) and eventTrends translation keys
Dashboard redesign: skeleton loading for race history, IRONMAN races surfaced via ContactId, fixed the infinite race-history fetch loop, and unified backgrounds/gradients to the design system
Archive redesign: cinematic hero band, headline stats, a flat results table, and consistent gutters clear of the fixed header across the whole tree; the header now auto-hides on scroll
Store and display the raw age_category label from the upstream TSV; show no age-group label when an athlete has no age data (e.g. fujihill 2026 preliminary results) and treat age<=0 as no-data in resolveAgeGroupBounds
Fix an import bug where athletes with a null birth_year hit an ID mismatch and returned 0 results; bump SCHEMA_VERSION to 14 to force a full re-import
Name unification: absorb birth-year-less kanji rows into the unique same-name canonical
Upgrade the local dev Postgres 16 → 18 and fix the valkey data volume; add libc6-compat to the runner stage so bun runs on Alpine
v3.6.0
2026-05-20
Public /stats page, major name-unification overhaul (#100), 308 canonical redirects
Add public /stats page (ja/en): hero KPIs (athletes / results / editions / events), participation by year, finishers by distance, Top 20 events, Top 20 prefectures, finish-rate by distance, median finish time by distance × year, and Top 20 most-active athletes. Linked from the footer.
scripts/generate-stats.ts now runs at the end of entrypoint sync and writes the aggregate payload to Redis stats:v2 so the request path never re-aggregates 4M results
Major name-unification overhaul (#100): switched the romaji-bridge Policy-B uniqueness check from id-level to canonical-level, added an order-insensitive sorted-token name key so `Akio Yamauchi` and `Yamauchi Akio` collide, added a kanji-only weak merge (same name + BY±1 + gender + location), and added a cross-cluster ContactId merge (BY±5 when both sides hold ContactIds). The 山内 昭夫 case collapsed from 15 rows to 1 canonical, surfacing all 28 IRONMAN races + 6 domestic races on one profile.
Compress canonical chains a second time after the merge step so `A → B → C` references collapse to 1-hop, otherwise family lookups miss races on the canonical page
Redirect /athletes/[name]/[birthYear] to its canonical URL with **HTTP 308** (the previous meta-refresh fallback was invisible to crawlers). The proxy now calls an internal API to compute the canonical decision; the page-level redirect stays as defense-in-depth.
Filter the athletes sitemap to canonical_athlete_id IS NULL so absorbed rows no longer emit duplicate URLs that Search Console flags as `Duplicate, Google chose different canonical`
Add POST /api/internal/invalidate-cache that the sync script calls to drop normalized-race-data:v1 and per-edition triathlon-data:v2:* keys — fixes the bug where newly-imported 2026 races looked like 404s because the standalone Docker tsx runtime couldn't load ioredis to do the invalidation itself
Add POST /api/internal/canonical-athlete (DB-backed lookup invoked by the proxy) and POST /api/internal/stats/write (fallback writer used when the stats script can't reach Redis directly)
Replace the unreadable gender × age-group heatmap on /stats with three sharper widgets: finish-rate by distance, median finish time by distance × year, and Top 20 most-active athletes (gender-colored, birth-year disambiguated)
v3.5.0
2026-05-13
Triathlon-Results SEO Push, OG-Image Fixes, Correct Race-History Ordering
Full rewrite of titles, descriptions, H1s, and lead copy on /archive, /archive/[event_id], and /archive/[event_id]/[year] targeting “トライアスロン リザルト” and “<event-name> リザルト”
Add CollectionPage + BreadcrumbList JSON-LD on /archive; add CollectionPage JSON-LD on event hubs with a yearly-editions ItemList
Emit hreflang (ja / en / x-default) and keywords meta on every archive route
Add an intro section (H2 + body copy) to /archive and a keyword-rich internal link in the footer
Fix /opengraph-image 404: the root layout referenced /opengraph-image but app/opengraph-image.tsx was missing — added a static OG card
Fix three OG-image routes (/athletes/[name], /athletes/[name]/[birthYear], /archive/[event_id]) that destructured params synchronously and so served the same fallback PNG for every URL — Next.js 15 passes params as a Promise
Normalize Promise<{ ... }> typing across all OG-image routes and add a regression test that renders each dynamic OG route with two distinct param sets and asserts the rendered React trees differ
Fix athlete-profile race-history sort: same-year races no longer land in arbitrary DB order — Vietnam 2026 (May) now sorts ahead of Colombo 2026 (Feb). Select editions.date and sort by full YYYY-MM-DD
Fix JTU-style T2 handling: preserve upstream transition_seconds on the last segment and invalidate the Redis race-data cache per changed edition (#98)
Detect BEFORE/AFTER transition convention per category and normalize to AFTER on import, so historical T1/T2 splits stop drifting (#99)
Add /llms-full.txt (machine-readable site index) and /humans.txt
Serve Markdown alternates at /index.md and /about.md, advertised via Link: rel="alternate"
Emit Link: rel="describedby" and Content-Signals: search=yes, ai-input=yes, ai-train=yes headers on public routes
Embed JSON-LD (SportsEvent + BreadcrumbList) on /archive/{event_id} and /archive/{event_id}/{year}
Prevent romaji-bridge false merges by requiring strict given-name match in addition to surname (#92)
Always run the merge pass after sync so name-merging does not lag (#92 follow-up)
Fix transaction-abort bug in Phase 5e canonical birth_year correction — NOT EXISTS pre-check replaces a try/catch that let UNIQUE constraint violations poison the transaction and brick the merge pass
Speed up the canonical birth_year query used during import
Ship an age-group bridge dry-run script for races that only record an age bracket — write-free, TSV output
Add manual kanji→romaji overrides for 石丸 / 伸二 and similar names
Move hardcoded Japanese strings on /calculator to next-intl (event type names, discipline headings, distance labels)
Consolidate distance-category colors and badges in lib/types/race.ts (consistent colors across chart and archive)
Replace the Age column with an Age Group (M35-39 style) column and derive age-group buckets from data (#91)
Delete dead code and unused exports flagged by knip/depcheck; drop the what-if simulator and estimated-wattage-box components
v3.3.0
2026-04-20
IRONMAN × Domestic Race Auto-Merge, Performance & Date i18n