1) Machine-wide Playwright / playwright-mcp audit outside `/mnt/rpi/repos/ballbox`

Commands run:
```bash
pwd

which playwright || true
which playwright-mcp || true
which node || true
which npm || true
which pnpm || true
which npx || true

npm root -g 2>/dev/null || true

for d in /home/sebas ~/.config ~/.cache ~/.local ~/.local/bin ~/.npm-global /mnt/rpi/repos; do
  [ -e "$d" ] && echo "--- $d" && find "$d" -maxdepth 3 \
    \( -iname '*playwright*' -o -iname '*mcp*' -o -iname '*harness*' -o -iname '*e2e*' -o -iname '*snapshot*' -o -iname '*export*' \) \
    2>/dev/null | sed 's#^#  #'
done

find /home/sebas ~/.config ~/.cache ~/.local /mnt/rpi/repos \
  -path /mnt/rpi/repos/ballbox -prune -o -name package.json -print 2>/dev/null | sed 's#^#  #' | head -500

printf '=== PATH ===\n%s\n' "$PATH"

for c in playwright playwright-mcp node npm pnpm npx; do
  if command -v "$c" >/dev/null 2>&1; then
    p=$(command -v "$c")
    echo "--- $c => $p"
    ls -l "$p"
    readlink -f "$p" || true
  fi
done

npm ls -g --depth=2 2>/dev/null | rg -n "playwright|mcp" || true

ls -la ~/.local/bin 2>/dev/null || true
ls -la ~/.npm-global/bin 2>/dev/null || true

find /home/sebas ~/.config ~/.cache ~/.local -maxdepth 5 \
  \( -iname '*playwright*' -o -iname 'playwright.config.*' -o -iname '*playwright-mcp*' \) \
  2>/dev/null | sort
```

Observed environment:
```text
pwd
/home/sebas
```

PATH:
```text
/mnt/rpi/pi-config/bin:/home/sebas/.local/bin:/home/sebas/.opencode/bin:/home/sebas/.npm-global/bin:/home/sebas/.local/bin:/home/sebas/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games
```

CLI availability:
```text
which playwright          -> not found
which playwright-mcp      -> not found
which node                -> /usr/bin/node
which npm                 -> /usr/bin/npm
which pnpm                -> /home/sebas/.local/bin/pnpm
which npx                 -> /usr/bin/npx
```

Resolved binaries:
```text
node => /usr/bin/node
npm  => /usr/bin/npm -> /usr/lib/node_modules/npm/bin/npm-cli.js
pnpm => /home/sebas/.local/bin/pnpm -> /usr/lib/node_modules/corepack/dist/pnpm.js
npx  => /usr/bin/npx -> /usr/lib/node_modules/npm/bin/npx-cli.js
```

Global npm root:
```text
/home/sebas/.npm-global/lib/node_modules
```

Global npm packages matching Playwright/MCP:
```text
none
```

User-local bins:
```text
~/.local/bin
  claude
  pnpm
  pnpx
  yarn
  yarnpkg

~/.npm-global/bin
  bird
  clawdbot
  clawdhub
  codex
  pi
  pnpm
  pnpx
```

Key findings outside `ballbox`:
```text
/home/sebas/.agents/opencode-harness-install
/home/sebas/opencode-harness
/mnt/rpi/repos/opencode-harness
/mnt/rpi/repos/FelipeVersion/tests/e2e
/mnt/rpi/repos/FelipeVersion/node_modules/playwright
/mnt/rpi/repos/FelipeVersion/node_modules/playwright-core
/mnt/rpi/repos/FelipeVersion/node_modules/@playwright
/mnt/rpi/repos/FelipeVersion/playwright.config.ts
```

Not found machine-wide:
- no `playwright` CLI on PATH
- no `playwright-mcp` CLI on PATH
- no global Playwright install under `~/.npm-global`
- no machine-wide reusable Playwright harness under `~/.config`, `~/.cache`, `~/.local`
- no standalone shared Playwright browser cache/install surfaced in searched dirs

Global/reusable pattern actually present:
- repo-local Playwright use exists, not machine-wide
- likely reusable non-Playwright harness candidates:
  - `/home/sebas/opencode-harness`
  - `/mnt/rpi/repos/opencode-harness`
- but from this recon they are just harness-named dirs, not proven Playwright suites
- only concrete Playwright test suites found outside `ballbox`:
  - `/mnt/rpi/repos/FelipeVersion/tests/e2e`
  - `/mnt/rpi/repos/FelipeVersion/playwright.config.ts`

Concrete recommendation: minimal durable setup
- Do not install Playwright globally unless you need a shared CLI for ad hoc runs.
- Do not depend on `playwright-mcp` being globally present; current machine state does not have it.
- Keep browser automation harness repo-local and versioned.
- Best durable location for a shared personal harness:
  - `/mnt/rpi/repos/browser-harness`
- If you want it tied to machine config/ops instead:
  - `/mnt/rpi/pi-config/browser-harness`
- Prefer `/mnt/rpi/repos/browser-harness` because:
  - easier git/versioning
  - avoids mixing executable test assets into machine config
  - can hold app-agnostic smoke tests and helper scripts
  - easier to run independently from any one product repo

Recommended minimal structure:
```text
/mnt/rpi/repos/browser-harness/
  package.json
  playwright.config.ts
  tests/
    smoke/
      local-web.spec.ts
      local-player-shell.spec.ts
      nginx-routing.spec.ts
  scripts/
    install-browsers.sh
  README.md
  .env.example
```

Recommended package baseline:
```json
{
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "codegen": "playwright codegen",
    "install:browsers": "playwright install chromium"
  },
  "devDependencies": {
    "@playwright/test": "^1.58.2"
  }
}
```

Recommended config baseline:
- Chromium only
- headless by default
- baseURL via env
- screenshots/traces on failure
- no Firefox/WebKit unless needed
- explicit artifact dir

Recommended first durable tests:
1. local HTTP smoke against `http://127.0.0.1`
2. nginx route smoke against key internal services
3. generic video/cache/offline harness page if you want reusable runtime checks across repos
4. auth/session smoke for admin pages only where stable and worthwhile

Recommendation on `playwright-mcp`:
- current machine audit says it is not installed or exposed on PATH
- if needed, install it in the dedicated harness repo or as a dedicated global tool later
- do not assume existing machine-wide setup

File pointers from audit:
- `/home/sebas/opencode-harness`
- `/mnt/rpi/repos/opencode-harness`
- `/mnt/rpi/repos/FelipeVersion/playwright.config.ts`
- `/mnt/rpi/repos/FelipeVersion/tests/e2e`

Checklist for task 1:
- [ ] Decide shared harness home: `/mnt/rpi/repos/browser-harness`
- [ ] Create repo-local Playwright project there
- [ ] Install only `@playwright/test`
- [ ] Run `playwright install chromium`
- [ ] Add 2–4 machine-local smoke specs
- [ ] Keep app-specific flows inside each app repo
- [ ] Do not rely on global `playwright` or `playwright-mcp`
- [ ] Optionally add wrapper script in `/mnt/rpi/pi-config/bin/` to run shared harness quickly

2) `/mnt/rpi/repos/ballbox` OPS-2 context, player runtime, snapshot/export tooling

Commands run:
```bash
cd /mnt/rpi/repos/ballbox

ls -l docs/streetcast-parity.md docs/next-steps-queue.md PLAN.md

rg -n --hidden -S "streetcast|player|runtime|snapshot|export|playwright|device|parity" . \
  --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' | head -1000

jq '{name, scripts, devDependencies, dependencies}' package.json

find . -path './node_modules' -prune -o -path './.next' -prune -o \
  \( -ipath '*player*' -o -ipath '*tvs*' -o -ipath '*runtime*' -o -ipath '*snapshot*' -o -ipath '*export*' \) \
  -print | sort | head -500

find tests/e2e -maxdepth 3 -type f | sort

rg -n --hidden -S "snapshot|export|manifest|heartbeat|impression|device|adb|android|tv" . \
  --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' | head -1200

rg -n "Export parity snapshot|parity snapshot|download.*json|blob|CacheStorage|offline|impression queue|heartbeat" \
  app components lib tests/e2e scripts --glob '!node_modules'

rg -n "export default.*player|/player/\\[screenId\\]|screenId" app components lib --glob '!node_modules' | head -400
```

Docs read:
- `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`
- `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
- `/mnt/rpi/repos/ballbox/PLAN.md`
- `/mnt/rpi/repos/ballbox/playwright.config.ts`
- `/mnt/rpi/repos/ballbox/package.json`

OPS-2 queue state:
File: `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
```md
### OPS-2 TVs runtime parity closure
- Priority: P0
- Status: ready
- Depends on: none
- Scope:
  - manually verify real-device CacheStorage/object-URL behavior on `/player/[screenId]`
  - update `docs/streetcast-parity.md` with concrete evidence
- Progress (2026-04-14): runtime now exports a JSON parity snapshot from `/player/[screenId]` to standardize evidence capture; remaining work is one real-device run and doc evidence entry.
```

Streetcast parity status:
File: `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`

Already covered:
- manifest polling
- sequential creative playback
- local media cache
- background preload
- offline continuity
- impression retry queue
- browser automation parity

Intentionally different:
- Ballbox APIs instead of Streetcast endpoints
- production TV runtime path is `/player/[screenId]`
- Ballbox Postgres scheduling is source of truth

Still pending:
- real-device verification before calling migration complete
- missing proof is hardware/browser behavior for CacheStorage + object-URL playback outside desktop automation
- on-screen `Export parity snapshot` action exists for standardized evidence capture

Manual real-device check already specified in doc:
1. open `/player/screen_001`
2. verify diagnostics:
   - `Playback: cached`
   - `Active source: blob`
   - `Cache API: available`
   - `Cached creatives: 1` or higher
   - `Network: online`
3. confirm video loops without visible reload gap
4. disconnect network
5. verify:
   - `Network: offline`
   - `Playback: cached`
   - `Active source: blob`
6. stay offline past one refresh interval, verify `Manifest refresh: failed`
7. reconnect, verify:
   - `Network: online`
   - queued impressions return to `0`
   - heartbeat timestamp advances again
8. capture:
   - photo/video
   - browser version / device model
   - screen id
   - exported JSON snapshot online and offline
   - deviations

Relevant implementation file pointers

Runtime route:
- `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`

Runtime React player:
- `/mnt/rpi/repos/ballbox/components/screen-player.tsx`

Shared runtime core:
- `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-runtime.ts`

Backend runtime/data helpers:
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-runtime.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-source.ts`

API routes used by runtime:
- `/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/health/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/screens/route.ts`

Contracts/invariants:
- `/mnt/rpi/repos/ballbox/lib/contracts/tvs.ts`
- `/mnt/rpi/repos/ballbox/lib/contracts/tvs-invariants.ts`
- `/mnt/rpi/repos/ballbox/lib/contracts/admin-tvs.ts`

Admin/runtime ops:
- `/mnt/rpi/repos/ballbox/components/tvs-admin-client.tsx`
- `/mnt/rpi/repos/ballbox/app/admin/screens/page.tsx`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-operations.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-screens.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-tv-devices.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-campaigns.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-creatives.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-assignments.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-mappers.ts`

Snapshot/export tooling:
- export button rendered in:
  - `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`
- parity snapshot feature references detected in:
  - `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`
  - `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
  - `/mnt/rpi/repos/ballbox/PLAN.md`

E2E/browser automation files:
- `/mnt/rpi/repos/ballbox/playwright.config.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/player-offline.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/player-runtime-side-effects.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/tvs-offline.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/tvs-runtime-side-effects.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/tvs-admin.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/support/playwright-global-cleanup.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/support/cleanup-playwright-db.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/support/test-fixtures.ts`

Playwright setup inside repo:
File: `/mnt/rpi/repos/ballbox/playwright.config.ts`
- test dir: `./tests/e2e`
- Chromium only
- baseURL: `http://127.0.0.1:3010`
- `webServer.command`: `pnpm dev`
- `reuseExistingServer: true`
- env:
  - `BALLBOX_ADMIN_PASSWORD=""`
  - `ATC_PUBLIC_API_BASE_URL="http://127.0.0.1:3010/api/atc-public"`
  - `ATC_PUBLIC_VALIDATE="false"`

Package scripts:
File: `/mnt/rpi/repos/ballbox/package.json`
```json
{
  "test:e2e": "playwright test",
  "test:contracts": "vitest run tests/contracts",
  "test:unit": "vitest run tests/unit",
  "test:integration": "vitest run tests/integration --passWithNoTests",
  "dev": "next dev --port 3010"
}
```

What is already implemented

From docs + code references:
- lightweight TV runtime served at `/player/[screenId]`
- runtime diagnostics on-screen
- manifest polling against internal Ballbox API
- sequential creative playback
- browser cache / cached blob playback path
- background preload/media warming
- last-good-manifest retention
- offline continuity path
- impression retry queue in browser
- heartbeat reporting
- impression reporting
- screen/admin CRUD and campaign/creative/assignment CRUD
- schedule-window filtering in runtime source/manifest generation
- strict Zod-backed contracts for TV APIs
- Playwright automation for offline/blob and side-effect checks
- one-click parity snapshot JSON export from runtime
- seeded test data for `screen_001`, `bb-device-*`, campaigns/creatives/assignments in `prisma/seed-data.ts`

Concrete evidence from file map:
- cached blob/object URL logic:
  - `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
- manifest fetch:
  - `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts`
- heartbeat POST:
  - `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts`
- impression POST:
  - `/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts`
- runtime route with export UI:
  - `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`
- offline/browser tests:
  - `/mnt/rpi/repos/ballbox/tests/e2e/player-offline.spec.ts`
  - `/mnt/rpi/repos/ballbox/tests/e2e/tvs-offline.spec.ts`
- side-effect tests:
  - `/mnt/rpi/repos/ballbox/tests/e2e/player-runtime-side-effects.spec.ts`
  - `/mnt/rpi/repos/ballbox/tests/e2e/tvs-runtime-side-effects.spec.ts`

What can be automated locally now
- start app locally with:
```bash
cd /mnt/rpi/repos/ballbox
pnpm dev
```

- run browser-level parity checks:
```bash
cd /mnt/rpi/repos/ballbox
pnpm test:e2e
```

- run only relevant OPS-2/browser specs:
```bash
cd /mnt/rpi/repos/ballbox
pnpm playwright test tests/e2e/player-offline.spec.ts
pnpm playwright test tests/e2e/player-runtime-side-effects.spec.ts
pnpm playwright test tests/e2e/tvs-offline.spec.ts
pnpm playwright test tests/e2e/tvs-runtime-side-effects.spec.ts
```

- validate contracts around TV APIs/runtime helpers:
```bash
cd /mnt/rpi/repos/ballbox
pnpm test:contracts
```

- seed/reset local DB state for known runtime fixtures:
```bash
cd /mnt/rpi/repos/ballbox
pnpm db:push
pnpm db:seed
```

- clean Playwright-created DB fixtures:
```bash
cd /mnt/rpi/repos/ballbox
dotenv -e .env.local -- tsx tests/e2e/support/cleanup-playwright-db.ts
```

- inspect seed/runtime entities:
  - `prisma/seed-data.ts`
  - `prisma/seed.ts`

What still requires a real device
Per `docs/streetcast-parity.md` and `PLAN.md`, still not closed by local automation:
- actual constrained-device browser behavior for `CacheStorage`
- actual constrained-device object-URL video playback behavior
- proof that cached blob playback survives real network loss on target TV/browser
- proof that playback keeps running through a failed manifest refresh on device
- proof that heartbeat/impression queues recover after reconnect on device
- collection of real evidence:
  - photos/video
  - device model
  - browser version
  - exported parity snapshots
  - doc update with results

Main contradiction/risk to note
- local Playwright tests already prove desktop Chromium parity path
- project docs explicitly say migration is still not complete because that proof is not enough for target TV hardware/browser
- so OPS-2 is not an implementation task now; it is an evidence-collection and validation task on real hardware

Concrete checklist

Machine-wide Playwright setup checklist:
- [ ] Accept there is no current machine-wide `playwright` or `playwright-mcp`
- [ ] Do not rely on global install for durable use
- [ ] Create shared harness at `/mnt/rpi/repos/browser-harness`
- [ ] Add Chromium-only Playwright config
- [ ] Add machine-local smoke specs
- [ ] Keep Ballbox-specific runtime tests inside `/mnt/rpi/repos/ballbox/tests/e2e`

Ballbox OPS-2 checklist:
- [ ] Review runtime route: `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`
- [ ] Review shared runtime core: `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
- [ ] Review backend manifest/heartbeat/impression paths:
  - `/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts`
- [ ] Seed local DB:
  ```bash
  cd /mnt/rpi/repos/ballbox && pnpm db:push && pnpm db:seed
  ```
- [ ] Run local parity automation:
  ```bash
  cd /mnt/rpi/repos/ballbox && pnpm test:e2e
  ```
- [ ] On real TV/browser open:
  ```text
  /player/screen_001
  ```
- [ ] While online verify diagnostics:
  - [ ] Playback: cached
  - [ ] Active source: blob
  - [ ] Cache API: available
  - [ ] Cached creatives >= 1
  - [ ] Network: online
- [ ] Export online parity snapshot JSON
- [ ] Disconnect network
- [ ] Verify:
  - [ ] Network: offline
  - [ ] Playback: cached
  - [ ] Active source: blob
- [ ] Wait past manifest refresh interval
- [ ] Verify `Manifest refresh: failed` without playback stopping
- [ ] Export offline parity snapshot JSON
- [ ] Reconnect network
- [ ] Verify:
  - [ ] Network: online
  - [ ] queued impressions return to 0
  - [ ] heartbeat advances
