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.
SENTRY_DSN is setBOT_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 tickinstrumentation.ts / instrumentation-client.ts). No-ops unless SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN are set/api/health endpoint that returns 200 only when Postgres is reachable, for an external uptime monitorauto-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 stepauto-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/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 leagueConferenceAssignment table — sparse per-league, per-dynasty-year override of a school's conference, falling back to the static default when unset/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 timePOST /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 RecordsGameStat rows with teamName: "App State" updated to the canonical "Appalachian State" so previously-submitted stats now show up in Record WatchDELETE /api/matchup/[id] — new endpoint backing the cancel actionGameResult and marks the associated Matchup as complete, so the result feeds into the standings without a separate step/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/api/weeks — added weekNumber: { lte: 22 } guard to exclude offseason/placeholder weeks from the stats week picker/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)/dd setup confirmation message now always shows Year XXXX, Week Label so the commish can verify year and week at a glanceAward 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/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/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/awards/manage (commish only) — same form with full alphabetical team picker. Live table of current-season awards grouped by award type, with per-row deleteGET /api/awards — returns awards for the league filtered by year (defaults to league.dynastyYear)POST /api/awards — commish can set any team; users are server-locked to their own team. Year is always server-set from league.dynastyYearDELETE /api/awards/[id] — allowed for the commish or the original submitterawards feature flag (enabled)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 correctlyPOST /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" valueMember.discordUserId is now nullable — web-created members are identified by userId (NextAuth User ID) rather than a Discord snowflakeMember.userId new field — FK to the NextAuth User table. New @@unique([leagueId, userId]) constraint for web membersLeague.guildId is now nullable — solo leagues have no Discord guild IDLeague.createdVia new field — "discord" (default, all existing leagues) or "solo" (web-created)/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 loginPOST /api/leagues/[leagueId]/advance — new API route for web-triggered advance. Solo leagues only; commish session requireddiscordId-required to user.id-required (NextAuth always populates this). Member lookups across all routes now use an OR clause: userId OR discordUserId/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 pageRecordSubmission 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/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 messageSchoolRecordsBanner — 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/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 actionsGET /api/records/school-status — checks which levels have global records for a school and returns the requesting user's own pending/rejected submissionsPOST /api/records/submissions — validates school ownership against member.teamId, enforces one pending submission per school+level globally (409 on duplicate), creates RecordSubmissionPOST /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)POST /api/admin/records/submissions/[id]/reject — stores rejection note, sets status to rejected, allows the user to re-submitimportRecordRows() extracted to lib/records.ts — shared by the admin import route and the approval endpoint; eliminates duplicationResultCard 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)/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 globallyPOST /api/admin/records/import — app-admin-gated endpoint; idempotent delete+reinsert for the submitted scope+level, validated against known categoriescpuOpponentTeam column to Matchup — stores the CPU opponent team abbreviation at schedule-set time, so it's visible before a result is loggedPATCH /api/matchup/[id] endpoint added<:Name:id>) now render as plain text instead of leaking raw markup into opponent sub-labelsRecord.leagueId is now nullable — null = global baseline shared across all leagues; league-specific overrides win via mergeRecords() helperFeatureFlag model added — DB-backed feature flags replacing env-var allowlists; isFeatureEnabled("records") is now the single gate/admin/* area — gated by APP_ADMIN_DISCORD_IDS env var (distinct from commish role); sidebar shows "App Admin" section when active/admin/records) — CRUD for global baseline records (leagueId=null), inline row editing, scope navigation/admin/feature-flags) — toggle switches for all known flags; creates DB row on first toggleleagueId: null for all 108 global records; run with node packages/db/scripts/seed-records.js [--force]FEATURE_RECORDS_ALLOWLIST)Record model to schema: scope (national/conference/school), level (season/game/career), category (9 fixed types), holder name/school/year/value/records) — tabbed by stat group (Passing/Rushing/Receiving/Defense), shows record holders with best-chaser progress bars and on-pace projections/records/manage) — commish CRUD: inline edit/delete, scope filters, add form with conditional conference/school fields/records/import) — OCR-based bulk import: select Season/Game/Career, drop a CFB Records screenshot, review 9 parsed rows, confirm to upsert/stats/manual) — tabbed Passing/Rushing/Receiving/Defense form, dynamic player rows, submits directly to DB without OCRFEATURE_RECORDS_ALLOWLIST env var (comma-separated Discord IDs); gates all Records pages, nav items, and dashboard widgetrecordBook template — detects CFB Records screen, extracts scope label + 9 fixed rows × 5 columns; exports parseRecordBook from OCR packagepackages/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]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.position: fixed anchored via getBoundingClientRect so the full 5×4 grid is always visible regardless of scroll position or parent overflow.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.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./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./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./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)./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.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.scheduler.js now advances currentClass on all active ScoutingEntry records (FR→SO→JR→SR) and deactivates graduated SR players.input-cell / select-cell CSS utilities — reusable form-field classes added to globals.css.neutralSite stored as a boolean on Matchup.Matchup.neutralSite — Boolean @default(false) column pushed to Railway./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.app/icon.png.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.Matchup.homeUserId — nullable Discord user ID indicating which player is playing at home.future_2026 to current status.homeUserId on the matchup when saving.--muted-foreground bumped from L=0.75 to L=0.82 for better readability of secondary text on dark backgrounds.ScheduleTable is rendered at the bottom of the commish dashboard too./stats/* route is active. Direct access without going through the hub page.Jimp.read was called once per region (~50–70× per image), making full 4K PNG decoding the dominant CPU cost per request.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./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./result view — shows the logged result for the current (or specified) week./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./api/result/[matchupId] PUT — confirm or dispute a pending result. Validates caller is the opposing player.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./games score display — matchup embed now shows confirmed results (e.g. W 42–17 (#8 vs #15)) and pending confirmation state (🕐 Awaiting confirmation).✅ 42–17)./stats submit) automatically creates a confirmed GameResult record. Manual /result log entries are skipped if a confirmed OCR result already exists.computeStandings() now falls back to confirmed GameResult records when no BoxScore exists for a completed matchup.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./matchup complete to /result log./result log to record their score after the game, then react ✅ to ready up./help — replaced /matchup complete with /result log and /result view in the quick reference.<input type="number"> (which showed unwanted spinner arrows) with <input type="text" inputMode="numeric">. Accepts up to 2 digits; non-numeric input is stripped.router.refresh() on successful OCR import so the player table repopulates immediately without a manual page refresh.Player 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).League.dynastyYear — tracks the current in-game season year (default 2026). Increments automatically on season rollover.PlayerSeason stubs for the current dynasty year; users fill in pos/OVR/archetype/devTrait on the Roster page.isRedshirtYear flag clears and redshirt is permanently marked.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.{ x:0.040, y:0.020, w:0.200, h:0.050 } and route to parseRoster().parseYear resilience — handles common Tesseract misreads: 5→S, 0→O, RY→RS. Correctly parses "50 RY" as SO with redshirt.performAdvance() 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.w-60 → w-64.All schema changes were applied to Railway via db:push during development. No additional migration needed on deploy.
League.dynastyYear Int @default(2026)PlayerSeason model (see above)Player.position, Player.archetype, Player.devTrait, Player.ovr — replaced by PlayerSeasonnpm run deploy needed — no new slash commands in this release.git push → Railway auto-deploys both the web service (Dockerfile) and bot service./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./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.db:push against Railway after deploy)League.streamAlertChannelId (String?) — stores the Discord channel ID for go-live announcementsLeague.streamAlertsEnabled (Boolean, default false) — feature toggle; preserves channel config when disabled/golive in their commands list so members are reminded to announce their stream when they're ready to play.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.
User #### placeholder. The old fallback is retained if the user can no longer be fetched (e.g. they left the server).<@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.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./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.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.db:push against Railway after deploy)Member.retired (Boolean, default false) — tracks whether a member has left the league/roster gained two new subcommands — npm run deploy must be run after deploy to register them with Discord.
/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./stats submit in their commands list so players know how to log stats without leaving the thread./stats submit gained a new option — npm run deploy must be run after deploy to register the updated command with Discord.
/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.defl to pdef to match the parsed column name (passes defended).db:push against Railway after deploy)GameStat, BoxScore, PassingStat, RushingStat, ReceivingStat, BlockingStat, DefenseStatDefenseStat.pdef replaces the old defl fieldThis is a new command — npm run deploy must be run after deploy to register /stats submit with Discord.
/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./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).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.
→ #thread channel link. Commissioners are added directly to threads (see below), making the link redundant.statusMessageId, which fails when it's stored as a JSON array. Both handlers now parse the array and check against the first message ID./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./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.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./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.db:push against Railway)League.autoAdvance (Boolean, default true) — stores manual vs auto advance preferenceWeek.manualAdvanceNotifiedAt (DateTime?) — tracks whether the commish ping has already been sent for a missed deadline in manual mode/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/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/dd commish is now a subcommand group; the promote flow moves from /dd commish <user> to /dd commish add <user>/mydesk command — personal quick-ref for your team, stream links, and matchup/matchup sim/schedule, /week, /roster, /matchup, /fw, /away, /games, /preview, /help