Operator saw the `http` plugin -- deleted when ping/dns/http were unified into the Monitor entity -- still reported as enabled, with no way to clear it. It was never an orphaned row. `plugin.http` was hardcoded in core DEFAULTS, so deleting the app_settings row did nothing: the default reasserted it on the next settings load. That is precisely why no cleanup UI could have fixed it. Rule 22 says the removed subsystem should have taken its setting with it. Purged the surviving references -- the DEFAULTS key, "http" in CAPABILITY_PLUGINS, the stale plugin_manager docstring example, and the capabilities blurb still advertising HTTP/uptime as a bundled capability -- plus a migration dropping any stored plugin.http row. Untouched: `http` as a MONITOR TYPE (icmp/tcp/dns/http) everywhere it appears, and http_001_initial, which is kept deliberately so existing DBs resolve the revision graph. For the general case, cleanup splits by provenance rather than being uniformly automatic or uniformly manual: Bundled plugins ship in the image and version atomically with core, so they cannot be transiently missing -- a plugin.* default with no bundled directory is unambiguously a bug. Guarded by a unit test that fails CI, which is the only part safe to automate. External plugins live in operator-mounted /data/plugins, where absence is ambiguous: unmounted volume, failed install, mid-upgrade. Auto-deleting their config would silently destroy unrecoverable credentials on a transient condition, so Settings now lists them under "Configured but not installed" with an explicit, confirmed, audit-logged Remove. The section hides entirely when empty, and the remove route refuses if the plugin is actually installed. Orphan detection reads STORED rows, never the DEFAULTS-merged view -- offering to remove a default-only key would be a lie -- and returns names only, since plugin config can hold credentials and listing orphans never needs their values. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
8.9 KiB
HTML
166 lines
8.9 KiB
HTML
{# steward/templates/settings/plugins.html #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs %}
|
|
{% block title %}Settings — Plugins — Steward{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Plugins", "")]) }}{% endblock %}
|
|
{% block content %}
|
|
{% set active_tab = "plugins" %}
|
|
{% include "settings/_tabs.html" %}
|
|
|
|
{# ── 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:600;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"
|
|
onsubmit="return confirm('Restart the app now? This will briefly interrupt all monitoring.')"
|
|
style="margin:0;">
|
|
<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 in the <a href="/hosts/">Hosts</a> and
|
|
<a href="/status">Status</a> sections, 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.
|
|
</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. Browse the catalog below.
|
|
</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;">
|
|
Stored settings for plugins Steward can't find. This usually means the plugin was
|
|
removed — but it also happens when an external plugin directory isn't mounted or an
|
|
install failed part-way, 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 name 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%;background:var(--yellow);flex-shrink:0;"
|
|
title="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:600;font-size:0.9rem;color:var(--text);">{{ name }}</span>
|
|
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
|
|
background:var(--yellow-dim);color:var(--yellow);">Not installed</span>
|
|
</div>
|
|
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.1rem;">
|
|
Settings key <code>plugin.{{ name }}</code> has no matching plugin.
|
|
</div>
|
|
</div>
|
|
<form method="post" action="/settings/plugins/orphans/{{ name }}/remove/"
|
|
onsubmit="return confirm('Remove stored settings for "{{ name }}"? Its saved configuration, including any credentials, will be discarded. This cannot be undone.');"
|
|
style="margin:0;flex-shrink:0;">
|
|
<button type="submit" class="btn btn-danger btn-sm" style="font-size:0.78rem;">Remove</button>
|
|
</form>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Plugin Repositories ───────────────────────────────────────────────────── #}
|
|
<div style="margin-bottom:2rem;max-width:720px;">
|
|
<div class="section-title" style="margin-bottom:0.75rem;">Plugin Repositories</div>
|
|
<p style="color:var(--text-muted);font-size:0.84rem;margin-bottom:0.75rem;">
|
|
Repositories are remote <code>index.yaml</code> files that Steward checks for available
|
|
and updated plugins. Add repos from other developers to expand the plugin catalog.
|
|
</p>
|
|
{% include "settings/_plugin_repos.html" %}
|
|
</div>
|
|
|
|
{# ── Plugin Catalog ────────────────────────────────────────────────────────── #}
|
|
<div style="max-width:720px;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
|
<div class="section-title">Plugin Catalog</div>
|
|
<button class="btn btn-ghost btn-sm" style="font-size:0.78rem;"
|
|
hx-get="/settings/plugins/catalog/?refresh=1"
|
|
hx-target="#plugin-catalog"
|
|
hx-swap="outerHTML">
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{% if repos %}
|
|
<div id="plugin-catalog"
|
|
hx-get="/settings/plugins/catalog/"
|
|
hx-trigger="load"
|
|
hx-swap="outerHTML">
|
|
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0;">Loading catalog…</div>
|
|
</div>
|
|
{% else %}
|
|
<div id="plugin-catalog" style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0;">
|
|
Add a plugin repository above to browse available plugins.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% endblock %}
|