Since RTT history keys make host.plugin_data non-empty after the very
first heartbeat, the "no hosts" empty state can now only trigger when
zero hosts have ever connected, not when hosts exist but haven't sent
plugin metrics yet (that in-between state no longer exists). Update
the copy to describe the actual current trigger condition.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEimzMv4Q5EjFg3hoiZ69T
host.plugin_data now always holds rtt_ipv4/rtt_ipv6 history after the
first heartbeat, which broke two pre-existing behaviors that assumed
plugin_data reflects only real client-collected data:
- The request_update ACK gate in handle_datagram() never fired again
after a host's first heartbeat, so stale OS/agent-version info was
never refreshed after a reconnect (only a client restart fixed it).
- plugin_data.clear() on every UP transition wiped rtt_* history too,
so a flaky host could never accumulate a useful RTT graph.
Add _is_rtt_key()/_has_real_plugin_data() helpers so both spots treat
rtt_* keys as synthetic: the gate now looks only at real plugin data,
and the recovery clear only drops real plugin keys, preserving RTT
history across reconnects.
Also fixes two pre-existing E127 continuation-indent flake8 issues in
tests/test_udp_rtt_history.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEimzMv4Q5EjFg3hoiZ69T
Add fetchHostInfo() call to the 30-second auto-refresh setInterval loop
for expanded host cards. This ensures the Connectivity table and RTT
charts update in real-time without requiring the user to collapse and
re-expand the host card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEimzMv4Q5EjFg3hoiZ69T
Pulls the CPU chart's SVG-drawing logic into a parameterized
renderTimeSeriesChart(elId, pts, opts) helper (yDomain, colorFor,
clipId, unitSuffix), keeping renderCpuChart's signature and behavior
unchanged. Task 5 will reuse this for the RTT chart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEimzMv4Q5EjFg3hoiZ69T
Capture RTT on every heartbeat using the existing plugin_data history
mechanism, recording under rtt_ipv4 or rtt_ipv6 keys. Maintains 100-sample
retention per host and address family.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEimzMv4Q5EjFg3hoiZ69T
flap._state persists at module level keyed by (host, service) and only
clears via a RECOVER-triggered quiet window. A host dropped mid-flap with
no RECOVER ever received leaves ok_since permanently None, so the
flapping flag could never clear on its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the 11-column table with severity-sorted record rows: host,
combined status chip, latency, alert chips, time-in-state, and the most
recent alert message. Per-family address/state/latency/last-change move
to a hover card on the status chip and permanently to a new Connectivity
table in the Host Overview info section (host info API now includes
connections). Last-alert is seeded from /api/0/alerts and kept fresh
from the event stream; the event log below uses the same row idiom with
its filters intact. WS reconnect state shows in the toolbar instead of
a modal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
Extract the settings redesign's tokens and components into shared
static/hbd-ui.css + hbd-ui.js and dedupe settings.html against them.
About becomes yaml-key sections with kv rows; Alerts gets stat tiles,
chip filters, and alert record rows (same fetch/ack logic); Host
Overview keeps its DOM and live-update JS but is re-skinned to the
token system with a page toolbar. Live Dashboard intentionally
untouched pending its own redesign.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
Sticky offsets (toolbar, rail, mobile chips, anchor margins) derive from
the measured nav/toolbar heights via CSS variables, since the fixed nav's
height varies with viewport width.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
One idiom for every section: mono identifier, badges, fact chips, inline
accordion editor. Scalar config sections merge into one grouped 'server'
section. Sidebar becomes YAML-key nav (mobile: horizontal chips); sticky
toolbar carries pending-changes state and Publish. Staging, publish,
channel CRUD, and permission gating are unchanged — same payload shapes
and endpoints.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
The users-section loop reused 'username' as its loop variable, overwriting
the requesting user's name so host/threshold filtering compared against the
last user in the config. Rename to 'uname' and add a regression test with a
user that is not last in the users dict.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
Setting args.boot = False after sending the boot message was dead code: the
SIGHUP restart re-execs via os.execv(sys.argv[0], sys.argv) with the original
argv, so the re-launched process re-parsed -b and announced a boot again. The
exiting process also sent a spurious shutdown (send_shutdown armed by -b), so
each config reload looked like a host reboot to the server.
Strip -b/--boot from the argv passed to execv, and skip the shutdown message
in cleanup() when dorestart is set. A real SIGTERM/SIGINT shutdown still sends
shutdown as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server inferred each plugin's collection interval from the gap between
the last two received PLG samples, then expired data at interval * 3. After
an outage this guess was wrong: the request_update re-send of collect-once
InfoPlugins produced two close samples, yielding a tiny inferred interval
that purged permanent info data minutes after recovery.
Clients now declare each plugin's interval in the PLG message (_interval).
The server uses it directly (from the first post-recovery sample), expiring
at interval * 3 so live data survives at least two full intervals; interval
0 marks collect-once InfoPlugins that never go stale. Legacy clients omit
the field and fall back to the previous inferred-gap behavior.
Adds _interval to all three clients: hbc, hbc_mini.py, hbc_mini.c.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
data.msgs grew without bound (eventlog appended forever, also persisted in
the pickle), and ws.py replayed the entire history to every dashboard
client on connect - one JSON frame per message - making page reloads
progressively sluggish. Cap the buffer in eventlog() (configurable via
msg_buffer_size, default 500) and slice the WebSocket replay to the last
30, matching the window the HTTP render already uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Host.plugin_timers holds asyncio TimerHandle objects (and their lambda
callbacks) which are not picklable, causing the 5-minute state save to
fail with "Can't pickle local object reset_plugin_timer.<locals>.<lambda>".
Add Host.__getstate__ to reset plugin_timers to {} before pickling,
mirroring Connection.__getstate__; timers are recreated on the next PLG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Use `is not None` for pathconf values so 0 is not silently dropped
- Broaden connectivity prefix check to catch bare "connectivity" key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Y axis now fits the actual data range with 10% padding rather than
fixed 0-100%. Grid lines use nice tick steps (1/2/5/10 × magnitude).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders an SVG line chart above the CPU Usage row using all available
history samples (up to 100). Color adapts green/orange/red by load level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hosts with watch: false in config no longer appear in the Alerts page
or nav bar alert counts. Events still appear in the Log of Events.
Hosts without a config entry default to watch: false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts host info assembly (owner, managers, hbc version/type,
last packet timestamp, threshold configs) into a testable module-level
helper, with 10 covering tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows any authenticated user to update their own full_name, avatar,
notification_channels, and password via the config YAML write path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move field-extraction inside the try/except in fetch_user so non-dict
responses from providers with empty profile_data_path (Gitea, GitHub)
raise OAuthError instead of an uncaught AttributeError. Apply
html.escape() to provider name, label, and logo URL in the login page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded Gitea OAuth handlers with generic {name}-parameterized
routes and update the login page to render a button for each configured
provider via oauth_mod.get_providers().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
purge_stale_alerts used _find_threshold to validate alert state keys,
but _find_threshold has no wildcard matching. A threshold configured as
"zfs_monitor.*.status" never matched the concrete alert state key
"zfs_monitor.tank.status", so every restart silently purged active ZFS
pool alert states and reset the grace period from scratch.
Also fix _check_pending_or_renotify to set last_notification after the
grace-period notification fires, so the re-notification interval is
anchored to when the alert was actually sent rather than the next PLG cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add regex filter input to the Alerts dashboard that filters displayed
hosts on every keystroke; invalid regex turns the border red
- Initialise the filter from ?filter= in the URL query string
- Change _build_url() to produce /alerts?filter=<hostname> so
notification links (Pushover, email, Matrix, etc.) land on the
alerts page pre-filtered to the alerting host
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reads oauth.gitea.logo from config and, when set, renders an <img>
inside the button with flex alignment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>