Commit Graph
268 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 4acaa42a97 fix(ansible): the playbook variables partial on classes; Run again assertion matches the link (#5331, #5332)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m17s
CI / unit (push) Successful in 1m39s
CI / publish (push) Successful in 1m49s
CI run 8705 on 0c72892: the schedule page's no-inline-style guard caught
_playbook_vars.html (shared by the run and schedule forms), and the run
page's Run again assertion left out the link's class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:53:38 -04:00
bvandeusenandClaude Opus 5.5 0c72892f58 feat(ansible): schedules read as sentences, each with its own page (#5332)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 59s
CI / unit (push) Failing after 1m22s
CI / publish (push) Skipped
- The Schedules tab: a verdict line (failing first, paused, next due),
  each schedule as one line (what runs where, how often, when next, how it
  last went); the row opens its page; Run now and Pause stay in the row.
- /ansible/schedules/new and /ansible/schedules/<id>: the form on classes,
  dry run beside the button, the playbook's variables rendered with the
  page; at the foot Delete behind the confirm dialog. ?source=&playbook=
  start a new one on a playbook (Schedule it on the playbook's page).
- A run started by a schedule links to that schedule's page.
- The run page's id guard allows the Run again link's ?from=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:51:15 -04:00
bvandeusenandClaude Opus 5.5 72a3261582 feat(ansible): Playbooks as one list, and a page per playbook that runs it (#5331, #5330)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Failing after 1m20s
CI / publish (push) Skipped
- /ansible/playbooks lists every playbook across sources: what it says it
  does, its category and destructive flag, how it last ran; the whole row
  opens the playbook. A source with none is one quiet line.
- /ansible/playbooks/<source>/<path>: the verdict (what it does, flagged
  destructive, last ran), the run form in place (dry run beside the button,
  options folded), the schedules that use it, its recent runs, the playbook
  itself folded. ?from=<run> fills the form from that run: Run again on a
  run's page.
- /ansible/browse, its view page and the /run-form fragment are gone, not
  redirected (rule 122); every link points at the new pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:49:05 -04:00
bvandeusenandClaude Opus 5.5 a02f224f22 feat(ansible): one strip of tabs, a run list that opens on its verdict, a run page in one line (#5328, #5329, #5330)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m25s
CI / unit (push) Successful in 1m46s
CI / publish (push) Successful in 1m9s
- section_tabs macro; Runs · Playbooks · Schedules · Inventory on every
  Ansible page, replacing each page's own set of header buttons.
- Runs: a verdict line (last 30 days, failures first with the latest named,
  running now, the schedule due next); under each status what the run did
  across its hosts (ansible/recap.py); a filter bar (playbook, status, who
  started it) that swaps the table and carries into the scroll paging.
- Run page: status, duration, start and who started it in one line; each
  host's result in words, worst first; the playbook it ran links to it;
  the log and failures on classes rather than inline colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:45:04 -04:00
bvandeusenandClaude Opus 5.5 7499c38037 feat(ansible): the whole run row opens the run (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 57s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m29s
Same .row-link / .row-target as the Alerts rules list (snippet #5320).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:36:26 -04:00
bvandeusenandClaude Opus 5.5 16b636b2f0 feat(alerts): the whole rule row opens the rule (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m2s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m30s
The name's link stretches over its row (.row-link / .row-target); the
History and Disable buttons sit above it and keep their own clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:23:02 -04:00
bvandeusenandClaude Opus 5.5 e37cd0a873 test(alerts): check the history's Action column on one rule's rows, which the test owns (#5316)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 53s
CI / unit (push) Successful in 1m19s
CI / publish (push) Successful in 1m19s
The all-rules page reads a Postgres shared with other tests, whose
Ansible-run events turn the column on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:06:33 -04:00
bvandeusenandClaude Opus 5.5 531a193bd1 feat(alerts): history reads as episodes; tests follow the confirm dialog (#5316, #5314)
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 54s
CI / unit (push) Successful in 1m34s
CI / publish (push) Skipped
History (#5316):
- Transitions fold into episodes (steward/alerts/episodes.py): each stretch
  out of quiet, the worst state it reached, how long it lasted, its worst
  reading against the rule ('3m 21s (fires above 3m)'), who was told across
  its changes, and its Ansible runs. The changes themselves fold under it.
- An episode cut by paging says so ('cleared later (above)', 'earlier').
- For one rule: a verdict ('Fired 4 times in the last 7d, 1h 2m in all') and
  no Rule column. Notified and Action show only when the range has any.

Tests:
- The confirm guard looks for a confirm( call, not the word in the comment
  that says it is gone (lesson #4365).
- The Hosts viewer test looks for the add-host dialog by id, since every page
  now carries the confirm dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:02:01 -04:00
bvandeusenandClaude Opus 5.5 0c66c1e7d3 feat(alerts): Steward's own confirm dialog, a verdict-first rules list, and a rule page in three sections (#5314, #5315, #5317)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Failing after 1m25s
CI / publish (push) Skipped
Confirm dialog (#5314):
- Every data-confirm form now asks in one native <dialog> (base.html + static/confirm.js)
  instead of window.confirm.
- The question becomes the heading and what follows it the detail; the act's
  button is named after the form's own button; Cancel has the focus and Esc
  backs out. Optional data-confirm-title/-detail/-action/-dismiss attributes.
- All 27 existing uses work as they are. Cancelling a run says 'Keep running'.

Alerts list (#5315):
- A verdict line (what is firing, how many rules, where notifications go,
  or 'notifies nobody yet' with a link) replaces the empty Active card.
- Each rule's name opens it; rows show 'last fired' and fold fire/clear into
  the condition; History and Disable stay; Delete leaves the list.
- .docker-verdict becomes the shared .page-verdict.

Rule page (#5317):
- Named for the rule (title and crumb), with when it last fired and how many
  changes of state it holds.
- Three sections: what it watches, when it fires (checks side by side,
  seconds echoed as a duration), what happens (where notifications go, no-data,
  the playbook, whose fields stay folded until ticked).
- Delete at the foot, behind the dialog, saying its history goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:58:38 -04:00
bvandeusenandClaude Opus 5.5 7cb6cb1fb5 test(docker): the dead-container page test dates its exit from the clock |ago reads (#5310)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 53s
CI / unit (push) Successful in 1m23s
CI / publish (push) Successful in 1m19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:37:07 -04:00
bvandeusenandClaude Opus 5.5 29be9e3590 feat(docker): the service and container pages say each fact once (#5310)
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 1m33s
CI / unit (push) Failing after 1m59s
CI / publish (push) Skipped
Service page:
- The header carries the verdict, mode, stack, image and when it was reported.
  'In trouble since' shows only while the service is in trouble; once recovered,
  'Last failed X ago · its logs'.
- The failure card shows only while the service is down, degraded or crash-looping.
- The tile strip and the kv card are gone.
- One Tasks table: running tasks first with CPU, memory and since when, then
  Swarm's record. The node is named as its Steward host throughout, and
  'N failed in 24h' sits in the caption.

Container page:
- Same shape: header verdict, a 'Last exit' card only while it is stopped
  after a failing exit, set-up and I/O in one kv card, and lifecycle as a table.
- A swarm task's breadcrumb goes through its service.

Both pages:
- Resource history is the one fragment (_usage_history.html): now and peak in
  cores and bytes, drawn with a floor of 1 core / 1 GiB (sparkline_svg floor=),
  so idle noise draws flat.
- The container page's old % sparklines, its private _sparkline and the unused
  bucket()/container_history() helpers are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:33:25 -04:00
bvandeusenandClaude Opus 5.5 6c486d2f9c fix(docker): keep the space before a heading's restarting/unhealthy count (#5214)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 56s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m12s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:40:56 -04:00
bvandeusenandClaude Opus 5.5 429629be61 fix(docker): the overview's failed count sits in the heading of the list it counts, no tile row beside it; stale moves into the verdict (#5214)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Failing after 1m19s
CI / publish (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:38:04 -04:00
bvandeusenandClaude Opus 5.5 2ecf4d57cb test(docker): the disk page's images carry the 12-character ids the agent sends (#5305)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 52s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:20:41 -04:00
bvandeusenandClaude Opus 5.5 8566c8f78e feat(docker): Docker disk is recorded over time per host, the Disk page shows each host's growth and folds images under their repository, and the overview has one disk line per host (#5304–#5306)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 54s
CI / unit (push) Successful in 1m21s
CI / publish (push) Skipped
- #5304 "Docker disk totals recorded over time per host, with a growth chart and alertable metrics"
- #5305 "Disk page: images grouped by repository, versions folded under it"
- #5306 "Docker overview: one disk line per host linking to the Disk page"

Each `docker system df` report is now also recorded as metrics, with the host
as the resource: disk_total_bytes, its parts, and disk_reclaimable_bytes. An
alert rule can watch them. The Disk page starts with the hosts side by side,
showing how much each grew over the selected range with a line of it, then
gives each host its own section. An image's versions fold under its
repository, which is charged only for the layers its versions don't share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:16:13 -04:00
bvandeusenandClaude Opus 5.5 0b5cd23d38 test(docker): a filtered overview leaves the service out of its table, not out of the failures list (#5299)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m12s
CI / unit (push) Successful in 1m36s
CI / publish (push) Successful in 1m35s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:14:33 -04:00
bvandeusenandClaude Opus 5.5 aae61f1d2c feat(docker): the Swarm page is laid out by node: role, what its containers use, and the services placed on each (#5303)
CI / lint (push) Successful in 5s
CI / integration (push) Failing after 1m27s
CI / unit (push) Successful in 1m40s
CI / publish (push) Skipped
#5303 "Swarm page laid out by node: role, resources and the tasks placed on each"

A verdict line first, then the nodes that are down or drained and the
services short of replicas, then one card per node. A node with a Steward
agent shows what its swarm tasks use against the machine's cores and memory,
service by service. A node without one lists Swarm's placement, by count.
The services table moved to the Docker overview, where it can be filtered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:11:46 -04:00
bvandeusenandClaude Opus 5.5 a775882bd4 test(docker): a plugin template can take its own name; the overview's assertions follow its new tables (#5299, #5302)
render_plugin_template's first three arguments are positional-only, so the
service page's own `name` reaches the template. The overview names a service
rather than its replica containers, and a stack chip keeps the search it was
clicked under.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:11:46 -04:00
bvandeusenandClaude Opus 5.5 2093f399e6 feat(docker): the Docker page leads with a verdict, lists services and containers as filterable tables, and each service has its own page with logs across its tasks (#5298–#5302)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m4s
CI / unit (push) Failing after 1m28s
CI / publish (push) Skipped
- #5298 "Docker overview: verdict line; only the problem tiles that aren't zero"
- #5299 "Docker overview: swarm services as a filterable, sortable table with a Stack column"
- #5300 "Docker overview: failures listed once, each with a link to the failed task's logs"
- #5301 "Docker overview: standalone containers in one table with a Host column"
- #5302 "Service page: replicas, image, task history, resource history and logs across all its tasks"

overview.py holds the rows, filters and sort as plain functions. The filter
bar sits outside the polled fragment and the URL keeps its state. A service's
page reads every task container Steward still holds anything for, the live
ones and removed ones known by their events, so a failed task's logs are
reachable after Docker has removed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:08:38 -04:00
bvandeusenandClaude Opus 5.5 7dccb2f303 fix(hosts): add HX-Request to Vary rather than replacing it; the session adds Cookie (#5294)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 52s
CI / unit (push) Successful in 1m17s
CI / publish (push) Successful in 1m1s
CI 8616: the integration test asserted Vary == 'HX-Request', but the session
layer appends 'Cookie'. resp.vary.add keeps both; the test asserts membership.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:57:22 -04:00
bvandeusenandClaude Opus 5.5 2990113e10 feat(hosts): the Hosts list searches, filters by group and status, sorts by column, and shows each host's groups (#5294)
CI / integration (push) Failing after 52s
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m17s
CI / publish (push) Skipped
#5294 "Hosts list: search, group and status filters; sortable columns; a Groups column".
Ansible inventory groups are Steward's grouping system (operator's ruling): a host's
groups are its linked target's, and membership overlaps, so the list filters by group
rather than sectioning by it.

- Filter bar: search on name or address, a group (or "No group"), a status (Down,
  Agent silent, Up, No monitors). The table swaps in alone and the URL keeps the filters.
- Name, Status, Latency, 30d uptime, CPU and Memory headers sort (sort_th in _macros);
  problems-first stays the default, and a host with no value goes last either way.
- A Groups column of chips, each filtering to its group; shown when any group exists.
- A swap is keyed on HX-Target, so Back to a pushed URL still gets the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:53:03 -04:00
bvandeusenandClaude Opus 5.5 fb20415a38 feat(hosts): a host's health tiles span the card and the gaps match; Add host is a dialog on the list (#5290, #5291)
CI / integration (push) Successful in 54s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m23s
CI / lint (push) Successful in 3s
#5290 "Host page: the health tiles span the card's width; even spacing above Ansible":
the four tiles take equal quarters (halves on a phone) and their sparklines stretch
across them; the Monitors + Agent row drops the Agent card's own margin, so the gap
above Ansible is 1rem like every other.

#5291 "Add host opens as a dialog over the Hosts list": a native <dialog> on /hosts/,
opened by Add host or /hosts/?add=1. A name clash re-renders the list with it open,
the error and what was typed. GET /hosts/new is gone; hosts/form.html is Edit only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:22:48 -04:00
bvandeusenandClaude Opus 5.5 bfe3618e88 feat(hosts): the Hosts list labels its figures and says how long; SLA tab gone; one place to add a host (#5279–#5283)
CI / integration (push) Successful in 53s
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 1m16s
CI / publish (push) Successful in 1m23s
Hosts list:
- CPU and Memory are their own labelled columns (#5279); a silent
  agent's last readings are greyed, and Status says "agent silent for 7m".
- One uptime figure, 30 days, with 24h and 7d on hover. The SLA tab
  (/hosts/uptime) repeated it and is removed (#5280; one home per fact).
- Status is a word with how long ("Down for 12m"); host_status now carries
  down_since. Edit and Delete live on the host's page only (#5281).
- Figures are right-aligned with tabular digits (.table .num), not a
  monospace face.

Agent fleet (#5282): its add-host form is gone, so hosts are added only on
Hosts -> Add host. That form was also the only way to get a token for a
manual curl install, so that moves to the host's Agent card as "Install
manually" (POST /fleet/<host_id>/register), shown on the fleet page as
before. "Delete" there reads "Unregister".

Add host (#5283): laid out like the add-monitor form; a name clash comes
back inline with what was typed instead of a bare text 400; the address
is optional, as the host page already allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:12:17 -04:00
bvandeusenandClaude Opus 5.5 d4974ddf6c feat(hosts): a host's page stays short — Docker folds past the busiest, monitors add on their own form, agent actions under Manage (#5276, #5277, #5278)
CI / integration (push) Successful in 59s
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m17s
CI / publish (push) Successful in 1m3s
Docker panel (#5276): problems first, then the 8 busiest running
containers by CPU; the other running and the stopped fold behind one
"+N more running · M stopped" line. Containers that are not swarm tasks
read the way the Docker page reads them: CI jobs as "CI: workflow · job",
buildx builders as "BuildKit builder · name".

Monitors card (#5277): the edit/delete icon buttons and the inline add
form are gone. Each row links to its monitor's page; "Add monitor" opens
the add form filed under this host, starting from its address, with the
host in its breadcrumbs and Cancel back to it.

Agent card (#5278): Full metrics stays in view; Update, Rotate token,
Unregister agent (was "Remove") and Re-provision sit under "Manage agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:08:46 -04:00
bvandeusenandClaude Opus 5.5 6788ce60f1 feat(monitors): a DNS monitor can name a resolver; the host page puts Ansible above Docker (#5214)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 53s
CI / unit (push) Successful in 1m19s
CI / publish (push) Successful in 1m20s
DNS monitors take an optional Resolver, a server's IP. Set, the check asks
that server directly for A and AAAA over UDP and records how long it took
to answer; empty keeps today's lookup through the container's resolver.
"Does my local DNS still answer for public names" and "does 1.1.1.1" are
now separate monitors. No new dependency.

On a host's page the Ansible card moves above the Docker list, which can
run to dozens of containers and hid it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:14:11 -04:00
bvandeusenandClaude Opus 5.5 c6fc02def5 feat(monitors): a monitor's page reads as a page — response time over 24h, labelled actions, how long, not "ago"; the add form says what each type checks (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m3s
CI / unit (push) Successful in 1m22s
CI / publish (push) Successful in 1m22s
Monitor page: a response-time chart for the last day with failed checks
marked; Edit / Pause / Delete as labelled buttons; the state tile says
"for 29m" instead of "for at least 29m 25s ago"; the check strip fills
its card; no failures is one line, not an empty box; Settings names the
type, target and the actual global interval, with readable field names.

Add monitor: a line under the fields says what the selected type checks
and when it counts as up; Name says it defaults to the target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:57:09 -04:00
bvandeusenandClaude Opus 5.5 a7b280aed9 fix(monitors): the check strip fills its column; TLS shows only when a monitor has a certificate (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 58s
CI / unit (push) Successful in 1m28s
CI / publish (push) Successful in 1m21s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:44 -04:00
bvandeusenandClaude Opus 5.5 434127b3e1 feat(monitors): Monitors absorbs the Status page; Status is removed (#5270)
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 1m10s
CI / unit (push) Successful in 1m36s
CI / publish (push) Successful in 1m28s
Status repeated the Monitors page's counts, check strip and uptime. Monitors
now has one header row and every column Status had: the last 30 checks,
latest response with its trend, uptime over 24h / 7d / 30d (the range picker
goes) and TLS days on every row. /status and /status/rows and the nav link
are removed outright; the dashboard Overview (/status/widget) stays and its
Details link goes to Monitors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:36:38 -04:00
bvandeusenandClaude Opus 5.5 30f4de3612 fix(docker): drop the unused .w-meter style; the bars test reads the loaded plugin's models (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m13s
CI / unit (push) Successful in 1m35s
CI / publish (push) Successful in 1m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:08:15 -04:00
bvandeusenandClaude Opus 5.5 05eaca3f2f feat(docker): Busiest Containers shows bars of the last hours beside each figure, scaled to the row's peak with a floor (#5214)
CI / lint (push) Successful in 4s
CI / integration (push) Failing after 59s
CI / unit (push) Failing after 1m27s
CI / publish (push) Skipped
The fill bars were a share of the host's capacity, a sliver on hosts with
80 cores. Each figure now sits beside 24 bars of the last 6 hours (1h/24h
per widget), each row scaled to its own peak but never below 1 core or
1 GiB (configurable per widget), so a 0.05-core container reads flat and
a bar's height means real load. One grouped date_bin query feeds them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:05:06 -04:00
bvandeusenandClaude Opus 5.5 97c864bf42 test(dashboard): count Hosts header cells, not bare <th> tags (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 57s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m58s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:56:58 -04:00
bvandeusenandClaude Opus 5.5 c3ba850a9c fix(traefik): bucket history with date_bin; integer-epoch division never grouped scrapes (#5256)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 14s
CI 8551 integration: "integer out of range" for a 2099 timestamp.

The SQL in the log showed a second problem: `CAST(epoch AS INTEGER) / 60`
compiled to `/ CAST(60 AS NUMERIC)`, i.e. decimal division. Every scrape got
its own "bucket", so the request chart and the Traefik page's sparklines
never averaged anything. The int4 cast would also overflow in 2038.

Both now use date_bin from the epoch, as host_agent/metrics_query.py does.
The integration test adds a second scrape 30 s later in the same minute and
checks that the bucket averages the totals and weights the 5xx share over
both scrapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:33:38 -04:00
bvandeusenandClaude Opus 5.5 39b6066f3b feat(traefik): request chart shows the total, a time axis and a weighted 5xx share; says why if it can't draw (#5256)
CI / integration (push) Failing after 53s
CI / unit (push) Successful in 1m23s
CI / publish (push) Skipped
CI / lint (push) Successful in 2s
#5256 "Traefik — Request chart: blank on the live dashboard; time axis and honest labels".

- "Req/s" was the average of the routers, not the total. Each scrape is now
  summed over routers, then averaged per bucket (request_history).
- The 5xx share is now weighted by each router's traffic. The axis is no
  longer pinned at 0–100 (suggestedMax 5).
- The x axis shows times, in the viewer's local time. The tooltip shows
  both series at that time.
- A verdict line comes first: now / peak req/s and the peak 5xx share. The
  figures read even if the chart doesn't.
- The blank live chart's cause is still unknown. Chart.js and
  chart-colors.js both load on the dashboard. A missing library or a thrown
  error is now written into the widget instead of leaving it empty, so the
  cause will show on the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:28:55 -04:00
bvandeusenandClaude Opus 5.5 0d603bc7dd feat(snmp): Devices widget gives a verdict, status in words and readable key readings (#5255)
CI / lint (push) Successful in 3s
CI / publish (push) Successful in 1m16s
CI / integration (push) Successful in 51s
CI / unit (push) Successful in 1m17s
#5255 "SNMP — Devices: status in words and key readings".

- Verdict first: "All N devices answering", "ups: no reply since 14m ago",
  "N of M devices not answering", or "N not polled yet".
- Devices that stopped answering come first, then those never polled, then
  the rest.
- One header row: Device / Readings / Polled.
- The name is the link; the separate "detail →" is gone.
- The hollow dot is replaced by words: "no data yet", or "no reply since …;
  readings are from then".
- Key readings: the first two in config order, gauges ahead of counters.
  Units come from the label suffix: _centisecs as a duration, _pct, _dV as
  volts, _octets as bytes total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:27:26 -04:00
bvandeusenandClaude Opus 5.5 a720403b47 test(docker,traefik): count header cells, not <thead; read cert days against the cert's own clock (#5253, #5254)
CI 8548: '<th' also matched '<thead', and the certificate test's clock was
read before its certificates were built, so 10 days came out as 10.00…

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:27:26 -04:00
bvandeusenandClaude Opus 5.5 ea5531ac8f feat(traefik): Routers widget shows current routers only, one header row, certs on the routers they serve (#5254)
CI / lint (push) Successful in 2s
CI / publish (push) Skipped
CI / integration (push) Successful in 52s
CI / unit (push) Failing after 1m17s
#5254 "Traefik — Routers: current routers only, header row, certs on the router they serve".

- Router hosts come from Traefik's API (/api/http|tcp/routers), read
  best-effort on each scrape. An optional api_url overrides it; the default
  is /api on the metrics host. A certificate's expiry then shows in a TLS
  column on the router whose host it covers. An expiring one makes that
  router a problem, graded by Settings → Thresholds.
- Only current: routers and certificates must have appeared in the last
  three scrapes. Certificates are upserted by serial and never deleted, so
  renewed or retired ones lingered on the widget.
- Widget order: verdict, problem routers with the reason, then the busiest
  healthy routers under one header (Router / Req/s / Bandwidth / p95 / TLS).
  A missing value shows "—".
- If the API is unreachable, expiring certificates are listed after the
  routers, not above them.
- The page splits certificates into served (with the routers using each,
  plus SANs) and "No longer served".
- Latest row per router: an index walk (recursive CTE + LATERAL) replaces
  DISTINCT plus a query per router.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:25:11 -04:00
bvandeusenandClaude Opus 5.5 d72a1991a7 feat(docker): Busiest Containers shows each figure with its scale; readable names (#5253)
The widget names the busiest container in a verdict ("Busiest: tdarr on
VDNT-Docker03, 1.6 of 8 cores."), then a fixed-layout table (container,
host, CPU in cores, memory in bytes) with each figure's share drawn as
shading behind it: of the host's cores, and of the memory limit or the
host's memory.

The numbers were already rendered, but each row grew as wide as its longest
name. A grid item won't shrink below its content, so Gitea CI container
names pushed the figures out of view and the cell scrolled sideways. Names
now come from display_name(): a swarm task by its service (no slot number),
an Actions job container as "CI: <workflow> · <job>". They cut off with …
and show whole on hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:18:44 -04:00
bvandeusenandClaude Opus 5.5 213a28c23f feat(docker): Services & Failures widget lists what is wrong, then packs the healthy (#5252)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Successful in 1m23s
CI / publish (push) Successful in 1m34s
A verdict line (services healthy · other containers running · failed in
24h), then "Needs a look": troubled services with why (crash-looping, N of
M running), since when and the last error, and troubled containers
(unhealthy, restarting, exited with an error, out of memory). Then "Failed
in the last 24h" by name, with how often, when and where. Then the healthy
rest one line each (name · hosts · replicas) in as many columns as the
widget is wide, collapsing after 40 to "+N more healthy".

Per-container CPU is gone from this widget: Busiest Containers owns it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:17:09 -04:00
bvandeusenandClaude Opus 5.5 102c84987d test(monitors): the widget's went-down time is read against the render's clock (#5250)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:17:09 -04:00
bvandeusenandClaude Opus 5.5 17abe450cc feat(dashboard): Hosts widget worst first, load against cores, one home per fact (#5251)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m19s
CI / unit (push) Failing after 1m31s
CI / publish (push) Skipped
The Hosts — Overview widget opens with a verdict ("VDNT-Util01: CPU high."
or "All 3 hosts healthy."), then one row per host under one header: CPU,
memory, disk / and load. Rows sort by host_glance: a down monitor, a
reading past critical, a quiet agent, a warning, then the healthy. Load
reads "16.8 / 8 cores", graded per core as Settings → Thresholds intends
(cores counted from the per-core CPU sub-resources, as on the host page).
A quiet agent's last figures are shown uncoloured, with when it last reported.

Ping and 24h uptime are gone from this widget: they belong to Monitors. So
are the unlabelled CPU sparkline and the 30-day uptime query it no longer
needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:15:01 -04:00
bvandeusenandClaude Opus 5.5 b5fcccba5f feat(dashboard): Monitors widget carries uptime; Uptime / SLA retired (#5250)
The Monitors widget opens with a verdict ("1 of 5 monitors down."), then one
row per monitor under a single header: latency and uptime over 24h, 7d and
30d, down monitors first with why and since when, the name linking to the
monitor. Counts follow the type filter. Problems always show; the limit
trims only the healthy rest ("+N more up").

Uptime / SLA (uptime_summary) repeated those numbers per host; migration
0033 moves dashboards onto Monitors. Shared widget styles (.w-verdict,
.w-table, .w-sub, .w-more) carry the widget-shape ruling for the widgets
that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:12:58 -04:00
bvandeusenandClaude Opus 5.5 4dcf958022 feat(dashboard): widgets fit their content; wide screens get more grid columns (#5249)
CI / integration (push) Successful in 55s
CI / lint (push) Successful in 4s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m28s
Each widget is as tall as its content, never taller than its saved height
(where it scrolls). The dashboard is capped at 2400px instead of 1400px.
Past 12 columns' worth of width it adds columns (one per 110px, up to 24)
and places widgets again in reading order at their saved widths, so more sit
side by side instead of stretching. Edit mode shows and saves the
12-column layout, so a layout doesn't depend on the screen it was edited on.

The share view uses the same layout code (static/dash-layout.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:10:42 -04:00
bvandeusenandClaude Opus 5.5 5492336f1e feat(dashboard): a curated default layout, merged duplicate widgets, a labelled Overview (#5213)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 50s
CI / unit (push) Failing after 1m16s
CI / publish (push) Skipped
- New installs start from widgets.RECOMMENDED, packed row by row for the
  enabled plugins; the edit drawer can reset a dashboard to it.
- Host Agent — Resources folds into Hosts — Overview (which gains load 1m),
  UniFi — Devices into UniFi — Overview; migration 0032 moves existing
  dashboards onto the survivors.
- The Overview labels what it counts (hosts, monitors up/down/pending,
  alerts firing) and lists firing alerts and down monitors with since-times.
- Uptime polls every 5 minutes (poll_seconds) instead of every cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:42:04 -04:00
bvandeusenandClaude Opus 5.5 5ec930bf76 feat(ui): Disk, Traefik, UniFi and Settings say things plainly (#5212)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 53s
CI / unit (push) Successful in 1m14s
CI / publish (push) Successful in 1m10s
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 3:

- Disk page: each tile says what it holds. "Writable layers" becomes
  "Container files" and Build cache says it is safe to clear. Shared is
  "Shared with other images" with a tooltip, and "dangling" is gone.
  Untagged images show by id instead of "<none>", a capped list says
  "The 50 largest of N images", and each host shows "as of" its report.
- Traefik: an expired certificate reads "expired 12 days ago", not "-12
  days" (cert_left). The Traffic empty state knows whether the access log
  is on: off points to Settings → Plugins → Traefik; on says to check the
  mounted log path.
- Moments use |ago with an exact UTC tooltip: UniFi last seen, alarms,
  events, speed test and DPI; AI token last used.
- Settings: the Auth tab is "Authentication" like its breadcrumb, plugin
  fields read "Metrics URL" not metrics_url (field_label), a source's pull
  interval is "every 1h", and the last report time drops the ISO "T".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:29:30 -04:00
bvandeusenandClaude Opus 5.5 e780932673 feat(ui): each run says who or what started it; run detail without ids (#5212)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 49s
CI / unit (push) Successful in 1m15s
CI / publish (push) Successful in 1m3s
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 2:

- A run records where it came from: AnsibleRun.origin ("schedule", "alert"
  or "mcp") and origin_id, set by the scheduler, Run now, alert actions and
  the MCP tools. Migration 0031 backfills past runs from alert events,
  schedules' last runs, and MCP audit entries.
- The runs list has a Started by column: "schedule nightly", "alert rule
  nas down", "alice via MCP" or "alice", linked where there is a page.
  The raw scope tooltip is gone.
- Run detail: no run id in the subtitle or body, Started by in words, and
  the host summary lists failed and unreachable hosts first. The live
  output only follows new lines while the reader is at the bottom, and
  when the run finishes the page reloads once so the badge and summary
  are final.
- list_ansible_runs and get_ansible_run report origin and origin_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:26:17 -04:00
bvandeusenandClaude Opus 5.5 a96112a0e1 test(alerts): the history row test's rule names its metric (#5212)
Run 8504 failed unit: tests/core/test_alert_history_row.py built a rule
with no metric_name, which _history_row now reads to say the reading in
words. The rule now names cpu_pct, and the value and condition asserts
expect "CPU 98%", "No CPU reading" and "CPU > 90%", plus an up/down case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:26:17 -04:00
bvandeusenandClaude Opus 5.5 3a772716ae feat(ui): audit, alert rules and the rule form say things in words (#5212)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Failing after 1m14s
CI / publish (push) Skipped
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 1:

- Audit log: each action reads as a sentence ("paused monitor"), with the
  code as its tooltip. Detail shows flags as yes/no, lists joined, a run's
  scope by name, spans as spans, and no ids the entity column already gives.
  The filter is now a picker of areas.
- Alert rules, history and Status say the condition in words: "is down",
  "Fullest disk >= 85%". Readings use metric_value, and history times use
  |ago.
- Rule form: sources and metrics by name, the threshold's unit, "fires
  when it is down/up" for an up/down metric instead of 0.5, and a metric
  reference built from the catalog.
- Schedules and Maintenance: viewers no longer see Run now, Pause, Edit,
  Delete or New. Intervals and next runs read as "every 2h" and "in 3h";
  a scope reads "Every alert on Host agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:23:02 -04:00
bvandeusenandClaude Opus 5.5 7275fad767 feat(ui): audit log, alert history and Ansible runs load as you scroll (#5211)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Successful in 1m13s
CI / publish (push) Successful in 1m7s
#5211 "Lists load as you scroll: audit, alert history, Ansible runs"
(milestone 477), per preference 172.

- core/paging.py: keyset paging over (time, id), newest first, with an
  opaque "<iso>|<id>" cursor; a page reads one row past itself to know
  whether another follows. No OFFSET, so rows added while someone scrolls
  never shift the pages after them.
- _macros.html scroll_more / scroll_end and static/scroll-more.js, written
  once: an IntersectionObserver with a 600px margin fetches the next page
  before the reader reaches the end; "Loading more…" while in flight,
  "End of the list." at the end, and a failed page says so with a Retry
  button instead of re-firing on every scroll.
- Audit log: no "Load more" link and no 500-row cap.
- Alert history: no 200-row stop with "pick a shorter range";
  alert_svc.history takes the cursor, so the MCP tool and the page share
  one query.
- Ansible runs: no silent cap of 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:10:53 -04:00
bvandeusenandClaude Opus 5.5 8e890c6914 feat(ui): host page leads with its alerts; old readings say how old (#5210)
CI / integration (push) Successful in 50s
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m20s
CI / publish (push) Successful in 1m13s
#5210 "Problems first, with since-time, across every list" (milestone 477),
second batch.

- Host page: an "Alerts on this host" strip first (rules on its agent
  series, its containers or its monitors: alert_svc.watches_host); its
  monitors problems first with "Down 12m · reason"; Docker above Ansible.
- Uptime page and widget: worst 24h uptime first, then 7d and 30d; hosts
  with no uptime yet last.
- Agent panel and fleet widget: last seen as "3m ago", not a clock time.
- SNMP: a device is "Reachable" only while it answered within the last 3
  polls; older readings say "No answer" with their age, and every device
  says when it was last read (was: reachable whenever any reading existed).
- Traefik: a router missing from 3 scrapes in a row is listed apart as no
  longer reported, by when it was last seen, instead of keeping its last
  state (a removed router stayed Failing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:02:20 -04:00
bvandeusenandClaude Opus 5.5 5a14e7c693 feat(ui): alerts, monitors, hosts, schedules and maintenance lists put problems first, with since-times (#5210)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m8s
CI / unit (push) Successful in 1m25s
CI / publish (push) Successful in 1m17s
#5210 "Problems first, with since-time, across every list" (milestone 477),
first batch.

- Alerts: the Active list reads firing, no data, acknowledged, pending, the
  longest-running first, and re-polls itself. An acknowledged alert says how
  long it has been breaching (from the alert's events; acknowledging moves
  last_transition_at) and, apart from that, who acknowledged it and when.
  Rules show what each is doing now; disabled rules sort last. The current
  value reads the latest-value table (#5208) instead of plugin_metrics.
- Monitors: a down monitor says "Down 12m · connection refused" in the list.
- Hosts: sorted down, agent gone quiet, pending, healthy. The agent column
  shows "—" for a missing reading (was 0%) and a Stale badge with its age.
  agent_metrics_by_host is gone; the list reads agent_overview_by_host.
- Schedules: failing first (failed or interrupted run, or a firing that could
  not start one), then running, then the rest, paused last; status badge
  from the shared states.
- Maintenance windows: active (ending soonest), then upcoming (starting
  soonest), with expired folded away; each says when it ends/starts/ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:58:56 -04:00