← All projects

Tower Optical Dashboard

Revenue and machine-health analytics for a national fleet of coin-op tower viewers

OwnedInternal Tool2026

Next.js · Supabase · PostgreSQL · Google Pub/Sub · Chart.js · Vercel

What it is

The internal analytics portal for Tower Optical, a coin-operated binocular-viewer company I own that has been in business since 1933. The viewers take tap-to-pay now, a dollar a view, and this is where the people who run them find out whether they are earning and whether anything is broken. Staff watch the whole fleet; venue partners see only their own sites, in aggregate. Almost none of them are engineers, which set the tone for everything in it.

Analytics, the view that answers both halves of the question at once — a KPI row across the top, transactions broken out by location below, and a gross-versus-net-to-Tower toggle above both.

What it does

Analytics leads with the fleet: a KPI row, transactions by location, and per-site badges for anything needing attention, filtered by site and date range and switchable between gross authorizations and net to Tower. Machine health is the triage view: what is offline, what is low on battery, what needs replacing. Each machine has its own page with cell-signal and battery telemetry, its device identity, and a map. Ask AI takes plain-English questions against the same data and stages anything consequential behind a Confirm. Underneath sits one rule: trust the number, show the caveat. Incomplete days are flagged and left out of averages, test transactions are filtered and marked, and an idle machine reads "no activity" rather than a silent zero.

Machine health is the triage view: counts across the top, then a card per site naming exactly which unit is offline, low on battery, or waiting on a replacement.
One machine, in depth: current device status above 30 days of cell-signal and battery telemetry. The identity panel below this, which carries the unit's payment-terminal identifiers, is deliberately excluded from the capture.
Ask AI, shown empty — this is the interface before anything is asked, not a transcript. No API key is configured in this environment, so there is no exchange to show; the four chips are the app's own built-in suggestions.

Revenue Sharing is the money side, and the one screen where being wrong costs real dollars. The viewers sit on someone else's property, so most sites take a cut of what their machines earn, and this page is where that cut gets calculated, sent and paid. Order of operations is the whole problem: the card fee comes off gross, then any park fee, then a blended state-and-local sales tax, and only what survives gets split. Terms are stored per site rather than assumed — each site's percentage, whether a park fee applies and whether it comes out before or after the split, its tax rates, where the money is wired, and for some sites a tiered rate that steps up past an annual threshold. Sites still taking coins get a manual cash box that folds into the split fee-free.

The intended run is monthly: pick the period, check the calculator against the Worldpay statement, send each venue a statement, then pay. Statements go out as a PDF behind a signed link that needs no login, because the people who read them are venue finance staff who will never have an account here. Payment is either an ACH transfer or a cheque recorded by number and bank, and the status of each one is tracked rather than assumed. Two guards sit under all of it: the payout layer is admin-only and re-checks on every action rather than trusting the interface, and venue partners never see this arithmetic at all — their view comes from a separate query that returns view counts and nothing else, so the fees, the splits and what Tower keeps are not merely hidden from them but absent from what they are served.

How it's built

Next.js on Vercel over Supabase Postgres, with partner isolation enforced in the database, not in the interface. Chart.js draws the charts, including a custom plugin that moves colliding bar labels out of each other's way. The brand typeface is self-hosted, and light and dark are two hand-tuned token sets rather than an inversion.

Working with Claude

Deciding what counts as a complete day took longer than building any chart. So did settling which authorizations are tests, and which machines have been reconciled against the processor's export. The second use was auditing what had been built: a design pass over the finished app found problems invisible to a sighted mouse user, including table rows that opened on click but not on Enter and a primary button at 3.3:1 in the theme the app defaults to. Handing the thing back for critique, then acting on the findings, is a different kind of useful than writing the code was.