# 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 | |---|---| | **Existing KPIs** | Orders this month · Active quotes · Customers | | **Today / Week / Month strips** | Orders received · Orders placed (POs) · Quotes received · Items processed | | **Driver map + stops list** | Today's `order_items` in `collection_pool` / `assigned` / `ready_delivery`, with the customer / company geoPoint used as the map pin | | **Quotes loaded per rep** | Per-rep table with `today` · `week` · `month` columns. Falls back to `customer.rep` when `quote.rep` is empty. | | **Today's rep activity** | Per-rep strip with Q (quotes today), O (orders today), ✓ (order_items completed today) | | **Rotating chart** | Revenue / Orders / Quotes — 8s rotation | | **Top customers** | Quotes per customer in the 12-week sample | | **Follow-ups** | First 8 quotes with a non-empty `follow_up_due` | 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-map.tsx ssr:false wrapper around Leaflet driver-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 the new day/week/month counters, quotesPerRep, userPerformance, driverStops 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 | | `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.