subime este video como fullscreen de la vending machine en la api de ballbox (/Users/sebas/Downloads/4536893-hd_720_1280_30fps.mp4 ) el path es en la laptop


**Clarifying the task**

I’m considering using the ballbox-company skill since it seems relevant to the context and API. It could be beneficial to check the laptop file as well. I might want to utilize laptop helpers to assist with this. I wonder if inspecting my memories is necessary, or if I just need to ask for some information. I think knowing the API endpoint could help since the skill likely has the required context. I'll need to read up on the skill further.


---
name: ballbox-company
description: Load Ballbox company context and operating namespace
---

# ballbox-company

## Purpose

Load the Ballbox company operating context when the request is about Ballbox.

## Workflow

1. Use the Ballbox namespace at `/mnt/rpi/repos/ballbox-company-agent` as the working context root.
2. Read `/mnt/rpi/repos/ballbox-company-agent/AGENTS.md` and follow its rules.
3. Use `/mnt/rpi/repos/ballbox-company-agent/index.md` as the default dashboard entry point.
4. Prefer shared-memory search before assuming facts; avoid invented certainty.
5. If the work implies repo/code changes, delegate to `~/ai-dev-workflow`.


Owner: Sebas. Work style: telegraph; min tokens; real gaps first.

## Mission

Act as Ballbox company agent.

Primary job:

- keep company context coherent
- search shared memory before assuming
- fill missing context from biggest gaps to smallest
- avoid invented certainty

## Ballbox baseline

- Ballbox is early-stage
- product thesis: one physical unit sells ball tubes and advertising inventory on integrated screens
- Ballbox currently has multiple open fronts, but current execution focus is overwhelmingly on the coaches/software wedge
- initial market: padel centers
- team:
  - Felipe Oliver: logistics and investment
  - Sebastián Vekselman: relationships with padel centers
  - Ilo Staryfurman: sales
  - Sebas: engineering / software
- ATC constraint:
  - ATC is **not** formally associated with Ballbox
  - valid wording: Sebastián has relevant category/network advantage
  - invalid wording: ATC is partner/channel/owner/institutional association
  - still, ATC-linked access/context is strategically indispensable across Ballbox fronts, not only coaches

## Memory

- shared memory: use the configured Ballbox shared-memory service/path; `~/agents-database` is only a fallback discovery location
- search Ballbox memory first
- when durable Ballbox facts appear, persist them
- prefer project-scoped or repo-scoped memories when possible
- when Ballbox work clearly implies repo changes, delegate to `~/ai-dev-workflow`

## Curiosity rule

For Ballbox, the agent should be relentlessly curious:

- ask what is missing in strategy
- ask what is missing in economics
- ask what is missing in operations
- ask what is missing in hardware/software
- ask what is missing in go-to-market
- turn unknowns into explicit notes/tasks instead of hand-waving

## Current missing context

- legal entity / ownership details
- pricing and revenue-share model
- pilot clubs / active customers
- hardware vendor and machine economics
- ad-sales packaging and pricing
- real contact channels

## Before handoff

- verify shared-memory search returns the new context
- call out unknowns explicitly


# Ballbox Company OS

## Now

- Cycle: `C-00`
- Main effort: make Ballbox state legible to agents
- Root constraint: no invented certainty
- Shared memory: use configured Ballbox shared-memory service/path; `~/agents-database` is only fallback discovery
- Entry path: start from `~/personal-agent`; delegate Ballbox-specific work here
- Current execution focus: coaches / ATC wedge (~99% of current product attention)

## Current focus

- clarify coach workflow wedge under ATC-linked leverage
- clarify commercial model: vending + advertising economics
- clarify pilots: which clubs, what status, what proof
- clarify operations: machine, refill, maintenance, ad-sales workflow
- clarify software boundary: payment, dispensing, screen playback, reporting

## Active projects

- [Company OS](./projects/company-os/.plan/index.md): operating system for Ballbox context and loops
- [Coaches OS](./projects/coaches-os/.plan/index.md): current highest-focus discovery track for coach workflows + ATC leverage
- [Commercial Model](./projects/commercial-model/.plan/index.md): pricing, revenue-share, packaging
- [Pilot Ops](./projects/pilot-ops/.plan/index.md): club rollout, machine operations, service loops
- [Product Surface](./projects/product-surface/.plan/index.md): payment, machine control, screen software

## Repo links

- product repo: `~/Code/ballbox`
- current product execution state: `~/Code/ballbox/PLAN.md`

## Key unknowns

- strongest coach pain and buyer path
- minimum ATC access depth for first product loop
- legal entity and ownership details
- exact revenue-share or unit economics by club type
- active pilots and their stage
- machine vendor and hardware economics
- ad inventory packaging and pricing
- real contact and channel map

## Entry points

- [Mission](./.self/mission.md)
- [Memory policy](./what-goes-where.md)
- [Cycle plan](./.cycle/C-00/plan.md)
- [Cycle review](./.cycle/C-00/review.md)
- [Working TODO](./.tasks/TODO.md)
- [Positioning](./strategy/positioning.md)
- [Worldview cycle beliefs](./worldview/cycle/current.md)
- [Signals index](./signals/index.md)
- [SOP index](./SOP/index.md)


**Searching for fullscreen API**

I need to search the codebase for the fullscreen API, possibly in the product repository. It’s probably a good idea to utilize a task list for active execution. I think creating a small to-do list would help me keep track of everything I need to do. This way, I can be organized and ensure I don’t miss anything important while I’m working on it. Let's get started on that!

todo

Upload fullscreen vending video to Ballbox API

Find Ballbox API/media flow, fetch video from laptop path, upload as fullscreen asset for vending machine, validate result.


Created #1: Upload fullscreen vending video to Ballbox API (pending)


Updated #1 (pending → in_progress)


**Inspecting repository**

I need to take a closer look at the repository, and I think it might be in the /Code/ballbox directory. I should run a search in that folder to find what I need. It's a straightforward task, but I want to make sure I'm thorough. I'll double-check that I’m looking in the right place to ensure I gather all the information. Let's see what I can find in there!

bash

cd /home/sebas/Code/ballbox && rg -n "fullscreen|vending|screen|upload|media" -S --glob '!node_modules' --glob '!dist' --glob '!build'

cd /home/sebas/Code/ballbox && ls -la && printf '\n---\n' && find . -maxdepth 2 -type f \( -name 'README*' -o -name 'PLAN.md' -o -name '.env*' -o -name 'package.json' \) | sort


/bin/bash: line 1: cd: /home/sebas/Code/ballbox: No such file or directory

Command exited with code 1


**Searching for repository**

I need to locate the repository, and I think it might be under /mnt/rpi. My best approach here is to use the `find` command to search for it. This should help me narrow down the directory quickly. While I’m at it, I want to double-check that I have the correct path and that I’m using the right syntax with `find`. Let's see what I can uncover!

find /home/sebas /mnt/rpi -maxdepth 4 -type d -name ballbox 2>/dev/null | head -20


cd /home/sebas/work/projects/ballbox && rg -n "fullscreen|vending|screen|upload|media" -S --glob '!node_modules' --glob '!dist' --glob '!build'

cd /home/sebas/work/projects/ballbox && ls -la && printf '\n---\n' && find . -maxdepth 2 -type f \( -name 'README*' -o -name 'PLAN.md' -o -name '.env*' -o -name 'package.json' \) | sort


