SOLOhow it works

SOLO puts one number on one screen. A small round display sits on your desk and shows a single metric you care about — nothing else competing for attention. This page explains how to get a number onto one.

Written for everyone. The main text assumes no technical background. Anything that needs a terminal is folded into an for engineers block you can expand — skip them entirely and the instructions still work.

01The four pieces

Everything lives in the cloud except the display itself. There is no server to maintain, no machine that has to stay switched on at home.

PieceWhat it is
The hub The brain. Stores your metrics, remembers 48 hours of history, and answers the display when it asks "what should I show?" Runs on Cloudflare.
The console This website. Where you create metrics, set what's displayed, and link displays.
The display The physical desk module — a 240×240 round screen. Asks the hub for its number every few seconds and draws it.
The simulator A web page that pretends to be a display. Same drawing code, real data. Use it to design a metric before any hardware exists.
Why it matters: because the hub is the only thing that decides what a display shows, swapping a broken module for a new one is just re-linking it. Nothing is configured on the device itself.

02Quick start — a number on a screen

  1. Create a slot. In the console, fill in "Add or update a slot". A slot is one metric. Give it a short name like signups and a label like "signups today".
  2. Send it a number. Something has to push the value in — a scheduled job, a workflow tool, anything that can make a web request. See Getting data in.
  3. Link a display. Power on a module (or open the simulator). It shows a six-character code. Type that code into the console's Devices section and pick the slot. Done.

The display updates on its own from then on. If you later want it to show something else, change it in the console — you never touch the device.

03Slots — deciding what to show

A slot is one metric. It holds the current value plus everything about how it should look.

FieldWhat it does
NameShort id used in links and by whatever sends the data. Lowercase letters, numbers, - and _ only.
LabelThe words shown above the number on the display. Keep it short — the screen is small.
UnitsShown under the number, e.g. % or orders. Leave blank for none.
DecimalsHow many decimal places the console shows.
Warm above / Hot aboveColour thresholds. Below warm the number is green, at or above warm it turns amber, at or above hot it turns red. Leave both blank for a plain white number.
Sourcewebhook — something pushes values in. ga4 — the hub fetches from Google Analytics itself every 3 minutes.
Stale afterHow many seconds old the value can get before the display marks it stale. Set this comfortably longer than how often the value is sent.
Important — the colours assume bigger is worse. That's right for a support backlog or server load. For something where bigger is better, like conversion rate, red would mean you're winning. Leave the thresholds blank for those metrics.
For engineers — slot fields

Slots are rows in a D1 (SQLite) table. decimals is honoured by the console only; the firmware has its own fmtValue() which switches to k/M above 10,000 and 1,000,000. Thresholds reach the device as a two-element array th: [warm, hot] and are dropped entirely unless both are set.

curl -X POST https://solo-hub.joachim-609.workers.dev/admin/slots \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"signups","label":"signups today","units":"",
       "decimals":0,"source":"webhook","warm":null,"hot":null,
       "stale_seconds":7200}'

04Getting data in

Push (recommended for most things)

Anything that can make a web request on a schedule can feed a slot: a GitHub Action, a hosted automation tool like n8n Cloud or Zapier, or a script running on a server you already have. It sends one number; the hub stores it.

Set the slot's Source to webhook. You'll need the ingest key — a password that lets a service write values. It's separate from the admin key so you can hand it to an automation without giving away control of the console.

Pull — Google Analytics

GA4 is built in. Set Source to ga4 and give the metric name (e.g. activeUsers). The hub fetches it every 3 minutes by itself — nothing else to set up, once the Google credentials are in place.

Rule of thumb: if a service can already send a web request, push it. Only use a built-in pull source when one exists.
For engineers — the ingest endpoint

One number per request. The slot must already exist and be source='webhook'; unknown slots return 404, and a ga4 slot would be overwritten by the next cron tick. Every write also appends to the 48-hour history table that backs the 24-hour comparison.

