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.

Sources Label=host
(one per line)
Fetch GET /api/v2/
summary.json
Parse components
→ health
Roll up worst state
wins
Menu bar colour + badge

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, so https://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=host form 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 on statuspage.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.ai for the whole board, or a single provider with updog.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.

ON YOUR MAC VitalsBar EXTERNAL · FREE Public status pages · Atlassian Statuspage · incident.io status pages · Updog polled directly over HTTPS YOU HOST · PRO Relay holds provider credentials PRIVATE API incident.io live incidents free · direct Pro · vitalsbar.v1 API key stays here
Free · public pages, polled straight from your Mac Pro · private status via a relay you host

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

FieldRequiredMeaning
schemayesMust be the literal "vitalsbar.v1". Any other value and the payload is not treated as native.
namenoProvider display name; overrides the source’s Label=.
urlnoLink opened when the row is clicked; falls back to the source host.
componentsnoThe subsystems that make up this provider (see below). Omit for a single-signal provider.
healthnoOverall health — only used when there are no components (the page-indicator role). One of the four states below.
detailnoOne-line subtitle. Defaults to a rolled-up summary of the components.

Each component

FieldRequiredMeaning
idyesStable per-component id. It keys silences as <source>::<id>, so keep it stable across polls.
nameyesComponent display name.
statusyesOne 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.
labelnoHuman status shown in the row and the silence menu (e.g. “elevated latency”).

The four health states (the values status and health accept):

  • operationalOperational
  • degradedDegraded
  • downDown
  • unknownUnknown

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:

PathPurpose
/incident.ioLive incident.io incidents as a vitalsbar.v1 feed (needs INCIDENT_IO_API_KEY)
/Index of mounted integration paths
/healthzLiveness probe (used by the container health check)

Configuration

Env varWhat it doesDefault
INCIDENT_IO_API_KEYEnables the /incident.io feedunset (feed disabled)
RELAY_INCIDENT_IO_MIN_SEVERITYReport only this severity and above (fails open if unknown)all severities
RELAY_AUTH_TOKENRequire a token on the feeds; sent as ?token=… from the appunset (open)
PORT / WEB_CONCURRENCYListen port / worker count8787 / 2
LOG_LEVELLog 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.

summary.json
HTTP not 2xx / unreachable Unknown “Could not reach status page”
No components listed fall back to the page indicator
Components, none affected Operational “All Systems Operational”
Components affected worst affected component wins

Component status → health

Each leaf component’s Statuspage status maps to a health state:

Statuspage component statusHealth
operationalOperational
degraded_performanceDegraded
partial_outageDegraded
under_maintenanceDegraded (unless overridden)
major_outageDown
any other non-operational valueDegraded

When a page has no components

Some pages expose only a single overall indicator. VitalsBar maps it directly:

Page indicatorHealth
noneOperational
minor, maintenanceDegraded
major, criticalDown
anything elseUnknown

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:

Operationalseverity 0
Unknownseverity 1
Degradedseverity 2
Downseverity 3

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.

Components Operational Degraded Operational
Provider Degraded
Overall Degraded

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

For 2 hours
For 1 day
For 2 days
For 1 week
Until resolved
Forever
  • 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 UserDefaults so 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.

Every 1 minute
Every 2 minutes default
Every 5 minutes

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.

KeyWhat it holdsDefault
VitalsBar.sourcesThe Label=host list, one per linethe five defaults
VitalsBar.refreshIntervalMinutesPolling interval in minutes2
VitalsBar.showOperationalList operational systems in the pane, or only problemstrue
VitalsBar.silencesPer-signal silences (JSON)empty
VitalsBar.checkHistoryRolling per-source observation history (JSON)empty
VitalsBar.licenseStatusCached Pro verdict (active/revoked)unset
VitalsBar.licenseExpiresAtEnd of the licensed update window, if anyunset
VitalsBar.licenseCheckedAtLast 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.