Release Notes

Changelog

Every DynastyDesk release, newest first. Web dashboard, Discord bot, database, and OCR changes all in one place — the same notes posted to our Discord announcements channel.

Join Discord for Live Updates Setup Guide →

v0.9.10

Latest 2026-07-05

Bot/Web — error tracking + uptime monitoring

  • botAdded Sentry error tracking — unhandled rejections, uncaught exceptions, login failures, and every slash-command/autocomplete error are now reported with command + user context. No-ops unless SENTRY_DSN is set
  • botScheduler now pings a Better Stack heartbeat (BOT_HEARTBEAT_URL) once per 15-minute tick so a crashed or hung cron loop pages instead of failing silently; each check in the tick is isolated so one failing check no longer aborts the rest of the tick
  • webAdded Sentry error tracking for Server Components, Route Handlers, Server Actions, and the browser (via instrumentation.ts / instrumentation-client.ts). No-ops unless SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN are set
  • webNew /api/health endpoint that returns 200 only when Postgres is reachable, for an external uptime monitor

v0.9.9

2026-07-04

CI — auto-publish the website changelog page

  • ciauto-release.yml now checks out the ddwebsite repo, regenerates changelog.html from this file via a small Node script (scripts/build-changelog.js in that repo), and pushes it — so dynastydesk.gg/changelog.html stays in sync with every release automatically, with no manual step

v0.9.8

2026-07-04

CI — restore Discord release notifications

  • ciNo Discord release post since v0.9.1, silently: auto-release.yml creates releases with the default GITHUB_TOKEN, and GitHub doesn't let GITHUB_TOKEN-triggered events cascade into other workflows — so release-changelog.yml's release: published listener never fired for any release the bot created (v0.9.2–v0.9.7). auto-release.yml now posts to Discord itself right after creating the release, instead of depending on that cross-workflow trigger

v0.9.7

2026-07-04

Web/Bot — conference data fixes, realignment page polish

  • webbotCorrected default conference for 9 schools to match current real-world alignment (ESPN standings): Boise State, Colorado State, Fresno State, San Diego State, Texas State, and Utah State now default to Pac-12; Northern Illinois and UTEP now default to Mountain West; Louisiana Tech now defaults to Sun Belt. Existing leagues that haven't customized a school's conference pick up the corrected default automatically
  • webConference realignment page — school logo cards now sit on a solid white backdrop with a thicker team-color border instead of a low-opacity color tint, fixing poor contrast for teams whose color was hard to see against the tinted background
  • webConference realignment page — conference columns now size to their content (narrow when showing logos only, wider when "Show school names" is on) instead of a fixed grid, so more columns fit on screen at once when collapsed

v0.9.6

2026-07-04

Web — custom conference realignment

  • webNew commish-only "Conferences" admin page (/league/conferences) — drag schools between conference columns (logo cards with a toggle to show school names) and Save to realign your league's conferences for the current dynasty year. Conferences themselves stay fixed; only school membership changes, and only for your league
  • webA banner on the League page prompts the commish to review conference alignment during the preseason/offseason
  • webNew "Conference Record" card on the team stats page — season and career win-loss record against conference opponents, plus a "Conference History" list showing the record compiled in any prior conference. Career totals are scoped per conference, so moving conferences starts a fresh career tally in the new one without touching the old one
  • webRecord Book — conference record chasers now reflect a school's current (realigned) conference instead of its static default; records already set in a school's previous conference are untouched and stay attributed there
  • dbNew ConferenceAssignment table — sparse per-league, per-dynasty-year override of a school's conference, falling back to the static default when unset

v0.9.5

2026-07-03

Web — track a second league

  • webWeb users can now create and track a second (or third+) league from the dashboard. A "+ Add another league" link in the sidebar and a matching "Create a new league" card on the league picker (/select-league) route to the existing create-team wizard with a ?new=true flag that bypasses the "you already have a league" redirect. Switching between leagues already worked (cookie-based active league + league picker) — this just unlocks reaching the wizard a second time

v0.9.4

2026-07-03

Stats — manual entry team name fix, roster pre-population

  • webPOST /api/stats/manual — team name is now always derived server-side from the submitter's assigned roster team instead of trusting client input. A free-typed value like "App State" (vs. the canonical "Appalachian State") matched neither the school name nor abbreviation, silently excluding those stats from Record Watch even though they showed up fine in League Records
  • webManual stat entry — the "Your team" field is now a read-only display once your team is known (falls back to an editable field only if a member has no team assigned yet)
  • webManual stat entry — Passing/Rushing/Receiving/Defense tabs now pre-populate rows for players who already have a stat logged this season in that category, instead of starting from a single blank row every time
  • dbOne-off data correction: 3 existing GameStat rows with teamName: "App State" updated to the canonical "Appalachian State" so previously-submitted stats now show up in Record Watch

v0.9.3

2026-07-03

Schedule — solo-league fixes, active week indicator, cancel a game

  • webSchedule page — the "vs. User" / "vs. CPU" toggle is now hidden entirely for leagues with no other members (solo web leagues), so the Set Schedule form defaults straight to the CPU team picker instead of showing an unusable "vs. User" option with nobody to pick
  • webSchedule page — Set Schedule and Edit Opponent now update the row immediately on save instead of waiting on a full page refresh to reflect the change
  • webSchedule page — Preseason (Week 1) no longer appears as a row, since it never has game data. A persistent active-week label now shows next to the page title year-round (including every offseason phase — Portal, Signing Day, Training Results, etc.) so you always know where you are even on weeks with no schedule row
  • webSchedule page — added a "Delete" action on any scheduled game that hasn't had a result or stats logged yet, for fixing a mistaken entry or a week that should've been a bye. Blocked once a result, forced-win, self-reported completion, or any stats are attached to the game
  • webSchedule page — added a "?" help icon next to the Schedule heading with quick tips: leave bye weeks blank, use the FCS (generic) opponent for placeholder FCS games, use Delete to undo a mistake, and how CPU vs. UvU result confirmation differs
  • webDELETE /api/matchup/[id] — new endpoint backing the cancel action