curl -fsS -X POST https://solo-hub.joachim-609.workers.dev/ingest/signups \
  -H "Authorization: Bearer $INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": 42}'

Use -f so a bad key fails your job loudly. Without it curl exits 0 on a 401 and you get a green build with a panel quietly going stale — the worst failure mode. An optional "ts" (unix seconds) lets you backfill.

A ready-to-copy GitHub Actions workflow lives in the repo at examples/github-actions-ingest.yml.

05Connecting a display

A new display knows nothing about you. The first time it reaches the hub it's given a six-character code, which it shows on screen. Typing that code into the console is what links the two.

  1. Power on the module. After it joins wifi it shows claim code and six characters.
  2. In the console's Devices section, enter the code and the slot name.
  3. Press Claim device. Within a few seconds the display switches to your metric.

Codes avoid easily-confused characters — there's no letter O or digit 0, no letter I or digit 1. If a character looks ambiguous, it's the one that isn't a number.

To move a display to a different metric, press Forget next to it in the Devices list. It reappears with a fresh code on its next check-in, and you claim it again against the new slot.

06Reading the panel

The display has four states. Knowing them makes most problems self-diagnosing.

claim code 7F2K9Q enter in web console

Waiting to be linked. Type the code into the console.

assigned -- waiting for first data

Linked, but the slot is empty. Nothing has sent a value yet.

signups today 247 +12.4% / 24h

Working. The number, its label, and the 24-hour change.

hub offline retrying...

Can't reach the hub. Usually wifi. It retries by itself.

The colour of the number

The ring around the edge

The ring shows how the number compares with 24 hours ago. It sweeps clockwise and green when the number is up, anticlockwise and red when it's down. A half-circle sweep means roughly a 50% change. The same figure is written underneath as a percentage.

"stale"

The word stale appears when the value is older than expected, and the number turns grey. It means the display is fine but nothing fresh is arriving — check whatever is meant to be sending the data.

07Troubleshooting

SymptomMost likely cause
Console says unauthorized Wrong admin key, or a stray space when pasting it.
Panel stuck on waiting for first data The slot exists but nothing has sent a value. Test by pushing one by hand.
Panel shows stale most of the time "Stale after" is shorter than the gap between updates. Raise it above your send interval, with headroom — scheduled jobs often run late.
Sender reports 404 Slot doesn't exist, or the name is misspelled. Names are case-sensitive.
Sender reports 401 Using the admin key where the ingest key is expected, or vice versa.
Screen is blank after flashing Wiring. The pin settings in the firmware must match how the panel is soldered.
Number jumps to a huge percentage The 24-hour comparison divides by yesterday's value, so metrics that sit near zero produce wild percentages. Expected, not a bug.
For engineers — API reference

Base URL https://solo-hub.joachim-609.workers.dev. Two secrets: ADMIN_KEY for everything under /admin, INGEST_KEY for writes. Admin also satisfies ingest. All responses are JSON; failures carry {"ok":false,"error":"..."}.

RouteAuthPurpose
GET /healthnoneLiveness + slot count
POST /ingest/:slotingest{"value":N,"ts":?}
GET /m/:slotingestFull metric, verbose keys
GET /d/:macnoneDevice poll — compact keys
GET /admin/slotsadminList slots
POST /admin/slotsadminCreate or update (upsert)
DELETE /admin/slots/:nameadminDelete + its history
GET /admin/devicesadminList devices
POST /admin/claimadmin{"code","slot"}
DELETE /admin/devices/:idadminForget a device
GET /admin/history/:slotadmin?hours=24, max 48

/d/:mac accepts a MAC in any case with or without separators. Its payload uses single-letter keys and must stay under 384 bytes — that's the firmware's fixed JSON buffer. Over it, the device parses nothing and freezes on its last frame rather than showing an error. The simulator prints payload size on every poll.

08Known limits

Honest list of what this version doesn't do, so nobody loses an afternoon to it.

SOLO rev E · console · simulator