1.2.2
-
New webhook reporter: native Slack/Discord/Teams/ntfy notifications. A
fourth reporter joins sentry/mail/shell in everyreportblock:webhook
sends an HTTP request (POST by default) to a configured URL with a
jinja2-templated body. The default body is a{"text": ...}JSON payload
carrying the same subject-plus-body text as the default mail/sentry
templates, JSON-encoded with jinja2'stojsonfilter so quotes, newlines,
and non-ASCII job output always produce valid JSON -- pointurlat a
Slack, Mattermost, or Teams incoming webhook and it works with no further
configuration.method,contentType,headers,body, andtimeout
cover everything else (Discord's{"content": ...}shape, ntfy's
plain-text body and header-driven priority, or your own endpoint). The URL
resolves like the sentry DSN (value/fromFile/fromEnvVar) and is
treated as a secret throughout: it is never logged, and the job-set
fingerprint redacts the inline URL value and all header values (which
commonly carryAuthorizationtokens). No new dependency -- outbound
delivery rides the core aiohttp. Note: because every job's effective
config gains the new default block, job-set ids change on upgrade;
replicas must be on the same version to compare ids, as before. -
Unchanged peers now answer gossip polls with a bodyless
304. Every
/peerresponse carries a strongETag(a content hash of the payload),
and each polling node echoes the tag of the last full body a peer served
it back asIf-None-Match, so a peer whose state has not changed since
then skips re-sending the full O(members + jobs) JSON: a converged, idle
cluster's steady-state round costs headers rather than bodies. This is a
transport optimization, not a protocol change. A304is still a fresh,
mutually-authenticated round trip, and because the tag is content-derived,
a match proves the peer's payload is exactly the one the poller already
holds, so the poller replays its cached observation and every gate (mutual
agreement, conflict detection, thecluster.driftAfterdebounce) advances
exactly as if the identical body had been re-sent. The one live field, a
job's seconds-to-next-fire countdown, is hashed as the absolute next-fire
time instead, so the tag stays stable between fires and rolls exactly when
a schedule fires. Mixed fleets degrade safely during a rolling upgrade: an
older peer ignoresIf-None-Matchand keeps serving full bodies, a
tagless response stops the poller from sending the header at all, an
unsolicited304is recorded as a failed poll, and an over-long or
non-printable tag is never stored or echoed. -
Fleet-view countdowns are aged, not frozen. With
304rounds
refreshing a peer's liveness without re-shipping its job summaries, a
stored snapshot can now legitimately outlive many polling rounds, so
GET /fleetre-derives each peer job's advertisedscheduled_in
countdown from the snapshot's age (an elapsed duration measured on the
local clock alone, so peer clock offsets never leak in) instead of serving
the value the snapshot arrived with, clamping at zero. The fire itself
rolls the peer'sETag, so the next poll ships a full body carrying the
real successor value. -
/peerbodies that do go out are gzip-compressed once they reach
1 KiB (below that, the per-request CPU spend outweighs the few bytes
saved). The polling side already advertised gzip support, and the existing
response-size cap applies to the decompressed payload, so compression
does not weaken it. -
Dashboard: the version and job-set id header chips copy their value on
click. Header text is chrome and is no longer text-selectable; the two
values worth grabbing hand themselves out instead. Clicking the version
chip copies the version, and clicking the job-set chip copies the full
job-set id even though the header shows only a short prefix; both
tooltips say so. The command palette carries the same two copies, so both
values stay reachable from the keyboard. -
Dashboard: the quick "power-on sweep" flash between boots is gone. The
full POST boot screen still replays once its cooldown elapses, but the
visits in between now start the app directly instead of playing a
full-screen power-on animation first. -
Internal: the conditional exchange ships with a matching batch of cluster
tests (tag stability across countdown ticks and rollover on a fire, the
304replay path, unsolicited-304and unusable-tag rejection, countdown
aging, and an end-to-end mutual-TLS304-plus-gzip round), and the wiki's
Architecture and Internals page documents the exchange.
Full Changelog: 1.2.1...1.2.2