Repository navigation
Releases: uplg/hora
Release list
v0.11.4
A security release for two advisories published on 2026-10-21 and
2026-10-23. Hora's scheduler, supervisor, maintenance and certificate tasks
all tick on tokio::time::Interval, so the Tokio one applies directly. The
minimum Rust version is now declared. No other change.
Security
- tokio 1.54.1. Fixes RUSTSEC-2026-0384: on the multi-threaded runtime,
a race betweenInterval::poll_tickand the time driver could drop a timer
entry the driver was firing, a use-after-free that showed up as assertion
panics, corrupted task reference counts or heap corruption. - ctutils 0.4.6. Fixes RUSTSEC-2026-0385: its mask generation and
u128
comparisons, meant to be constant-time, could compile to branches. The other
dependencies take their compatible patch releases (webpki-roots 1.0.11,
among others).
Changed
- Minimum supported Rust version: 1.99.1, declared as
rust-versionin
the workspace manifest. Building from source with an older toolchain now
fails up front instead of somewhere in the dependency tree. The Docker image
is unaffected.
v0.11.3
A security release for three certificate advisories published on
2026-10-19 and 2026-10-20, plus an unmaintained crate gone. Hora checks
certificates with rustls-webpki on its outbound TLS (HTTPS and TLS probes,
notification channels, the release check). No other change.
Security
- rustls-webpki 0.103.16, webpki-roots and webpki-root-certs 1.0.10.
Fixes RUSTSEC-2026-0382 (certificate paths through the distrusted emSign
intermediates, whose private keys are now well known, could be accepted),
RUSTSEC-2026-0381 and RUSTSEC-2026-0383 (the bundled roots still trusted
emSign ECC Root CA - G3, the anchor of those intermediates). The other
dependencies take their compatible patch releases (tokio 1.54.0, among
others). - basic-toml is gone (RUSTSEC-2026-0374, unmaintained). Askama pulled it
in only to read anaskama.toml, which Hora does not have: itsconfig
feature is off, the templates render the same.
v0.11.2
A security release: rustls moves to 0.23.46, which fixes four advisories
published on 2026-10-14. Hora uses rustls for its outbound TLS (HTTPS and
TLS probes, notification channels, the release check). No other change.
Security
- rustls 0.23.46. Fixes RUSTSEC-2026-0346 (
Debugoutput of client
session values could expose TLS secrets), RUSTSEC-2026-0347 (buffered
data sent to the outer server after an ECH rejection), RUSTSEC-2026-0348
(Brotli certificate decompression could allocate up to 1 GiB before
authentication) and RUSTSEC-2026-0349 (an unauthenticated ServerHello
could downgrade later connections to classical key exchange). The other
dependencies take their compatible patch releases (tokio 1.53.3, hyper
1.12, aws-lc-rs 1.18.2, among others).
v0.11.1
Your logo on your status page, and word of new releases. The header can
show your own mark beside the title, one per theme, and Hora now tells
you when a newer version is out: on your status page, for your eyes only,
and once through your channels, with what it brings.
Added
- Your logo in the header.
[page] logo = "logo.svg"shows your own
mark beside the title, in place of its initial tile, on every page and
on the monthly report. SVG, PNG, WebP or JPEG, read with the config and
served at/logo;logo_darkadds the dark theme's version, which shows
whenever the page is dark, chosen by the system or by?theme=dark. - Hear of new Hora releases. Once a day Hora asks GitHub for its latest
release; a newer one shows on the status page to the operator (version,
the opening of its changelog, links to the release and upgrade notes) and
is sent once through the channels.[updates] check = falseturns it
off,notifypicks the channels ([]: the banner alone). Release-watch
messages carry the notes' opening too (messagein the webhook payload).
v0.11.0
A new face and a hardening release. Hora gets its brand (the night watch,
a little owl that dozes while all is well) and a status page redesigned
around it; the findings of a full audit are fixed; and the page stays
instant on huge databases: measured on 575 million checks across 700
monitors, the status page answers in 1.5 ms at p99 (it took 13 s in
0.10.0), weighs 71 KB instead of 6.5 MB, and the daemon peaks at 353 MB of
RAM instead of 887 MB. See UPGRADES.md for what to
check before upgrading.
Added
-
A redesigned status page. The hour and one sentence ("Everything is
running."), count chips, then what needs attention first; groups that are
all well fold away, large groups show compact rows, and each monitor's
daily bar is one image instead of ninety elements. Every state reads as a
colour, a shape and a word, never colour alone. Light and dark follow the
system,?theme=light|darkforces one (handy for embeds and kiosks).
Down monitors say since when ("Down · 38 min"), rows show uptime over the
bar's days, maintenance days are marked, and recent incidents and a
"No incident in N days" chip sit under the groups. -
/monitor/{id}: a page per monitor with its figures, the 24-hour
chart, a day-by-day bar whose details open by keyboard or tap, the
hour-by-day latency heatmap (moved here from/history), what each
vantage point sees, and its incidents. -
/watchers(operator only): the mesh from this node, its peers'
heartbeats, how often outages were confirmed, and each shared service as
seen from each place. -
"Not an outage." A down seen from this node only, while every peer
reaches the target, shows as up with a notice explaining why nobody was
paged. The JSON API is unchanged. -
alerts.notify_unconfirmed: the channels that receive a local-only
down (see Changed), validated like anynotifyroute. The webhook payload
gainslocal_only(bool) ondownandrecoveredevents. -
A certificate that cannot be read alerts (
cert_unreadable: TLS
certificate could not be read, with the error), once per streak and not
while the monitor is down. It used to be a log line, and the expiry of a
server whose STARTTLS dialogue or TLS versions changed silently stopped
being watched. -
The HTTP probe decodes
gzip,br,deflateandzstdbodies
before the assertions run (a CDN that always compresses failed every
keyword check).max_body_kbcaps the decoded size. -
Kiosk refresh is opt-in:
?refresh=<secs>(10 to 3600) reloads the
page; without it the page no longer reloads itself every 30 seconds,
which closed open groups and disturbed screen readers. -
hora compactgives the file's free pages back to the disk: applies
the retention now (--purge-removedskips the 7-day grace), rewrites the
file withVACUUM INTOand swaps it in atomically.--dry-runmeasures
every table and index and the gain. It needs the daemon stopped: the
daemon now holds<db>.lock, and CLI commands hold<db>.writers.lock
so none of them writes into a file being replaced.hora topshows the
database size and suggests a compaction past a fifth of free pages; the
operator/api/summaryreportsstorage { db_bytes, reclaimable_bytes }. -
Add to home screen: a web app manifest and icons, so a phone can keep
the status page as an app (no service worker: always the live page). -
The monthly report gets a day strip per service and shorter headings, and
is cached per month and audience. -
History, timeline, post-mortem, the empty and private-page states and the
plain-text (curl) output share the new design and wording. -
The documentation site wears the brand, with new pages: When Hora fits,
Concepts (how a failed check becomes one alert, or none), The status page,
Brand, Development, and the changelog. -
/api/announcetakesuntilas a time of day (18:00, UTC, the next
occurrence) as well as a duration (4h), likehora announce; the hint
inhora topis now true. -
Silence a watched peer:
POST /api/silenceandhora silenceaccept a
peer'slisten_id, and ad-hoc silences now mute peer-watch alerts too. -
Degraded alerts say why: a push with
status=degraded&msg=...or an
exec plugin's WARNING output is carried into the alert (and into the
webhook'smessagefield, now set ondegradedevents). -
hora_monitor_status{id,name,status}Prometheus gauge, set for every
monitor including the ones whose status is still unknown. -
Permissions-PolicyandCross-Origin-Opener-Policyresponse headers. -
hora event -- <title>andhora announce -- <title>record a title that
starts with a subcommand word (hora announce clear skiesused to clear
every banner). -
just deps: a report-only freshness check across crates, docs packages,
pinned Actions and base images.
Changed
- Local-only downs no longer page. With
confirm_with_peers, a down
that every peer that answered sees up used to be sent to the monitor's
channels anyway, softened. It now goes toalerts.notify_unconfirmed(or
is only recorded: incident, timeline, Not an outage). Only that explicit
contradiction quiets a down: no peer, no answer, or any peer seeing it
down alert as before. The peers are asked again every 5 minutes while it
lasts; once they no longer all see it up, the real down goes out, once.
A recovery goes to whoever received a down, across a daemon restart too
(the routing is kept on the incident, migration0022, instant). The
operator panel Why nobody was woken says what the config does with such
a down. - Probe failures name their cause. connection failed: connection
refused, connection failed: TLS invalid peer certificate: Expired
instead of a bare connection failed; proxy failed: tunnel error: proxy
authorization required (HTTP 407) or proxy failed: SOCKS error:
credentials not accepted instead of blaming the target; domain does not
exist (NXDOMAIN) apart from no A records found, and server answered
Server Failure (rcode 2) instead of hickory's debug text; an assertion
judged on a body cut atmax_body_kbsays (body cut at 256 KiB). A
push to an unknown id, a cron grace of 20 seconds (+ 20s grace, was
+ 0m grace) and an ICMP error (icmp destination unreachable (code 1)
from 192.0.2.1) read as what they are. - An https monitor redirected to plain http is down (redirected to
plain http, public reason of the same words): following it carried the
session in clear text without a word. - TCP and certificate connects try the next address after 250 ms
(happy-eyeballs): a dual-stack name whose IPv6 address blackholes no
longer times out while IPv4 answers. - A custom
dns_resolverfalls back to TCP for answers too large for a
datagram (long TXT, SPF, DKIM records), which used to fail as truncated. - A proxied monitor's certificate is read through its proxy (HTTP
CONNECT,socks5,socks5h), like its probe;hora probedoes the
same. It was read by a direct dial around the proxy. - Certificate warnings count hours under a day (expires in 10 hours;
hora probeprints 10 hours left): with less than a day left they said
has expired. POST /api/push/{id}answers 401 for an unknown id, like a wrong
token (it answered 404, which listed the push ids).- An announcement's
untilis at most a year (365d); a longer one is
refused (400, CLI error). - A confirmation peer that does not watch the target (it answers 404) is no
longer counted as unreachable: the verdict reads no peer watches this
target, unconfirmed, and/watchersno longer counts it under No
answer. - A peer that misses a vantage poll keeps its last view for three rounds:
one lost poll no longer makes its column (and the Not an outage notice)
vanish for a minute. - The status summary is never built on a request. A background task
rebuilds it continuously (about 150 ms on 575M checks, was 13-20 s) and
every page,/api/summary,/metrics, group page and peer exchange is
served from the latest snapshot, at most a few seconds old. It is built
once for the operator and cut in memory for the public and each group;
pages are rendered once per snapshot and audience. Only the first request
after start waits for the first build (logged as "status page ready").
Announcing, clearing banners or recording an event returns once the page
shows it. - 24-hour figures read the hourly roll-ups plus the current hour, not
millions of raw checks, and hourly roll-ups now run every 5 minutes
instead of every 6 hours. p50/p95/p99 come from per-hour latency
histograms: exact below 64 ms, within 1.6% above. - Push heartbeats drive alerting with their own verdict. A push with
status=downmakes the monitor down on the next tick (confirmed after
fail_threshold), with the job'smsgas the reason;status=degraded
counts as degraded, soalert_on_degradedworks for push monitors. It
used to be read as a heartbeat like any other, so the alert came one
interval late, as a missed heartbeat, without the message. - A push monitor or watched peer that never sent a heartbeat alerts
("no heartbeat received yet") once its interval (or its first scheduled
run plus grace) has passed since Hora first watched it - across restarts.
It used to stay grey forever. - Alert state survives edits and restarts. Editing a monitor or peer no
longer re-sends a down for an ongoing outage, nor loses its recovery; a
daemon restart mid-outage does not announce the down again (it is seeded
from the open incident) and still sends the recovery. Monitor tasks that
die are restarted and logged. - Notifications are worded once for every channel. The ten channels
render from one neutral message and only add their own markup and...
v0.10.0
Added
- Upstream release watch (
release = { github = "owner/repo", ... }per
monitor): twice a day Hora asks GitHub for the project's latest release
(no draft, no prerelease) and alerts, once per release and across restarts,
when it is newer than the version that runs - "matrix-construct/tuwunel
v1.9.2 is out (running v1.9.1)", with the link to the notes, through the
monitor'snotifyrouting and muted during its maintenance. The running
version is a literal (current) or, so it cannot drift from what is
deployed, asked of the service itself (current_url, pluscurrent_query,
a JSONPath, when the answer is JSON). Newrelease_availablewebhook event
(project,current,latest,url); schema migration0018
(release_watch) applies automatically. As with every new key, a config
that usesreleaseis rejected by an older binary (deny_unknown_fields).
v0.9.6
Fixed
- Peer polls no longer rebuild the status page.
GET /api/peer/monitors,
which every mesh peer's vantage poller calls once a minute, built the whole
authenticated summary (90 days of daily bars, percentiles, sparklines) to
read two fields per monitor. The summary cache lives 5 seconds, so every
poll paid for a full rebuild: on a node with months of history on 2 vCPU,
about one second of CPU a minute, and a steady stream of sqlx
slow statementwarnings once the host was busy. The endpoint now reads
only what it answers with (the recent checks behindstatus, the 24h
percentiles behindp50_ms), with the same values the summary reports.
Changed
- Hourly roll-up follows the clock. Raw checks used to be rolled up
intochecks_hourlyonly once 7 days old, so the status page's daily bars
re-aggregated the last 9 days of raw checks on every rebuild (0.72 s over
270k checks). Every ended hour is now rolled up (5-minute margin, on the
existing maintenance tick), anddaily_allreads the hourly buckets plus
only the raw checks above the newest bucket, a few hours at most: about
0.08 s on the same data. Both reads share one frontier, so they add up
without overlap even when a roll-up commits between them. Days are also
grouped as integer UTC day numbers instead of a per-rowstrftime
string. Monitors whoseretention_daysis under 7 now keep hourly
history too, since their raw checks roll up before retention prunes them.
v0.9.5
Fixed
- STARTTLS certificate checks on port 25: the SMTP negotiation announced
EHLO hora, a bare label that RFC 5321 does not allow. MX servers enforce
the rule on port 25 (Stalwart answers550 5.5.0 Invalid EHLO domain), so
the certificate of astarttls = "smtp"monitor on an MX was never read,
and no expiry or pin alert could fire for it. Hora now announces the
address literal of its end of the connection ([192.0.2.10],
[IPv6:2001:db8::a]), which is valid everywhere, including in a
container whose hostname is a bare id.
Added
ehlo_nameonstarttls = "smtp"monitors: the identity to announce
inEHLOinstead of the address literal. Validated at load as a fully
qualified domain or an RFC 5321 address literal.
Changed
croner4.0 (pushscheduleand[digest]crons). croner 4 rejects
the shorthand steps 3.x accepted (5/5,/10); Hora parses with
sloppy_rangeson, so existing schedules keep loading unchanged. Also
syn3.0.6,rustix1.1.5,cfg-if1.0.5 andunicode-ident1.0.26.
yoke-deriveandzerofrom-derivestay on 0.8.2 / 0.1.7 (second
synstructure, as in 0.9.4).- Alpine 3.24 runtime image (was 3.23).
- Docs site:
astro7.3.3,@astrojs/starlight0.42.1.
v0.9.4
Fixed
- Matrix notifications on spec-strict homeservers (Tuwunel, Conduit,
conduwuit): messages went out asPOST …/rooms/{id}/send/m.room.message
without a transaction id, a Synapse leniency that those servers answer
with404 M_UNRECOGNIZED, so every Matrix delivery failed. The notifier
now uses the spec endpoint,PUT …/send/m.room.message/{txnId}, with an
id unique per notification and reused across retries: a homeserver that
stored the message but lost the response deduplicates instead of posting
it twice.
Changed
- Dependency refresh, TLS stack first:
rustls0.23.45,tokio-rustls
0.26.5,aws-lc-rs1.18.1 (aws-lc-sys0.45),reqwest0.13.5, plus
hickory0.26.3,askama0.16.1,toml1.1.6,tower-http0.7.1 and
the rest of the lockfile.yoke-deriveandzerofrom-derivestay on
0.8.2 / 0.1.7: their newer patches pull a secondsynstructure, which the
duplicate-version ban rejects. Thewit-bindgencargo-deny skip is gone
(one version left).cargo auditandcargo denyare clean.
v0.9.3
Added
- Badge styles: the status and uptime badges accept
?style=flat-square
and?style=for-the-badge(the default staysflat). Contributed by
@vladkens.
Changed
- Badge rendering is delegated to
badgelibinstead of the built-in
renderer; badges are slightly more compact thanks to accurate text-width
measurement. Behaviour is otherwise unchanged - same colors, same 404 for
an unknown or private monitor.
Security
- Dependency refresh:
h20.4.19 clears RUSTSEC-2026-0258 (unbounded
queueing of empty DATA frames, low severity) and the yankedchacha20
0.10.1 moves to 0.10.2.