Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 40 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,26 @@ Per-integration extras (e.g., macOS Calendar access for `calendar_countdown`) ar

## How it works

The display is a shared 72×16 canvas. Each integration publishes text, shapes, or status via the `busybar.client.BusyBarClient` API (see [`src/busybar/client.py`](src/busybar/client.py)). The display arbitrates by **priority**:
The display is a shared 72×16 canvas. Each integration publishes text, shapes, or status via the `busybar.client.BusyBarClient` API (see [`src/busybar/client.py`](src/busybar/client.py)). The display arbitrates by **priority**, through the shared ladder in [`src/busybar/display.py`](src/busybar/display.py):

- **Built-in apps** sit at priority **10** — the idle baseline.
- **Integrations** claim higher priorities to preempt the baseline: `calendar_countdown` at **20**, CI alerts at **60**.
- **Active BUSY session** at **90** — an authenticated BUSY firmware state that outranks all integrations (e.g., CI failures blink the LED during an active session).
- **Equal-or-higher priority wins** the display. Each element has an optional `timeout`; if the source doesn't refresh within that window, the element self-clears.
| Priority | Tier | Occupied by |
|---|---|---|
| 20 | `PRIORITY_AMBIENT` | `calendar_countdown`'s baseline countdown (normal and in-progress) |
| 21 | `PRIORITY_OVERLAY` | `ci_status`'s short-dwell running badge and GitHub GraphQL/REST quota gauges |
| 25 | `PRIORITY_AMBIENT_RAISED` | `calendar_countdown` inside `approach_minutes`, outside `notice_minutes` — no longer interruptible by the overlay tier |
| 60 | `PRIORITY_ALERT` | `ci_status`'s failure/stuck-queue alert badges |
| 65 | `PRIORITY_AMBIENT_URGENT` | `calendar_countdown` inside `notice_minutes`/`warn_minutes` — outranks even a live alert |
| 90 | `PRIORITY_SESSION` | An authenticated BUSY/CUSTOM work session on the device — outranks everything else |

The `application_name` field tags the source, letting the display track ownership and multi-instance behavior.
Two firmware facts shape all of the above: equal priority from a different `application_name` is **rejected** (`409`), not a hand-off — only a strictly higher number preempts; and a preempted app's elements are **evicted, not restored** — the lower-priority app only reclaims the screen via its own next scheduled redraw, never automatically. Each element carries an optional `timeout`; if its source doesn't refresh within that window, the element self-clears rather than sticking on screen indefinitely.

**Overlay dwell/rotation.** `ci_status`'s overlay-tier frames (running badge, then the GraphQL and REST quota gauges) each draw for one `OVERLAY_DWELL_SECONDS` (10s) dwell slot, then stay silent for at least one more dwell period before redrawing — giving `calendar_countdown`'s own ambient-tier redraws (also tuned to a 10s cadence) a real chance to land in the resulting gap. Because eviction is one-way, the two integrations trade the panel back and forth rather than alternating cleanly; see each integration's README for the measured recovery rates.

**Escalation beats alerts.** As an upcoming calendar event gets closer, `calendar_countdown` climbs from `PRIORITY_AMBIENT` (20) through `PRIORITY_AMBIENT_RAISED` (25, inside `approach_minutes`) to `PRIORITY_AMBIENT_URGENT` (65, inside `notice_minutes`/`warn_minutes`) — strictly above `ci_status`'s own `PRIORITY_ALERT` (60), so a persistent CI failure can no longer permanently bury an imminent event. `ci_status` needs no special handling for this: its alert draw gets a `409` while the calendar holds the higher tier, treats that as an expected rejection, and reappears on its own next poll once the calendar drops back to baseline.

**Snooze by acknowledgment.** `ci_status` alerts can be snoozed entirely through the device's native **start** button — no separate UI or config edit. Starting a BUSY/CUSTOM session while an alert is showing, then ending it, snoozes that exact failure/stuck fingerprint for `snooze_minutes`; any change to the fingerprint (a new failure, or the original resolving and a new one appearing) re-alerts immediately, even mid-snooze.

