Live sales dashboard for the Callback 8020 flip-phone, scraped from the Commodore store once every 10 minutes.
Every sample is stored in SQLite, so the tracker survives restarts and resumes without losing history. The dashboard is a zoomable Plotly chart with one-click PNG export.
In this screenshot, we enabled monotone (i.e. smooth) lines. Beneath the sales chart is a heat chart that shows strong sales performance in red and slower performance in blue.
Here's a screenshot where we enabled stepped lines as well as the burn-down of sales batches with its legend shown on the right-hand Y axis. Each batch consists of 500 phones.
./build # run tests (default task)
./build run # start the dashboard at http://localhost:8020Requires Python 3.8+ (3.9+ recommended). The dashboard loads Plotly from a CDN,
so the viewing browser needs internet access. No pip install is required.
./build # run tests
./build run [port] [args] # start dashboard locally, forwarding extra tracker.py args
./build once # scrape once, print JSON (cron mode)
./build png [--out F.png] # export a chart PNG (or HTML snapshot if plotly is absent)
./build db stats # row count, range, latest values
./build db export --fmt csv --out sales.csv
./build db trim --keep 1000
./build db reset
./build deploy-pi # deploy to the Raspberry Pi and restart it
./build pi-status # tracker status on the PiThe tracker runs headless on a Pi and auto-starts at boot via an @reboot
crontab (rootless — no sudo needed). Passwordless SSH to the Pi is assumed.
./build deploy-pi # copies code, restarts, refreshes the cron entryDefaults: host pi, remote dir ~/callback-tracker, port 8020. Override with
PI_HOST, PI_DIR, PI_PORT. Discoverable on the Pi via ~/CALLBACK-TRACKER.txt
and ~/callback-tracker/status.sh. History (data.db) is preserved across deploys.
The store page renders a live "units left in Batch N" widget in its server-side HTML, so a plain HTTP fetch is enough — no browser, no JS, no API key:
<strong class="commobot-battery__count">272</strong>
<span class="commobot-battery__units-label"> / 500 pre-order units left in <b>Batch 5</b></span>From that: units_left=272, batch_size=500, batch=5, sold_in_batch=228, and
cumulative sold = (batch − 1) × batch_size + sold_in_batch = 2228 — so already
sold-out batches (1–4) are counted automatically.
Layered, single process, zero dependencies:
scraper.py—fetch()the store page,parse()the widget HTML, derivecumulative_sold.store.py— thread-safe SQLite; one row per sample, upserted by minute, so re-scraping is idempotent.server.py— servesdashboard.htmland a small JSON API.app.py— runs the scraper on a daemon thread alongside the HTTP server; both share oneStorebehind a single lock, so reads and writes never race.admin.py— a separate CLI for PNG export and DB maintenance (stats,export,trim,reset).config.py— env-driven defaults (LAUNCH_TIME,BASE_SOLD,BATCH_SIZE), imported by every module above and importing none of them back.
API: GET /api/history (full history), /api/latest, /api/meta, /api/scrape (scrape now).
The dashboard is static HTML — it polls /api/history and renders the chart
entirely client-side with Plotly from a CDN; the server does no templating.
This shape follows from the source: the store page server-renders its data,
so a plain fetch stands in for a headless browser, and one scrape every 10
minutes is light enough that a single SQLite writer is all the database
this needs.
| env | default | meaning |
|---|---|---|
LAUNCH_TIME |
2026-06-30 08:00 UTC (hard-coded) | launch anchor on the chart; all times shown in UTC |
BASE_SOLD |
auto: (batch−1) × batch_size |
absolute units sold before the current batch |
BATCH_SIZE |
the size seen on the page (500) | override the batch size used by the auto calculation |
callback_tracker/ scraper, store, server, admin, app, config
tests/ stdlib unittest (parser + store), no dependencies
tracker.py entry point (scripts / cron / `./build run`)
dashboard.html Plotly dashboard served at /
build task runner