v0.9.2

2026-07-03

Schedule — FCS placeholder opponent

  • webSchedule page — "vs. CPU" opponent picker (both the Set Schedule form and the Edit Opponent form) now includes an "FCS (generic)" placeholder option, for the games where CFB 25 subs in a generic FCS team instead of a real school. Renders with its own neutral badge instead of the unrecognized-team fallback styling

v0.9.1

2026-07-01

Stats — Dashboard "Add Stats" shortcut

  • webDashboard schedule — each game row now shows an "Add Stats" link under the week label when a matchup exists but no stats have been submitted yet. Clicking it opens manual entry with the correct week pre-selected, keeping score logging and stat entry in one place

v0.9.0

2026-06-27

Stats — Manual entry improvements + mid-dynasty setup

  • webManual stats entry — roster autocomplete: player names are now pre-populated from the active roster, filtered by position group per tab (QB for Passing, RB/FB/HB for Rushing, WR/TE/RB for Receiving, defensive positions for Defense). Free-text still allowed for players not on roster
  • webManual stats entry — optional box score section: away/home team, score, and rank fields. When submitted, auto-syncs a confirmed GameResult and marks the associated Matchup as complete, so the result feeds into the standings without a separate step
  • web/stats now redirects to /stats/players — the old landing page with an embedded upload widget is replaced by Import Stats and Enter Manually buttons directly on the Player Stats page header
  • webSidebar Stats section now lists "Import Stats" and "Enter Manually" as explicit sub-items alongside Team Stats and Player Stats
  • web/api/weeks — added weekNumber: { lte: 22 } guard to exclude offseason/placeholder weeks from the stats week picker
  • bot/dd setup — new starting_year option (integer, 2020–2060). Sets league.dynastyYear so leagues joining mid-dynasty don't have to manually advance through years. Defaults to the existing year on re-run (same safe-re-run behaviour as starting_week)
  • bot/dd setup confirmation message now always shows Year XXXX, Week Label so the commish can verify year and week at a glance
  • docsREADME — added "Starting Point Scenarios" section covering: fresh league (defaults), mid-season setup, mid-dynasty setup, and solo user joining a league already in progress

v0.8.0

2026-06-27

