An open API service indexing awesome lists of open source software.

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

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.

[![Build](https://github.com/chrisgleissner/callback-tracker/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/chrisgleissner/callback-tracker/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/chrisgleissner/callback-tracker/graph/badge.svg)](https://codecov.io/gh/chrisgleissner/callback-tracker)
![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-3776AB)
![No dependencies](https://img.shields.io/badge/dependencies-stdlib-success)
![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff)

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.

![Dashboard](docs/dashboard-lite.png)

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

![Dashboard](docs/dashboard.png)

## 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
```