# TV Dashboard — install + use PIN-gated office TV display showing live orders, quotes, customers, and revenue from PocketBase. ## Quick start 1. Open `https:///tv` from the TV browser (or any browser pointed at the display). 2. Type the **6-digit PIN** on the on-screen keypad with the TV remote (D-pad + Enter) or a wireless keyboard. 3. Dashboard lands on `/board` and stays unlocked for **12 hours**. 4. Tap **Lock** (top right) to bounce back to the PIN screen. The PIN lives in `.env.local` as `TV_DASHBOARD_PIN`. The session cookie is signed with `TV_SESSION_SECRET` so rotating that env var invalidates every existing TV session (forces re-PIN). ## What it shows | Section | Tiles | |---|---| | **Period matrix** | Today / Week / Month counts: orders received, orders placed, quotes received, items processed | | **Driver stops** | Map + numbered list of the driver named by `TV_DRIVER_ID` — both active deliveries (`order_items.item_status = "assigned"`) and supplier pickups (`stock_purchase_items.status = "assigned_to_driver"`). Stops drop off as he marks them collected in the driver app. | | **Rep activity** | Per-rep table with today / week / month × quotes / orders counts. Falls back to `customer.rep` when `quote.rep` is empty. | The same `getDashboardData()` powers the main CRM `/dashboard`; the TV view adds `getTvDashboardData()` on top with the day / week / month counters, per-rep quotes, user performance, and driver stops. ## How it stays fresh - **Initial fetch** on mount. - **Polling** every 30s as a safety net. - **PB realtime** — subscribes to `orders`, `quotes`, `order_items`, and `customers`; any change triggers an immediate refetch. ## File map ``` src/app/(tv)/ layout.tsx dark shell, no sidebar pin/page.tsx on-screen PIN keypad (auto-submit on 6 digits) board/ page.tsx server guard → if no valid session, redirect to /pin tv-view.tsx client component, all the rendering components/ driver-stops-map.tsx numbered colour-coded pins per stop tv-map-inner.tsx the actual Leaflet map (browser-only) src/app/api/ tv/login/route.ts POST { pin } → sets tv_session cookie or 401 tv/logout/route.ts POST → clears cookie dashboard/route.ts GET → public TV dashboard JSON (no auth) src/lib/server/ tv-session.ts HMAC-signed cookie helpers tv-dashboard-service.ts day/week/month counters, quotesPerRep, userPerformance, driverStops driver-stops.ts shared listDriverStops + update (driver app) src/middleware.ts no-op (matcher in place for future) ``` ## Setting up a new TV 1. Open the URL on the TV's browser. 2. Open Chrome's auto-start settings (or the equivalent on the TV's browser) so the dashboard comes back automatically after a reboot. 3. Disable screen savers / sleep — the dashboard will stay on indefinitely. ## Troubleshooting - **No data on the dashboard.** Confirm `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD` are set in `.env.local` on the server. The `/api/dashboard` route uses admin PB credentials to read aggregated data. - **"Invalid PIN" forever.** Check `TV_DASHBOARD_PIN` in `.env.local` on the server. The PIN must match exactly (case-sensitive, no spaces). - **TV keeps asking for PIN every reload.** Sessions default to 168 hours (`TV_SESSION_HOURS`). Either bump it up or accept re-entry. - **Stale data even though realtime is wired.** PB realtime requires the page to be connected to the PB server. If the TV is behind a corporate proxy that strips WebSockets, realtime is dead and the 30s polling picks up the slack. - **`/dashboard` (existing CRM) errored after deploying.** There was a route collision — the TV view lives at `/board`, not `/dashboard`, so the two don't clash. - **Driver map shows no pins.** The map needs GPS coordinates on each customer's `coordinates` geoPoint (or fall back to the parent company's `coordinates`). Null Island `{0,0}` (PB's default for un-geocoded rows) is dropped on purpose. Geocode the customers via the main CRM and pins will appear. - **Quotes per rep shows "Unassigned".** Quotes auto-fall back to `customer.rep` when `quote.rep` is empty. If both are empty the row groups under "Unassigned". Set the rep on the customer / quote in the CRM to populate the table. | Var | Purpose | Required | |---|---|---| | `TV_DASHBOARD_PIN` | 4-8 digit PIN staff enter on the TV | yes | | `TV_SESSION_SECRET` | HMAC secret for signing the cookie | yes | | `TV_SESSION_HOURS` | TTL (default 12, currently set to **168 = 1 week**) | no | | `TV_DRIVER_ID` | PB `users.id` of the driver whose active stops appear on the stops tile. When unset the tile collapses to a "set TV_DRIVER_ID" placeholder so the rest of the dashboard still renders. | no | | `TV_REP_ROLES` | Comma-separated `user_type` values that mark a user as a rep for the activity list (default `REP`). A user is counted as a rep when their `user_type` array contains any of these values. Example: `TV_REP_ROLES=REP,QUOTER` to also include the QUOTER role. | no | | `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD` | PB admin auth for `/api/dashboard` | yes (reuses existing vars) | Generate a session secret with: ```bash node -e "console.log(require('crypto').randomBytes(48).toString('base64'))" ``` ## Out of scope (intentionally) - No multi-screen layouts. Single screen tuned for 1080p. - No clickable order rows. Read-only display; the office can click through to the main CRM from a desktop. - No custom dashboards per user. The TV shows the same things every shift.