Awards — Trophy Case

  • dbNew Award model — stores season awards per league per dynasty year. Fields: category (individual / all_american / all_conference), awardType (named award e.g. heisman), tier (first / second / freshman), conference, playerName, playerPos, teamAbbr, teamName, submittedBy
  • web/awards — Trophy Case view page. Three sections: Individual Awards (14 named awards table), All-Americans (1st/2nd/Freshman tier tabs with position-ordered grid), All-Conference (conference chips + tier tabs). Year selector tabs for historical browsing; defaults to league.dynastyYear. Your team's winners are highlighted per section
  • web/awards/submit — user entry form. Pick category → pick award/tier → pick position → select from your roster dropdown (falls back to free-text if player isn't on roster). Year is auto-filled from the active dynasty year
  • web/awards/manage (commish only) — same form with full alphabetical team picker. Live table of current-season awards grouped by award type, with per-row delete
  • webGET /api/awards — returns awards for the league filtered by year (defaults to league.dynastyYear)
  • webPOST /api/awards — commish can set any team; users are server-locked to their own team. Year is always server-set from league.dynastyYear
  • webDELETE /api/awards/[id] — allowed for the commish or the original submitter
  • webRoster page — award badges appear inline next to player names: 🏆 for individual award winners, ⭐ (yellow) for All-Americans, ⭐ (purple) for All-Conference. Matched by player name; multiple badges shown if a player won multiple awards. Tooltips show the award name on hover
  • webSidebar — "Awards" nav item (Medal icon) with Trophy Case / Submit My Awards / Manage Awards sub-items. Gated behind awards feature flag (enabled)

v0.7.1

2026-06-26

Records — contributor badge + scope normalization

  • webContributor badge (🏆 amber trophy icon) on the Standings and League pages for users who have had a global record approved. Previously broke for non-Discord users because the badge lookup went through Member.discordUserId rather than the Account OAuth relation; fixed to query the Account table and match on userId so magic-link and Google OAuth users earn the badge correctly
  • webPOST /api/records/ocr — added "PAC12" to the CONFERENCE_ALIASES lookup table. Previously, OCR reads of "PAC12" (no dash) were not normalized and created a duplicate conference scope alongside the existing "PAC-12" records. Existing duplicate records in the database were migrated to the canonical "Pac-12" value

v0.7.0

2026-06-26

Non-Discord users — solo team management

  • dbMember.discordUserId is now nullable — web-created members are identified by userId (NextAuth User ID) rather than a Discord snowflake
  • dbMember.userId new field — FK to the NextAuth User table. New @@unique([leagueId, userId]) constraint for web members
  • dbLeague.guildId is now nullable — solo leagues have no Discord guild ID
  • dbLeague.createdVia new field — "discord" (default, all existing leagues) or "solo" (web-created)
  • webLogin page now offers three sign-in options: email magic link (password-free), Google OAuth, and Discord OAuth — all on one page
  • webNew /create-team page — two-step wizard to pick an FBS school and name a league, then creates a solo league (no Discord required). Accessible from the "no league" state after login
  • webAdvance Week button — commishes of solo leagues can advance the week directly from the dashboard. Full season rollover (player class progression, scouting board advances, SR graduation) runs identically to the Discord bot's advance path. Disabled with tooltip for Discord-managed leagues
  • webOnboarding checklist card — shown to new solo users on the dashboard: Team created, Add first player, Upload first game stat. Dismissible; auto-hides when all steps complete
  • webPOST /api/leagues/[leagueId]/advance — new API route for web-triggered advance. Solo leagues only; commish session required
  • webAll auth guards updated from discordId-required to user.id-required (NextAuth always populates this). Member lookups across all routes now use an OR clause: userId OR discordUserId

v0.6.6

2026-06-26

Records — My Record Watch page

  • web/my-records — new "Records" entry in the My Team sidebar section. Shows your team's active players chasing school, conference, and national records on a single page
  • webSeason tab displays a progress bar and on-pace season projection for your best player in each stat category, across all three scopes simultaneously. A ★ badge and "record broken!" label appear when a player has already surpassed the current mark
  • webGame and Career tabs show the current record holders for reference without live progress columns
  • webIf your school has no records imported yet, the School section is replaced by a CTA card linking to the Contribute Records page
  • webMembers with no team assigned see a gate message prompting their commissioner to set a team

v0.6.5

2026-06-26

Records — User-contributed school records

  • dbRecordSubmission model — stores user-submitted school record screenshots through an approval queue before entering the global record book. Fields: school (abbr), level, status (pending/approved/rejected), Discord submitter identity, rows (JSON), and optional adminNotes
  • web/records/submit — user-facing upload page for contributing school record screenshots. Scope is pre-locked to the user's assigned team; reuses the same OCR pipeline and editable review table as the admin import flow. Shows a warning if OCR detects a different school than expected. Users without a team assignment see a gate message
  • webSchoolRecordsBanner — shown on the Record Book page when a user navigates to their own school's scope. Displays per-level status (imported / pending / rejected) and a "Contribute records" CTA for any missing levels. Rejected submissions show the admin's note and allow re-submission
  • web/admin/records/submissions — admin queue page with Pending / Approved / Rejected tabs, sorted oldest-first. Each submission shows an inline editable review table (admin can correct OCR before approving), an "will overwrite existing" badge when global records already exist for that school+level, and Approve / Reject with note actions
  • webAdmin nav gains a "Submissions Queue" entry (Inbox icon) with a live pending-count badge
  • webSidebar nav gains "Contribute School Records" sub-item under Records (visible to all users when the records feature flag is enabled)
  • webGET /api/records/school-status — checks which levels have global records for a school and returns the requesting user's own pending/rejected submissions
  • webPOST /api/records/submissions — validates school ownership against member.teamId, enforces one pending submission per school+level globally (409 on duplicate), creates RecordSubmission
  • webPOST /api/admin/records/submissions/[id]/approve — approves with optional row edits; calls the shared importRecordRows() helper (same idempotent delete+reinsert path as the admin import endpoint)
  • webPOST /api/admin/records/submissions/[id]/reject — stores rejection note, sets status to rejected, allows the user to re-submit
  • webimportRecordRows() extracted to lib/records.ts — shared by the admin import route and the approval endpoint; eliminates duplication
  • webResultCard extracted to components/records/ResultCard.tsx — shared by the admin import page, admin submissions queue, and user submit page. Accepts readonlyScope prop (locks scope display) and scopeMismatch prop (shows warning when OCR detected a different school)

v0.6.4

2026-06-25

Fixed — Season and Game record OCR

  • ocrSeason and Game screenshots now parse all 9 values correctly. Previously, when the level tab text was unreadable, the parser defaulted to Career column layout and the year+value were concatenated into a single number (e.g. "2020623" instead of year=2020, value=623). Small values (1–2 digits) were also silently dropped when read from a narrow single-column crop.
  • ocrYear and stat value are now read as one combined region (same wide crop as the career stat column, extended right). Post-processing splits the result at character 4: the year always occupies the first four digits, the stat value follows. A single PSM 6 read on 5–9 digits of content is far more reliable than two separate narrow crops whose content varies from 1 to 5 digits.
  • ocrAutomatic Season vs Career detection: when the level tab text is unreadable, three probe reads on the year-column position determine the layout. If ≥2 of the first 3 rows contain a 4-digit value in the range 1950–2030, the screenshot is treated as Season/Game (career stat totals are always outside that range). The resolved level is returned in the API response so the review card pre-selects the correct tab.

v0.6.3

2026-06-24

Admin — Global Records import via screenshot

  • webAdmin Import Records page (/admin/records/import) — OCR-based bulk import of the global record baseline: select Season/Game/Career level, drop a CFB Records screenshot, review 9 parsed rows with editable fields and low-confidence warnings, then confirm to overwrite that scope+level globally
  • webAdmin sidebar gains "Import Records" nav link; Global Records page gains "Import screenshot →" shortcut button
  • webPOST /api/admin/records/import — app-admin-gated endpoint; idempotent delete+reinsert for the submitted scope+level, validated against known categories

v0.6.2

2026-06-24

Fixed

  • dbAdd cpuOpponentTeam column to Matchup — stores the CPU opponent team abbreviation at schedule-set time, so it's visible before a result is logged
  • [web] Schedule — CPU opponent team now saves when setting schedule — selecting a team from the CPU dropdown and clicking "Set Schedule" previously silently dropped the selection; it's now sent to the API and stored on the matchup
  • [web] Schedule — existing bot-created sim matchups now show "Set Opponent" / "Edit Opponent" — previously these rows had no interactive path to attach a CPU opponent team; they now expand an inline team picker that PATCHes the matchup. New PATCH /api/matchup/[id] endpoint added
  • [web] Schedule — Discord emoji stripped from member display names — platform usernames containing Discord custom emoji syntax (<:Name:id>) now render as plain text instead of leaking raw markup into opponent sub-labels

v0.6.1

2026-06-23

Records — global architecture, admin area, and redesigned navigation

  • dbRecord.leagueId is now nullable — null = global baseline shared across all leagues; league-specific overrides win via mergeRecords() helper
  • dbFeatureFlag model added — DB-backed feature flags replacing env-var allowlists; isFeatureEnabled("records") is now the single gate
  • web/admin/* area — gated by APP_ADMIN_DISCORD_IDS env var (distinct from commish role); sidebar shows "App Admin" section when active
  • webAdmin Global Records page (/admin/records) — CRUD for global baseline records (leagueId=null), inline row editing, scope navigation
  • webAdmin Feature Flags page (/admin/feature-flags) — toggle switches for all known flags; creates DB row on first toggle
  • webRecord book redesign — game-mirrored layout: Level tabs (Season / Game / Career) + 3-row scope navigation: National pill · Conference pills row · School dropdown row (defaults to user's own school)
  • webCommish "Manage Records" relabelled to "League Records"; import copy clarified as dynasty override only
  • dbSeed script updated: leagueId: null for all 108 global records; run with node packages/db/scripts/seed-records.js [--force]

v0.6.0

2026-06-22

Records module (feature-flagged — commish only via FEATURE_RECORDS_ALLOWLIST)

  • dbAdd Record model to schema: scope (national/conference/school), level (season/game/career), category (9 fixed types), holder name/school/year/value
  • webRecord Book page (/records) — tabbed by stat group (Passing/Rushing/Receiving/Defense), shows record holders with best-chaser progress bars and on-pace projections
  • webManage Records page (/records/manage) — commish CRUD: inline edit/delete, scope filters, add form with conditional conference/school fields
  • webImport Records page (/records/import) — OCR-based bulk import: select Season/Game/Career, drop a CFB Records screenshot, review 9 parsed rows, confirm to upsert
  • webDashboard widget — "Record Watch" card shows logged-in user's players chasing records with progress bars (≥30% threshold), linked to full record book
  • webManual game stat entry form (/stats/manual) — tabbed Passing/Rushing/Receiving/Defense form, dynamic player rows, submits directly to DB without OCR
  • webSidebar nav: Records section with Record Book / Import / Manage sub-links (Import + Manage are commish-only)
  • webFeature flag: FEATURE_RECORDS_ALLOWLIST env var (comma-separated Discord IDs); gates all Records pages, nav items, and dashboard widget
  • ocrNew recordBook template — detects CFB Records screen, extracts scope label + 9 fixed rows × 5 columns; exports parseRecordBook from OCR package
  • dbSeed script: packages/db/scripts/seed-records.js — pre-populates National, SEC, Big Ten, and Alabama school records from real NCAA history; run with DATABASE_URL=... node packages/db/scripts/seed-records.js [leagueId]

v0.5.6

2026-06-22

Fixed

  • [web] Matchups — upcoming weeks with queued games now visible — the week picker was filtered to current and complete weeks only, hiding any upcoming week that a commish had already queued matchups into via /matchup set. The query now also includes any week that has at least one matchup, so pre-set games appear immediately. Week buttons also now show the friendly schedule label (e.g. "Week 0") instead of the raw internal week number.
  • [web] HS Recruiting — pitch picker dropdown no longer clipped — dropdown was being cut off by the table's overflow container. Now uses position: fixed anchored via getBoundingClientRect so the full 5×4 grid is always visible regardless of scroll position or parent overflow.

v0.5.5

2026-06-22

Changed

  • [web] HS Recruiting — pitch picker as dropdown — pitch picker is now a compact select-style trigger that shows the chosen pitch icon + name. Clicking opens the 5×4 icon grid as a popover; selecting closes it immediately. No longer takes up a full row when a pitch is already set.

v0.5.4

2026-06-22

Changed

  • [web] HS Recruiting — pitch picker — "Correct Pitch" is now a 5×4 icon grid matching the game's 20 recruiting pitches (College Experience, Team Player, Campus Personality, Gamer, Standard Bearer, Student of the Game, Hometown Hero, Status Seeker, The Clutch, Primetime Player, Coach Connection, Aspirational Goals, To The House, Football Influencer, Clocked In, Star Search, Grassroots, Conference Legend, Sunday Player, Gym Rat). Replaces the old Yes/No boolean toggle. Schema column changed from boolean to string to store the pitch name.

v0.5.3

2026-06-22

Fixed

  • [ocr] Dev trait star icon bleed — shifted devTrait crop x from 0.893 → 0.903 to skip the star icon glyph that was still bleeding into the OCR region and producing garbled reads.

v0.5.2

2026-06-20

Fixed

  • [ocr] Player card preprocessing — inverted (white-on-dark) regions now invert *before* normalize() so histogram stretching works correctly for dark-text-on-light output. Scale increased 2x → 3x and contrast 0.3 → 0.6 for sharper binarization on the condensed game font. Region coordinates for OVR, name, position, archetype, class, and dev trait all widened/tightened for better coverage.

v0.5.1

2026-06-20

Added

  • [ocr] Players Leaving screen parser — new playersLeaving.js template scans the Week 23 "Players Leaving" table (name, position, year/redshirt, OVR, reason). Uses readText per-cell to preserve text fields. Classifier detects the screen by its header region; parseImage dispatches to parsePlayersLeaving.
  • [web] Portal OCR import/api/ocr/portal-out endpoint accepts one or more Players Leaving screenshots and returns parsed rows. The Transfer Portal "My Players" tab now has an "Import from screenshot" button that opens a two-step modal: upload → review table with inline editing, checkboxes to exclude rows, and graduation entries greyed out automatically. Confirmed rows are posted to /api/portal (direction="out") and appear in the list immediately.

v0.5.0

2026-06-21

Added

  • [web] Recruiting module — new "Recruiting" section under My Team sidebar with three sub-sections:
  • HS Board (/recruiting/hs) — track high school recruits with star rating, grade (Gem/Normal/Bust), position, archetype, dev trait, class, OVR rank, state rank, correct pitch, and status (Targeting → Offered → Committed → Signed → Decommitted). Status changes are immediate; all other fields are inline-editable via expandable rows. Signed recruits surface an "Add to Roster" button once Week 29 (post-NSD) is reached.
  • Transfer Portal (/recruiting/portal) — tabbed Incoming / My Players view. Incoming players track fromSchool and status (Available → Committed → Signed Elsewhere → Withdrawn). Outgoing (your own) players track portal outcome (In Portal → Returned / Signed Elsewhere / Unsigned).
  • Scouting Board (/recruiting/scouting) — long-lived big board for recruits who signed elsewhere but may enter the portal in future seasons. Class (FR→SO→JR→SR) advances automatically each season rollover; SR entries graduate (deactivated). "Move to Transfer Portal" promotes a player directly to the incoming portal list.
  • [db] Recruit, ScoutingEntry, PortalEntry models — three new tables pushed to Railway covering all recruiting data. Recruit and PortalEntry are scoped to a dynasty year; ScoutingEntry is year-agnostic and updates in place via class progression.
  • [bot] Scouting board class progression — season rollover in scheduler.js now advances currentClass on all active ScoutingEntry records (FR→SO→JR→SR) and deactivates graduated SR players.
  • [web] Global input-cell / select-cell CSS utilities — reusable form-field classes added to globals.css.

v0.4.6

2026-06-19

Added

  • [web] Neutral site location — "Neutral" option added alongside Home / Away / Unknown in the Set Schedule and Log Result forms. Both UvU players see a yellow "N" badge on their schedule row. neutralSite stored as a boolean on Matchup.
  • [db] Matchup.neutralSiteBoolean @default(false) column pushed to Railway.
  • [bot] /result log → thread post — after logging a result, the bot now posts a summary message to the matchup thread (if one exists). CPU games post immediately on confirmation; UvU games post "pending confirmation" with the opponent @-mentioned.
  • [web] Favicon — replaced the default Next.js favicon with the DynastyDesk logo via app/icon.png.

v0.4.5

2026-06-19

Added

  • [web] Set Schedule — empty-week rows now show a "+" placeholder with a "Set Schedule" inline form. Users can pick UvU vs. CPU, select the opponent from a dropdown, and mark themselves Home or Away. For past weeks, a "Log Result" checkbox lets you submit scores in the same step.
  • [web] Home/Away tracking — new homeUserId field on Matchup (DB migration applied). Schedule rows display an H or @ badge. The Log Result form also exposes a Home / Away / Unknown selector that updates the matchup record.
  • [db] Matchup.homeUserId — nullable Discord user ID indicating which player is playing at home.

Changed

  • [web] Team badge colors — all 138 teams now use real primary brand hex colors (e.g. Ohio State scarlet, LSU purple, Tennessee orange) instead of hash-derived hues. Sac State green badge; NDSU dark green badge.
  • [web] NDSU + Sacramento State enabled — both teams promoted from future_2026 to current status.
  • [web] Nav order — sidebar sections reordered to My Team → League → League Ops.
  • [web] Dashboard header — page title changed from "Week X" to "{year} Schedule" (e.g. "2026 Schedule"). The active week is already visually indicated by the NOW badge on the schedule itself.
  • [web] Preseason hidden during season — the Preseason row group is suppressed once any regular-season week has started.
  • [web] Log Result form — includes Home / Away / Unknown selector; updates homeUserId on the matchup when saving.

v0.4.4

2026-06-19

Changed

  • [web] Sidebar nav restructured — new top-level "League" section (Matchups, Standings) visible to all users. "My Team" section now contains only personal items (Dashboard, Roster, Stats). Commish-only items (League admin, Activity) moved to a "League Ops" section at the bottom.
  • [web] Dashboard layout — weekly matchups panel removed from the commish dashboard; schedule now occupies the full 2/3 column for both commish and regular users. Matchups are accessible via the League nav.
  • [web] Schedule redesign — each week is now a distinct card with a 48×48 team badge (deterministic per-team color derived from abbreviation), large monospace score, and a prominent colored W/L badge. School name shown as the primary opponent label; player name as subtitle. Ranked matchups display inline below the opponent.
  • [web] Typography--muted-foreground bumped from L=0.75 to L=0.82 for better readability of secondary text on dark backgrounds.

v0.4.3

2026-06-18

Fixed

  • [web] Commish dashboard missing schedule — commishes saw an early-return view with only the current-week matchup list and no personal schedule. Schedule data is now fetched before the commish/user split, and ScheduleTable is rendered at the bottom of the commish dashboard too.

Changed

  • [web] Stats sub-nav in sidebar — the Stats nav item now expands to show "Team Stats" and "Player Stats" as indented sub-links whenever any /stats/* route is active. Direct access without going through the hub page.

v0.4.2

2026-06-18

Changed

  • [ocr] Image cache per OCR session — each uploaded image is now decoded from disk once per submission and reused for all region crops. Previously Jimp.read was called once per region (~50–70× per image), making full 4K PNG decoding the dominant CPU cost per request.
  • [ocr] Persistent Tesseract worker — the worker now stays alive between requests and auto-terminates after 5 minutes of inactivity. Previously it was torn down after every OCR request, paying the full language-model cold-start cost on each submission.

v0.4.1

2026-06-18

Added

  • [db] OcrCorrection model — stores per-field diffs between raw Tesseract output and user-saved values on every stat submission. Captures screen type, field name, OCR value, corrected value, and confidence score.
  • [web] OCR correction logging — raw OCR output is now snapshotted before the review/edit step and diffed against the final saved values. Corrections are written non-fatally after each submission as groundwork for future OCR accuracy analysis and pattern detection.

v0.4.0

2026-06-18

Added

Schedule System

  • [web] Season schedule on dashboard — user dashboard now shows a full season schedule (weeks 1–22) replacing the single matchup card. Preseason and Regular Season are expanded by default; Postseason (weeks 18–22) is collapsed until a user has activity there. Offseason weeks (23–30) are hidden entirely.
  • [web] Year selector — schedule header includes a season dropdown when multiple dynasty years exist, allowing users to browse past seasons read-only.
  • [bot] /result log — new slash command to record post-game results. Options: score, opponent_score (required), plus optional opponent_team, my_rank, opponent_rank, week. CPU/sim games auto-confirm; UvU games send a Discord DM to the opponent.
  • [bot] /result view — shows the logged result for the current (or specified) week.
  • [bot] UvU result confirmation — when a player logs a UvU result, the opposing player receives a Discord DM with ✅ Confirm / ❌ Dispute buttons. Confirming marks the matchup complete; disputing alerts the commish.
  • [web] Inline result logging — past weeks with no result show a "Log Result" button that expands an inline form. UvU games prompt the opponent to confirm or dispute directly in the schedule table.
  • [web] /api/result POST — creates or updates a GameResult record. CPU games confirm immediately; UvU games go pending and trigger a bot DM via the scheduler polling loop.
  • [web] /api/result/[matchupId] PUT — confirm or dispute a pending result. Validates caller is the opposing player.

Score & Result Tracking

  • [db] GameResult model — stores userScore, opponentScore, opponentTeam, myRank, opponentRank, result (W/L/T), status (pending/confirmed/disputed), submittedBy, confirmedBy, and notificationSent flag for bot DM dispatch.
  • [bot] /games score display — matchup embed now shows confirmed results (e.g. W 42–17 (#8 vs #15)) and pending confirmation state (🕐 Awaiting confirmation).
  • [bot] Status board scores — completed UvU games on the status board now show the score inline (e.g. ✅ 42–17).
  • [bot/web] OCR → GameResult sync — submitting box score screenshots (web Submit page or bot /stats submit) automatically creates a confirmed GameResult record. Manual /result log entries are skipped if a confirmed OCR result already exists.
  • [web] Standings fallbackcomputeStandings() now falls back to confirmed GameResult records when no BoxScore exists for a completed matchup.

Multi-Season Support

  • [db] Week.dynastyYear — weeks are now scoped to a dynasty year. Unique constraint changed from (leagueId, weekNumber) to (leagueId, weekNumber, dynastyYear), fixing a latent bug that would have broken week creation in season 2.

Changed

  • [bot] Thread creation message — references updated from /matchup complete to /result log.
  • [bot] Game reminder DM — updated to direct players to use /result log to record their score after the game, then react ✅ to ready up.
  • [bot] /help — replaced /matchup complete with /result log and /result view in the quick reference.

v0.3.1

2026-06-18

Fixed

  • [web] Position dropdown — updated to game-specific positions: QB, HB, FB, WR, TE, T, G, C, REDG, LEDG, DT, SAM, WILL, MIKE, CB, FS, SS, K, P. Previous list used generic labels (RB, OL, DL, LB, DB) that didn't match in-game values.
  • [web] Archetype field — changed from free-text input to a position-dependent dropdown. Valid archetypes populate automatically when a position is selected; changing position clears archetype if the current value is no longer valid. Locked with placeholder text until a position is set.
  • [web] Dev trait dropdown — corrected values to Normal, Impact, Star, Elite.
  • [web] OVR input — replaced <input type="number"> (which showed unwanted spinner arrows) with <input type="text" inputMode="numeric">. Accepts up to 2 digits; non-numeric input is stripped.
  • [web] Roster table empty after upload — added router.refresh() on successful OCR import so the player table repopulates immediately without a manual page refresh.
  • [ocr] Position normalization — OCR import now maps game-native position codes to canonical values before saving: LT/RT→T, LG/RG→G, RE→REDG, LE→LEDG, MLB→MIKE, LOLB→WILL, ROLB→SAM. Previously only QB and TE imported correctly; all other positions stored the raw game value.

v0.3.0

2026-06-18

Added

Web Dashboard

  • [web] Role-aware dashboard — commishes see all matchups, ready counts, and recent activity; regular users see their personal matchup card, last game box score + leaders, and enrichment status.
  • [web] Roster page — each user's player table with inline editing for pos, OVR, archetype, dev trait, year, and redshirt flag. Incomplete players (missing pos or OVR) sorted to top with a yellow indicator.
  • [web] Roster screenshot upload — drag-and-drop modal on the Roster page accepts PS5 4K screenshots and imports up to 10 players per image via OCR. Multiple screenshots can be uploaded in one batch; duplicates are deduplicated by name.
  • [web] Stats submit drag-and-drop — the Submit page now accepts screenshots via drag-and-drop in addition to the file picker.
  • [web] Role-split sidebar — League Ops section (Matchups, League, Activity) is visible to commishes only; My Team section (Dashboard, Roster, Standings, Stats, Submit) is shown to all members.
  • [web] League Ops → League page — lists all active league members (renamed from "Roster").

Player System

  • [db] PlayerSeason modelPlayer is now identity-only (name, jersey, active). All season-specific data (pos, OVR, archetype, devTrait, year, redshirt, isRedshirtYear) lives in PlayerSeason keyed by (playerId, dynastyYear).
  • [db] League.dynastyYear — tracks the current in-game season year (default 2026). Increments automatically on season rollover.
  • [web] Player enrichment — stats submission (web + bot) creates PlayerSeason stubs for the current dynasty year; users fill in pos/OVR/archetype/devTrait on the Roster page.
  • [web] Redshirt tracking — users can toggle "RS?" on any player mid-season via the Roster page. At season rollover, redshirted players stay at their current class; the isRedshirtYear flag clears and redshirt is permanently marked.

OCR — Roster Screen

  • [ocr] Roster template — new OCR template for the in-game Roster screen. Calibrated against 3840×2160 PS5 screenshots: startY=0.430, rowHeight=0.045, stripH=0.018. Reads name, year (including RS suffix), position, and OVR for up to 15 rows per screenshot.
  • [ocr] Roster classification — classifier updated to detect "ROSTERS" header at { x:0.040, y:0.020, w:0.200, h:0.050 } and route to parseRoster().
  • [ocr] parseYear resilience — handles common Tesseract misreads: 5→S, 0→O, RY→RS. Correctly parses "50 RY" as SO with redshirt.

Bot

  • [bot] Season rollover logicperformAdvance() now detects week-30 advance and runs class progression: FR→SO→JR→SR, RS-flagged players stay same class, SR players without RS flag are marked inactive (graduated). OVR clears for the new season; pos/archetype/devTrait carry forward.

Changed

  • [web] Navigation renamed — "My Players" → "Roster"; old "Roster" (league members) → "League".
  • [web] Font — base font size bumped to 18px. Section labels in the sidebar are more prominent with accent color + gradient rule.
  • [web] Sidebar widerw-60w-64.

Schema changes

All schema changes were applied to Railway via db:push during development. No additional migration needed on deploy.

  • Added League.dynastyYear Int @default(2026)
  • Added PlayerSeason model (see above)
  • Removed flat Player.position, Player.archetype, Player.devTrait, Player.ovr — replaced by PlayerSeason

Deploy notes

  • No npm run deploy needed — no new slash commands in this release.
  • git push → Railway auto-deploys both the web service (Dockerfile) and bot service.

v0.2.7

2026-06-11

Fixed

  • /advance start FW outcomes — FWs recorded by the commish via /matchup fw (which sets status: complete) were previously invisible in the advance wizard. The response now includes a "FW outcomes recorded this week" section listing all completed FW games alongside the existing pending-FW checklist. Same fix applied to /preview.

v0.2.6

2026-06-09

Added

  • /golive command — members run this when they're about to stream their game. The bot posts a rich embed to a dedicated stream alert channel with the member's team name, stream link(s) (Twitch and/or YouTube — both shown if both are configured), current week label, and matchup details (opponent team name + mention for UvU games, "Simulation game" for CPU matchups). Fails gracefully with actionable ephemeral messages if stream alerts aren't configured, the member has no stream link, or the alert channel has been deleted.
  • /dd streams channel [#channel] (commish) — sets an existing channel as the stream alert destination, or auto-creates #stream-alerts when no channel is provided. Enables stream alerts in one step.
  • /dd streams enable (commish) — re-enables stream alerts after they've been disabled. Errors clearly if no alert channel has been configured yet.
  • /dd streams disable (commish) — pauses stream alerts without clearing the channel configuration, so re-enabling is instant.

Schema changes (run db:push against Railway after deploy)

  • League.streamAlertChannelId (String?) — stores the Discord channel ID for go-live announcements
  • League.streamAlertsEnabled (Boolean, default false) — feature toggle; preserves channel config when disabled

Changed

  • UvU and CPU game thread opening messages — both thread types now include /golive in their commands list so members are reminded to announce their stream when they're ready to play.

Note

This release adds a new command and a new subcommand group — npm run deploy must be run after deploy to register /golive and /dd streams with Discord.

v0.2.5

2026-06-08

Fixed

  • Thread names — matchup threads now use the member's Discord display name when no team is assigned, instead of the opaque User #### placeholder. The old fallback is retained if the user can no longer be fetched (e.g. they left the server).
  • FW DM notifications — commish DMs now show display names instead of raw <@id> mention syntax. Mobile push notification previews are now human-readable.
  • /advance start FW checklist — the FW pending section now shows exactly who to grant the win to (grant FW to @winner) and is labelled as a checklist rather than a generic warning.

v0.2.4

2026-06-04

Changed

  • OCR preprocessing — cropped regions are now 2× upscaled, converted to greyscale, histogram-normalised, and contrast-boosted before recognition. Improves accuracy across all stat screens without requiring higher-quality screenshots.
  • Tesseract page segmentation — switched from default PSM 3 (full page analysis) to PSM 7 (single text line). More appropriate for stat cell crops; reduces misclassification and slightly lowers CPU cost.
  • Numeric field cleanup — all stat columns (everything except player and team name) now go through a substitution pass (O→0, l/I→1, S→5, B→8, G→6, Z→2) followed by non-numeric character stripping. Fixes common OCR misreads that passed the text whitelist.
  • /stats submit low-confidence detail — the confirmation embed now shows the specific player and flagged field values for every low-confidence row (e.g. ↳ ⚠️ J. SMITH — comp: "l2", yards: "I87"), so users can judge whether to confirm or discard before reacting.
  • /stats submit confirmation messaging — footer and discard message are now contextual: when warnings are present, the footer advises confirming if values look right; the discard message no longer suggests re-uploading the same screenshots will produce a different result.

Fixed

  • Team name leading-digit strip — the L2 button prompt that appears near the team name region was already stripped in defense and blocking templates but not in passing, rushing, or receiving. Now applied consistently across all five stat templates.

v0.2.3

2026-06-03

Added

  • /roster retire @user (commish) — marks a member as retired when they leave the league. Retired members are immediately removed from the status board and no longer counted in the ready/not-ready tally. Historical matchup and stat data is preserved.
  • /roster unretire @user (commish) — restores a retired member to the active roster in case of a mistake or return.
  • /roster view — retired section — the roster view now shows a 🚫 Retired section below the active roster so commishes can see who has been retired.

Fixed

  • Stream link removal — passing clear, remove, -, or none as the twitch or youtube value in /roster add or /roster set now wipes the stored URL. Previously there was no way to remove a stream link once set.

Schema changes (run db:push against Railway after deploy)

  • Member.retired (Boolean, default false) — tracks whether a member has left the league

Note

/roster gained two new subcommands — npm run deploy must be run after deploy to register them with Discord.

v0.2.2

2026-06-03

Changed

  • /stats submit — week selection — new optional week integer option lets users submit stats for any week that has already started. Validates against existing current/complete weeks and replies with the valid range if the number is invalid. Defaults to the current week when omitted.
  • /stats submit — duplicate guard — before confirming, the bot checks whether stats already exist for that user + week. If a prior submission exists, the confirmation embed turns orange and the footer shows an overwrite warning. Reacting ✅ deletes all prior records in a transaction and writes fresh data; ❌ cancels with no changes.
  • UvU and CPU game thread opening messages — both thread types now include /stats submit in their commands list so players know how to log stats without leaving the thread.

Note

/stats submit gained a new option — npm run deploy must be run after deploy to register the updated command with Discord.

v0.2.1

2026-06-01

Added

  • /stats submit command — upload up to 7 post-game screenshots; the bot automatically parses all stat screens (box score, passing, rushing, receiving, blocking, defense) via Tesseract OCR, shows a confirmation embed with player/team counts and low-confidence warnings, and saves to the database on ✅ reaction.
  • OCR engine — fully calibrated for PS5 4K after-game screenshots. Classifier detects screen type (passing/rushing/receiving/blocking/defense1/defense2/boxScore) and layout (afterGame). Column coordinates tuned against real screenshots for all stat categories.
  • Defense two-screen merge — defense stats are split across two game screens; the bot accepts both screenshots and merges them by row position automatically.

Changed

  • Defense schema field — renamed defl to pdef to match the parsed column name (passes defended).
  • Classifier robustness — added column-header fallback detection for blocking and defense screens that don't expose a stat-type label in the standard region.
  • Defense merger — changed from hard throw on row count mismatch to a graceful trim with warning log; handles the noise rows in D2 that D1's threshold filter removes.

Schema changes (run db:push against Railway after deploy)

  • New models: GameStat, BoxScore, PassingStat, RushingStat, ReceivingStat, BlockingStat, DefenseStat
  • DefenseStat.pdef replaces the old defl field

Note

This is a new command — npm run deploy must be run after deploy to register /stats submit with Discord.

v0.1.11

2026-05-28

Fixed

  • /matchup set future-week bug — the week option previously accepted a raw integer that mapped to the internal database week number, not the display label. Entering 7 while in "Week 5" resolved to internal week 7 = display "Week 5" = the *current* week, so the matchup was treated as live, a thread was created immediately with the wrong week label, and the game appeared on the current board. All three week options (set, view, clear) now use the same autocomplete dropdown as /dd setup and /week set, presenting display labels ("Week 5", "Week 6"…) that resolve to the correct internal values.

Changed

  • /matchup view and /matchup clear error/response messages now show display week labels ("Week 7") instead of raw internal numbers ("Week 9").
  • /matchup set default description updated to clarify that omitting week defaults to *next* week (not current week).

Note

This release changes slash command option types — npm run deploy must be run after deploy to re-register commands with Discord and activate the autocomplete dropdowns.

v0.1.10

2026-05-28

Changed

  • Games section now shows User vs User matchups only — CPU/sim games are no longer listed on the status board. Anyone not in the games list is assumed to have a CPU game or bye week.
  • Section renamed from "Games This Week" to "User vs User Games This Week".
  • Thread links removed from board — game lines no longer include the → #thread channel link. Commissioners are added directly to threads (see below), making the link redundant.

Fixed

  • Reactions broken on split boards — leagues large enough to trigger the v0.1.9 multi-message split had completely non-functional ✅/🏝️ reactions. The reaction handlers were doing a direct string equality check against statusMessageId, which fails when it's stored as a JSON array. Both handlers now parse the array and check against the first message ID.

Added

  • Commissioners auto-added to UvU threads — when a User vs User matchup thread is created (via /matchup set or /advance confirm), all league commissioners are automatically added as thread members. This gives commishes visibility and admin access to every game thread without needing a link on the board.

v0.1.9

2026-05-28

Added

  • Manual advance mode/schedule cadence now accepts an advance option (auto or manual). In manual mode the week does not advance automatically when the deadline passes; instead the bot pings all commissioners in the league channel and waits for a commish to run /advance manually. Advance mode is shown in /schedule view.
  • /dd board — force re-post the status board for the current week (replaces the existing board without advancing the week). Useful when the board is lost or needs a fresh post.

Fixed

  • Status board overflow for large leagues — boards with 18+ members and many matchup lines previously crashed with DiscordAPIError[50035]: Must be 2000 or fewer in length. The board now splits into multiple messages when content exceeds Discord's 2000-character limit. Reactions remain on the first message; updates edit each chunk in place.
  • Multiple commissioners can now use all commish-gated commands (/dd board, /dd reset, /dd channel, etc.) — previously only the first commissioner ID was checked in some paths.
  • /dd subcommands are now visible to all registered commissioners, not just server admins.

Schema changes (run db:push against Railway)

  • League.autoAdvance (Boolean, default true) — stores manual vs auto advance preference
  • Week.manualAdvanceNotifiedAt (DateTime?) — tracks whether the commish ping has already been sent for a missed deadline in manual mode

v0.1.3

2026-05-25

Fixed

  • /matchup set no longer creates a thread immediately when the target week is not the active week — thread creation now only fires when the matchup's week number matches league.currentWeek; future-week matchups display "Thread will be created when that week is active" instead

v0.1.2

2026-05-25

Added

  • /dd commish add <user> — promote a user to commissioner
  • /dd commish remove <user> — revoke commissioner permissions (blocked if last commish)
  • /dd commish list — display all current league commissioners

Changed

  • /dd commish is now a subcommand group; the promote flow moves from /dd commish <user> to /dd commish add <user>

v0.1.1

2026-05-20

Added

  • /mydesk command — personal quick-ref for your team, stream links, and matchup
  • Sim/CPU matchup support via /matchup sim
  • Post-game ready reminders sent to UvU threads

v0.1.0

2026-05-18

Initial release

  • League setup, advance scheduling, and status board
  • Ready/away reaction tracking
  • UvU matchup threads with stream links
  • /schedule, /week, /roster, /matchup, /fw, /away, /games, /preview, /help
  • DST-correct timezone handling via IANA timezones
  • Railway + PostgreSQL deployment