https://github.com/chrisgleissner/callback-tracker
Tracks sales stats of the Commodore Callback 8020
https://github.com/chrisgleissner/callback-tracker
callback commodore phone plotly python3 sailfish-os sales sqlite
Last synced: about 4 hours ago
JSON representation
Tracks sales stats of the Commodore Callback 8020
- Host: GitHub
- URL: https://github.com/chrisgleissner/callback-tracker
- Owner: chrisgleissner
- Created: 2026-06-30T14:25:45.000Z (1 day ago)
- Default Branch: main
- Last Pushed: 2026-07-01T07:30:58.000Z (about 22 hours ago)
- Last Synced: 2026-07-01T08:22:12.714Z (about 21 hours ago)
- Topics: callback, commodore, phone, plotly, python3, sailfish-os, sales, sqlite
- Language: Python
- Homepage:
- Size: 1.98 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# Callback 8020 Tracker
Live sales dashboard for the [Callback 8020](https://commodore.net/store/callback-8020/) flip-phone,
scraped from the Commodore store once every 10 minutes.
[](https://github.com/chrisgleissner/callback-tracker/actions/workflows/ci.yml)
[](https://codecov.io/gh/chrisgleissner/callback-tracker)



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.
## Lite Mode
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.

## Dark Mode
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.

## Quick start
```bash
./build # run tests (default task)
./build run # start the dashboard at http://localhost:8020
```
Requires 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.
## Tasks
```bash
./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 Pi
```
## Deploy to a Raspberry Pi
The 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.
```bash
./build deploy-pi # copies code, restarts, refreshes the cron entry
```
Defaults: 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.
## How it works
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:
```html
272
/ 500 pre-order units left in Batch 5
```
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.
## Architecture
Layered, single process, zero dependencies:
- **`scraper.py`** — `fetch()` the store page, `parse()` the widget HTML, derive `cumulative_sold`.
- **`store.py`** — thread-safe SQLite; one row per sample, upserted by minute, so re-scraping is idempotent.
- **`server.py`** — serves `dashboard.html` and a small JSON API.
- **`app.py`** — runs the scraper on a daemon thread alongside the HTTP server; both share one `Store` behind 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.
### Configuration (env)
| 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 |
## Project layout
```
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
```