Repository navigation
Status Badges
cronstable serves small SVG images that show the health of one job, or of every job at once, in the style of the build badges on many README files. Put one in a README, a wiki page, or a home dashboard, and readers can see whether a job passes without opening the web dashboard.
| Endpoint | Shows |
|---|---|
GET /jobs/{name}/badge.svg |
The job's name and its verdict. An unknown job name gets a 404. |
GET /badge.svg |
cronstable and a count of the jobs in the most urgent state. |
Both endpoints are on by default whenever the HTTP API is on. They follow the API's authentication: a daemon that requires a token serves them only to callers that send it. A browser also refuses to load a badge from the daemon into a page from another origin. To lift both limits, make the badges public.
curl http://localhost:8080/jobs/nightly-backup/badge.svg
curl http://localhost:8080/badge.svgA job's badge shows the first message in this table that applies to the job:
| Message | Color | When |
|---|---|---|
disabled |
gray | The job has enabled: false. |
paused |
gray | A runtime pause is in effect. |
failing |
red | The last run failed. |
overdue |
orange | An SLA check is in breach. |
passing |
green | The last run succeeded. |
cancelled |
gray | The last run was canceled. |
unknown |
gray | The last run was interrupted before it recorded an outcome. |
running |
blue | A run is in progress, and the daemon has no earlier run on record. |
pending |
gray | The daemon has no run of the job on record, such as before its first run. |
The badge reads the same state as the dashboard's status column and its
OVERDUE chip. A run that is in progress, or waiting in a
resource pool queue, leaves the badge on the last run's
result, so the badge of a job that runs every minute holds steady at
passing or failing. A scheduled run that a pause held back ran nothing,
so the badge keeps the last run's result while the dashboard shows the slot
as Skipped. While the daemon runs, a failing job still reads failing
after a maintenance pause of any length.
Without a state section, the daemon keeps run history in
memory, so after a restart a job's badge reads pending until the job runs
again. With one, the daemon reads each job's newest records back from its
run ledger after a restart, and
the badge shows the last run they hold. When a job's newest records are
all slots that a pause skipped, the daemon looks further back for the
job's last run after the startup reads finish, one job at a time. Until
that look-back finishes, the badge reads pending, or paused while the
pause holds. If a look-back times out, for example because the store is
slow, the daemon stops looking, and the badges of that job and of the jobs
still waiting read pending until each job runs again. A badge also reads
pending until the job runs again when the daemon can't read the ledger
at all after a restart.
The ledger keeps state.maxRunsPerJob records per job (1000 by default),
skipped slots included. The look-back stops after 10,000 records, even with
maxRunsPerJob: 0, which keeps every record. If a pause skips as many slots
as the ledger keeps, or 10,000 slots if that's fewer, the last run is out of
reach, and the badge reads pending until the job runs again.
A badge reports the runs that the node serving it has on record, as that
node's dashboard does. In a cluster, point
a job's badge at the node that runs the job, not at a load balancer in front
of several nodes. The fleet badge counts the jobs as that node sees them. On
a gossip cluster, or a lease cluster with cluster.observability,
GET /fleet has the cluster-wide view.
The fleet badge counts the jobs whose own badges show the most urgent of three messages:
-
failing, in red, such as2 failing. -
overdue, in orange, when no job is failing. -
passing, in green, when no job is failing or overdue.
The fleet badge reads no status when no job shows any of the three, for
example before any job has run. It reads no jobs when the configuration
has none.
| Parameter | Default | Meaning |
|---|---|---|
label |
The job's name, or cronstable on the fleet badge |
The text of the badge's left half: 1 to 64 characters, with no control characters or line breaks. |
style |
flat |
flat draws rounded corners and a soft gradient. flat-square draws square corners with no gradient. |
A label that is blank, too long, or contains a control character or a
line break gets a 400 whose JSON error body names the problem, and so
does a style outside those two. An empty style takes the default. A
label made only of spaces and zero-width characters, such as a zero-width
space or a combining mark, counts as blank.
curl "http://localhost:8080/jobs/nightly-backup/badge.svg?label=nightly%20backup&style=flat-square"The web dashboard's command palette lists
Copy badge Markdown for each job, and Copy fleet badge Markdown. The
terminal dashboard's palette has the same commands.
The palettes list them only when the daemon serves badges, which they read
from GET /whoami about once a minute, so the
commands follow a configuration reload. No URL reaches the badge of a job
whose name is empty, ., or .., or contains { or }, so the palettes
leave out that job's Copy badge Markdown. Each command copies Markdown
that shows the badge and links it to the dashboard. Unless the badges are
public, the confirmation adds "embeds need web.badges.public" (see
make badges public). A job's badge links to the
job's drawer:
[](https://cron.example.com/#job/nightly-backup)The image's alt text is the job's name, with a space in place of each line
break. A backslash escapes each character in the name that Markdown would
read as syntax, such as a bracket, a backtick, or |. The Markdown uses the
address that the dashboard connects to, without any user name or password in
it. If readers reach the daemon at another address, such as a reverse proxy,
change the host before you publish the Markdown. On a daemon that requires a
token, the link opens the
token prompt first, and the job's drawer
opens after the reader saves the token. A daemon with web.ui: false
serves no dashboard page, so the dashboards copy its badges without the
link.
A page that accepts HTML can use an image tag instead:
<a href="https://cron.example.com/"><img src="https://cron.example.com/badge.svg" alt="cronstable status"></a>Pages fetch embedded images without your bearer token. GitHub fetches README images through its image proxy, and a wiki or a home dashboard fetches them from each reader's browser. On a daemon that requires a token, both fetches fail.
Even when the daemon requires no token, a browser refuses to load a badge
from the daemon into a page from another origin. The badge's
Cross-Origin-Resource-Policy: same-origin header blocks it as an image,
and its frame-ancestors 'self' policy blocks it in a frame, such as an
<object>, <embed>, or <iframe>. Without these headers, any website
that a reader visits could load a badge and tell the job's verdict from
its size. GitHub's image proxy fetches the badge from GitHub's servers and
serves it with its own headers, which leave out
Cross-Origin-Resource-Policy, so on a daemon that requires no token, a
badge in a GitHub README shows without web.badges.public.
Set web.badges.public to lift both limits:
web:
listen:
- http://0.0.0.0:8080
authToken:
fromEnvVar: CRONSTABLE_WEB_TOKEN
badges:
public: trueThe setting exempts only the two badge routes from the token, and their
headers then let pages on any origin load them as images or in frames (see
security headers). A public badge shows a job's name
and verdict. Anyone who can reach the listener, and any website open in a
browser that can reach it, can request the badge of any job name, learn
from a 404 whether a job by that name exists, and read the verdict of a
job that does. Leave public off when job names or their health are
sensitive.
GitHub's image proxy fetches from the internet, so a badge in a GitHub
README also needs an address that the internet can reach, such as a reverse
proxy in front of the daemon. An https:// listener with web.tls.clientCa
requires a client certificate, which GitHub's image proxy can't present, so
serve the badges through a listener or a reverse proxy that doesn't require
one.
The view scope covers both badge routes, so a daemon that serves a
public read-only board
through web.anonymousScopes serves its badges without a token. A page
from another origin can load them straight from the daemon only with
web.badges.public.
Each badge image carries an ETag, a hash of the image's bytes, and
Cache-Control: no-cache. The no-cache directive tells a cache, such as
the image proxy that GitHub uses for README images, to check with the daemon
before it reuses a stored copy. While the badge is unchanged, that check gets
304 Not Modified and no image data. The daemon always sets these two
headers itself, so a web.headers entry can't replace them.
While nothing changes, the daemon reuses one count of the fleet badge's jobs for up to a second, however often pages request it. A run that starts or finishes, a pause that you set or lift, or a configuration reload makes the next request count again. A pause that runs out, or a new SLA breach, shows within a second.
The web.badges option takes a boolean or a map:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Serve both badge routes. |
public |
bool | false |
Serve both badge routes without the bearer token, and let pages from other origins load the badges. |
badges: true is the same as the default. With badges: false, the daemon
doesn't register the badge routes, so a request for a badge gets the same
answer as a request for any unknown path.
A browser that opens a badge URL directly renders the image as a document of
the daemon's origin. The daemon escapes all of the text in the image, and
every badge image, including a 304 reply, carries these headers:
| Header | Default | Public badges |
|---|---|---|
Content-Security-Policy |
default-src 'none'; frame-ancestors 'self' |
default-src 'none' |
X-Content-Type-Options |
nosniff |
nosniff |
X-Frame-Options |
SAMEORIGIN |
Not sent |
Cross-Origin-Resource-Policy |
same-origin |
cross-origin |
By default, Cross-Origin-Resource-Policy stops a page from another origin
from loading a badge as an image, and frame-ancestors stops it from
framing one. X-Frame-Options still stops the frame when a
Content-Security-Policy entry in web.headers leaves out
frame-ancestors. A web.headers entry with the same name, in any letter
case, replaces a default, as on the dashboard page. The entries apply to
public badges too.
See also: HTTP Control API, Web Dashboard, Late-Run Detection (SLA), and Calendar Export (iCal).
This wiki documents cronstable. See the README and the changelog.
cronstable is a fork of gjcarneiro/yacron.
cronstable™ and the cronstable logo are trademarks of Parker Loflin. The code is MIT-licensed. See TRADEMARKS.md and LICENSE.
- Getting Started
- Configuration
- Job Behavior
-
Integrations
- Reporting (Mail, Sentry, Shell, Webhook, Push, Event Log)
- Push Notifications
- Pairing from the Terminal
- Windows Event Log
- Metrics with Prometheus
- Metrics with statsd
- HTTP Control API
- LAN Discovery (Bonjour/mDNS)
- Listener TLS
- Calendar Export (iCal)
- Status Badges
- Schedule Pressure
- Duplicate Schedule Detection
- Suggest a Slot
- Why Didn't It Run?
- Web Dashboard
- Terminal Dashboard
- MCP Server (Model Context Protocol)
- Reference and Development