Documentation
How VitalsBar works
The formats VitalsBar expects and exactly how it behaves, from the source list you type in Settings to the single colour it shows in your menu bar.
Overview
VitalsBar polls the public status page of each service you care about, reduces every response to one of four health states, and shows the single worst state as a colour (and, when something is wrong, a count) in your menu bar. Click the icon for a pane that breaks the roll-up back down per service and per subsystem.
Everything is local. There is no VitalsBar account, server, or telemetry:
the app talks only to the status pages you list, and stores your settings,
silences, and history in macOS UserDefaults on your Mac.
(one per line)
summary.json
→ health
wins
Status sources
The Status sources field in Settings is a plain-text list, one
entry per line. Every entry must use the Label=host form:
AWS (via Updog)=https://updog.ai/status/amazonaws
Claude=status.claude.com
Cloudflare=www.cloudflarestatus.com
Datadog EU=status.datadoghq.eu
GitHub=www.githubstatus.com
Linear=linearstatus.com
OpenAI=status.openai.com
-
Label is everything before the first
=; it’s the display name shown in the pane. host is everything after it. -
A bare host (
status.openai.com) is all you need — VitalsBar finds its status API for you. A leading scheme is optional, sohttps://status.openai.com/is equivalent. For a status page split into several sub-pages (incident.io calls them regions/products), keep the sub-page’s path to scope to just that one:www.finstatus.com/eu-hosting. -
Only the
Label=hostform is honoured. Bare hosts (a line with no=), and entries with an empty label or empty host, are ignored. Blank lines are ignored too. - If the field ends up with no valid entries, VitalsBar falls back to the five defaults above.
- Leaving the field blank prefills it with those defaults the next time Settings opens, so you have an editable starting point.
Supported providers — auto-detected
VitalsBar recognises three kinds of public source and works out which one a host serves the first time it sees it (that probe is cached per host, so it’s a one-time cost, not a per-refresh one):
-
Atlassian Statuspage — the Statuspage
API (
https://<host>/api/v2/summary.json). Covers services hosted onstatuspage.io: GitHub, Cloudflare, Datadog, Anthropic, and hundreds more. -
incident.io — its component API (
https://<host>/proxy/<host>), since incident.io’s Statuspage-compatible endpoint omits components. A multi-region page (such as Intercom’s Fin) is scoped to the sub-page you point at. -
Updog (Datadog’s cross-provider outage tracker) — point at
updog.aifor the whole board, or a single provider withupdog.ai/status/<provider>(e.g./status/amazonaws, whose services become components). Updog infers outages from Datadog telemetry rather than a provider’s own reporting, so a detected outage shows as Degraded.
All three give component-level detail, per-component silencing and the menu-bar badge, and all are free — no license needed. A host that serves none resolves to the Unknown state.
Stable ids & silence survival
Each source gets an id derived from its host, not its position in the
list. A host that matches a known default inherits that default’s slug
(e.g. openai); any other host uses the host string itself. True
collisions are disambiguated with a numeric suffix (-2,
-3). Because ids are stable across reorders and edits, the
services you’ve silenced stay silenced when you
rearrange the list.
Migration: an older comma-separated list is converted to one-per-line automatically on first launch of a build that expects newlines.
Pro & relay feeds
Public status pages are free, forever. What
VitalsBar Pro unlocks is relay feeds: a
source whose endpoint serves the native vitalsbar.v1 JSON
format instead of a Statuspage summary. That’s the format a
self-hosted relay emits — private, aggregated status from your own
infrastructure (for example live incident.io incidents), so provider
credentials stay on the relay host rather than on each laptop.
The gate is on the format, not the URL: any source that resolves to a
vitalsbar.v1 document requires an active Pro license. A Statuspage
or incident.io status-page source is never gated. Without a license, a relay
source is recognized but not resolved — its row reads
VitalsBar Pro required rather than
silently disappearing.
incident.io — two different things. A public incident.io status page is a free source like any other. What Pro’s relay adds is live incident.io incidents, pulled straight from your own incident.io account (its API key lives on the relay) — private, internal detail a public status page doesn’t expose.
Licensing & devices
- Enter your key under Settings → License. One license covers a team; each Mac counts as one device (Polar calls it an activation).
- Activating validates the key with Polar and claims a device. If every device is in use, activation is refused until one is freed. Remove license from this Mac releases the device for another one.
-
The key and its activation id are stored in the macOS Keychain; only
the resulting verdict is cached in
UserDefaults. - VitalsBar re-checks the license on launch and periodically. If Polar is briefly unreachable, an offline grace window keeps Pro working — only a positive revoke (or grace expiry) turns it off, so a network blip never locks you out.
The vitalsbar.v1 format
vitalsbar.v1 is VitalsBar’s native feed format — the
contract a relay (or any endpoint of yours) serves so
the app can show it as a provider. It’s a plain JSON document served
over HTTP; VitalsBar auto-detects it by the
schema field, so the same source line works whether it points at
a public Statuspage or your own feed. The format is open — publishing one is
the intended way to connect your own services.
{
"schema": "vitalsbar.v1",
"name": "Payments",
"url": "https://status.example.com",
"components": [
{ "id": "api", "name": "API", "status": "operational" },
{ "id": "webhook", "name": "Webhooks", "status": "degraded", "label": "elevated latency" },
{ "id": "payout", "name": "Payouts", "status": "down", "label": "processor outage" }
]
}
Fields
| Field | Required | Meaning |
|---|---|---|
schema | yes | Must be the literal "vitalsbar.v1". Any other value and the payload is not treated as native. |
name | no | Provider display name; overrides the source’s Label=. |
url | no | Link opened when the row is clicked; falls back to the source host. |
components | no | The subsystems that make up this provider (see below). Omit for a single-signal provider. |
health | no | Overall health — only used when there are no components (the page-indicator role). One of the four states below. |
detail | no | One-line subtitle. Defaults to a rolled-up summary of the components. |
Each component
| Field | Required | Meaning |
|---|---|---|
id | yes | Stable per-component id. It keys silences as <source>::<id>, so keep it stable across polls. |
name | yes | Component display name. |
status | yes | One of the four health states below. Statuspage words (e.g. major_outage) are also accepted and mapped; any unrecognised non-operational value becomes Degraded — never silently Unknown. |
label | no | Human status shown in the row and the silence menu (e.g. “elevated latency”). |
The four health states (the values status and health accept):
operational— Operationaldegraded— Degradeddown— Downunknown— Unknown
Roll-up
With components, the provider’s overall health is the
worst component state, exactly like a Statuspage source — so one
component Down turns the row red. Without
components, the top-level health is used directly. Either way, a
component-less vitalsbar.v1 reply and a Statuspage page-indicator
behave the same.
Consuming a vitalsbar.v1 feed is a Pro feature.
The format itself is open — you can serve and inspect it freely; the license
gates the app’s use of it, not knowledge of it.
Self-host the relay
The relay is a small self-hosted service that emits
vitalsbar.v1 feeds, so provider credentials
(like an incident.io API key) live on one host you control rather than
on every laptop. Today it materialises live incident.io incidents; each
integration is mounted at its own path and served as an isolated feed.
Run it with Docker
Pull the prebuilt image and pass your provider key:
docker run -d --name vitalsbar-relay \
-p 8787:8787 \
-e INCIDENT_IO_API_KEY=inc_… \
ghcr.io/jbbarth/vitalsbar-relay:latest
The container serves on port 8787 and exposes:
| Path | Purpose |
|---|---|
/incident.io | Live incident.io incidents as a vitalsbar.v1 feed (needs INCIDENT_IO_API_KEY) |
/ | Index of mounted integration paths |
/healthz | Liveness probe (used by the container health check) |
Configuration
| Env var | What it does | Default |
|---|---|---|
INCIDENT_IO_API_KEY | Enables the /incident.io feed | unset (feed disabled) |
RELAY_INCIDENT_IO_MIN_SEVERITY | Report only this severity and above (fails open if unknown) | all severities |
RELAY_AUTH_TOKEN | Require a token on the feeds; sent as ?token=… from the app | unset (open) |
PORT / WEB_CONCURRENCY | Listen port / worker count | 8787 / 2 |
LOG_LEVEL | Log verbosity (info/debug) | info |
Point the app at it
Add one source line per feed in Settings → General → Status sources, using the full URL so it’s fetched as-is:
Incidents=https://relay.your-co.com/incident.io
App Transport Security: a relay on another host must be served over
HTTPS — the app blocks plain http:// to non-loopback hosts.
On the same Mac, http://127.0.0.1:8787/incident.io is fine (use
127.0.0.1/localhost, not 0.0.0.0).
Security: without RELAY_AUTH_TOKEN the relay is open to
anyone who can reach the port. Keep it behind a network boundary (VPN/private
network) and serve HTTPS if it leaves your LAN — never expose it unauthenticated
to the internet.
How a check works
For each source, VitalsBar requests
https://<host>/api/v2/summary.json and turns the response
into one row. summary.json carries both the page-level indicator
and every component’s status, which is what lets a provider’s
health reflect only the subsystems that are actually degraded.
Component status → health
Each leaf component’s Statuspage status maps to a health state:
| Statuspage component status | Health |
|---|---|
operational | Operational |
degraded_performance | Degraded |
partial_outage | Degraded |
under_maintenance | Degraded (unless overridden) |
major_outage | Down |
| any other non-operational value | Degraded |
When a page has no components
Some pages expose only a single overall indicator. VitalsBar maps it directly:
| Page indicator | Health |
|---|---|
none | Operational |
minor, maintenance | Degraded |
major, critical | Down |
| anything else | Unknown |
The one-line detail
- Nothing affected → “Operational” (or “All Systems Operational”).
- One subsystem affected → “<Name> — <state>”, e.g. “API — degraded”.
- Several affected → “N components affected”.
The health model
Every signal lands in exactly one of four states, ordered by severity:
The severity ordering is what drives the roll-up: the worst state always wins, so “one thing is down” is never hidden behind “most things are fine”. Note that Unknown outranks Operational but is quieter than an actual problem; an unreachable page nudges the signal without crying outage.
Per-source overrides
Two built-in defaults carry overrides so their roll-up reflects product health rather than noise. A source you type whose host matches a default inherits that default’s overrides automatically.
Treat-as-operational
Some providers report normal operating modes as non-operational statuses.
Cloudflare, for instance, lists re-routed edge locations as
under_maintenance, which is routine and shouldn’t turn the
provider amber. Such statuses are forced back to
Operational.
Group scoping
A page can expose hundreds of components. Cloudflare enumerates a component per city; counting each one would make product health hostage to a single data centre. When a source is scoped to named component groups (e.g. “Cloudflare Sites and Services”), only components inside those groups count; group headers themselves are always skipped.
These overrides ship with the defaults for Cloudflare. Other hosts you add are evaluated straight, with no components ignored.
Silencing
Silencing mutes a signal you don’t want colouring your menu bar: a whole provider, or a single subsystem (e.g. a compliance region you never use). A silenced signal stops contributing to the menu-bar colour and badge, but stays visible in the pane’s Silenced group so you can bring it back.
Silencing is view-only: it never touches the provider or its incident.
It’s stored locally in UserDefaults and does not sync
across machines (the tradeoff of staying backend-free).
Durations
- Timed silences (2 hours … 1 week) expire on their own after each refresh.
- Until resolved clears the moment the signal returns to Operational.
- Forever permanently mutes something you never care about, until you un-silence it by hand.
Each silence is keyed to a stable id (<providerId>
for a whole provider, or <providerId>::<componentId>
for one subsystem), so it survives restarts and list reorders.
History & sparkline
Statuspage exposes only current status, so VitalsBar builds the per-row sparkline from observations it accumulates itself. Every refresh records one tick (a timestamp + health) per source.
- History is kept per source, rolling, up to the last 40 observations.
- It’s persisted to
UserDefaultsso the sparkline is populated across restarts. - The pane renders it as spaced bars, coloured by each tick’s health.
With a 2-minute interval, 40 ticks is roughly the last 80 minutes of history; at 5 minutes it’s a little over three hours.
Refresh & polling
VitalsBar checks once immediately at launch, then repeats on your chosen interval. All configured sources are checked concurrently, then re-sorted back into the order you listed them.
The header’s refresh control forces an immediate check without waiting for the timer. After every refresh, timed silences are expired and “until resolved” silences whose signal recovered are cleared.
Settings & storage
Everything VitalsBar remembers lives in macOS UserDefaults under
the app’s domain, with no files to manage and nothing off your Mac.
| Key | What it holds | Default |
|---|---|---|
VitalsBar.sources | The Label=host list, one per line | the five defaults |
VitalsBar.refreshIntervalMinutes | Polling interval in minutes | 2 |
VitalsBar.showOperational | List operational systems in the pane, or only problems | true |
VitalsBar.silences | Per-signal silences (JSON) | empty |
VitalsBar.checkHistory | Rolling per-source observation history (JSON) | empty |
VitalsBar.licenseStatus | Cached Pro verdict (active/revoked) | unset |
VitalsBar.licenseExpiresAt | End of the licensed update window, if any | unset |
VitalsBar.licenseCheckedAt | Last successful license re-check (drives offline grace) | unset |
The Pro license key and its activation id are the only secrets, and they
live in the macOS Keychain — not in UserDefaults. The keys
above cache just the verdict.
Also in Settings
- Launch at login: registers VitalsBar as a macOS login item.
- Automatic updates: VitalsBar updates itself via Sparkle; toggle background checks, or use Check now for an immediate one. Since 0.2.0 builds are Developer-ID signed and notarized.
Something unclear or missing? It probably means the behaviour is worth refining; the docs track the app release by release. See the changelog for what recently changed.