Files
FabledSteward/steward/templates/settings/plugins.html
T
bvandeusenandClaude Opus 5.5 24700ee608
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 56s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m9s
feat(settings): Settings brought into line with the rest of Steward (milestone 493)
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

150 lines
8.3 KiB
HTML

{# steward/templates/settings/plugins.html #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% block title %}Settings — Plugins — Steward{% endblock %}
{% from "settings/_tabs.html" import settings_tabs %}
{% block section_tabs %}{{ settings_tabs("plugins") }}{% endblock %}
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Plugins", "")]) }}{% endblock %}
{% block content %}
{# ── Restart banner ────────────────────────────────────────────────────────── #}
<div id="restart-banner"></div>
{% macro plugin_card(plugin) %}
{% set fail_reason = plugin_failures.get(plugin._dir) %}
<div class="card" style="padding:0.85rem 1rem;display:flex;align-items:center;gap:0.75rem;">
{% if fail_reason %}
<span style="width:8px;height:8px;border-radius:50%;background:var(--red);flex-shrink:0;" title="{{ fail_reason }}"></span>
{% elif plugin._enabled %}
<span style="width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0;"></span>
{% else %}
<span style="width:8px;height:8px;border-radius:50%;background:var(--border-mid);flex-shrink:0;"></span>
{% endif %}
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:baseline;gap:0.5rem;flex-wrap:wrap;">
<span style="font-weight:500;font-size:0.9rem;color:var(--text);">{{ plugin.get('name', plugin._dir) }}</span>
<span style="font-size:0.75rem;color:var(--text-muted);">v{{ plugin.get('version', '?') }}</span>
{% if fail_reason %}
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
background:color-mix(in srgb,var(--red) 15%,var(--bg));color:var(--red);">Error</span>
{% elif plugin._enabled %}
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
background:color-mix(in srgb,var(--green) 12%,var(--bg));color:var(--green);">Enabled</span>
{% else %}
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
background:var(--bg-elevated);color:var(--text-muted);">Disabled</span>
{% endif %}
</div>
{% if plugin.get('description') %}
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.1rem;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ plugin.description }}</div>
{% endif %}
{% if fail_reason %}
<div style="font-size:0.77rem;color:var(--red);margin-top:0.1rem;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="{{ fail_reason }}">{{ fail_reason }}</div>
{% endif %}
</div>
<a href="/settings/plugins/{{ plugin._dir }}/"
class="btn btn-ghost btn-sm" style="flex-shrink:0;font-size:0.78rem;">Settings →</a>
</div>
{% endmacro %}
{# ── Monitoring capabilities (host facets) ─────────────────────────────────── #}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.4rem;">
<div class="section-title">Monitoring capabilities</div>
<form method="post" action="/settings/plugins/restart/"
hx-post="/settings/plugins/restart/" hx-target="#restart-banner" hx-swap="outerHTML"
data-confirm="Restart the app now? This will briefly interrupt all monitoring."
style="margin:0;">{{ csrf_field() }}
<button type="submit" class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Restart App</button>
</form>
</div>
<p style="color:var(--text-muted);font-size:0.82rem;margin:0 0 0.75rem;max-width:720px;">
Built-in ways to monitor your hosts (agent metrics, SNMP, Docker). These are
facets of a host — you'll see their data on each host in <a href="/hosts/">Hosts</a>,
not as separate areas. On by default.
</p>
{% if capabilities %}
<div style="display:grid;gap:0.6rem;max-width:720px;margin-bottom:1.5rem;">
{% for plugin in capabilities %}{{ plugin_card(plugin) }}{% endfor %}
</div>
{% else %}
<div class="card" style="color:var(--text-muted);font-size:0.9rem;margin-bottom:1.5rem;">None installed.</div>
{% endif %}
{# ── Integrations (external systems) ───────────────────────────────────────── #}
<div class="section-title" style="margin-bottom:0.4rem;">Integrations</div>
<p style="color:var(--text-muted);font-size:0.82rem;margin:0 0 0.75rem;max-width:720px;">
External systems Steward pulls data from (e.g. Traefik, UniFi). Enable and configure each one
to surface its own dashboard. Off until you set them up. A plugin of your own goes in
<code>/data/plugins</code>; restart and it appears here.
</p>
{% if integrations %}
<div style="display:grid;gap:0.6rem;max-width:720px;margin-bottom:1.5rem;">
{% for plugin in integrations %}{{ plugin_card(plugin) }}{% endfor %}
</div>
{% else %}
<div class="card" style="color:var(--text-muted);font-size:0.9rem;margin-bottom:1.5rem;">
No integrations installed.
</div>
{% endif %}
{# ── Orphaned settings ─────────────────────────────────────────────────────── #}
{# Hidden entirely when there is nothing to clean up — an empty "Configured but
not installed" panel would read as a problem rather than a clean state. #}
{% if orphaned_plugins %}
<div style="margin-bottom:1.5rem;max-width:720px;">
<div class="section-title" style="margin-bottom:0.4rem;">Configured but not installed</div>
<p style="color:var(--text-muted);font-size:0.82rem;margin:0 0 0.75rem;">
Plugins Steward has settings for, or tried to load, but cannot find on disk — including
anything counted by the "failed to load" banner. This usually means the plugin was
removed, but it also happens when an external plugin directory isn't mounted, so
nothing is deleted automatically. Removing an entry
discards that plugin's saved configuration, including any credentials.
</p>
<div style="display:grid;gap:0.6rem;">
{% for orphan in orphaned_plugins %}
<div class="card" style="padding:0.85rem 1rem;display:flex;align-items:center;gap:0.75rem;">
<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;
background:{{ 'var(--red)' if orphan.reason else 'var(--yellow)' }};"
title="{{ orphan.reason or 'Configured but not installed' }}"></span>
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:baseline;gap:0.5rem;flex-wrap:wrap;">
<span style="font-weight:500;font-size:0.9rem;color:var(--text);">{{ orphan.name }}</span>
{% if orphan.reason %}
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
background:color-mix(in srgb,var(--red) 15%,var(--bg));color:var(--red);">Failed to load</span>
{% else %}
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
background:var(--yellow-dim);color:var(--yellow);">Not installed</span>
{% endif %}
</div>
{% if orphan.reason %}
<div style="font-size:0.77rem;color:var(--red);margin-top:0.1rem;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
title="{{ orphan.reason }}">{{ orphan.reason }}</div>
{% endif %}
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.1rem;">
{% if orphan.removable %}
Settings key <code>plugin.{{ orphan.name }}</code> has no matching plugin.
{% else %}
Enabled by a built-in default with no stored settings to remove — this is a
packaging bug, not leftover configuration. Please report it.
{% endif %}
</div>
</div>
{% if orphan.removable %}
<form method="post" action="/settings/plugins/orphans/{{ orphan.name }}/remove/"
data-confirm="Remove stored settings for &quot;{{ orphan.name }}&quot;? Its saved configuration, including any credentials, will be discarded. This cannot be undone."
style="margin:0;flex-shrink:0;">{{ csrf_field() }}
<button type="submit" class="btn btn-danger btn-sm" style="font-size:0.78rem;">Remove</button>
</form>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock %}