Add full PIN-protected TV dashboard stack including auth, redirects, map view, APIs, and server services. Also include these critical refinements: - Overhaul PO number generation to use per-company monthly sequences - Rework quote rejection to bounce quotes back to operator queues instead of marking them terminal - Add post_processing order tab group for clearer workflow visibility - Fix collection pool tooltip positioning, overflow clipping, and default collapsed state - Refactor customer list API, clean up unused form props, and fix item intel popover target ID handling - Add comprehensive tests for new components and update existing test expectations
5.3 KiB
5.3 KiB
TV Dashboard — install + use
PIN-gated office TV display showing live orders, quotes, customers, and revenue from PocketBase.
Quick start
- Open
https://<your-coolify-host>/tvfrom the TV browser (or any browser pointed at the display). - Type the 6-digit PIN on the on-screen keypad with the TV remote (D-pad + Enter) or a wireless keyboard.
- Dashboard lands on
/boardand stays unlocked for 12 hours. - 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, andcustomers; 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
- Open the URL on the TV's browser.
- Open Chrome's auto-start settings (or the equivalent on the TV's browser) so the dashboard comes back automatically after a reboot.
- Disable screen savers / sleep — the dashboard will stay on indefinitely.
Troubleshooting
- No data on the dashboard. Confirm
PB_ADMIN_EMAIL/PB_ADMIN_PASSWORDare set in.env.localon the server. The/api/dashboardroute uses admin PB credentials to read aggregated data. - "Invalid PIN" forever. Check
TV_DASHBOARD_PINin.env.localon 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
coordinatesgeoPoint (or fall back to the parent company'scoordinates). 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.repwhenquote.repis 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:
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.