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. -
A line whose first non-blank character is
#is a comment and is skipped entirely: not fetched, not classified, not shown. It’s the way to park a source without losing how it was spelled:# Datadog US=status.datadoghq.com. Only a leading#counts, so one inside a value (a URL fragment, say) is left alone. -
If the field ends up with no valid entries, VitalsBar falls back to
the seven defaults above, including when you comment every line out,
so
#-ing the whole list restores the defaults rather than emptying the pane. - 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 four kinds of public source and works out which one a host serves the first time it sees it (the result is cached per host, so it’s a one-time cost, not a per-refresh one; updown.io is recognised by host name alone, with no probe at all):
-
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. An incident opened against no component at all still shows up, as its own silenceable row. -
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. -
updown.io: point at a status page (
demo.updown.io,updown.io/p/<token>) or a single published check (updown.io/<token>). updown publishes no status API, so VitalsBar reads the page itself; on a multi-check page every check becomes a component, keyed by its updown token. Because updown checks for reachability, a failing check is Down rather than degraded, and a disabled check is left out entirely: it isn’t being watched, so it carries no signal either way. Note thatupdown.ioon its own is updown’s homepage, not a status page: it advertises the product with sample checks in the very same markup, so VitalsBar refuses it rather than report a green provider you never configured.
All four give component-level detail, per-component silencing and the menu-bar badge, and all are free, with no license needed. (The exception is a source that is a single check (a one-check updown page), which is reported at the provider level, since a one-component breakdown would just be the row twice.) 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. Each integration is mounted at its own path and served as an isolated feed: live incident.io incidents, and
CI status from GitHub.
Feeds are refreshed on a timer, never by a request. A background task per
integration polls the provider and stores a snapshot; requests only ever read it.
That’s what lets one relay serve a whole team: provider load is
3600 / interval calls an hour no matter how many people poll, how
often, or how many arrive at once. There is deliberately no path from a request
to a provider API, so a burst of clients can’t stampede one.
Feeds carry an ETag and Cache-Control: no-cache, so a
client that already has the current body revalidates and receives a
304 rather than downloading it again. The app does this on its own, with no configuration, and since a feed only changes when its provider
does, most polls are 304s in steady state.
A failed refresh keeps the last good snapshot rather than blanking the
feed, so a blip doesn’t turn a working row gray. The snapshot keeps ageing
though, and past RELAY_STALE_AFTER_SECONDS the row reports
Unknown with its age and the reason. It
is never reported as Down: the relay being
unable to reach a provider is not the same as that provider being broken.
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) |
/github/ci | CI status for every watched repository, one component each |
/github/ci/<owner>/<repo> | Just that repository, as its own row |
/ | Index of mounted integration paths |
/healthz | Liveness probe, plus each snapshot’s age and last error |
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) |
RELAY_REFRESH_SECONDS | How often each provider is polled | 60 |
RELAY_STALE_AFTER_SECONDS | Age at which a snapshot stops being trusted and the row goes gray | 600 |
PORT / WEB_CONCURRENCY | Listen port / worker count | 8787 / 1 |
LOG_LEVEL | Log verbosity (info/debug) | info |
The GitHub CI feed takes its own
GITHUB_APP_* and RELAY_GITHUB_* variables, listed with
its setup. A provider may also override the shared refresh cadence: GitHub polls
every 30 seconds, since one batched query costs a single rate-limit point.
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).
One worker, on purpose. Each worker runs its own refresh timers and holds
its own snapshots, so a second worker polls every provider twice and keeps a
second cache that can disagree with the first, so a client polling twice in a
row could see the feed jump backwards. Raising WEB_CONCURRENCY means
moving the snapshots somewhere shared first. Request recycling is off for the same
reason: restarting a worker throws its cache away.
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.
CI status from GitHub
The relay can report whether main is green across your repositories, including private ones, which is the point: the credential lives on
the relay, not on every laptop. Point one source line at a single repository to
give it its own row, or at the whole feed to get one row with a component per
repository.
The relay polls GitHub on its own timer and serves every client from that one snapshot, so a team of any size costs GitHub the same handful of API calls an hour. A single batched GraphQL query covers every watched repository.
What “green” means
A repository is judged by the last finished commit on its default branch,
not by whatever is building right now. Push to main and the row keeps
its previous verdict until the new checks settle. Otherwise every push
would flip the row for as long as a build takes, and the signal would be noise.
The verdict comes from GitHub’s own rollup, which combines Actions check
runs and commit statuses:
| Last settled commit | Health |
|---|---|
| All checks passed | Operational |
| A check failed or errored | Degraded |
| Every recent commit still building | Unknown |
| No checks on the default branch | Unknown |
A failing build is amber, not red. Red is worth reserving for “a
service people depend on is unavailable”. A broken main is a
problem for the team that owns it, and on a busy repository it happens often
enough that spending red on it would teach everyone to ignore red.
The rollup counts every check on the commit: deploy previews, coverage bots and third-party integrations included. If one of those is flaky it will turn the row red alongside the real build.
Create a GitHub App
Use a GitHub App rather than a personal access token. It can be scoped to exactly the repositories you want, its permissions are read-only and auditable, it gets a higher rate limit, and (the practical argument) it doesn’t stop working the day the person who created the token leaves. Create it under the organisation, not your own account, for the same reason. You’ll need to be an org owner, or hold the “GitHub App manager” role.
-
Open the registration form
https://github.com/organizations/<YOUR-ORG>/settings/apps/newOr navigate: organisation Settings → Developer settings → GitHub Apps → New GitHub App.
-
Name it, and turn webhooks off
GitHub App name must be unique across all of GitHub:
VitalsBar CI, prefixed with your org if that’s taken. Homepage URL is required but unused; your relay’s URL will do. Leave Callback URL blank; nobody signs in through this App.Under Webhook, uncheck Active. The relay polls, so there is no endpoint to deliver to; leaving it on just accumulates failed deliveries.
Set Where can this GitHub App be installed? to Only on this account.
-
Grant four read-only permissions
Under Repository permissions, and nothing else:
Permission Access Why ContentsRead-only The default branch and its commit history ChecksRead-only Check runs in the rollup Commit statusesRead-only Commit statuses in the rollup MetadataRead-only Mandatory; selected for you Both Checks and Commit statuses are needed. The rollup merges the two, and with only one of them GitHub doesn’t refuse cleanly: it returns
Resource not accessible by integrationin the middle of an otherwise fine response, which surfaces later as an empty feed.Leave every organisation and account permission at No access, and subscribe to no events.
-
Create it, then generate a private key
Note the App ID shown at the top of the page that follows. Then scroll to Private keys → Generate a private key; a
.pemfile downloads. GitHub shows it once, so keep it and put it wherever the relay host keeps secrets. -
Install it on the organisation
Install App in the sidebar, then install on the organisation with All repositories. There is nothing to hand-pick and nothing to keep in sync as repositories come and go: the relay narrows the list itself, as described below.
-
Note the installation id
After installing, the browser sits on a URL ending in the number you need:
https://github.com/organizations/<ORG>/settings/installations/<INSTALLATION_ID>
Which repositories get watched
Granting the App an entire organisation would mean querying hundreds of
repositories, most of them dormant, forks or experiments: slow, noisy, and a list nobody wants to curate. So the relay derives the watched set itself: the
repositories the App can see, minus archived ones, minus anything not pushed in
the last RELAY_GITHUB_ACTIVE_DAYS days.
That filtering is free. Listing an installation’s repositories already returns each one’s last push time, so deciding what’s active costs no extra call and reads no commit history. The list is recomputed on each refresh, so a repository that goes quiet drops out and one that wakes up comes back, with no configuration either way.
Any branch counts. The push time GitHub reports covers pushes to any branch, not just the default one, so a repository with only feature-branch activity stays in the set. That’s deliberately generous: the cost of including a quiet repository is one entry in a batched query, while wrongly excluding one means a row that silently stops updating.
Check the App before wiring the relay up
Most mistakes above surface later as an empty feed rather than an error. This pre-flight walks the whole chain: signs a JWT, names any missing permission, finds the installation, mints a token, lists the granted repositories, then runs the real CI query and prints each repository’s last settled commit alongside the query’s measured cost.
curl -O https://vitalsbar.org/verify-github-app.py
export GITHUB_APP_ID=123456
export GITHUB_APP_PRIVATE_KEY_PATH=~/Downloads/vitalsbar-ci.…private-key.pem
uv run --with "pyjwt[crypto]" --with httpx python verify-github-app.py
GITHUB_APP_INSTALLATION_ID is optional; with one
installation the script finds it and prints it. Needs
uv, or
run it with any Python that has pyjwt[crypto] and httpx.
(download instead)
Configure the relay
| Env var | What it does | Default |
|---|---|---|
GITHUB_APP_ID | Required. The App ID from step 4 | unset (feed disabled) |
GITHUB_APP_INSTALLATION_ID | Required. The number from step 6 | unset (feed disabled) |
GITHUB_APP_PRIVATE_KEY | Required, unless the path below is set. The .pem contents | unset (feed disabled) |
GITHUB_APP_PRIVATE_KEY_PATH | …or a path to the .pem instead, for a mounted secret | unset |
RELAY_GITHUB_ACTIVE_DAYS | Only watch repositories pushed within this window | 7 |
RELAY_GITHUB_HISTORY_DEPTH | Commits scanned for the last settled one | 20 |
With a required one missing, the relay warns at startup naming the variable, and the feed reports Unknown saying the same thing. It never fails silently or reports a false green.
WARNING: GitHub CI feed will report unknown: GITHUB_APP_ID is not set
A worked example, with the key mounted as a file rather than pasted into the environment:
docker run -d --name vitalsbar-relay \
-p 8787:8787 \
-v /etc/vitalsbar/github-app.pem:/secrets/github-app.pem:ro \
-e GITHUB_APP_ID=123456 \
-e GITHUB_APP_INSTALLATION_ID=98765432 \
-e GITHUB_APP_PRIVATE_KEY_PATH=/secrets/github-app.pem \
ghcr.io/jbbarth/vitalsbar-relay:latest
Pasting the key into an environment variable? Multi-line values get mangled routinely: shells, compose files and secret managers all like to turn the
newlines into a literal \n. The relay repairs that, because the error
it otherwise causes (could not deserialize key data) reads like a
corrupt key rather than a quoting problem. Mounting the file avoids the question.
Why scan several commits? A busy monorepo can have a dozen commits building
at once, so the last settled one may be well behind the head. Depth is
nearly free in rate-limit terms (GraphQL prices a query by the connections
it opens, not the rows it returns), but it is real work for GitHub to
answer, and asking for too much at once earns a gateway error rather than a
reply. Hence a modest default. Raise RELAY_GITHUB_HISTORY_DEPTH if a
repository keeps reporting Unknown because
every commit in the window was still building.
Point the app at it
Checkout=https://relay.your-co.com/github/ci/your-org/checkout
Billing=https://relay.your-co.com/github/ci/your-org/billing
All CI=https://relay.your-co.com/github/ci
A per-repository line gives that repository its own row, with its own history
sparkline, its own silence and its own click-through. The
bare /github/ci line gives one row covering everything, with each
repository as a component you can silence individually. Use whichever matches how
closely you watch them.
How a check works
For each source, VitalsBar requests one endpoint and turns the response into
one row. Which endpoint depends on the kind detected for that host: an Atlassian
Statuspage serves https://<host>/api/v2/summary.json,
incident.io its component API, and an updown.io page is fetched as-is and read
as HTML. The decision below describes a summary.json response, which
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. The other formats are normalised into those same components, so
everything downstream behaves identically.
That last branch is an incident.io speciality: a provider can open an incident
without attaching it to any component on the page: every dot stays green
while something is plainly broken. VitalsBar surfaces that incident as a
subsystem of its own, named after the incident and tagged with its status
(investigating, identified, monitoring).
It colours the provider like any other affected subsystem, and it can be
silenced on its own without muting the provider. An
incident that does list components is already carried by those components,
so it gets no extra row; and one that has recovered (impact back to
operational) reads green.
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.
An incident.io incident the provider hasn’t mapped to any component joins the roll-up as a synthetic subsystem at Degraded, so worst-wins still applies and the incident stays addressable on its own rather than only through its provider.
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, a single subsystem (e.g. a compliance region you never use, or one updown.io check), or a single ongoing incident the provider hasn’t mapped to any component. 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, <providerId>::<componentId>
for one subsystem, or <providerId>::incident:<incidentId>
for one unmapped incident), so it survives restarts and list reorders. On an
incident, Until resolved clears as soon as that incident leaves the
page’s ongoing list.
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.
Updates
VitalsBar updates itself with Sparkle, the standard updater for Mac apps distributed outside the App Store. It reads an appcast (a small XML feed listing recent builds) from the same host that serves the downloads:
https://files.vitalsbar.org/vitalsbar/appcast.xml
- Background checks are on by default and run about once a day. The Check for updates automatically toggle in Settings turns them off; Check now beside it runs one immediately, whatever the toggle says.
- Every build is signed twice. Sparkle verifies an EdDSA signature against a public key baked into the app, so a build not signed with the release key is refused even if the feed or the download host were tampered with. Separately, builds are Developer ID signed and notarized by Apple (since 0.2.0), so macOS opens them without a Gatekeeper prompt.
- Nothing about your Mac is sent. The appcast is a static file fetched over HTTPS; Sparkle’s optional system-profile reporting is not enabled.
- The feed carries a rolling window of recent builds rather than every release ever made. Sparkle always offers the newest one, so an install that’s been idle a long time goes straight to the current version.
The app and its updates are free. A Pro license unlocks relay feeds; it isn’t needed to receive updates.
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 seven 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: toggle Sparkle’s background checks, or use Check now for an immediate one. See Updates.
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.