尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A static GitHub Pages site for a Dancing with the Stars fantasy league. The browser is the interface; Supabase stores the shared league data.
Repository rename preparation and manual settings are documented in Repository rename migration. The current deployment remains supported until the rename; do not run the avatar cutover until the new website and its images are live.
index.html, styles.css, roster.css, app.js, and ui.js serve the original league.league-workspace.js serves other leagues; postdraft-view.js contains shared views.supabase-client.js is the single shared Supabase browser client.Images/ holds cast portraits, the DWTS logo, and judge-score graphics.supabase/ contains the database migrations and policies that have been
applied incrementally while building the league.The Supabase publishable key in supabase-client.js is expected to be public.
Row-level security policies—not that key—separate public reads, league
commissioner controls, and platform-owner show administration.
league_members.fantasy_team_id is set, it
displays only that account’s team and adopts the team name as its heading./score-desk/ as a separate platform-owner workspace.
It is the only interface that creates or changes canonical weeks, dances,
judges' scores, cast appearances, and completed-week corrections./cast-roster/ and is likewise restricted to
the platform owner. It owns canonical cast roles, partnerships, portraits,
biographies, career highlights, past-win information, and the universal role rates.There is no build step. Open the project through any simple static web server,
or let GitHub Pages serve the main branch. Do not open index.html directly:
the module imports and Supabase connection are designed for a web origin.
Upload photos to Images/Dances/Week N/ on the main branch, using the dance
card's couple name and a numbered suffix, for example
Images/Dances/Week 3/Amber Glenn and Pasha Pashkov-1.jpeg. Use and or &
between partner names; .jpg, .jpeg, .png, and .webp are supported.
Additional photos can use -2, -3, and so on. The live site checks the public
repository about every five minutes while open and updates matching dance cards
without a page reload. Returning to the tab also checks for new photos. Until a
matching photo is uploaded, the card continues without one. GitHub Actions
automatically compresses uploaded originals, creates small WebP card images,
and updates a photo manifest. A new upload appears first using the original;
the card switches to its smaller image when processing finishes. If GitHub's
repository listing is unavailable, the app uses the manifest and its last
known photo list. Locally, run npm ci and npm run photos after adding images;
npm run photos:check verifies the generated files.
The shared league workspace checks live scores and dances every 30 seconds,
reloads slower-changing cast/settings data at most every two minutes, and
checks the week's roster snapshot every five minutes. Database guards still
capture the roster when a week is completed. Prediction history is fetched in
explicit pages by market-history.js when a graph needs it, avoiding the
1,000-row API response cap.
This is an existing, migrated project. Do not run supabase/schema.sql on
the current project: it describes the original pre-migration players model.
supabase/migrations/20260929_live_public_baseline.sql is the verified,
schema-only public-schema export from September 29, 2026. It is a reference
baseline, not a migration to apply to the live project. See
supabase/migrations/README.md before refreshing it.
The current migration/cleanup sequence and live-season boundaries are in
docs/efficiency-and-data-integrity-plan.md.
For a fresh rebuild, begin with the original schema and then apply migrations in their documented dependency order. For this existing league, only run a new file when its comment says it is the next needed migration.
supabase/harden-score-desk-and-role-rates.sql has been applied to the live
league. It adds database-level scoring safeguards, default role-rate editing,
and weekly roster snapshots.
Run supabase/remove-completed-week-edit-lock.sql once after it. This removes
only the old completed-week lock, leaving duplicate-dance and competing-pair
safeguards intact. A completed week then hands corrections to its Week Ledger.
Run supabase/atomic-dance-saves-and-week-order.sql once to enable atomic dance
saves and the inline week editor's dance reordering. It does not change existing
dances or scores. Until it is run, dance entry keeps using the older save path
and week details can still be edited, but saving a changed dance order will ask
for the migration.
Run supabase/add-league-member-accounts.sql before adding manager accounts.
It creates the private league_members account-to-team mapping, first and last
names, commissioner flag, row-level security, and a safe name-update function.
After creating users in Supabase Authentication, add one league_members row
per user to connect that Auth user to a fantasy team. Until this migration is
run, the existing commissioner account continues to use the legacy fallback.
Then run supabase/refine-manager-profiles-and-cast-labels.sql. It adds each
manager's optional My Team navigation preference, the safe manager self-edit
function, public team-manager display names, and Judge/Host subtypes. It also
converts the old Hough cast role into a Hough scoring-rate checkbox within the
Judges + Hosts category. The separate Hough role rate remains the scoring rate
used by anyone with that checkbox selected.
Finally, run supabase/perfect-current-workflows.sql. It is the current
hardening migration: commissioner permissions use the league_members role,
partnership/team/rate/deletion edits become atomic, historically referenced
cast cannot be deleted, a missing performance can be added from a completed
week's Ledger, and an optional week title remains truly blank. Run this SQL
before publishing the matching frontend changes because the site deliberately
no longer falls back to partial multi-request dance saves.
Then run supabase/add-week-airing-dates.sql. It adds optional first- and
second-night airing dates to each week, validates their order, and keeps those
dates in the same atomic save as week setup and dance ordering. The My Team
page shows every completed week plus only the next scheduled week; an upcoming
week displays its airing date instead of a misleading zero-point total.
Then run supabase/edit-completed-week-details.sql. It adds corrections for a
completed week's title, theme, airing dates, guest-judge name, and elimination
format from the Week Ledger. The database verifies that elimination settings
still match the recorded result and prevents unsafe guest-judge additions or
removals after scores have been finalized. The following migration narrows this
correction access from commissioners to the platform owner.
Finally, run supabase/separate-platform-score-desk.sql. It creates the private
platform-owner identity used by the separate Score Desk and moves database
write access for canonical show facts away from the league-commissioner role.
The migration seeds the current owner account from its existing Supabase Auth
email; future commissioners cannot grant themselves this permission.
Then run supabase/separate-cast-source-and-league-settings.sql. It moves
canonical cast editing behind the platform-owner account, gives commissioners
narrow team-assignment functions, adds public cast-profile fields and a
commissioner-editable league name, and adds a distinct season-finale marker.
The existing is_finale field continues to mean “no elimination”; the new
marker hides Add Week only when the last scheduled week is the season finale.
Run supabase/restrict-mirrorball-wins-to-pros-and-houghs.sql after that. It
limits past Mirrorball-win entry to pros (including eliminated pros) and to
Judges/Hosts with Hough scoring enabled, with the same rule enforced by the
database.
Run supabase/fix-league-name-permissions.sql on installations that already
applied the league-settings migration. It fixes the commissioner league-name
save path while keeping direct writes to league_settings blocked.
Run supabase/add-manager-trading.sql to enable private one-for-one trade
offers between linked manager accounts. Managers can accept, deny, or make one
counter that changes exactly one side of the offer. Accepting a trade swaps
both current team assignments atomically; completed-week snapshots remain
unchanged, and the new assignments are captured when the next week completes.
Then run supabase/enhance-trade-expiration-history.sql. It gives each new or
countered offer a fresh 48-hour response window, records private event history
for both involved teams, and removes expired offers the next time either
manager opens or acts on trading. The matching interface adds countdowns,
image-based offer building, custom confirmations, and a History view.
Then run supabase/fix-trade-counter-history-and-cancellation.sql. It records
the original offer before replacing it with a counteroffer and lets the sender
of either an offer or counteroffer cancel while the other manager is deciding.
Cancelled offers are retained in the private history for both teams.
Finally, run supabase/optimize-current-query-indexes.sql. It adds indexes for
week ordering, cast appearances, current team rosters, historical snapshots,
and trade conflict/history lookups. It does not change data, permissions, or
scoring behavior.
Then run supabase/enforce-one-for-one-roster-swaps.sql. It replaces separate
commissioner add/remove controls with one atomic free-agent swap, keeping every
established fantasy-team roster the same size and protecting cast members in
active trade offers.
Run supabase/allow-managers-to-claim-available-cast.sql after it. This exposes
that same atomic swap to each signed-in manager for their own team only, powering
the Team-page free-agent claim flow without allowing direct additions or drops.
Then run supabase/harden-data-integrity-and-manager-inputs.sql. It locks
free-agent claims to the signed-in manager's own team even when that manager is
also a commissioner, records competing trade offers as Superseded before an
accepted trade removes them, and enforces manager/team/navigation text limits
at the database boundary.
Then run supabase/prepare-multi-league-foundation.sql. It creates and seeds
only the existing league, scopes current league-owned rows, and mirrors current
rosters and rates into league-specific tables. It does not create a second
league or expose league creation, and the current single-league interface keeps
working unchanged.
Then run supabase/add-trade-result-notifications.sql. It keeps a newly
accepted or denied trade in the sending manager's Active tab until they dismiss
the result. Dismissal moves it into the normal private History view and never
deletes the historical record.
Then run supabase/add-user-profiles.sql. It creates a canonical profile for
every existing Supabase Auth account without changing any Auth UUID or login
method, generates deterministic temporary usernames, and installs the signup
trigger for future accounts. Profile display names become the identity source;
legacy names on league_members and fantasy_teams remain synchronized for
older cached clients, while completed-week snapshot names remain deliberately
historical. Users can update their own username and display name, upload a
profile picture, or choose a cast photo. Only the safe public directory fields
are available for league discovery.
Next, run supabase/activate-multi-league-workspaces.sql before deploying the
matching website changes. This one-time migration leaves the existing league
and its historical data intact. It adds private owner/member workspaces,
username and revocable-link invitations, league-specific teams and rates, a
randomized snake draft, and league-specific roster snapshots and trades. It
does not create another league automatically. New leagues are created from the
account menu by an onboarded user. A new league remains in setup, then drafting,
and becomes active only after all configured rounds are picked. The roster
size is the number of draft rounds and locks when drafting starts. Managers
make each pick using Claim under Available Cast on My Team; after the draft,
claims require releasing a current cast member.
After that migration, run supabase/fix-default-league-read-access.sql before
using the signed-in legacy league pages. It restores execute access to the
public default-league ID helper used by read policies and removes an obsolete
email-based policy that could write teams outside the original league.
Run supabase/add-profile-picture-storage.sql to enable profile uploads. The
public bucket accepts JPEG, PNG, and WebP files up to 2 MB; authenticated users
can upload only into their own UUID-named folder. Cast-photo selection does not
require an upload.
Then run supabase/finish-league-draft-setup.sql before publishing the next
website update. Setup leagues automatically size rosters as managers join:
3 managers → 12 cast each, 4 → 10, 5 → 8, and 6 → 7, capped by the actual
cast pool. Until three join, the 3-manager size is shown provisionally.
This earlier migration allowed owners to override sizing before the draft starts;
the later roster-balance migration removes that option for new leagues.
At this migration step, 3–6 managers can start a draft; the later
three-to-five-manager-leagues.sql migration lowers that limit to 3–5.
Once the final pick is made, the league
receives historical snapshots for every already-completed week using the
original draft teams, so all earlier show points count. This migration also
repairs completed secondary-league drafts that have the full pick record.
Deployment check: after running the migration, sign in as three existing users, confirm their current league/teams still appear, create a test league from the account menu, invite the other two users by username or link, and make all 36 draft picks with the automatic 12-person roster. To make a shorter smoke test, the owner can manually reduce the roster to 1 or 2 in League settings before starting. Confirm all completed show points appear after the final pick, and that switching leagues never changes another league's roster. The project has no local Supabase test database, so this multi-account check remains important. Do not rerun old schema files on the live project.
Identity ownership after this migration:
auth.users owns authentication and keeps every existing UUID unchanged.profiles owns username, display name, avatar, and onboarding state.league_members owns per-league membership, team connection, owner/member
access, and navigation preferences—not account identity.fantasy_teams owns league-specific team names. Its manager_name column is
a temporary compatibility mirror and is no longer independently edited.weekly_roster_snapshots.manager_name remains a frozen historical label so
later profile changes cannot rewrite completed-week history.After the previously applied supabase/delete-setup-league.sql, run
supabase/league-limits-and-full-deletion.sql. It allows each account
to create up to two non-original leagues and hold up to five active league
memberships (including the original league). It also lets a league owner delete
their drafted secondary league and its league-specific records. Deletion is
permanent; the original league is protected. Existing managers with backfilled
profiles are marked onboarded, while new accounts still confirm their profile.
For live drafts, run supabase/live-draft-clock.sql after the league-limits
migration. It starts a two-minute server-side turn clock, records automatic
random picks, and gives drafts already underway a fresh two-minute turn when
installed. Signed-in league members keep the clock moving while the draft is
open; if pg_cron is installed, a scheduled job also advances expired turns
when nobody has the site open.
For draft readiness and optional untimed drafts, run
supabase/draft-readiness-and-untimed-mode.sql after
supabase/pause-league-draft.sql and before deploying the matching site update.
Regular managers can mark themselves ready or unready in Draft; the
commissioner can start only when all regular managers are ready. The start
confirmation can disable both the two-minute timer and automatic picks for
that draft. Existing drafts keep their timed behavior.
For the new-league roster rules, run
supabase/secondary-league-roster-balance-and-airing-lock.sql after both
supabase/draft-readiness-and-untimed-mode.sql and
supabase/add-week-airing-dates.sql. It does not alter the original league.
This migration originally used preset roster sizes, including seven cast
members per team with six managers. The later 3–5-manager migration supersedes
that size. During drafts and later swaps, each team may add at
most the whole-number share of the remaining active pros and stars; eliminated
cast and all other roles are Bonus. If a preset cannot be filled under those
shares, the draft permits one extra active Pro or Star per team (not both),
with a corresponding Bonus-pick limit. That draft-only exception does not
permit additional active cast through later swaps. An over-limit existing player is retained
after an elimination but another cannot be added. Secondary-league roster
changes are blocked on either airing date in Eastern time, and that day's
roster snapshot is used for weekly scoring. Surprise cast in every league
scores at the chosen normal Bonus-role rate plus two; classify any existing
Surprise cast in the cast editor so retroactive scoring is unambiguous.
Cast profiles before and during the draft preview points from completed shows
using the league's current appearance rates; these are not points already
earned by a fantasy team.
For the current 3–5 manager limit, run
supabase/three-to-five-manager-leagues.sql after the roster-balance migration.
New and setup leagues use exactly 12, 10, or 8 cast per team for 3, 4, or 5
managers. The commissioner counts toward the five-manager limit, and the
database rejects a sixth active membership regardless of whether someone
joins by username invite, link, or code. The draft start check also verifies
that any Pro-or-Star Flex spots can be filled league-wide. A Flex pick cannot
take a Pro or Star still needed for another manager's standard draft slot.
Post-draft Pro and
Star limits continue to recalculate as cast roles change: an existing player
may remain above a lowered limit, but a free-agent claim, trade offer, or
counteroffer cannot bring an active Pro or Star onto a roster that would
remain above that limit. Swapping an over-limit player for Bonus cast is
allowed.
Run supabase/dynamic-roster-limits.sql after the draft-freeze and
airing-until-completion migrations. This supersedes the draft-only Flex rules
above for regular leagues. The original league remains completely exempt.
Roster sizes remain 12/10/8 for 3/4/5 teams. Combined active cap is
ceil((active Pros + active Stars) / teams); each individual role cap is
ceil((active Pros + active Stars) / teams / 2); Bonus cap is
roster_size - combined_active_cap + 1. Participating unmanaged teams still
count in the denominator. Nothing removes cast from existing over-limit teams.
Optionally run supabase/check-dynamic-roster-limits.sql afterward to inspect
current caps, retained over-limit teams and client/private function privileges.
That check is read-only and is intended for the SQL editor, not a client RPC.
Over-limit teams may exchange only from an overfilled category into a category with room; if only the combined active cap is exceeded, either active category may leave for Bonus. Each exchange must reduce an excess without increasing another. Partial corrections are supported. Both recipients are checked when offers/counters are sent and again at acceptance. The normal airing lock, orphan-team auto-accept, commissioner timer controls, scores and snapshots stay unchanged. Active free-agent swaps now replace the outgoing assignment in one atomic update, keeping total roster size unchanged.
Authenticated members can call get_league_roster_rules(p_league_id) for the
web/iOS contract: version, status, limits, draft_can_start,
roster_changes_locked, and teams. Limits include pro_max, star_max,
active_max, bonus_max, roster_size, team_count, exempt, and
legacy_draft_limits. Each team returns team_id, counts keyed Pro/Star/
Bonus, over_limit_categories, combined_active_over, allowed_exchanges
(outgoing category -> allowed incoming categories), and
draft_eligible_categories (category -> boolean, populated during drafting).
The category matrix does not waive actual availability, ownership, pending
offers, draft turn or airing restrictions. Existing mutation RPC signatures
are unchanged. Refresh this read after a pick, trade, swap or week completion.
Draft eligibility uses exact max-flow supply checks so a legal-looking pick cannot strand another manager. Numeric limits and cast roles are frozen at draft start. A draft already running when this migration first runs retains its old numeric allowances until it ends. Automatic picks randomly choose within the first eligible category in Pro -> Star -> Bonus order. The UI never shows a separate Flex category or changes an existing drafted member's role.
npm run check:rosters tests all 39 scenarios and corrective exchanges. The
additional scripts/check-dynamic-rosters-db.mjs test runs the actual migration
only in an explicitly named disposable PostgreSQL container; never use it
against live Supabase.
Google and Apple sign-in use the existing Supabase Auth users and profile trigger. New users finish onboarding by choosing a username and display name; existing password users can connect either provider without changing their Auth UUID. Configure both providers and the deployed site's redirect URL in Supabase Auth. Enable manual identity linking for the profile's Connect buttons; keep email/password login enabled until existing members have linked an identity. The website no longer offers new email/password registration.
Run supabase/add-kalshi-market-predictions.sql after the cast-profile and
week-airing-date migrations, then run
supabase/add-market-prediction-history-and-schedule.sql. The first script maps
the 2026 Kalshi ticker suffixes to canonical star IDs and creates the current
prediction table. The second adds Eastern airing times (8–10 PM by default),
the owner-controlled weekly elimination toggle, and append-only graph history.
The first migration fails if any ticker code cannot be matched to a cast member.
Predictions do not change fantasy points, roster rules, cast roles, or manually
entered eliminations.
Run supabase/backfill-kalshi-market-history.sql after the history migration
to import the owner-supplied Week 1 and 2 pre-airing snapshots. It is safe to
rerun. It intentionally leaves finalist and Week 1–2 elimination history empty;
each point retains its original hourly candle timestamp and the 7:59 PM
Eastern observation time. The source converter is
scripts/build-market-history-backfill.mjs.
Run supabase/airing-trade-and-draft-windows.sql after
supabase/add-market-prediction-history-and-schedule.sql. In secondary leagues,
trades and roster changes pause from two hours before each airing until two
hours after its scheduled end (normally 6 PM–midnight Eastern). Draft starts,
manual picks, and automatic picks pause 15 minutes before an airing and resume
only after that week is marked complete. A timed draft keeps its remaining
turn time across the pause; commissioner-paused drafts stay paused. The rules
read any overridden airing times from Score Desk.
Deploy supabase/functions/sync-market-predictions as a Supabase Edge Function
with JWT verification disabled. Set its MARKET_PREDICTIONS_SYNC_SECRET to a
long random secret. Supabase supplies SUPABASE_URL and
SUPABASE_SERVICE_ROLE_KEY to the function; neither the service-role key nor
the sync secret belongs in the site or repository. In Supabase Vault, store
market_prediction_sync_url (the deployed function URL) and
market_prediction_sync_secret (the same secret set on the Edge Function),
then run supabase/schedule-market-prediction-sync.sql. Supabase Cron calls the
function every five minutes. The function refreshes every 15 minutes outside
an airing and every five minutes during either airing night, saving hourly or
ten-minute graph points respectively. Only successful fetches advance the
schedule; retries cannot duplicate a point. The GitHub workflow is manual-only
for an emergency forced refresh; its repository secrets are optional unless
you use that workflow.
Weekly elimination markets are off by default. In Score Desk → Edit Week, verify the new Kalshi market is open before enabling the toggle. Override the first and second airing times there if needed. Weekly predictions stop at the final airing's end, even if the week has not been marked complete. Next week's prediction stays absent until its market is enabled and has a valid quote. Eliminated couples receive no new quotes or history points; their past graph data remains. The UI hides stale or closed markets. Review Kalshi's current data-display terms before publishing.
Before deploying the invite-sharing UI, enable Supabase Cron (pg_cron) and run
supabase/short-league-invite-codes.sql in the Supabase SQL editor. Each new
setup league receives a link and six-character code when it is created. The
commissioner can retrieve the current pair or refresh it, invalidating the
previous pair. Both expire after 48 hours. A minutely Cron job renews expired
pairs in the background, and the next commissioner visit also renews any
pair the job missed. Confirm the rotate-mirrorball-league-invites job is
active in the Supabase Cron dashboard. Existing links without codes are
replaced on the commissioner's next visit. Starting the draft revokes the pair immediately.
The invite dialog displays the code only: Copy link copies just the URL, while
Share passes a short message containing the URL and code to the device share sheet.
Code entry requires sign-in and is limited to five attempts per account per
hour. Enter codes from the account menu (or Manage invites before joining a
league); the signed-out home never accepts a code. Commissioners invite an
existing member by entering their exact username, without a people-search
list. A shared link opens an invitation notice for signed-out visitors and
appears with the other invitations in the account menu after sign-in.
supabase/standardize-dance-types.sql before deploying the dance-type
dropdown. Its dance_types catalog is the approved spelling list for every
competitive dance; add future types with an SQL insert into that table. Fusion
uses two different catalog styles and is saved as First/Second Fusion.node scripts/check-cache-keys.mjs verifies that every page references the
same JavaScript and CSS cache key. The GitHub workflow runs this check on
every push and pull request.Before publishing the account-deletion UI, run supabase/account-deletion.sql
in the Supabase SQL editor, then deploy supabase/functions/delete-account
with JWT verification enabled (supabase functions deploy delete-account --project-ref mdrrnanxqazecqviaass).
The function uses Supabase's existing SUPABASE_URL, SUPABASE_ANON_KEY, and
SUPABASE_SERVICE_ROLE_KEY server-side secrets; never put the service-role key
in website code. Test with a disposable account before announcing the feature.
Users delete their own account while editing their account profile. The
confirmation dialog requires an acknowledgement checkbox before they can
confirm deletion.
Uploaded profile photos, sign-in credentials, profile, invitations, and league
memberships are removed. A team stays in the league without a manager, with
its manager name anonymized. An active manager automatically becomes
commissioner if the deleted user owned a league. The last platform owner and
the only manager in a league must assign a successor or delete the league
before deleting their account. Commissioners may rename only these unmanaged
teams. Eligible trades requested from an unmanaged team complete immediately,
still subject to the usual roster and airing checks. The hourly database job
fully deletes unmanaged teams and their team-specific history after the
season-finale week is complete and the airing lock has passed. Confirm the
cleanup-deleted-account-teams job exists in Supabase Cron.
Run supabase/auto-create-week-dances-and-photo-diagnostics.sql in full in
Supabase SQL Editor after the existing automation and wiki-score-order SQL.
It repairs Week 5 immediately, enables/wakes its dance-info job, and returns
the competitive-card count (11 with the current cast). It leaves the existing
wiki tag, judge order and other selected automation modes intact. It does not
complete a week, overwrite manual details, or download completed-week photos.
Cards are now created in the database when the previous week is completed,
or when the next week is added after completion. Creation runs at transaction
end so that newly eliminated couples are excluded. Existing competitive cards
are preserved; legitimate multiple-dance weeks are not restricted to one dance
per couple. Only eligible upcoming weeks are seeded, and a season finale ends
the chain. The owner-only prepare_week_competitive_dances(p_week_id) RPC is
an idempotent website repair path that returns all competitive rows for the week.
The website merges this response by ID rather than inserting its own cards.
Wiki dance types, songs/artists and scores remain visible after their first valid retrieval. Confirmation needs two distinct consecutive matching runs, per field. Changes restart that field's count; missing data breaks the streak; manual entries are protected. Once both dance type and song are confirmed, a couple is omitted from pre-show targets; once scores are confirmed, it is omitted from live-show targets. The SQL preserves the existing importer and wiki score-order wrapper while making missing JSON arrays safe.
The updated scripts/Automations/dwts-photos.py reports timeline/match counts,
date/processed/ambiguous skips, download attempts, empty downloads, non-image
files, upload counts, pending couples and classified downloader warnings.
No raw downloader output, authentication cookies or tokens are included.
dwts-worker.py adds GitHub reconciliation counts and logs these diagnostics;
the database saves them in each photo run's summary, and Score Desk displays
them under Automation. Empty downloads never mark a couple's photos complete.
Both scripts must also be updated on the automation server; no new secrets or
Edge Function deployment are needed.
Tests: npm run check:automation, Python unit tests in scripts/tests, and
node scripts/check-week-automation-db.mjs mirrorball-automation-test-20261009.
The database test accepts only an explicitly named disposable Docker container
and never accepts a live database URL.