{"id":51335700,"url":"https://github.com/chrisgleissner/callback-tracker","last_synced_at":"2026-07-02T02:08:11.512Z","repository":{"id":368572327,"uuid":"1285151785","full_name":"chrisgleissner/callback-tracker","owner":"chrisgleissner","description":"Tracks sales stats of the Commodore Callback 8020","archived":false,"fork":false,"pushed_at":"2026-07-01T07:30:58.000Z","size":2072,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-01T08:22:12.714Z","etag":null,"topics":["callback","commodore","phone","plotly","python3","sailfish-os","sales","sqlite"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chrisgleissner.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-30T14:25:45.000Z","updated_at":"2026-07-01T07:31:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chrisgleissner/callback-tracker","commit_stats":null,"previous_names":["chrisgleissner/callback-tracker"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/chrisgleissner/callback-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgleissner%2Fcallback-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgleissner%2Fcallback-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgleissner%2Fcallback-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgleissner%2Fcallback-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrisgleissner","download_url":"https://codeload.github.com/chrisgleissner/callback-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgleissner%2Fcallback-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35029809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["callback","commodore","phone","plotly","python3","sailfish-os","sales","sqlite"],"created_at":"2026-07-02T02:08:10.779Z","updated_at":"2026-07-02T02:08:11.488Z","avatar_url":"https://github.com/chrisgleissner.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Callback 8020 Tracker\n\nLive sales dashboard for the [Callback 8020](https://commodore.net/store/callback-8020/) flip-phone,\nscraped from the Commodore store once every 10 minutes.\n\n[![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)\n[![codecov](https://codecov.io/gh/chrisgleissner/callback-tracker/graph/badge.svg)](https://codecov.io/gh/chrisgleissner/callback-tracker)\n![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-3776AB)\n![No dependencies](https://img.shields.io/badge/dependencies-stdlib-success)\n![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff)\n\nEvery sample is stored in SQLite, so the tracker survives restarts and resumes\nwithout losing history. The dashboard is a zoomable Plotly chart with one-click\nPNG export.\n\n## Lite Mode\n\nIn 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.\n\n![Dashboard](docs/dashboard-lite.png)\n\n## Dark Mode\n\nHere'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.\n\n![Dashboard](docs/dashboard.png)\n\n## Quick start\n\n```bash\n./build            # run tests (default task)\n./build run        # start the dashboard at http://localhost:8020\n```\n\nRequires Python 3.8+ (3.9+ recommended). The dashboard loads Plotly from a CDN,\nso the viewing browser needs internet access. No `pip install` is required.\n\n## Tasks\n\n```bash\n./build                    # run tests\n./build run [port] [args]  # start dashboard locally, forwarding extra tracker.py args\n./build once               # scrape once, print JSON (cron mode)\n./build png [--out F.png]  # export a chart PNG (or HTML snapshot if plotly is absent)\n./build db stats           # row count, range, latest values\n./build db export --fmt csv --out sales.csv\n./build db trim --keep 1000\n./build db reset\n./build deploy-pi          # deploy to the Raspberry Pi and restart it\n./build pi-status          # tracker status on the Pi\n```\n\n## Deploy to a Raspberry Pi\n\nThe tracker runs headless on a Pi and auto-starts at boot via an `@reboot`\ncrontab (rootless — no `sudo` needed). Passwordless SSH to the Pi is assumed.\n\n```bash\n./build deploy-pi          # copies code, restarts, refreshes the cron entry\n```\n\nDefaults: host `pi`, remote dir `~/callback-tracker`, port `8020`. Override with\n`PI_HOST`, `PI_DIR`, `PI_PORT`. Discoverable on the Pi via `~/CALLBACK-TRACKER.txt`\nand `~/callback-tracker/status.sh`. History (`data.db`) is preserved across deploys.\n\n## How it works\n\nThe store page renders a live \"units left in Batch N\" widget in its server-side\nHTML, so a plain HTTP fetch is enough — no browser, no JS, no API key:\n\n```html\n\u003cstrong class=\"commobot-battery__count\"\u003e272\u003c/strong\u003e\n\u003cspan class=\"commobot-battery__units-label\"\u003e / 500 pre-order units left in \u003cb\u003eBatch 5\u003c/b\u003e\u003c/span\u003e\n```\n\nFrom that: `units_left=272`, `batch_size=500`, `batch=5`, `sold_in_batch=228`, and\n**cumulative sold = (batch − 1) × batch_size + sold_in_batch = 2228** — so already\nsold-out batches (1–4) are counted automatically.\n\n## Architecture\n\nLayered, single process, zero dependencies:\n\n- **`scraper.py`** — `fetch()` the store page, `parse()` the widget HTML, derive `cumulative_sold`.\n- **`store.py`** — thread-safe SQLite; one row per sample, upserted by minute, so re-scraping is idempotent.\n- **`server.py`** — serves `dashboard.html` and a small JSON API.\n- **`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.\n- **`admin.py`** — a separate CLI for PNG export and DB maintenance (`stats`, `export`, `trim`, `reset`).\n- **`config.py`** — env-driven defaults (`LAUNCH_TIME`, `BASE_SOLD`, `BATCH_SIZE`), imported by every module above and importing none of them back.\n\nAPI: `GET /api/history` (full history), `/api/latest`, `/api/meta`, `/api/scrape` (scrape now).\n\nThe dashboard is static HTML — it polls `/api/history` and renders the chart\nentirely client-side with Plotly from a CDN; the server does no templating.\nThis shape follows from the source: the store page server-renders its data,\nso a plain fetch stands in for a headless browser, and one scrape every 10\nminutes is light enough that a single SQLite writer is all the database\nthis needs.\n\n### Configuration (env)\n\n| env           | default                         | meaning                                              |\n|---------------|---------------------------------|------------------------------------------------------|\n| `LAUNCH_TIME` | 2026-06-30 08:00 UTC (hard-coded) | launch anchor on the chart; all times shown in UTC |\n| `BASE_SOLD`   | auto: `(batch−1) × batch_size`  | absolute units sold before the current batch         |\n| `BATCH_SIZE`  | the size seen on the page (500) | override the batch size used by the auto calculation |\n\n## Project layout\n\n```\ncallback_tracker/   scraper, store, server, admin, app, config\ntests/              stdlib unittest (parser + store), no dependencies\ntracker.py          entry point (scripts / cron / `./build run`)\ndashboard.html      Plotly dashboard served at /\nbuild               task runner\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisgleissner%2Fcallback-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisgleissner%2Fcallback-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisgleissner%2Fcallback-tracker/lists"}