Added digital badges (first release): a new Badges page on the dashboard shows your service-usage badges (early adopter, complete profile, first AI plan, feedback contributor). Race-performance badges will follow once athlete claiming is available
Internal: updated dependencies and removed dead code
v3.10.0
2026-06-19
Faster race-result pages, fix 404 on age-less athlete profiles, and admin tooling
Performance: warm the most popular race-result pages right after each deploy, so the first visitor no longer pays for a heavy cold render (previously the cache was wiped on every deploy)
Bug fix: opening the profile of an athlete from an age-less race (unknown birth year) returned 404 — now resolved, including from search results
Re-implemented athlete claim (linking a user to their athlete) safely with manual admin approval: DB invariants, audit trail, and revocation so no one can merge someone else's data (a claim-request UI will follow)
Added a read-only admin dashboard (/admin): registered users, premium status, goals, and entered data at a glance, restricted to Auth0 admins
v3.9.0
2026-06-18
Large security / type-safety / performance hardening (senior-review pass) and mobile breadcrumbs
Security: fix an unauthenticated cache-poisoning hole where a crafted request could replace another user's AI analysis — the server now rebuilds the analysis input from authoritative data instead of trusting the request body
Security: make the internal APIs (/api/internal/**) fail-closed (disabled when the shared secret is unset), and add rate limiting to the browser-reachable read endpoints
Security: protect the iCal (calendar) feed with a revocable token so a guessed or leaked URL can no longer expose someone else's training plan
API: validate goal / my-data update requests with zod to block unexpected (mass-assignment) fields
Quality: enable TypeScript's noUncheckedIndexedAccess across the whole codebase to catch undefined array/object access at the type level, and raise the test-coverage floor
Performance: render most of the landing page as Server Components to cut initial JS and hydration
Performance: compute athlete career deviations from DB aggregates instead of a full-roster JS pass; trim the data bundled to the client on the race-result and start-list pages; improve athletes-list paging and counting
Data layer: consolidate the DB schema source-of-truth and connection budget to reduce connection-exhaustion risk at startup
i18n: localize the start-list pages (Japanese / English)
UI: show the breadcrumb on the race-result page on phones too (horizontally scrollable), so you can navigate up and see where you are
UI: fix the English home page (/en) rendering with an extra container and padding, so its layout matches the Japanese home page
v3.8.1
2026-06-17
Bug fixes: plot every race on the career deviation chart, dark-mode login button readability
Add an Overall line to the athlete career-deviation chart so every finished race is plotted (previously races with few age-group peers, and very senior athletes, silently dropped off)
Fix the header login button text being unreadable (low contrast) in dark mode
v3.8.0
2026-06-17
Locale-in-URL i18n (JA/EN) with CDN caching, plus design-system unification
Internationalize public pages with next-intl (locale decided by URL): Japanese stays unprefixed, English is served under /en (issue #134, Phase 2)
Make public pages CDN-cacheable: drop the NEXT_LOCALE cookie so locale comes only from the URL, and emit CDN-Cache-Control from the proxy (public pages = public + stale-while-revalidate, /dashboard and /auth = no-store). Scope the auth middleware to authenticated paths only
SEO: add locale-aware canonical/hreflang (ja / en / x-default) to every public page and emit hreflang alternates in the sitemap (Phase 3)
Internationalize the AI coach page
Add an on-demand revalidation API (/api/internal/revalidate) and call it on container start to refresh the public list pages, fixing the build-time empty shell that would otherwise be served after a deploy
Design-system unification: replace raw neutral colors with semantic tokens across public, landing, and dashboard pages so dark mode renders correctly
Fix a dark-mode bug where the default Button's text was unreadable due to low contrast (a glued class name in the Button variant)
Fix legacy URL redirects (issue #100 canonical / old archive and athlete URLs) so they also work under /en
v3.7.1
2026-06-10
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