@@ -5,7 +5,83 @@ All notable changes to this project are documented in this file.
55The format is based on [ Keep a Changelog] ( https://keepachangelog.com/en/1.1.0/ ) ,
66and this project adheres to [ Semantic Versioning] ( https://semver.org/spec/v2.0.0.html ) .
77
8- ## [ Unreleased]
8+ ## [ 0.4.0] - 2026-06-09
9+
10+ ### Added
11+
12+ - ** Availability SLOs, error budgets and burn-rate alerts** : ` slo_uptime = 99.9 `
13+ (+ optional ` slo_window_days ` , default 30) per monitor. The status page and
14+ ` /api/summary ` show the error budget left over the window (computed from the
15+ same merged daily history as the bars, so it survives raw retention); alerts
16+ are Google-SRE multi-window burn rates - fast (2% of budget in 1h, confirmed
17+ over 5m) and slow (5% in 6h, confirmed over 30m) - via a new ` budget_burn `
18+ event on every notification channel, with an exhaustion ETA. Edge-triggered:
19+ one alert per episode, re-armed when the long window cools.
20+ - ** Cron-aware push monitors** : ` schedule = "0 3 * * *" ` (five-field cron, UTC)
21+ plus ` grace_secs ` (default 1800) on a push monitor alerts only when a
22+ scheduled run misses its grace window, instead of the fixed
23+ ` interval_secs ` gap - made for nightly jobs, à la Healthchecks.io.
24+ - ** Root-cause alert grouping** (` alerts.group_window_secs ` , default 30):
25+ a monitor confirmed down whose ` depends_on ` upstream is also down waits out
26+ the window; if the upstream alerts (or already has), the dependent's alert -
27+ and its later recovery - fold into that single notification, transitively
28+ along dependency chains. A flap inside the window sends nothing. Incident
29+ records are unaffected (history stays complete). Set 0 to disable.
30+ - ** Uptime Kuma import** now also maps ` json-query ` monitors (JSONPath +
31+ expected value), request ` headers ` , ` timeout ` , single expected status codes,
32+ ` expiryNotification = false ` (→ ` check_cert = false ` ) and Kuma groups:
33+ monitors under a Kuma folder get ` group = "<folder name>" ` . Both current and
34+ legacy Kuma field spellings (` maxretries ` , ` accepted_statuscodes ` ,
35+ ` dns_resolve_type ` ) are accepted.
36+ - ** DNS monitors** (` kind = "dns" ` ): resolve a hostname (A, AAAA, CNAME, MX, NS,
37+ TXT, SRV, SOA or PTR via ` dns_record ` , system or custom ` dns_resolver ` ) and
38+ optionally ** pin the expected answer** with ` dns_expected ` (comma-separated,
39+ order-insensitive - hijack detection that does not flap on round-robin
40+ rotation). Without ` dns_expected ` , any non-empty answer counts as up.
41+ - ** TLS certificate pinning** (` cert_pin ` , hex SHA-256 of the leaf public key):
42+ a fingerprint matching neither the pin nor the last seen value fires a
43+ ` CertChanged ` alert - once per change, surviving restarts, muted during
44+ maintenance windows like other alerts. The alert carries the old and new
45+ fingerprints, so a first mismatch also tells you the correct pin to configure.
46+ - ** Automatic incident history** : confirmed down/up transitions are recorded as
47+ incidents (start, end, duration, error, root cause and blast radius), served
48+ on ` /history ` (server-rendered, no JS) and as an ** Atom feed** at
49+ ` /history.atom ` . Incidents survive restarts: a still-open incident is
50+ re-attached on startup and closed on the first healthy tick. Closed incidents
51+ are pruned after a year.
52+ - ** Prometheus ` /metrics ` ** (text exposition format): ` hora_monitor_up ` ,
53+ ` hora_monitor_degraded ` , ` hora_monitor_uptime_ratio ` (24h),
54+ ` hora_monitor_last_latency_ms ` , ` hora_monitor_latency_ms{quantile=…} `
55+ (p50/p95/p99) and ` hora_cert_expiry_days ` , all labelled ` {id, name} ` .
56+ - ** Private monitors** : ` public = false ` hides a monitor from the
57+ unauthenticated status page, ` /api/summary ` , latency API, badges, ` /metrics `
58+ and the incident history. A viewer token (` server.auth_token ` , live-reloaded,
59+ sent as ` Authorization: Bearer ` or ` ?token= ` ) reveals the full view; both
60+ views are cached. Config validation rejects ` public = false ` without a token.
61+ - ** Plain-text status for terminals** : ` curl status.example.com ` (or an
62+ ` Accept: text/plain ` request) returns an aligned text rendering of the status
63+ page, groups, topology annotations and peers included.
64+ - ** Long-term downsampling** : raw checks roll up into hourly buckets after
65+ 7 days and daily buckets after 90, kept for a year. Buckets are written once
66+ (never recomputed from partially-pruned raw data) and the daily uptime bars
67+ transparently read them beyond the raw retention window. Aggregates of
68+ removed monitors are swept like any other orphan.
69+ - ** ntfy, Gotify and Pushover** notification channels (`type = "ntfy" |
70+ "gotify" | "pushover"`), with the shared retry/redaction policy; the Gotify
71+ token travels as a header, never in the URL.
72+ - ** ` hora check ` ** : validate the configuration and exit non-zero on error
73+ (CI-friendly), and ** ` hora import kuma <backup.json> ` ** : convert an Uptime
74+ Kuma backup to Hora TOML on stdout (http/keyword, port, ping, dns and push
75+ monitors; anything else becomes a commented stub). Plus ` --version ` /` -V ` .
76+
77+ ### Changed
78+
79+ - Down alerts of monitors whose ` depends_on ` upstream is also down now wait
80+ out the grouping window (up to ` alerts.group_window_secs ` , default 30s)
81+ before being sent - or folded. Root-cause alerts are unaffected. Set
82+ ` group_window_secs = 0 ` for the previous one-alert-per-monitor behaviour.
83+ - ` /api/monitors/{id}/latency ` answers 404 for private monitors without the
84+ viewer token, exactly as for unknown ids.
985
1086## [ 0.3.0] - 2026-06-08
1187
0 commit comments