Commit Graph
17 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 0bac1d2068 feat(traefik): the page leads with its verdict and the request chart (#5710)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m27s
CI / unit (push) Failing after 1m38s
CI / publish (push) Skipped
The Traefik page now reads state first: a verdict naming each router that
needs a look, why, and when it started (from its history buckets over the
page's range), then the request chart following the page's time range, then
the problem cards (which say since when too), the proxy's own figures, and
the healthy rest. The verdict is filled out of band by /rows each poll; the
"Needs attention" and "Healthy routers" tiles it replaces are gone.

The request chart takes ?range= from the page; past a day its times carry the
day, and its clock is 24-hour (hourCycle h23).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:50:09 -04:00
bvandeusenandClaude Opus 5.5 24700ee608 feat(settings): Settings brought into line with the rest of Steward (milestone 493)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 56s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m9s
The last page of #5214's walk. Settings still described Steward as it was
before monitors were unified and SNMP was rebuilt, drew its tabs below the
breadcrumb, and showed the report schedule in UTC.

- Tabs (#5377): settings/_tabs.html is a settings_tabs() macro called in
  each page's section_tabs block, above the trail, as Ansible's are. The
  breadcrumb guard also fails any page that {% include %}s a tab strip.
- Report time (#5378): reports.schedule_tz keeps the zone of whoever saved
  the schedule; check_and_send compares in it (report_due), and the form shows
  the day and hour on the reader's clock (schedule_in) under their zone's
  name. "Last sent" uses |when.
- Wording (#5379): the report contents list what the report holds (monitors,
  not ping-enabled hosts); "Ping latency" is "Response time" and says what it
  grades; plugin detail says enabling loads now and disabling needs a
  restart; the monitor interval says it applies on the next check (the
  scheduler reads it every pass); the dead list-config branch is gone.
- Traefik (#5380): the 5xx and 4xx cutoffs are settings
  (traefik.threshold.error_5xx_pct 0.1, error_4xx_pct 1.0) in a "Traefik
  routers" group on Thresholds while the plugin runs, read by router_state
  through error_cutoffs(). The Traefik page has a Thresholds button for
  admins, to /settings/thresholds/#traefik.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:28:43 -04:00
bvandeusenandClaude Opus 5.5 18867c8a50 fix(traefik): bandwidth and 4xx sparklines; cards in a grid with a gap; slow rows/widget log their phases (#5355)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m8s
CI / unit (push) Successful in 1m32s
CI / publish (push) Successful in 2m1s
The operator: "load was still slow" (the access log: 7.6-8.0 s on every
poll, warm), "why do the bandwidth and 4xx sections not also get
sparklines", and "no gap between the widgets". router_history reads the
4xx share and bandwidth too, and the card draws all five lines. The
problem cards sit in .node-grid with a bottom margin; a CSS column box
dropped the last card's margin. rows() and widget() log each phase when a
request passes 1 s, so the slow query names itself on the next load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:23:48 -04:00
bvandeusenandClaude Opus 5.5 7ce61a2099 fix(traefik): grade routers on their last 15 minutes; the Config tile reads Traefik's reload time (#5355)
CI / lint (push) Failing after 2s
CI / integration (push) Successful in 54s
CI / unit (push) Successful in 1m24s
CI / publish (push) Skipped
The operator: problem cards "can disappear so quickly". Each router was
graded on its latest scrape, so a 4xx share near the 1% cutoff crossed it
on one scrape and fell back on the next. recent_window reads every
router's last 15 minutes in one query, the error shares weighted by
traffic; grade_routers uses it on the page and the dashboard widget, and
the why says "over the last 15 min".

traefik_config_last_reload_success is the Unix time of the last good
reload, not a 0/1 flag, so the tile said "Reload failed" on every scrape.
traefik_005 stores it as config_last_reload_at, and the tile reads
"Config reloaded 3m ago". The router index also carries the 4xx share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:26:10 -04:00
bvandeusenandClaude Opus 5.5 eaf9f1bcb8 fix(traefik): request chart redraws once the dashboard has placed it, and refreshes every 5 min (#5214)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 53s
CI / unit (push) Successful in 1m22s
CI / publish (push) Successful in 1m26s
A chart built while its cell was still being laid out could stay blank until
a window resize; it now measures and draws again after layout settles. Each
refresh rebuilds the chart (the flash), and its buckets are minutes wide, so
the widget refreshes every 5 minutes instead of every poll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:24:29 -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 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 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 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 414c942cbf feat(plugins): Traefik problems first, UniFi/SNMP/fleet on shared shapes, audit names its entities, templates on tokens (#5126)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m5s
CI / unit (push) Successful in 1m19s
CI / publish (push) Successful in 57s
Traefik: routers are graded from their latest scrape (router_state: crit on
any real 5xx share, warn on p95 past the latency critical cutoff or a high
4xx share). Failing and slow routers lead as cards that say why; healthy
ones fold into one table that stays open or shut across polls. Proxy
counts are stat tiles; certificates are graded by Settings -> Thresholds
(tls_days).

UniFi: WAN health is a stat strip (latency graded by the thresholds),
devices carry status dots, event categories and firewall actions are
badges, the alarm count reuses .count-badge (renamed from .nav-count).
SNMP: each device wears a status badge and its readings are stat tiles.

Agent fleet: the table comes first, with a Status column (Reporting /
Silent / Never reported, by the host page's stale_after_seconds), silent
agents first, hosts linked, last seen as |ago.

Audit: entries name their entity (one query per type, steward/audit/
service.py) and link it; something deleted since keeps the name its entry
recorded and says "(deleted)".

Tokens: charts read colours through chart-colors.js (stewardColor resolves
a token to the rgb the browser computed; Chart.js defaults take the page's
text and border colours), so host, history, Traefik and SNMP charts drop
their hex. Template hex colours reach 0 (HEX_CEILING = 0), the last rgba
pills go, and heavy weights (600/700/bold, 60 of them) become 500, the
house pair being 400/500; a guard keeps templates there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:25:09 -04:00
bvandeusenandClaude Opus 5.5 bf08169176 feat(ui): shared |bytes |rate |duration |ago filters and status_class from Settings → Thresholds (#5093)
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 1m28s
CI / integration (push) Successful in 1m7s
CI / publish (push) Successful in 2m40s
#5093 "Shared format filters and status_class driven by Settings → Thresholds".

- steward/core/formatting.py: human_bytes (binary units), human_rate,
  human_duration (two largest units), ago (a <time> titled with the absolute
  UTC time) and status_class(value, kind, thresholds) -> ok/warn/crit/unknown,
  which raises on a kind the thresholds don't name.
- app.py registers them as the bytes, rate, duration and ago filters and the
  status_class global; threshold_style is now graded by status_class (and caps
  its weight at 500).
- Replaces the copies: docker's _human_bytes/_human_uptime, core alerts'
  _duration, the host-agent fmt_bps/fmt_bytes macros, the Traefik and UniFi
  byte-rate arithmetic, the swarm-failures ago macro and two uptime spans.
- Tests: a table per helper, and every template compiled against the app's
  filters, so an unknown filter fails CI rather than a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:25:06 -04:00
bvandeusenandClaude Opus 5.5 65411d3ce8 feat(ui): base stylesheet on the Steward design-system tokens; readable status colours (#5092)
CI / unit (push) Successful in 1m20s
CI / lint (push) Successful in 5s
CI / integration (push) Successful in 1m12s
CI / publish (push) Successful in 1m43s
#5092 "Tokens". base.html's inline palette was its own: white text on the
gold button (1.81:1), a red at 3.26:1 on a card, a warning yellow at hue
42 beside the gold accent at 46, a lavender page title, serif labels at
13px and weights up to 700.

- static/theme.css is design system 6's generated sheet (FabledSword + the
  Steward accent). static/steward.css is base.html's stylesheet moved out,
  with :root now aliases onto those tokens (--bg, --text-muted, --green…)
  so every template keeps rendering; it states no colour of its own.
- Status text colours are Steward overrides recorded in the design system
  (tokens 182-184): success #5FB87A, warning #E08A3C, error #EC7A6C. The
  house -fg mixes read "up" as grey hint text (dE 8.2) and warning as the
  accent (dE 11.5); these are >= 4.5:1 on page, card, hover and their own
  15-25% tints. --orange is now the midpoint of warning and error.
- Primary button and active range button: dark text on gold (7.82:1).
  Danger button on the house destructive fill. Badges and alerts take the
  -fg colour on a tint; the gold badge uses --fs-accent-fg.
- Type: Inter body, Fraunces only for page titles and h1/h2, JetBrains
  Mono for code; section and widget titles become the tiny caps label in
  the body face; weights capped at 500.
- Removed: the lavender title and share-view gradient, #606080, and the
  undefined --card-bg, --accent-faint, --accent-bg, --bg-alt, --border-low
  and --row-alt (now real tokens); the dead settings/index.html.
- Guards (tests/test_style_tokens.py): every var(--x) used is defined;
  steward.css states no colour or weight over 500; hex colours in
  templates may only go down (ceiling 75, from 81 before this change),
  each with a self-test that it can fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:18:49 -04:00
bvandeusenandClaude Opus 4.8 af60ca446d fix(plugins): complete steward rename across bundled plugins; clear lint debt
CI / integration (push) Successful in 2m8s
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 7s
The fabledscryer->steward rename had only ever reached host_agent. The other
five bundled plugins (http, snmp, traefik, unifi, docker) still imported
`from fabledscryer.*` (package no longer exists) and read FABLEDSCRYER_* env
vars — so every one of them was broken at import since the original rebrand.
CI stayed green only because none are enabled by default and migrations don't
import plugin modules. Now that they version in-tree, complete the rename:
- fabledscryer.* -> steward.* imports across all five plugins
- FABLEDSCRYER_* -> STEWARD_* in plugin migration env.py files
- author/repository/homepage + user-facing 'Fabled Scryer' strings -> Steward
- snmp/scheduler.py: also drop dead `now`/datetime; record_metric from steward

Adds tests/test_no_legacy_names.py — fails if 'scryer'/'roundtable' ever
reappear in shipped code (the drift bit twice; this stops a third time).

Also clears pre-existing ruff lint debt (unused imports, semicolon statements,
mid-file import) surfaced by the new lint lane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:22:20 -04:00
bvandeusenandClaude Opus 4.8 a7a281cb11 feat(plugins): fold first-party plugins in-tree; bundled + external roots
First-party plugins (host_agent, http, snmp, traefik, unifi, docker) are now
tracked under plugins/ and baked into the image, so they version atomically
with core — ending the cross-repo import drift the roundtable->steward rename
exposed. History for these files is preserved in the archived Roundtable-plugins
repo.

Plugin discovery becomes multi-root: PLUGIN_DIR (single) -> PLUGIN_DIRS
(bundled first, then external) + PLUGIN_INSTALL_DIR. Bundled ships in the image;
third-party plugins still mount at runtime into the external root
(STEWARD_PLUGIN_DIR, default /data/plugins) and downloads/installs land there.
Bundled shadows external on a name collision.

- config.py: load_bootstrap returns plugin_dirs + plugin_install_dir
- app.py: iterate PLUGIN_DIRS at the migration + load sites
- migration_runner.py: discover_all_in() unions every plugin root
- plugin_manager.py: resolve_plugin_path() (pure, first-root-wins); load /
  install / hot-reload span all roots; installs target the external root
- settings/routes.py: _discover_plugins scans all roots, dedup bundled-first
- Dockerfile: COPY plugins/ ; docker-compose: drop host bind, document external
- tests/test_plugin_dirs.py: resolution, multi-root discovery, bootstrap split

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 08:37:24 -04:00
bvandeusenandClaude Sonnet 4.6 fa318073f1 chore: move plugins/ to separate repo, gitignore the directory
plugins/ is now tracked at bvandeusen/fabledscryer-plugins.
Removed tracked traefik files from this repo's index (files remain on disk).
Added plugins/ to .gitignore so the directory is ignored going forward.

PLUGIN_DIR in config.yaml still points to plugins/ for local dev and Docker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:33:22 -04:00
bvandeusenandClaude Sonnet 4.6 165a202ba4 feat: UI redesign (CSS system, DNS widget, Traefik dashboard widget)
- Replace base.html style block with full CSS design system using custom properties (dark theme, cards, tables, badges, dots, widgets)
- Add DNS blueprint (fablednetmon/dns/) with /dns/ and /dns/rows routes + templates
- Add Traefik /widget route and widget.html fragment for dashboard
- Update dashboard: DNS + Traefik widgets, traefik_enabled config detection
- Update all templates to use new CSS classes (page-title, section-title, card-flush, table, badge-*, dot-*, btn-*)
- Register dns_bp in app.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 23:37:46 -04:00
bvandeusen 2164ea28f2 feat: traefik UI (per-router dashboard with SVG sparklines) 2026-03-18 18:43:05 -04:00