components/theme-provider.tsx:7:} from 'next-themes'
app/classes-by-date/page.tsx:1:import Link from "next/link";
tests/unit/tvs/runtime.test.ts:35:    const nextManifest = {
tests/unit/tvs/runtime.test.ts:44:    expect(getActiveCreativeIndex(currentManifest, nextManifest, 1)).toBe(1);
tests/unit/tvs/runtime.test.ts:45:    expect(getActiveCreativeIndex(currentManifest, nextManifest, 0)).toBe(0);
tests/unit/tvs/invariants.test.ts:16:  it("filters out future and expired assignments", () => {
tests/unit/tvs/invariants.test.ts:29:        id: "future-assignment",
app/coaches/page.tsx:2:import Link from "next/link";
app/clubs/coaches/page.tsx:1:import { redirect } from "next/navigation";
app/clubs/page.tsx:1:import { redirect } from "next/navigation";
components/coach-access-page-client.tsx:4:import { useSearchParams } from "next/navigation";
components/navbar.tsx:5:import Image from "next/image"
components/club-services-home-client.tsx:3:import Link from "next/link";
components/players-admin-player-card.tsx:45:                const nextName = event.target.value;
components/players-admin-player-card.tsx:46:                setPlayers((current) => current.map((item) => (item.id === player.id ? { ...item, name: nextName } : item)));
components/players-admin-player-card.tsx:53:                const nextValue = event.target.value;
components/players-admin-player-card.tsx:55:                  current.map((item) => (item.id === player.id ? { ...item, whatsappPhone: nextValue || null } : item))
components/players-admin-player-card.tsx:64:                const nextValue = event.target.value;
components/players-admin-player-card.tsx:66:                  current.map((item) => (item.id === player.id ? { ...item, telegramHandle: nextValue || null } : item))
components/classes-offers-page-client.tsx:4:import Link from "next/link";
tests/e2e/player-runtime-side-effects.spec.ts:31:  expect(heartbeatText).not.toContain("pending");
tests/e2e/tvs-runtime-side-effects.spec.ts:31:  expect(heartbeatText).not.toContain("pending");
lib/payments.ts:289:  const next = await prisma.paymentSession.update({
lib/payments.ts:318:  return next;
lib/coach-availability-matching.ts:1:import { unstable_cache } from "next/cache";
components/coach-settings-page-client.tsx:3:import Link from "next/link";
components/coach-settings-page-client.tsx:5:import { useRouter, useSearchParams } from "next/navigation";
components/coach-settings-page-client.tsx:340:    const nextView = searchParams.get("view");
components/coach-settings-page-client.tsx:341:    if (nextView === "performance" || nextView === "configuration" || nextView === "students") return nextView;
components/coach-settings-page-client.tsx:388:    const nextView = searchParams.get("view");
components/coach-settings-page-client.tsx:389:    if (nextView === "performance" || nextView === "configuration" || nextView === "students") {
components/coach-settings-page-client.tsx:390:      setView(nextView);
components/coach-settings-page-client.tsx:397:  function handleViewChange(nextView: CoachPageView) {
components/coach-settings-page-client.tsx:398:    setView(nextView);
components/coach-settings-page-client.tsx:401:    if (nextView === "calendar") {
components/coach-settings-page-client.tsx:404:      params.set("view", nextView);
components/coach-settings-page-client.tsx:407:    const nextQuery = params.toString();
components/coach-settings-page-client.tsx:408:    router.replace(nextQuery ? `/coaches/settings?${nextQuery}` : "/coaches/settings", { scroll: false });
components/coach-settings-page-client.tsx:415:  function syncDrafts(nextSettings: CoachSettingsResponse) {
components/coach-settings-page-client.tsx:416:    setSettings(nextSettings);
components/coach-settings-page-client.tsx:418:      nextSettings.availability.items.map((item) => ({
components/coach-settings-page-client.tsx:425:      nextSettings.availability.items[0]?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "America/Montevideo"
components/coach-settings-page-client.tsx:427:    setClubSelectionDraft(nextSettings.externalClubs.selection);
components/coach-settings-page-client.tsx:428:    setPackagesDraft(nextSettings.commercial.packages.map((pkg) => createPackageDraft(pkg)));
components/coach-settings-page-client.tsx:429:    setMercadoPagoHandle(nextSettings.commercial.mercadoPagoHandle ?? "");
components/coach-settings-page-client.tsx:446:        const nextDrafts: Record<string, CoachStudentContactDraft> = {};
components/coach-settings-page-client.tsx:448:          nextDrafts[student.player.id] = current[student.player.id] ?? createStudentContactDraft(student.player);
components/coach-settings-page-client.tsx:450:        return nextDrafts;
components/coach-settings-page-client.tsx:495:      const nextDefaults = createStudentBookingDraft(settings, studentsPanel);
components/coach-settings-page-client.tsx:499:        playerId: current.playerId && playerStillExists ? current.playerId : nextDefaults.playerId,
components/coach-settings-page-client.tsx:500:        venueId: current.venueId && venueStillExists ? current.venueId : nextDefaults.venueId,
components/coach-settings-page-client.tsx:501:        date: current.date || nextDefaults.date,
components/coach-settings-page-client.tsx:502:        startTime: current.startTime || nextDefaults.startTime,
components/coach-settings-page-client.tsx:503:        durationMinutes: current.durationMinutes || nextDefaults.durationMinutes,
components/coach-settings-page-client.tsx:504:        repeatWeeklyCount: current.repeatWeeklyCount || nextDefaults.repeatWeeklyCount,
components/coach-settings-page-client.tsx:506:        timezone: current.timezone || nextDefaults.timezone,
components/coach-settings-page-client.tsx:540:    () => currentWeekBookings.filter((booking) => booking.paymentStatus === "pending").length,
components/coach-settings-page-client.tsx:1051:      const next = { ...current };
components/coach-settings-page-client.tsx:1052:      delete next[playerId];
components/coach-settings-page-client.tsx:1053:      return next;
components/coach-settings-page-client.tsx:1278:                              const pendingCount = session.participants.length - paidCount;
components/coach-settings-page-client.tsx:1324:                                      {pendingCount > 0 ? (
components/coach-settings-page-client.tsx:1325:
{pendingCount} reserva{pendingCount === 1 ? "" : "s"} sin pagar

components/coach-settings-page-client.tsx:1387:                            selectedCalendarSession.participants.filter((participant) => participant.paymentStatus === "pending").length
components/coach-settings-page-client.tsx:2413:                            {student.nextLessonAt ? formatDateTime(student.nextLessonAt) : "Sin clase futura"}
components/screens-runtime-grid.tsx:3:import Link from "next/link";
lib/coach-students-backend.ts:38:  paymentStatus: "pending" | "paid";
lib/coach-students-backend.ts:46:  nextLessonAt: string | null;
lib/coach-students-backend.ts:212:    const next = new Date(startsAt);
lib/coach-students-backend.ts:213:    next.setDate(startsAt.getDate() + index * 7);
lib/coach-students-backend.ts:214:    return next;
lib/coach-students-backend.ts:278:    paymentStatus: args.booking.paymentStatus === "PAID" ? "paid" : "pending",
lib/coach-students-backend.ts:450:      nextLessonAt: string | null;
lib/coach-students-backend.ts:463:        nextLessonAt: null,
lib/coach-students-backend.ts:474:      if (!entry.nextLessonAt || startsAt < new Date(entry.nextLessonAt).getTime()) {
lib/coach-students-backend.ts:475:        entry.nextLessonAt = booking.startsAt;
lib/coach-students-backend.ts:489:      nextLessonAt: entry.nextLessonAt,
lib/coach-students-backend.ts:494:      if (left.nextLessonAt && right.nextLessonAt) {
lib/coach-students-backend.ts:495:        return new Date(left.nextLessonAt).getTime() - new Date(right.nextLessonAt).getTime();
lib/coach-students-backend.ts:497:      if (left.nextLessonAt) return -1;
lib/coach-students-backend.ts:498:      if (right.nextLessonAt) return 1;
lib/payments-mercadopago.ts:39:    const next = value?.trim();
lib/payments-mercadopago.ts:40:    if (next) return next;
components/tvs-admin-client.tsx:3:import Link from "next/link";
components/tvs-admin-client.tsx:300:                      const nextVenue = adminData.venues.find((venue) => venue.id === event.target.value);
components/tvs-admin-client.tsx:301:                      if (!nextVenue) return;
components/tvs-admin-client.tsx:309:                                venueId: nextVenue.id,
components/tvs-admin-client.tsx:310:                                venueName: nextVenue.venueName,
components/tvs-admin-client.tsx:311:                                clubId: nextVenue.clubId,
components/tvs-admin-client.tsx:312:                                clubName: nextVenue.clubName,
components/tvs-admin-client.tsx:360:                      const nextTvDevice = adminData.tvDevices.find((tvDevice) => tvDevice.id === event.target.value);
components/tvs-admin-client.tsx:367:                                tvDeviceId: nextTvDevice?.id ?? null,
components/tvs-admin-client.tsx:368:                                tvDeviceName: nextTvDevice?.name ?? null,
tests/contracts/coaches/students-routes.test.ts:117:          nextLessonAt: "2026-04-20T13:00:00.000Z",
lib/http/json.ts:1:import { NextResponse } from "next/server";
lib/tvs-player-runtime-core.ts:12:  lastManifestRefreshStatus: "pending" | "ok" | "failed";
lib/tvs-player-runtime-core.ts:46:    lastManifestRefreshStatus: "pending",
lib/tvs-player-runtime-core.ts:83:  function getActiveCreativeIndex(currentManifest: ScreenManifest, nextManifest: ScreenManifest, currentIndex: number) {
lib/tvs-player-runtime-core.ts:84:    if (!nextManifest.creatives.length) {
lib/tvs-player-runtime-core.ts:93:    const nextIndex = nextManifest.creatives.findIndex((creative) => creative.id === currentCreative.id);
lib/tvs-player-runtime-core.ts:94:    return nextIndex >= 0 ? nextIndex : 0;
lib/tvs-player-runtime-core.ts:109:    const nextQueue = queue.concat(item);
lib/tvs-player-runtime-core.ts:110:    if (nextQueue.length <= MAX_IMPRESSION_RETRY_ITEMS) {
lib/tvs-player-runtime-core.ts:111:      return nextQueue;
lib/tvs-player-runtime-core.ts:114:    return nextQueue.slice(nextQueue.length - MAX_IMPRESSION_RETRY_ITEMS);
lib/tvs-player-runtime-core.ts:186:  function setActiveIndex(nextIndex: number) {
lib/tvs-player-runtime-core.ts:193:    const boundedIndex = ((nextIndex % creatives.length) + creatives.length) % creatives.length;
lib/tvs-player-runtime-core.ts:235:    const nextSources: Record<string, string> = {};
lib/tvs-player-runtime-core.ts:236:    const nextIds = new Set(state.manifest.creatives.map((creative) => creative.id));
lib/tvs-player-runtime-core.ts:260:        nextSources[creative.id] = existingObjectUrl;
lib/tvs-player-runtime-core.ts:267:      nextSources[creative.id] = objectUrl;
lib/tvs-player-runtime-core.ts:271:      if (nextIds.has(creativeId)) {
lib/tvs-player-runtime-core.ts:284:      cachedMediaSources: nextSources,
lib/tvs-player-runtime-core.ts:285:      cachedCreativeCount: Object.keys(nextSources).length,
lib/tvs-player-runtime-core.ts:287:      runtimeMode: Object.keys(nextSources).length > 0 ? "cached" : "streaming",
lib/tvs-player-runtime-core.ts:390:      const nextManifest = (await response.json()) as ScreenManifest;
lib/tvs-player-runtime-core.ts:393:        manifest: nextManifest,
lib/tvs-player-runtime-core.ts:394:        activeIndex: getActiveCreativeIndex(state.manifest, nextManifest, state.activeIndex),
tests/contracts/sports/routes.test.ts:2:import { NextRequest } from "next/server";
components/screen-player.tsx:31:    () => "pending"
components/screen-player.tsx:112:    ? "pending"
components/screen-player.tsx:171:
Last heartbeat: {snapshot.lastHeartbeatAt ? new Date(snapshot.lastHeartbeatAt).toLocaleString("es-AR") : "pending"}

components/screen-player.tsx:172:
Last impression: {snapshot.lastImpressionId ?? "pending"}

lib/atc-public.ts:1:import { unstable_cache } from "next/cache";
components/product-overview-card.tsx:1:import Link from "next/link";
lib/club-accounts-backend.ts:842:    const next = new Date(startsAt);
lib/club-accounts-backend.ts:843:    next.setDate(startsAt.getDate() + index * 7);
lib/club-accounts-backend.ts:844:    return next;
lib/club-accounts-backend.ts:1981:  const nextSlug = slugify(input.name);
lib/club-accounts-backend.ts:1985:      slug: nextSlug,
lib/club-accounts-backend.ts:2013:        slug: nextSlug,
lib/club-accounts-backend.ts:2060:    const nextCourtIds = new Set(input.courts.flatMap((court) => (court.id ? [court.id] : [])));
lib/club-accounts-backend.ts:2065:        id: { notIn: Array.from(nextCourtIds) },
components/admin-shell.tsx:2:import Link from "next/link";
components/admin-shell.tsx:3:import Image from "next/image";
components/sports-admin-shell.tsx:27:  next: SetStateAction<T[]>,
components/sports-admin-shell.tsx:31:    [key]: typeof next === "function" ? (next as (value: T[]) => T[])(current[key] as T[]) : next,
components/sports-admin-shell.tsx:48:  const setCoaches: Dispatch<SetStateAction<BallboxStoreSportsCoach[]>> = (next) =>
components/sports-admin-shell.tsx:49:    updateSportsCollection(setData, "coaches", next);
components/sports-admin-shell.tsx:51:  const setOpenMatches: Dispatch<SetStateAction<BallboxStoreSportsEvent[]>> = (next) =>
components/sports-admin-shell.tsx:52:    updateSportsCollection(setData, "openMatches", next);
components/sports-admin-shell.tsx:54:  const setClasses: Dispatch<SetStateAction<BallboxStoreSportsEvent[]>> = (next) =>
components/sports-admin-shell.tsx:55:    updateSportsCollection(setData, "classes", next);
lib/tvs-runtime.ts:47:  nextManifest: ScreenManifest,
lib/tvs-runtime.ts:50:  if (nextManifest.creatives.length === 0) {
lib/tvs-runtime.ts:59:  const nextIndex = nextManifest.creatives.findIndex((creative) => creative.id === currentCreative.id);
lib/tvs-runtime.ts:60:  return nextIndex >= 0 ? nextIndex : 0;
lib/tvs-runtime.ts:76:  const nextQueue = [...queue, item];
lib/tvs-runtime.ts:77:  if (nextQueue.length <= MAX_IMPRESSION_RETRY_ITEMS) {
lib/tvs-runtime.ts:78:    return nextQueue;
lib/tvs-runtime.ts:81:  return nextQueue.slice(nextQueue.length - MAX_IMPRESSION_RETRY_ITEMS);
components/classes-request-form.tsx:78:  const [pending, setPending] = useState(false);
components/classes-request-form.tsx:294:        loading={pending}
components/classes-request-form.tsx:295:        disabled={pending}
components/classes-request-form.tsx:298:        {pending ? "Enviando..." : "Solicitar clase"}
components/weekly-availability-grid.tsx:13:  onChange: (next: WeeklyAvailabilityRange[]) => void;
components/weekly-availability-grid.tsx:57:  const next: WeeklyAvailabilityRange[] = [];
components/weekly-availability-grid.tsx:73:      next.push({
components/weekly-availability-grid.tsx:82:  return next;
components/weekly-availability-grid.tsx:87:  const nextCells = new Set<string>();
components/weekly-availability-grid.tsx:91:      nextCells.add(key);
components/weekly-availability-grid.tsx:95:  return compressCellsToRanges(nextCells);
components/weekly-availability-grid.tsx:128:  function updateCell(dayOfWeek: number, slot: string, nextValue: boolean) {
components/weekly-availability-grid.tsx:129:    const nextCells = new Set(selectedCells);
components/weekly-availability-grid.tsx:132:    if (nextValue) {
components/weekly-availability-grid.tsx:133:      nextCells.add(key);
components/weekly-availability-grid.tsx:135:      nextCells.delete(key);
components/weekly-availability-grid.tsx:138:    onChange(compressCellsToRanges(nextCells));
components/weekly-availability-grid.tsx:188:                      const nextValue = !isSelected;
components/weekly-availability-grid.tsx:189:                      setDragMode(nextValue);
components/weekly-availability-grid.tsx:190:                      updateCell(dayOfWeek, slot, nextValue);
components/club-venue-page-client.tsx:3:import Link from "next/link";
components/club-venue-page-client.tsx:254:  const pendingSaveRef = useRef<{ draft: VenueDraft; signature: string } | null>(null);
components/club-venue-page-client.tsx:272:      const nextDraft = createDraft(parsed.venue);
components/club-venue-page-client.tsx:277:      setDraft(nextDraft);
components/club-venue-page-client.tsx:278:      lastSavedDraftSignature.current = createDraftSignature(nextDraft);
components/club-venue-page-client.tsx:299:  async function saveDraft(nextDraft: VenueDraft, signature: string, options?: { background?: boolean }) {
components/club-venue-page-client.tsx:312:          name: nextDraft.name,
components/club-venue-page-client.tsx:313:          status: nextDraft.status,
components/club-venue-page-client.tsx:314:          openingTime: nextDraft.openingTime,
components/club-venue-page-client.tsx:315:          closingTime: nextDraft.closingTime,
components/club-venue-page-client.tsx:316:          coachReservationPrice: nextDraft.coachReservationPrice,
components/club-venue-page-client.tsx:317:          clubCoachReservationDiscount: nextDraft.clubCoachReservationDiscount,
components/club-venue-page-client.tsx:318:          coachCancellationWindowHours: nextDraft.coachCancellationWindowHours,
components/club-venue-page-client.tsx:319:          externalCoachesAllowed: nextDraft.externalCoachesAllowed,
components/club-venue-page-client.tsx:320:          coachHours: nextDraft.coachHours,
components/club-venue-page-client.tsx:321:          coachPriceOverrides: nextDraft.coachPriceOverrides.map((override) => ({
components/club-venue-page-client.tsx:327:          courts: nextDraft.courts.map((court) => ({
components/club-venue-page-client.tsx:346:      pendingSaveRef.current = null;
components/club-venue-page-client.tsx:379:      if (pendingSaveRef.current) {
components/club-venue-page-client.tsx:380:        const pending = pendingSaveRef.current;
components/club-venue-page-client.tsx:381:        pendingSaveRef.current = null;
components/club-venue-page-client.tsx:382:        void saveDraftEffect(pending.draft, pending.signature, { background: true });
components/club-venue-page-client.tsx:393:      pendingSaveRef.current = null;
components/club-venue-page-client.tsx:398:    pendingSaveRef.current = { draft, signature };
components/club-venue-page-client.tsx:406:      pendingSaveRef.current = null;
components/club-venue-page-client.tsx:474:            const next = { ...current, ...patch };
components/club-venue-page-client.tsx:475:            const availableRanges = buildDailyAvailabilityRanges(next.openingTime, next.closingTime);
components/club-venue-page-client.tsx:478:              ...next,
components/club-venue-page-client.tsx:837:              onChange={(next) => updateDraft({ coachHours: clampWeeklyAvailabilityRanges(next, availableCoachRanges) })}
components/club-venue-page-client.tsx:863:                  const nextBase = Math.max(0, Number(event.target.value || 0));
components/club-venue-page-client.tsx:865:                    coachReservationPrice: nextBase,
components/club-venue-page-client.tsx:866:                    clubCoachReservationDiscount: Math.min(draft.clubCoachReservationDiscount, nextBase),
lib/contracts/coach-students.ts:53:    paymentStatus: z.enum(["pending", "paid"]).default("pending"),
lib/contracts/coach-students.ts:63:    nextLessonAt: z.string().min(1).nullable(),
tests/contracts/network/routes.test.ts:2:import { NextRequest } from "next/server";
components/admin-product-page.tsx:1:import Link from "next/link";
components/admin-product-page.tsx:11:  nextSteps: string[];
components/admin-product-page.tsx:22:  nextSteps,
components/admin-product-page.tsx:69:            {nextSteps.map((item) => (
tests/contracts/network/network-admin-venue-card.test.tsx:72:    const nextClubs = update?.(clubs);
tests/contracts/network/network-admin-venue-card.test.tsx:73:    expect(nextClubs?.[0].venues[0].name).toBe("Venue Renamed");
tests/contracts/network/network-admin-venue-card.test.tsx:74:    expect(nextClubs?.[0].venues[0].slug).toBe("venue-one");
tests/contracts/network/network-admin-venue-card.test.tsx:86:    const nextClubs = update?.(clubs);
tests/contracts/network/network-admin-venue-card.test.tsx:87:    expect(nextClubs?.[0].venues[0].vendingMachines[0].active).toBe(false);
components/payments-admin-client.tsx:3:import Image from "next/image";
components/payments-admin-client.tsx:246:        const next = updater(item);
components/payments-admin-client.tsx:247:        const parsedReference = parsePaymentExternalReference(next.externalReference);
components/payments-admin-client.tsx:250:          ...next,
components/payments-admin-client.tsx:254:            vendingMachineIdMatchesReference: parsedReference ? parsedReference.vendingMachineId === next.vendingMachineId : false,
components/payments-admin-client.tsx:255:            machineTxnIdMatchesReference: parsedReference ? parsedReference.machineTxnId === next.machineTxnId : false,
tests/contracts/network/network-admin-club-card.test.tsx:75:    const nextClubs = update?.(clubs);
tests/contracts/network/network-admin-club-card.test.tsx:76:    expect(nextClubs?.[0].atcSportclubId).toBe("999");
tests/contracts/network/network-admin-club-card.test.tsx:77:    expect(nextClubs?.[1].atcSportclubId).toBe("107");
components/classes-by-date-page-client.tsx:3:import Link from "next/link";
lib/coaches-backend.ts:398:  const nextCells = new Set<string>();
lib/coaches-backend.ts:402:      nextCells.add(key);
lib/coaches-backend.ts:406:  return normalizeRanges(compressCellsToRanges(nextCells));
lib/coaches-backend.ts:414:  const nextCells = new Set<string>();
lib/coaches-backend.ts:418:      nextCells.add(key);
lib/coaches-backend.ts:422:  return normalizeRanges(compressCellsToRanges(nextCells));
tests/contracts/players/players-admin-player-card.test.tsx:70:    const nextPlayers = update?.(players);
tests/contracts/players/players-admin-player-card.test.tsx:71:    expect(nextPlayers?.[0].name).toBe("Player Renamed");
tests/contracts/players/players-admin-player-card.test.tsx:72:    expect(nextPlayers?.[1].name).toBe("Player Two");
components/club-coaches-page-client.tsx:3:import Link from "next/link";
components/club-coaches-page-client.tsx:5:import { useRouter, useSearchParams } from "next/navigation";
components/club-coaches-page-client.tsx:292:    const nextView = searchParams.get("view");
components/club-coaches-page-client.tsx:293:    if (nextView === "analytics" || nextView === "venues" || nextView === "coaches") return nextView;
components/club-coaches-page-client.tsx:342:    const nextView = searchParams.get("view");
components/club-coaches-page-client.tsx:343:    if (nextView === "analytics" || nextView === "venues" || nextView === "coaches") {
components/club-coaches-page-client.tsx:344:      setView(nextView);
components/club-coaches-page-client.tsx:351:  function handleViewChange(nextView: ClubPageView) {
components/club-coaches-page-client.tsx:352:    setView(nextView);
components/club-coaches-page-client.tsx:355:    if (nextView === "calendar") {
components/club-coaches-page-client.tsx:358:      params.set("view", nextView);
components/club-coaches-page-client.tsx:361:    const nextQuery = params.toString();
components/club-coaches-page-client.tsx:362:    router.replace(nextQuery ? `/clubs/coaches?${nextQuery}` : "/clubs/coaches", { scroll: false });
components/club-coaches-page-client.tsx:1004:                      const nextVenueId = event.target.value;
components/club-coaches-page-client.tsx:1005:                      const nextVenue = venues.find((venue) => venue.id === nextVenueId) ?? null;
components/club-coaches-page-client.tsx:1006:                      const nextCourt = nextVenue?.courts.find((court) => court.coachesEnabled) ?? nextVenue?.courts[0] ?? null;
components/club-coaches-page-client.tsx:1007:                      const nextCoach =
components/club-coaches-page-client.tsx:1008:                        manualClassOptions.coaches.find((coach) => coach.venueId === nextVenueId && getActiveCoachPackages(coach).length > 0) ??
components/club-coaches-page-client.tsx:1009:                        manualClassOptions.coaches.find((coach) => coach.venueId === nextVenueId) ??
components/club-coaches-page-client.tsx:1011:                      const nextPackage = getActiveCoachPackages(nextCoach)[0] ?? null;
components/club-coaches-page-client.tsx:1016:                            venueId: nextVenueId,
components/club-coaches-page-client.tsx:1017:                            courtId: nextCourt?.id ?? "",
components/club-coaches-page-client.tsx:1018:                            coachId: nextCoach?.id ?? "",
components/club-coaches-page-client.tsx:1020:                              nextVenueId,
components/club-coaches-page-client.tsx:1021:                              nextCoach?.id ?? "",
components/club-coaches-page-client.tsx:1026:                          nextPackage
components/club-coaches-page-client.tsx:1046:                      const nextCoachId = event.target.value;
components/club-coaches-page-client.tsx:1047:                      const nextCoach = availableManualCoaches.find((coach) => coach.id === nextCoachId) ?? null;
components/club-coaches-page-client.tsx:1048:                      const nextPackage = getActiveCoachPackages(nextCoach)[0] ?? null;
components/club-coaches-page-client.tsx:1053:                            coachId: nextCoachId,
components/club-coaches-page-client.tsx:1054:                            courtPriceMinor: resolveManualCourtPrice(current.venueId, nextCoachId, current.date, current.time),
components/club-coaches-page-client.tsx:1056:                          nextPackage
components/club-coaches-page-client.tsx:1077:                      const nextPackage = availableManualPackages.find((salesPackage) => salesPackage.id === event.target.value) ?? null;
components/club-coaches-page-client.tsx:1078:                      setManualClassDraft((current) => applyCoachPackageToDraft(current, nextPackage));
components/club-coaches-page-client.tsx:1118:                      const nextPlayer = manualClassOptions.players.find((player) => player.id === event.target.value) ?? null;
components/club-coaches-page-client.tsx:1122:                        playerWhatsappPhone: nextPlayer?.whatsappPhone ?? "",
components/club-coaches-page-client.tsx:1637:                                        const nextCoachId = block?.coachId ?? (currentCoachFitsVenue ? current.coachId : fallbackCoach?.id ?? "");
components/club-coaches-page-client.tsx:1638:                                        const nextCoach = manualClassOptions.coaches.find((coach) => coach.id === nextCoachId) ?? null;
components/club-coaches-page-client.tsx:1639:                                        const nextPackage =
components/club-coaches-page-client.tsx:1640:                                          current.packageId && getActiveCoachPackages(nextCoach).some((salesPackage) => salesPackage.id === current.packageId)
components/club-coaches-page-client.tsx:1641:                                            ? getActiveCoachPackages(nextCoach).find((salesPackage) => salesPackage.id === current.packageId) ?? null
components/club-coaches-page-client.tsx:1642:                                            : getActiveCoachPackages(nextCoach)[0] ?? null;
components/club-coaches-page-client.tsx:1649:                                              coachId: nextCoachId,
components/club-coaches-page-client.tsx:1654:                                                nextCoachId,
components/club-coaches-page-client.tsx:1659:                                            nextPackage
components/sports-admin-client-config.ts:80:    const nextVenue = data.venues.find((venue) => venue.id === venueId);
components/sports-admin-client-config.ts:81:    if (!nextVenue) return event;
components/sports-admin-client-config.ts:85:      venueId: nextVenue.id,
components/sports-admin-client-config.ts:86:      venueName: nextVenue.name,
components/sports-admin-client-config.ts:87:      clubId: nextVenue.clubId,
components/sports-admin-client-config.ts:88:      clubName: nextVenue.clubName,
README.md:189:- `docs/next-steps-queue.md`
components/authenticated-workspace-navbar.tsx:3:import Link from "next/link";
app/manifest.ts:1:import type { MetadataRoute } from "next";
components/footer.tsx:2:import Image from "next/image"
app/layout.tsx:2:import type { Metadata } from 'next'
app/layout.tsx:3:import { Analytics } from '@vercel/analytics/next'
components/admin-shell-frame.tsx:4:import { usePathname } from "next/navigation";
app/offline/page.tsx:1:import Link from "next/link";
components/route-feedback.tsx:4:import { usePathname } from "next/navigation";
components/route-feedback.tsx:93:  const [pending, setPending] = useState(false);
components/route-feedback.tsx:98:  const pendingRef = useRef(false);
components/route-feedback.tsx:102:    pendingRef.current = pending;
components/route-feedback.tsx:103:  }, [pending]);
components/route-feedback.tsx:111:    if (!pending) return;
components/route-feedback.tsx:131:  }, [pending]);


Sí. Ya quedó.

Respuesta corta:
1. Conceptualmente sí, eran ~6 líneas de cambio.
2. Sí, estaban listos.
3. Ya hice push a `main`.

Push:
- `51f1aed` Improve html-first accessibility across core flows

Validación pasada antes del push:
- `pnpm typecheck`
- `pnpm lint`
- `pnpm build`
- `pnpm vitest run tests/contracts/classes/classes-request-form.test.tsx tests/contracts/weekly-availability-grid.test.tsx`
- además el pre-push corrió suite más amplia y pasó

Nota:
- en git quedó como un batch grande consolidado, no 6 commits separados.
- pero funcionalmente agrupa esas líneas de trabajo.


[telegram] te pido que me des tareas pendientes fuera de esto de accesibilidad


Sí. Fuera de accesibilidad, lo pendiente importante en Ballbox hoy es esto:

## 1. Negocio / validación
- todavía no hay clientes
- falta cerrar pilotos con primeras máquinas
- Ballbox sigue en etapa de validación para clubes de pádel

## 2. Payments / máquinas TCN
- provider-side Mercado Pago ya probado
- falta todo el tramo máquina real:
  - request `8000`
  - render de `CodeUrl`
  - aprobación → dispense
  - retries/duplicates
  - expiración/timeouts
  - runbook de incidentes
- además falta confirmar preguntas abiertas con TCN
- después de eso: congelar contrato Ballbox ↔ TCN
- luego: implementar flujo real con modos `BALLBOX` y `TENANT`

## 3. Rollout payments
- onboarding guiado para primeras 5 máquinas
- settlement de clubes sigue manual
- split automático / marketplace queda para más adelante

## 4. TVs
- según plan, el próximo pick fuerte seguía siendo cierre de paridad runtime real
- falta rerun de paridad local + evidencia en dispositivo real
- hay tema histórico de Playwright/webServer en esta Pi, aunque parte ya se estabilizó

## 5. Sports / product discovery
- la tesis de coaches sigue verde
- falta más evidencia real de workflow de coaches
- no está cerrada la propuesta de valor todavía
- siguiente paso de sports después de `/classes/me`:
  - review liviano de requests entrantes

## 6. ATC
- hoy solo hay lectura pública acotada
- write/admin de ATC no está integrado
- expandir ATC solo si el loop de requests demuestra necesidad

## 7. Product / ops
- seguir con `docs/next-steps-queue.md` como source of truth
- en prioridad el plan menciona:
  - `OPS-2`
  - luego `ARCH-2`, `ARCH-3`
  - luego `PERF-1`
  - luego `PAY-3`

## 8. Arquitectura / maintainability
- seguir sacando cosas de seams viejos y consolidando módulos backend/frontend
- mantener `lib/ballbox-store.ts` como compat layer, no entrypoint real
- continuar refactors marcados en `docs/maintainability-priorities-2026-03.md`

## 9. Calidad
- aunque la base está armada, falta seguir ampliando:
  - e2e útiles
  - unit/integration/contracts según riesgo
  - evidencia de runtime real para TVs
  - pruebas de machine payment flow cuando exista hardware

Si querés, te lo convierto en:
- backlog ejecutivo priorizado
o
- “qué haría next 1-2-3” bien corto.


[telegram] dame los que sean mas mantenimiento que cambio de producto


Sí. Los más de mantenimiento/tech, no producto, serían:

## Prioridad alta
- cierre `OPS-2` de TVs
  - rerun de paridad local
  - evidencia real en dispositivo
  - terminar de cerrar runtime parity
- seguir refactor de arquitectura
  - continuar `ARCH-2` y `ARCH-3`
  - mantener separación backend/frontend
  - reducir dependencia de `lib/ballbox-store.ts`
- performance
  - `PERF-1`
  - revisar SSR/caches/cargas pesadas donde todavía haya costo

## Prioridad media
- ampliar test coverage útil
  - más contracts
  - más unit/integration
  - e2e donde den señal real
- endurecer flows operativos
  - errores explícitos
  - degradación limpia si DB/integraciones fallan
  - runbooks/evidencia reproducible
- limpieza de docs técnicas
  - mantener `PLAN.md`
  - `docs/next-steps-queue.md`
  - docs implementadas / RFCs sin drift

## Prioridad media-baja
- seguir modularizando frontend grande
  - shells
  - hooks
  - config compartida
  - componentes presentacionales
- seguir modularizando backend
  - route helpers
  - backend modules por dominio
- revisar consistencia de naming y contratos
  - evitar aliases legacy
  - mantener shape estable en APIs

## Más puntual
- estabilizar tooling local en esta Pi
  - Playwright
  - builds repetibles
  - scripts de corrida batch
- revisar autosaves y flush-on-exit
  - club/coaches/venue flows
  - asegurar que no haya pérdida de cambios
- revisar observabilidad liviana
  - logs
  - evidencia de fallos
  - snapshots/diagnostics donde ya existe base

Si querés, te los ordeno como:
- quick wins
- mantenimiento estructural
- deuda técnica pesada.


[telegram] hace con ralph loop las arch y perf


**Considering user request**

I need to comply with the user's request, which involves running the loop architecture and performance assessments. I might have to use our runner for this, but there's a chance we need to run the blocking script ourselves and wait for the results. It sounds like I should create a tasks file around ARCH-2, ARCH-3, and PERF-1 based on the next steps in the documentation. I’ll need to inspect those docs for the specific IDs.

bash

cd /home/sebas/work/projects/ballbox && rg -n "ARCH-2|ARCH-3|PERF-1" docs/next-steps-queue.md docs/*.md PLAN.md


# Next Steps Queue

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

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

## Recently Completed (2026-04-14)

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

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

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

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

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

## P0 Ready

### OPS-1 Payments real-world validation closure
- Priority: P0
- Status: completed
- Depends on: none
- Scope shipped:
  - public webhook smoke against the native MercadoPago webhook path
  - real payment persisted as approved in `/admin/payments`
  - observed payload quirks identified and handled
- Progress (2026-04-14): payments migration to host app started in one feature branch; health/sessions now expose internal MercadoPago mode, Ballbox ships `/notification/mercadopago` intake with order fetch verification, and native order creation now exists at `/api/integrations/payments/orders`.
- Progress (2026-04-19): PAY-2 cleanup removed bridge-oriented operational status from host-app health/docs; OPS-1 is now focused on validating the native MercadoPago order-create + webhook-persist path only.
- Progress (2026-04-23): TCN protocol review shows the `additional hardware devices` note is about machine-side QR scanning, while the machine payment flow (`FunCode=8000`) already returns `CodeUrl` for machine-screen QR display. This means the admin QR page should be treated as a provider test harness, and production integration should move to a TCN adapter layer.
- Progress (2026-05-05): first production order-create attempt against `https://ballbox.app/api/integrations/payments/orders` failed before buyer payment with Mercado Pago `400 unsupported_properties` because `notification_url` was rejected (`additionalProperties '$.notification_url' not allowed`). Ballbox should stop sending inline `notification_url` for this account/API surface and rely on the existing account-level Mercado Pago webhook configuration before rerunning the real-payment check.
- Closure (2026-05-13): a real approved payment now persists locally and is visible in `/admin/payments`. Mercado Pago webhook noise from `topic_merchant_order_wh` remains intentionally ignored. Closure required two Ballbox compatibility fixes for live provider payloads: product id fallback from `items[0].external_code` and robust parsing of `total_amount` when Mercado Pago returns a string.
- Expected payoff: closes highest-risk integration uncertainty

### OPS-2 TVs runtime parity closure
- Priority: P0
- Status: ready
- Depends on: none
- Scope:
  - manually verify real-device CacheStorage/object-URL behavior on `/player/[screenId]`
  - update `docs/streetcast-parity.md` with concrete evidence
- Progress (2026-04-14): runtime now exports a JSON parity snapshot from `/player/[screenId]` to standardize evidence capture; remaining work is one real-device run and doc evidence entry.
- Progress (2026-04-19): machine-local Playwright setup is now standardized outside the repo at `/mnt/rpi/repos/browser-harness`; repo Playwright now uses a stable production-style e2e server path on this Pi, and stock TV seed data now keeps `screen_001` actively programmed after `pnpm db:seed`.
- Remaining closure needs:
  - run `pnpm db:seed`, then rerun local parity/e2e on `screen_001`
  - run the documented real-device parity capture (online snapshot, offline snapshot, reconnect recovery)
  - then update this queue item and `PLAN.md` to completed
- Expected payoff: de-risks constrained-device production behavior

## P1 After P0

### ARCH-2 Continue `ballbox-store` caller migration
- Priority: P1
- Status: completed
- Depends on: ARCH-1 (done)
- Scope shipped:
  - TVs admin/runtime pages and routes now import direct TVs backend modules instead of `lib/ballbox-store.ts`
  - classes pages/routes/helpers now import `lib/classes-backend.ts` directly for offer/request reads and writes
  - shared TV types now live in `lib/tvs-backend-types.ts`, keeping `lib/ballbox-store.ts` as a compatibility facade instead of the primary entrypoint
- Evidence:
  - `app/admin/tvs/page.tsx`
  - `app/api/admin/tvs/*`
  - `app/api/tvs/heartbeat/route.ts`
  - `app/api/classes/requests/route.ts`
  - `lib/classes.ts`
  - `lib/tvs-source.ts`
- Expected payoff: less coupling and better module discoverability

### ARCH-3 Test taxonomy split
- Priority: P1
- Status: completed
- Depends on: DOC-1 (done)
- Scope shipped:
  - isolated backend/helper tests moved out of `tests/contracts` into `tests/unit`
  - route/schema contract coverage stayed in `tests/contracts`
  - `vitest` layered scripts/includes remain aligned with `contracts`, `unit`, and `integration`
- Evidence:
  - `tests/unit/classes/backend.test.ts`
  - `tests/unit/network/backend-mappers.test.ts`
  - `tests/unit/network/use-network-admin-client-state.test.tsx`
  - `tests/unit/players/use-players-admin-client-state.test.tsx`
  - `tests/unit/sports/backend-events.test.ts`
  - `tests/unit/tvs/runtime.test.ts`
- Expected payoff: faster debugging and more intentional coverage growth

### PERF-1 TTFB focused pass
- Priority: P1
- Status: completed
- Depends on: OPS-1
- Scope shipped:
  - measured production-like local server timings for `/admin`, `/classes`, and `/classes-by-date`
  - removed ATC summary hydration from `/classes` SSR club-name shaping
  - parallelized `/classes-by-date` ATC club/day lookups and added a short shared cache for the assembled calendar
- Evidence:
  - `lib/classes-backend.ts`
  - `lib/coach-availability-matching.ts`
  - local timings moved from roughly `/classes` 3.09s and `/classes-by-date` 6.54s to `/classes` 1.96s and `/classes-by-date` 2.69s on the same Pi
- Expected payoff: better responsiveness without blind cache complexity

### PAY-2 Full payments migration into Ballbox host app
- Priority: P1
- Status: completed
- Depends on: OPS-1
- Scope shipped:
  - native MercadoPago webhook + QR order lifecycle is now owned in Ballbox routes/lib
  - bridge-oriented operational status copy removed from health/docs/admin summary
  - legacy approved-callback route reduced to transitional compatibility only; native path is now the documented operational flow
- Evidence:
  - `/notification/mercadopago`
  - `/api/integrations/payments/orders`
  - `app/api/integrations/payments/health/route.ts`
  - `app/api/integrations/payments/sessions/route.ts`
  - `docs/implemented-payments.md`
- Expected payoff: one-repo payments ownership, simpler operations, faster debugging

### PAY-3 TCN vending-machine adapter over native MercadoPago
- Priority: P1
- Status: ready
- Depends on: OPS-1, PAY-2
- Scope:
  - define the Ballbox machine-facing adapter for TCN payment init (`FunCode=8000`) over the existing MercadoPago order-create route
  - map TCN `MachineID`, `TradeNo`, `SlotNo`, `Price`, `NotityUrl`, and response `CodeUrl` to Ballbox-native payment fields
  - define the payment-success handshake back to TCN so approved payments can unlock dispense
  - support one shared machine payment flow with two payment-owner modes:
    - `BALLBOX` for club machines that Ballbox collects on
    - `TENANT` for customer-owned collection setups such as Adidas rental machines
  - keep `/admin/payments` positioned as an operator test/debug harness rather than the production vending UX
  - document the exact adapter contract and remaining TCN unknowns before implementation
- Notes:
  - TCN protocol evidence is documented in `docs/tcn-payments-integration-plan.md`
  - master rollout direction is documented in `docs/payments-machine-rollout-rfc.md`
  - technical implementation shape is documented in `docs/payments-machine-implementation-rfc.md`
  - do not assume extra scanner hardware for the buyer flow; the open question is adapter contract detail, not QR display feasibility
  - club settlement automation can be revisited after the machine-facing flow is stable; manual bookkeeping is the current default
- Expected payoff: closes the actual machine integration path without fragmenting Ballbox collection vs tenant-direct collection into separate technical stacks

## P2 Sequenced Refactors

### PAY-4 Mercado Pago native split for club revenue share
- Priority: P2
- Status: blocked
- Depends on: OPS-1, PAY-2, PAY-3
- Scope:
  - keep scheduled manual settlements as the current operating default for Ballbox-collected club machines
  - document Mercado Pago native split as the only future automation path under consideration for club payouts
  - if automation is ever chosen, define onboarding, credential storage, rotation, and support rules before implementation
  - capture the trigger conditions that justify leaving manual bookkeeping behind
- Notes:
  - Ballbox currently runs QR vending payments through the native Mercado Pago order/webhook flow, so split automation is a separate future decision
  - first-wave rollout uses mixed owner modes (`BALLBOX` + `TENANT`), and settlement work only applies to the `BALLBOX` side
  - keep this behind real-world payments validation first (`OPS-1`) and machine-facing TCN adapter definition (`PAY-3`)
- Expected payoff: a clear path from manual settlements to a future Mercado Pago split if the machine flow and business volume justify it

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

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

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


docs/next-steps-queue.md:96:### ARCH-2 Continue `ballbox-store` caller migration
docs/next-steps-queue.md:113:### ARCH-3 Test taxonomy split
docs/next-steps-queue.md:130:### PERF-1 TTFB focused pass
docs/next-steps-queue.md:201:- Depends on: ARCH-2, ARCH-3
docs/next-steps-queue.md:96:### ARCH-2 Continue `ballbox-store` caller migration
docs/next-steps-queue.md:113:### ARCH-3 Test taxonomy split
docs/next-steps-queue.md:130:### PERF-1 TTFB focused pass
docs/next-steps-queue.md:201:- Depends on: ARCH-2, ARCH-3
PLAN.md:214:- Then continue with `P1` in dependency order: `ARCH-2` and `ARCH-3` -> `PERF-1` -> `PAY-3`.


**Clarifying maintenance tasks**

I need to address the user's question about the Ralph loop architecture and performance. It's important to point out that there's a contradiction since those queue items are already completed, so no further action is needed there. I should then offer the next maintenance targets: PAY-3, OPS-2, or ARCH-4, which are currently blocked. I’ll keep this response concise to make it easy for the user to understand!

No conviene.

`ARCH-2`, `ARCH-3` y `PERF-1` ya figuran como `completed` en `docs/next-steps-queue.md`.

O sea:
- `ARCH-2` done
- `ARCH-3` done
- `PERF-1` done

Entonces no tiene sentido correr un Ralph loop sobre esos IDs tal cual.

Lo que sí puedo hacer es una de estas 3:

1. `OPS-2`
- cierre de paridad TVs runtime
- más mantenimiento/ops real

2. `PAY-3`
- adapter TCN vending-machine
- más implementación técnica que producto, pero ya toca integración nueva

3. `ARCH-4` prep work
- aunque está `blocked`, puedo correr loop para:
  - mapear impacto
  - preparar plan de migración
  - identificar desbloqueos
- no para “completarlo” formalmente

Mi recomendación:
- si querés mantenimiento puro: `OPS-2`
- si querés arquitectura pura: pre-work de `ARCH-4`

Decime cuál y te largo el Ralph loop bloqueante.


Summary:
## Goal
- Improve Ballbox accessibility following an html-first philosophy in the Next.js app.
- Implement changes directly in `/home/sebas/work/projects/ballbox`, validate them, and push to `main`.
- Build a reusable blocking orchestration script that launches sequential `pi` agents with file-based handoff/logging, then test it on remaining Ballbox accessibility tasks.
- Assess whether additional accessibility work remains.

## Constraints & Preferences
- User wants direct implementation, not just planning.
- Push changes directly to `main` when ready.
- For agent orchestration: use real bash, launch new `pi` agents, communicate state via files only, no manual intervention during the loop.
- New reusable runner should be blocking, log hardcoded steps, and return control only at the end.
- Prefer semantic HTML-first improvements over JS-heavy custom controls.
- Keep working in the existing repo paths; do not invent separate workflows.

## Progress
### Done
- [x] Added accessibility plan doc: `/home/sebas/work/projects/ballbox/docs/html-first-accessibility-plan.md`
- [x] Improved `components/classes-by-date-page-client.tsx`
  - Removed fullscreen overlay flow in favor of inline detail panel.
  - Added `fieldset`/`legend`, `aria-expanded`, `aria-controls`, `aria-pressed`, live region.
- [x] Improved `components/payments-admin-client.tsx`
  - Replaced custom selects with native `<select>` in QR flows.
  - Added real `form`/`fieldset`/`label` structure and accessible status messaging.
- [x] Improved `components/authenticated-workspace-navbar.tsx`
  - Replaced JS profile menu with native `details/summary`.
- [x] Improved `components/navbar.tsx`
  - Replaced mobile menu toggle with native `details/summary`.
- [x] Improved `components/club-coaches-page-client.tsx`
  - Replaced venue creation disclosure with native disclosure.
  - Replaced calendar mode/venue/coach/day selection with semantic radio groups + `fieldset/legend`.
  - Added semantic shells for manual class and requests panels.
  - Improved calendar grid cell `aria-label`s.
  - Converted manual class flow to real `form` submit and `role="alert"` error.
- [x] Improved `components/coach-settings-page-client.tsx`
  - Replaced student composer open/close JS with disclosure structure.
  - Replaced club selection mode buttons with semantic radios.
  - Added `aria-pressed` on selectable club cards.
- [x] Additional loop-driven changes landed in one final commit:
  - `components/classes-request-form.tsx`
  - `components/club-venue-page-client.tsx`
  - `components/coach-access-page-client.tsx`
  - `components/weekly-availability-grid.tsx`
  - `tests/contracts/classes/classes-request-form.test.tsx`
  - `tests/contracts/weekly-availability-grid.test.tsx` (new)
  - docs/plan updates
- [x] Built reusable runner:
  - `/home/sebas/pi-config/bin/ralph-loop-run`
  - `/home/sebas/pi-config/docs/ralph-loop-run.md`
  - updated `/home/sebas/pi-config/SERVICES-ARCHITECTURE.md`
- [x] Tested runner on remaining Ballbox tasks with 6-step blocking run:
  - run dir: `/home/sebas/runtime/ralph-loops/20260517T163526Z`
  - all tasks marked done
- [x] Final Ballbox commit pushed to `main`:
  - `51f1aed` `Improve html-first accessibility across core flows`

### In Progress
- [ ] No active implementation. User last asked whether any accessibility work remains.

### Blocked
- None currently.

## Key Decisions
- **HTML-first over custom widgets**: Prefer native `a`, `button`, `form`, `input`, `select`, `details`, radios, tables, headings, labels before ARIA-heavy custom controls.
- **Use inline/document-flow panels instead of overlays where possible**: Reduced complexity and improved keyboard/screen-reader behavior.
- **Use `details/summary` for disclosure-style UI**: Applied to nav/profile/menu-like and expandable admin panels to reduce JS-only state.
- **Use semantic grouped controls for selection**: Calendar modes, venue/coach/day filters, and club selection modes were converted to radios with `fieldset/legend`.
- **Use a blocking orchestrator for future loops**: `ralph-loop-run` was created instead of detached fire-and-forget, because user wants hardcoded step logs and control returned at the end.
- **Final remaining accessibility work is mostly audit/polish**: No large obvious hotspots remain; next work should be manual QA and systematic audits.

## Next Steps
1. Answer the user’s question directly: yes, accessibility work still remains, but mostly polish rather than major structural fixes.
2. If continuing implementation, prioritize:
   1. manual screen reader QA (NVDA/VoiceOver) on critical flows,
   2. keyboard QA (focus order, visible focus, escape/cancel),
   3. contrast audit on dark UI states,
   4. mobile/touch/zoom/reflow audit,
   5. dense table review (`caption`, headers, reading order),
   6. global async/live-message audit,
   7. convert repeated fixes into shared design-system primitives,
   8. broader automated accessibility tests.
3. If asked to keep coding, inspect current `main` state first and create a small prioritized backlog from those audit areas.

## Critical Context
- Repo: `/home/sebas/work/projects/ballbox`
- Reusable runner:
  - `/home/sebas/pi-config/bin/ralph-loop-run`
  - `/home/sebas/pi-config/docs/ralph-loop-run.md`
- Tested runner output:
  - `/home/sebas/runtime/ralph-loops/20260517T163526Z/STATE.md`
  - `/home/sebas/runtime/ralph-loops/20260517T163526Z/TASKS.md`
  - `/home/sebas/runtime/ralph-loops/20260517T163526Z/runner.log`
- Final pushed Ballbox commit: `51f1aed`
- Previously pushed commits in this effort include:
  - `1a15883` `Improve html-first accessibility flows`
  - `859f80b` `Use native details menu in workspace navbar`
  - `5099987` `Use native mobile nav disclosure`
  - `166d1e7` `Use native venue creation disclosure`
  - `7eb3ba9` `Use native student composer disclosure`
  - `f3a9d73` `Use semantic calendar filter controls`
  - `1ad344c` `Use semantic calendar day selection`
  - `121454a` `Add semantic manual class disclosure shell`
  - `31befb3` `Add semantic requests disclosure shell`
  - `e1b53c9` `Improve manual class form semantics`
  - `002784d` `Use semantic club selection controls`
- Final user-visible conclusion already established in conversation:
  - Remaining work: screen reader QA, keyboard QA, contrast, mobile/touch/zoom, dense tables, async messages, shared primitives, broader automated accessibility testing.

---

**Turn Context (split turn):**

## Original Request
[User asked in Spanish: “¿Y telegram, backlog global y pendientes de Ballbox general qué hay?” — they wanted a quick status on Telegram plus the global Ballbox backlog/pending items.]

## Early Progress
- [Assistant searched repo docs for backlog/pending information instead of answering from memory.]
- [Inspected `PLAN.md`, listed files under `docs/`, and ran a repo-wide grep for terms like `TODO`, `backlog`, `pending`, `next`, `follow-up`, and `future`.]
- [Found `PLAN.md` with Ballbox host-app status: marketing home at `/`, shared admin at `/admin`, product areas for TVs/payments/network/sports/players, TV runtime routes, and current validation-stage context (no customers yet, depends on pilots).]
- [Identified likely backlog-related docs such as `docs/next-steps-queue.md`, maintainability/priorities docs, and multiple product/domain plans/RFCs.]

## Context for Suffix
- [The retained suffix likely continues reading backlog docs to extract pending/global items.]
- [Important base context: Ballbox is still in validation/pilot stage; `/admin` and several product modules already exist; TVs appears relatively advanced with runtime/admin, DB flows, scheduling windows, Zod API contracts, diagnostics, and a noted OPS-2 local parity rerun with machine-local issues partly fixed.]
- [The user’s request is broad (“global backlog” / “general pendings”), so the answer should synthesize cross-product pending work from planning docs rather than only one feature area.]

<read-files>
/home/sebas/.agents/skills/tmux-pi-job-run/SKILL.md
/home/sebas/.agents/skills/tmux-pi-job-setup/SKILL.md
/home/sebas/pi-config/bin/pi-loop
/home/sebas/pi-config/docs/pi-loop.md
/home/sebas/runtime/ralph-loops/20260517T163526Z/STATE.md
/home/sebas/runtime/ralph-loops/20260517T163526Z/TASKS.md
/home/sebas/runtime/ralph-loops/ballbox-a11y-20260517-120638/STATE.md
/home/sebas/runtime/ralph-loops/ballbox-a11y-20260517-120638/TASKS.md
/home/sebas/work/projects/ballbox/PLAN.md
/home/sebas/work/projects/ballbox/app/admin/payments/page.tsx
/home/sebas/work/projects/ballbox/app/classes-by-date/page.tsx
/home/sebas/work/projects/ballbox/package.json
/home/sebas/work/projects/ballbox/tests/e2e/classes.spec.ts
/home/sebas/work/projects/ballbox/tests/e2e/payments-admin.spec.ts
</read-files>

<modified-files>
/home/sebas/pi-config/SERVICES-ARCHITECTURE.md
/home/sebas/pi-config/bin/ralph-loop-run
/home/sebas/pi-config/docs/ralph-loop-run.md
/home/sebas/runtime/ralph-loop-ballbox-remaining-tasks.md
/home/sebas/work/projects/ballbox/components/authenticated-workspace-navbar.tsx
/home/sebas/work/projects/ballbox/components/classes-by-date-page-client.tsx
/home/sebas/work/projects/ballbox/components/club-coaches-page-client.tsx
/home/sebas/work/projects/ballbox/components/coach-settings-page-client.tsx
/home/sebas/work/projects/ballbox/components/navbar.tsx
/home/sebas/work/projects/ballbox/components/payments-admin-client.tsx
/home/sebas/work/projects/ballbox/docs/html-first-accessibility-plan.md
</modified-files>


/new


queued new Telegram Pi session: pi-telegram


Hecho.