prisma/seed.ts:1147:    screens,
tests/contracts/network/network-admin-create-club-section.test.tsx:3:import { cleanup, render, screen } from "@testing-library/react";
tests/contracts/network/network-admin-create-club-section.test.tsx:64:    expect(screen.getByTestId("section-header")).toBeTruthy();
tests/contracts/network/network-admin-create-club-section.test.tsx:65:    expect(screen.getByTestId("create-club-form")).toBeTruthy();
tests/contracts/network/network-admin-shell.test.tsx:3:import { cleanup, render, screen } from "@testing-library/react";
tests/contracts/network/network-admin-shell.test.tsx:93:    expect(screen.getByTestId("network-admin-stats")).toBeTruthy();
tests/contracts/network/network-admin-shell.test.tsx:94:    expect(screen.getByTestId("network-admin-create-club-section")).toBeTruthy();
tests/contracts/network/network-admin-shell.test.tsx:95:    expect(screen.getByTestId("network-admin-club-list")).toBeTruthy();
tests/contracts/network/network-admin-venue-card.test.tsx:3:import { cleanup, fireEvent, render, screen } from "@testing-library/react";
tests/contracts/network/network-admin-venue-card.test.tsx:22:        screens: 2,
tests/contracts/network/network-admin-venue-card.test.tsx:23:        screenIds: ["screen_a", "screen_b"],
tests/contracts/network/network-admin-venue-card.test.tsx:24:        vendingMachines: [
tests/contracts/network/network-admin-venue-card.test.tsx:29:            screenId: "screen_a",
tests/contracts/network/network-admin-venue-card.test.tsx:67:    fireEvent.change(screen.getByDisplayValue("Venue One"), { target: { value: "Venue Renamed" } });
tests/contracts/network/network-admin-venue-card.test.tsx:81:    fireEvent.click(screen.getAllByRole("checkbox")[1]);
tests/contracts/network/network-admin-venue-card.test.tsx:87:    expect(nextClubs?.[0].venues[0].vendingMachines[0].active).toBe(false);
tests/contracts/network/network-admin-club-card.test.tsx:3:import { cleanup, fireEvent, render, screen } from "@testing-library/react";
tests/contracts/network/network-admin-club-card.test.tsx:22:        screens: 2,
tests/contracts/network/network-admin-club-card.test.tsx:23:        screenIds: ["screen_a", "screen_b"],
tests/contracts/network/network-admin-club-card.test.tsx:24:        vendingMachines: [],
tests/contracts/network/network-admin-club-card.test.tsx:70:    fireEvent.change(screen.getByDisplayValue("106"), { target: { value: "999" } });
tests/contracts/network/network-admin-club-card.test.tsx:83:    expect(screen.getByText("No venues yet.")).toBeTruthy();
components/network-admin-stats.tsx:20:
Vending machines

components/network-admin-stats.tsx:26:
Screens

components/network-admin-stats.tsx:28:          {clubs.reduce((total, club) => total + club.venues.reduce((venueTotal, venue) => venueTotal + venue.screens, 0), 0)}
tests/contracts/network/backend-modules.test.ts:16:  vendingMachine: {
tests/contracts/network/backend-modules.test.ts:54:      venues: [{ screens: [{ id: "screen_001" }, { id: "screen_002" }] }],
tests/contracts/network/backend-modules.test.ts:65:      screenCount: 2,
tests/contracts/network/backend-modules.test.ts:93:        vendingMachines: { include: { screen: { select: { id: true } } } },
tests/contracts/network/backend-modules.test.ts:94:        screens: { select: { id: true } },
tests/contracts/network/backend-modules.test.ts:103:      vendingMachines: [],
tests/contracts/network/backend-modules.test.ts:104:      screens: [],
tests/contracts/network/backend-modules.test.ts:119:  it("creates vending machines with trimmed labels", async () => {
tests/contracts/network/backend-modules.test.ts:121:    prisma.vendingMachine.create.mockResolvedValue({
tests/contracts/network/backend-modules.test.ts:128:    const { createVendingMachineInDb } = await import("@/lib/network-backend-vending-machines");
tests/contracts/network/backend-modules.test.ts:129:    const result = await createVendingMachineInDb("venue_001", {
tests/contracts/network/backend-modules.test.ts:134:    expect(prisma.vendingMachine.create).toHaveBeenCalledWith({
tests/contracts/network/backend-modules.test.ts:155:  it("blocks vending machine deletion when payment sessions still exist", async () => {
tests/contracts/network/backend-modules.test.ts:156:    prisma.vendingMachine.findUnique.mockResolvedValue({
tests/contracts/network/backend-modules.test.ts:158:      screen: null,
tests/contracts/network/backend-modules.test.ts:162:    const { deleteVendingMachineInDb } = await import("@/lib/network-backend-vending-machines");
tests/contracts/network/backend-modules.test.ts:163:    const result = await deleteVendingMachineInDb("venue_001", "vm_001");
tests/contracts/network/backend-modules.test.ts:167:      error: "VENDING_MACHINE_HAS_PAYMENT_SESSIONS",
tests/contracts/network/backend-modules.test.ts:170:    expect(prisma.vendingMachine.delete).not.toHaveBeenCalled();
components/admin-shell-frame.tsx:38:      title: "Screens Runtime",
tests/contracts/players/players-admin-stats.test.tsx:3:import { cleanup, render, screen } from "@testing-library/react";
tests/contracts/players/players-admin-stats.test.tsx:32:  { id: "club_1", atcSportclubId: "106", name: "Club One", locationName: null, venues: 1, machines: 1, screens: 1 },
tests/contracts/players/players-admin-stats.test.tsx:33:  { id: "club_2", atcSportclubId: "107", name: "Club Two", locationName: null, venues: 1, machines: 1, screens: 1 },
tests/contracts/players/players-admin-stats.test.tsx:34:  { id: "club_3", atcSportclubId: "108", name: "Club Three", locationName: null, venues: 1, machines: 1, screens: 1 },
tests/contracts/players/players-admin-stats.test.tsx:45:    expect(screen.getByText("Players")).toBeTruthy();
tests/contracts/players/players-admin-stats.test.tsx:46:    expect(screen.getByText("Memberships")).toBeTruthy();
tests/contracts/players/players-admin-stats.test.tsx:47:    expect(screen.getByText("Clubs linked")).toBeTruthy();
tests/contracts/players/players-admin-stats.test.tsx:48:    expect(screen.getByText("Network clubs")).toBeTruthy();
tests/contracts/players/players-admin-stats.test.tsx:49:    expect(screen.getAllByText("2")).toHaveLength(3);
tests/contracts/players/players-admin-stats.test.tsx:50:    expect(screen.getByText("3")).toBeTruthy();
components/cta.tsx:89:                    Contacto inmediato
tests/contracts/players/players-admin-create-player-section.test.tsx:3:import { cleanup, fireEvent, render, screen } from "@testing-library/react";
tests/contracts/players/players-admin-create-player-section.test.tsx:35:    fireEvent.change(screen.getByPlaceholderText("Player name"), { target: { value: "Player Renamed" } });
tests/contracts/players/players-admin-create-player-section.test.tsx:38:    expect(screen.getByText("Players + memberships")).toBeTruthy();
tests/contracts/players/players-admin-create-player-section.test.tsx:39:    expect(screen.queryByText("Player created")).toBeNull();
tests/contracts/players/players-admin-create-player-section.test.tsx:41:    expect(screen.getByRole("button", { name: "Add player" })).toHaveProperty("disabled", true);
components/payments-admin-client.tsx:15:type PaymentsQrVendingMachineOption = {
components/payments-admin-client.tsx:23:  vendingMachines: PaymentsQrVendingMachineOption[];
components/payments-admin-client.tsx:34:  vendingMachineId: string;
components/payments-admin-client.tsx:46:  vendingMachineId: "",
components/payments-admin-client.tsx:62:  VENDING_MACHINE_NOT_FOUND: "La vending machine indicada no existe.",
components/payments-admin-client.tsx:162:  const firstVendingMachineId = qrOptions.vendingMachines[0]?.id ?? "";
components/payments-admin-client.tsx:166:    vendingMachineId: firstVendingMachineId,
components/payments-admin-client.tsx:175:    vendingMachineId: firstVendingMachineId,
components/payments-admin-client.tsx:190:    if (!anyAmountForm.vendingMachineId) return "Select a vending machine.";
components/payments-admin-client.tsx:195:  }, [anyAmountForm.productId, anyAmountForm.vendingMachineId, anyAmountMinor]);
components/payments-admin-client.tsx:203:    if (!productAmountForm.vendingMachineId) return "Select a vending machine.";
components/payments-admin-client.tsx:209:  }, [productAmountForm.amountMinor, productAmountForm.productId, productAmountForm.vendingMachineId, productAmountMinor, selectedProduct?.priceMinor]);
components/payments-admin-client.tsx:254:            vendingMachineIdMatchesReference: parsedReference ? parsedReference.vendingMachineId === next.vendingMachineId : false,
components/payments-admin-client.tsx:297:      vendingMachineId: String(payload.vendingMachineId ?? ""),
components/payments-admin-client.tsx:336:                  const machine = qrOptions.vendingMachines.find((item) => item.id === anyAmountForm.vendingMachineId);
components/payments-admin-client.tsx:346:                    vendingMachineId: anyAmountForm.vendingMachineId,
components/payments-admin-client.tsx:362:
components/payments-admin-client.tsx:363:                  Vending machine
components/payments-admin-client.tsx:364:                  <NativeSelect id="any-amount-vending-machine" value={anyAmountForm.vendingMachineId} onChange={(value) => setAnyAmountForm((current) => ({ ...current, vendingMachineId: value }))}>
components/payments-admin-client.tsx:365:                    {qrOptions.vendingMachines.map((machine) => (
components/payments-admin-client.tsx:436:                  const machine = qrOptions.vendingMachines.find((item) => item.id === productAmountForm.vendingMachineId);
components/payments-admin-client.tsx:438:                    vendingMachineId: productAmountForm.vendingMachineId,
components/payments-admin-client.tsx:465:
components/payments-admin-client.tsx:466:                  Vending machine
components/payments-admin-client.tsx:467:                  <NativeSelect id="product-amount-vending-machine" value={productAmountForm.vendingMachineId} onChange={(value) => setProductAmountForm((current) => ({ ...current, vendingMachineId: value }))}>
components/payments-admin-client.tsx:468:                    {qrOptions.vendingMachines.map((machine) => (
components/payments-admin-client.tsx:547:
{generatedQr.vendingMachineId}

components/payments-admin-client.tsx:593:
Vending machines

components/payments-admin-client.tsx:594:
{data.summary.vendingMachines}

components/payments-admin-client.tsx:622:              Surface admin para reconciliar callbacks aprobados persistidos localmente. Corrige referencia, vending machine y metadata sobre el flujo de pagos ya centralizado en Ballbox.
components/payments-admin-client.tsx:665:
components/payments-admin-client.tsx:666:                Vending machine id
components/payments-admin-client.tsx:667:                <Input id="create-vending-machine-id" value={form.vendingMachineId} onChange={(event) => setForm((current) => ({ ...current, vendingMachineId: event.target.value }))} placeholder="vending_machine_id" />
components/payments-admin-client.tsx:679:                <Input id="create-external-reference" value={form.externalReference} onChange={(event) => setForm((current) => ({ ...current, externalReference: event.target.value }))} placeholder="payment:vendingMachine:txn" />
components/payments-admin-client.tsx:715:                  !form.vendingMachineId.trim() ||
components/payments-admin-client.tsx:733:            !item.correlation.vendingMachineIdMatchesReference || !item.correlation.machineTxnIdMatchesReference;
components/payments-admin-client.tsx:755:                  <label className="text-sm text-muted-foreground" htmlFor={`payment-session-${item.sessionId}-vending-machine`}>
components/payments-admin-client.tsx:756:                    Vending machine id
components/payments-admin-client.tsx:758:                      id={`payment-session-${item.sessionId}-vending-machine`}
components/payments-admin-client.tsx:759:                      value={item.vendingMachineId}
components/payments-admin-client.tsx:760:                      onChange={(event) => updateItem(item.sessionId, (current) => ({ ...current, vendingMachineId: event.target.value }))}
components/payments-admin-client.tsx:835:                    ? `${item.correlation.parsedReference.vendingMachineId} / ${item.correlation.parsedReference.machineTxnId}`
components/payments-admin-client.tsx:852:                          vendingMachineId: item.vendingMachineId,
components/hero.tsx:12:

components/hero.tsx:34:              Instalamos máquinas vending de pelotas de pádel en tu club.{" "}
tests/contracts/players/players-admin-player-list.test.tsx:3:import { cleanup, render, screen } from "@testing-library/react";
tests/contracts/players/players-admin-player-list.test.tsx:24:  { id: "club_1", atcSportclubId: "106", name: "Club One", locationName: null, venues: 1, machines: 1, screens: 1 },
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:62:    expect(screen.getAllByTestId("player-card")).toHaveLength(2);
tests/contracts/players/players-admin-shell.test.tsx:3:import { cleanup, render, screen } from "@testing-library/react";
tests/contracts/players/players-admin-shell.test.tsx:49:  { id: "club_1", atcSportclubId: "106", name: "Club One", locationName: null, venues: 1, machines: 1, screens: 1 },
tests/contracts/players/players-admin-shell.test.tsx:94:    expect(screen.getByTestId("players-admin-stats")).toBeTruthy();
tests/contracts/players/players-admin-shell.test.tsx:95:    expect(screen.getByTestId("players-admin-create-player-section")).toBeTruthy();
tests/contracts/players/players-admin-shell.test.tsx:96:    expect(screen.getByTestId("players-admin-player-list")).toBeTruthy();
tests/contracts/players/players-admin-player-card.test.tsx:3:import { cleanup, fireEvent, render, screen } from "@testing-library/react";
tests/contracts/players/players-admin-player-card.test.tsx:33:  { id: "club_1", atcSportclubId: "106", name: "Club One", locationName: null, venues: 1, machines: 1, screens: 1 },
tests/contracts/players/players-admin-player-card.test.tsx:34:  { id: "club_2", atcSportclubId: "107", name: "Club Two", locationName: null, venues: 1, machines: 1, screens: 1 },
tests/contracts/players/players-admin-player-card.test.tsx:65:    fireEvent.change(screen.getByDisplayValue("Player One"), { target: { value: "Player Renamed" } });
tests/contracts/players/players-admin-player-card.test.tsx:78:    expect(screen.getByText("No memberships yet.")).toBeTruthy();
tests/contracts/players/players-admin-player-card.test.tsx:79:    expect(screen.getByRole("button", { name: "Add membership" })).toHaveProperty("disabled", false);
app/classes-by-date/page.tsx:11:
app/player/[screenId]/route.ts:44:  @media (max-width: 960px) {
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:79:          Volver a screens
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:152:  if (!manifest.ok && manifest.error === "SCREEN_NOT_FOUND") {
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/coaches/settings/page.tsx:7:
app/coaches/page.tsx:7:
public/player-runtime.js:9:  const screenId = bootstrap.screenId;
public/player-runtime.js:11:    screenId,
public/player-runtime.js:79:    const activeSource = state.mediaSyncPending
public/player-runtime.js:81:      : activeCreative && state.cachedMediaSources[activeCreative.id]
public/player-runtime.js:87:      screenId: screenId,
public/player-runtime.js:123:    link.download = `ballbox-parity-${screenId}-${Date.now()}.json`;
public/player-runtime.js:158:      elements.count.textContent = `La pantalla ${screenId} no tiene contenido activo.`;
public/player-runtime.js:164:    if (state.mediaSyncPending && !state.cachedMediaSources[activeCreative.id]) {
public/player-runtime.js:167:      elements.count.textContent = `Preparing cached media for creative ${state.activeIndex + 1} de ${state.manifest.creatives.length}`;
public/player-runtime.js:173:    const nextSrc = state.cachedMediaSources[activeCreative.id] || runtime.normalizeCreativeUrl(activeCreative.url);
public/player-runtime.js:276:    await runtime.syncCachedMedia();
app/page.tsx:14:
public/icon.svg:3:    @media (prefers-color-scheme: light) {
public/icon.svg:7:    @media (prefers-color-scheme: dark) {
app/api/integrations/payments/orders/route.ts:44:  const vendingMachine = await prisma.vendingMachine.findUnique({
app/api/integrations/payments/orders/route.ts:45:    where: { id: parsedBody.data.vendingMachineId },
app/api/integrations/payments/orders/route.ts:48:  if (!vendingMachine) {
app/api/integrations/payments/orders/route.ts:49:    return paymentsErrorResponse("VENDING_MACHINE_NOT_FOUND", 404);
app/api/integrations/payments/orders/route.ts:71:    vendingMachineId: parsedBody.data.vendingMachineId,
app/api/integrations/payments/orders/route.ts:90:    parsedBody.data.description?.trim() || `${product.name} (${vendingMachine.label || vendingMachine.id})`;
app/api/integrations/payments/orders/route.ts:93:    vendingMachineId: parsedBody.data.vendingMachineId,
app/clubs/coaches/page.tsx:14:
app/clubs/page.tsx:14:
app/clubs/venues/[venueId]/page.tsx:15:
app/classes/(authenticated)/layout.tsx:6:

app/offline/page.tsx:5:
app/admin/page.tsx:13:      { label: "Screens", value: metrics.tvs.screens },
app/admin/page.tsx:18:      { label: "Machines", value: metrics.payments.vendingMachines },
app/admin/page.tsx:60:            Open screens runtime
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:4:Move Ballbox machine ad/media control from hardcoded repo mappings to API-managed desired state so future changes do not require rebuilding or reinstalling the Android sidecar.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:9:- `ImageScreen/` is the fullscreen standby slot.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:13:- Freshly synced media may require restarting the main vending app or rebooting Android before visible playback updates reliably apply.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:14:- The implementation plan should assume both actions are useful: restart vending app and reboot Android.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:15:- High-resolution/high-fps fullscreen video can decode poorly on the machine; a 2160x3840 60fps test clip played in slow motion for the first seconds.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:16:- A safer fullscreen profile worked better operationally: 30fps with lower decode load. Current compromise test moved from 720x1280 30fps to a higher-quality 1080x1920 30fps version to preserve visual quality while staying below the failing 60fps profile.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:45:  "managedDirs": ["ImageScreen", "VideoAndImageAd"],
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:48:      "target": "ImageScreen/foo.mp4",
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:66:- admin-authenticated upload and assignment UI/API
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:97:  - restart vending app
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:104:- add upload flow and stored asset references
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:117:That gives immediate leverage while keeping migration path open to DB/admin UI.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:119:## Immediate implementation steps
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:124:5. constrain fullscreen transcode defaults to machine-safe profiles like 24/30fps and lower resolution instead of assuming source originals are decodable in real time
docs/tcn-remote-ads-autonomy-2026-06-06.md:9:The active research direction is to control machine ads/media through the Android runtime and known machine file paths instead of depending on Yunshu cloud entitlements.
docs/tcn-remote-ads-autonomy-2026-06-06.md:21:- Felipe later said: `yo puse un video en la carpeta ImageScreen` and clarified it was `el video grande`.
docs/tcn-remote-ads-autonomy-2026-06-06.md:31:- Therefore remote ad/media control should be evaluated first as a machine-side file sync problem, not first as an OurVend VIP/API problem.
docs/tcn-remote-ads-autonomy-2026-06-06.md:39:Confirmed vendor media buckets used by Ballbox packages:
docs/tcn-remote-ads-autonomy-2026-06-06.md:42:- `TcnFoldercopy/ImageScreen/`
docs/tcn-remote-ads-autonomy-2026-06-06.md:76:- `TcnFoldercopy/ImageScreen/流程图.jpg`
docs/tcn-remote-ads-autonomy-2026-06-06.md:82:- `TcnFoldercopy/ImageScreen/ballbox-home-secondary.png`
docs/tcn-remote-ads-autonomy-2026-06-06.md:92:- Felipe explicitly said he put a video into `ImageScreen`.
docs/tcn-remote-ads-autonomy-2026-06-06.md:110:- some visible media can be controlled by loose files in vendor folders
docs/tcn-remote-ads-autonomy-2026-06-06.md:111:- other visible media are embedded inside APK resources
docs/tcn-remote-ads-autonomy-2026-06-06.md:113:  1. file-bucket sync for loose media
docs/tcn-remote-ads-autonomy-2026-06-06.md:126:The modified TCN app itself fetches Ballbox media and writes or reads from Ballbox-managed sources.
docs/screen-runtime-delivery-investigation.md:1:# Screen Runtime Delivery Investigation
docs/screen-runtime-delivery-investigation.md:4:- TVs with limited connectivity should not have to download the broader Ballbox Next runtime just to play scheduled media.
docs/screen-runtime-delivery-investigation.md:7:- Historical baseline: `/screens/[screenId]` was rendered from the Ballbox Next host app through a client component in `components/screen-player.tsx`.
docs/screen-runtime-delivery-investigation.md:8:- Current production direction: `/player/[screenId]` is served from Ballbox as plain HTML + JavaScript.
docs/screen-runtime-delivery-investigation.md:16:- Measured current client chunks for the legacy `/screens/[screenId]` build:
docs/screen-runtime-delivery-investigation.md:30:  - `/api/tvs/manifest/[screenId]`
docs/screen-runtime-delivery-investigation.md:49:- Keep Ballbox APIs, but replace the screen page with a minimal HTML + vanilla JS player shell.
docs/screen-runtime-delivery-investigation.md:89:1. Extract playback/cache/reporting logic from `ScreenPlayer` into framework-agnostic modules.
docs/screen-runtime-delivery-investigation.md:91:   - candidate path: `/player/[screenId]`
docs/screen-runtime-delivery-investigation.md:95:5. Legacy `/screens/[screenId]` path removed in favor of `/player/[screenId]`.
docs/ballbox-db-plan.md:19:- `VendingMachine`
docs/ballbox-db-plan.md:20:- `Screen`
docs/ballbox-db-plan.md:38:- `Club -> Venue -> VendingMachine`
docs/ballbox-db-plan.md:39:- `Club -> Venue -> Screen`
docs/ballbox-db-plan.md:40:- `Screen -> TvDevice` optional one-to-one
docs/ballbox-db-plan.md:41:- `Screen -> Assignment -> Campaign`
docs/ballbox-db-plan.md:42:- `VendingMachine -> PaymentSession`
docs/ballbox-db-plan.md:53:   - `Screen.deviceId`
docs/ballbox-db-plan.md:55:4. Payments now persist canonical `vendingMachineId` and `productId` foreign keys.
docs/ballbox-db-plan.md:56:5. Post-`pnpm db:push` plus `pnpm db:seed` verification against Ballbox Postgres confirms physical tables still expose canonical `Screen.vendingMachineId`, `Impression.tvDeviceId`, and `PaymentSession.vendingMachineId`.
app/admin/screens/page.tsx:2:import { ScreensRuntimeGrid } from "@/components/screens-runtime-grid";
app/admin/screens/page.tsx:9:  { label: "TV device sync", value: "Polling internal /api/tvs/manifest/:screenId" },
app/admin/screens/page.tsx:11:  { label: "Media core", value: "Lightweight HTML + JS player shell" },
app/admin/screens/page.tsx:14:export default async function AdminScreensPage() {
app/admin/screens/page.tsx:22:
Screens Runtime

app/admin/screens/page.tsx:25:            mientras el backend especializado de media sigue separado.
app/admin/screens/page.tsx:65:
Available screens

app/admin/screens/page.tsx:72:        <ScreensRuntimeGrid initialScreens={source.screens} />
app/admin/screens/page.tsx:86:
Hay {source.screens.length} screens disponibles para runtime individual.

docs/repo-map.md:17:- UI surfaces: `app/admin/tvs`, `app/admin/screens`, `app/player/[screenId]/route.ts`
docs/implemented-sports.md:21:- `/clubs/venues/[venueId]` now gives each venue its own configuration screen, with the same shared club navbar and a guided step-by-step layout where base settings, coach hours, reservation pricing, courts, and venue-specific coach assignment are all visible without hidden accordions; weekly coach hours and coach-enabled court selection stay on-screen by default, and each coach can either float across all enabled courts with `Asignar todas las canchas` or hold one fixed court inside that venue.
docs/implemented-sports.md:23:- Authenticated coach/club screens now share the same top navbar with section tabs plus a profile-initial menu for sign out; the old corner text links and dedicated explainer hero panels are gone, and the shared navbar now exposes explicit nav labels, `aria-current`/pressed state, and a screen-reader label on the profile trigger.
docs/implemented-sports.md:24:- Sports auth/request/venue flows now also announce key feedback through semantic live regions: `/coaches` sign-in/sign-up errors use `role="alert"`, `/classes/[offerId]` request success/error copy is announced, and `/clubs/venues/[venueId]` autosave state is exposed as a live status badge for screen-reader users.
docs/coaches-discovery-execution-plan.md:7:- Therefore the immediate job is not shipping the coach product; it is reducing uncertainty in the right order.
app/admin/login/page.tsx:36:
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-execution-plan.md:60:  - test real QR render on machine screen
app/layout.tsx:14:  title: 'Ballbox | Máquinas Vending de Pelotas de Pádel para Clubes',
app/layout.tsx:18:    title: 'Ballbox | Máquinas Vending de Pelotas de Pádel para Clubes',
app/layout.tsx:24:    title: 'Ballbox | Máquinas Vending de Pelotas de Pádel para Clubes',
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:4:- Treat the current `/admin/payments` QR generator as an operator test harness, not the final production payment UX for vending machines.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:5:- Production vending flow should integrate with the TCN machine protocol and let the machine render the buyer-facing QR on its own screen.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:9:- That wording applies to **machine-side QR scanning**, not to the machine displaying a QR on its screen for the buyer to scan with a phone.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:21:5. TCN machine renders the QR on its screen.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:31:  - `MachineID` -> Ballbox `vendingMachineId` or external machine identifier
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:45:- Decide the canonical mapping between TCN `MachineID` and Ballbox `VendingMachine`.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:54:- Do not frame it as the final vending-machine payment UX.
docs/implemented-host-app.md:9:- The public home navigation now surfaces `Reservas de cancha` and `Sign in` directly instead of exposing coach/admin labels in the marketing navbar, and both desktop/mobile nav links now expose explicit labels plus `aria-current` state for keyboard and screen-reader users.
docs/implemented-host-app.md:23:- Native payment persistence validates canonical `payment-vm_{vendingMachineId}-txn_{machineTxnId}` references before writing local approved sessions.
docs/implemented-host-app.md:28:- Ballbox owns internal venues/screens/campaigns data and persists TVs in Ballbox Postgres; clubs are ATC-sourced and stored as `atcSportclubId` links only.
docs/implemented-host-app.md:36:- TVs player delivery is a special-case host-app surface: `/player/[screenId]` is served from Ballbox but intentionally bypasses the Next client/React runtime in favor of plain HTML + JavaScript for constrained devices.
docs/implemented-host-app.md:39:- Network now has DB-backed admin CRUD for ATC-linked clubs, venues, and vending machines.
docs/tcn-machine-reality-2026-05-26.html:24:
Purpose: current source of truth for Ballbox work on TCN vending machines.

docs/tcn-machine-reality-2026-05-26.html:43:
Machine hardware	TCN vending machine, observed model TCN-CSC-8C(V49).

docs/tcn-machine-reality-2026-05-26.html:44:
Machine UI/runtime	Android-based local runtime with backstage/technical menus, local storage, USB import/export, ads/media support.

docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:82:- temporary in-code or env-backed mapping by `vendingMachineId`
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:89:- `payment-vm_{vendingMachineId}-txn_{machineTxnId}`
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:95:- `vendingMachineId`
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:116:- `vendingMachineId`
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:161:  vendingMachineId,
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:168:resolvePaymentOwnerForMachine(vendingMachineId)
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:199:Add machine ownership to `VendingMachine` and payment initiation state to a new `PaymentOperation` table.
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:208:- machine owner config stored in a local mapping module keyed by `vendingMachineId`
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-implementation-rfc.md:227:2. derive `vendingMachineId`
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-rollout-rfc.md:4:- Proposed operating plan for the first vending rollout.
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-rollout-rfc.md:8:- Use one Ballbox-controlled technical flow for all vending machines.
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-rollout-rfc.md:20:- The immediate risk is not payout automation. The immediate risk is getting the machine payment path working end to end.
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-rollout-rfc.md:99:- vending machine identity
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-rollout-rfc.md:111:- provider order / external reference -> vending machine -> payment owner mode -> MP config
docs/streetcast-parity.md:4:- Manifest polling: covered by `/player/[screenId]` lightweight runtime refresh loop.
docs/streetcast-parity.md:6:- Local media cache: covered by screen-scoped browser cache with cached blob playback preference.
docs/streetcast-parity.md:7:- Background preload: covered by manifest-driven media warming after each runtime refresh.
docs/streetcast-parity.md:10:- Browser automation parity: covered by Playwright cached-blob/offline checks on `/player/[screenId]`.
docs/streetcast-parity.md:14:- The production TV runtime path is `/player/[screenId]`.
docs/streetcast-parity.md:20:- 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.
docs/streetcast-parity.md:21:- Seeded local evidence now has active TV programming again after the TV seed campaign windows were extended in `prisma/seed-data.ts`, so parity runs can use stock `screen_001` after `pnpm db:seed`.
docs/streetcast-parity.md:26:- Stock seed data now provides active TV programming for `screen_001` after `pnpm db:seed`, so local cached-blob playback, first impression write, queued-impression recovery, and manifest-refresh-failure continuity can be validated without manual TV fixture repair first.
docs/streetcast-parity.md:29:1. Open `/player/screen_001` on the target TV/browser with Ballbox pointed at the real/shared DB.
docs/streetcast-parity.md:50:    - exact screen id used
docs/coaches-coordination-gap-analysis.md:51:- no workflow screen for managing one class/group over time
docs/coaches-coordination-gap-analysis.md:102:2. add one class coordination screen or drawer
docs/tcn-future-action-options.html:25:
Prioritize the parts needed for clean machine delivery first: logos, welcome screens, product visuals, visible labels, and language cleanup.

docs/tcn-future-action-options.html:41:
Why now: biggest immediate operational value after delivery readiness.

docs/coaches-solution-thesis.md:28:- It uses ATC public-read availability immediately, without blocking on admin-grade access.
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:32:- `ImageScreen/流程图.jpg`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:36:- `/machine-assets/2601070188/ImageScreen/流程图.jpg`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:52:- `public/machine-assets/2601070188/ImageScreen/流程图.jpg`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:106:- add simple operator upload/change flow later
docs/html-first-accessibility-plan.md:25:- Add keyboard + screen reader acceptance checks for core booking and admin flows.
docs/coach-availability-rfc.md:16:- `/clubs/venues/[venueId]` now gives each venue a dedicated setup screen with opening/closing time, weekly coach hours, external-coach availability, reservation settings including a club-only discount, courts, and venue-specific coach assignment.
docs/coach-availability-rfc.md:17:- Authenticated coach/club screens now use a shared navbar with section tabs and a profile-initial menu for sign out instead of top-corner text links or standalone explainer hero panels.
docs/classes-mvp-rfc.md:133:## Immediate Follow-up
docs/payments-club-settlement-strategy.md:5:- Do not force Mercado Pago split payments into the current vending QR flow.
docs/payments-club-settlement-strategy.md:15:- Each vending machine belongs to one club/venue or tenant contract.
docs/payments-club-settlement-strategy.md:18:- Settlement strategy still matters whenever Ballbox is the economic collector, even if the physical machine payment rail is mediated by third parties.
docs/payments-club-settlement-strategy.md:32:- vendingMachineId
docs/payments-club-settlement-strategy.md:63:- current QR vending flow does not map cleanly
docs/maintainability-priorities-2026-03.md:108:  - `components/screen-player.tsx`
docs/maintainability-priorities-2026-03.md:110:  - `app/player/[screenId]/route.ts`
docs/maintainability-priorities-2026-03.md:119:  - TV runtime cache/media/impression/heartbeat/manifest-refresh logic now lives in one shared core used by both shells.
docs/maintainability-priorities-2026-03.md:149:  - Inconsistent candidates: `app/api/admin/tvs/screens/[screenId]/route.ts`, `app/api/admin/tvs/tv-devices/route.ts`, `app/api/admin/payments/sessions/route.ts`
docs/bb-002-slice-1-plan.md:50:- `/player/[screenId]` behavior stays unchanged
docs/coaches-opportunity-brief.md:78:## Immediate Next Questions
docs/implemented-network.md:5:- Ballbox owns the canonical shared `network` model for venues, vending machines, screens, and tv devices; clubs are now ATC-sourced via `atcSportclubId` and hydrated at runtime.
docs/implemented-network.md:7:- Current network CRUD covers clubs, venues, and vending machines.
docs/implemented-network.md:14:- Vending-machine route handlers now call `lib/network-backend-vending-machines.ts` directly and use `lib/prisma-errors.ts` for unique-constraint classification.
docs/implemented-network.md:25:- Ballbox persists `Club`, `Venue`, and `VendingMachine` rows directly in Ballbox Postgres.
docs/implemented-network.md:28:- `Screen` and `TvDevice` also live in the same shared core and are consumed today by TVs and payments flows.
docs/implemented-tvs.md:5:- Ballbox owns the canonical TVs entity model for clubs, venues, screens, campaigns, assignments, tv devices, and impressions.
docs/implemented-tvs.md:9:- `/admin/tvs` links each screen directly to its runtime and manifest.
docs/implemented-tvs.md:10:- `/admin/tvs` now supports admin CRUD for screens and tv devices through Ballbox-owned admin APIs.
docs/implemented-tvs.md:11:- `/admin/tvs` now supports admin CRUD for campaigns, creatives, and screen assignments through Ballbox-owned admin APIs.
docs/implemented-tvs.md:15:- `/api/tvs/screens`
docs/implemented-tvs.md:16:- `/api/tvs/manifest/[screenId]`
docs/implemented-tvs.md:20:- `/api/admin/tvs/screens`
docs/implemented-tvs.md:21:- `/api/admin/tvs/screens/[screenId]`
docs/implemented-tvs.md:35:- `/admin/screens` lists available screens.
docs/implemented-tvs.md:36:- `/player/[screenId]` renders the lightweight TV runtime path served as plain HTML + JavaScript.
docs/implemented-tvs.md:37:- The runtime rotates creatives from a Ballbox DB-backed manifest derived from active screen-to-campaign assignments.
docs/implemented-tvs.md:41:- Runtime now warms a screen-scoped browser media cache and prefers cached blob playback when available.
docs/implemented-tvs.md:42:- Runtime keeps the last good manifest during transient manifest fetch failures so cached media can continue playing offline.
docs/implemented-tvs.md:43:- Runtime preloads manifest creatives after refresh and evicts stale cached media when programming changes.
docs/implemented-tvs.md:44:- Cache/media/impression/heartbeat/manifest-refresh behavior now comes from one shared runtime core in `lib/tvs-player-runtime-core.ts`; React and lightweight HTML+JS are delivery adapters over the same logic.
docs/implemented-tvs.md:45:- Browser e2e coverage now verifies cached blob playback survives an offline transition on `/player/[screenId]`.
docs/implemented-tvs.md:48:- Stock demo seed scheduling now keeps `screen_001` and `screen_002` on active campaign windows after `pnpm db:seed`, so local parity/e2e runs no longer depend on manually refreshing fixture dates first.
docs/implemented-tvs.md:49:- TVs admin and `/admin/screens` link operators to the lightweight player path.
docs/implemented-tvs.md:51:- `GET /api/tvs/manifest/[screenId]` returns:
docs/implemented-tvs.md:52:  - `404` when the screen does not exist
docs/implemented-tvs.md:53:  - `409` when the screen exists but has no active programming
docs/implemented-tvs.md:59:- `POST /api/tvs/impression` now returns `409` when the creative exists but is not active for the target screen.
docs/implemented-tvs.md:65:- Post-`pnpm db:push` plus `pnpm db:seed` verification confirms Ballbox Postgres keeps canonical `Screen.vendingMachineId` and `Impression.tvDeviceId`, with no legacy `deviceId` column reintroduced.
docs/implemented-tvs.md:75:  - lightweight `/player/[screenId]` cached blob playback and runtime side effects when the target screen has active programming
docs/implemented-tvs.md:76:  - admin CRUD for screens, tv devices, campaigns, creatives, and assignments
docs/coaches-icp-segmentation.md:43:## Immediate Decision Rule
docs/implemented-payments.md:34:- Post-`pnpm db:push` plus `pnpm db:seed` verification confirms Ballbox Postgres keeps canonical `PaymentSession.vendingMachineId` and does not expose a `PaymentSessionRef` table or `machineId` compatibility column.
docs/implemented-payments.md:46:- Native payment correlation requires canonical `externalReference` values shaped as `payment-vm_{vendingMachineId}-txn_{machineTxnId}`.
docs/domain-model-target.md:6:- End state does not require backward compatibility with current naming or legacy intermediate fields.
docs/domain-model-target.md:15:- `VendingMachine`
docs/domain-model-target.md:19:- `Screen`
docs/domain-model-target.md:22:  - optional link to `VendingMachine`
docs/domain-model-target.md:23:  - optional: `vendingMachineId`
docs/domain-model-target.md:25:  - belongs to `Screen`
docs/domain-model-target.md:26:  - required: `screenId`
docs/domain-model-target.md:27:  - one device per screen
docs/domain-model-target.md:33:  - belongs to `VendingMachine`
docs/domain-model-target.md:35:  - required: `vendingMachineId`
docs/domain-model-target.md:49:  - joins `Campaign` to `Screen`
docs/domain-model-target.md:51:  - required: `screenId`
docs/domain-model-target.md:83:- `Venue 1 -> N VendingMachine`
docs/domain-model-target.md:84:- `Venue 1 -> N Screen`
docs/domain-model-target.md:85:- `VendingMachine 0..1 -> 0..1 Screen`
docs/domain-model-target.md:86:- `Screen 1 -> 0..1 TvDevice`
docs/domain-model-target.md:87:- `VendingMachine 1 -> N PaymentSession`
docs/domain-model-target.md:89:- `Screen 1 -> N Assignment`
docs/domain-model-target.md:100:- rename current `Machine` references to `VendingMachine`
docs/domain-model-target.md:101:- rename current `ScreenCampaignAssignment` to `Assignment`
docs/domain-model-target.md:106:- `PaymentSession`, `Assignment`, `VendingMachine`, and `TvDevice` are the canonical persisted names.
app/api/network/venues/[venueId]/vending-machines/route.ts:2:  networkVendingMachineMutationResponseSchema,
app/api/network/venues/[venueId]/vending-machines/route.ts:3:  vendingMachineCreateSchema,
app/api/network/venues/[venueId]/vending-machines/route.ts:5:import { createVendingMachineInDb } from "@/lib/network-backend-vending-machines";
app/api/network/venues/[venueId]/vending-machines/route.ts:16:  const parsedBody = await parseNetworkJsonBody(request, vendingMachineCreateSchema);
app/api/network/venues/[venueId]/vending-machines/route.ts:23:    const machine = await createVendingMachineInDb(venueId, parsedBody.data);
app/api/network/venues/[venueId]/vending-machines/route.ts:29:    return networkJsonResponse(networkVendingMachineMutationResponseSchema.parse({ ok: true, item: machine }), 201);
app/api/network/venues/[venueId]/vending-machines/route.ts:32:      return networkErrorResponse("VENDING_MACHINE_ID_CONFLICT", 409);
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:3:  networkVendingMachineMutationResponseSchema,
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:4:  vendingMachineUpdateSchema,
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:7:  deleteVendingMachineInDb,
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:8:  updateVendingMachineInDb,
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:9:} from "@/lib/network-backend-vending-machines";
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:20:  context: { params: Promise<{ venueId: string; vendingMachineId: string }> }
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:22:  const { venueId, vendingMachineId } = await context.params;
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:23:  const parsedBody = await parseNetworkJsonBody(request, vendingMachineUpdateSchema);
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:30:    const machine = await updateVendingMachineInDb(venueId, vendingMachineId, parsedBody.data);
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:33:      return networkErrorResponse("VENDING_MACHINE_NOT_FOUND", 404);
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:36:    return networkJsonResponse(networkVendingMachineMutationResponseSchema.parse({ ok: true, item: machine }));
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:39:      return networkErrorResponse("VENDING_MACHINE_ID_CONFLICT", 409);
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:48:  context: { params: Promise<{ venueId: string; vendingMachineId: string }> }
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:50:  const { venueId, vendingMachineId } = await context.params;
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:51:  const result = await deleteVendingMachineInDb(venueId, vendingMachineId);
app/api/network/venues/[venueId]/vending-machines/[vendingMachineId]/route.ts:54:    const status = result.error === "VENDING_MACHINE_NOT_FOUND" ? 404 : 409;
app/api/admin/summary/route.ts:24:        vendingMachines: metrics.network.machines,
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/api/tvs/heartbeat/route.ts:7:import { recordScreenHeartbeatInDb } from "@/lib/tvs-backend-runtime";
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:25:    const status = heartbeat.error === "SCREEN_TV_DEVICE_NOT_LINKED" ? 409 : 404;
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/screens/route.ts:2:import { tvScreensResponseSchema } from "@/lib/contracts/tvs";
app/api/tvs/screens/route.ts:9:    tvScreensResponseSchema.parse({
app/api/tvs/screens/route.ts:15:    items: source.screens,
app/api/tvs/health/route.ts:17:      screens: source.screens.length,
app/api/admin/tvs/assignments/[assignmentId]/route.ts:30:      result.error === "ASSIGNMENT_NOT_FOUND" || result.error === "SCREEN_NOT_FOUND"
app/api/admin/tvs/screens/route.ts:2:import { createAdminTvsErrorResponse, tvAdminScreenCreateSchema } from "@/lib/contracts/admin-tvs";
app/api/admin/tvs/screens/route.ts:3:import { createTvScreenInDb } from "@/lib/tvs-backend-screens";
app/api/admin/tvs/screens/route.ts:7:  const parsedBody = tvAdminScreenCreateSchema.safeParse(body);
app/api/admin/tvs/screens/route.ts:16:  const result = await createTvScreenInDb(parsedBody.data);
app/api/admin/tvs/campaigns/[campaignId]/assignments/route.ts:24:      result.error === "CAMPAIGN_NOT_FOUND" || result.error === "SCREEN_NOT_FOUND"
app/api/admin/tvs/screens/[screenId]/route.ts:2:import { createAdminTvsErrorResponse, tvAdminScreenUpdateSchema } from "@/lib/contracts/admin-tvs";
app/api/admin/tvs/screens/[screenId]/route.ts:3:import { deleteTvScreenInDb, updateTvScreenInDb } from "@/lib/tvs-backend-screens";
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:11:  const parsedBody = tvAdminScreenUpdateSchema.safeParse(body);
app/api/admin/tvs/screens/[screenId]/route.ts:20:  const result = await updateTvScreenInDb(screenId, parsedBody.data);
app/api/admin/tvs/screens/[screenId]/route.ts:24:      result.error === "SCREEN_NOT_FOUND" ||
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);
app/api/admin/tvs/screens/[screenId]/route.ts:44:    const status = result.error === "SCREEN_NOT_FOUND" ? 404 : 409;

[Showing lines 961-1366 of 1366 (50.0KB limit). Full output: /tmp/pi-bash-8cd574b4c0206eb1.log]


total 772
drwxr-xr-x 18 sebas sebas   4096 Jun  8 16:23 .
drwxr-xr-x 11 sebas sebas   4096 Jun  6 18:04 ..
-rw-rw-r--  1 sebas sebas   4949 Jun  8 23:33 AGENTS.md
drwxr-xr-x  3 sebas sebas   4096 Jun  6 18:00 android
drwxr-xr-x 12 sebas sebas   4096 May 13 11:33 app
-rw-r--r--  1 sebas sebas   1700 Mar 21 15:36 CLAUDE.md
drwxr-xr-x  3 sebas sebas   4096 May 17 05:48 components
-rw-r--r--  1 sebas sebas    427 Mar 21 15:36 components.json
drwxr-xr-x  3 sebas sebas   4096 Jun  8 16:23 data
-rw-rw-r--  1 sebas sebas    506 Apr 15 18:12 docker-compose.yml
drwxr-xr-x  4 sebas sebas   4096 Jun  8 16:23 docs
-rw-rw-r--  1 sebas sebas    530 Apr 15 18:12 .env.example
-rw-rw-r--  1 sebas sebas    617 Apr 20 15:17 .env.local
-rw-r--r--  1 sebas sebas    574 Apr 22 15:09 .env.production
-rw-r--r--  1 sebas sebas    369 Apr 10 19:31 eslint.config.mjs
drwxr-xr-x  9 sebas sebas   4096 Jun  8 17:25 .git
drwxr-xr-x  2 sebas sebas   4096 May  6 09:49 .githooks
drwxr-xr-x  3 sebas sebas   4096 Apr 11 10:42 .github
-rw-r--r--  1 sebas sebas    376 Apr 22 15:15 .gitignore
drwxr-xr-x  4 sebas sebas   4096 Jun  6 16:38 lib
drwxr-xr-x  8 sebas sebas   4096 Jun  8 17:27 .next
-rw-r--r--  1 sebas sebas    410 Apr 10 19:31 next.config.mjs
-rw-r--r--  1 sebas sebas    247 Jun  6 17:45 next-env.d.ts
drwxr-xr-x 13 sebas sebas   4096 Apr 24 18:06 node_modules
-rw-r--r--  1 sebas sebas   4256 May  7 16:23 package.json
-rw-rw-r--  1 sebas sebas   4934 May 26 16:28 PLAN.md
-rw-r--r--  1 sebas sebas   1316 May  7 17:30 playwright.config.ts
-rw-r--r--  1 sebas sebas 281361 Apr 10 19:31 pnpm-lock.yaml
-rw-r--r--  1 sebas sebas    144 Mar 21 15:36 postcss.config.mjs
drwxr-xr-x  3 sebas sebas   4096 May  6 09:48 prisma
-rw-r--r--  1 sebas sebas   2264 Apr 22 15:15 proxy.ts
drwxr-xr-x  5 sebas sebas   4096 Jun  6 16:37 public
-rw-rw-r--  1 sebas sebas   8450 May 18 04:36 README.md
drwxr-xr-x  2 sebas sebas   4096 Jun  6 17:41 scripts
drwxrwxr-x  2 sebas sebas   4096 May 17 05:07 test-results
drwxr-xr-x  6 sebas sebas   4096 Apr 14 10:13 tests
-rw-r--r--  1 sebas sebas    695 Mar 21 15:36 tsconfig.json
-rw-r--r--  1 sebas sebas 334290 Apr 24 13:46 tsconfig.tsbuildinfo
-rw-r--r--  1 sebas sebas    457 Apr 14 10:13 vitest.config.ts

---
./.env.example
./.env.local
./.env.production
./.next/package.json
./package.json
./PLAN.md
./README.md


**Searching for upload flow docs**

I need to find the upload flow documentation. I'll start by searching for "machine-assets/admin" as it seems like a possible route to the information I need. Okay, this could help me locate the relevant docs quickly and effectively! Hopefully, I can narrow it down and find exactly what I'm looking for without wasting too much time. Let’s see how this pans out!

cd /home/sebas/work/projects/ballbox && rg -n "machine-assets|ImageScreen|VideoAndImageAd|asset|upload flow|fullscreen" docs app lib public prisma -S


docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:9:- `ImageScreen/` is the fullscreen standby slot.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:10:- `VideoAndImageAd/` is the smaller top video slot.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:15:- High-resolution/high-fps fullscreen video can decode poorly on the machine; a 2160x3840 60fps test clip played in slow motion for the first seconds.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:16:- A safer fullscreen profile worked better operationally: 30fps with lower decode load. Current compromise test moved from 720x1280 30fps to a higher-quality 1080x1920 30fps version to preserve visual quality while staying below the failing 60fps profile.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:45:  "managedDirs": ["ImageScreen", "VideoAndImageAd"],
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:48:      "target": "ImageScreen/foo.mp4",
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:49:      "url": "https://ballbox.app/machine-assets/.../foo.mp4",
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:62:- assets still served from `public/machine-assets/...`
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:65:- DB-backed machine asset entries
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:104:- add upload flow and stored asset references
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:105:- optionally track asset history and rollback
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:111:- keep manifest truthful: only list assets that exist and hash correctly
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:124:5. constrain fullscreen transcode defaults to machine-safe profiles like 24/30fps and lower resolution instead of assuming source originals are decodable in real time
docs/tcn-remote-ads-autonomy-2026-06-06.md:19:- Felipe said the sent videos explain which asset goes where.
docs/tcn-remote-ads-autonomy-2026-06-06.md:21:- Felipe later said: `yo puse un video en la carpeta ImageScreen` and clarified it was `el video grande`.
docs/tcn-remote-ads-autonomy-2026-06-06.md:30:- Ballbox already has accepted USB update packages that replace machine-visible assets by placing files into exact vendor folders.
docs/tcn-remote-ads-autonomy-2026-06-06.md:42:- `TcnFoldercopy/ImageScreen/`
docs/tcn-remote-ads-autonomy-2026-06-06.md:45:- `TcnFoldercopy/VideoAndImageAd/`
docs/tcn-remote-ads-autonomy-2026-06-06.md:76:- `TcnFoldercopy/ImageScreen/流程图.jpg`
docs/tcn-remote-ads-autonomy-2026-06-06.md:82:- `TcnFoldercopy/ImageScreen/ballbox-home-secondary.png`
docs/tcn-remote-ads-autonomy-2026-06-06.md:92:- Felipe explicitly said he put a video into `ImageScreen`.
docs/tcn-remote-ads-autonomy-2026-06-06.md:122:- writing vendor-path assets
docs/tcn-remote-ads-autonomy-2026-06-06.md:139:### APK-embedded asset path
docs/tcn-remote-ads-autonomy-2026-06-06.md:164:2. pick one low-risk folder-driven asset slot
docs/tcn-remote-ads-autonomy-2026-06-06.md:165:3. overwrite with a clearly different test asset
lib/machine-ads-manifest.ts:5:const MACHINE_ADS_ROOT = path.join(process.cwd(), "public", "machine-assets");
lib/machine-ads-manifest.ts:41:  return path.join(MACHINE_ADS_ROOT, publicPath.replace(/^\/machine-assets\//, ""));
docs/bb-002-slice-1-plan.md:28:- maybe existing icon assets in `public/`
docs/tcn-machine-reality-2026-05-26.html:90:
Exports, logs, SQLite dumps, and asset packs should be preserved as operational artifacts.

docs/tcn-machine-reality-2026-05-26.html:97:
Android asset control for ads, branding, translations, and local content packs.

docs/tcn-future-action-options.html:23:
Investigate and standardize Ballbox branding changes in v3Skins, Text, local assets, and visible machine UI surfaces.

docs/tcn-future-action-options.html:58:
Document and standardize asset-pack generation for TCNFolder.

docs/tcn-future-action-options.html:61:
Prefer truth-preserving local asset control over fake cloud autonomy claims.

docs/tcn-simple-ads-backend-mvp-2026-06-06.md:21:- manifest is generated from files stored in `public/machine-assets/2601070188/...`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:31:- `VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:32:- `ImageScreen/流程图.jpg`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:35:- `/machine-assets/2601070188/VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:36:- `/machine-assets/2601070188/ImageScreen/流程图.jpg`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:51:- `public/machine-assets/2601070188/VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:52:- `public/machine-assets/2601070188/ImageScreen/流程图.jpg`
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:66:        "target": "VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4",
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:67:        "url": "https://ballbox.app/machine-assets/2601070188/VideoAndImageAd/See%20You%20Again%20(Cover)--音悦Tai.mp4",


# TCN API-driven machine ads RFC

## Goal
Move Ballbox machine ad/media control from hardcoded repo mappings to API-managed desired state so future changes do not require rebuilding or reinstalling the Android sidecar.

## Reality confirmed on device
- Installed sidecar can sync successfully on Android 7.1.2 once `minSdk` and storage behavior are fixed.
- Correct target root on the machine is `/sdcard/TcnFolder`.
- `ImageScreen/` is the fullscreen standby slot.
- `VideoAndImageAd/` is the smaller top video slot.
- Both folders accept looped video files.
- Current sidecar prune behavior can delete stale files inside managed folders.
- Freshly synced media may require restarting the main vending app or rebooting Android before visible playback updates reliably apply.
- The implementation plan should assume both actions are useful: restart vending app and reboot Android.
- High-resolution/high-fps fullscreen video can decode poorly on the machine; a 2160x3840 60fps test clip played in slow motion for the first seconds.
- A safer fullscreen profile worked better operationally: 30fps with lower decode load. Current compromise test moved from 720x1280 30fps to a higher-quality 1080x1920 30fps version to preserve visual quality while staying below the failing 60fps profile.

## Problem with current MVP
Current backend manifest generation is hardcoded in `lib/machine-ads-manifest.ts`.
That means every content change currently needs a code edit and deploy.
This is too slow for operator use and does not scale to arbitrary file replacement/prune.

## Target state
Ballbox should own a per-machine desired state model.
The sidecar should:
1. wait ~10 seconds after boot before first sync
2. fetch manifest
3. download listed files
4. verify hashes
5. write exact target paths
6. prune stale files inside managed directories
7. continue periodic sync afterward
8. keep a manual operator menu accessible at all times from some discoverable place

Changing content should mean changing Ballbox backend state, not Android code.

## Proposed manifest shape
```json
{
  "machineId": "2601070188",
  "generatedAt": "2026-06-08T00:00:00.000Z",
  "version": "...",
  "restartPolicy": "app",
  "targetRootHint": "/sdcard/TcnFolder",
  "managedDirs": ["ImageScreen", "VideoAndImageAd"],
  "files": [
    {
      "target": "ImageScreen/foo.mp4",
      "url": "https://ballbox.app/machine-assets/.../foo.mp4",
      "sha256": "...",
      "bytes": 123,
      "restartRequired": "app"
    }
  ]
}
```

## Desired backend model
Short term:
- file-backed machine config in repo
- one JSON document per machine
- assets still served from `public/machine-assets/...`

Medium term:
- DB-backed machine asset entries
- admin-authenticated upload and assignment UI/API
- no code edits required for normal operator changes

## Minimal data model
Per machine config:
- `machineId`
- `targetRootHint`
- `managedDirs[]`
- `restartPolicy`
- `entries[]`

Per entry:
- `target`
- `publicPath`
- `restartRequired`
- optional future metadata: `label`, `active`, `slot`, `contentType`

## Rollout plan
### Phase 1
- move hardcoded manifest config into file-backed JSON
- expose `managedDirs` and `targetRootHint` in manifest
- update sidecar default target root to `/sdcard/TcnFolder`
- keep sidecar reinstalls rare

### Phase 2
- sidecar runs automatically after boot with ~10 second delay
- sidecar repeats sync periodically in background
- sidecar keeps a manual operator menu reachable from an accessible entrypoint instead of hiding sync behind install-time flows
- operator menu includes at least:
  - sync now
  - open logs/status
  - restart vending app
  - reboot Android system

### Phase 3
- add admin mutation path in Ballbox backend
- validate target paths against allowed machine directories
- allow add/replace/remove entries per machine
- add upload flow and stored asset references
- optionally track asset history and rollback

## Safety constraints
- prune only inside explicitly managed directories
- never allow arbitrary absolute target paths from user input
- validate machine target prefixes against allowlist
- keep manifest truthful: only list assets that exist and hash correctly
- operator UX must make restart/reboot requirements explicit when runtime cache prevents hot swap
- reboot action is irreversible enough operationally that the UI should make intent obvious and avoid accidental taps

## Current implementation decision
Start with a repo-backed config file because it is the cheapest path from hardcoded code to API-driven desired state.
That gives immediate leverage while keeping migration path open to DB/admin UI.

## Immediate implementation steps
1. add machine config JSON files under repo data directory
2. refactor manifest builder to read config instead of hardcoded map
3. include `managedDirs` and `targetRootHint` in manifest response
4. update Android sidecar default root to `/sdcard/TcnFolder`
5. constrain fullscreen transcode defaults to machine-safe profiles like 24/30fps and lower resolution instead of assuming source originals are decodable in real time
6. add boot receiver / startup path that waits ~10 seconds before first sync
7. add periodic background sync scheduling
8. add persistent operator-accessible menu entry with restart-app and reboot-system actions
9. later add admin write path


**Inspecting manifest configuration**

I need to get the current manifest config file and check it out. It seems like I should look into both the library and the data involved. I’m thinking it’s crucial to ensure everything is set up correctly. Do I have all the latest versions? If not, I might need to update or adjust some parts. This feels like a straightforward task, but attention to detail is essential for a smooth process.

cd /home/sebas/work/projects/ballbox && find data public/machine-assets -maxdepth 4 -type f | sort


import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";

const MACHINE_ADS_ROOT = path.join(process.cwd(), "public", "machine-assets");
const MACHINE_ADS_CONFIG_ROOT = path.join(process.cwd(), "data", "machine-ads");

type RestartRequired = "app" | "reboot";

type MachineAdsConfigEntry = {
  target: string;
  publicPath: string;
  restartRequired: RestartRequired;
};

type MachineAdsConfig = {
  machineId: string;
  targetRootHint: string;
  managedDirs: string[];
  restartPolicy: RestartRequired;
  entries: MachineAdsConfigEntry[];
};

export type MachineAdsManifest = {
  machineId: string;
  version: string;
  generatedAt: string;
  restartPolicy: RestartRequired;
  targetRootHint: string;
  managedDirs: string[];
  files: Array<{
    target: string;
    url: string;
    sha256: string;
    bytes: number;
    restartRequired: RestartRequired;
  }>;
};

function toPublicFilePath(publicPath: string) {
  return path.join(MACHINE_ADS_ROOT, publicPath.replace(/^\/machine-assets\//, ""));
}

async function getFileDigest(filePath: string) {
  const contents = await readFile(filePath);
  return {
    sha256: createHash("sha256").update(contents).digest("hex"),
    bytes: contents.byteLength,
  };
}

async function readMachineAdsConfig(machineId: string): Promise<MachineAdsConfig | null> {
  const filePath = path.join(MACHINE_ADS_CONFIG_ROOT, `${machineId}.json`);

  try {
    const raw = await readFile(filePath, "utf8");
    return JSON.parse(raw) as MachineAdsConfig;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      return null;
    }

    throw error;
  }
}

export async function getMachineAdsManifest(machineId: string, origin: string): Promise<MachineAdsManifest | null> {
  const config = await readMachineAdsConfig(machineId);

  if (!config) {
    return null;
  }

  const resolvedFiles = await Promise.all(
    config.entries.map(async (entry) => {
      const filePath = toPublicFilePath(entry.publicPath);
      const digest = await getFileDigest(filePath);

      return {
        target: entry.target,
        url: new URL(entry.publicPath, origin).toString(),
        sha256: digest.sha256,
        bytes: digest.bytes,
        restartRequired: entry.restartRequired,
      };
    })
  );

  return {
    machineId: config.machineId,
    version: resolvedFiles.map((file) => `${file.target}:${file.sha256}`).join("|"),
    generatedAt: new Date().toISOString(),
    restartPolicy: config.restartPolicy,
    targetRootHint: config.targetRootHint,
    managedDirs: config.managedDirs,
    files: resolvedFiles,
  };
}


data/machine-ads/2601070188.json
public/machine-assets/2601070188/ImageScreen/14597490_1080x1920_30fps.mp4
public/machine-assets/2601070188/ImageScreen/14597490_2160_3840_60fps.mp4
public/machine-assets/2601070188/ImageScreen/14597490_720x1280_30fps.mp4
public/machine-assets/2601070188/ImageScreen/See You Again (Cover)--音悦Tai.mp4
public/machine-assets/2601070188/VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4


**Checking file transfer and config**

I need to copy a file from my laptop into a specific directory and update the config JSON accordingly. I think the file path might be in my Downloads folder. To transfer it, I'll likely use rsync or copy commands, which should be installed. I also need to inspect the config file to ensure that the local server can recognize the deployment. Plus, the user wants to upload a video in fullscreen, which I think is safe given its resolution. I'll also check if I should sanitize the filename.

cd /home/sebas/work/projects/ballbox && ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,codec_name,duration,bit_rate -of json public/machine-assets/2601070188/ImageScreen/14597490_1080x1920_30fps.mp4 public/machine-assets/2601070188/ImageScreen/14597490_720x1280_30fps.mp4 2>/dev/null || true


/machine-assets/2601070188/VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4


(no output)


laptop-run 'ls -lh /Users/sebas/Downloads/4536893-hd_720_1280_30fps.mp4 && ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,codec_name,bit_rate -show_entries format=duration,size -of json /Users/sebas/Downloads/4536893-hd_720_1280_30fps.mp4'


-rw-r--r--@ 1 sebas  staff   1.1M Jun  8 16:58 /Users/sebas/Downloads/4536893-hd_720_1280_30fps.mp4
zsh:1: command not found: ffprobe

Command exited with code 127


**Considering file transfer options**

I think I need to copy some files and maybe use local ffprobe for that. It could be helpful for checking the file details or formats. I might also consider using rsync on my laptop to transfer files efficiently, ensuring everything is synced correctly. It seems like a straightforward approach, but I want to make sure I’ve got all the details right for this task. Let's see how it goes!

mkdir -p /home/sebas/work/projects/ballbox/public/machine-assets/2601070188/ImageScreen && laptop-rsync /Users/sebas/Downloads/4536893-hd_720_1280_30fps.mp4 /home/sebas/work/projects/ballbox/public/machine-assets/2601070188/ImageScreen/


sending incremental file list
rsync: [sender] change_dir "/Users/sebas/Downloads" failed: No such file or directory (2)

sent 19 bytes  received 12 bytes  62.00 bytes/sec
total size is 0  speedup is 0.00
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1338) [sender=3.4.1]

Command exited with code 23


**Exploring helper usage**

I need to understand how to use the helper function, and I wonder if the remote source needs a hostname prefix. Maybe I should read up on AGENTS or inspect the helper script to get clarity. It’s also worth figuring out which `laptop-rsync` command to use for this context. So, I’ll gather all this information to ensure I’m on the right path!

type laptop-rsync && sed -n '1,220p' $(which laptop-rsync)

type laptop-read && sed -n '1,220p' $(which laptop-read)


laptop-rsync is /home/sebas/pi-config/bin/laptop-rsync
#!/usr/bin/env bash
set -euo pipefail
host="${LAPTOP_HOST:-brag-pipe-fur-kneel.emperor-ratio.ts.net}"
port="${LAPTOP_PORT:-2222}"
key="${LAPTOP_SSH_KEY:-$HOME/.ssh/id_ed25519_laptop_agent}"
ssh_cmd=(ssh -p "$port" -o BatchMode=yes -o IdentitiesOnly=yes)
if [[ -f "$key" ]]; then
  ssh_cmd+=( -i "$key" )
fi
if [[ $# -lt 2 ]]; then
  echo "usage: laptop-rsync <src> <dest> [extra rsync args...]" >&2
  echo "examples:" >&2
  echo "  laptop-rsync ${host}:/Users/sebas/Downloads/file ./" >&2
  echo "  laptop-rsync ./file ${host}:/Users/sebas/Downloads/" >&2
  exit 2
fi
rsync -av --progress -e "${ssh_cmd[*]}" "$@"


laptop-read is /home/sebas/pi-config/bin/laptop-read
#!/usr/bin/env bash
set -euo pipefail
host="${LAPTOP_HOST:-brag-pipe-fur-kneel.emperor-ratio.ts.net}"
port="${LAPTOP_PORT:-2222}"
key="${LAPTOP_SSH_KEY:-$HOME/.ssh/id_ed25519_laptop_agent}"
ssh_opts=(-p "$port" -o BatchMode=yes -o IdentitiesOnly=yes)
if [[ -f "$key" ]]; then
  ssh_opts+=( -i "$key" )
fi
if [[ $# -ne 1 ]]; then
  echo "usage: laptop-read <remote-path>" >&2
  exit 2
fi
ssh "${ssh_opts[@]}" "$host" "cat -- \"$1\""


receiving file list ... 1 file to consider
4536893-hd_720_1280_30fps.mp4

              0   0%    0.00kB/s    0:00:00
      1,106,003 100%    4.95MB/s    0:00:00 (xfr#1, to-chk=0/1)

sent 42 bytes  received 1,106,592 bytes  737,756.00 bytes/sec
total size is 1,106,003  speedup is 1.00


**Updating configuration**

I think I need to update the configuration to replace the current ImageScreen entry with a new fullscreen asset. Maybe deleting the old one would be a good idea, but I could also just point the target to the new file while keeping the existing one in the directory. If I prune the managed directories, it'll clean up stale files. Also, I should probably validate using ffprobe locally to ensure everything's working properly.

cd /home/sebas/work/projects/ballbox && ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,codec_name,bit_rate -show_entries format=duration,size -of json public/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4

cd /home/sebas/work/projects/ballbox && git status --short public/machine-assets/2601070188/ImageScreen data/machine-ads


?? public/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4


**Editing configuration settings**

I need to edit the config, but perhaps there's no code change needed after all. I should test the manifest using a local server to see if the app is running. I'll consider using a simple node snippet for the import function since it might make things easier. I wonder if it’s expected to work well with node/tsx. I definitely need to make those edits first to ensure everything is in place.


Successfully replaced 1 block(s) in /home/sebas/work/projects/ballbox/data/machine-ads/2601070188.json.

    ...
  9       "publicPath": "/machine-assets/2601070188/VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4",
 10       "restartRequired": "app"
 11     },
 12     {
-13       "target": "ImageScreen/14597490_1080x1920_30fps.mp4",
-14       "publicPath": "/machine-assets/2601070188/ImageScreen/14597490_1080x1920_30fps.mp4",
+13       "target": "ImageScreen/4536893-hd_720_1280_30fps.mp4",
+14       "publicPath": "/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4",
 15       "restartRequired": "app"
 16     }
 17   ]
 18 }

--- /home/sebas/work/projects/ballbox/data/machine-ads/2601070188.json
+++ /home/sebas/work/projects/ballbox/data/machine-ads/2601070188.json
@@ -9,10 +9,10 @@
       "publicPath": "/machine-assets/2601070188/VideoAndImageAd/See You Again (Cover)--音悦Tai.mp4",
       "restartRequired": "app"
     },
     {
-      "target": "ImageScreen/14597490_1080x1920_30fps.mp4",
-      "publicPath": "/machine-assets/2601070188/ImageScreen/14597490_1080x1920_30fps.mp4",
+      "target": "ImageScreen/4536893-hd_720_1280_30fps.mp4",
+      "publicPath": "/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4",
       "restartRequired": "app"
     }
   ]
 }


**Validating manifest**

I’m thinking I should validate the manifest, possibly through a node function. It might be helpful to inspect the package scripts to see if the app is running correctly. I wonder if there are any key details in those scripts that could help troubleshoot any issues. Maybe I should double-check everything to ensure I’m catching any potential problems early on! Let's keep exploring this to make sure everything functions smoothly.

cd /home/sebas/work/projects/ballbox && node -e "const {getMachineAdsManifest}=require('./lib/machine-ads-manifest.ts');"


dotenv -e .env.local -- tsx scripts/backfill-atc-clubs.ts

dotenv -e .env.local -- tsx scripts/revalidate-atc-clubs.ts

dotenv -e .env.local -- tsx prisma/seed.ts

vitest run tests/integration --passWithNoTests


**Considering TypeScript setup**

I'm thinking about whether to use a TS file as a no-op, and I realize I might need a better approach by using a TSX import to print the manifest. Additionally, I’m considering if a curl route is necessary while the dev server is running. Maybe I should inspect the local service, but I wonder if it's needed. I’ll go ahead and use a TSX script for clarity in this process.

cd /home/sebas/work/projects/ballbox && npx tsx -e "import { getMachineAdsManifest } from './lib/machine-ads-manifest.ts'; const m = await getMachineAdsManifest('2601070188','https://ballbox.app'); console.log(JSON.stringify(m,null,2));"


node:internal/process/promises:394
    triggerUncaughtException(err, true /* fromPromise */);
    ^

Error: Transform failed with 1 error:
/eval.ts:1:81: ERROR: Top-level await is currently not supported with the "cjs" output format
    at failureErrorWithLog (/home/sebas/work/projects/ballbox/node_modules/.pnpm/esbuild@0.27.4/node_modules/esbuild/lib/main.js:1748:15)
    at /home/sebas/work/projects/ballbox/node_modules/.pnpm/esbuild@0.27.4/node_modules/esbuild/lib/main.js:1017:50
    at responseCallbacks.<computed> (/home/sebas/work/projects/ballbox/node_modules/.pnpm/esbuild@0.27.4/node_modules/esbuild/lib/main.js:884:9)
    at handleIncomingPacket (/home/sebas/work/projects/ballbox/node_modules/.pnpm/esbuild@0.27.4/node_modules/esbuild/lib/main.js:939:12)
    at Socket.readFromStdout (/home/sebas/work/projects/ballbox/node_modules/.pnpm/esbuild@0.27.4/node_modules/esbuild/lib/main.js:862:7)
    at Socket.emit (node:events:519:28)
    at addChunk (node:internal/streams/readable:561:12)
    at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
    at Readable.push (node:internal/streams/readable:392:5)
    at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) {
  errors: [
    {
      detail: undefined,
      id: '',
      location: {
        column: 81,
        file: '/eval.ts',
        length: 5,
        line: 1,
        lineText: "import { getMachineAdsManifest } from './lib/machine-ads-manifest.ts'; const m = await getMachineAdsManifest('2601070188','https://ballbox.app'); console.log(JSON.stringify(m,null,2));",
        namespace: '',
        suggestion: ''
      },
      notes: [],
      pluginName: '',
      text: 'Top-level await is currently not supported with the "cjs" output format'
    }
  ],
  warnings: []
}

Node.js v22.22.2

Command exited with code 1


cd /home/sebas/work/projects/ballbox && npx tsx -e "import { getMachineAdsManifest } from './lib/machine-ads-manifest.ts'; (async()=>{ const m = await getMachineAdsManifest('2601070188','https://ballbox.app'); console.log(JSON.stringify(m,null,2)); })();"


**Considering deployment options**

I might need to think about deployment, as the user mentioned, "subime este video ... en la api de ballbox." It sounds like just changing the local repo may not be enough if production isn't updated. I should check the git remote or deployment method, maybe even automate it from local. I also need to confirm if the production path relates to a specific deployed branch or if it's tied to the same machine service. Let’s investigate the current setup, checking the app route and possibly comparing it for clarity.

curl -fsSL https://ballbox.app/api/machine-ads/2601070188 | jq .

cd /home/sebas/work/projects/ballbox && rg -n "machine-ads" app -S


curl: (22) The requested URL returned error: 404


app/api/machines/[machineId]/ads-manifest/route.ts:3:import { getMachineAdsManifest } from "@/lib/machine-ads-manifest";


curl -fsSL https://ballbox.app/api/machines/2601070188/ads-manifest | jq .


import { NextResponse } from "next/server";

import { getMachineAdsManifest } from "@/lib/machine-ads-manifest";

export async function GET(
  request: Request,
  context: { params: Promise<{ machineId: string }> }
) {
  const { machineId } = await context.params;
  const manifest = await getMachineAdsManifest(machineId, request.url);

  if (!manifest) {
    return NextResponse.json(
      {
        ok: false,
        error: "MACHINE_NOT_FOUND",
        message: `No simple ads manifest is configured for machine ${machineId}.`,
      },
      { status: 404 }
    );
  }

  return NextResponse.json({ ok: true, manifest });
}


**Figuring out deployment methods**

I need to deploy to live now and decide on the best deployment method. Maybe I could go with a local next service that pulls from the git main branch? I wonder if I can directly modify the deployed files or if they need to be in the same directory. It seems like ballbox.app uses /home/sebas/work/projects/ballbox for the current repository, but it doesn’t seem to auto-reload. I should inspect the process and maybe use ps to check before starting from this directory.

ps -ef | rg 'next (start|dev)|ballbox|3020|3000' -n -S


119:avahi        743       1  0 Jun08 ?        00:00:20 avahi-daemon: running [ballbox-first.local]
284:sebas     858393  841539  0 11:08 ?        00:00:00 /bin/bash -c ps -ef | rg 'next (start|dev)|ballbox|3020|3000' -n -S
286:sebas     858395  858393  0 11:08 ?        00:00:00 rg next (start|dev)|ballbox|3020|3000 -n -S


**Considering Docker and Nginx**

I’m wondering if I should look into Docker, specifically focusing on the Nginx configuration or perhaps its upstream settings. Checking the configuration for the service in question, rg ballbox.app, could provide me with valuable insights. There’s a lot to consider here, so I need to gather the right information. I’ll make sure to inspect all necessary components to find a proper solution!

systemctl --type=service --all | rg -i 'ballbox|next|node|pm2'

rg -n 'ballbox.app|work/projects/ballbox|next start|3000|3020' /home/sebas/pi-config /etc/nginx /etc/systemd -S 2>/dev/null


  kmod-static-nodes.service                                                                 loaded    active     exited       Create List of Static Device Nodes
  systemd-tmpfiles-setup-dev-early.service                                                  loaded    active     exited       Create Static Device Nodes in /dev gracefully
  systemd-tmpfiles-setup-dev.service                                                        loaded    active     exited       Create Static Device Nodes in /dev
  systemd-tpm2-setup-early.service                                                          loaded    inactive   dead         Early TPM SRK Setup
  systemd-tpm2-setup.service                                                                loaded    inactive   dead         TPM SRK Setup
  tailscaled.service                                                                        loaded    active     running      Tailscale node agent


LISTEN 0      4096                100.116.176.16:443        0.0.0.0:*
LISTEN 0      511                        0.0.0.0:80         0.0.0.0:*
LISTEN 0      4096   [fd7a:115c:a1e0::c532:b010]:443           [::]:*
LISTEN 0      511                           [::]:80            [::]:*


/home/sebas/pi-config/docs/ralph-loop-run.md:16:  --repo /home/sebas/work/projects/ballbox \
/home/sebas/pi-config/scripts/session-memory-analyzer.py:204:{json.dumps(meta.get('branch') or [], ensure_ascii=False)[:30000]}


**Evaluating deployment options**

I'm thinking about whether the hosting is static and not local. The user might mean "api de ballbox," suggesting they need to edit a deployed repository, possibly on Vercel. I need to figure out the git remote and how it relates to the deployment. If the repository auto-deploys from a commit to main, that could simplify things. However, since they asked about uploading, I might need to confirm pushing to main and check the branch status and remotes.

cd /home/sebas/work/projects/ballbox && git branch --show-current && git status --short && git remote -v

cd /home/sebas/work/projects/ballbox && rg -n "vercel|netlify|railway|render|fly|docker compose|deploy" README.md PLAN.md docs .github -S


main
 M AGENTS.md
 M data/machine-ads/2601070188.json
 M docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md
?? android/
?? public/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4
origin	https://github.com/sebasfavaron/ballbox.git (fetch)
origin	https://github.com/sebasfavaron/ballbox.git (push)


README.md:59:pnpm db:deploy
README.md:79:- `pnpm dev` siempre usa DB local: intenta arrancarla con Docker/Docker Compose primero y con paths no-Docker después, corre `db:deploy` + `db:generate`, y luego levanta Next dev.
README.md:82:- Los wrappers Prisma de migrate/deploy/status rechazan por default `DATABASE_URL` no local; para override explícito usar `ALLOW_REMOTE_PRISMA_COMMANDS=1`.
README.md:85:- Producción aplica `pnpm db:deploy` desde GitHub Actions al hacer push a `main`.
README.md:125:- `pre-push` ejecuta `docs:check`, `db:local:up`, `db:deploy`, `db:check-migrations`, `lint`, `typecheck`, `test:contracts` y `build` antes de permitir el push a `main`.
docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md:20:That means every content change currently needs a code edit and deploy.
docs/tcn-remote-ads-autonomy-2026-06-06.md:114:  2. APK patching/redeployment for embedded resources
docs/screen-runtime-delivery-investigation.md:7:- Historical baseline: `/screens/[screenId]` was rendered from the Ballbox Next host app through a client component in `components/screen-player.tsx`.
docs/screen-runtime-delivery-investigation.md:57:  - Ballbox still owns deployment and APIs
docs/screen-runtime-delivery-investigation.md:63:- Split the player into a separate static app/repo/deployment.
docs/screen-runtime-delivery-investigation.md:66:  - can optimize caching/CDN/deploy cadence independently
docs/screen-runtime-delivery-investigation.md:68:  - extra deployment surface
docs/screen-runtime-delivery-investigation.md:98:- Start by extracting logic, not by creating a new deployment.
docs/implemented-sports.md:4:- `/admin/sports` now renders a Ballbox-local admin UI for coaches, open matches, and classes.
docs/implemented-sports.md:6:- `/classes` now renders the first public student-facing sports surface.
docs/implemented-sports.md:7:- `/classes-by-date` now renders a coach-availability-driven reservation surface that starts by asking for a zone and then filters visible clubs / slots to that area.
docs/implemented-sports.md:10:- `/classes` and `/classes/[offerId]` now render Ballbox-local offer data first, then hydrate ATC public-read context asynchronously from cached host-app APIs so first paint is not gated by ATC response time.
docs/implemented-sports.md:18:- `/clubs/coaches` now checks the club session during page render too, so the `Entrenadores` card cannot land on a client-only shell that later bounces authenticated club users back to `/coaches`.
docs/implemented-sports.md:20:- `/clubs/coaches` now checks the club session during page render too, so the `Entrenadores` card cannot land on a client-only shell that later bounces authenticated club users back to `/coaches`.
docs/implemented-sports.md:47:- ATC public-read lookups are now cached briefly on the server because the visible availability query is shared across visitors; without that cache, Ballbox would repeat the same ATC fetch for each page load.
docs/implemented-sports.md:91:- The selected slot is still a local lead preference, not a confirmed reservation; ATC availability can move between render and submit, and Ballbox rejects stale selections when revalidation no longer finds the slot.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-execution-plan.md:60:  - test real QR render on machine screen
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:5:- Production vending flow should integrate with the TCN machine protocol and let the machine render the buyer-facing QR on its own screen.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-integration-plan.md:21:5. TCN machine renders the QR on its screen.
docs/coach-availability-rfc.md:270:- Render the slot if either:
docs/coach-availability-rfc.md:277:- The matching algorithm must be deterministic so repeated renders show the same slots.
docs/archive/tcn-roadmap-watch-2026-05-26/tcn-payments-open-questions.md:24:6. Does the stock TCN app render the QR natively after a successful `8000` response, or is any machine-side UI configuration required?
docs/archive/tcn-roadmap-watch-2026-05-26/payments-machine-rollout-rfc.md:115:- The machine should render the buyer-facing QR.
docs/implemented-players.md:4:- `/admin/players` renders inside Ballbox.
docs/tcn-simple-ads-backend-mvp-2026-06-06.md:14:Current live-style URL shape when deployed on Ballbox:
docs/implemented-tvs.md:4:- `/admin/tvs` renders inside Ballbox.
docs/implemented-tvs.md:36:- `/player/[screenId]` renders the lightweight TV runtime path served as plain HTML + JavaScript.
docs/implemented-tvs.md:50:- This is the main special-case surface in Ballbox: TVs stay inside the same repo/deploy/domain, but the device runtime is intentionally served outside the Next client/React runtime as plain HTML + JavaScript.
docs/implemented-network.md:4:- `/admin/network` renders inside Ballbox.
docs/coaches-design-partner-outreach.md:17:No estamos vendiendo nada ahora. Queremos entender el workflow real y aprender de gente que lo vive todas las semanas.
.github/workflows/db-deploy-production.yml:1:name: db-deploy-production
.github/workflows/db-deploy-production.yml:9:  deploy-production-migrations:
.github/workflows/db-deploy-production.yml:33:        run: pnpm db:deploy
docs/maintainability-priorities-2026-03.md:76:- Problem: admin surfaces bundle data loading, mutation orchestration, local editing state, messaging, and render logic in very large client components.
docs/maintainability-priorities-2026-03.md:92:  - Split data and mutation logic from rendering.
docs/maintainability-priorities-2026-03.md:133:  - broad rerenders
docs/maintainability-priorities-2026-03.md:313:- Keep one deployable app, but enforce internal module boundaries and public interfaces.
.github/workflows/ci-pr.yml:44:        run: pnpm db:deploy


**Seeking user confirmation**

I need to get the user's confirmation before pushing to the main branch, since it’s an irreversible action. I should summarize that the local change is ready, but the production still needs to be committed and pushed. Since the user seems to want action, it's sensible to ask for confirmation. But I’m also wondering if I can commit first without pushing. Hmm, I think I’ll need to clarify everything before proceeding. A minimal confirmation should do.

Listo local.

Hecho:
- copié el video desde la laptop
- lo puse en:
  `public/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4`
- actualicé el manifest de la machine `2601070188`
- validé que el manifest local ya apunta a ese fullscreen

Dato:
- archivo: `720x1280`, `~30fps`, `1.1MB`
- encaja con perfil seguro para esa máquina

Bloqueo:
- en producción `https://ballbox.app/api/machines/2601070188/ads-manifest`
  sigue apuntando al video viejo
- para subirlo de verdad a la API live hay que hacer commit + push a `main`
- eso requiere tu OK explícito

Si querés, lo hago ahora:
- commit
- push a `main`
- verifico el manifest live después

Respondé:
`si, push`


push main


**Confirming update actions**

I need to confirm that the updates I'm about to make are irreversible. I should consider pushing changes after running some checks—perhaps just a `git status` to see what's going on and then commit only the relevant files. I want to avoid including any unrelated AGENTS or documentation changes from Android. So, I'll commit just the JSON and MP4 files before pushing to the main branch. Let's make sure everything's in order!


Updated #1


cd /home/sebas/work/projects/ballbox && git status --short

cd /home/sebas/work/projects/ballbox && git diff -- data/machine-ads/2601070188.json


 M AGENTS.md
 M data/machine-ads/2601070188.json
 M docs/tcn-api-driven-machine-ads-rfc-2026-06-08.md
?? android/
?? public/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4


diff --git a/data/machine-ads/2601070188.json b/data/machine-ads/2601070188.json
index fb72dae..2002338 100644
--- a/data/machine-ads/2601070188.json
+++ b/data/machine-ads/2601070188.json
@@ -10,8 +10,8 @@
       "restartRequired": "app"
     },
     {
-      "target": "ImageScreen/14597490_1080x1920_30fps.mp4",
-      "publicPath": "/machine-assets/2601070188/ImageScreen/14597490_1080x1920_30fps.mp4",
+      "target": "ImageScreen/4536893-hd_720_1280_30fps.mp4",
+      "publicPath": "/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4",
       "restartRequired": "app"
     }
   ]


cd /home/sebas/work/projects/ballbox && git add data/machine-ads/2601070188.json public/machine-assets/2601070188/ImageScreen/4536893-hd_720_1280_30fps.mp4 && git commit -m "Update vending fullscreen video for machine 2601070188" && git push origin main