diff --git a/README.md b/README.md index fe4cd54..cadd169 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. |