- [ ] Capture photo/video + device/browser metadata
- [ ] Update:
  - `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`
  - `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
  - `/mnt/rpi/repos/ballbox/PLAN.md`

Most relevant file pointers
```text
/mnt/rpi/repos/ballbox/docs/streetcast-parity.md
/mnt/rpi/repos/ballbox/docs/next-steps-queue.md
/mnt/rpi/repos/ballbox/PLAN.md
/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts
/mnt/rpi/repos/ballbox/components/screen-player.tsx
/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts
/mnt/rpi/repos/ballbox/lib/tvs-runtime.ts
/mnt/rpi/repos/ballbox/lib/tvs-backend-runtime.ts
/mnt/rpi/repos/ballbox/lib/tvs-source.ts
/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts
/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts
/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts
/mnt/rpi/repos/ballbox/tests/e2e/player-offline.spec.ts
/mnt/rpi/repos/ballbox/tests/e2e/player-runtime-side-effects.spec.ts
/mnt/rpi/repos/ballbox/tests/e2e/tvs-offline.spec.ts
/mnt/rpi/repos/ballbox/tests/e2e/tvs-runtime-side-effects.spec.ts
/mnt/rpi/repos/ballbox/playwright.config.ts
/mnt/rpi/repos/ballbox/prisma/seed-data.ts
/mnt/rpi/repos/ballbox/prisma/seed.ts
```

[User]
Task: Repeat the two recon tasks yourself and provide full detailed output:
1) Audit machine-wide Playwright/playwright-mcp setup outside /mnt/rpi/repos/ballbox. Check common dirs under /home/sebas, ~/.config, ~/.cache, ~/.local, and available CLIs. Find any existing reusable harness/test suite or global patterns. Recommend minimal durable setup with own tests and where it should live.
2) Inspect /mnt/rpi/repos/ballbox for OPS-2 context. Read docs/streetcast-parity.md, docs/next-steps-queue.md, PLAN.md, and locate /player runtime code plus any snapshot/export tooling. Summarize what is already implemented, what can be automated locally, and what still requires a real device.
Return file pointers and a concrete checklist.

[Assistant]
bash

set -e
printf '=== pwd ===\n'; pwd
printf '\n=== which playwright / playwright-mcp / node / npm / pnpm / npx ===\n'; which playwright || true; which playwright-mcp || true; which node || true; which npm || true; which pnpm || true; which npx || true
printf '\n=== npm globals top ===\n'; npm root -g 2>/dev/null || true
printf '\n=== list likely dirs ===\n'; for d in /home/sebas ~/.config ~/.cache ~/.local ~/.local/bin ~/.npm-global /mnt/rpi/repos; do [ -e "$d" ] && echo "--- $d" && find "$d" -maxdepth 3 \( -iname '*playwright*' -o -iname '*mcp*' -o -iname '*harness*' -o -iname '*e2e*' -o -iname '*snapshot*' -o -iname '*export*' \) 2>/dev/null | sed 's#^#  #'; done
printf '\n=== piped package.json hits ===\n'; find /home/sebas ~/.config ~/.cache ~/.local /mnt/rpi/repos -path /mnt/rpi/repos/ballbox -prune -o -name package.json -print 2>/dev/null | sed 's#^#  #' | head -500

set -e
cd /mnt/rpi/repos/ballbox
printf '=== repo root ===\n'; pwd
printf '\n=== target docs ===\n'; ls -l docs/streetcast-parity.md docs/next-steps-queue.md PLAN.md
printf '\n=== locate player/runtime/snapshot/export refs ===\n'; rg -n --hidden -S "streetcast|player|runtime|snapshot|export|playwright|device|parity" . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' | head -1000

[toolResult]
  /mnt/rpi/repos/ballbox/tests/e2e
  /mnt/rpi/repos/ballbox/node_modules/@playwright
  /mnt/rpi/repos/ballbox/playwright.config.ts

=== piped package.json hits ===
  /home/sebas/.opencode/node_modules/multipasta/dist/esm/package.json
  /home/sebas/.opencode/node_modules/multipasta/package.json
  /home/sebas/.opencode/node_modules/shebang-command/package.json
  /home/sebas/.opencode/node_modules/which/package.json
  /home/sebas/.opencode/node_modules/ini/package.json
  /home/sebas/.opencode/node_modules/kubernetes-types/package.json
  /home/sebas/.opencode/node_modules/@standard-schema/spec/package.json
  /home/sebas/.opencode/node_modules/msgpackr/package.json
  /home/sebas/.opencode/node_modules/@opencode-ai/sdk/package.json
  /home/sebas/.opencode/node_modules/@opencode-ai/plugin/package.json
  /home/sebas/.opencode/node_modules/path-key/package.json
  /home/sebas/.opencode/node_modules/detect-libc/package.json
  /home/sebas/.opencode/node_modules/zod/mini/package.json
  /home/sebas/.opencode/node_modules/zod/v4-mini/package.json
  /home/sebas/.opencode/node_modules/zod/locales/package.json
  /home/sebas/.opencode/node_modules/zod/v4/mini/package.json
  /home/sebas/.opencode/node_modules/zod/v4/locales/package.json
  /home/sebas/.opencode/node_modules/zod/v4/core/package.json
  /home/sebas/.opencode/node_modules/zod/v4/package.json
  /home/sebas/.opencode/node_modules/zod/v4/classic/package.json
  /home/sebas/.opencode/node_modules/zod/v3/package.json
  /home/sebas/.opencode/node_modules/zod/package.json
  /home/sebas/.opencode/node_modules/fast-check/lib/cjs/package.json
  /home/sebas/.opencode/node_modules/fast-check/package.json
  /home/sebas/.opencode/node_modules/shebang-regex/package.json
  /home/sebas/.opencode/node_modules/uuid/package.json
  /home/sebas/.opencode/node_modules/effect/package.json
  /home/sebas/.opencode/node_modules/node-gyp-build-optional-packages/package.json
  /home/sebas/.opencode/node_modules/isexe/package.json
  /home/sebas/.opencode/node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64/package.json
  /home/sebas/.opencode/node_modules/yaml/browser/package.json
  /home/sebas/.opencode/node_modules/yaml/package.json
  /home/sebas/.opencode/node_modules/toml/package.json
  /home/sebas/.opencode/node_modules/msgpackr-extract/package.json
  /home/sebas/.opencode/node_modules/find-my-way-ts/dist/esm/package.json
  /home/sebas/.opencode/node_modules/find-my-way-ts/QueryString/package.json
  /home/sebas/.opencode/node_modules/find-my-way-ts/package.json
  /home/sebas/.opencode/node_modules/pure-rand/lib/esm/package.json
  /home/sebas/.opencode/node_modules/pure-rand/package.json
  /home/sebas/.opencode/node_modules/cross-spawn/package.json
  /home/sebas/.opencode/package.json
  /home/sebas/.bun/install/cache/braces@3.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/tr46@0.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/zod@3.24.2@@@1/package.json
  /home/sebas/.bun/install/cache/arctic@2.3.4@@@1/package.json
  /home/sebas/.bun/install/cache/fill-range@7.1.1@@@1/package.json
  /home/sebas/.bun/install/cache/web-tree-sitter@0.24.5@@@1/package.json
  /home/sebas/.bun/install/cache/bash-language-server@5.6.0@@@1/package.json
  /home/sebas/.bun/install/cache/@one-ini/wasm@0.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/@standard-schema/spec@1.0.0-7b1dbbf11ca1d290@@@1/package.json
  /home/sebas/.bun/install/cache/.tmp/.99a7f7a3fb633bee-00000001.plugin/package.json
  /home/sebas/.bun/install/cache/hono@4.12.8@@@1/dist/types/package.json
  /home/sebas/.bun/install/cache/hono@4.12.8@@@1/dist/cjs/package.json
  /home/sebas/.bun/install/cache/hono@4.12.8@@@1/package.json
  /home/sebas/.bun/install/cache/to-regex-range@5.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.2.27@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.3.7@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.3.3@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.3.7@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.3.0@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.2.27@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.3.3@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.3.0@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-textdocument@1.0.12@@@1/lib/esm/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-textdocument@1.0.12@@@1/package.json
  /home/sebas/.bun/install/cache/@nodelib/fs.walk@1.2.8@@@1/package.json
  /home/sebas/.bun/install/cache/@nodelib/fs.stat@2.0.5@@@1/package.json
  /home/sebas/.bun/install/cache/@nodelib/fs.scandir@2.1.5@@@1/package.json
  /home/sebas/.bun/install/cache/brace-expansion@2.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/commander@13.1.0@@@1/package.json
  /home/sebas/.bun/install/cache/is-number@7.0.0@@@1/package.json
  /home/sebas/.bun/install/cache/aws4fetch@1.0.20@@@1/package.json
  /home/sebas/.bun/install/cache/jose@5.9.6@@@1/dist/node/esm/package.json
  /home/sebas/.bun/install/cache/jose@5.9.6@@@1/dist/browser/package.json
  /home/sebas/.bun/install/cache/jose@5.9.6@@@1/package.json
  /home/sebas/.bun/install/cache/micromatch@4.0.8@@@1/package.json
  /home/sebas/.bun/install/cache/fuzzy-search@3.2.1@@@1/package.json
  /home/sebas/.bun/install/cache/fast-glob@3.3.3@@@1/package.json
  /home/sebas/.bun/install/cache/editorconfig@2.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-protocol@3.17.2@@@1/package.json
  /home/sebas/.bun/install/cache/reusify@1.1.0@@@1/package.json
  /home/sebas/.bun/install/cache/picomatch@2.3.2@@@1/package.json
  /home/sebas/.bun/install/cache/pyright@1.1.408@@@1/package.json
  /home/sebas/.bun/install/cache/turndown@7.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/balanced-match@1.0.2@@@1/package.json
  /home/sebas/.bun/install/cache/is-glob@4.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/semver@7.7.4@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-types@3.17.2@@@1/package.json
  /home/sebas/.bun/install/cache/webidl-conversions@3.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/is-extglob@2.1.1@@@1/package.json
  /home/sebas/.bun/install/cache/run-parallel@1.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/glob-parent@5.1.2@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-jsonrpc@8.0.2@@@1/package.json
  /home/sebas/.bun/install/cache/queue-microtask@1.2.3@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/crypto@1.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/binary@1.0.0@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/encoding@1.1.0@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/encoding@0.4.1@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/asn1@1.0.0@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/jwt@0.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/minimatch@10.0.1@@@1/dist/esm/package.json
  /home/sebas/.bun/install/cache/minimatch@10.0.1@@@1/dist/commonjs/package.json
  /home/sebas/.bun/install/cache/minimatch@10.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver@8.0.2@@@1/package.json
  /home/sebas/.bun/install/cache/@openauthjs/openauth@0.4.3@@@1/package.json
  /home/sebas/.bun/install/cache/@mixmark-io/domino@2.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/fastq@1.20.1@@@1/package.json
  /home/sebas/.bun/install/cache/merge2@1.4.1@@@1/package.json
  /home/sebas/.bun/install/cache/node-fetch@2.7.0@@@1/package.json
  /home/sebas/.bun/install/cache/opencode-anthropic-auth@0.0.13@@@1/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/mini/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4-mini/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/locales/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/mini/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/locales/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/core/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/classic/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v3/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/package.json
  /home/sebas/.bun/install/cache/whatwg-url@5.0.0@@@1/package.json
  /home/sebas/.config/opencode/node_modules/multipasta/dist/esm/package.json
  /home/sebas/.config/opencode/node_modules/multipasta/package.json
  /home/sebas/.config/opencode/node_modules/shebang-command/package.json
  /home/sebas/.config/opencode/node_modules/which/package.json
  /home/sebas/.config/opencode/node_modules/ini/package.json
  /home/sebas/.config/opencode/node_modules/kubernetes-types/package.json
  /home/sebas/.config/opencode/node_modules/@standard-schema/spec/package.json
  /home/sebas/.config/opencode/node_modules/msgpackr/package.json
  /home/sebas/.config/opencode/node_modules/@opencode-ai/sdk/package.json
  /home/sebas/.config/opencode/node_modules/@opencode-ai/plugin/package.json
  /home/sebas/.config/opencode/node_modules/path-key/package.json
  /home/sebas/.config/opencode/node_modules/detect-libc/package.json
  /home/sebas/.config/opencode/node_modules/zod/mini/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4-mini/package.json
  /home/sebas/.config/opencode/node_modules/zod/locales/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/mini/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/locales/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/core/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/classic/package.json
  /home/sebas/.config/opencode/node_modules/zod/v3/package.json
  /home/sebas/.config/opencode/node_modules/zod/package.json
  /home/sebas/.config/opencode/node_modules/fast-check/lib/cjs/package.json
  /home/sebas/.config/opencode/node_modules/fast-check/package.json
  /home/sebas/.config/opencode/node_modules/shebang-regex/package.json
  /home/sebas/.config/opencode/node_modules/uuid/package.json
  /home/sebas/.config/opencode/node_modules/effect/package.json
  /home/sebas/.config/opencode/node_modules/node-gyp-build-optional-packages/package.json
  /home/sebas/.config/opencode/node_modules/isexe/package.json
  /home/sebas/.config/opencode/node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64/package.json
  /home/sebas/.config/opencode/node_modules/yaml/browser/package.json
  /home/sebas/.config/opencode/node_modules/yaml/package.json
  /home/sebas/.config/opencode/node_modules/toml/package.json
  /home/sebas/.config/opencode/node_modules/msgpackr-extract/package.json
  /home/sebas/.config/opencode/node_modules/find-my-way-ts/dist/esm/package.json
  /home/sebas/.config/opencode/node_modules/find-my-way-ts/QueryString/package.json
  /home/sebas/.config/opencode/node_modules/find-my-way-ts/package.json
  /home/sebas/.config/opencode/node_modules/pure-rand/lib/esm/package.json
  /home/sebas/.config/opencode/node_modules/pure-rand/package.json
  /home/sebas/.config/opencode/node_modules/cross-spawn/package.json
  /home/sebas/.config/opencode/package.json
  /home/sebas/.local/share/opencode/bin/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/source-map/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/partial-json/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/jws/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yoctocolors/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/y18n/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/color-name/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ansi-regex/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cli-highlight/node_modules/chalk/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cli-highlight/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/data-uri-to-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/extract-zip/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minipass/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minipass/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minipass/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/has-flag/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/safe-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/supports-color/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fast-xml-parser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/type/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/syntax/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/compiler/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/parser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/errors/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/value/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/system/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@sinclair/typebox/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ws/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/gaxios/build/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/gaxios/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/parse5-htmlparser2-tree-adapter/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/gcp-metadata/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/std-env/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/koffi/src/cnoke/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/koffi/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ajv-formats/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/marked/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ip-address/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ansi-styles/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/mime-db/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fast-xml-builder/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ieee754/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ts-algebra/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/json-schema-to-ts/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/protobufjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/netmask/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/google-auth-library/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@tootallnate/quickjs-emscripten/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/color-convert/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fd-slicer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/jwa/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/balanced-match/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/balanced-match/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/balanced-match/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yauzl/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/thenify/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/graceful-fs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/escodegen/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/glob/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/glob/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/glob/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yargs/helpers/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yargs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ms/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/end-of-stream/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@types/node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@types/yauzl/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@types/retry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@types/mime-types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/retry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/long/umd/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/long/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/get-stream/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/eventstream-codec/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/property-provider/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-uri-escape/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/eventstream-serde-browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/middleware-content-length/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/node-config-provider/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-utf8/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-endpoints/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-defaults-mode-browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/signature-v4/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-body-length-browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/eventstream-serde-config-resolver/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/smithy-client/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/querystring-parser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/fetch-http-handler/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/middleware-endpoint/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/middleware-serde/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/protocol-http/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/middleware-retry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/invalid-dependency/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/url-parser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-defaults-mode-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/uuid/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/is-array-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/eventstream-serde-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/querystring-builder/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/config-resolver/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-config-provider/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-stream/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/hash-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-hex-encoding/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/shared-ini-file-loader/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/service-error-classification/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/eventstream-serde-universal/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-retry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-body-length-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/credential-provider-imds/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/middleware-stack/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-base64/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-middleware/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/node-http-handler/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/core/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@smithy/util-buffer-from/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/pac-proxy-agent/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cliui/node_modules/ansi-regex/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cliui/node_modules/strip-ansi/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cliui/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/get-east-asian-width/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/node-fetch/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/get-uri/node_modules/data-uri-to-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/get-uri/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/esprima/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/openai/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/web-streams-polyfill/ponyfill/es6/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/web-streams-polyfill/ponyfill/es2018/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/web-streams-polyfill/ponyfill/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/web-streams-polyfill/es6/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/web-streams-polyfill/es2018/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/web-streams-polyfill/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/require-directory/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/strnum/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/p-retry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/hosted-git-info/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fast-deep-equal/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/json-schema-traverse/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/thenify-all/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/degenerator/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/http-proxy-agent/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ecdsa-sig-formatter/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/extend/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/mini/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v4-mini/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/locales/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v4/mini/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v4/locales/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v4/core/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v4/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v4/classic/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/v3/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/undici/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/pi-tui/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/jiti/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/pi-agent-core/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/pi-ai/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mariozechner/clipboard/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/sha256-js/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/sha256-browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/crc32/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/util/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/bignumber.js/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/debug/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/smart-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/file-type/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fetch-blob/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/bowser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/base64-js/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/chalk/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/uuid/dist/cjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/uuid/dist/cjs-browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/uuid/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/path-scurry/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/path-scurry/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/path-scurry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/string-width/node_modules/ansi-regex/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/string-width/node_modules/strip-ansi/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/string-width/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/pac-resolver/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ajv/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/socks/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/formdata-polyfill/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/tslib/modules/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/tslib/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/brace-expansion/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/brace-expansion/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/brace-expansion/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/strtok3/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/lru-cache/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/lru-cache/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/lru-cache/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/proxy-agent/node_modules/lru-cache/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/proxy-agent/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/highlight.js/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/middleware-host-header/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/util-user-agent-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/nested-clients/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/util-endpoints/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/middleware-logger/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/util-user-agent-browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/token-providers/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/region-config-resolver/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/util-locate-window/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/middleware-user-agent/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/util-format-url/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/xml-builder/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/core/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws-sdk/middleware-recursion-detection/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yargs-parser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/is-fullwidth-code-point/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod-to-json-schema/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod-to-json-schema/dist/cjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/zod-to-json-schema/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/pump/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/node-domexception/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/parse5/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yaml/browser/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yaml/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/signal-exit/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/require-from-string/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@aws/lambda-invoke-store/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@babel/runtime/helpers/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@babel/runtime/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/esutils/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ast-types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/mime-types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/buffer-equal-constant-time/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/proper-lockfile/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/packages/mistralai-gcp/examples/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/packages/mistralai-gcp/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/packages/mistralai-azure/examples/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/packages/mistralai-azure/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/tests/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/examples/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@mistralai/mistralai/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/base64/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/path/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/inquire/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/utf8/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/float/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/eventemitter/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/aspromise/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/fetch/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/codegen/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@protobufjs/pool/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/undici-types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@borewit/text-codec/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/once/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/google-logging-utils/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/agent-base/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/wrap-ansi/node_modules/ansi-regex/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/wrap-ansi/node_modules/strip-ansi/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/wrap-ansi/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/uint8array-extras/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minimatch/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minimatch/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minimatch/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/proxy-from-env/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@google/genai/node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@google/genai/web/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@google/genai/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@anthropic-ai/sdk/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fast-uri/benchmark/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/fast-uri/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/estraverse/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/escalade/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/pend/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@tokenizer/token/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@tokenizer/inflate/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/basic-ftp/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/json-bigint/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/@silvia-odwyer/photon-node/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/object-assign/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/wrappy/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/strip-ansi/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/mz/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/https-proxy-agent/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/path-expression-matcher/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/diff/libcjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/diff/libesm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/diff/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/emoji-regex/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/buffer-crc32/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ignore/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/token-types/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/any-promise/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/socks-proxy-agent/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/get-caller-file/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/custom-provider-qwen-cli/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/with-deps/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/sandbox/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/custom-provider-anthropic/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/env-paths/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/fdir/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/@isaacs/fs-minipass/dist/esm/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/@isaacs/fs-minipass/dist/commonjs/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/@isaacs/fs-minipass/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/@isaacs/cliui/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/color-name/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/ansi-regex/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/string-width-cjs/node_modules/ansi-regex/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/string-width-cjs/node_modules/strip-ansi/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/string-width-cjs/node_modules/emoji-regex/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/string-width-cjs/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/minipass/dist/esm/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/minipass/dist/commonjs/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/minipass/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/exponential-backoff/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/ip-address/package.json
  /home/sebas/.cache/node/corepack/v1/pnpm/10.33.0/dist/node_modules/shebang-command/package.json

[Showing lines 39-544 of 544 (50.0KB limit). Full output: /tmp/pi-bash-441a3f27d6869dcf.log]

[toolResult]
./tests/contracts/players/backend-modules.test.ts:82:        id: "player_001",
./tests/contracts/players/backend-modules.test.ts:93:  it("creates memberships only when player and club exist without duplicates", async () => {
./tests/contracts/players/backend-modules.test.ts:94:    prisma.player.findUnique
./tests/contracts/players/backend-modules.test.ts:95:      .mockResolvedValueOnce({ id: "player_001" })
./tests/contracts/players/backend-modules.test.ts:97:        id: "player_001",
./tests/contracts/players/backend-modules.test.ts:113:    const { createMembershipInDb } = await import("@/lib/players-backend");
./tests/contracts/players/backend-modules.test.ts:114:    const result = await createMembershipInDb("player_001", { clubId: "club_001" });
./tests/contracts/players/backend-modules.test.ts:118:        playerId: "player_001",
./tests/contracts/players/backend-modules.test.ts:125:        id: "player_001",
./tests/contracts/players/backend-modules.test.ts:140:  it("blocks player deletion when memberships still exist", async () => {
./tests/contracts/players/backend-modules.test.ts:141:    prisma.player.findUnique.mockResolvedValue({
./tests/contracts/players/backend-modules.test.ts:142:      id: "player_001",
./tests/contracts/players/backend-modules.test.ts:146:    const { deletePlayerInDb } = await import("@/lib/players-backend");
./tests/contracts/players/backend-modules.test.ts:147:    const result = await deletePlayerInDb("player_001");
./tests/contracts/players/backend-modules.test.ts:151:      error: "PLAYER_HAS_MEMBERSHIPS",
./tests/contracts/players/backend-modules.test.ts:154:    expect(prisma.player.delete).not.toHaveBeenCalled();
./app/clubs/venues/[venueId]/page.tsx:3:export const dynamic = "force-dynamic";
./app/clubs/venues/[venueId]/page.tsx:11:export default async function ClubVenuePage({ params }: PageProps) {
./app/page.tsx:12:export default function HomePage() {
./tests/contracts/players/players-admin-player-list.test.tsx:6:import { PlayersAdminPlayerList } from "@/components/players-admin-player-list";
./tests/contracts/players/players-admin-player-list.test.tsx:7:import type { BallboxStoreAdminPlayer, BallboxStoreClubListItem } from "@/lib/ballbox-store";
./tests/contracts/players/players-admin-player-list.test.tsx:9:const playerCardSpy = vi.fn();
./tests/contracts/players/players-admin-player-list.test.tsx:11:vi.mock("@/components/players-admin-player-card", () => ({
./tests/contracts/players/players-admin-player-list.test.tsx:12:  PlayersAdminPlayerCard: (props: unknown) => {
./tests/contracts/players/players-admin-player-list.test.tsx:13:    playerCardSpy(props);
./tests/contracts/players/players-admin-player-list.test.tsx:14:    return
;
./tests/contracts/players/players-admin-player-list.test.tsx:18:const players: BallboxStoreAdminPlayer[] = [
./tests/contracts/players/players-admin-player-list.test.tsx:19:  { id: "player_1", name: "Player One", whatsappPhone: null, telegramHandle: null, memberships: [] },
./tests/contracts/players/players-admin-player-list.test.tsx:20:  { id: "player_2", name: "Player Two", whatsappPhone: null, telegramHandle: null, memberships: [] },
./tests/contracts/players/players-admin-player-list.test.tsx:27:function renderPlayerList(overrides?: Partial<ComponentProps<typeof PlayersAdminPlayerList>>) {
./tests/contracts/players/players-admin-player-list.test.tsx:29:    <PlayersAdminPlayerList
./tests/contracts/players/players-admin-player-list.test.tsx:30:      players={players}
./tests/contracts/players/players-admin-player-list.test.tsx:31:      setPlayers={vi.fn()}
./tests/contracts/players/players-admin-player-list.test.tsx:34:      membershipClubIds={{ player_2: "club_1" }}
./tests/contracts/players/players-admin-player-list.test.tsx:46:describe("PlayersAdminPlayerList", () => {
./tests/contracts/players/players-admin-player-list.test.tsx:49:    playerCardSpy.mockReset();
./tests/contracts/players/players-admin-player-list.test.tsx:52:  it("renders the empty state when there are no players", () => {
./tests/contracts/players/players-admin-player-list.test.tsx:53:    renderPlayerList({ players: [] });
./tests/contracts/players/players-admin-player-list.test.tsx:55:    expect(screen.getByText("No players yet.")).toBeTruthy();
./tests/contracts/players/players-admin-player-list.test.tsx:58:  it("forwards each player into the extracted player card seam", () => {
./tests/contracts/players/players-admin-player-list.test.tsx:59:    const setPlayers = vi.fn();
./tests/contracts/players/players-admin-player-list.test.tsx:60:    renderPlayerList({ setPlayers });
./tests/contracts/players/players-admin-player-list.test.tsx:62:    expect(screen.getAllByTestId("player-card")).toHaveLength(2);
./tests/contracts/players/players-admin-player-list.test.tsx:63:    expect(playerCardSpy).toHaveBeenNthCalledWith(
./tests/contracts/players/players-admin-player-list.test.tsx:66:        player: players[0],
./tests/contracts/players/players-admin-player-list.test.tsx:67:        setPlayers,
./tests/contracts/players/players-admin-player-list.test.tsx:69:        membershipClubIds: { player_2: "club_1" },
./tests/contracts/players/players-admin-client-config.test.ts:2:import { emptyPlayerForm, resolveMembershipClubId } from "@/components/players-admin-client-config";
./tests/contracts/players/players-admin-client-config.test.ts:3:import type { BallboxStoreAdminPlayer, BallboxStoreClubListItem } from "@/lib/ballbox-store";
./tests/contracts/players/players-admin-client-config.test.ts:10:const player: BallboxStoreAdminPlayer = {
./tests/contracts/players/players-admin-client-config.test.ts:11:  id: "player_1",
./tests/contracts/players/players-admin-client-config.test.ts:12:  name: "Player One",
./tests/contracts/players/players-admin-client-config.test.ts:24:describe("players-admin-client-config", () => {
./tests/contracts/players/players-admin-client-config.test.ts:25:  it("exports the shared empty player form", () => {
./tests/contracts/players/players-admin-client-config.test.ts:26:    expect(emptyPlayerForm).toEqual({ name: "", whatsappPhone: "", telegramHandle: "" });
./tests/contracts/players/players-admin-client-config.test.ts:30:    expect(resolveMembershipClubId({ player_1: "club_9" }, player, clubs)).toBe("club_9");
./tests/contracts/players/players-admin-client-config.test.ts:34:    expect(resolveMembershipClubId({}, player, clubs)).toBe("club_2");
./tests/contracts/players/players-admin-client-config.test.ts:38:    expect(resolveMembershipClubId({}, { ...player, memberships: [...player.memberships, { id: "membership_2", clubId: "club_2", clubName: "Club Two" }] }, clubs)).toBe("");
./tests/contracts/players/players-admin-player-card.test.tsx:6:import { PlayersAdminPlayerCard } from "@/components/players-admin-player-card";
./tests/contracts/players/players-admin-player-card.test.tsx:7:import type { BallboxStoreAdminPlayer, BallboxStoreClubListItem } from "@/lib/ballbox-store";
./tests/contracts/players/players-admin-player-card.test.tsx:9:const players: BallboxStoreAdminPlayer[] = [
./tests/contracts/players/players-admin-player-card.test.tsx:11:    id: "player_1",
./tests/contracts/players/players-admin-player-card.test.tsx:12:    name: "Player One",
./tests/contracts/players/players-admin-player-card.test.tsx:18:    id: "player_2",
./tests/contracts/players/players-admin-player-card.test.tsx:19:    name: "Player Two",
./tests/contracts/players/players-admin-player-card.test.tsx:31:function renderPlayerCard(overrides?: Partial<ComponentProps<typeof PlayersAdminPlayerCard>>) {
./tests/contracts/players/players-admin-player-card.test.tsx:33:    <PlayersAdminPlayerCard
./tests/contracts/players/players-admin-player-card.test.tsx:34:      player={players[0]}
./tests/contracts/players/players-admin-player-card.test.tsx:35:      setPlayers={vi.fn()}
./tests/contracts/players/players-admin-player-card.test.tsx:50:describe("PlayersAdminPlayerCard", () => {
./tests/contracts/players/players-admin-player-card.test.tsx:55:  it("updates the targeted player name through setPlayers", () => {
./tests/contracts/players/players-admin-player-card.test.tsx:56:    const setPlayers = vi.fn();
./tests/contracts/players/players-admin-player-card.test.tsx:57:    renderPlayerCard({ setPlayers });
./tests/contracts/players/players-admin-player-card.test.tsx:59:    fireEvent.change(screen.getByDisplayValue("Player One"), { target: { value: "Player Renamed" } });
./tests/contracts/players/players-admin-player-card.test.tsx:61:    const update = setPlayers.mock.calls[0]?.[0] as ((value: BallboxStoreAdminPlayer[]) => BallboxStoreAdminPlayer[]) | undefined;
./tests/contracts/players/players-admin-player-card.test.tsx:64:    const nextPlayers = update?.(players);
./tests/contracts/players/players-admin-player-card.test.tsx:65:    expect(nextPlayers?.[0].name).toBe("Player Renamed");
./tests/contracts/players/players-admin-player-card.test.tsx:66:    expect(nextPlayers?.[1].name).toBe("Player Two");
./tests/contracts/players/players-admin-player-card.test.tsx:69:  it("renders the empty memberships state for a player without memberships", () => {
./tests/contracts/players/players-admin-player-card.test.tsx:70:    renderPlayerCard({ player: players[1] });
./lib/club-auth.ts:8:export const CLUB_SESSION_COOKIE = "ballbox_club_session";
./lib/club-auth.ts:12:export function createClubSessionCookieValue(email: string, passwordHash: string) {
./lib/club-auth.ts:16:export function getClubSessionEmail(cookieValue: string | undefined | null) {
./lib/club-auth.ts:20:export function isValidClubSession(cookieValue: string | undefined | null, passwordHash: string | null | undefined) {
./lib/club-auth.ts:24:export function normalizeClubEmailInput(value: string) {
./tests/contracts/players/players-admin-shell.test.tsx:6:import { PlayersAdminShell } from "@/components/players-admin-shell";
./tests/contracts/players/players-admin-shell.test.tsx:7:import { playersAdminShellStyles } from "@/components/players-admin-shell-styles";
./tests/contracts/players/players-admin-shell.test.tsx:8:import type { BallboxStoreAdminPlayer, BallboxStoreClubListItem } from "@/lib/ballbox-store";
./tests/contracts/players/players-admin-shell.test.tsx:11:const createPlayerSectionSpy = vi.fn();
./tests/contracts/players/players-admin-shell.test.tsx:12:const playerListSpy = vi.fn();
./tests/contracts/players/players-admin-shell.test.tsx:14:vi.mock("@/components/players-admin-stats", () => ({
./tests/contracts/players/players-admin-shell.test.tsx:15:  PlayersAdminStats: (props: unknown) => {
./tests/contracts/players/players-admin-shell.test.tsx:17:    return
;
./tests/contracts/players/players-admin-shell.test.tsx:21:vi.mock("@/components/players-admin-create-player-section", () => ({
./tests/contracts/players/players-admin-shell.test.tsx:22:  PlayersAdminCreatePlayerSection: (props: unknown) => {
./tests/contracts/players/players-admin-shell.test.tsx:23:    createPlayerSectionSpy(props);
./tests/contracts/players/players-admin-shell.test.tsx:24:    return
;
./tests/contracts/players/players-admin-shell.test.tsx:28:vi.mock("@/components/players-admin-player-list", () => ({
./tests/contracts/players/players-admin-shell.test.tsx:29:  PlayersAdminPlayerList: (props: unknown) => {
./tests/contracts/players/players-admin-shell.test.tsx:30:    playerListSpy(props);
./tests/contracts/players/players-admin-shell.test.tsx:31:    return
;
./tests/contracts/players/players-admin-shell.test.tsx:35:const players: BallboxStoreAdminPlayer[] = [
./tests/contracts/players/players-admin-shell.test.tsx:37:    id: "player_1",
./tests/contracts/players/players-admin-shell.test.tsx:38:    name: "Player One",
./tests/contracts/players/players-admin-shell.test.tsx:49:function renderShell(overrides?: Partial<ComponentProps<typeof PlayersAdminShell>>) {
./tests/contracts/players/players-admin-shell.test.tsx:51:    <PlayersAdminShell
./tests/contracts/players/players-admin-shell.test.tsx:52:      players={players}
./tests/contracts/players/players-admin-shell.test.tsx:53:      setPlayers={vi.fn()}
./tests/contracts/players/players-admin-shell.test.tsx:55:      message="Player created"
./tests/contracts/players/players-admin-shell.test.tsx:56:      playerForm={{ name: "Player One", whatsappPhone: "", telegramHandle: "" }}
./tests/contracts/players/players-admin-shell.test.tsx:57:      setPlayerForm={vi.fn()}
./tests/contracts/players/players-admin-shell.test.tsx:59:      membershipClubIds={{ player_1: "club_1" }}
./tests/contracts/players/players-admin-shell.test.tsx:67:describe("PlayersAdminShell", () => {
./tests/contracts/players/players-admin-shell.test.tsx:71:    createPlayerSectionSpy.mockReset();
./tests/contracts/players/players-admin-shell.test.tsx:72:    playerListSpy.mockReset();
./tests/contracts/players/players-admin-shell.test.tsx:76:    const setPlayers = vi.fn();
./tests/contracts/players/players-admin-shell.test.tsx:77:    const setPlayerForm = vi.fn();
./tests/contracts/players/players-admin-shell.test.tsx:82:      setPlayers,
./tests/contracts/players/players-admin-shell.test.tsx:84:      playerForm: { name: "Player Two", whatsappPhone: "", telegramHandle: "" },
./tests/contracts/players/players-admin-shell.test.tsx:85:      setPlayerForm,
./tests/contracts/players/players-admin-shell.test.tsx:86:      membershipClubIds: { player_1: "club_9" },
./tests/contracts/players/players-admin-shell.test.tsx:91:    expect(screen.getByTestId("players-admin-stats")).toBeTruthy();
./tests/contracts/players/players-admin-shell.test.tsx:92:    expect(screen.getByTestId("players-admin-create-player-section")).toBeTruthy();
./tests/contracts/players/players-admin-shell.test.tsx:93:    expect(screen.getByTestId("players-admin-player-list")).toBeTruthy();
./tests/contracts/players/players-admin-shell.test.tsx:97:        players,
./tests/contracts/players/players-admin-shell.test.tsx:99:        cardClassName: playersAdminShellStyles.statCard,
./tests/contracts/players/players-admin-shell.test.tsx:103:    expect(createPlayerSectionSpy).toHaveBeenCalledWith(
./tests/contracts/players/players-admin-shell.test.tsx:107:        playerForm: { name: "Player Two", whatsappPhone: "", telegramHandle: "" },
./tests/contracts/players/players-admin-shell.test.tsx:108:        setPlayerForm,
./tests/contracts/players/players-admin-shell.test.tsx:109:        cardClassName: playersAdminShellStyles.sectionCard,
./tests/contracts/players/players-admin-shell.test.tsx:114:    expect(playerListSpy).toHaveBeenCalledWith(
./tests/contracts/players/players-admin-shell.test.tsx:116:        players,
./tests/contracts/players/players-admin-shell.test.tsx:117:        setPlayers,
./tests/contracts/players/players-admin-shell.test.tsx:120:        membershipClubIds: { player_1: "club_9" },
./tests/contracts/players/players-admin-shell.test.tsx:122:        articleCardClassName: playersAdminShellStyles.articleCard,
./tests/contracts/players/players-admin-shell.test.tsx:123:        insetPanelClassName: playersAdminShellStyles.insetPanel,
./tests/contracts/players/players-admin-shell.test.tsx:124:        wellClassName: playersAdminShellStyles.well,
./tests/contracts/players/players-admin-shell.test.tsx:125:        selectClassName: playersAdminShellStyles.select,
./lib/tvs-backend-mappers.ts:10:export function mapScreenLifecycleStatus(status: "ACTIVE" | "INACTIVE") {
./lib/tvs-backend-mappers.ts:14:export function mapInputScreenStatus(status: "active" | "inactive") {
./lib/tvs-backend-mappers.ts:18:export function mapScreenStatus(lastSeen: Date | null) {
./lib/tvs-backend-mappers.ts:23:export function resolveCreativeUrl(url: string) {
./lib/tvs-backend-mappers.ts:32:export function mapTvAdminVenueOption(
./lib/tvs-backend-mappers.ts:48:export function mapTvAdminDevice(tvDevice: {
./lib/tvs-backend-mappers.ts:58:    id: tvDevice.id,
./lib/tvs-backend-mappers.ts:59:    name: tvDevice.name,
./lib/tvs-backend-mappers.ts:60:    location: tvDevice.location,
./lib/tvs-backend-mappers.ts:61:    manifestUrl: tvDevice.manifestUrl,
./lib/tvs-backend-mappers.ts:62:    lastSeen: toIsoOrNull(tvDevice.lastSeen),
./lib/tvs-backend-mappers.ts:63:    linkedScreenId: tvDevice.screen?.id ?? null,
./lib/tvs-backend-mappers.ts:64:    linkedScreenName: tvDevice.screen?.name ?? null,
./lib/tvs-backend-mappers.ts:65:    impressionCount: tvDevice._count.impressions,
./lib/tvs-backend-mappers.ts:69:export function mapTvAdminScreen(
./lib/tvs-backend-mappers.ts:77:    tvDevice: { id: string; name: string; lastSeen: Date | null } | null;
./lib/tvs-backend-mappers.ts:91:    operationalStatus: mapScreenStatus(screen.tvDevice?.lastSeen ?? null),
./lib/tvs-backend-mappers.ts:92:    tvDeviceId: screen.tvDevice?.id ?? null,
./lib/tvs-backend-mappers.ts:93:    tvDeviceName: screen.tvDevice?.name ?? null,
./lib/tvs-backend-mappers.ts:94:    lastSeen: toIsoOrNull(screen.tvDevice?.lastSeen),
./lib/tvs-backend-mappers.ts:99:export function mapTvAdminCampaign(campaign: {
./app/api/classes/session/route.ts:5:export async function POST(request: Request) {
./app/api/classes/session/route.ts:9:export async function GET() {
./tests/contracts/players/use-players-admin-client-state.test.tsx:5:import { emptyPlayerForm } from "@/components/players-admin-client-config";
./tests/contracts/players/use-players-admin-client-state.test.tsx:6:import { usePlayersAdminClientState } from "@/components/use-players-admin-client-state";
./tests/contracts/players/use-players-admin-client-state.test.tsx:7:import type { BallboxStoreAdminPlayer } from "@/lib/ballbox-store";
./tests/contracts/players/use-players-admin-client-state.test.tsx:9:const initialPlayers: BallboxStoreAdminPlayer[] = [
./tests/contracts/players/use-players-admin-client-state.test.tsx:11:    id: "player_1",
./tests/contracts/players/use-players-admin-client-state.test.tsx:12:    name: "Player One",
./tests/contracts/players/use-players-admin-client-state.test.tsx:25:const reloadedPlayers: BallboxStoreAdminPlayer[] = [
./tests/contracts/players/use-players-admin-client-state.test.tsx:27:    id: "player_2",
./tests/contracts/players/use-players-admin-client-state.test.tsx:28:    name: "Player Reloaded",
./tests/contracts/players/use-players-admin-client-state.test.tsx:35:describe("usePlayersAdminClientState", () => {
./tests/contracts/players/use-players-admin-client-state.test.tsx:41:    const { result } = renderHook(() => usePlayersAdminClientState(initialPlayers));
./tests/contracts/players/use-players-admin-client-state.test.tsx:43:    expect(result.current.players).toEqual(initialPlayers);
./tests/contracts/players/use-players-admin-client-state.test.tsx:44:    expect(result.current.playerForm).toBe(emptyPlayerForm);
./tests/contracts/players/use-players-admin-client-state.test.tsx:50:  it("reloads players after a successful mutation", async () => {
./tests/contracts/players/use-players-admin-client-state.test.tsx:55:        json: async () => ({ items: reloadedPlayers }),
./tests/contracts/players/use-players-admin-client-state.test.tsx:59:    const { result } = renderHook(() => usePlayersAdminClientState(initialPlayers));
./tests/contracts/players/use-players-admin-client-state.test.tsx:62:      await result.current.runMutation(async () => {}, "Player updated");
./tests/contracts/players/use-players-admin-client-state.test.tsx:66:      expect(result.current.players).toEqual(reloadedPlayers);
./tests/contracts/players/use-players-admin-client-state.test.tsx:69:    expect(result.current.message).toBe("Player updated");
./tests/contracts/players/use-players-admin-client-state.test.tsx:71:    expect(fetch).toHaveBeenCalledWith("/api/admin/players", { cache: "no-store" });
./tests/contracts/players/use-players-admin-client-state.test.tsx:78:    const { result } = renderHook(() => usePlayersAdminClientState(initialPlayers));
./tests/contracts/players/use-players-admin-client-state.test.tsx:82:        throw new Error("PLAYER_NAME_CONFLICT");
./tests/contracts/players/use-players-admin-client-state.test.tsx:83:      }, "Player updated");
./tests/contracts/players/use-players-admin-client-state.test.tsx:86:    expect(result.current.players).toEqual(initialPlayers);
./tests/contracts/players/use-players-admin-client-state.test.tsx:87:    expect(result.current.message).toBe("Ya existe otro player con ese nombre.");
./app/api/classes/logout/route.ts:4:export async function POST(request: Request) {
./lib/http/payments-route-helpers.ts:5:export async function parsePaymentsJsonBody<TSchema extends z.ZodTypeAny>(request: Request, schema: TSchema) {
./lib/http/payments-route-helpers.ts:9:export function paymentsErrorResponse(
./lib/http/payments-route-helpers.ts:17:export function paymentsInvalidPayloadResponse(error: z.ZodError) {
./lib/http/payments-route-helpers.ts:21:export function paymentsJsonResponse
(body: TBody, status?: number) {
./lib/http/payments-route-helpers.ts:25:export function paymentsForeignKeyErrorResponse(fieldName: string | null | undefined) {
./lib/http/players-route-helpers.ts:2:import { createAdminPlayersErrorResponse, type AdminPlayersErrorCode } from "@/lib/contracts/admin-players";
./lib/http/players-route-helpers.ts:5:export async function parsePlayersJsonBody<TSchema extends z.ZodTypeAny>(request: Request, schema: TSchema) {
./lib/http/players-route-helpers.ts:9:export function playersErrorResponse(
./lib/http/players-route-helpers.ts:10:  error: AdminPlayersErrorCode,
./lib/http/players-route-helpers.ts:14:  return jsonResponse(createAdminPlayersErrorResponse(error, status, details), status);
./lib/http/players-route-helpers.ts:17:export function playersInvalidPayloadResponse(error: z.ZodError) {
./lib/http/players-route-helpers.ts:18:  return playersErrorResponse("INVALID_PAYLOAD", 400, { issues: error.flatten() });
./lib/http/players-route-helpers.ts:21:export function playersJsonResponse
(body: TBody, status?: number) {
./app/api/classes/requests/route.ts:10:export async function POST(request: Request) {
./lib/http/sports-route-helpers.ts:5:export async function parseSportsJsonBody<TSchema extends z.ZodTypeAny>(request: Request, schema: TSchema) {
./lib/http/sports-route-helpers.ts:9:export function sportsErrorResponse(
./lib/http/sports-route-helpers.ts:17:export function sportsInvalidPayloadResponse(error: z.ZodError) {
./lib/http/sports-route-helpers.ts:21:export function sportsJsonResponse
(body: TBody, status?: number) {
./lib/http/network-route-helpers.ts:5:export async function parseNetworkJsonBody<TSchema extends z.ZodTypeAny>(request: Request, schema: TSchema) {
./lib/http/network-route-helpers.ts:9:export function networkErrorResponse(
./lib/http/network-route-helpers.ts:17:export function networkInvalidPayloadResponse(error: z.ZodError) {
./lib/http/network-route-helpers.ts:21:export function networkJsonResponse
(body: TBody, status?: number) {
./lib/http/json.ts:4:export async function parseJsonBody<TSchema extends z.ZodTypeAny>(request: Request, schema: TSchema) {
./lib/http/json.ts:9:export function jsonResponse
(body: TBody, status?: number) {
./app/api/classes/offers/route.ts:5:export async function GET() {
./lib/coach-auth.ts:8:export const COACH_SESSION_COOKIE = "ballbox_coach_session";
./lib/coach-auth.ts:12:export function createCoachSessionCookieValue(email: string, passwordHash: string) {
./lib/coach-auth.ts:16:export function getCoachSessionEmail(cookieValue: string | undefined | null) {
./lib/coach-auth.ts:20:export function isValidCoachSession(cookieValue: string | undefined | null, passwordHash: string | null | undefined) {
./lib/coach-auth.ts:24:export function normalizeCoachEmailInput(value: string) {
./app/api/classes/offers/[offerId]/route.ts:8:export async function GET(request: Request, { params }: { params: Promise<{ offerId: string }> }) {
./lib/players-backend.ts:6:export type BallboxStorePlayer = {
./lib/players-backend.ts:16:export type BallboxStoreAdminPlayer = {
./lib/players-backend.ts:28:const playerMembershipsInclude = {
./lib/players-backend.ts:44:function mapPlayer(player: {
./lib/players-backend.ts:56:}, summaryMap: Map<string, AtcSportclubSummary | null>): BallboxStoreAdminPlayer {
./lib/players-backend.ts:58:    id: player.id,
./lib/players-backend.ts:59:    name: player.name,
./lib/players-backend.ts:60:    whatsappPhone: player.whatsappPhone ?? null,
./lib/players-backend.ts:61:    telegramHandle: player.telegramHandle ?? null,
./lib/players-backend.ts:62:    memberships: player.memberships.map((membership) => ({
./lib/players-backend.ts:76:export async function getPlayersFromDb(): Promise<BallboxStorePlayer[]> {
./lib/players-backend.ts:78:    const players = await prisma.player.findMany({
./lib/players-backend.ts:79:      include: playerMembershipsInclude,
./lib/players-backend.ts:85:    const clubs = players.flatMap((player) => player.memberships.map((membership) => membership.club));
./lib/players-backend.ts:88:    return players.map((player) => ({
./lib/players-backend.ts:89:      id: player.id,
./lib/players-backend.ts:90:      name: player.name,
./lib/players-backend.ts:91:      whatsappPhone: player.whatsappPhone ?? null,
./lib/players-backend.ts:92:      telegramHandle: player.telegramHandle ?? null,
./lib/players-backend.ts:93:      membershipCount: player.memberships.length,
./lib/players-backend.ts:94:      clubIds: player.memberships.map((membership) => membership.club.id),
./lib/players-backend.ts:95:      clubNames: player.memberships.map((membership) => summaryMap.get(membership.club.id)?.name ?? null),
./lib/players-backend.ts:102:    const legacyPlayers = await prisma.$queryRawUnsafe<Array<{ id: string; name: string }>>(
./lib/players-backend.ts:103:      'SELECT "id", "name" FROM "Player" ORDER BY "createdAt" ASC'
./lib/players-backend.ts:105:    const memberships = await prisma.$queryRawUnsafe<Array<{ id: string; playerId: string; clubId: string; atcSportclubId: string | null }>>(
./lib/players-backend.ts:106:      'SELECT m."id", m."playerId", m."clubId", c."atcSportclubId" FROM "Membership" m JOIN "Club" c ON c."id" = m."clubId" ORDER BY m."createdAt" ASC'
./lib/players-backend.ts:111:    const membershipsByPlayerId = memberships.reduce(
./lib/players-backend.ts:113:        const current = map.get(membership.playerId) ?? [];
./lib/players-backend.ts:115:        map.set(membership.playerId, current);
./lib/players-backend.ts:118:      new Map<string, Array<{ id: string; playerId: string; clubId: string; atcSportclubId: string | null }>>()
./lib/players-backend.ts:121:    return legacyPlayers.map((player) => {
./lib/players-backend.ts:122:      const playerMemberships = membershipsByPlayerId.get(player.id) ?? [];
./lib/players-backend.ts:124:        id: player.id,
./lib/players-backend.ts:125:        name: player.name,
./lib/players-backend.ts:128:        membershipCount: playerMemberships.length,
./lib/players-backend.ts:129:        clubIds: playerMemberships.map((membership) => membership.clubId),
./lib/players-backend.ts:130:        clubNames: playerMemberships.map((membership) => summaryMap.get(membership.clubId)?.name ?? null),
./lib/players-backend.ts:136:export async function getAdminPlayersFromDb(): Promise<BallboxStoreAdminPlayer[]> {
./lib/players-backend.ts:137:  const players = await prisma.player.findMany({
./lib/players-backend.ts:138:    include: playerMembershipsInclude,
./lib/players-backend.ts:144:  const clubs = players.flatMap((player) => player.memberships.map((membership) => membership.club));
./lib/players-backend.ts:147:  return players.map((player) => mapPlayer(player, summaryMap));
./lib/players-backend.ts:150:export async function getAdminPlayerByIdFromDb(playerId: string): Promise<BallboxStoreAdminPlayer | null> {
./lib/players-backend.ts:151:  const player = await prisma.player.findUnique({
./lib/players-backend.ts:152:    where: { id: playerId },
./lib/players-backend.ts:153:    include: playerMembershipsInclude,
./lib/players-backend.ts:156:  if (!player) {
./lib/players-backend.ts:160:  const summaryMap = await mapAtcSportclubSummaryByClub(player.memberships.map((membership) => membership.club));
./lib/players-backend.ts:162:  return mapPlayer(player, summaryMap);
./lib/players-backend.ts:165:export async function createPlayerInDb(input: { name: string; whatsappPhone?: string | null; telegramHandle?: string | null }) {
./lib/players-backend.ts:166:  const player = await prisma.player.create({
./lib/players-backend.ts:174:  return getAdminPlayerByIdFromDb(player.id);
./lib/players-backend.ts:177:export async function updatePlayerInDb(
./lib/players-backend.ts:178:  playerId: string,
./lib/players-backend.ts:181:  const existing = await prisma.player.findUnique({
./lib/players-backend.ts:182:    where: { id: playerId },
./lib/players-backend.ts:190:  await prisma.player.update({
./lib/players-backend.ts:191:    where: { id: playerId },
./lib/players-backend.ts:199:  return getAdminPlayerByIdFromDb(playerId);
./lib/players-backend.ts:202:export async function deletePlayerInDb(playerId: string) {
./lib/players-backend.ts:203:  const player = await prisma.player.findUnique({
./lib/players-backend.ts:204:    where: { id: playerId },
./lib/players-backend.ts:212:  if (!player) {
./lib/players-backend.ts:213:    return { ok: false as const, error: "PLAYER_NOT_FOUND" as const };
./lib/players-backend.ts:216:  if (player.memberships.length > 0) {
./lib/players-backend.ts:219:      error: "PLAYER_HAS_MEMBERSHIPS" as const,
./lib/players-backend.ts:220:      membershipCount: player.memberships.length,
./lib/players-backend.ts:224:  await prisma.player.delete({
./lib/players-backend.ts:225:    where: { id: playerId },
./lib/players-backend.ts:231:export async function createMembershipInDb(playerId: string, input: { clubId: string }) {
./lib/players-backend.ts:232:  const [player, club, existingMembership] = await Promise.all([
./lib/players-backend.ts:233:    prisma.player.findUnique({
./lib/players-backend.ts:234:      where: { id: playerId },
./lib/players-backend.ts:243:        playerId,
./lib/players-backend.ts:250:  if (!player) {
./lib/players-backend.ts:251:    return { ok: false as const, error: "PLAYER_NOT_FOUND" as const };
./lib/players-backend.ts:264:      playerId,
./lib/players-backend.ts:269:  const playerWithMemberships = await getAdminPlayerByIdFromDb(playerId);
./lib/players-backend.ts:272:    item: playerWithMemberships!,
./lib/players-backend.ts:276:export async function deleteMembershipInDb(membershipId: string) {
./lib/network-backend-mappers.ts:1:export function mapVenueStatus(status: "ACTIVE" | "INACTIVE") {
./lib/network-backend-mappers.ts:5:export function mapInputVenueStatus(status: "active" | "inactive") {
./lib/tvs-backend-screens.ts:11:export async function createTvScreenInDb(input: {
./lib/tvs-backend-screens.ts:16:  tvDeviceId?: string;
./lib/tvs-backend-screens.ts:25:  if (input.tvDeviceId) {
./lib/tvs-backend-screens.ts:26:    const tvDevice = await prisma.tvDevice.findUnique({
./lib/tvs-backend-screens.ts:27:      where: { id: input.tvDeviceId },
./lib/tvs-backend-screens.ts:35:    if (!tvDevice) return { ok: false as const, error: "TV_DEVICE_NOT_FOUND" as const };
./lib/tvs-backend-screens.ts:36:    if (tvDevice.screen) {
./lib/tvs-backend-screens.ts:37:      return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
./lib/tvs-backend-screens.ts:50:  if (input.tvDeviceId?.trim()) {
./lib/tvs-backend-screens.ts:51:    await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:52:      where: { id: input.tvDeviceId.trim() },
./lib/tvs-backend-screens.ts:62:export async function updateTvScreenInDb(
./lib/tvs-backend-screens.ts:69:    tvDeviceId?: string;
./lib/tvs-backend-screens.ts:75:      tvDevice: {
./lib/tvs-backend-screens.ts:92:  const normalizedTvDeviceId =
./lib/tvs-backend-screens.ts:93:    input.tvDeviceId === undefined ? undefined : input.tvDeviceId.trim().length > 0 ? input.tvDeviceId.trim() : null;
./lib/tvs-backend-screens.ts:95:  if (normalizedTvDeviceId) {
./lib/tvs-backend-screens.ts:96:    const tvDevice = await prisma.tvDevice.findUnique({
./lib/tvs-backend-screens.ts:97:      where: { id: normalizedTvDeviceId },
./lib/tvs-backend-screens.ts:105:    if (!tvDevice) return { ok: false as const, error: "TV_DEVICE_NOT_FOUND" as const };
./lib/tvs-backend-screens.ts:106:    if (tvDevice.screen && tvDevice.screen.id !== screenId) {
./lib/tvs-backend-screens.ts:107:      return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
./lib/tvs-backend-screens.ts:121:  if (input.tvDeviceId !== undefined) {
./lib/tvs-backend-screens.ts:122:    if (existing.tvDevice?.id && existing.tvDevice.id !== normalizedTvDeviceId) {
./lib/tvs-backend-screens.ts:123:      await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:124:        where: { id: existing.tvDevice.id },
./lib/tvs-backend-screens.ts:129:    if (normalizedTvDeviceId) {
./lib/tvs-backend-screens.ts:130:      await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:131:        where: { id: normalizedTvDeviceId },
./lib/tvs-backend-screens.ts:140:export async function deleteTvScreenInDb(screenId: string) {
./lib/tvs-backend-screens.ts:144:      tvDevice: {
./lib/tvs-backend-screens.ts:169:  if (screen.tvDevice?.id) {
./lib/tvs-backend-screens.ts:170:    await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:171:      where: { id: screen.tvDevice.id },
./lib/ballbox-datetime.ts:79:export function formatBallboxDateTime(value: string) {
./lib/ballbox-datetime.ts:84:export function formatBallboxTime(value: string) {
./lib/ballbox-datetime.ts:89:export function formatBallboxShortDateLabel(value: string) {
./app/api/atc-public/availability/sportclubs/[sportclubId]/route.ts:28:      name: "Playwright",
./app/api/atc-public/availability/sportclubs/[sportclubId]/route.ts:34:export async function GET(_request: Request, context: { params: Promise<{ sportclubId: string }> }) {
./lib/coach-students-backend.ts:2:import { getPlayersFromDb, type BallboxStorePlayer } from "@/lib/players-backend";
./lib/coach-students-backend.ts:19:export type BallboxCoachStudentBooking = {
./lib/coach-students-backend.ts:21:  player: BallboxStorePlayer;
./lib/coach-students-backend.ts:41:export type BallboxCoachStudentSummary = {
./lib/coach-students-backend.ts:42:  player: BallboxStorePlayer;
./lib/coach-students-backend.ts:50:export type BallboxCoachStudentsPanel = {
./lib/coach-students-backend.ts:52:    playerCount: number;
./lib/coach-students-backend.ts:59:  players: BallboxStorePlayer[];
./lib/coach-students-backend.ts:129:function mapPlayerFromMemberships(player: {
./lib/coach-students-backend.ts:140:}): BallboxStorePlayer {
./lib/coach-students-backend.ts:142:    id: player.id,
./lib/coach-students-backend.ts:143:    name: player.name,
./lib/coach-students-backend.ts:144:    whatsappPhone: player.whatsappPhone ?? null,
./lib/coach-students-backend.ts:145:    telegramHandle: player.telegramHandle ?? null,
./lib/coach-students-backend.ts:146:    membershipCount: player.memberships.length,
./lib/coach-students-backend.ts:147:    clubIds: player.memberships.map((membership) => membership.club.id),
./lib/coach-students-backend.ts:148:    clubNames: player.memberships.map((membership) => getClubDisplayName(membership.club)),
./lib/coach-students-backend.ts:211:    player: {
./lib/coach-students-backend.ts:239:    player: mapPlayerFromMemberships(args.booking.player),
./lib/coach-students-backend.ts:371:export async function getCoachStudentsPanelFromDb(coachId: string): Promise<BallboxCoachStudentsPanel | null> {
./lib/coach-students-backend.ts:372:  const [context, players, bookings] = await Promise.all([
./lib/coach-students-backend.ts:374:    getPlayersFromDb(),
./lib/coach-students-backend.ts:379:        player: {
./lib/coach-students-backend.ts:426:      player: BallboxStorePlayer;
./lib/coach-students-backend.ts:437:      studentMap.get(booking.player.id) ??
./lib/coach-students-backend.ts:439:        player: booking.player,
./lib/coach-students-backend.ts:460:    studentMap.set(booking.player.id, entry);
./lib/coach-students-backend.ts:465:      player: entry.player,
./lib/coach-students-backend.ts:483:      playerCount: players.length,
./lib/coach-students-backend.ts:492:    players,
./lib/coach-students-backend.ts:511:      player: {
./lib/coach-students-backend.ts:550:export async function createCoachStudentBookingInDb(
./lib/coach-students-backend.ts:553:      playerId: string;
./lib/coach-students-backend.ts:563:  const [context, player] = await Promise.all([
./lib/coach-students-backend.ts:565:    prisma.player.findUnique({
./lib/coach-students-backend.ts:566:      where: { id: input.playerId },
./lib/coach-students-backend.ts:575:  if (!player) {
./lib/coach-students-backend.ts:576:    return { ok: false as const, error: "PLAYER_NOT_FOUND" as const };
./lib/coach-students-backend.ts:636:      playerId: input.playerId,
./lib/coach-students-backend.ts:653:export async function updateCoachStudentPlayerContactInDb(
./lib/coach-students-backend.ts:655:  playerId: string,
./lib/coach-students-backend.ts:670:  const player = await prisma.player.findUnique({
./lib/coach-students-backend.ts:671:    where: { id: playerId },
./lib/coach-students-backend.ts:675:  if (!player) {
./lib/coach-students-backend.ts:676:    return { ok: false as const, error: "PLAYER_NOT_FOUND" as const };
./lib/coach-students-backend.ts:679:  const updated = await prisma.player.update({
./lib/coach-students-backend.ts:680:    where: { id: playerId },
./lib/coach-students-backend.ts:704:    item: mapPlayerFromMemberships(updated),
./lib/coach-students-backend.ts:708:export async function cancelCoachStudentBookingInDb(coachId: string, bookingId: string) {
./lib/tv-heartbeats.ts:11:export function recordTvHeartbeat(screenId: string, timestamp = new Date()) {
./lib/tv-heartbeats.ts:21:export function getTvHeartbeat(screenId: string) {
./lib/tv-heartbeats.ts:25:export function getTvStatusFromLastSeen(lastSeen: string, now = new Date()) {
./app/api/account/session/route.ts:27:export async function POST(request: Request) {
./lib/classes-backend.ts:6:export type BallboxStoreClassAtcSlot = {
./lib/classes-backend.ts:16:export type BallboxStoreClassAtcContext = {
./lib/classes-backend.ts:26:export type BallboxStoreClassOffer = {
./lib/classes-backend.ts:52:export type BallboxStoreClassRequestCreated = {
./lib/classes-backend.ts:60:export type BallboxStoreClassRequestListItem = {
./lib/classes-backend.ts:199:export async function getActiveClassOffersFromDb(): Promise<BallboxStoreClassOffer[]> {
./lib/classes-backend.ts:216:export async function getClassOfferByIdFromDb(offerId: string): Promise<BallboxStoreClassOffer | null> {
./lib/classes-backend.ts:231:export async function createClassRequestInDb(input: {
./lib/classes-backend.ts:331:export async function listClassRequestsByStudentEmailFromDb(studentEmail: string): Promise<BallboxStoreClassRequestListItem[]> {
./lib/network-backend-clubs.ts:5:export type BallboxStoreClubListItem = {
./lib/network-backend-clubs.ts:15:export type BallboxStoreClubDetail = {
./lib/network-backend-clubs.ts:98:export async function getClubListFromDb(): Promise<BallboxStoreClubListItem[]> {
./lib/network-backend-clubs.ts:121:export async function getClubDetailFromDb(clubId: string): Promise<BallboxStoreClubDetail | null> {
./lib/network-backend-clubs.ts:146:export async function getClubsAdminDataFromDb(): Promise<BallboxStoreClubDetail[]> {
./tests/contracts/coaches/students-routes.test.ts:13:const updateCoachStudentPlayerContactInDb = vi.fn();
./tests/contracts/coaches/students-routes.test.ts:21:  updateCoachStudentPlayerContactInDb,
./tests/contracts/coaches/students-routes.test.ts:50:        playerCount: 2,
./tests/contracts/coaches/students-routes.test.ts:57:      players: [
./tests/contracts/coaches/students-routes.test.ts:59:          id: "player_001",
./tests/contracts/coaches/students-routes.test.ts:79:          player: {
./tests/contracts/coaches/students-routes.test.ts:80:            id: "player_001",
./tests/contracts/coaches/students-routes.test.ts:106:          player: {
./tests/contracts/coaches/students-routes.test.ts:107:            id: "player_001",
./tests/contracts/coaches/students-routes.test.ts:138:        player: {
./tests/contracts/coaches/students-routes.test.ts:139:          id: "player_001",
./tests/contracts/coaches/students-routes.test.ts:170:          playerId: "player_001",
./tests/contracts/coaches/students-routes.test.ts:196:          playerId: "player_001",
./tests/contracts/coaches/students-routes.test.ts:227:    updateCoachStudentPlayerContactInDb.mockResolvedValue({
./tests/contracts/coaches/students-routes.test.ts:230:        id: "player_001",
./tests/contracts/coaches/students-routes.test.ts:240:    const { PATCH } = await import("@/app/api/coaches/students/players/[playerId]/route");
./tests/contracts/coaches/students-routes.test.ts:242:      new Request("http://localhost/api/coaches/students/players/player_001", {
./tests/contracts/coaches/students-routes.test.ts:250:      { params: Promise.resolve({ playerId: "player_001" }) }
./app/api/tvs/screens/route.ts:5:export async function GET() {
./lib/shared-secret.ts:6:export function resolveSharedSecret(sources: SharedSecretSource[]) {
./lib/shared-secret.ts:17:export function buildBearerAuthHeaders(secret?: string) {
./lib/shared-secret.ts:27:export function hasValidBearerAuthHeader(
./lib/tvs-backend-runtime.ts:44:export async function getTvSourceFromDb() {
./lib/tvs-backend-runtime.ts:54:        tvDevice: {
./lib/tvs-backend-runtime.ts:104:      location: screen.location ?? screen.tvDevice?.location ?? "Unknown location",
./lib/tvs-backend-runtime.ts:105:      status: mapScreenStatus(screen.tvDevice?.lastSeen ?? null),
./lib/tvs-backend-runtime.ts:106:      lastSeen: screen.tvDevice?.lastSeen?.toISOString() ?? new Date(0).toISOString(),
./lib/tvs-backend-runtime.ts:109:      externalTvDeviceId: screen.tvDevice?.id ?? undefined,
./lib/tvs-backend-runtime.ts:110:      impressionCount: screen.tvDevice?._count.impressions ?? 0,
./lib/tvs-backend-runtime.ts:136:export async function getTvManifestFromDb(screenId: string) {
./lib/tvs-backend-runtime.ts:141:      tvDevice: {
./lib/tvs-backend-runtime.ts:191:export async function recordScreenHeartbeatInDb(screenId: string, timestamp = new Date()) {
./lib/tvs-backend-runtime.ts:196:      tvDevice: {
./lib/tvs-backend-runtime.ts:208:  if (!screen.tvDevice?.id) {
./lib/tvs-backend-runtime.ts:209:    return { ok: false as const, error: "SCREEN_TV_DEVICE_NOT_LINKED" as const };
./lib/tvs-backend-runtime.ts:212:  await prisma.tvDevice.update({
./lib/tvs-backend-runtime.ts:213:    where: { id: screen.tvDevice.id },
./lib/tvs-backend-runtime.ts:226:export async function recordImpressionInDb(screenId: string, creativeId: string) {
./lib/tvs-backend-runtime.ts:231:      tvDevice: {
./lib/tvs-backend-runtime.ts:260:  if (!screen.tvDevice) {
./lib/tvs-backend-runtime.ts:261:    return { ok: false as const, error: "SCREEN_TV_DEVICE_NOT_LINKED" as const, status: 409 };
./lib/tvs-backend-runtime.ts:285:      tvDeviceId: screen.tvDevice.id,
./app/api/tvs/heartbeat/route.ts:9:export async function POST(request: Request) {
./app/api/tvs/heartbeat/route.ts:25:    const status = heartbeat.error === "SCREEN_TV_DEVICE_NOT_LINKED" ? 409 : 404;
./lib/prisma-errors.ts:3:export function isPrismaUniqueConstraintError(error: unknown) {
./lib/prisma-errors.ts:7:export function isPrismaForeignKeyConstraintError(error: unknown) {
./lib/prisma-errors.ts:11:export function getPrismaForeignKeyFieldName(error: unknown) {
./app/api/tvs/impression/route.ts:9:export async function POST(request: Request) {
./lib/ballbox-store.ts:37:  createTvDeviceInDb,
./lib/ballbox-store.ts:38:  deleteTvDeviceInDb,
./lib/ballbox-store.ts:39:  updateTvDeviceInDb,
./lib/ballbox-store.ts:40:} from "@/lib/tvs-backend-tv-devices";
./lib/ballbox-store.ts:52:} from "@/lib/tvs-backend-runtime";
./lib/ballbox-store.ts:55:  createPlayerInDb,
./lib/ballbox-store.ts:57:  deletePlayerInDb,
./lib/ballbox-store.ts:58:  getAdminPlayerByIdFromDb,
./lib/ballbox-store.ts:59:  getAdminPlayersFromDb,
./lib/ballbox-store.ts:60:  getPlayersFromDb,
./lib/ballbox-store.ts:61:  updatePlayerInDb,
./lib/ballbox-store.ts:62:} from "@/lib/players-backend";
./lib/ballbox-store.ts:90:export type { BallboxStoreClubDetail, BallboxStoreClubListItem } from "@/lib/network-backend-clubs";
./lib/ballbox-store.ts:91:export type { BallboxStoreAdminPlayer, BallboxStorePlayer } from "@/lib/players-backend";
./lib/ballbox-store.ts:92:export type {
./lib/ballbox-store.ts:98:export type { BallboxStoreSportsAdminData, BallboxStoreSportsEvent } from "@/lib/sports-backend-events";
./lib/ballbox-store.ts:100:export type BallboxStoreScreen = {
./lib/ballbox-store.ts:110:  externalTvDeviceId?: string;
./lib/ballbox-store.ts:114:export type BallboxStoreCampaign = {
./lib/ballbox-store.ts:123:export type { BallboxStoreSportsCoach } from "@/lib/sports-backend-coaches";
./lib/ballbox-store.ts:125:export type BallboxStoreTvAdminVenueOption = {
./lib/ballbox-store.ts:132:export type BallboxStoreTvAdminTvDevice = {
./lib/ballbox-store.ts:143:export type BallboxStoreTvAdminScreen = {
./lib/ballbox-store.ts:153:  tvDeviceId: string | null;
./lib/ballbox-store.ts:154:  tvDeviceName: string | null;
./lib/ballbox-store.ts:159:export type BallboxStoreTvAdminCreative = {
./lib/ballbox-store.ts:166:export type BallboxStoreTvAdminAssignment = {
./lib/ballbox-store.ts:175:export type BallboxStoreTvAdminCampaign = {
./lib/ballbox-store.ts:187:export {
./lib/ballbox-store.ts:197:  createTvDeviceInDb,
./lib/ballbox-store.ts:207:  deletePlayerInDb,
./lib/ballbox-store.ts:210:  deleteTvDeviceInDb,
./lib/ballbox-store.ts:214:  createPlayerInDb,
./lib/ballbox-store.ts:215:  getAdminPlayerByIdFromDb,
./lib/ballbox-store.ts:216:  getAdminPlayersFromDb,
./lib/ballbox-store.ts:223:  getPlayersFromDb,
./lib/ballbox-store.ts:232:  updatePlayerInDb,
./lib/ballbox-store.ts:237:  updateTvDeviceInDb,
./app/api/tvs/manifest/[screenId]/route.ts:8:export async function GET(
./app/api/tvs/health/route.ts:5:export async function GET() {
./tests/unit/sports/sports-admin-client-config.test.ts:43:  it("exports shared empty forms", () => {
./lib/club-accounts-backend.ts:6:import { getPlayersFromDb } from "@/lib/players-backend";
./lib/club-accounts-backend.ts:8:export type BallboxClubAccountProfile = {
./lib/club-accounts-backend.ts:15:export type BallboxClubAccountRecord = {
./lib/club-accounts-backend.ts:20:export type BallboxClubVenueSummary = {
./lib/club-accounts-backend.ts:25:export type BallboxClubWeeklyRange = {
./lib/club-accounts-backend.ts:32:export type BallboxClubRosterCoach = {
./lib/club-accounts-backend.ts:47:export type BallboxClubVenueConfiguration = {
./lib/club-accounts-backend.ts:77:export type BallboxClubDashboardCoach = {
./lib/club-accounts-backend.ts:88:export type BallboxClubDashboardCoachTableRow = {
./lib/club-accounts-backend.ts:100:export type BallboxClubDashboardRequest = {
./lib/club-accounts-backend.ts:120:export type BallboxClubDashboardManualClassOptions = {
./lib/club-accounts-backend.ts:138:  players: Array<{
./lib/club-accounts-backend.ts:145:export type BallboxClubDashboardCalendarBlock = {
./lib/club-accounts-backend.ts:160:  playerName: string | null;
./lib/club-accounts-backend.ts:165:export type BallboxClubDashboardCalendar = {
./lib/club-accounts-backend.ts:194:export type BallboxClubDashboardAnalytics = {
./lib/club-accounts-backend.ts:216:    playerName: string;
./lib/club-accounts-backend.ts:224:export type BallboxClubDashboardVenue = {
./lib/club-accounts-backend.ts:237:export type BallboxClubDashboard = {
./lib/club-accounts-backend.ts:254:export type BallboxClubManualClassBooking = {
./lib/club-accounts-backend.ts:259:  playerId: string;
./lib/club-accounts-backend.ts:260:  playerName: string;
./lib/club-accounts-backend.ts:261:  playerWhatsappPhone: string | null;
./lib/club-accounts-backend.ts:678:  player: {
./lib/club-accounts-backend.ts:779:        playerName: null,
./lib/club-accounts-backend.ts:806:        playerName: booking.player.name,
./lib/club-accounts-backend.ts:887:    entry.studentIds.add(booking.player.id);
./lib/club-accounts-backend.ts:896:    playerName: booking.player.name,
./lib/club-accounts-backend.ts:1079:export async function getClubByContactEmailFromDb(email: string): Promise<BallboxClubAccountProfile | null> {
./lib/club-accounts-backend.ts:1089:export async function getClubAccountRecordByEmailFromDb(email: string): Promise<BallboxClubAccountRecord | null> {
./lib/club-accounts-backend.ts:1099:export async function getClubByIdFromDb(clubId: string): Promise<BallboxClubAccountProfile | null> {
./lib/club-accounts-backend.ts:1109:export async function createClubAccountInDb(input: { name: string; email: string; passwordHash: string }) {
./lib/club-accounts-backend.ts:1159:export async function getClubRosterFromDb(clubId: string) {
./lib/club-accounts-backend.ts:1193:        player: {
./lib/club-accounts-backend.ts:1221:  const [clubRecord, coaches, invites, players, bookings, studentRequests] = await Promise.all([
./lib/club-accounts-backend.ts:1228:    getPlayersFromDb(),
./lib/club-accounts-backend.ts:1339:    players: players.map((player) => ({
./lib/club-accounts-backend.ts:1340:      id: player.id,
./lib/club-accounts-backend.ts:1341:      name: player.name,
./lib/club-accounts-backend.ts:1342:      whatsappPhone: player.whatsappPhone ?? null,
./lib/club-accounts-backend.ts:1366:export async function getClubVenueDetailFromDb(clubId: string, venueId: string) {
./lib/club-accounts-backend.ts:1383:export async function createClubManualClassInDb(
./lib/club-accounts-backend.ts:1389:    playerId: string;
./lib/club-accounts-backend.ts:1390:    playerWhatsappPhone?: string | null;
./lib/club-accounts-backend.ts:1401:  const [coach, player] = await Promise.all([
./lib/club-accounts-backend.ts:1432:    prisma.player.findUnique({
./lib/club-accounts-backend.ts:1433:      where: { id: input.playerId },
./lib/club-accounts-backend.ts:1450:  if (!player) {
./lib/club-accounts-backend.ts:1451:    return { ok: false as const, error: "PLAYER_NOT_FOUND" as const };
./lib/club-accounts-backend.ts:1527:  const updatedPlayer =
./lib/club-accounts-backend.ts:1528:    input.playerWhatsappPhone !== undefined
./lib/club-accounts-backend.ts:1529:      ? await prisma.player.update({
./lib/club-accounts-backend.ts:1530:          where: { id: player.id },
./lib/club-accounts-backend.ts:1532:            whatsappPhone: normalizeOptionalText(input.playerWhatsappPhone),
./lib/club-accounts-backend.ts:1540:      : player;
./lib/club-accounts-backend.ts:1545:      playerId: player.id,
./lib/club-accounts-backend.ts:1559:      playerId: true,
./lib/club-accounts-backend.ts:1574:      playerId: booking.playerId,
./lib/club-accounts-backend.ts:1575:      playerName: updatedPlayer.name,

[Showing lines 456-1010 of 1010 (50.0KB limit). Full output: /tmp/pi-bash-e6acdba1af99f7d4.log]

[Assistant]
set -e
printf '=== PATH ===\n%s\n' "$PATH"
printf '\n=== resolve cli symlinks ===\n'; for c in playwright playwright-mcp node npm pnpm npx; do if command -v "$c" >/dev/null 2>&1; then p=$(command -v "$c"); echo "--- $c => $p"; ls -l "$p"; readlink -f "$p" || true; fi; done
printf '\n=== npm global packages matching playwright/mcp ===\n'; npm ls -g --depth=2 2>/dev/null | rg -n "playwright|mcp" || true
printf '\n=== local bins ===\n'; ls -la ~/.local/bin 2>/dev/null || true; printf '\n=== npm-global bins ===\n'; ls -la ~/.npm-global/bin 2>/dev/null || true
printf '\n=== broader find under home/config/cache/local for playwright files ===\n'; find /home/sebas ~/.config ~/.cache ~/.local -maxdepth 5 \( -iname '*playwright*' -o -iname 'playwright.config.*' -o -iname '*playwright-mcp*' \) 2>/dev/null | sort

set -e
cd /mnt/rpi/repos/ballbox
printf '=== docs file list ===\n'; ls -l docs/streetcast-parity.md docs/next-steps-queue.md PLAN.md
printf '\n=== player runtime dirs/files ===\n'; find . -path './node_modules' -prune -o -path './.next' -prune -o \( -ipath '*player*' -o -ipath '*tvs*' -o -ipath '*runtime*' -o -ipath '*snapshot*' -o -ipath '*export*' \) -print | sort | head -500
printf '\n=== package scripts and playwright config ===\n'; jq '{name, scripts, devDependencies, dependencies}' package.json
printf '\n=== tests/e2e tree ===\n'; find tests/e2e -maxdepth 3 -type f | sort
printf '\n=== snapshot/export script grep ===\n'; rg -n --hidden -S "snapshot|export|manifest|heartbeat|impression|device|adb|android|tv" . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' | head -1200

[toolResult]
=== PATH ===
/mnt/rpi/pi-config/bin:/home/sebas/.local/bin:/home/sebas/.opencode/bin:/home/sebas/.npm-global/bin:/home/sebas/.local/bin:/home/sebas/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games

=== resolve cli symlinks ===
--- node => /usr/bin/node
-rwxr-xr-x 1 root root 120592136 Jan 12 17:48 /usr/bin/node
/usr/bin/node
--- npm => /usr/bin/npm
lrwxrwxrwx 1 root root 38 Jan 13 14:28 /usr/bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js
/usr/lib/node_modules/npm/bin/npm-cli.js
--- pnpm => /home/sebas/.local/bin/pnpm
lrwxrwxrwx 1 sebas sebas 54 Apr 10 15:16 /home/sebas/.local/bin/pnpm -> ../../../../usr/lib/node_modules/corepack/dist/pnpm.js
/usr/lib/node_modules/corepack/dist/pnpm.js
--- npx => /usr/bin/npx
lrwxrwxrwx 1 root root 38 Jan 13 14:28 /usr/bin/npx -> ../lib/node_modules/npm/bin/npx-cli.js
/usr/lib/node_modules/npm/bin/npx-cli.js

=== npm global packages matching playwright/mcp ===

=== local bins ===
total 8
drwxr-xr-x 2 sebas sebas 4096 Apr 10 15:16 .
drwx------ 5 sebas sebas 4096 Apr 14 12:54 ..
lrwxrwxrwx 1 sebas sebas   46 Jan 15 21:48 claude -> /home/sebas/.local/share/claude/versions/2.1.7
lrwxrwxrwx 1 sebas sebas   54 Apr 10 15:16 pnpm -> ../../../../usr/lib/node_modules/corepack/dist/pnpm.js
lrwxrwxrwx 1 sebas sebas   54 Apr 10 15:16 pnpx -> ../../../../usr/lib/node_modules/corepack/dist/pnpx.js
lrwxrwxrwx 1 sebas sebas   54 Apr 10 15:16 yarn -> ../../../../usr/lib/node_modules/corepack/dist/yarn.js
lrwxrwxrwx 1 sebas sebas   57 Apr 10 15:16 yarnpkg -> ../../../../usr/lib/node_modules/corepack/dist/yarnpkg.js

=== npm-global bins ===
total 12
drwxr-xr-x 2 sebas sebas 4096 Apr 16 16:33 .
drwxr-xr-x 4 sebas sebas 4096 Jan 27 11:48 ..
lrwxrwxrwx 1 sebas sebas   46 Jan 27 22:02 bird -> ../lib/node_modules/@steipete/bird/dist/cli.js
lrwxrwxrwx 1 sebas sebas   42 Feb 16 12:23 clawdbot -> ../lib/node_modules/clawdbot/dist/entry.js
lrwxrwxrwx 1 sebas sebas   44 Jan 28 10:38 clawdhub -> ../lib/node_modules/clawdhub/bin/clawdhub.js
lrwxrwxrwx 1 sebas sebas   46 Mar 23 14:27 codex -> ../lib/node_modules/@openai/codex/bin/codex.js
lrwxrwxrwx 1 sebas sebas   61 Apr 16 16:33 pi -> ../lib/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
lrwxrwxrwx 1 sebas sebas   37 Jan 28 09:09 pnpm -> ../lib/node_modules/pnpm/bin/pnpm.cjs
lrwxrwxrwx 1 sebas sebas   37 Jan 28 09:09 pnpx -> ../lib/node_modules/pnpm/bin/pnpx.cjs

=== broader find under home/config/cache/local for playwright files ===

[toolResult]
Parse(body);
./app/api/tvs/heartbeat/route.ts:15:      createTvsErrorResponse("INVALID_REQUEST", 400, {
./app/api/tvs/heartbeat/route.ts:22:  const heartbeat = await recordScreenHeartbeatInDb(parsedBody.data.screenId);
./app/api/tvs/heartbeat/route.ts:24:  if (!heartbeat.ok) {
./app/api/tvs/heartbeat/route.ts:25:    const status = heartbeat.error === "SCREEN_TV_DEVICE_NOT_LINKED" ? 409 : 404;
./app/api/tvs/heartbeat/route.ts:26:    return NextResponse.json(createTvsErrorResponse(heartbeat.error, status), { status });
./app/api/tvs/heartbeat/route.ts:30:    tvHeartbeatResponseSchema.parse({
./app/api/tvs/heartbeat/route.ts:32:    screenId: heartbeat.screenId,
./app/api/tvs/heartbeat/route.ts:33:    lastSeen: heartbeat.lastSeen,
./lib/http/json.ts:4:export async function parseJsonBody<TSchema extends z.ZodTypeAny>(request: Request, schema: TSchema) {
./lib/http/json.ts:9:export function jsonResponse<TBody>(body: TBody, status?: number) {
./lib/coach-auth.ts:8:export const COACH_SESSION_COOKIE = "ballbox_coach_session";
./lib/coach-auth.ts:12:export function createCoachSessionCookieValue(email: string, passwordHash: string) {
./lib/coach-auth.ts:16:export function getCoachSessionEmail(cookieValue: string | undefined | null) {
./lib/coach-auth.ts:20:export function isValidCoachSession(cookieValue: string | undefined | null, passwordHash: string | null | undefined) {
./lib/coach-auth.ts:24:export function normalizeCoachEmailInput(value: string) {
./app/api/tvs/impression/route.ts:3:  createTvsErrorResponse,
./app/api/tvs/impression/route.ts:4:  tvImpressionRequestSchema,
./app/api/tvs/impression/route.ts:5:  tvImpressionResponseSchema,
./app/api/tvs/impression/route.ts:6:} from "@/lib/contracts/tvs";
./app/api/tvs/impression/route.ts:7:import { reportTvImpression } from "@/lib/tvs-source";
./app/api/tvs/impression/route.ts:9:export async function POST(request: Request) {
./app/api/tvs/impression/route.ts:11:  const parsedBody = tvImpressionRequestSchema.safeParse(body);
./app/api/tvs/impression/route.ts:15:      createTvsErrorResponse("INVALID_REQUEST", 400, {
./app/api/tvs/impression/route.ts:22:  const result = await reportTvImpression(parsedBody.data.screenId, parsedBody.data.creativeId);
./app/api/tvs/impression/route.ts:25:    return NextResponse.json(createTvsErrorResponse(result.error, result.status || 502), {
./app/api/tvs/impression/route.ts:31:    tvImpressionResponseSchema.parse({
./app/api/tvs/impression/route.ts:33:    impressionId: result.impressionId,
./lib/players-backend.ts:6:export type BallboxStorePlayer = {
./lib/players-backend.ts:16:export type BallboxStoreAdminPlayer = {
./lib/players-backend.ts:76:export async function getPlayersFromDb(): Promise<BallboxStorePlayer[]> {
./lib/players-backend.ts:136:export async function getAdminPlayersFromDb(): Promise<BallboxStoreAdminPlayer[]> {
./lib/players-backend.ts:150:export async function getAdminPlayerByIdFromDb(playerId: string): Promise<BallboxStoreAdminPlayer | null> {
./lib/players-backend.ts:165:export async function createPlayerInDb(input: { name: string; whatsappPhone?: string | null; telegramHandle?: string | null }) {
./lib/players-backend.ts:177:export async function updatePlayerInDb(
./lib/players-backend.ts:202:export async function deletePlayerInDb(playerId: string) {
./lib/players-backend.ts:231:export async function createMembershipInDb(playerId: string, input: { clubId: string }) {
./lib/players-backend.ts:276:export async function deleteMembershipInDb(membershipId: string) {
./lib/network-backend-mappers.ts:1:export function mapVenueStatus(status: "ACTIVE" | "INACTIVE") {
./lib/network-backend-mappers.ts:5:export function mapInputVenueStatus(status: "active" | "inactive") {
./tests/contracts/network/use-network-admin-client-forms.test.tsx:26:      result.current.setVenueForms({ club_2: { name: "Venue Two", slug: "venue-two", status: "inactive" } });
./lib/tvs-backend-screens.ts:2:import { mapInputScreenStatus } from "@/lib/tvs-backend-mappers";
./lib/tvs-backend-screens.ts:11:export async function createTvScreenInDb(input: {
./lib/tvs-backend-screens.ts:16:  tvDeviceId?: string;
./lib/tvs-backend-screens.ts:25:  if (input.tvDeviceId) {
./lib/tvs-backend-screens.ts:26:    const tvDevice = await prisma.tvDevice.findUnique({
./lib/tvs-backend-screens.ts:27:      where: { id: input.tvDeviceId },
./lib/tvs-backend-screens.ts:35:    if (!tvDevice) return { ok: false as const, error: "TV_DEVICE_NOT_FOUND" as const };
./lib/tvs-backend-screens.ts:36:    if (tvDevice.screen) {
./lib/tvs-backend-screens.ts:37:      return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
./lib/tvs-backend-screens.ts:50:  if (input.tvDeviceId?.trim()) {
./lib/tvs-backend-screens.ts:51:    await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:52:      where: { id: input.tvDeviceId.trim() },
./lib/tvs-backend-screens.ts:62:export async function updateTvScreenInDb(
./lib/tvs-backend-screens.ts:69:    tvDeviceId?: string;
./lib/tvs-backend-screens.ts:75:      tvDevice: {
./lib/tvs-backend-screens.ts:92:  const normalizedTvDeviceId =
./lib/tvs-backend-screens.ts:93:    input.tvDeviceId === undefined ? undefined : input.tvDeviceId.trim().length > 0 ? input.tvDeviceId.trim() : null;
./lib/tvs-backend-screens.ts:95:  if (normalizedTvDeviceId) {
./lib/tvs-backend-screens.ts:96:    const tvDevice = await prisma.tvDevice.findUnique({
./lib/tvs-backend-screens.ts:97:      where: { id: normalizedTvDeviceId },
./lib/tvs-backend-screens.ts:105:    if (!tvDevice) return { ok: false as const, error: "TV_DEVICE_NOT_FOUND" as const };
./lib/tvs-backend-screens.ts:106:    if (tvDevice.screen && tvDevice.screen.id !== screenId) {
./lib/tvs-backend-screens.ts:107:      return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
./lib/tvs-backend-screens.ts:121:  if (input.tvDeviceId !== undefined) {
./lib/tvs-backend-screens.ts:122:    if (existing.tvDevice?.id && existing.tvDevice.id !== normalizedTvDeviceId) {
./lib/tvs-backend-screens.ts:123:      await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:124:        where: { id: existing.tvDevice.id },
./lib/tvs-backend-screens.ts:129:    if (normalizedTvDeviceId) {
./lib/tvs-backend-screens.ts:130:      await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:131:        where: { id: normalizedTvDeviceId },
./lib/tvs-backend-screens.ts:140:export async function deleteTvScreenInDb(screenId: string) {
./lib/tvs-backend-screens.ts:144:      tvDevice: {
./lib/tvs-backend-screens.ts:169:  if (screen.tvDevice?.id) {
./lib/tvs-backend-screens.ts:170:    await prisma.tvDevice.update({
./lib/tvs-backend-screens.ts:171:      where: { id: screen.tvDevice.id },
./app/api/tvs/manifest/[screenId]/route.ts:3:  createTvsErrorResponse,
./app/api/tvs/manifest/[screenId]/route.ts:4:  tvManifestResponseSchema,
./app/api/tvs/manifest/[screenId]/route.ts:5:} from "@/lib/contracts/tvs";
./app/api/tvs/manifest/[screenId]/route.ts:6:import { getTvManifestFromSource } from "@/lib/tvs-source";
./app/api/tvs/manifest/[screenId]/route.ts:8:export async function GET(
./app/api/tvs/manifest/[screenId]/route.ts:13:  const manifest = await getTvManifestFromSource(screenId);
./app/api/tvs/manifest/[screenId]/route.ts:15:  if (!manifest.ok) {
./app/api/tvs/manifest/[screenId]/route.ts:16:    return NextResponse.json(createTvsErrorResponse(manifest.error, manifest.status), {
./app/api/tvs/manifest/[screenId]/route.ts:17:      status: manifest.status,
./app/api/tvs/manifest/[screenId]/route.ts:22:    tvManifestResponseSchema.parse({
./app/api/tvs/manifest/[screenId]/route.ts:23:      version: manifest.version,
./app/api/tvs/manifest/[screenId]/route.ts:24:      screenId: manifest.screenId,
./app/api/tvs/manifest/[screenId]/route.ts:25:      generatedAt: manifest.generatedAt,
./app/api/tvs/manifest/[screenId]/route.ts:26:      creatives: manifest.creatives,
./lib/ballbox-datetime.ts:79:export function formatBallboxDateTime(value: string) {
./lib/ballbox-datetime.ts:84:export function formatBallboxTime(value: string) {
./lib/ballbox-datetime.ts:89:export function formatBallboxShortDateLabel(value: string) {
./tests/contracts/network/backend-mappers.test.ts:2:import { mapInputVenueStatus, mapVenueStatus } from "@/lib/network-backend-mappers";
./tests/contracts/network/backend-mappers.test.ts:11:    expect(mapInputVenueStatus("active")).toBe("ACTIVE");
./tests/contracts/network/backend-mappers.test.ts:12:    expect(mapInputVenueStatus("inactive")).toBe("INACTIVE");
./app/api/tvs/health/route.ts:2:import { tvHealthResponseSchema } from "@/lib/contracts/tvs";
./app/api/tvs/health/route.ts:3:import { getTvDataSource } from "@/lib/tvs-source";
./app/api/tvs/health/route.ts:5:export async function GET() {
./app/api/tvs/health/route.ts:6:  const source = await getTvDataSource();
./app/api/tvs/health/route.ts:9:    tvHealthResponseSchema.parse({
./app/api/tvs/health/route.ts:11:    module: "tvs",
./tests/contracts/network/network-admin-shell.test.tsx:57:      setVenueForms={vi.fn()}
./tests/contracts/network/network-admin-shell.test.tsx:77:    const setVenueForms = vi.fn();
./tests/contracts/network/network-admin-shell.test.tsx:84:      setVenueForms,
./tests/contracts/network/network-admin-shell.test.tsx:123:        setVenueForms,
./tests/contracts/network/network-admin-club-card.test.tsx:44:      setVenueForms={vi.fn()}
./lib/coach-students-backend.ts:11:type CoachStudentVenueOption = {
./lib/coach-students-backend.ts:19:export type BallboxCoachStudentBooking = {
./lib/coach-students-backend.ts:41:export type BallboxCoachStudentSummary = {
./lib/coach-students-backend.ts:50:export type BallboxCoachStudentsPanel = {
./lib/coach-students-backend.ts:73:  venues: CoachStudentVenueOption[];
./lib/coach-students-backend.ts:371:export async function getCoachStudentsPanelFromDb(coachId: string): Promise<BallboxCoachStudentsPanel | null> {
./lib/coach-students-backend.ts:550:export async function createCoachStudentBookingInDb(
./lib/coach-students-backend.ts:653:export async function updateCoachStudentPlayerContactInDb(
./lib/coach-students-backend.ts:708:export async function cancelCoachStudentBookingInDb(coachId: string, bookingId: string) {
./lib/tv-heartbeats.ts:1:type HeartbeatRecord = {
./lib/tv-heartbeats.ts:9:const heartbeats = new Map<string, HeartbeatRecord>();
./lib/tv-heartbeats.ts:11:export function recordTvHeartbeat(screenId: string, timestamp = new Date()) {
./lib/tv-heartbeats.ts:12:  const heartbeat = {
./lib/tv-heartbeats.ts:15:  } satisfies HeartbeatRecord;
./lib/tv-heartbeats.ts:17:  heartbeats.set(screenId, heartbeat);
./lib/tv-heartbeats.ts:18:  return heartbeat;
./lib/tv-heartbeats.ts:21:export function getTvHeartbeat(screenId: string) {
./lib/tv-heartbeats.ts:22:  return heartbeats.get(screenId) ?? null;
./lib/tv-heartbeats.ts:25:export function getTvStatusFromLastSeen(lastSeen: string, now = new Date()) {
./tests/contracts/network/network-admin-club-list.test.tsx:42:      setVenueForms={vi.fn()}
./app/api/clubs/session/route.ts:17:export async function POST(request: Request) {
./app/api/clubs/club-route-helpers.ts:7:export async function resolveClubFromSession() {
./lib/classes-backend.ts:6:export type BallboxStoreClassAtcSlot = {
./lib/classes-backend.ts:16:export type BallboxStoreClassAtcContext = {
./lib/classes-backend.ts:26:export type BallboxStoreClassOffer = {
./lib/classes-backend.ts:52:export type BallboxStoreClassRequestCreated = {
./lib/classes-backend.ts:60:export type BallboxStoreClassRequestListItem = {
./lib/classes-backend.ts:199:export async function getActiveClassOffersFromDb(): Promise<BallboxStoreClassOffer[]> {
./lib/classes-backend.ts:216:export async function getClassOfferByIdFromDb(offerId: string): Promise<BallboxStoreClassOffer | null> {
./lib/classes-backend.ts:231:export async function createClassRequestInDb(input: {
./lib/classes-backend.ts:331:export async function listClassRequestsByStudentEmailFromDb(studentEmail: string): Promise<BallboxStoreClassRequestListItem[]> {
./app/api/clubs/invites/route.ts:10:export async function POST(request: Request) {
./lib/network-backend-clubs.ts:5:export type BallboxStoreClubListItem = {
./lib/network-backend-clubs.ts:15:export type BallboxStoreClubDetail = {
./lib/network-backend-clubs.ts:98:export async function getClubListFromDb(): Promise<BallboxStoreClubListItem[]> {
./lib/network-backend-clubs.ts:121:export async function getClubDetailFromDb(clubId: string): Promise<BallboxStoreClubDetail | null> {
./lib/network-backend-clubs.ts:146:export async function getClubsAdminDataFromDb(): Promise<BallboxStoreClubDetail[]> {
./lib/shared-secret.ts:6:export function resolveSharedSecret(sources: SharedSecretSource[]) {
./lib/shared-secret.ts:17:export function buildBearerAuthHeaders(secret?: string) {
./lib/shared-secret.ts:27:export function hasValidBearerAuthHeader(
./app/api/clubs/logout/route.ts:4:export async function POST() {
./lib/tvs-backend-runtime.ts:1:import { createEmptyTvManifest } from "@/lib/contracts/tvs";
./lib/tvs-backend-runtime.ts:2:import { filterActiveTvAssignments } from "@/lib/contracts/tvs-invariants";
./lib/tvs-backend-runtime.ts:4:import { mapScreenStatus, resolveCreativeUrl } from "@/lib/tvs-backend-mappers";
./lib/tvs-backend-runtime.ts:19:  return filterActiveTvAssignments(assignments, now).map((assignment) => assignment.campaignId);
./lib/tvs-backend-runtime.ts:38:    filterActiveTvAssignments(assignments, now).flatMap((assignment) =>
./lib/tvs-backend-runtime.ts:44:export async function getTvSourceFromDb() {
./lib/tvs-backend-runtime.ts:46:  const [screens, campaigns, totalImpressions] = await Promise.all([
./lib/tvs-backend-runtime.ts:54:        tvDevice: {
./lib/tvs-backend-runtime.ts:58:                impressions: true,
./lib/tvs-backend-runtime.ts:93:    prisma.impression.count(),
./lib/tvs-backend-runtime.ts:104:      location: screen.location ?? screen.tvDevice?.location ?? "Unknown location",
./lib/tvs-backend-runtime.ts:105:      status: mapScreenStatus(screen.tvDevice?.lastSeen ?? null),
./lib/tvs-backend-runtime.ts:106:      lastSeen: screen.tvDevice?.lastSeen?.toISOString() ?? new Date(0).toISOString(),
./lib/tvs-backend-runtime.ts:109:      externalTvDeviceId: screen.tvDevice?.id ?? undefined,
./lib/tvs-backend-runtime.ts:110:      impressionCount: screen.tvDevice?._count.impressions ?? 0,
./lib/tvs-backend-runtime.ts:129:      totalImpressions,
./lib/tvs-backend-runtime.ts:136:export async function getTvManifestFromDb(screenId: string) {
./lib/tvs-backend-runtime.ts:141:      tvDevice: {
./lib/tvs-backend-runtime.ts:163:  const creatives = filterActiveTvAssignments(screen.assignments, now).flatMap((assignment) =>
./lib/tvs-backend-runtime.ts:178:      manifest: createEmptyTvManifest(screenId),
./lib/tvs-backend-runtime.ts:191:export async function recordScreenHeartbeatInDb(screenId: string, timestamp = new Date()) {
./lib/tvs-backend-runtime.ts:196:      tvDevice: {
./lib/tvs-backend-runtime.ts:208:  if (!screen.tvDevice?.id) {
./lib/tvs-backend-runtime.ts:209:    return { ok: false as const, error: "SCREEN_TV_DEVICE_NOT_LINKED" as const };
./lib/tvs-backend-runtime.ts:212:  await prisma.tvDevice.update({
./lib/tvs-backend-runtime.ts:213:    where: { id: screen.tvDevice.id },
./lib/tvs-backend-runtime.ts:226:export async function recordImpressionInDb(screenId: string, creativeId: string) {
./lib/tvs-backend-runtime.ts:231:      tvDevice: {
./lib/tvs-backend-runtime.ts:260:  if (!screen.tvDevice) {
./lib/tvs-backend-runtime.ts:261:    return { ok: false as const, error: "SCREEN_TV_DEVICE_NOT_LINKED" as const, status: 409 };
./lib/tvs-backend-runtime.ts:283:  const impression = await prisma.impression.create({
./lib/tvs-backend-runtime.ts:285:      tvDeviceId: screen.tvDevice.id,
./lib/tvs-backend-runtime.ts:293:    impressionId: impression.id,
./lib/prisma-errors.ts:3:export function isPrismaUniqueConstraintError(error: unknown) {
./lib/prisma-errors.ts:7:export function isPrismaForeignKeyConstraintError(error: unknown) {
./lib/prisma-errors.ts:11:export function getPrismaForeignKeyFieldName(error: unknown) {
./app/api/clubs/me/route.ts:5:export async function GET() {
./app/api/clubs/accounts/route.ts:16:export async function POST(request: Request) {
./lib/ballbox-store.ts:22:  createTvCampaignInDb,
./lib/ballbox-store.ts:23:  deleteTvCampaignInDb,
./lib/ballbox-store.ts:24:  updateTvCampaignInDb,
./lib/ballbox-store.ts:25:} from "@/lib/tvs-backend-campaigns";
./lib/ballbox-store.ts:27:  createTvAssignmentInDb,
./lib/ballbox-store.ts:28:  deleteTvAssignmentInDb,
./lib/ballbox-store.ts:29:  updateTvAssignmentInDb,
./lib/ballbox-store.ts:30:} from "@/lib/tvs-backend-assignments";
./lib/ballbox-store.ts:32:  createTvCreativeInDb,
./lib/ballbox-store.ts:33:  deleteTvCreativeInDb,
./lib/ballbox-store.ts:34:  updateTvCreativeInDb,
./lib/ballbox-store.ts:35:} from "@/lib/tvs-backend-creatives";
./lib/ballbox-store.ts:37:  createTvDeviceInDb,
./lib/ballbox-store.ts:38:  deleteTvDeviceInDb,
./lib/ballbox-store.ts:39:  updateTvDeviceInDb,
./lib/ballbox-store.ts:40:} from "@/lib/tvs-backend-tv-devices";
./lib/ballbox-store.ts:42:  createTvScreenInDb,
./lib/ballbox-store.ts:43:  deleteTvScreenInDb,
./lib/ballbox-store.ts:44:  updateTvScreenInDb,
./lib/ballbox-store.ts:45:} from "@/lib/tvs-backend-screens";
./lib/ballbox-store.ts:46:import { getTvAdminOperationsDataFromDb } from "@/lib/tvs-backend-operations";
./lib/ballbox-store.ts:48:  getTvManifestFromDb,
./lib/ballbox-store.ts:49:  getTvSourceFromDb,
./lib/ballbox-store.ts:50:  recordImpressionInDb,
./lib/ballbox-store.ts:51:  recordScreenHeartbeatInDb,
./lib/ballbox-store.ts:52:} from "@/lib/tvs-backend-runtime";
./lib/ballbox-store.ts:90:export type { BallboxStoreClubDetail, BallboxStoreClubListItem } from "@/lib/network-backend-clubs";
./lib/ballbox-store.ts:91:export type { BallboxStoreAdminPlayer, BallboxStorePlayer } from "@/lib/players-backend";
./lib/ballbox-store.ts:92:export type {
./lib/ballbox-store.ts:98:export type { BallboxStoreSportsAdminData, BallboxStoreSportsEvent } from "@/lib/sports-backend-events";
./lib/ballbox-store.ts:100:export type BallboxStoreScreen = {
./lib/ballbox-store.ts:110:  externalTvDeviceId?: string;
./lib/ballbox-store.ts:111:  impressionCount: number;
./lib/ballbox-store.ts:114:export type BallboxStoreCampaign = {
./lib/ballbox-store.ts:123:export type { BallboxStoreSportsCoach } from "@/lib/sports-backend-coaches";
./lib/ballbox-store.ts:125:export type BallboxStoreTvAdminVenueOption = {
./lib/ballbox-store.ts:132:export type BallboxStoreTvAdminTvDevice = {
./lib/ballbox-store.ts:136:  manifestUrl: string | null;
./lib/ballbox-store.ts:140:  impressionCount: number;
./lib/ballbox-store.ts:143:export type BallboxStoreTvAdminScreen = {
./lib/ballbox-store.ts:153:  tvDeviceId: string | null;
./lib/ballbox-store.ts:154:  tvDeviceName: string | null;
./lib/ballbox-store.ts:159:export type BallboxStoreTvAdminCreative = {
./lib/ballbox-store.ts:163:  impressionCount: number;
./lib/ballbox-store.ts:166:export type BallboxStoreTvAdminAssignment = {
./lib/ballbox-store.ts:175:export type BallboxStoreTvAdminCampaign = {
./lib/ballbox-store.ts:183:  creatives: BallboxStoreTvAdminCreative[];
./lib/ballbox-store.ts:184:  assignments: BallboxStoreTvAdminAssignment[];
./lib/ballbox-store.ts:187:export {
./lib/ballbox-store.ts:193:  createTvAssignmentInDb,
./lib/ballbox-store.ts:194:  createTvCampaignInDb,
./lib/ballbox-store.ts:195:  createTvCreativeInDb,
./lib/ballbox-store.ts:196:  createTvScreenInDb,
./lib/ballbox-store.ts:197:  createTvDeviceInDb,
./lib/ballbox-store.ts:204:  deleteTvAssignmentInDb,
./lib/ballbox-store.ts:205:  deleteTvCampaignInDb,
./lib/ballbox-store.ts:206:  deleteTvCreativeInDb,
./lib/ballbox-store.ts:209:  deleteTvScreenInDb,
./lib/ballbox-store.ts:210:  deleteTvDeviceInDb,
./lib/ballbox-store.ts:226:  getTvAdminOperationsDataFromDb,
./lib/ballbox-store.ts:227:  getTvManifestFromDb,
./lib/ballbox-store.ts:228:  getTvSourceFromDb,
./lib/ballbox-store.ts:233:  updateTvAssignmentInDb,
./lib/ballbox-store.ts:234:  updateTvCampaignInDb,
./lib/ballbox-store.ts:235:  updateTvCreativeInDb,
./lib/ballbox-store.ts:236:  updateTvScreenInDb,
./lib/ballbox-store.ts:237:  updateTvDeviceInDb,
./lib/ballbox-store.ts:241:  recordImpressionInDb,
./lib/ballbox-store.ts:242:  recordScreenHeartbeatInDb,
./app/api/clubs/manual-classes/route.ts:27:export async function POST(request: Request) {
./app/api/clubs/coaches/[coachId]/route.ts:16:export async function PATCH(request: Request, context: RouteContext) {
./app/api/clubs/coaches/route.ts:9:export async function GET(request: Request) {
./tests/unit/payments/mercadopago.test.ts:21:  it("validates webhook signature with canonical manifest", () => {
./tests/unit/payments/mercadopago.test.ts:26:    const manifest = `id:${dataId};request-id:${requestId};ts:${ts};`;
./tests/unit/payments/mercadopago.test.ts:27:    const digest = crypto.createHmac("sha256", webhookSecret).update(manifest).digest("hex");
./tests/unit/payments/mercadopago.test.ts:43:    const manifest = `id:${dataId};ts:${ts};`;
./tests/unit/payments/mercadopago.test.ts:44:    const digest = crypto.createHmac("sha256", webhookSecret).update(manifest).digest("hex").toUpperCase();
./lib/club-accounts-backend.ts:8:export type BallboxClubAccountProfile = {
./lib/club-accounts-backend.ts:15:export type BallboxClubAccountRecord = {
./lib/club-accounts-backend.ts:20:export type BallboxClubVenueSummary = {
./lib/club-accounts-backend.ts:25:export type BallboxClubWeeklyRange = {
./lib/club-accounts-backend.ts:32:export type BallboxClubRosterCoach = {
./lib/club-accounts-backend.ts:47:export type BallboxClubVenueConfiguration = {
./lib/club-accounts-backend.ts:77:export type BallboxClubDashboardCoach = {
./lib/club-accounts-backend.ts:88:export type BallboxClubDashboardCoachTableRow = {
./lib/club-accounts-backend.ts:100:export type BallboxClubDashboardRequest = {
./lib/club-accounts-backend.ts:120:export type BallboxClubDashboardManualClassOptions = {
./lib/club-accounts-backend.ts:145:export type BallboxClubDashboardCalendarBlock = {
./lib/club-accounts-backend.ts:165:export type BallboxClubDashboardCalendar = {
./lib/club-accounts-backend.ts:194:export type BallboxClubDashboardAnalytics = {
./lib/club-accounts-backend.ts:224:export type BallboxClubDashboardVenue = {
./lib/club-accounts-backend.ts:237:export type BallboxClubDashboard = {
./lib/club-accounts-backend.ts:254:export type BallboxClubManualClassBooking = {
./lib/club-accounts-backend.ts:1079:export async function getClubByContactEmailFromDb(email: string): Promise<BallboxClubAccountProfile | null> {
./lib/club-accounts-backend.ts:1089:export async function getClubAccountRecordByEmailFromDb(email: string): Promise<BallboxClubAccountRecord | null> {
./lib/club-accounts-backend.ts:1099:export async function getClubByIdFromDb(clubId: string): Promise<BallboxClubAccountProfile | null> {
./lib/club-accounts-backend.ts:1109:export async function createClubAccountInDb(input: { name: string; email: string; passwordHash: string }) {
./lib/club-accounts-backend.ts:1159:export async function getClubRosterFromDb(clubId: string) {
./lib/club-accounts-backend.ts:1366:export async function getClubVenueDetailFromDb(clubId: string, venueId: string) {
./lib/club-accounts-backend.ts:1383:export async function createClubManualClassInDb(
./lib/club-accounts-backend.ts:1586:export async function createClubCoachInviteInDb(clubId: string) {
./lib/club-accounts-backend.ts:1590:export async function createClubVenueInDb(
./lib/club-accounts-backend.ts:1659:export async function updateClubVenueInDb(
./lib/club-accounts-backend.ts:1790:export async function assignCoachVenueInDb(clubId: string, coachId: string, venueId: string | null) {
./lib/club-accounts-backend.ts:1846:export async function releaseCoachFromClubInDb(clubId: string, coachId: string) {
./app/api/clubs/venues/[venueId]/route.ts:17:export async function GET(_request: Request, context: RouteContext) {
./app/api/clubs/venues/[venueId]/route.ts:40:export async function PATCH(request: Request, context: RouteContext) {
./app/api/clubs/venues/route.ts:10:export async function POST(request: Request) {
./lib/platform.ts:1:export type ProductArea = {
./lib/platform.ts:2:  slug: "tvs" | "payments" | "network" | "sports" | "players";
./lib/platform.ts:13:export type CanonicalEntity =
./lib/platform.ts:17:  | "TvDevice"
./lib/platform.ts:22:  | "Impression"
./lib/platform.ts:28:export const productAreas: ProductArea[] = [
./lib/platform.ts:30:    slug: "tvs",
./lib/platform.ts:31:    title: "TVs",
./lib/platform.ts:32:    summary: "Pantallas, campaigns, creatives, manifests e impresiones.",
./lib/platform.ts:34:    href: "/admin/tvs",
./lib/platform.ts:38:    capabilities: ["Campaign ops", "Screen health", "Creative previews", "Manifest delivery"],
./lib/platform.ts:86:export const canonicalEntities: CanonicalEntity[] = [
./lib/platform.ts:90:  "TvDevice",
./lib/platform.ts:95:  "Impression",
./lib/platform.ts:102:export const runtimeSurface = {
./lib/platform.ts:106:  capabilities: ["Manifest polling", "TV device diagnostics", "Content playback", "Store-and-forward telemetry"],
./tests/unit/sports/sports-admin-client-config.test.ts:10:  updateEventDraftVenue,
./tests/unit/sports/sports-admin-client-config.test.ts:43:  it("exports shared empty forms", () => {
./tests/unit/sports/sports-admin-client-config.test.ts:69:    expect(updateEventDraftVenue(events, "event_1", "venue_2", sportsData)).toEqual([
./lib/prisma.ts:7:export const prisma =
./app/api/integrations/payments/sessions/route.ts:7:export async function GET() {
./lib/contracts/admin-tvs.ts:3:export const tvAdminScreenStatusSchema = z.enum(["active", "inactive"]);
./lib/contracts/admin-tvs.ts:4:export const adminTvsErrorCodeSchema = z.enum([
./lib/contracts/admin-tvs.ts:7:  "TV_DEVICE_NOT_FOUND",
./lib/contracts/admin-tvs.ts:8:  "TV_DEVICE_LINKED_TO_SCREEN",
./lib/contracts/admin-tvs.ts:9:  "TV_DEVICE_HAS_IMPRESSIONS",
./lib/contracts/admin-tvs.ts:16:  "CREATIVE_HAS_IMPRESSIONS",
./lib/contracts/admin-tvs.ts:21:export const adminTvsErrorResponseSchema = z
./lib/contracts/admin-tvs.ts:24:    error: adminTvsErrorCodeSchema,
./lib/contracts/admin-tvs.ts:30:export const tvAdminScreenCreateSchema = z.object({
./lib/contracts/admin-tvs.ts:34:  status: tvAdminScreenStatusSchema.default("active"),
./lib/contracts/admin-tvs.ts:35:  tvDeviceId: z.string().trim().min(1).optional(),
./lib/contracts/admin-tvs.ts:38:export const tvAdminScreenUpdateSchema = z
./lib/contracts/admin-tvs.ts:43:    status: tvAdminScreenStatusSchema.optional(),
./lib/contracts/admin-tvs.ts:44:    tvDeviceId: z.string().trim().optional(),
./lib/contracts/admin-tvs.ts:52:      value.tvDeviceId !== undefined,
./lib/contracts/admin-tvs.ts:58:export const tvAdminTvDeviceCreateSchema = z.object({
./lib/contracts/admin-tvs.ts:61:  manifestUrl: z.string().trim().url("Manifest URL must be valid").optional(),
./lib/contracts/admin-tvs.ts:64:export const tvAdminTvDeviceUpdateSchema = z
./lib/contracts/admin-tvs.ts:68:    manifestUrl: z
./lib/contracts/admin-tvs.ts:69:      .union([z.string().trim().url("Manifest URL must be valid"), z.literal("")])
./lib/contracts/admin-tvs.ts:73:    (value) => value.name !== undefined || value.location !== undefined || value.manifestUrl !== undefined,
./lib/contracts/admin-tvs.ts:79:export const tvAdminCampaignCreateSchema = z.object({
./lib/contracts/admin-tvs.ts:86:export const tvAdminCampaignUpdateSchema = z
./lib/contracts/admin-tvs.ts:104:export const tvAdminCreativeCreateSchema = z.object({
./lib/contracts/admin-tvs.ts:109:export const tvAdminCreativeUpdateSchema = z
./lib/contracts/admin-tvs.ts:118:export const tvAdminAssignmentCreateSchema = z.object({
./lib/contracts/admin-tvs.ts:125:export const tvAdminAssignmentUpdateSchema = z
./lib/contracts/admin-tvs.ts:143:export type AdminTvsErrorCode = z.infer<typeof adminTvsErrorCodeSchema>;
./lib/contracts/admin-tvs.ts:145:export function createAdminTvsErrorResponse(
./lib/contracts/admin-tvs.ts:146:  error: AdminTvsErrorCode,
./lib/contracts/admin-tvs.ts:150:  return adminTvsErrorResponseSchema.parse({
./lib/contracts/tvs-invariants.ts:1:export type TvScheduleWindow = {
./lib/contracts/tvs-invariants.ts:8:export type TvAssignmentWithCampaignWindow = {
./lib/contracts/tvs-invariants.ts:17:export function isTvScheduleActiveAt(now: Date, schedule: TvScheduleWindow) {
./lib/contracts/tvs-invariants.ts:25:export function filterActiveTvAssignments<T extends TvAssignmentWithCampaignWindow>(
./lib/contracts/tvs-invariants.ts:30:    isTvScheduleActiveAt(now, {
./scripts/revalidate-atc-clubs.ts:65:  const tvDevices = screenIds.length
./scripts/revalidate-atc-clubs.ts:66:    ? await prisma.tvDevice.findMany({
./scripts/revalidate-atc-clubs.ts:71:  const tvDeviceIds = tvDevices.map((tvDevice) => tvDevice.id);
./scripts/revalidate-atc-clubs.ts:129:  if (tvDeviceIds.length > 0) {
./scripts/revalidate-atc-clubs.ts:130:    await prisma.impression.deleteMany({ where: { tvDeviceId: { in: tvDeviceIds } } });
./scripts/revalidate-atc-clubs.ts:131:    await prisma.tvDevice.deleteMany({ where: { id: { in: tvDeviceIds } } });
./scripts/revalidate-atc-clubs.ts:156:        deletedTvDeviceCount: tvDeviceIds.length,
./app/api/integrations/payments/health/route.ts:6:export async function GET() {
./lib/contracts/accounts.ts:5:export const accountTypeSchema = z.enum(["coach", "club"]);
./lib/contracts/accounts.ts:7:export const accountSessionCreateSchema = z
./lib/contracts/accounts.ts:15:export const accountSessionResponseSchema = z
./lib/contracts/accounts.ts:22:export const accountErrorCodeSchema = z.enum([
./lib/contracts/accounts.ts:31:export const accountErrorResponseSchema = z
./lib/contracts/accounts.ts:40:export function createAccountErrorResponse(error: z.infer<typeof accountErrorCodeSchema>, status: number, details?: Record<string, unknown>) {
./lib/contracts/coach-students.ts:3:export const coachStudentPlayerSchema = z
./lib/contracts/coach-students.ts:15:export const coachStudentVenueSchema = z
./lib/contracts/coach-students.ts:24:export const coachStudentCourtSchema = z
./lib/contracts/coach-students.ts:31:export const coachStudentBookingSchema = z
./lib/contracts/coach-students.ts:35:    venue: coachStudentVenueSchema,
./lib/contracts/coach-students.ts:47:export const coachStudentSummarySchema = z
./lib/contracts/coach-students.ts:58:export const coachStudentsResponseSchema = z
./lib/contracts/coach-students.ts:72:    venues: z.array(coachStudentVenueSchema),
./lib/contracts/coach-students.ts:78:export const coachStudentBookingCreateSchema = z
./lib/contracts/coach-students.ts:105:export const coachStudentBookingMutationResponseSchema = z
./lib/contracts/coach-students.ts:112:export const coachStudentBookingDeleteResponseSchema = z
./lib/contracts/coach-students.ts:119:export const coachStudentContactUpdateSchema = z
./lib/contracts/coach-students.ts:129:export const coachStudentContactMutationResponseSchema = z
./app/api/integrations/payments/orders/route.ts:19:export async function GET() {
./app/api/integrations/payments/orders/route.ts:37:export async function POST(request: Request) {
./app/api/integrations/payments/approved/route.ts:18:export async function GET() {
./app/api/integrations/payments/approved/route.ts:32:export async function POST(request: NextRequest) {
./lib/contracts/club-accounts.ts:32:export const clubProfileSchema = z
./lib/contracts/club-accounts.ts:41:export const clubSessionCreateSchema = z
./lib/contracts/club-accounts.ts:48:export const clubAccountCreateSchema = z
./lib/contracts/club-accounts.ts:66:export const clubSessionResponseSchema = z
./lib/contracts/club-accounts.ts:73:export const clubCoachSchema = z
./lib/contracts/club-accounts.ts:92:export const clubCoachInviteSchema = z
./lib/contracts/club-accounts.ts:105:export const clubVenueSchema = z
./lib/contracts/club-accounts.ts:143:export const clubConfigurationSchema = z
./lib/contracts/club-accounts.ts:150:export const clubDashboardCoachSchema = z
./lib/contracts/club-accounts.ts:163:export const clubDashboardCoachTableRowSchema = z
./lib/contracts/club-accounts.ts:177:export const clubDashboardRequestSchema = z
./lib/contracts/club-accounts.ts:201:export const clubDashboardManualClassOptionsSchema = z
./lib/contracts/club-accounts.ts:233:export const clubDashboardCalendarBlockSchema = z
./lib/contracts/club-accounts.ts:255:export const clubDashboardCalendarSchema = z
./lib/contracts/club-accounts.ts:280:export const clubDashboardAnalyticsSchema = z
./lib/contracts/club-accounts.ts:322:export const clubDashboardVenueSchema = z
./lib/contracts/club-accounts.ts:337:export const clubDashboardSchema = z
./lib/contracts/club-accounts.ts:358:export const clubCoachesResponseSchema = z
./lib/contracts/club-accounts.ts:369:export const clubInviteCreateSchema = z.object({}).strict();
./lib/contracts/club-accounts.ts:371:export const clubInviteMutationResponseSchema = z
./lib/contracts/club-accounts.ts:378:export const clubVenueMutationResponseSchema = z
./lib/contracts/club-accounts.ts:385:export const clubVenueDetailResponseSchema = z
./lib/contracts/club-accounts.ts:395:export const clubCoachMutationResponseSchema = z
./lib/contracts/club-accounts.ts:402:export const clubManualClassCreateSchema = z
./lib/contracts/club-accounts.ts:433:export const clubManualClassMutationResponseSchema = z
./lib/contracts/club-accounts.ts:455:export const clubVenueCreateSchema = z
./lib/contracts/club-accounts.ts:472:export const clubVenueUpdateSchema = z
./lib/contracts/club-accounts.ts:536:export const clubCoachUpdateSchema = z.union([
./lib/contracts/club-accounts.ts:550:export const clubErrorCodeSchema = z.enum([
./lib/contracts/club-accounts.ts:571:export const clubErrorResponseSchema = z
./lib/contracts/club-accounts.ts:580:export function createClubErrorResponse(error: z.infer<typeof clubErrorCodeSchema>, status: number, details?: Record<string, unknown>) {
./app/api/players/route.ts:5:export async function GET() {
./lib/contracts/classes.ts:5:export const classAtcSlotSchema = z
./lib/contracts/classes.ts:17:export const classAtcContextSchema = z
./lib/contracts/classes.ts:29:export const classOfferCoachSchema = z
./lib/contracts/classes.ts:37:export const classOfferVenueSchema = z
./lib/contracts/classes.ts:46:export const classOfferListItemSchema = z
./lib/contracts/classes.ts:67:export const classOfferDetailSchema = classOfferListItemSchema;
./lib/contracts/classes.ts:69:export const classesOffersResponseSchema = z
./lib/contracts/classes.ts:76:export const classesOfferDetailResponseSchema = z
./lib/contracts/classes.ts:83:export const classRequestSelectionModeSchema = z.enum(["slot", "flexible"]);
./lib/contracts/classes.ts:85:export const classRequestCreateSchema = z
./lib/contracts/classes.ts:124:export const classRequestCreatedSchema = z
./lib/contracts/classes.ts:134:export const classRequestCreateResponseSchema = z
./lib/contracts/classes.ts:141:export const classesSessionStatusResponseSchema = z
./lib/contracts/classes.ts:149:export const classesErrorCodeSchema = z.enum(["INVALID_PAYLOAD", "OFFER_NOT_FOUND", "SLOT_NOT_AVAILABLE"]);
./lib/contracts/classes.ts:151:export const classesErrorResponseSchema = z
./lib/contracts/classes.ts:160:export type ClassesErrorCode = z.infer<typeof classesErrorCodeSchema>;
./lib/contracts/classes.ts:162:export function createClassesErrorResponse(error: ClassesErrorCode, status: number, details?: Record<string, unknown>) {
./lib/contracts/classes.ts:171:export function normalizeOptionalClassesText(value: string | undefined) {
./lib/contracts/network.ts:3:export const NETWORK_CONTRACT_VERSION = 1;
./lib/contracts/network.ts:5:export const venueStatusSchema = z.enum(["active", "inactive"]);
./lib/contracts/network.ts:7:export const networkErrorCodeSchema = z.enum([
./lib/contracts/network.ts:28:export const networkErrorResponseSchema = z
./lib/contracts/network.ts:37:export const clubCreateSchema = z
./lib/contracts/network.ts:43:export const clubUpdateSchema = z
./lib/contracts/network.ts:57:export const venueCreateSchema = z
./lib/contracts/network.ts:65:export const venueUpdateSchema = z
./lib/contracts/network.ts:76:export const vendingMachineCreateSchema = z
./lib/contracts/network.ts:83:export const vendingMachineUpdateSchema = z
./lib/contracts/network.ts:93:export const networkVendingMachineSchema = z
./lib/contracts/network.ts:102:export const networkVenueSchema = z
./lib/contracts/network.ts:115:export const networkClubListItemSchema = z
./lib/contracts/network.ts:127:export const networkClubDetailSchema = z
./lib/contracts/network.ts:137:export const networkCreatedVendingMachineSchema = z
./lib/contracts/network.ts:146:export const networkListResponseSchema = z
./lib/contracts/network.ts:154:export const networkClubDetailResponseSchema = z
./lib/contracts/network.ts:161:export const networkVenueMutationResponseSchema = z
./lib/contracts/network.ts:169:export const networkClubMutationResponseSchema = z
./lib/contracts/network.ts:176:export const networkVendingMachineMutationResponseSchema = z
./lib/contracts/network.ts:183:export const networkDeleteResponseSchema = z
./lib/contracts/network.ts:189:export type NetworkErrorCode = z.infer<typeof networkErrorCodeSchema>;
./lib/contracts/network.ts:191:export function createNetworkErrorResponse(
./lib/contracts/admin-players.ts:3:export const adminPlayersErrorCodeSchema = z.enum([
./lib/contracts/admin-players.ts:13:export const adminPlayersErrorResponseSchema = z
./lib/contracts/admin-players.ts:22:export const adminPlayerMembershipSchema = z
./lib/contracts/admin-players.ts:30:export const adminPlayerItemSchema = z
./lib/contracts/admin-players.ts:40:export const adminPlayersListResponseSchema = z
./lib/contracts/admin-players.ts:47:export const adminPlayerMutationResponseSchema = z
./lib/contracts/admin-players.ts:54:export const adminPlayerDeleteResponseSchema = z
./lib/contracts/admin-players.ts:60:export const adminPlayerCreateSchema = z
./lib/contracts/admin-players.ts:68:export const adminPlayerUpdateSchema = z
./lib/contracts/admin-players.ts:79:export const adminMembershipCreateSchema = z
./lib/contracts/admin-players.ts:85:export type AdminPlayersErrorCode = z.infer<typeof adminPlayersErrorCodeSchema>;
./lib/contracts/admin-players.ts:87:export function createAdminPlayersErrorResponse(
./lib/contracts/payments.ts:9:export const paymentSessionStatusSchema = z.enum(["approved"]);
./lib/contracts/payments.ts:11:export const paymentsAdminErrorCodeSchema = z.enum([
./lib/contracts/payments.ts:19:export const paymentsAdminErrorResponseSchema = z
./lib/contracts/payments.ts:28:export const paymentSessionCreateSchema = z.object({
./lib/contracts/payments.ts:43:export const paymentApprovalCallbackSchema = z.object({
./lib/contracts/payments.ts:57:export const paymentSessionUpdateSchema = z
./lib/contracts/payments.ts:102:export const paymentOrderModeSchema = z.enum(["static", "dynamic", "hybrid"]);
./lib/contracts/payments.ts:104:export const paymentOrderCreateSchema = z.object({
./lib/contracts/payments.ts:120:export const paymentOrderCreateResponseSchema = z
./lib/contracts/payments.ts:143:export type PaymentsAdminErrorCode = z.infer<typeof paymentsAdminErrorCodeSchema>;
./lib/contracts/payments.ts:145:export function createPaymentsAdminErrorResponse(
./lib/contracts/sports.ts:3:export const sportsCoachSchema = z
./lib/contracts/sports.ts:12:export const sportsEventSchema = z
./lib/contracts/sports.ts:24:export const sportsResponseSchema = z
./app/api/network/[clubId]/venues/[venueId]/route.ts:16:export async function PATCH(request: Request, context: { params: Promise<{ clubId: string; venueId: string }> }) {
./app/api/network/[clubId]/venues/[venueId]/route.ts:46:export async function DELETE(_request: Request, context: { params: Promise<{ clubId: string; venueId: string }> }) {
./components/players-admin-stats.tsx:9:export function PlayersAdminStats({ players, clubs, cardClassName }: PlayersAdminStatsProps) {
./lib/contracts/admin-sports.ts:10:export const adminSportsErrorCodeSchema = z.enum([
./lib/contracts/admin-sports.ts:19:export const adminSportsErrorResponseSchema = z
./lib/contracts/admin-sports.ts:28:export const adminSportsDataSchema = z
./lib/contracts/admin-sports.ts:54:export const adminSportsOperationsResponseSchema = z
./lib/contracts/admin-sports.ts:61:export const adminCoachCreateSchema = z
./lib/contracts/admin-sports.ts:68:export const adminCoachUpdateSchema = z
./lib/contracts/admin-sports.ts:78:export const adminEventCreateSchema = z
./lib/contracts/admin-sports.ts:86:export const adminEventUpdateSchema = z
./lib/contracts/admin-sports.ts:97:export const adminSportsCoachMutationResponseSchema = z
./lib/contracts/admin-sports.ts:104:export const adminSportsEventMutationResponseSchema = z
./lib/contracts/admin-sports.ts:111:export const adminSportsDeleteResponseSchema = z
./lib/contracts/admin-sports.ts:117:export type AdminSportsErrorCode = z.infer<typeof adminSportsErrorCodeSchema>;
./lib/contracts/admin-sports.ts:119:export function createAdminSportsErrorResponse(
./app/api/network/[clubId]/venues/route.ts:15:export async function POST(request: Request, context: { params: Promise<{ clubId: string }> }) {
./components/network-admin-create-venue-form.tsx:11:  setVenueForms: Dispatch<SetStateAction<Record<string, VenueFormState>>>;
./components/network-admin-create-venue-form.tsx:18:export function NetworkAdminCreateVenueForm({
./components/network-admin-create-venue-form.tsx:22:  setVenueForms,
./components/network-admin-create-venue-form.tsx:32:        onChange={(event) => setVenueForms((current) => ({ ...current, [clubId]: { ...venueForm, name: event.target.value } }))}
./components/network-admin-create-venue-form.tsx:38:        onChange={(event) => setVenueForms((current) => ({ ...current, [clubId]: { ...venueForm, slug: event.target.value } }))}
./components/network-admin-create-venue-form.tsx:45:          setVenueForms((current) => ({ ...current, [clubId]: { ...venueForm, status: event.target.value as VenueStatus } }))
./components/network-admin-create-venue-form.tsx:72:            setVenueForms((current) => ({ ...current, [clubId]: emptyVenueForm }));
./app/api/network/[clubId]/route.ts:18:export async function GET(_request: Request, context: { params: Promise<{ clubId: string }> }) {
./app/api/network/[clubId]/route.ts:29:export async function PATCH(request: Request, context: { params: Promise<{ clubId: string }> }) {
./app/api/network/[clubId]/route.ts:67:export async function DELETE(_request: Request, context: { params: Promise<{ clubId: string }> }) {
./components/sports-admin-events-section.tsx:8:  updateEventDraftVenue,
./components/sports-admin-events-section.tsx:35:export function SportsAdminEventsSection({
./components/sports-admin-events-section.tsx:116:                  setItems((current) => updateEventDraftVenue(current, item.id, event.target.value, { venues }))
./lib/contracts/coaches.ts:20:export const coachSessionCreateSchema = z
./lib/contracts/coaches.ts:27:export const coachAccountCreateSchema = z
./lib/contracts/coaches.ts:46:export const coachProfileSchema = z
./lib/contracts/coaches.ts:58:export const coachSessionResponseSchema = z
./lib/contracts/coaches.ts:65:export const coachAvailabilityItemSchema = z
./lib/contracts/coaches.ts:75:export const coachAvailabilityCreateItemSchema = coachAvailabilityItemSchema
./lib/contracts/coaches.ts:79:export const coachWeeklyRangeSchema = z
./lib/contracts/coaches.ts:87:export const coachAvailabilityResponseSchema = z
./lib/contracts/coaches.ts:94:export const coachAvailabilityUpdateSchema = z
./lib/contracts/coaches.ts:100:export const coachClubOptionSchema = z
./lib/contracts/coaches.ts:122:export const coachClubSelectionSchema = z
./lib/contracts/coaches.ts:129:export const coachClubsResponseSchema = z
./lib/contracts/coaches.ts:137:export const coachClubsUpdateSchema = coachClubSelectionSchema;
./lib/contracts/coaches.ts:139:export const coachSalesPackageSchema = z
./lib/contracts/coaches.ts:151:export const coachSalesPackageInputSchema = coachSalesPackageSchema
./lib/contracts/coaches.ts:184:export const coachCommercialUpdateSchema = z
./lib/contracts/coaches.ts:218:export const coachAvailabilityContextSchema = z
./lib/contracts/coaches.ts:227:export const coachCommercialSchema = z
./lib/contracts/coaches.ts:234:export const coachDashboardClubSchema = z

[toolResult]
# Streetcast Runtime Parity

## Covered
- Manifest polling: covered by `/player/[screenId]` lightweight runtime refresh loop.
- Sequential creative playback: covered by runtime rotation over active manifest creatives.
- Local media cache: covered by screen-scoped browser cache with cached blob playback preference.
- Background preload: covered by manifest-driven media warming after each runtime refresh.
- Offline continuity: covered by last-good-manifest retention plus cached blob playback fallback.
- Impression retry queue: covered by bounded browser queue flushed on interval and `online` events.
- Browser automation parity: covered by Playwright cached-blob/offline checks on `/player/[screenId]`.

## Intentionally Different
- Ballbox runtime uses Ballbox-owned manifest/impression APIs instead of Streetcast endpoints.
- The production TV runtime path is `/player/[screenId]`.
- Playback source of truth is Ballbox Postgres scheduling, not legacy Streetcast ownership data.

## Still Pending
- Real-device verification is still required before calling Streetcast migration complete.
- The missing proof is hardware/browser behavior for CacheStorage plus object-URL playback outside local desktop automation.
- The runtime now ships an on-screen `Export parity snapshot` action that downloads a JSON snapshot (screen id, manifest/playback/cache/network/reporting state, user agent, capture time) to standardize evidence capture.

## Manual Real-Device Check
1. Open `/player/screen_001` on the target TV/browser with Ballbox pointed at the real/shared DB.
2. Wait for the runtime diagnostics card to show:
   - `Playback: cached`
   - `Active source: blob`
   - `Cache API: available`
   - `Cached creatives: 1` or higher
   - `Network: online`
3. Confirm the video starts and loops without a visible reload gap while the diagnostics stay on `blob`.
4. Disconnect network on the device or gateway after the blob source is active.
5. Confirm the runtime keeps playing and the diagnostics switch to:
   - `Network: offline`
   - `Playback: cached`
   - `Active source: blob`
6. Keep the device offline past one manifest refresh interval and verify `Manifest refresh: failed` appears without playback stopping.
7. Reconnect network and confirm:
   - `Network: online`
   - queued impressions eventually return to `0`
   - heartbeat timestamp advances again
8. Capture evidence:
    - photo/video of the runtime while online and offline
    - browser version / device model
    - exact screen id used
    - JSON file exported from `Export parity snapshot` in both online and offline moments
    - any deviation from the expected diagnostics

## Exit Criteria
- One real device shows cached blob playback before and during an offline window.
- Runtime keeps playing through at least one failed manifest refresh while offline.
- Heartbeat and impression reporting recover after reconnect.
- This doc should be updated with the device/browser/date/result once the run happens.

[toolResult]
# Next Steps Queue

## Purpose
- Keep `PLAN.md` actionable without turning it into a long unordered backlog.
- Let any agent pick the next best task quickly using priority + dependency.

## Picking Rule
- Pick the highest-priority task with `Status: ready`.
- If a task is `blocked`, unblock its dependency first.
- After shipping, update this file, `PLAN.md`, and relevant `docs/implemented-*.md` in the same work.

## Recently Completed (2026-04-14)

### DOC-1 Docs normalization baseline
- Priority: P0
- Status: completed
- Depends on: none
- Scope shipped:
  - docs navigation/categories normalized in `docs/index.md`
  - `docs/repo-map.md` maintained as a fast entrypoint map
  - stale/missing references removed from active docs surfaces (`README.md`, `PLAN.md`, `docs/index.md`)
- Evidence: `node scripts/check-doc-references.mjs` passes

### DOC-2 PLAN execution hygiene
- Priority: P0
- Status: completed
- Depends on: none
- Scope shipped:
  - `PLAN.md` next-steps section reduced to queue pointer + explicit pick order
  - active queue wording avoids mixing done narrative
  - handoff wording keeps one clear "pick this next" target
- Expected payoff: next-agent selection is immediate

### DOC-3 Docs drift CI guard
- Priority: P0
- Status: completed
- Depends on: none
- Scope shipped:
  - CI check validates docs references in `README.md`, `PLAN.md`, and `docs/index.md`
  - CI fails on broken/missing local doc references
- Evidence: `.github/workflows/docs-drift-guard.yml`, `scripts/check-doc-references.mjs`

### ARCH-1 Route helper boundary cleanup
- Priority: P1
- Status: completed
- Depends on: DOC-1 (done)
- Scope shipped:
  - route helper/envelope parsing moved to shared `lib/http/*`
  - deep relative helper imports removed from network, players, sports, and payments route handlers
- Evidence: route handlers now import `@/lib/http/*-route-helpers`

### OPS-0 Admin surface 500 fix (`/admin/tvs`, `/admin/network`, `/admin/sports`, `/admin/players`)
- Priority: P0
- Status: completed
- Depends on: none
- Scope shipped:
  - aligned Prisma `Club.atcSportclubId` with live nullable data to prevent runtime `P2032` decode failures
  - regenerated Prisma client after schema alignment
  - added `db:backfill-atc-clubs` to sync local club names with ATC summary by `atcSportclubId` while keeping null-ATC clubs supported
- Evidence: Prisma `club.findMany` now handles rows with `atcSportclubId = null`

## P0 Ready

### OPS-1 Payments real-world validation closure
- Priority: P0
- Status: ready
- Depends on: none
- Scope:
  - run one public webhook smoke with real callback path
  - run one real payment and verify persisted approved callback in `/admin/payments`
  - document observed payload + public URL evidence
- Progress (2026-04-14): payments migration to host app started in one feature branch; health/sessions now expose internal MercadoPago mode, Ballbox ships `/notification/mercadopago` intake with signature validation + order fetch, and native order creation now exists at `/api/integrations/payments/orders`.
- Expected payoff: closes highest-risk integration uncertainty

### OPS-2 TVs runtime parity closure
- Priority: P0
- Status: ready
- Depends on: none
- Scope:
  - manually verify real-device CacheStorage/object-URL behavior on `/player/[screenId]`
  - update `docs/streetcast-parity.md` with concrete evidence
- Progress (2026-04-14): runtime now exports a JSON parity snapshot from `/player/[screenId]` to standardize evidence capture; remaining work is one real-device run and doc evidence entry.
- Expected payoff: de-risks constrained-device production behavior

## P1 After P0

### ARCH-2 Continue `ballbox-store` caller migration
- Priority: P1
- Status: ready
- Depends on: ARCH-1 (done)
- Scope:
  - migrate callers to direct domain modules where clearer
  - keep `lib/ballbox-store.ts` as minimal compatibility facade only
- Progress (2026-04-14): sports route/page callers and sports UI/test types now import direct sports backend modules; players route/page callers now import `lib/players-backend.ts` directly (plus `lib/network-backend-clubs.ts` for admin club list hydration); payments admin session routes now consume Prisma error helpers from `lib/prisma-errors.ts`; and network route/page callers now import direct network backend modules (`clubs`, `club-mutations`, `venues`, `vending-machines`) plus `lib/prisma-errors.ts`. Remaining scope continues with the same migration pattern for TVs and classes surfaces still reading via `lib/ballbox-store.ts`.
- Expected payoff: less coupling and better module discoverability

### ARCH-3 Test taxonomy split
- Priority: P1
- Status: ready
- Depends on: DOC-1 (done)
- Scope:
  - separate tests by intent: `contracts`, `unit`, `integration`
  - align `vitest.config.ts` includes with those layers
- Progress (2026-04-14): `vitest` include patterns now cover `tests/contracts`, `tests/unit`, and `tests/integration`; dedicated scripts now exist for each layer, and first unit moves landed (`network/route-helpers`, sports client config/state tests).
- Expected payoff: faster debugging and more intentional coverage growth

### PERF-1 TTFB focused pass
- Priority: P1
- Status: ready
- Depends on: OPS-1
- Scope:
  - measure `/classes` and `/admin` server timings
  - add only the cache/instrumentation changes justified by measurement
- Expected payoff: better responsiveness without blind cache complexity

### PAY-2 Full payments migration into Ballbox host app
- Priority: P1
- Status: ready
- Depends on: OPS-1
- Scope:
  - migrate remaining bridge-owned MercadoPago flow (webhook + order lifecycle) into Ballbox routes/lib
  - remove reliance on `BALLBOX_PAYMENTS_URL` bridge dependency for operational status
  - keep callback compatibility during transition, then retire bridge dependency cleanly
- Progress (2026-04-14): host app now includes MercadoPago webhook route (`/notification/mercadopago`) plus signature verification and order fetch primitives.
- Progress (2026-04-14): host app now also includes QR order creation (`/api/integrations/payments/orders`) with canonical `externalReference` generation (`payment:{vendingMachineId}:{machineTxnId}`).
- Expected payoff: one-repo payments ownership, simpler operations, faster debugging

### PAY-3 Split payments (MercadoPago Partners) per club
- Priority: P1
- Status: blocked
- Depends on: OPS-1, PAY-2
- Scope:
  - define a fixed Ballbox split-payments setup equivalent to v1 so every approved payment can route a configurable percentage to the club owning the vending machine
  - add an admin CTA in club configuration to start/link MercadoPago partner onboarding for that club
  - persist the minimum per-club MercadoPago partner metadata/credentials required for payout routing and operational support
  - document security and rotation rules for stored credentials before go-live
- Notes:
  - implementation details are intentionally pending discovery when this task is started
  - keep this behind real-world payments validation first (`OPS-1`)
- Expected payoff: platform-controlled split payments with club-aligned revenue distribution

## P2 Sequenced Refactors

### ARCH-4 Domain folder architecture
- Priority: P2
- Status: blocked
- Depends on: ARCH-2, ARCH-3
- Scope:
  - move flat prefix-based modules into domain folders in `lib/` and `components/`
  - preserve behavior; focus on discoverability and ownership boundaries
- Expected payoff: major readability improvement for future work

### ARCH-5 Route IA normalization (`/classes*` + `/coaches*`)
- Priority: P2
- Status: blocked
- Depends on: DOC-1
- Scope:
  - align route grouping and naming with real auth/surface behavior
  - keep redirects/aliases explicit during transition
- Expected payoff: lower confusion in feature placement and routing maintenance

### ARCH-6 Naming conventions and metadata cleanup
- Priority: P2
- Status: blocked
- Depends on: ARCH-4
- Scope:
  - codify canonical naming terms and singular/plural rules
  - align package metadata with repo identity
- Expected payoff: fewer naming mismatches and clearer ownership vocabulary

[toolResult]
# Ballbox Host App Plan

## Goal
- Convert `ballbox` into the host app for Ballbox platform.
- Preserve the current marketing home at `/`.
- Add a shared admin shell at `/admin`.
- Keep product boundaries explicit with dedicated subroutes.
- Converge on a shared `network` core model that all product domains reference.

## Current State
- Ballbox overall is still in validation stage for padel centers; there are no customers yet and near-term proof still depends on pilots with the first machines.
- `/` remains the marketing home.
- `/admin` is live as the shared Ballbox admin shell.
- Product-separated admin areas exist for `tvs`, `payments`, `network`, `sports`, and `players`.
- `/admin/screens` exists as the runtime entry area for TVs.
- `/player/[screenId]` is the lightweight TV runtime route served as plain HTML + JavaScript inside Ballbox.
- `/player/[screenId]` now exists as the lightweight TV runtime route served as plain HTML + JavaScript inside Ballbox.
- TVs are internal to Ballbox as a module, not a separate app.
- TVs admin and `/api/tvs/*` use an internal Ballbox ownership model for clubs, venues, screens, and campaigns.
- TVs runtime/admin already read Ballbox Postgres directly through Prisma.
- TVs runtime heartbeats and impressions already write to Ballbox DB-backed flows.
- TVs manifest/admin scheduling now respects campaign and assignment time windows.
- TVs now expose a strict Zod-backed API contract plus contract tests.
- TVs runtime now exposes on-screen diagnostics for cache/object-URL/manual parity verification on real devices.
- TVs runtime diagnostics now also support a one-click parity snapshot JSON export (playback/cache/network/reporting state + user agent + capture timestamp) to standardize real-device evidence capture.
- TVs player runtime cache/media/impression/heartbeat/manifest-refresh logic now lives in one shared core used by both the React player and the lightweight plain-JS player shell.
- Maintainability review for the next refactor cycle is documented in `docs/maintainability-priorities-2026-03.md`.
- Payments are now being consolidated into Ballbox host app: native MercadoPago webhook intake exists under `/notification/mercadopago` with signature validation and order fetch.
- Payments host app now also exposes native QR order creation under `/api/integrations/payments/orders`, generating canonical `externalReference` values required for approved-session persistence correlation.
- Legacy bridge callback compatibility still exists at `/api/integrations/payments/approved` during transition.
- `/admin/payments` now exposes Ballbox-local CRUD/reconciliation for approved payment sessions plus correlation checks against canonical `payment:{vendingMachineId}:{machineTxnId}` references.
- Payments admin session routes now read Prisma error helpers from `lib/prisma-errors.ts` instead of importing them via `lib/ballbox-store.ts`.
- Network core now has DB-backed admin CRUD for ATC-linked clubs, venues, and vending machines inside Ballbox.
- Prisma `Club.atcSportclubId` now matches live nullable data to prevent admin loader `P2032` crashes on legacy clubs without ATC linkage (`/admin/network`, `/admin/players`, `/admin/sports`, `/admin/tvs`).
- Clubs keep nullable `atcSportclubId` support for legacy/local rows, and repo now ships `db:backfill-atc-clubs` to reconcile local club names with ATC summary data when ids exist.
- Network club list/detail/admin query shaping has started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- Network club create/update/delete helpers have now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- Network venue mutation/delete helpers have also started moving out of `lib/ballbox-store.ts` into dedicated backend modules.
- Network vending-machine route callers now import `lib/network-backend-vending-machines.ts` directly and read unique-constraint classification from `lib/prisma-errors.ts`.
- Network club/venue route callers and `/admin/network` page now import direct network backend modules (`lib/network-backend-clubs.ts`, `lib/network-backend-club-mutations.ts`, `lib/network-backend-venues.ts`) plus `lib/prisma-errors.ts`.
- Network route handlers now share `lib/http/network-route-helpers.ts` for JSON parsing plus success/error envelopes across club, venue, and vending-machine contract routes.
- Players, sports, and payments route handlers now also share `lib/http/*-route-helpers.ts` so public/admin contract surfaces no longer depend on deep route-local helper imports.
- Network admin frontend view extraction has started moving venue-level and club-level rendering blocks out of `components/network-admin-client.tsx` into smaller components.
- Network admin frontend section composition has now also started moving club-list rendering, create-club section wiring, and derived venue-form wiring out of `components/network-admin-client.tsx` so it trends further toward a shell.
- Network admin frontend client-state, mutation orchestration, and local form-state wiring have started moving out of `components/network-admin-client.tsx` into dedicated hooks so the client trends toward a shell.
- Network admin frontend form defaults and status-option constants now live in a shared client config so shell and child components consume one source of truth.
- Network admin frontend layout/class wiring now also lives in a dedicated shell component so `components/network-admin-client.tsx` is closer to trivial hook composition.
- Network admin frontend shell chrome styles now also live in a tiny shared helper so the shell stays closer to pure composition/wiring.
- Players admin frontend client-state, form defaults, shell composition, and presentational stats/create/list-card seams now live in dedicated frontend modules so the client follows the same maintainable pattern already used by network.
- Sports admin frontend client-state, form defaults, shell composition, and coach/event section rendering now live in dedicated frontend seams so `components/sports-admin-client.tsx` is closer to pure wiring.
- Final naming direction is `network` core with `Club`, `Venue`, `VendingMachine`, `Screen`, and `TvDevice`.
- Post-`pnpm db:push` plus `pnpm db:seed` verification confirms physical DB columns stay canonical as `vendingMachineId` and `tvDeviceId`, with no `machineId`, `deviceId`, `/devices`, or `PaymentSessionRef` persistence aliases reintroduced.
- TVs admin now has DB-backed CRUD for screens and tv devices inside Ballbox.
- TV device admin mutations have started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV screen admin mutations have now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV campaign admin mutations have now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV creative admin mutations have now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV assignment admin mutations have now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV admin venue-option query shaping has now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV admin device/screen/campaign operations query shaping has now also started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- TV runtime source/manifest/heartbeat/impression helpers now also delegate from `lib/ballbox-store.ts` into a dedicated TV runtime backend module.
- TVs admin now has DB-backed CRUD for campaigns, creatives, and screen assignments inside Ballbox.
- Admin now supports a simple shared-password access layer when `BALLBOX_ADMIN_PASSWORD` is configured.
- TVs impressions now reject creatives that are not active for the target screen.
- Players API now reads Ballbox Postgres for `Player` and `Membership` rollups.
- Player backend read/admin query+mutation helpers have now started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- Sports coach backend helpers have now started moving out of `lib/ballbox-store.ts` into a dedicated backend module.
- Sports events plus classes offer/request backend helpers have now also started moving out of `lib/ballbox-store.ts` into dedicated backend modules.
- Sports now has DB-backed entities and admin/read APIs for coaches, open matches, and classes.
- `/classes` now exists as the first non-admin public Ballbox surface.
- `/classes` now lists Ballbox-local `ClassOffer` records, shows offer detail, surfaces visible ATC public-read slots, and accepts local `ClassRequest` submissions with either a concrete selected slot or a flexible any-schedule/any-location fallback.
- `/classes-by-date` now ships as a zone-first reservation calendar driven by coach availability plus ATC club availability, including explicit “coach disponible, pero sin canchas” slots and a required first zone selection based on the club's public location label when available.
- `/classes/me` now ships as a lightweight read-only student follow-up view (email-filtered request history) so demos can show the request loop without claiming full reservation confirmation.
- `/classes` initial HTML now renders from Ballbox-local offer data first and resolves ATC public-read context asynchronously after paint, with a short shared server cache to avoid repeating the same ATC lookup per visitor.
- Rapid demo execution now follows `docs/demo-iteration-mode.md` (`L1 story` / `L2 contracted demo` / `L3 production candidate`) so pivots stay fast without leaking demo shortcuts into core modules.
- `/coaches` now acts as the Ballbox account hub where the first differentiation is `Coach` vs `Club`.
- `/coaches` now defaults to `Sign in`, and `Sign up` is the fallback path for new accounts.
- `/coaches/settings` now lands on a coach dashboard first with `Calendar` and `Analytics` sub-sections: calendar shows the current week's coach availability plus taken student reservations, while analytics shows weekly earnings and student consistency/review rows; setup stays in `Mi Perfil` and student follow-up stays in `Alumnos`.
- `/clubs` now exists as the club-only service home that opens right after club sign-in/sign-up and groups the operational surfaces into service cards, with `Entrenadores` as the active entry point into the existing coaches workspace and the remaining services marked as `Proximamente`.
- `/clubs/coaches` now lands on a club workspace with top-level `Calendar`, `Analytics`, `Sedes`, and `Profesores` sections: calendar shows the current week's coach availability and reservations with sede/coach/court filters plus in-place manual class loading, analytics shows professor/reservation money splits and review placeholders, `Sedes` groups venue creation plus venue quick settings, and `Profesores` groups invite links plus the club coach table.
- `/clubs/coaches` now also enforces the club session at the page boundary before rendering the workspace, preventing the `Entrenadores` service-card hop from dropping users back into the shared `/coaches` access hub after a valid club login.
- Club workspace clickable actions are now visually distinguished with stronger accent borders, CTA chips, and hover/focus states, while existing-setting edits in club quick settings and venue detail auto-save in place instead of requiring explicit `Guardar` buttons or reloading the user back to the top.
- `/api/coaches/students` now lets a coach load existing Ballbox players into concrete occupied time slots, validating availability, venue fit, and overlap rules, and the coach workspace now surfaces those students in a dedicated `Alumnos` section with WhatsApp / Telegram contact fields.
- `/clubs/venues/[venueId]` now exists as the dedicated venue detail view for configuring venue opening/closing time, coach hours, external-coach availability, reservation settings, courts, and the coaches assigned to that venue.
- `CoachAvailability` weekly ranges and `CoachClub` selections now model coach intent separately from ATC court availability, while coach-side editing is clamped to the hours enabled by the assigned venue or the selected external clubs.
- Clubs now carry optional ATC sportclub ids to query availability at the club level.
- Clubs now also carry an optional contact email used for demo-grade club access and roster management.
- Venues now also store one opening time, one closing time, weekly coach-availability ranges, an external-coach availability flag, coach-enabled courts per venue, a special reservation price for coach classes, a club-coach-only discount on that fee, and a configurable late-cancellation window in hours.
- Coaches now can also be assigned to one venue inside their club.
- Coaches now also persist a Mercado Pago placeholder handle plus reusable sales-package rows for pricing, duration, class format, and max-student setup.
- Coaches and clubs now also store per-account password hashes for sign-in.
- Clubs now also persist coach invite tokens for generic join links, while still tolerating older email-bound invites.
- Local development now has a first-class Docker Postgres path via `docker-compose.yml` plus `.env.local`, and the seed now provisions demo coach/club passwords for local sign-in.
- Auth/session routes now return explicit database-unavailable errors when Prisma cannot reach or borrow a DB connection, instead of surfacing those failures as generic bad-login states.
- `/admin` overview now reads lightweight aggregate counts instead of hydrating full product datasets during SSR.
- Coach experience discovery is still early: current research input is an online form, so Ballbox does not yet have enough real-world coach workflow evidence to lock the sports product thesis.
- ATC admin/write is still not integrated; only a small public-read availability boundary now exists for `/classes` offer context and must degrade cleanly when unavailable.
- Local quality gate now includes `pnpm lint`, `pnpm typecheck`, `pnpm test:contracts`, `pnpm test:e2e`, and `pnpm build`.
- Vitest taxonomy now includes layered includes for `tests/contracts`, `tests/unit`, and `tests/integration`, with dedicated scripts for each layer.

## Route Map
- `/`: marketing home
- `/admin`: admin overview
- `/admin/tvs`: TV and screen operations
- `/admin/payments`: payment operations and reconciliation
- `/admin/network`: clubs, venues, vending machines
- `/admin/sports`: open matches, coaches, academy
- `/admin/players`: players y memberships
- `/admin/screens`: screen runtime admin surface hosted inside Ballbox
- `/classes`: public class discovery + request surface
- `/classes/[offerId]`: public offer detail + request form
- `/classes-by-date`: zone-first reservation discovery calendar
- `/coaches`: account hub for coach vs club
- `/classes/me`: read-only student request follow-up
- `/coaches/settings`: coach dashboard with `Calendar` / `Analytics`, plus `Mi Perfil` and `Alumnos`
- `/clubs`: club service home
- `/clubs/coaches`: club workspace with `Calendar` / `Analytics` / `Sedes` / `Profesores`
- authenticated coach/club workspace routes now use a shared top navbar with section tabs and an avatar-triggered sign-out menu instead of top-corner text links or standalone explainer hero panels
- coach workspace now also includes student follow-up and manually occupied-hour management using existing `Player` records, including coach-editable WhatsApp and Telegram contact data
- `/clubs/venues/[venueId]`: dedicated venue setup and venue roster

## Backend Split
- Ballbox Next.js host:
  - app shell
  - admin UI
  - shared auth/RBAC foundation
  - low-complexity CRUD
  - BFF aggregation and integration routes
  - shared `network` core entities
  - internal TVs module and runtime
- External specialized services:
  - none required for current payments callback/webhook flow (legacy bridge compatibility remains temporarily accepted)

## Implemented
- Shared admin shell and product-separated routes are in place.
- Admin shell copy now adapts per route instead of repeating one generic hero across every module.
- TVs now resolve admin/runtime state from a canonical internal Ballbox model with explicit club, venue, screen, and campaign ownership.
- TVs runtime now has a per-screen route and manifest-driven player UI.
- TVs now also expose a lightweight non-React runtime at `/player/[screenId]` for constrained devices.
- Prisma/Postgres foundation now exists in Ballbox for the durable replacement.
- Ballbox Postgres has the TVs schema pushed and seeded with current local fixture data.
- Network APIs now read/write Ballbox Postgres through Prisma.
- TVs source/manifest/impression/heartbeat paths already read/write against Ballbox Postgres.
- TVs admin now exposes direct runtime and manifest links per screen for operations.
- TVs errors now use a shared strict shape and the contract is validated by `pnpm test:contracts`.
- Payments integration health and sessions routes now report Ballbox-internal status plus MercadoPago configuration/webhook metadata.
- Payments approved-session callbacks now land in Ballbox, validate canonical payment correlation, and persist as `PaymentSession` records.
- Payments now have an admin CRUD/reconciliation surface over local `PaymentSession` rows via `/api/admin/payments/*`.
- Final target model is documented in `docs/domain-model-target.md`; end state does not keep retro-compatibility shims.
- ESLint is now wired and the repo exposes an explicit `typecheck` script.
- `/admin` and current admin-only APIs can now be gated by a shared password cookie session.
- `/admin/tvs` now performs admin CRUD for screens and tv devices through admin-only APIs under `/api/admin/tvs/*`.
- `/admin/tvs` now performs admin CRUD for campaigns, creatives, and assignments through admin-only APIs under `/api/admin/tvs/*`.
- TVs impression writes now validate creative-to-screen activity windows before persisting telemetry.
- Players now have DB-backed read/admin APIs for `Player` and `Membership`.
- Players route + page callers now import `lib/players-backend.ts` (and `lib/network-backend-clubs.ts` for club lists) directly instead of relying on `lib/ballbox-store.ts` for runtime access.
- Sports now have DB-backed read/admin APIs for `Coach`, `OpenMatch`, and `Class`.
- Classes MVP now also has DB-backed public APIs for `ClassOffer` and `ClassRequest`.
- `/classes/me` now provides a lightweight student request-history surface over Ballbox-local `ClassRequest` records (read-only, demo-grade email lookup).
- `ClassRequest` now snapshots the student's selected visible slot when they choose one, and otherwise persists an explicit flexible-selection fallback mode.
- Coach availability now ships as weekly ranges with 90-minute slots, filtered by coach club selection and ATC club availability.
- Coach and club account entry now starts at `/coaches`, with `Sign in` first and `Sign up` only for users without an account.
- Coaches and clubs now sign in with per-account email + password credentials, and `/api/account/session` routes each session to the correct dashboard.
- Coach sign-up now asks only for name, email, and password; if a club invited that coach, the invite link auto-links the account to that club.
- `/coaches/settings` now opens on a dashboard split into `Calendar` and `Analytics`, then lets coaches move into configuration from the shared navbar to choose external clubs, edit weekly availability only inside allowed venue/club windows, configure reusable class packages, and save a Mercado Pago placeholder handle.
- `/coaches/settings` now also lets coaches attach existing Ballbox players to concrete booked slots in their active venues, so taken hours, weekly earnings, consistency tracking, and lightweight student follow-up live inside the coach workspace.
- Club sign-in/sign-up now lands on `/clubs`, a service home that separates the club's operational modules before entering the detailed coaches workspace.
- Club accounts now have a dedicated workspace at `/clubs/coaches` split into `Calendar`, `Analytics`, `Sedes`, and `Profesores`, while venue-by-venue deep setup still lives at `/clubs/venues/[venueId]`, all under the same shared navbar/profile chrome.
- Venue setup now uses two simple fields for opening and closing time plus a collapsible weekly half-hour grid for coach hours, shows closed periods in red while editing coach availability, lets clubs mark whether external coaches are allowed, keeps coach-enabled courts in a dedicated selection panel, and includes reservation pricing/cancellation settings both in the venue detail and as quick settings on `/clubs/coaches`, including a discount that applies only to coaches assigned to that club.
- ATC lookups for class surfaces now resolve by club (no court coupling), and class request validation rechecks the selected slot date/time against ATC public-read.
- `/classes` SSR now avoids blocking first paint on ATC public-read fetches; the page paints local offer data first, then hydrates ATC availability from cached API responses.
- `/admin` overview SSR now uses direct aggregate counts instead of loading full admin datasets only to derive metrics.
- Sports/classes facade exports in `lib/ballbox-store.ts` now delegate to dedicated sports-events and classes backend modules instead of keeping that slice inline.
- Sports route + page callers now import `lib/sports-backend-events.ts` and `lib/sports-backend-coaches.ts` directly, so `lib/ballbox-store.ts` is no longer the primary sports entrypoint.
- TV runtime/backend facade exports in `lib/ballbox-store.ts` now delegate to `lib/tvs-backend-runtime.ts` instead of keeping source/manifest/heartbeat/impression logic inline.
- UI color/system tokens are split between landing (`app/styles/landing-tokens.css`) and admin (`app/styles/admin-tokens.css`).

## Streetcast Migration Check
- Source of truth for parity review: `/Users/sebas/Code/street-cast-pwa`.
- Covered in Ballbox today:
  - manifest polling exists in `components/screen-player.tsx`
  - sequential creative playback exists in `components/screen-player.tsx`
  - impression writes exist in `components/screen-player.tsx`
  - runtime health signaling exists via heartbeats in `components/screen-player.tsx`
- Missing or not yet verified against Streetcast behavior:
  - no manual real-device verification yet proves CacheStorage/object-URL playback outside local browser automation

## Next Steps (Priority + Dependency Queue)
- Source of truth for task backlog and dependencies: `docs/next-steps-queue.md`.
- Selection rule for the next agent: pick the highest-priority task marked `ready` in that doc.
- Current pick (`P0`, `ready`): `OPS-1` payments real-world validation closure.
- Immediately after `OPS-1`: `OPS-2` TVs runtime parity closure.
- Then continue with `P1` in dependency order: `ARCH-2` and `ARCH-3` -> `PERF-1` -> `PAY-2` -> `PAY-3`.
- Keep detailed backlog/status only in `docs/next-steps-queue.md` to avoid drift.

### Product Continuation Constraints
- Keep `sports` discovery honest: no claims beyond evidence in `docs/coaches-discovery-stage.md`.
- Next sports expansion after `/classes/me`: lightweight operator review for incoming requests.
- Expand ATC scope beyond public-read only after the request loop proves it is needed.

## Coach Discovery Docs
- Company-level strategic mirror for this track lives in `~/ballbox-company-agent/projects/coaches-os/`; source of truth for product-repo execution remains this `PLAN.md` plus local docs.
- Source of truth for current coach/padel/ATC discovery framing lives in `docs/coaches-discovery-stage.md`.
- Current form instrument is documented in `docs/coaches-form-current.md`.
- Current ATC API discovery is documented in `docs/atc-integration-discovery.md`.
- Working templates for this track live in `docs/coaches-opportunity-brief.md`, `docs/coaches-research-repository.md`, and `docs/coaches-validation-checklist-2-weeks.md`.
- Guided execution docs for the next discovery phase live in `docs/coaches-discovery-execution-plan.md`, `docs/coaches-icp-segmentation.md`, `docs/coaches-wedges-matrix.md`, and `docs/coaches-interview-script.md`.
- Next synthesis docs live in `docs/coaches-jtbd-workflow-map.md`, `docs/coaches-solution-thesis.md`, and `docs/atc-access-request-checklist.md`.
- Outreach and post-access planning docs live in `docs/coaches-design-partner-outreach.md` and `docs/atc-integration-spec-skeleton.md`.
- Implementation gap analysis for the earlier coach-coordination wedge lives in `docs/coaches-coordination-gap-analysis.md`.
- Current first non-admin public route plan lives in `docs/classes-subroute-plan.md`.
- Current MVP RFC lives in `docs/classes-mvp-rfc.md`.
- Minimum doc set for the sports discovery track, in order:
  1. opportunity brief for coaches/padel centers
  2. ICP plus segment scoring
  3. research repository for form results and interviews
  4. JTBD/workflow map based on real evidence
  5. solution thesis for the first wedge
  6. ATC integration discovery/spec only if the chosen wedge requires it
  7. MVP PRD for one workflow loop
  8. GTM note with ATC treated as a possible future channel, not current affiliation
- Until form/interview evidence exists, these docs must use hypothesis language and avoid claims that coach pains, willingness to pay, or ATC-dependent workflows are already validated.

## Auth Direction
- `/admin` should get a simple access layer first, with no product-level RBAC in the first pass.
- `/player` stays unauthenticated for now; do not block runtime rollout on device or screen identity flows yet.
- Server-to-server integrations should use shared secrets as the immediate default.
- Revisit stronger auth and role models only after the host app surfaces and integration boundaries stabilize.

## Persistence Direction
- Do not invest further in memory-backed or file-backed persistence for TVs.
- Ballbox PostgreSQL is the chosen durable store for TVs after migration.
- Ballbox must own the schema for the final shared `network` entities plus the downstream product models that reference them.
- No compatibility layer is required for local-only development; Ballbox can be reinvented directly on its own DB contract.

## Acceptance Criteria
- Landing still works at `/`.
- `/admin` exists and links to product-specific areas.
- `/admin/tvs`, `/admin/payments`, `/admin/network`, `/admin/sports`, `/admin/players`, and `/admin/screens` render in Ballbox.
- `app/api` contains a host-app BFF foundation for admin and integrations.
- TVs no longer depend on Streetcast or hardcoded ownership data; Ballbox exposes internal `/api/tvs/*` backed by Ballbox Postgres.
- Ballbox exposes the concrete per-screen TV runtime under `/player/[screenId]`.
- TVs migration is not complete until the Streetcast parity checklist is reviewed and every legacy runtime feature is marked as covered, intentionally dropped, or replaced.
- TVs runtime must support local cached playback for video creatives instead of relying only on remote streaming.
- Ballbox accepts approved payment callbacks under `/api/integrations/payments/approved` and persists them locally.
- Ballbox exposes network CRUD under `/admin/network` plus `/api/network*`.
- Final schema ownership and relationships match `docs/domain-model-target.md`.
- `pnpm lint`, `pnpm typecheck`, `pnpm test:contracts`, `pnpm test:e2e`, and `pnpm build` run successfully in local repo state.
- `pnpm db:push` plus `pnpm db:seed` must keep demo `ClassOffer` bootstrap aligned with the shipped `/classes` MVP.
- `pnpm test:contracts` passes for TVs contract enforcement.
- `pnpm test:e2e` passes for browser-level TVs cached playback verification.

## Agent Handoff
- Real DB in use: Ballbox Prisma schema was pushed to the Postgres configured in Ballbox `DATABASE_URL`.
- Seed already run successfully against that DB.
- Current DB counts after seed were:
  - clubs: 2
  - venues: 3
  - vendingMachines: 4
  - tvDevices: 2
  - screens: 2
  - advertisers: 2
  - campaigns: 2
  - creatives: 3
  - assignments: 3
  - products: 2
  - players: 3
  - memberships: 4
  - coaches: 2
  - openMatches: 2
  - classes: 2
- New DB-backed read/write layer lives in `lib/ballbox-store.ts`.
- Bridge-era in-memory TVs modules were removed; seed fixtures now live under `prisma/seed-data.ts`.
- Routes already moved:
  - `/api/network`
  - `/api/network/[clubId]`
  - `/api/tvs/heartbeat`
  - `/api/tvs/impression`
  - `/api/tvs/manifest/[screenId]` via `lib/tvs-source.ts`
  - `/api/tvs/screens` and `/api/tvs/health` via `lib/tvs-source.ts`
- TVs persistence cleanup is complete; do not reintroduce in-memory compatibility layers.

[toolResult]
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  globalSetup: "./tests/e2e/support/playwright-global-cleanup.ts",
  globalTeardown: "./tests/e2e/support/playwright-global-cleanup.ts",
  testDir: "./tests/e2e",
  reporter: "list",
  timeout: 45_000,
  use: {
    baseURL: "http://127.0.0.1:3010",
    screenshot: "only-on-failure",
    trace: "retain-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
      },
    },
  ],
  webServer: {
    command: "pnpm dev",
    url: "http://127.0.0.1:3010",
    reuseExistingServer: true,
    stdout: "pipe",
    stderr: "pipe",
    env: {
      ...process.env,
      BALLBOX_ADMIN_PASSWORD: "",
      ATC_PUBLIC_API_BASE_URL: "http://127.0.0.1:3010/api/atc-public",
      ATC_PUBLIC_VALIDATE: "false",
    },
    timeout: 120_000,
  },
});

[toolResult]
{
  "name": "my-v0-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "node scripts/next-build-safe.mjs",
    "build:prod-local": "dotenv -e .env.production -- next build",
    "docs:check": "node scripts/check-doc-references.mjs",
    "db:generate": "dotenv -e .env.local -- prisma generate",
    "db:backfill-atc-clubs": "dotenv -e .env.local -- tsx scripts/backfill-atc-clubs.ts",
    "db:local:down": "docker compose down",
    "db:local:logs": "docker compose logs -f postgres",
    "db:local:up": "docker compose up -d postgres",
    "db:push": "dotenv -e .env.local -- prisma db push",
    "db:revalidate-atc-clubs": "dotenv -e .env.local -- tsx scripts/revalidate-atc-clubs.ts",
    "db:seed": "dotenv -e .env.local -- tsx prisma/seed.ts",
    "db:studio": "dotenv -e .env.local -- prisma studio",
    "dev": "next dev --port 3010",
    "prepare": "node scripts/install-git-hooks.mjs",
    "lint": "eslint .",
    "postinstall": "prisma generate",
    "pretypecheck": "rm -rf .next/types .next/dev/types",
    "test:contracts": "vitest run tests/contracts",
    "test:unit": "vitest run tests/unit",
    "test:integration": "vitest run tests/integration --passWithNoTests",
    "test:e2e": "playwright test",
    "typecheck": "tsc --noEmit --incremental false",
    "start": "next start",
    "start:prod-local": "dotenv -e .env.production -- next start"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.10.0",
    "@prisma/client": "^5.22.0",
    "@radix-ui/react-accordion": "1.2.2",
    "@radix-ui/react-alert-dialog": "1.1.4",
    "@radix-ui/react-aspect-ratio": "1.1.1",
    "@radix-ui/react-avatar": "1.1.2",
    "@radix-ui/react-checkbox": "1.1.3",
    "@radix-ui/react-collapsible": "1.1.2",
    "@radix-ui/react-context-menu": "2.2.4",
    "@radix-ui/react-dialog": "1.1.4",
    "@radix-ui/react-dropdown-menu": "2.1.4",
    "@radix-ui/react-hover-card": "1.1.4",
    "@radix-ui/react-label": "2.1.1",
    "@radix-ui/react-menubar": "1.1.4",
    "@radix-ui/react-navigation-menu": "1.2.3",
    "@radix-ui/react-popover": "1.1.4",
    "@radix-ui/react-progress": "1.1.1",
    "@radix-ui/react-radio-group": "1.2.2",
    "@radix-ui/react-scroll-area": "1.2.2",
    "@radix-ui/react-select": "2.1.4",
    "@radix-ui/react-separator": "1.1.1",
    "@radix-ui/react-slider": "1.2.2",
    "@radix-ui/react-slot": "1.1.1",
    "@radix-ui/react-switch": "1.1.2",
    "@radix-ui/react-tabs": "1.1.2",
    "@radix-ui/react-toast": "1.2.4",
    "@radix-ui/react-toggle": "1.1.1",
    "@radix-ui/react-toggle-group": "1.1.1",
    "@radix-ui/react-tooltip": "1.1.6",
    "@vercel/analytics": "1.3.1",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.0.4",
    "date-fns": "4.1.0",
    "embla-carousel-react": "8.5.1",
    "input-otp": "1.4.1",
    "lucide-react": "^0.454.0",
    "next": "16.2.1",
    "next-themes": "^0.4.6",
    "react": "19.2.0",
    "react-day-picker": "9.8.0",
    "react-dom": "19.2.0",
    "react-hook-form": "^7.60.0",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.4",
    "sonner": "^1.7.4",
    "tailwind-merge": "^3.3.1",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^1.1.2",
    "zod": "3.25.76"
  },
  "devDependencies": {
    "@playwright/test": "^1.58.2",
    "@tailwindcss/postcss": "^4.1.9",
    "@testing-library/dom": "^10.4.1",
    "@testing-library/react": "^16.3.2",
    "@types/node": "^22",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "baseline-browser-mapping": "^2.10.9",
    "dotenv-cli": "^11.0.0",
    "eslint": "^9.39.4",
    "eslint-config-next": "16.2.1",
    "jsdom": "^29.0.1",
    "postcss": "^8.5",
    "prisma": "^5.22.0",
    "tailwindcss": "^4.1.9",
    "tsx": "^4.20.5",
    "tw-animate-css": "1.3.3",
    "typescript": "^5",
    "vitest": "^4.1.0"
  }
}

[Assistant]
set -e
cd /mnt/rpi/repos/ballbox
printf '=== focused player/runtime files ===\n'; find app components lib tests/e2e scripts -path '*/node_modules' -prune -o \( -iname '*player*' -o -iname '*screen*' -o -iname '*tv*' -o -iname '*snapshot*' -o -iname '*export*' \) -type f -print | sort | sed -n '1,300p'
printf '\n=== e2e file contents names ===\n'; for f in tests/e2e/* tests/e2e/**/*; do [ -f "$f" ] && echo "--- $f"; done
printf '\n=== grep export parity snapshot ===\n'; rg -n "Export parity snapshot|parity snapshot|download.*json|blob|CacheStorage|offline|impression queue|heartbeat" app components lib tests/e2e scripts --glob '!node_modules'
printf '\n=== locate /player route ===\n'; rg -n "export default.*player|/player/\[screenId\]|screenId" app components lib --glob '!node_modules' | head -400