The `application_name` field tags each draw's source, letting the display track ownership and multi-instance behavior.

## Adding an integration

Expand Down Expand Up @@ -122,38 +134,30 @@ client uses (`draw`, `clear`, `status`, `get_busy`, `set_busy_simple`,
`play_audio`) is a synchronous request/response call and mirrors 1:1
over cloud.

### Post-merge live-probe checklist

This round's tests are entirely mocked — no cloud request has been made
against a real token, since the operator hadn't provisioned one yet.
**Before relying on cloud fallback in practice**, run this checklist
once a real `cloud_token` is in `config.toml`:

1. **Forced-cloud draw probe.** Set `transport = "cloud"` temporarily
and run a `client.draw(...)` (e.g. via either integration's
`--once --dry-run=false` path, or a one-off script) to confirm the
token is valid and a real device draw round-trips over the cloud
relay end to end.
2. **Cadence headroom check.** No rate limit or cadence guidance is
documented anywhere for the cloud API (see
`scratchpad/busy-cloud-api-research.md`'s "Open items"). Run
`calendar_countdown` (10s ambient redraw cadence) forced onto cloud
transport for a few minutes and confirm no throttling/errors show up
before trusting cloud fallback to hold up under sustained polling.
3. **Base-URL ambiguity.** This codebase defaults `cloud_base_url` to
`https://api.busy.app/busybar`, but busylib-py's own hardcoded default
is the differently-hosted `https://proxy.busy.app` — an unresolved
discrepancy in the source research, not something this round's mocked
tests can settle. Confirm which base actually works against a live
token (or whether both do) and update the default/docs here if
`api.busy.app/busybar` turns out to be wrong or non-canonical.

Set `transport` back to `"auto"` (or leave it, since `"auto"` is the
default) once the checklist above passes.
### Verified against the live cloud API

The v1.6 launch tests were entirely mocked; the checklist that shipped
with that round has since been run against a real device and a real
token, with these results:

- **Base URL confirmed.** `https://api.busy.app/busybar` (the shipped
`cloud_base_url` default) is correct and working — the `busylib-py`
discrepancy noted during research (its own hardcoded default is the
differently-hosted `https://proxy.busy.app`) does not apply to this
client. No change needed to `cloud_base_url`.
- **Forced-cloud draw probe: 5/5 `DRAWN`.** Round-trip latency
300–465ms, median 353ms — well inside the cloud transport's `(5, 15)`s
timeout, and ample headroom under `calendar_countdown`'s 10s ambient
redraw cadence (a cloud-relayed redraw comfortably completes well
before the next one is due).
- **Auto-fallback is live in production.** Running with `transport =
"auto"`; local→cloud degradation and cloud→local recovery transitions
are logged at `INFO` exactly as designed (see `BusyBarClient`'s class
docstring in [`src/busybar/client.py`](src/busybar/client.py)).

## What's inside

| Integration | Description |
|---|---|
| [`calendar_countdown`](integrations/calendar_countdown/) | Next-meeting countdown on the LED display from macOS Calendar; optional auto-BUSY during events. |
| [`ci_status`](integrations/ci_status/) | GitHub Actions status via REST with ETag caching; red alert on failure, yellow on stuck-queued runs. |
| [`calendar_countdown`](integrations/calendar_countdown/) | Live countdown to your next macOS Calendar event. Four-stage escalation as an event approaches — `approach_minutes` (30m default), `notice_minutes` (15m, amber), `warn_minutes` (5m, red), and a final-minute LED blink — plus one audio chirp precisely at event start. The countdown itself turns teal while the event is in progress. Optional `auto_busy` starts a BUSY session automatically for the event's duration. |
| [`ci_status`](integrations/ci_status/) | GitHub Actions status via the REST API with ETag caching (near-zero steady-state quota cost). Red alert badges on failure, amber on stale-queued runs, either snoozable via the device's native start button. While a run is active, an overlay-tier rotation shows a running badge (ETA plus a "remain"/"left" label) alongside GitHub GraphQL/REST quota gauges. Optional account-wide watching auto-discovers and monitors every repo you own, not just an explicit list. |
Loading