Dugout Value
Baseball research on pitching, hitting, and projections, plus tools for comparing players, contracts, and trade value.
翻譯準備中
Second-screen NFL fantasy companion for Sleeper and ESPN
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Windows desktop and laptop overlay for NFL Sundays. One Electron app: a glanceable companion board and a compact always-on-top HUD over the game stream on this PC. Same live data, no backend. Visual language is Sunday Tape — ice / lime / alert on near-black, not gold Booth. Google TV and the LAN overlay are shelved.
Sleeper uses the official read-only HTTP API. ESPN access is unofficial, uses your own login, and is for personal companion use only.
The public marketing site is a separate Next.js app in site/. It is not part of the Electron build.檢視完整 README ↗Windows desktop and laptop overlay for NFL Sundays. One Electron app: a glanceable companion board and a compact always-on-top HUD over the game stream on this PC. Same live data, no backend. Visual language is Sunday Tape — ice / lime / alert on near-black, not gold Booth. Google TV and the LAN overlay are shelved.
Sleeper uses the official read-only HTTP API. ESPN access is unofficial, uses your own login, and is for personal companion use only.
The public marketing site is a separate Next.js app in site/. It is not part of the Electron build.
cd site
npm install
npm run dev
npm install
npm start
Preseason, no live games, or need screenshots? Replay is a demo mode: a scripted Week 3 Sunday with six fake friend-group leagues. Start it with npm run replay, npm run replay:capture, or by launching Sideline with --sideline-replay. The pinned frame is Friday Night Gridiron: Maya's Ice Box 98.4 vs Owen's Hash Marks 91.2, Est. win% 62/38. Four Sleeper and two ESPN boards, short benches, a moving scoring tape with an injury, a waiver, and a trade, plus the NFL ticker. From there the slate plays out: halftime ends, early games go final, and the late window kicks off. A small Replay chip sits in the top bar while Replay is on. Quit and relaunch Sideline normally to use your real leagues.
From a dev checkout (power-user escape hatch):
npm run replay # SIDELINE_REPLAY=1: Replay armed, plays out from the pinned frame
npm run replay:capture # also SIDELINE_REPLAY_HOLD=1: holds the pinned frame for marketing stills
Replay is fixture-only. It never calls a sports-data API, never touches ESPN cookies or your Sleeper username (sign-in, sign-out, and league edits are refused while it's armed), and its slug league ids are stripped from settings on the next live launch.
Connect is a hub with two peer cards: ESPN and Sleeper. Shortcuts and update settings sit in a quiet footer on the hub. How-to copy is on each path.
Open the Sleeper card and type your username (not email, not password). Sideline calls GET /v1/user/{username}, then lists your NFL leagues for the current league_season. Every league is checked; uncheck any you do not want, then Add selected. Sideline opens Boards after that first add. Remove a league from the hub, or Sign out of Sleeper.
Open the ESPN card and Sign in with ESPN. Sideline clears the previous in-app ESPN session, then an in-app window opens ESPN's own login (2FA/OTP included). Sideline never sees or stores your password. After you sign in, it reads espn_s2 and SWID from the persist:espn session partition on this machine and attaches them to later GETs.
Those cookies expire (often after a few weeks). When they do, sign in again the same way — leftover cookie names from a dead session cannot skip the login window. The hub card shows Needs re-login.
League discovery is best-effort and unofficial. The path shows a checklist (all on). Uncheck unwanted leagues, then Add selected. If a league is missing, open Advanced and paste the numeric league ID from the ESPN fantasy URL (.../football/league?leagueId=XXXX).
Public leagues sometimes work with no cookies. Private leagues need espn_s2 + SWID. Remove a league from the hub, or Sign out of ESPN.
Ctrl+Shift+O (Cmd+Shift+O on Mac). Turning HUD on opens Overlay Studio: five placements, click-select a team frame or the ticker, then nudge that block’s position/size. The edge control slides the studio to the window border and pulls it back out. Preset 4 stacks both teams on the same side. Cycle the HUD onto the next monitor with Ctrl+Shift+M. [ / ] cycle leagues in the companion. All of these are editable under Connect → Keyboard shortcuts.POS | NAME | PTS grid. Score ticks flash lime +N or alert red -N on the pts cell, then settle. Save over Preset N keeps the current layout on that slot.setIgnoreMouseEvents(true, { forward: true })). Edit restores the mouse.http://127.0.0.1:7333/overlay (port increments if 7333 is taken)docs/design/hud.md for occupied zones.Defaults (Windows-first; Ctrl is Cmd on Mac via CommandOrControl):
| Action | Default |
|---|---|
| Toggle HUD | Ctrl+Shift+O |
| Cycle HUD to next display | Ctrl+Shift+M |
| Next league | Ctrl+Shift+] |
| Previous league | Ctrl+Shift+[ |
| Edit HUD on the overlay | Ctrl+Shift+E |
Chorded shortcuts are global (work even when Sideline is not focused). A binding without a modifier only works while Sideline is focused. Connect → Keyboard shortcuts to change or reset them; Sideline will not bind the same key to two actions. One monitor: cycle-display is a no-op and shows a brief status.
Companion still has O (HUD), E (Studio panel), and Esc (close Studio) on the Scoreboard.
Google TV and the LAN overlay are shelved. There is no Play Store app, no Wi-Fi toggle, and no pairing code. Sideline is a Windows desktop and laptop overlay. The overlay server binds 127.0.0.1 only. OBS Browser Source stays http://127.0.0.1:7333/overlay with no token. A saved “Allow devices on this Wi-Fi” setting is turned off on upgrade and its token is cleared, so the network does not reopen if that work returns later. The Android project and pairing code stay in the tree behind TV_OVERLAY_ENABLED in src/shared/tvOverlay.ts.
Windows is the first-class overlay target. macOS uses type: 'panel', setAlwaysOnTop(..., 'screen-saver'), setVisibleOnAllWorkspaces({ visibleOnFullScreen: true }), and accessory activation policy so it can sit above fullscreen video. That last setting hides the Dock icon; use the tray icon to show the companion.
~30s idle, ~3s start-to-start when an NFL game is actually in on ESPN's public scoreboard or a kickoff on that scoreboard is within ~10 minutes. A Friday, Saturday, or Sunday-morning window with no game near stays idle. The Thu–Mon calendar window is only the fallback when the scoreboard cannot be reached (every host failing for ~5 minutes, or not fetched yet on launch). ESPN fantasy uses the unofficial lm-api-reads JSON views (not HTML scrape); live ticks request mMatchupScore / mLiveScoring for the current week, with mTeam cached from discovery or a one-time parallel fetch. Selected + pinned scoring GETs start before the public NFL scoreboard download (~251KB); that download is cached 10s so a 3s scoring tick does not re-download it, and it does not block the LEAGUES list. Other boards reuse the last snapshot for ~30s, and those snapshots persist to disk for 12 hours so a restart can paint the LEAGUES grid immediately. Sleeper matchups refresh every tick on the hot path; rosters/users and league lists cache ~5 min in memory, and persist to disk for 12 hours so a restart can overlay live /matchups without waiting on those GETs. Player map is daily (30s timeout on first download). Stay well under Sleeper's 1000 req/min guidance. A 429, an HTML 403 edge block, or three 5xx in a row pauses requests to that host (honoring Retry-After, otherwise 10s doubling to 5 min); the HUD keeps the last scores and shows a quiet "ESPN slow, holding last scores" (or Sleeper) line until a request succeeds.
Yahoo, pick'em, DFS, betting, odds, moneylines, sportsbook UI, chat, drafts, and any write actions (lineups, waivers).
Windows is the first packaging target: a one-click NSIS installer. macOS can wait.
On a Windows machine:
npm install
npm run build:win
The setup exe lands at dist/Sideline-Setup.exe. It is a per-user install (no Administrator prompt), creates a desktop shortcut and a Start menu entry named Sideline, and does not need to be code-signed to run. That command never uploads a GitHub Release (--publish never).
Because the build is unsigned, Windows SmartScreen will likely show Windows protected your PC. Choose More info → Run anyway. Expected for a personal unsigned .exe. Authenticode signing is an optional follow-up so that warning goes away; it is not required for private use and is not part of this updater work.
Companion chrome uses the locked broadcast S (src/renderer/assets/broadcast-s.svg, window/tray build/broadcast-s.png). Windows taskbar/shortcut and Mac packaging (build/icon.png / icon.svg / icon.ico / icon.icns) are that same solid lime #B6FF3B S on black. scripts/generate-app-icon.py derives those from build/broadcast-s.svg. After npm run build:win (or a reinstall), Windows icon cache can keep the old taskbar/shortcut ICO until it refreshes.
npm run build:mac is wired and uses build/icon.icns, with notarization off. Run that on a Mac when you want a .dmg; it is not the current goal. macOS auto-update is out of scope until the app is signed/notarized.
Packaged builds flip Electron fuses (electron-builder.yml), including cookie encryption. That encryption is one-way: after you install a fused build, sign in to ESPN again. npm start and electron-vite preview are not fused.
Installed Windows builds check public GitHub Releases (ketheridge7/sideline) via electron-updater. People running the installed app do not need a GitHub token. npm start / electron-vite never talks to the updater.
Connect → Check for updates shows Checking, Up to date, Downloading N%, Ready (restart), or an error. Packaged builds also check a few seconds after the window is ready, then every 4 hours, and download in the background with no dialogs. When the download finishes, a banner counts down 10 seconds and relaunches through a silent NSIS install (quitAndInstall(true, true)). Later waits until the next quit. npm start does not check; it says updates are available in the installed app.
main.version in package.json (semver).vX.Y.Z to match that version, and push the tag — or on Windows run npm run build:win:publish.electron-builder uploads into it (releaseType: draft). Uploading the installer and blockmap at the same time used to create a second release when none existed yet. https://github.com/ketheridge7/sideline/releases/latest/download/Sideline-Setup.exe stays valid because the installer name does not include the version. electron-updater still picks the file from path and sha512 in that release's latest.yml.stagingPercentage into latest.yml when SIDELINE_STAGING_PERCENTAGE is set, then gh release edit --draft=false publishes it. The job fails unless exactly one release for the tag contains Sideline-Setup.exe, Sideline-Setup.exe.blockmap, and latest.yml.During the NFL season, do not push a v* tag Thursday through Monday (Eastern).
Set repository variable SIDELINE_STAGING_PERCENTAGE to an integer from 1 to 99 before a risky tag. The release workflow writes that into latest.yml. electron-updater then offers the build only to that share of installs (a stable per-machine roll). Leave the variable unset for a full rollout.
Tag workflow (.github/workflows/release.yml) runs npm run typecheck and npm test, creates the draft, builds NSIS on windows-latest, stamps latest.yml, then publishes. It maps GITHUB_TOKEN to GH_TOKEN for electron-builder and gh (contents: write on the same repo). No extra secret. A local npm run build:win:publish also uploads into a draft and does not publish it. Pull requests run the same typecheck and test job (.github/workflows/ci.yml).
Uploading assets needs GitHub auth. Checking/downloading updates does not.
GITHUB_TOKEN (already provided). Do not add a PAT to the workflow.gh auth login, or set a GH_TOKEN env var to a PAT that can create releases, then npm run build:win:publish. electron-builder reads GH_TOKEN. Never commit it, and never bake it into the app.npm run build:win still builds the installer without publishing.