[toolResult]
=== focused player/runtime files ===
components/players-admin-client-config.ts
components/players-admin-client.tsx
components/players-admin-create-player-section.tsx
components/players-admin-player-card.tsx
components/players-admin-player-list.tsx
components/players-admin-shell-styles.ts
components/players-admin-shell.tsx
components/players-admin-stats.tsx
components/screen-player.tsx
components/screens-runtime-grid.tsx
components/tvs-admin-client.tsx
components/use-players-admin-client-state.ts
lib/contracts/admin-players.ts
lib/contracts/admin-tvs.ts
lib/contracts/players.ts
lib/contracts/tvs-invariants.ts
lib/contracts/tvs.ts
lib/http/players-route-helpers.ts
lib/players-backend.ts
lib/tv-heartbeats.ts
lib/tvs-backend-assignments.ts
lib/tvs-backend-campaigns.ts
lib/tvs-backend-creatives.ts
lib/tvs-backend-mappers.ts
lib/tvs-backend-operations.ts
lib/tvs-backend-runtime.ts
lib/tvs-backend-screens.ts
lib/tvs-backend-tv-devices.ts
lib/tvs-player-runtime-core.ts
lib/tvs-runtime.ts
lib/tvs-source.ts
tests/e2e/player-offline.spec.ts
tests/e2e/player-runtime-side-effects.spec.ts
tests/e2e/players-admin.spec.ts
tests/e2e/tvs-admin.spec.ts
tests/e2e/tvs-offline.spec.ts
tests/e2e/tvs-runtime-side-effects.spec.ts

=== e2e file contents names ===
--- tests/e2e/admin-mobile-smoke.spec.ts
--- tests/e2e/admin-navigation-and-network.spec.ts
--- tests/e2e/classes.spec.ts
--- tests/e2e/payments-admin.spec.ts
--- tests/e2e/player-offline.spec.ts
--- tests/e2e/player-runtime-side-effects.spec.ts
--- tests/e2e/players-admin.spec.ts
--- tests/e2e/sports-admin.spec.ts
--- tests/e2e/tvs-admin.spec.ts
--- tests/e2e/tvs-offline.spec.ts
--- tests/e2e/tvs-runtime-side-effects.spec.ts
--- tests/e2e/support/admin-auth.ts
--- tests/e2e/support/cleanup-playwright-db.ts
--- tests/e2e/support/playwright-global-cleanup.ts
--- tests/e2e/support/test-fixtures.ts

=== grep export parity snapshot ===
lib/tvs-backend-mappers.ts:4:import { getTvStatusFromLastSeen } from "@/lib/tv-heartbeats";
lib/tvs-backend-mappers.ts:19:  if (!lastSeen) return "offline" as const;
lib/tv-heartbeats.ts:9:const heartbeats = new Map<string, HeartbeatRecord>();
lib/tv-heartbeats.ts:12:  const heartbeat = {
lib/tv-heartbeats.ts:17:  heartbeats.set(screenId, heartbeat);
lib/tv-heartbeats.ts:18:  return heartbeat;
lib/tv-heartbeats.ts:22:  return heartbeats.get(screenId) ?? null;
lib/tv-heartbeats.ts:30:  return "offline" as const;
lib/ballbox-store.ts:106:  status: "online" | "warning" | "offline";
lib/ballbox-store.ts:152:  operationalStatus: "online" | "warning" | "offline";
components/screens-runtime-grid.tsx:15:  offline: "Offline",
components/screens-runtime-grid.tsx:21:  offline: "border-rose-400/40 bg-rose-400/15 text-rose-200",
lib/contracts/tvs.ts:5:export const tvScreenStatusSchema = z.enum(["online", "warning", "offline"]);
lib/tvs-player-runtime-core.ts:264:      const blob = await cachedResponse.blob();
lib/tvs-player-runtime-core.ts:265:      const objectUrl = URL.createObjectURL(blob);
lib/tvs-player-runtime-core.ts:293:      const response = await fetch("/api/tvs/heartbeat", {
components/screen-player.tsx:24:      window.addEventListener("offline", callback);
components/screen-player.tsx:27:        window.removeEventListener("offline", callback);
components/screen-player.tsx:30:    () => (navigator.onLine ? "online" : "offline"),
components/screen-player.tsx:114:      ? "blob"
components/screen-player.tsx:171:
Last heartbeat: {snapshot.lastHeartbeatAt ? new Date(snapshot.lastHeartbeatAt).toLocaleString("es-AR") : "pending"}

app/api/tvs/heartbeat/route.ts:22:  const heartbeat = await recordScreenHeartbeatInDb(parsedBody.data.screenId);
app/api/tvs/heartbeat/route.ts:24:  if (!heartbeat.ok) {
app/api/tvs/heartbeat/route.ts:25:    const status = heartbeat.error === "SCREEN_TV_DEVICE_NOT_LINKED" ? 409 : 404;
app/api/tvs/heartbeat/route.ts:26:    return NextResponse.json(createTvsErrorResponse(heartbeat.error, status), { status });
app/api/tvs/heartbeat/route.ts:32:    screenId: heartbeat.screenId,
app/api/tvs/heartbeat/route.ts:33:    lastSeen: heartbeat.lastSeen,
app/player/[screenId]/route.ts:125:

app/player/[screenId]/route.ts:131:              <button class="top-link" id="diag-export" type="button">Export parity snapshot</button>
tests/e2e/tvs-runtime-side-effects.spec.ts:27:test("screen runtime records heartbeat and impression side effects", async ({ page }) => {
tests/e2e/tvs-runtime-side-effects.spec.ts:30:  const heartbeatResponsePromise = page.waitForResponse(
tests/e2e/tvs-runtime-side-effects.spec.ts:31:    (response) => response.request().method() === "POST" && response.url().endsWith("/api/tvs/heartbeat")
tests/e2e/tvs-runtime-side-effects.spec.ts:39:  const heartbeatResponse = await heartbeatResponsePromise;
tests/e2e/tvs-runtime-side-effects.spec.ts:42:  expect(heartbeatResponse.status()).toBe(200);
tests/e2e/tvs-runtime-side-effects.spec.ts:45:  const heartbeatBody = (await heartbeatResponse.json()) as { ok: boolean; screenId: string; lastSeen?: string };
tests/e2e/tvs-runtime-side-effects.spec.ts:49:  expect(heartbeatBody.ok).toBe(true);
tests/e2e/tvs-runtime-side-effects.spec.ts:50:  expect(heartbeatBody.screenId).toBe("screen_001");
tests/e2e/tvs-runtime-side-effects.spec.ts:51:  expect(heartbeatBody.lastSeen).toBeTruthy();
tests/e2e/tvs-runtime-side-effects.spec.ts:57:  await expect(page.getByText(/^Last heartbeat:/)).not.toContainText("pending");
tests/e2e/player-runtime-side-effects.spec.ts:27:test("lightweight player records heartbeat and impression side effects", async ({ page }) => {
tests/e2e/player-runtime-side-effects.spec.ts:30:  const heartbeatResponsePromise = page.waitForResponse(
tests/e2e/player-runtime-side-effects.spec.ts:31:    (response) => response.request().method() === "POST" && response.url().endsWith("/api/tvs/heartbeat")
tests/e2e/player-runtime-side-effects.spec.ts:39:  const heartbeatResponse = await heartbeatResponsePromise;
tests/e2e/player-runtime-side-effects.spec.ts:42:  expect(heartbeatResponse.status()).toBe(200);
tests/e2e/player-runtime-side-effects.spec.ts:45:  const heartbeatBody = (await heartbeatResponse.json()) as { ok: boolean; screenId: string; lastSeen?: string };
tests/e2e/player-runtime-side-effects.spec.ts:49:  expect(heartbeatBody.ok).toBe(true);
tests/e2e/player-runtime-side-effects.spec.ts:50:  expect(heartbeatBody.screenId).toBe("screen_001");
tests/e2e/player-runtime-side-effects.spec.ts:51:  expect(heartbeatBody.lastSeen).toBeTruthy();
tests/e2e/player-runtime-side-effects.spec.ts:57:  await expect(page.getByText(/^Last heartbeat:/)).not.toContainText("pending");
tests/e2e/tvs-offline.spec.ts:3:test("screen runtime keeps cached playback when browser goes offline", async ({ browser }) => {
tests/e2e/tvs-offline.spec.ts:15:  await expect(page.getByText("Active source: blob")).toBeVisible();
tests/e2e/tvs-offline.spec.ts:21:  await expect(video).toHaveAttribute("src", /blob:/);
tests/e2e/tvs-offline.spec.ts:27:  await expect(page.getByText("Active source: blob")).toBeVisible();
tests/e2e/tvs-offline.spec.ts:28:  await expect(page.getByText("Network: offline")).toBeVisible();
tests/e2e/tvs-offline.spec.ts:29:  await expect(video).toHaveAttribute("src", /blob:/);
tests/e2e/player-offline.spec.ts:3:test("lightweight player keeps cached playback when browser goes offline", async ({ browser }) => {
tests/e2e/player-offline.spec.ts:16:  await expect(page.getByText("Active source: blob")).toBeVisible();
tests/e2e/player-offline.spec.ts:22:  await expect(video).toHaveAttribute("src", /blob:/);
tests/e2e/player-offline.spec.ts:28:  await expect(page.getByText("Active source: blob")).toBeVisible();
tests/e2e/player-offline.spec.ts:29:  await expect(page.getByText("Network: offline")).toBeVisible();
tests/e2e/player-offline.spec.ts:30:  await expect(video).toHaveAttribute("src", /blob:/);

=== locate /player route ===
lib/tvs-backend-mappers.ts:113:    screenId: string;
lib/tvs-backend-mappers.ts:136:      screenId: assignment.screenId,
lib/tvs-backend-screens.ts:37:      return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
lib/tvs-backend-screens.ts:54:        screenId: screen.id,
lib/tvs-backend-screens.ts:59:  return { ok: true as const, screenId: screen.id };
lib/tvs-backend-screens.ts:63:  screenId: string,
lib/tvs-backend-screens.ts:73:    where: { id: screenId },
lib/tvs-backend-screens.ts:106:    if (tvDevice.screen && tvDevice.screen.id !== screenId) {
lib/tvs-backend-screens.ts:107:      return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
lib/tvs-backend-screens.ts:112:    where: { id: screenId },
lib/tvs-backend-screens.ts:125:        data: { screenId: null },
lib/tvs-backend-screens.ts:132:        data: { screenId },
lib/tvs-backend-screens.ts:137:  return { ok: true as const, screenId };
lib/tvs-backend-screens.ts:140:export async function deleteTvScreenInDb(screenId: string) {
lib/tvs-backend-screens.ts:142:    where: { id: screenId },
lib/tvs-backend-screens.ts:173:        screenId: null,
lib/tvs-backend-screens.ts:178:  await prisma.screen.delete({ where: { id: screenId } });
lib/tv-heartbeats.ts:2:  screenId: string;
lib/tv-heartbeats.ts:11:export function recordTvHeartbeat(screenId: string, timestamp = new Date()) {
lib/tv-heartbeats.ts:13:    screenId,
lib/tv-heartbeats.ts:17:  heartbeats.set(screenId, heartbeat);
lib/tv-heartbeats.ts:21:export function getTvHeartbeat(screenId: string) {
lib/tv-heartbeats.ts:22:  return heartbeats.get(screenId) ?? null;
lib/network-backend-clubs.ts:29:      screenId: string | null;
lib/network-backend-clubs.ts:32:    screenIds: string[];
lib/network-backend-clubs.ts:89:        screenId: machine.screen?.id ?? null,
lib/network-backend-clubs.ts:92:      screenIds: venue.screens.map((screen) => screen.id),
lib/tvs-backend-runtime.ts:136:export async function getTvManifestFromDb(screenId: string) {
lib/tvs-backend-runtime.ts:139:    where: { id: screenId },
lib/tvs-backend-runtime.ts:178:      manifest: createEmptyTvManifest(screenId),
lib/tvs-backend-runtime.ts:185:    screenId,
lib/tvs-backend-runtime.ts:191:export async function recordScreenHeartbeatInDb(screenId: string, timestamp = new Date()) {
lib/tvs-backend-runtime.ts:193:    where: { id: screenId },
lib/tvs-backend-runtime.ts:221:    screenId: screen.id,
lib/tvs-backend-runtime.ts:226:export async function recordImpressionInDb(screenId: string, creativeId: string) {
lib/tvs-backend-runtime.ts:229:    where: { id: screenId },
lib/ballbox-store.ts:168:  screenId: string;
components/network-admin-venue-card.tsx:129:        {venue.screenIds.length > 0 ? ` · ${venue.screenIds.join(", ")}` : ""}
components/network-admin-venue-card.tsx:150:                screen {machine.screenId ?? "none"}
lib/contracts/admin-tvs.ts:119:  screenId: z.string().trim().min(1, "Screen is required"),
lib/contracts/admin-tvs.ts:127:    screenId: z.string().trim().min(1, "Screen is required").optional(),
lib/contracts/admin-tvs.ts:134:      value.screenId !== undefined ||
components/tvs-admin-client.tsx:62:  screenId: "",
components/tvs-admin-client.tsx:661:                  screenId: adminData.screens[0]?.id ?? "",
components/tvs-admin-client.tsx:922:                        value={assignmentForm.screenId}
components/tvs-admin-client.tsx:926:                            [campaign.id]: { ...assignmentForm, screenId: event.target.value },
components/tvs-admin-client.tsx:962:                        disabled={busy || assignmentForm.screenId.length === 0}
components/tvs-admin-client.tsx:969:                                screenId: assignmentForm.screenId,
components/tvs-admin-client.tsx:981:                              [campaign.id]: { ...emptyAssignmentForm, screenId: adminData.screens[0]?.id ?? "" },
components/tvs-admin-client.tsx:995:                              value={assignment.screenId}
components/tvs-admin-client.tsx:1008:                                                  screenId: event.target.value,
components/tvs-admin-client.tsx:1115:                                      screenId: assignment.screenId,
lib/contracts/network.ts:98:    screenId: z.string().min(1).nullable().optional(),
lib/contracts/network.ts:110:    screenIds: z.array(z.string().min(1)),
lib/contracts/tvs.ts:27:    screenId: z.string().min(1),
lib/contracts/tvs.ts:33:    screenId: z.string().min(1),
lib/contracts/tvs.ts:101:    screenId: z.string().min(1),
lib/contracts/tvs.ts:110:    screenId: z.string().min(1),
lib/contracts/tvs.ts:139:export function createEmptyTvManifest(screenId: string) {
lib/contracts/tvs.ts:141:    version: `ballbox-db-empty-${screenId}`,
lib/contracts/tvs.ts:142:    screenId,
lib/network-backend-vending-machines.ts:68:    screenId: updatedMachine.screen?.id ?? null,
lib/network-backend-vending-machines.ts:95:      screenId: machine.screen.id,
components/screen-player.tsx:8:  screenId: string;
components/screen-player.tsx:12:export function ScreenPlayer({ screenId, initialManifest }: ScreenPlayerProps) {
components/screen-player.tsx:15:      screenId,
components/screen-player.tsx:123:
La pantalla {screenId} no tiene contenido activo.

components/screen-player.tsx:147:            {screenId}
lib/tvs-player-runtime-core.ts:21:  screenId,
lib/tvs-player-runtime-core.ts:24:  screenId: string;
lib/tvs-player-runtime-core.ts:101:      screenId: id,
lib/tvs-player-runtime-core.ts:137:      const raw = window.localStorage.getItem(getTvImpressionQueueStorageKey(screenId));
lib/tvs-player-runtime-core.ts:155:          typeof candidate.screenId === "string" &&
lib/tvs-player-runtime-core.ts:167:    window.localStorage.setItem(getTvImpressionQueueStorageKey(screenId), JSON.stringify(queue));
lib/tvs-player-runtime-core.ts:225:    const cache = await caches.open(getTvMediaCacheName(screenId));
lib/tvs-player-runtime-core.ts:298:        body: JSON.stringify({ screenId }),
lib/tvs-player-runtime-core.ts:320:          screenId,
lib/tvs-player-runtime-core.ts:374:    const queue = enqueueImpressionRetry(readQueue(), createImpressionRetryItem(screenId, creativeId));
lib/tvs-player-runtime-core.ts:381:      const response = await fetch(`/api/tvs/manifest/${screenId}`, {
lib/tvs-runtime.ts:11:  screenId: string;
lib/tvs-runtime.ts:18:  screenId: string;
lib/tvs-runtime.ts:37:export function getTvMediaCacheName(screenId: string) {
lib/tvs-runtime.ts:38:  return `${TVS_MEDIA_CACHE_PREFIX}:${screenId}`;
lib/tvs-runtime.ts:41:export function getTvImpressionQueueStorageKey(screenId: string) {
lib/tvs-runtime.ts:42:  return `${TVS_IMPRESSION_QUEUE_PREFIX}:${screenId}`;
lib/tvs-runtime.ts:63:export function createImpressionRetryItem(screenId: string, creativeId: string, queuedAt = new Date()) {
lib/tvs-runtime.ts:67:    id: `${screenId}:${creativeId}:${queuedAtIso}`,
lib/tvs-runtime.ts:68:    screenId,
lib/tvs-source.ts:7:export async function getTvManifestFromSource(screenId: string) {
lib/tvs-source.ts:8:  return getTvManifestFromDb(screenId);
lib/tvs-source.ts:11:export async function reportTvImpression(screenId: string, creativeId: string) {
lib/tvs-source.ts:12:  return recordImpressionInDb(screenId, creativeId);
lib/tvs-backend-tv-devices.ts:93:    return { ok: false as const, error: "TV_DEVICE_LINKED_TO_SCREEN" as const, screenId: tvDevice.screen.id };
lib/tvs-backend-assignments.ts:17:    screenId: string;
lib/tvs-backend-assignments.ts:25:    prisma.screen.findUnique({ where: { id: input.screenId }, select: { id: true } }),
lib/tvs-backend-assignments.ts:35:        screenId: input.screenId,
lib/tvs-backend-assignments.ts:55:    screenId?: string;
lib/tvs-backend-assignments.ts:68:  if (input.screenId) {
lib/tvs-backend-assignments.ts:70:      where: { id: input.screenId },
lib/tvs-backend-assignments.ts:81:        screenId: input.screenId,
app/api/tvs/heartbeat/route.ts:16:        reason: "screenId is required",
app/api/tvs/heartbeat/route.ts:22:  const heartbeat = await recordScreenHeartbeatInDb(parsedBody.data.screenId);
app/api/tvs/heartbeat/route.ts:32:    screenId: heartbeat.screenId,
app/api/tvs/impression/route.ts:16:        reason: "screenId and creativeId are required",
app/api/tvs/impression/route.ts:22:  const result = await reportTvImpression(parsedBody.data.screenId, parsedBody.data.creativeId);
app/api/tvs/manifest/[screenId]/route.ts:10:  context: { params: Promise<{ screenId: string }> }
app/api/tvs/manifest/[screenId]/route.ts:12:  const { screenId } = await context.params;
app/api/tvs/manifest/[screenId]/route.ts:13:  const manifest = await getTvManifestFromSource(screenId);
app/api/tvs/manifest/[screenId]/route.ts:24:      screenId: manifest.screenId,
app/player/[screenId]/route.ts:63:function renderMissingScreenHtml(screenId: string) {
app/player/[screenId]/route.ts:77:
No existe una pantalla con id ${escapeHtml(screenId)}.

app/player/[screenId]/route.ts:88:  screenId: string;
app/player/[screenId]/route.ts:96:    <title>Ballbox Player ${escapeHtml(bootstrap.screenId)}</title>
app/player/[screenId]/route.ts:103:
${escapeHtml(bootstrap.screenId)}

app/player/[screenId]/route.ts:111:            Manifest
app/player/[screenId]/route.ts:147:  context: { params: Promise<{ screenId: string }> }
app/player/[screenId]/route.ts:149:  const { screenId } = await context.params;
app/player/[screenId]/route.ts:150:  const manifest = await getTvManifestFromSource(screenId);
app/player/[screenId]/route.ts:153:    return new Response(renderMissingScreenHtml(screenId), {
app/player/[screenId]/route.ts:162:  const initialManifest = !manifest.ok ? createEmptyTvManifest(screenId) : manifest;
app/player/[screenId]/route.ts:166:      screenId,
app/admin/screens/page.tsx:9:  { label: "TV device sync", value: "Polling internal /api/tvs/manifest/:screenId" },
app/api/admin/tvs/screens/[screenId]/route.ts:7:  context: { params: Promise<{ screenId: string }> }
app/api/admin/tvs/screens/[screenId]/route.ts:9:  const { screenId } = await context.params;
app/api/admin/tvs/screens/[screenId]/route.ts:20:  const result = await updateTvScreenInDb(screenId, parsedBody.data);
app/api/admin/tvs/screens/[screenId]/route.ts:38:  context: { params: Promise<{ screenId: string }> }
app/api/admin/tvs/screens/[screenId]/route.ts:40:  const { screenId } = await context.params;
app/api/admin/tvs/screens/[screenId]/route.ts:41:  const result = await deleteTvScreenInDb(screenId);

[toolResult]
=== pwd ===
/home/sebas

=== which playwright / playwright-mcp / node / npm / pnpm / npx ===
/usr/bin/node
/usr/bin/npm
/home/sebas/.local/bin/pnpm
/usr/bin/npx

=== npm globals top ===
/home/sebas/.npm-global/lib/node_modules

=== list likely dirs ===
--- /home/sebas
  /home/sebas/.claude/shell-snapshots
  /home/sebas/.claude/shell-snapshots/snapshot-bash-1768524481126-ww61n2.sh
  /home/sebas/.claude/shell-snapshots/snapshot-bash-1769561545343-fw08a3.sh
  /home/sebas/.claude/shell-snapshots/snapshot-bash-1769603556547-3xzhc8.sh
  /home/sebas/.codex/shell_snapshots
  /home/sebas/.agents/opencode-harness-install
  /home/sebas/opencode-harness
  /home/sebas/bug-bounty/reports/RAGFlow-MCP-SSRF-2026-02-13.md
--- /home/sebas/.config
--- /home/sebas/.cache
--- /home/sebas/.local
  /home/sebas/.local/share/opencode/snapshot
--- /home/sebas/.local/bin
--- /home/sebas/.npm-global
--- /mnt/rpi/repos
  /mnt/rpi/repos/opencode-harness
  /mnt/rpi/repos/FelipeVersion/.next/export-marker.json
  /mnt/rpi/repos/FelipeVersion/tests/e2e
  /mnt/rpi/repos/FelipeVersion/node_modules/playwright
  /mnt/rpi/repos/FelipeVersion/node_modules/playwright-core
  /mnt/rpi/repos/FelipeVersion/node_modules/node-exports-info
  /mnt/rpi/repos/FelipeVersion/node_modules/@playwright
  /mnt/rpi/repos/FelipeVersion/playwright.config.ts
  /mnt/rpi/repos/ballbox/.next/export-marker.json
  /mnt/rpi/repos/ballbox/tests/e2e
  /mnt/rpi/repos/ballbox/node_modules/@playwright
  /mnt/rpi/repos/ballbox/playwright.config.ts

=== piped package.json hits ===
  /home/sebas/.opencode/node_modules/multipasta/dist/esm/package.json
  /home/sebas/.opencode/node_modules/multipasta/package.json
  /home/sebas/.opencode/node_modules/shebang-command/package.json
  /home/sebas/.opencode/node_modules/which/package.json
  /home/sebas/.opencode/node_modules/ini/package.json
  /home/sebas/.opencode/node_modules/kubernetes-types/package.json
  /home/sebas/.opencode/node_modules/@standard-schema/spec/package.json
  /home/sebas/.opencode/node_modules/msgpackr/package.json
  /home/sebas/.opencode/node_modules/@opencode-ai/sdk/package.json
  /home/sebas/.opencode/node_modules/@opencode-ai/plugin/package.json
  /home/sebas/.opencode/node_modules/path-key/package.json
  /home/sebas/.opencode/node_modules/detect-libc/package.json
  /home/sebas/.opencode/node_modules/zod/mini/package.json
  /home/sebas/.opencode/node_modules/zod/v4-mini/package.json
  /home/sebas/.opencode/node_modules/zod/locales/package.json
  /home/sebas/.opencode/node_modules/zod/v4/mini/package.json
  /home/sebas/.opencode/node_modules/zod/v4/locales/package.json
  /home/sebas/.opencode/node_modules/zod/v4/core/package.json
  /home/sebas/.opencode/node_modules/zod/v4/package.json
  /home/sebas/.opencode/node_modules/zod/v4/classic/package.json
  /home/sebas/.opencode/node_modules/zod/v3/package.json
  /home/sebas/.opencode/node_modules/zod/package.json
  /home/sebas/.opencode/node_modules/fast-check/lib/cjs/package.json
  /home/sebas/.opencode/node_modules/fast-check/package.json
  /home/sebas/.opencode/node_modules/shebang-regex/package.json
  /home/sebas/.opencode/node_modules/uuid/package.json
  /home/sebas/.opencode/node_modules/effect/package.json
  /home/sebas/.opencode/node_modules/node-gyp-build-optional-packages/package.json
  /home/sebas/.opencode/node_modules/isexe/package.json
  /home/sebas/.opencode/node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64/package.json
  /home/sebas/.opencode/node_modules/yaml/browser/package.json
  /home/sebas/.opencode/node_modules/yaml/package.json
  /home/sebas/.opencode/node_modules/toml/package.json
  /home/sebas/.opencode/node_modules/msgpackr-extract/package.json
  /home/sebas/.opencode/node_modules/find-my-way-ts/dist/esm/package.json
  /home/sebas/.opencode/node_modules/find-my-way-ts/QueryString/package.json
  /home/sebas/.opencode/node_modules/find-my-way-ts/package.json
  /home/sebas/.opencode/node_modules/pure-rand/lib/esm/package.json
  /home/sebas/.opencode/node_modules/pure-rand/package.json
  /home/sebas/.opencode/node_modules/cross-spawn/package.json
  /home/sebas/.opencode/package.json
  /home/sebas/.bun/install/cache/braces@3.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/tr46@0.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/zod@3.24.2@@@1/package.json
  /home/sebas/.bun/install/cache/arctic@2.3.4@@@1/package.json
  /home/sebas/.bun/install/cache/fill-range@7.1.1@@@1/package.json
  /home/sebas/.bun/install/cache/web-tree-sitter@0.24.5@@@1/package.json
  /home/sebas/.bun/install/cache/bash-language-server@5.6.0@@@1/package.json
  /home/sebas/.bun/install/cache/@one-ini/wasm@0.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/@standard-schema/spec@1.0.0-7b1dbbf11ca1d290@@@1/package.json
  /home/sebas/.bun/install/cache/.tmp/.99a7f7a3fb633bee-00000001.plugin/package.json
  /home/sebas/.bun/install/cache/hono@4.12.8@@@1/dist/types/package.json
  /home/sebas/.bun/install/cache/hono@4.12.8@@@1/dist/cjs/package.json
  /home/sebas/.bun/install/cache/hono@4.12.8@@@1/package.json
  /home/sebas/.bun/install/cache/to-regex-range@5.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.2.27@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.3.7@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.3.3@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.3.7@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.3.0@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/plugin@1.2.27@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.3.3@@@1/package.json
  /home/sebas/.bun/install/cache/@opencode-ai/sdk@1.3.0@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-textdocument@1.0.12@@@1/lib/esm/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-textdocument@1.0.12@@@1/package.json
  /home/sebas/.bun/install/cache/@nodelib/fs.walk@1.2.8@@@1/package.json
  /home/sebas/.bun/install/cache/@nodelib/fs.stat@2.0.5@@@1/package.json
  /home/sebas/.bun/install/cache/@nodelib/fs.scandir@2.1.5@@@1/package.json
  /home/sebas/.bun/install/cache/brace-expansion@2.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/commander@13.1.0@@@1/package.json
  /home/sebas/.bun/install/cache/is-number@7.0.0@@@1/package.json
  /home/sebas/.bun/install/cache/aws4fetch@1.0.20@@@1/package.json
  /home/sebas/.bun/install/cache/jose@5.9.6@@@1/dist/node/esm/package.json
  /home/sebas/.bun/install/cache/jose@5.9.6@@@1/dist/browser/package.json
  /home/sebas/.bun/install/cache/jose@5.9.6@@@1/package.json
  /home/sebas/.bun/install/cache/micromatch@4.0.8@@@1/package.json
  /home/sebas/.bun/install/cache/fuzzy-search@3.2.1@@@1/package.json
  /home/sebas/.bun/install/cache/fast-glob@3.3.3@@@1/package.json
  /home/sebas/.bun/install/cache/editorconfig@2.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-protocol@3.17.2@@@1/package.json
  /home/sebas/.bun/install/cache/reusify@1.1.0@@@1/package.json
  /home/sebas/.bun/install/cache/picomatch@2.3.2@@@1/package.json
  /home/sebas/.bun/install/cache/pyright@1.1.408@@@1/package.json
  /home/sebas/.bun/install/cache/turndown@7.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/balanced-match@1.0.2@@@1/package.json
  /home/sebas/.bun/install/cache/is-glob@4.0.3@@@1/package.json
  /home/sebas/.bun/install/cache/semver@7.7.4@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver-types@3.17.2@@@1/package.json
  /home/sebas/.bun/install/cache/webidl-conversions@3.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/is-extglob@2.1.1@@@1/package.json
  /home/sebas/.bun/install/cache/run-parallel@1.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/glob-parent@5.1.2@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-jsonrpc@8.0.2@@@1/package.json
  /home/sebas/.bun/install/cache/queue-microtask@1.2.3@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/crypto@1.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/binary@1.0.0@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/encoding@1.1.0@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/encoding@0.4.1@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/asn1@1.0.0@@@1/package.json
  /home/sebas/.bun/install/cache/@oslojs/jwt@0.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/minimatch@10.0.1@@@1/dist/esm/package.json
  /home/sebas/.bun/install/cache/minimatch@10.0.1@@@1/dist/commonjs/package.json
  /home/sebas/.bun/install/cache/minimatch@10.0.1@@@1/package.json
  /home/sebas/.bun/install/cache/vscode-languageserver@8.0.2@@@1/package.json
  /home/sebas/.bun/install/cache/@openauthjs/openauth@0.4.3@@@1/package.json
  /home/sebas/.bun/install/cache/@mixmark-io/domino@2.2.0@@@1/package.json
  /home/sebas/.bun/install/cache/fastq@1.20.1@@@1/package.json
  /home/sebas/.bun/install/cache/merge2@1.4.1@@@1/package.json
  /home/sebas/.bun/install/cache/node-fetch@2.7.0@@@1/package.json
  /home/sebas/.bun/install/cache/opencode-anthropic-auth@0.0.13@@@1/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/mini/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4-mini/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/locales/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/mini/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/locales/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/core/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v4/classic/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/v3/package.json
  /home/sebas/.bun/install/cache/zod@4.1.8@@@1/package.json
  /home/sebas/.bun/install/cache/whatwg-url@5.0.0@@@1/package.json
  /home/sebas/.config/opencode/node_modules/multipasta/dist/esm/package.json
  /home/sebas/.config/opencode/node_modules/multipasta/package.json
  /home/sebas/.config/opencode/node_modules/shebang-command/package.json
  /home/sebas/.config/opencode/node_modules/which/package.json
  /home/sebas/.config/opencode/node_modules/ini/package.json
  /home/sebas/.config/opencode/node_modules/kubernetes-types/package.json
  /home/sebas/.config/opencode/node_modules/@standard-schema/spec/package.json
  /home/sebas/.config/opencode/node_modules/msgpackr/package.json
  /home/sebas/.config/opencode/node_modules/@opencode-ai/sdk/package.json
  /home/sebas/.config/opencode/node_modules/@opencode-ai/plugin/package.json
  /home/sebas/.config/opencode/node_modules/path-key/package.json
  /home/sebas/.config/opencode/node_modules/detect-libc/package.json
  /home/sebas/.config/opencode/node_modules/zod/mini/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4-mini/package.json
  /home/sebas/.config/opencode/node_modules/zod/locales/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/mini/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/locales/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/core/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/package.json
  /home/sebas/.config/opencode/node_modules/zod/v4/classic/package.json
  /home/sebas/.config/opencode/node_modules/zod/v3/package.json
  /home/sebas/.config/opencode/node_modules/zod/package.json
  /home/sebas/.config/opencode/node_modules/fast-check/lib/cjs/package.json
  /home/sebas/.config/opencode/node_modules/fast-check/package.json
  /home/sebas/.config/opencode/node_modules/shebang-regex/package.json
  /home/sebas/.config/opencode/node_modules/uuid/package.json
  /home/sebas/.config/opencode/node_modules/effect/package.json
  /home/sebas/.config/opencode/node_modules/node-gyp-build-optional-packages/package.json
  /home/sebas/.config/opencode/node_modules/isexe/package.json
  /home/sebas/.config/opencode/node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64/package.json
  /home/sebas/.config/opencode/node_modules/yaml/browser/package.json
  /home/sebas/.config/opencode/node_modules/yaml/package.json
  /home/sebas/.config/opencode/node_modules/toml/package.json
  /home/sebas/.config/opencode/node_modules/msgpackr-extract/package.json
  /home/sebas/.config/opencode/node_modules/find-my-way-ts/dist/esm/package.json
  /home/sebas/.config/opencode/node_modules/find-my-way-ts/QueryString/package.json
  /home/sebas/.config/opencode/node_modules/find-my-way-ts/package.json
  /home/sebas/.config/opencode/node_modules/pure-rand/lib/esm/package.json
  /home/sebas/.config/opencode/node_modules/pure-rand/package.json
  /home/sebas/.config/opencode/node_modules/cross-spawn/package.json
  /home/sebas/.config/opencode/package.json
  /home/sebas/.local/share/opencode/bin/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/source-map/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/partial-json/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/jws/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/yoctocolors/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/y18n/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/color-name/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/ansi-regex/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cli-highlight/node_modules/chalk/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/cli-highlight/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/data-uri-to-buffer/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/extract-zip/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minipass/dist/esm/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minipass/dist/commonjs/package.json
  /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/node_modules/minipass/package.json

[324 more lines in file. Use offset=221 to continue.]

[toolResult]
=== repo root ===
/mnt/rpi/repos/ballbox

=== target docs ===
-rw-r--r-- 1 sebas sebas  8104 Apr 14 22:04 docs/next-steps-queue.md
-rw-r--r-- 1 sebas sebas  2981 Apr 14 10:13 docs/streetcast-parity.md
-rw-rw-r-- 1 sebas sebas 28718 Apr 18 17:06 PLAN.md

=== locate player/runtime/snapshot/export refs ===
./vitest.config.ts:3:export default defineConfig({
./postcss.config.mjs:8:export default config
./eslint.config.mjs:5:export default defineConfig([
./tests/e2e/tvs-admin.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/tvs-admin.spec.ts:8:  E2E_TV_DEVICE_NAME_PREFIX,
./tests/e2e/tvs-admin.spec.ts:14:test("tvs admin supports screen, tv device, campaign, creative, and assignment CRUD", async ({ page }) => {
./tests/e2e/tvs-admin.spec.ts:18:  const tvDeviceName = `${E2E_TV_DEVICE_NAME_PREFIX} ${suffix}`;
./tests/e2e/tvs-admin.spec.ts:19:  const tvDeviceNameUpdated = `${tvDeviceName} Updated`;
./tests/e2e/tvs-admin.spec.ts:30:  const tvDevicesSection = page.locator("section.min-w-0.rounded-3xl").filter({ has: page.getByRole("heading", { name: "TV devices" }) }).first();
./tests/e2e/tvs-admin.spec.ts:34:  const tvDeviceCreateForm = tvDevicesSection.locator("div.mt-5.grid").first();
./tests/e2e/tvs-admin.spec.ts:37:  await tvDeviceCreateForm.getByPlaceholder("TV device name").fill(tvDeviceName);
./tests/e2e/tvs-admin.spec.ts:38:  await tvDeviceCreateForm.getByPlaceholder("Location").first().fill("North wall");
./tests/e2e/tvs-admin.spec.ts:39:  await tvDeviceCreateForm.getByPlaceholder("https://...").fill("https://example.com/manifests/initial.json");
./tests/e2e/tvs-admin.spec.ts:40:  await tvDeviceCreateForm.getByRole("button", { name: "Add TV device" }).click();
./tests/e2e/tvs-admin.spec.ts:41:  await expect(page.getByText("TV device created")).toBeVisible();
./tests/e2e/tvs-admin.spec.ts:43:  const tvDeviceCard = tvDevicesSection
./tests/e2e/tvs-admin.spec.ts:47:  await expect(tvDeviceCard).toBeVisible();
./tests/e2e/tvs-admin.spec.ts:48:  await tvDeviceCard.locator("input").nth(0).fill(tvDeviceNameUpdated);
./tests/e2e/tvs-admin.spec.ts:49:  await tvDeviceCard.locator("input").nth(1).fill("South wall");
./tests/e2e/tvs-admin.spec.ts:50:  await tvDeviceCard.locator("input").nth(2).fill("https://example.com/manifests/updated.json");
./tests/e2e/tvs-admin.spec.ts:51:  await tvDeviceCard.getByRole("button", { name: "Save" }).click();
./tests/e2e/tvs-admin.spec.ts:52:  await expect(page.getByText("TV device updated")).toBeVisible();
./tests/e2e/tvs-admin.spec.ts:53:  await expect(tvDeviceCard.locator("input").nth(0)).toHaveValue(tvDeviceNameUpdated);
./tests/e2e/tvs-admin.spec.ts:57:  await screenCreateForm.locator("select").nth(1).selectOption({ label: tvDeviceNameUpdated });
./tests/e2e/tvs-admin.spec.ts:66:  await screenCard.locator("select").nth(2).selectOption({ label: tvDeviceNameUpdated });
./tests/e2e/tvs-admin.spec.ts:70:  await expect(screenCard.locator("select").nth(2).locator("option:checked")).toHaveText(tvDeviceNameUpdated);
./tests/e2e/tvs-admin.spec.ts:72:  await expect(tvDeviceCard).toContainText(screenNameUpdated);
./tests/e2e/tvs-admin.spec.ts:136:  await tvDeviceCard.getByRole("button", { name: "Delete" }).click();
./tests/e2e/tvs-admin.spec.ts:137:  await expect(page.getByText("TV device deleted")).toBeVisible();
./tests/e2e/tvs-admin.spec.ts:138:  await expect(tvDevicesSection.locator("div.rounded-2xl").filter({ hasText: tvDeviceNameUpdated })).toHaveCount(0);
./prisma/seed.ts:14:  seedPlayers,
./prisma/seed.ts:123:  for (const player of seedPlayers) {
./prisma/seed.ts:124:    await prisma.player.upsert({
./prisma/seed.ts:125:      where: { id: player.id },
./prisma/seed.ts:127:        name: player.name,
./prisma/seed.ts:130:        id: player.id,
./prisma/seed.ts:131:        name: player.name,
./prisma/seed.ts:140:        playerId: membership.playerId,
./prisma/seed.ts:145:        playerId: membership.playerId,
./prisma/seed.ts:305:    if (screen.externalTvDeviceId) {
./prisma/seed.ts:306:      await prisma.tvDevice.upsert({
./prisma/seed.ts:307:        where: { id: screen.externalTvDeviceId },
./prisma/seed.ts:315:          id: screen.externalTvDeviceId,
./prisma/seed.ts:380:  const [clubs, venues, vendingMachines, tvDevices, screens, advertisers, campaigns, creatives, assignments, products, players, memberships, coaches, openMatches, classes, classOffers, classRequests] =
./prisma/seed.ts:385:    prisma.tvDevice.count(),
./prisma/seed.ts:392:    prisma.player.count(),
./prisma/seed.ts:405:    tvDevices,
./prisma/seed.ts:412:    players,
./tests/e2e/tvs-runtime-side-effects.spec.ts:3:import { expect, test } from "@playwright/test";
./tests/e2e/tvs-runtime-side-effects.spec.ts:27:test("screen runtime records heartbeat and impression side effects", async ({ page }) => {
./tests/e2e/tvs-runtime-side-effects.spec.ts:37:  await page.goto("/player/screen_001");
./tests/e2e/tvs-runtime-side-effects.spec.ts:63:    select: { tvDevice: { select: { id: true, lastSeen: true } } },
./tests/e2e/tvs-runtime-side-effects.spec.ts:66:  expect(screen?.tvDevice?.id).toBeTruthy();
./tests/e2e/tvs-runtime-side-effects.spec.ts:70:      const tvDevice = await prisma.tvDevice.findUnique({
./tests/e2e/tvs-runtime-side-effects.spec.ts:71:        where: { id: screen!.tvDevice!.id },
./tests/e2e/tvs-runtime-side-effects.spec.ts:74:      return tvDevice?.lastSeen?.getTime() ?? 0;
./tests/e2e/tvs-runtime-side-effects.spec.ts:82:          tvDeviceId: screen!.tvDevice!.id,
./prisma/seed-data.ts:1:export type SeedClub = {
./prisma/seed-data.ts:7:export type SeedVenue = {
./prisma/seed-data.ts:13:export type SeedVendingMachine = {
./prisma/seed-data.ts:19:export type SeedScreen = {
./prisma/seed-data.ts:27:  externalTvDeviceId?: string;
./prisma/seed-data.ts:30:export type SeedCampaign = {
./prisma/seed-data.ts:38:export type SeedCampaignCreative = {
./prisma/seed-data.ts:45:export type SeedScreenCampaign = {
./prisma/seed-data.ts:50:export type SeedProduct = {
./prisma/seed-data.ts:55:export type SeedPlayer = {
./prisma/seed-data.ts:60:export type SeedMembership = {
./prisma/seed-data.ts:62:  playerId: string;
./prisma/seed-data.ts:66:export type SeedCoach = {
./prisma/seed-data.ts:74:export type SeedCoachAvailability = {
./prisma/seed-data.ts:82:export type SeedCoachClub = {
./prisma/seed-data.ts:87:export type SeedOpenMatch = {
./prisma/seed-data.ts:94:export type SeedClass = {
./prisma/seed-data.ts:101:export type SeedClassOffer = {
./prisma/seed-data.ts:116:export const seedClubs: SeedClub[] = [
./prisma/seed-data.ts:121:export const seedVenues: SeedVenue[] = [
./prisma/seed-data.ts:127:export const seedVendingMachines: SeedVendingMachine[] = [
./prisma/seed-data.ts:134:export const seedScreens: SeedScreen[] = [
./prisma/seed-data.ts:143:    externalTvDeviceId: "bb-device-1",
./prisma/seed-data.ts:153:    externalTvDeviceId: "bb-device-2",
./prisma/seed-data.ts:157:export const seedProducts: SeedProduct[] = [
./prisma/seed-data.ts:162:export const seedPlayers: SeedPlayer[] = [
./prisma/seed-data.ts:163:  { id: "player_001", name: "Micaela Torres" },
./prisma/seed-data.ts:164:  { id: "player_002", name: "Lucas Pereyra" },
./prisma/seed-data.ts:165:  { id: "player_003", name: "Sofia Benitez" },
./prisma/seed-data.ts:168:export const seedMemberships: SeedMembership[] = [
./prisma/seed-data.ts:169:  { id: "membership_001", playerId: "player_001", clubId: "club_palermo" },
./prisma/seed-data.ts:170:  { id: "membership_002", playerId: "player_001", clubId: "club_san_isidro" },
./prisma/seed-data.ts:171:  { id: "membership_003", playerId: "player_002", clubId: "club_palermo" },
./prisma/seed-data.ts:172:  { id: "membership_004", playerId: "player_003", clubId: "club_san_isidro" },
./prisma/seed-data.ts:175:export const seedCoaches: SeedCoach[] = [
./prisma/seed-data.ts:192:export const seedCoachAvailabilities: SeedCoachAvailability[] = [
./prisma/seed-data.ts:216:export const seedCoachClubs: SeedCoachClub[] = [
./prisma/seed-data.ts:221:export const seedOpenMatches: SeedOpenMatch[] = [
./prisma/seed-data.ts:236:export const seedClasses: SeedClass[] = [
./prisma/seed-data.ts:251:export const seedClassOffers: SeedClassOffer[] = [
./prisma/seed-data.ts:295:export const seedCampaigns: SeedCampaign[] = [
./prisma/seed-data.ts:312:export const seedCampaignCreatives: SeedCampaignCreative[] = [
./prisma/seed-data.ts:318:export const seedScreenCampaigns: SeedScreenCampaign[] = [
./tests/e2e/support/playwright-global-cleanup.ts:6:    ["exec", "dotenv", "-e", ".env.local", "--", "tsx", "tests/e2e/support/cleanup-playwright-db.ts"],
./tests/e2e/support/playwright-global-cleanup.ts:16:    throw new Error(message || "Playwright DB cleanup failed");
./tests/e2e/support/playwright-global-cleanup.ts:20:export default async function playwrightGlobalCleanup() {
./tests/e2e/support/test-fixtures.ts:1:export const E2E_CLUB_SLUG_PREFIX = "pw-e2e-club-";
./tests/e2e/support/test-fixtures.ts:2:export const E2E_CLUB_NAME_PREFIX = "PW E2E Club";
./tests/e2e/support/test-fixtures.ts:3:export const E2E_ATC_SPORTCLUB_ID_PREFIX = "pw-e2e-atc-";
./tests/e2e/support/test-fixtures.ts:4:export const E2E_ATC_SPORTCLUB_ID = `${E2E_ATC_SPORTCLUB_ID_PREFIX}106`;
./tests/e2e/support/test-fixtures.ts:5:export const E2E_VENUE_SLUG_PREFIX = "pw-e2e-venue-";
./tests/e2e/support/test-fixtures.ts:6:export const E2E_VENUE_NAME_PREFIX = "PW E2E Venue";
./tests/e2e/support/test-fixtures.ts:7:export const E2E_MACHINE_LABEL_PREFIX = "PW E2E Machine";
./tests/e2e/support/test-fixtures.ts:8:export const E2E_PAYMENT_SESSION_PREFIX = "pw-e2e-session-";
./tests/e2e/support/test-fixtures.ts:9:export const E2E_PLAYER_NAME_PREFIX = "PW E2E Player";
./tests/e2e/support/test-fixtures.ts:10:export const E2E_COACH_NAME_PREFIX = "PW E2E Coach";
./tests/e2e/support/test-fixtures.ts:11:export const E2E_OPEN_MATCH_TITLE_PREFIX = "PW E2E Open Match";
./tests/e2e/support/test-fixtures.ts:12:export const E2E_CLASS_TITLE_PREFIX = "PW E2E Class";
./tests/e2e/support/test-fixtures.ts:13:export const E2E_SCREEN_NAME_PREFIX = "PW E2E Screen";
./tests/e2e/support/test-fixtures.ts:14:export const E2E_TV_DEVICE_NAME_PREFIX = "PW E2E TV Device";
./tests/e2e/support/test-fixtures.ts:15:export const E2E_CAMPAIGN_NAME_PREFIX = "PW E2E Campaign";
./tests/e2e/support/test-fixtures.ts:16:export const E2E_ADVERTISER_NAME_PREFIX = "PW E2E Advertiser";
./tests/e2e/support/test-fixtures.ts:17:export const E2E_CREATIVE_URL_PREFIX = "/videos/pw-e2e-creative-";
./tests/e2e/support/test-fixtures.ts:19:export function createE2ESuffix() {
./prisma/schema.prisma:158:model TvDevice {
./prisma/schema.prisma:177:  tvDevice         TvDevice?
./prisma/schema.prisma:227:  tvDevice   TvDevice @relation(fields: [tvDeviceId], references: [id])
./prisma/schema.prisma:228:  tvDeviceId String
./prisma/schema.prisma:344:  player          Player                   @relation(fields: [playerId], references: [id])
./prisma/schema.prisma:345:  playerId        String
./prisma/schema.prisma:360:  @@index([playerId, startsAt])
./prisma/schema.prisma:432:model Player {
./prisma/schema.prisma:445:  player    Player   @relation(fields: [playerId], references: [id])
./prisma/schema.prisma:446:  playerId  String
./tests/e2e/support/admin-auth.ts:2:import type { Page } from "@playwright/test";
./tests/e2e/support/admin-auth.ts:38:export async function ensureAdminSession(page: Page) {
./tests/e2e/support/admin-auth.ts:53:export async function gotoAdmin(page: Page, path: string) {
./tests/e2e/support/cleanup-playwright-db.ts:15:  E2E_PLAYER_NAME_PREFIX,
./tests/e2e/support/cleanup-playwright-db.ts:17:  E2E_TV_DEVICE_NAME_PREFIX,
./tests/e2e/support/cleanup-playwright-db.ts:24:export async function cleanupPlaywrightDb() {
./tests/e2e/support/cleanup-playwright-db.ts:31:  const [clubs, venues, players, screens, tvDevices, campaigns] = await Promise.all([
./tests/e2e/support/cleanup-playwright-db.ts:49:    prisma.player.findMany({
./tests/e2e/support/cleanup-playwright-db.ts:51:        name: { startsWith: E2E_PLAYER_NAME_PREFIX },
./tests/e2e/support/cleanup-playwright-db.ts:61:    prisma.tvDevice.findMany({
./tests/e2e/support/cleanup-playwright-db.ts:63:        name: { startsWith: E2E_TV_DEVICE_NAME_PREFIX },
./tests/e2e/support/cleanup-playwright-db.ts:81:  const playerIds = players.map((player) => player.id);
./tests/e2e/support/cleanup-playwright-db.ts:83:  const tvDeviceIds = tvDevices.map((tvDevice) => tvDevice.id);
./tests/e2e/support/cleanup-playwright-db.ts:116:  if (campaignIds.length > 0 || tvDeviceIds.length > 0) {
./tests/e2e/support/cleanup-playwright-db.ts:120:          tvDeviceIds.length > 0 ? { tvDeviceId: { in: tvDeviceIds } } : undefined,
./tests/e2e/support/cleanup-playwright-db.ts:124:          | { tvDeviceId: { in: string[] } }
./tests/e2e/support/cleanup-playwright-db.ts:156:  if (tvDeviceIds.length > 0) {
./tests/e2e/support/cleanup-playwright-db.ts:157:    await prisma.tvDevice.deleteMany({
./tests/e2e/support/cleanup-playwright-db.ts:159:        id: { in: tvDeviceIds },
./tests/e2e/support/cleanup-playwright-db.ts:172:  if (playerIds.length > 0 || clubIds.length > 0) {
./tests/e2e/support/cleanup-playwright-db.ts:176:          playerIds.length > 0 ? { playerId: { in: playerIds } } : undefined,
./tests/e2e/support/cleanup-playwright-db.ts:178:        ].filter(Boolean) as Array<{ playerId?: { in: string[] }; clubId?: { in: string[] } }>,
./tests/e2e/support/cleanup-playwright-db.ts:204:  if (playerIds.length > 0) {
./tests/e2e/support/cleanup-playwright-db.ts:205:    await prisma.player.deleteMany({
./tests/e2e/support/cleanup-playwright-db.ts:206:      where: { id: { in: playerIds } },
./tests/e2e/support/cleanup-playwright-db.ts:212:  await cleanupPlaywrightDb();
./tests/e2e/admin-navigation-and-network.spec.ts:1:import { expect, test } from "@playwright/test";
./proxy.ts:11:  "/api/players",
./proxy.ts:31:export async function proxy(request: NextRequest) {
./proxy.ts:55:    ["/api/network", "/api/players", "/api/sports", "/api/tvs/screens", "/api/integrations/payments"].some(
./proxy.ts:66:export const config = {
./proxy.ts:71:    "/api/players",
./tests/e2e/player-runtime-side-effects.spec.ts:3:import { expect, test } from "@playwright/test";
./tests/e2e/player-runtime-side-effects.spec.ts:27:test("lightweight player records heartbeat and impression side effects", async ({ page }) => {
./tests/e2e/player-runtime-side-effects.spec.ts:37:  await page.goto("/player/screen_001");
./tests/e2e/player-runtime-side-effects.spec.ts:63:    select: { tvDevice: { select: { id: true, lastSeen: true } } },
./tests/e2e/player-runtime-side-effects.spec.ts:66:  expect(screen?.tvDevice?.id).toBeTruthy();
./tests/e2e/player-runtime-side-effects.spec.ts:70:      const tvDevice = await prisma.tvDevice.findUnique({
./tests/e2e/player-runtime-side-effects.spec.ts:71:        where: { id: screen!.tvDevice!.id },
./tests/e2e/player-runtime-side-effects.spec.ts:74:      return tvDevice?.lastSeen?.getTime() ?? 0;
./tests/e2e/player-runtime-side-effects.spec.ts:82:          tvDeviceId: screen!.tvDevice!.id,
./tests/e2e/admin-mobile-smoke.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/sports-admin.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/tvs-offline.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/tvs-offline.spec.ts:3:test("screen runtime keeps cached playback when browser goes offline", async ({ browser }) => {
./tests/e2e/tvs-offline.spec.ts:12:  await page.goto("/player/screen_001");
./tests/e2e/payments-admin.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/players-admin.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/players-admin.spec.ts:3:import { E2E_PLAYER_NAME_PREFIX, createE2ESuffix } from "./support/test-fixtures";
./tests/e2e/players-admin.spec.ts:7:test("players admin supports player and membership CRUD", async ({ page }) => {
./tests/e2e/players-admin.spec.ts:9:  const playerName = `${E2E_PLAYER_NAME_PREFIX} ${suffix}`;
./tests/e2e/players-admin.spec.ts:10:  const playerNameUpdated = `${playerName} Updated`;
./tests/e2e/players-admin.spec.ts:12:  await gotoAdmin(page, "/admin/players");
./tests/e2e/players-admin.spec.ts:14:  await page.getByPlaceholder("Player name").fill(playerName);
./tests/e2e/players-admin.spec.ts:15:  await page.getByRole("button", { name: "Add player" }).click();
./tests/e2e/players-admin.spec.ts:17:  await expect(page.getByText("Player created")).toBeVisible();
./tests/e2e/players-admin.spec.ts:19:  const playerArticle = page
./tests/e2e/players-admin.spec.ts:21:    .filter({ has: page.locator(`input[value^="${playerName}"]`) })
./tests/e2e/players-admin.spec.ts:24:  await expect(playerArticle.locator(`input[value="${playerName}"]`)).toBeVisible();
./tests/e2e/players-admin.spec.ts:26:  await playerArticle.locator(`input[value="${playerName}"]`).fill(playerNameUpdated);
./tests/e2e/players-admin.spec.ts:27:  await playerArticle.getByRole("button", { name: "Save" }).click();
./tests/e2e/players-admin.spec.ts:29:  await expect(page.getByText("Player updated")).toBeVisible();
./tests/e2e/players-admin.spec.ts:30:  await expect(playerArticle.locator(`input[value="${playerNameUpdated}"]`)).toBeVisible();
./tests/e2e/players-admin.spec.ts:32:  const membershipSelect = playerArticle.locator("select").first();
./tests/e2e/players-admin.spec.ts:36:  await playerArticle.getByRole("button", { name: "Add membership" }).click();
./tests/e2e/players-admin.spec.ts:39:  await expect(playerArticle.getByText(selectedClubLabel ?? "", { exact: false }).first()).toBeVisible();
./tests/e2e/players-admin.spec.ts:41:  await playerArticle.getByRole("button", { name: "Remove" }).click();
./tests/e2e/players-admin.spec.ts:44:  await expect(playerArticle.getByRole("button", { name: "Remove" })).toHaveCount(0);
./tests/e2e/players-admin.spec.ts:46:  await playerArticle.getByRole("button", { name: "Delete" }).click();
./tests/e2e/players-admin.spec.ts:48:  await expect(page.getByText("Player deleted")).toBeVisible();
./tests/e2e/players-admin.spec.ts:49:  await expect(page.locator(`input[value="${playerNameUpdated}"]`)).toHaveCount(0);
./tests/e2e/player-offline.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/e2e/player-offline.spec.ts:3:test("lightweight player keeps cached playback when browser goes offline", async ({ browser }) => {
./tests/e2e/player-offline.spec.ts:12:  await page.goto("/player/screen_001");
./tests/e2e/classes.spec.ts:1:import { expect, test } from "@playwright/test";
./tests/contracts/tvs/admin-routes.test.ts:7:const createTvDeviceInDb = vi.fn();
./tests/contracts/tvs/admin-routes.test.ts:12:const deleteTvDeviceInDb = vi.fn();
./tests/contracts/tvs/admin-routes.test.ts:18:const updateTvDeviceInDb = vi.fn();
./tests/contracts/tvs/admin-routes.test.ts:25:  createTvDeviceInDb,
./tests/contracts/tvs/admin-routes.test.ts:30:  deleteTvDeviceInDb,
./tests/contracts/tvs/admin-routes.test.ts:36:  updateTvDeviceInDb,
./tests/contracts/tvs/admin-routes.test.ts:48:      tvDevices: [],
./tests/contracts/tvs/admin-routes.test.ts:57:    expect(payload).toEqual({ ok: true, item: { venues: [], tvDevices: [], screens: [], campaigns: [] } });
./tests/contracts/tvs/admin-routes.test.ts:90:  it("returns 409 for tv device delete with impressions", async () => {
./tests/contracts/tvs/admin-routes.test.ts:91:    deleteTvDeviceInDb.mockResolvedValue({ ok: false, error: "TV_DEVICE_HAS_IMPRESSIONS", impressionCount: 2 });
./tests/contracts/tvs/admin-routes.test.ts:93:    const { DELETE } = await import("@/app/api/admin/tvs/tv-devices/[tvDeviceId]/route");
./tests/contracts/tvs/admin-routes.test.ts:94:    const response = await DELETE(new Request("http://localhost/api/admin/tvs/tv-devices/tv_001"), {
./tests/contracts/tvs/admin-routes.test.ts:95:      params: Promise.resolve({ tvDeviceId: "tv_001" }),
./tests/contracts/tvs/routes.test.ts:67:          externalTvDeviceId: "tv_device_001",
./tests/contracts/tvs/screen-player.test.tsx:5:import { ScreenPlayer } from "@/components/screen-player";
./tests/contracts/tvs/screen-player.test.tsx:6:import { getTvImpressionQueueStorageKey, getTvMediaCacheName, type ScreenManifest } from "@/lib/tvs-runtime";
./tests/contracts/tvs/screen-player.test.tsx:53:describe("ScreenPlayer browser runtime", () => {
./tests/contracts/tvs/screen-player.test.tsx:117:  async function flushRuntime() {
./tests/contracts/tvs/screen-player.test.tsx:129:    const { container } = render(<ScreenPlayer screenId="screen_001" initialManifest={initialManifest} />);
./tests/contracts/tvs/screen-player.test.tsx:131:    await flushRuntime();
./tests/contracts/tvs/screen-player.test.tsx:169:    const { container } = render(<ScreenPlayer screenId="screen_001" initialManifest={initialManifest} />);
./tests/contracts/tvs/screen-player.test.tsx:171:    await flushRuntime();
./tests/contracts/tvs/screen-player.test.tsx:177:    await flushRuntime();
./tests/contracts/tvs/screen-player.test.tsx:208:    render(<ScreenPlayer screenId="screen_001" initialManifest={initialManifest} />);
./tests/contracts/tvs/screen-player.test.tsx:209:    await flushRuntime();
./tests/contracts/tvs/screen-player.test.tsx:215:    await flushRuntime();
./tests/contracts/tvs/backend-runtime.test.ts:18:  tvDevice: {
./tests/contracts/tvs/backend-runtime.test.ts:25:describe("tvs backend runtime module", () => {
./tests/contracts/tvs/backend-runtime.test.ts:46:        tvDevice: {
./tests/contracts/tvs/backend-runtime.test.ts:83:    const { getTvSourceFromDb } = await import("@/lib/tvs-backend-runtime");
./tests/contracts/tvs/backend-runtime.test.ts:101:          externalTvDeviceId: "tv_001",
./tests/contracts/tvs/backend-runtime.test.ts:128:      tvDevice: {
./tests/contracts/tvs/backend-runtime.test.ts:133:    const { getTvManifestFromDb } = await import("@/lib/tvs-backend-runtime");
./tests/contracts/tvs/backend-runtime.test.ts:147:  it("records heartbeat against linked tv device", async () => {
./tests/contracts/tvs/backend-runtime.test.ts:151:      tvDevice: {
./tests/contracts/tvs/backend-runtime.test.ts:156:    const { recordScreenHeartbeatInDb } = await import("@/lib/tvs-backend-runtime");
./tests/contracts/tvs/backend-runtime.test.ts:159:    expect(prisma.tvDevice.update).toHaveBeenCalledWith({
./tests/contracts/tvs/backend-runtime.test.ts:174:      tvDevice: {
./tests/contracts/tvs/backend-runtime.test.ts:193:    const { recordImpressionInDb } = await import("@/lib/tvs-backend-runtime");
./app/classes/login/page.tsx:3:export default async function ClassesLoginPage({
./tests/contracts/tvs/backend-modules.test.ts:36:  tvDevice: {
./tests/contracts/tvs/backend-modules.test.ts:63:  it("creates tv devices with trimmed and normalized optional fields", async () => {
./tests/contracts/tvs/backend-modules.test.ts:64:    prisma.tvDevice.create.mockResolvedValue({ id: "tv_001" });
./tests/contracts/tvs/backend-modules.test.ts:65:    prisma.tvDevice.findUnique.mockResolvedValue({ id: "tv_001", name: "Lobby TV" });
./tests/contracts/tvs/backend-modules.test.ts:67:    const { createTvDeviceInDb } = await import("@/lib/tvs-backend-tv-devices");

[750 more lines in file. Use offset=261 to continue.]

[Assistant]
1) Machine-wide Playwright / playwright-mcp audit outside `/mnt/rpi/repos/ballbox`

Commands run:
```bash
pwd

which playwright || true
which playwright-mcp || true
which node || true
which npm || true
which pnpm || true
which npx || true

npm root -g 2>/dev/null || true

for d in /home/sebas ~/.config ~/.cache ~/.local ~/.local/bin ~/.npm-global /mnt/rpi/repos; do
  [ -e "$d" ] && echo "--- $d" && find "$d" -maxdepth 3 \
    \( -iname '*playwright*' -o -iname '*mcp*' -o -iname '*harness*' -o -iname '*e2e*' -o -iname '*snapshot*' -o -iname '*export*' \) \
    2>/dev/null | sed 's#^#  #'
done

find /home/sebas ~/.config ~/.cache ~/.local /mnt/rpi/repos \
  -path /mnt/rpi/repos/ballbox -prune -o -name package.json -print 2>/dev/null | sed 's#^#  #' | head -500

printf '=== PATH ===\n%s\n' "$PATH"

for c in playwright playwright-mcp node npm pnpm npx; do
  if command -v "$c" >/dev/null 2>&1; then
    p=$(command -v "$c")
    echo "--- $c => $p"
    ls -l "$p"
    readlink -f "$p" || true
  fi
done

npm ls -g --depth=2 2>/dev/null | rg -n "playwright|mcp" || true

ls -la ~/.local/bin 2>/dev/null || true
ls -la ~/.npm-global/bin 2>/dev/null || true

find /home/sebas ~/.config ~/.cache ~/.local -maxdepth 5 \
  \( -iname '*playwright*' -o -iname 'playwright.config.*' -o -iname '*playwright-mcp*' \) \
  2>/dev/null | sort
```

Observed environment:
```text
pwd
/home/sebas
```

PATH:
```text
/mnt/rpi/pi-config/bin:/home/sebas/.local/bin:/home/sebas/.opencode/bin:/home/sebas/.npm-global/bin:/home/sebas/.local/bin:/home/sebas/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games
```

CLI availability:
```text
which playwright          -> not found
which playwright-mcp      -> not found
which node                -> /usr/bin/node
which npm                 -> /usr/bin/npm
which pnpm                -> /home/sebas/.local/bin/pnpm
which npx                 -> /usr/bin/npx
```

Resolved binaries:
```text
node => /usr/bin/node
npm  => /usr/bin/npm -> /usr/lib/node_modules/npm/bin/npm-cli.js
pnpm => /home/sebas/.local/bin/pnpm -> /usr/lib/node_modules/corepack/dist/pnpm.js
npx  => /usr/bin/npx -> /usr/lib/node_modules/npm/bin/npx-cli.js
```

Global npm root:
```text
/home/sebas/.npm-global/lib/node_modules
```

Global npm packages matching Playwright/MCP:
```text
none
```

User-local bins:
```text
~/.local/bin
  claude
  pnpm
  pnpx
  yarn
  yarnpkg

~/.npm-global/bin
  bird
  clawdbot
  clawdhub
  codex
  pi
  pnpm
  pnpx
```

Key findings outside `ballbox`:
```text
/home/sebas/.agents/opencode-harness-install
/home/sebas/opencode-harness
/mnt/rpi/repos/opencode-harness
/mnt/rpi/repos/FelipeVersion/tests/e2e
/mnt/rpi/repos/FelipeVersion/node_modules/playwright
/mnt/rpi/repos/FelipeVersion/node_modules/playwright-core
/mnt/rpi/repos/FelipeVersion/node_modules/@playwright
/mnt/rpi/repos/FelipeVersion/playwright.config.ts
```

Not found machine-wide:
- no `playwright` CLI on PATH
- no `playwright-mcp` CLI on PATH
- no global Playwright install under `~/.npm-global`
- no machine-wide reusable Playwright harness under `~/.config`, `~/.cache`, `~/.local`
- no standalone shared Playwright browser cache/install surfaced in searched dirs

Global/reusable pattern actually present:
- repo-local Playwright use exists, not machine-wide
- likely reusable non-Playwright harness candidates:
  - `/home/sebas/opencode-harness`
  - `/mnt/rpi/repos/opencode-harness`
- but from this recon they are just harness-named dirs, not proven Playwright suites
- only concrete Playwright test suites found outside `ballbox`:
  - `/mnt/rpi/repos/FelipeVersion/tests/e2e`
  - `/mnt/rpi/repos/FelipeVersion/playwright.config.ts`

Concrete recommendation: minimal durable setup
- Do not install Playwright globally unless you need a shared CLI for ad hoc runs.
- Do not depend on `playwright-mcp` being globally present; current machine state does not have it.
- Keep browser automation harness repo-local and versioned.
- Best durable location for a shared personal harness:
  - `/mnt/rpi/repos/browser-harness`
- If you want it tied to machine config/ops instead:
  - `/mnt/rpi/pi-config/browser-harness`
- Prefer `/mnt/rpi/repos/browser-harness` because:
  - easier git/versioning
  - avoids mixing executable test assets into machine config
  - can hold app-agnostic smoke tests and helper scripts
  - easier to run independently from any one product repo

Recommended minimal structure:
```text
/mnt/rpi/repos/browser-harness/
  package.json
  playwright.config.ts
  tests/
    smoke/
      local-web.spec.ts
      local-player-shell.spec.ts
      nginx-routing.spec.ts
  scripts/
    install-browsers.sh
  README.md
  .env.example
```

Recommended package baseline:
```json
{
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "codegen": "playwright codegen",
    "install:browsers": "playwright install chromium"
  },
  "devDependencies": {
    "@playwright/test": "^1.58.2"
  }
}
```

Recommended config baseline:
- Chromium only
- headless by default
- baseURL via env
- screenshots/traces on failure
- no Firefox/WebKit unless needed
- explicit artifact dir

Recommended first durable tests:
1. local HTTP smoke against `http://127.0.0.1`
2. nginx route smoke against key internal services
3. generic video/cache/offline harness page if you want reusable runtime checks across repos
4. auth/session smoke for admin pages only where stable and worthwhile

Recommendation on `playwright-mcp`:
- current machine audit says it is not installed or exposed on PATH
- if needed, install it in the dedicated harness repo or as a dedicated global tool later
- do not assume existing machine-wide setup

File pointers from audit:
- `/home/sebas/opencode-harness`
- `/mnt/rpi/repos/opencode-harness`
- `/mnt/rpi/repos/FelipeVersion/playwright.config.ts`
- `/mnt/rpi/repos/FelipeVersion/tests/e2e`

Checklist for task 1:
- [ ] Decide shared harness home: `/mnt/rpi/repos/browser-harness`
- [ ] Create repo-local Playwright project there
- [ ] Install only `@playwright/test`
- [ ] Run `playwright install chromium`
- [ ] Add 2–4 machine-local smoke specs
- [ ] Keep app-specific flows inside each app repo
- [ ] Do not rely on global `playwright` or `playwright-mcp`
- [ ] Optionally add wrapper script in `/mnt/rpi/pi-config/bin/` to run shared harness quickly

2) `/mnt/rpi/repos/ballbox` OPS-2 context, player runtime, snapshot/export tooling

Commands run:
```bash
cd /mnt/rpi/repos/ballbox

ls -l docs/streetcast-parity.md docs/next-steps-queue.md PLAN.md

rg -n --hidden -S "streetcast|player|runtime|snapshot|export|playwright|device|parity" . \
  --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' | head -1000

jq '{name, scripts, devDependencies, dependencies}' package.json

find . -path './node_modules' -prune -o -path './.next' -prune -o \
  \( -ipath '*player*' -o -ipath '*tvs*' -o -ipath '*runtime*' -o -ipath '*snapshot*' -o -ipath '*export*' \) \
  -print | sort | head -500

find tests/e2e -maxdepth 3 -type f | sort

rg -n --hidden -S "snapshot|export|manifest|heartbeat|impression|device|adb|android|tv" . \
  --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' | head -1200

rg -n "Export parity snapshot|parity snapshot|download.*json|blob|CacheStorage|offline|impression queue|heartbeat" \
  app components lib tests/e2e scripts --glob '!node_modules'

rg -n "export default.*player|/player/\\[screenId\\]|screenId" app components lib --glob '!node_modules' | head -400
```

Docs read:
- `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`
- `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
- `/mnt/rpi/repos/ballbox/PLAN.md`
- `/mnt/rpi/repos/ballbox/playwright.config.ts`
- `/mnt/rpi/repos/ballbox/package.json`

OPS-2 queue state:
File: `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
```md
### OPS-2 TVs runtime parity closure
- Priority: P0
- Status: ready
- Depends on: none
- Scope:
  - manually verify real-device CacheStorage/object-URL behavior on `/player/[screenId]`
  - update `docs/streetcast-parity.md` with concrete evidence
- Progress (2026-04-14): runtime now exports a JSON parity snapshot from `/player/[screenId]` to standardize evidence capture; remaining work is one real-device run and doc evidence entry.
```

Streetcast parity status:
File: `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`

Already covered:
- manifest polling
- sequential creative playback
- local media cache
- background preload
- offline continuity
- impression retry queue
- browser automation parity

Intentionally different:
- Ballbox APIs instead of Streetcast endpoints
- production TV runtime path is `/player/[screenId]`
- Ballbox Postgres scheduling is source of truth

Still pending:
- real-device verification before calling migration complete
- missing proof is hardware/browser behavior for CacheStorage + object-URL playback outside desktop automation
- on-screen `Export parity snapshot` action exists for standardized evidence capture

Manual real-device check already specified in doc:
1. open `/player/screen_001`
2. verify diagnostics:
   - `Playback: cached`
   - `Active source: blob`
   - `Cache API: available`
   - `Cached creatives: 1` or higher
   - `Network: online`
3. confirm video loops without visible reload gap
4. disconnect network
5. verify:
   - `Network: offline`
   - `Playback: cached`
   - `Active source: blob`
6. stay offline past one refresh interval, verify `Manifest refresh: failed`
7. reconnect, verify:
   - `Network: online`
   - queued impressions return to `0`
   - heartbeat timestamp advances again
8. capture:
   - photo/video
   - browser version / device model
   - screen id
   - exported JSON snapshot online and offline
   - deviations

Relevant implementation file pointers

Runtime route:
- `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`

Runtime React player:
- `/mnt/rpi/repos/ballbox/components/screen-player.tsx`

Shared runtime core:
- `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-runtime.ts`

Backend runtime/data helpers:
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-runtime.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-source.ts`

API routes used by runtime:
- `/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/health/route.ts`
- `/mnt/rpi/repos/ballbox/app/api/tvs/screens/route.ts`

Contracts/invariants:
- `/mnt/rpi/repos/ballbox/lib/contracts/tvs.ts`
- `/mnt/rpi/repos/ballbox/lib/contracts/tvs-invariants.ts`
- `/mnt/rpi/repos/ballbox/lib/contracts/admin-tvs.ts`

Admin/runtime ops:
- `/mnt/rpi/repos/ballbox/components/tvs-admin-client.tsx`
- `/mnt/rpi/repos/ballbox/app/admin/screens/page.tsx`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-operations.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-screens.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-tv-devices.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-campaigns.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-creatives.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-assignments.ts`
- `/mnt/rpi/repos/ballbox/lib/tvs-backend-mappers.ts`

Snapshot/export tooling:
- export button rendered in:
  - `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`
- parity snapshot feature references detected in:
  - `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`
  - `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
  - `/mnt/rpi/repos/ballbox/PLAN.md`

E2E/browser automation files:
- `/mnt/rpi/repos/ballbox/playwright.config.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/player-offline.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/player-runtime-side-effects.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/tvs-offline.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/tvs-runtime-side-effects.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/tvs-admin.spec.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/support/playwright-global-cleanup.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/support/cleanup-playwright-db.ts`
- `/mnt/rpi/repos/ballbox/tests/e2e/support/test-fixtures.ts`

Playwright setup inside repo:
File: `/mnt/rpi/repos/ballbox/playwright.config.ts`
- test dir: `./tests/e2e`
- Chromium only
- baseURL: `http://127.0.0.1:3010`
- `webServer.command`: `pnpm dev`
- `reuseExistingServer: true`
- env:
  - `BALLBOX_ADMIN_PASSWORD=""`
  - `ATC_PUBLIC_API_BASE_URL="http://127.0.0.1:3010/api/atc-public"`
  - `ATC_PUBLIC_VALIDATE="false"`

Package scripts:
File: `/mnt/rpi/repos/ballbox/package.json`
```json
{
  "test:e2e": "playwright test",
  "test:contracts": "vitest run tests/contracts",
  "test:unit": "vitest run tests/unit",
  "test:integration": "vitest run tests/integration --passWithNoTests",
  "dev": "next dev --port 3010"
}
```

What is already implemented

From docs + code references:
- lightweight TV runtime served at `/player/[screenId]`
- runtime diagnostics on-screen
- manifest polling against internal Ballbox API
- sequential creative playback
- browser cache / cached blob playback path
- background preload/media warming
- last-good-manifest retention
- offline continuity path
- impression retry queue in browser
- heartbeat reporting
- impression reporting
- screen/admin CRUD and campaign/creative/assignment CRUD
- schedule-window filtering in runtime source/manifest generation
- strict Zod-backed contracts for TV APIs
- Playwright automation for offline/blob and side-effect checks
- one-click parity snapshot JSON export from runtime
- seeded test data for `screen_001`, `bb-device-*`, campaigns/creatives/assignments in `prisma/seed-data.ts`

Concrete evidence from file map:
- cached blob/object URL logic:
  - `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
- manifest fetch:
  - `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts`
- heartbeat POST:
  - `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts`
- impression POST:
  - `/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts`
- runtime route with export UI:
  - `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`
- offline/browser tests:
  - `/mnt/rpi/repos/ballbox/tests/e2e/player-offline.spec.ts`
  - `/mnt/rpi/repos/ballbox/tests/e2e/tvs-offline.spec.ts`
- side-effect tests:
  - `/mnt/rpi/repos/ballbox/tests/e2e/player-runtime-side-effects.spec.ts`
  - `/mnt/rpi/repos/ballbox/tests/e2e/tvs-runtime-side-effects.spec.ts`

What can be automated locally now
- start app locally with:
```bash
cd /mnt/rpi/repos/ballbox
pnpm dev
```

- run browser-level parity checks:
```bash
cd /mnt/rpi/repos/ballbox
pnpm test:e2e
```

- run only relevant OPS-2/browser specs:
```bash
cd /mnt/rpi/repos/ballbox
pnpm playwright test tests/e2e/player-offline.spec.ts
pnpm playwright test tests/e2e/player-runtime-side-effects.spec.ts
pnpm playwright test tests/e2e/tvs-offline.spec.ts
pnpm playwright test tests/e2e/tvs-runtime-side-effects.spec.ts
```

- validate contracts around TV APIs/runtime helpers:
```bash
cd /mnt/rpi/repos/ballbox
pnpm test:contracts
```

- seed/reset local DB state for known runtime fixtures:
```bash
cd /mnt/rpi/repos/ballbox
pnpm db:push
pnpm db:seed
```

- clean Playwright-created DB fixtures:
```bash
cd /mnt/rpi/repos/ballbox
dotenv -e .env.local -- tsx tests/e2e/support/cleanup-playwright-db.ts
```

- inspect seed/runtime entities:
  - `prisma/seed-data.ts`
  - `prisma/seed.ts`

What still requires a real device
Per `docs/streetcast-parity.md` and `PLAN.md`, still not closed by local automation:
- actual constrained-device browser behavior for `CacheStorage`
- actual constrained-device object-URL video playback behavior
- proof that cached blob playback survives real network loss on target TV/browser
- proof that playback keeps running through a failed manifest refresh on device
- proof that heartbeat/impression queues recover after reconnect on device
- collection of real evidence:
  - photos/video
  - device model
  - browser version
  - exported parity snapshots
  - doc update with results

Main contradiction/risk to note
- local Playwright tests already prove desktop Chromium parity path
- project docs explicitly say migration is still not complete because that proof is not enough for target TV hardware/browser
- so OPS-2 is not an implementation task now; it is an evidence-collection and validation task on real hardware

Concrete checklist

Machine-wide Playwright setup checklist:
- [ ] Accept there is no current machine-wide `playwright` or `playwright-mcp`
- [ ] Do not rely on global install for durable use
- [ ] Create shared harness at `/mnt/rpi/repos/browser-harness`
- [ ] Add Chromium-only Playwright config
- [ ] Add machine-local smoke specs
- [ ] Keep Ballbox-specific runtime tests inside `/mnt/rpi/repos/ballbox/tests/e2e`

Ballbox OPS-2 checklist:
- [ ] Review runtime route: `/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts`
- [ ] Review shared runtime core: `/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts`
- [ ] Review backend manifest/heartbeat/impression paths:
  - `/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts`
  - `/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts`
- [ ] Seed local DB:
  ```bash
  cd /mnt/rpi/repos/ballbox && pnpm db:push && pnpm db:seed
  ```
- [ ] Run local parity automation:
  ```bash
  cd /mnt/rpi/repos/ballbox && pnpm test:e2e
  ```
- [ ] On real TV/browser open:
  ```text
  /player/screen_001
  ```
- [ ] While online verify diagnostics:
  - [ ] Playback: cached
  - [ ] Active source: blob
  - [ ] Cache API: available
  - [ ] Cached creatives >= 1
  - [ ] Network: online
- [ ] Export online parity snapshot JSON
- [ ] Disconnect network
- [ ] Verify:
  - [ ] Network: offline
  - [ ] Playback: cached
  - [ ] Active source: blob
- [ ] Wait past manifest refresh interval
- [ ] Verify `Manifest refresh: failed` without playback stopping
- [ ] Export offline parity snapshot JSON
- [ ] Reconnect network
- [ ] Verify:
  - [ ] Network: online
  - [ ] queued impressions return to 0
  - [ ] heartbeat advances
- [ ] Capture photo/video + device/browser metadata
- [ ] Update:
  - `/mnt/rpi/repos/ballbox/docs/streetcast-parity.md`
  - `/mnt/rpi/repos/ballbox/docs/next-steps-queue.md`
  - `/mnt/rpi/repos/ballbox/PLAN.md`

Most relevant file pointers
```text
/mnt/rpi/repos/ballbox/docs/streetcast-parity.md
/mnt/rpi/repos/ballbox/docs/next-steps-queue.md
/mnt/rpi/repos/ballbox/PLAN.md
/mnt/rpi/repos/ballbox/app/player/[screenId]/route.ts
/mnt/rpi/repos/ballbox/components/screen-player.tsx
/mnt/rpi/repos/ballbox/lib/tvs-player-runtime-core.ts
/mnt/rpi/repos/ballbox/lib/tvs-runtime.ts
/mnt/rpi/repos/ballbox/lib/tvs-backend-runtime.ts
/mnt/rpi/repos/ballbox/lib/tvs-source.ts
/mnt/rpi/repos/ballbox/app/api/tvs/manifest/[screenId]/route.ts
/mnt/rpi/repos/ballbox/app/api/tvs/heartbeat/route.ts
/mnt/rpi/repos/ballbox/app/api/tvs/impression/route.ts
/mnt/rpi/repos/ballbox/tests/e2e/player-offline.spec.ts
/mnt/rpi/repos/ballbox/tests/e2e/player-runtime-side-effects.spec.ts
/mnt/rpi/repos/ballbox/tests/e2e/tvs-offline.spec.ts
/mnt/rpi/repos/ballbox/tests/e2e/tvs-runtime-side-effects.spec.ts
/mnt/rpi/repos/ballbox/playwright.config.ts
/mnt/rpi/repos/ballbox/prisma/seed-data.ts
/mnt/rpi/repos/ballbox/prisma/seed.ts
```