Files
FabledSteward/steward/templates/settings/plugins.html
T
bvandeusenandClaude Opus 5.5 f67678998c
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 1m12s
CI / unit (push) Successful in 1m25s
CI / publish (push) Skipped
refactor(plugins): no plugin catalog; plugins ship in the image or are mounted (#5374)
First-party plugins have shipped inside the image since a7a281c, and the
catalog's only repository (Steward-plugins) answers 404, so Steward polled a
dead URL every minute for nothing. The operator chose to remove the catalog
entirely (#5214 "Review: readable and fast on the live instance").

Removed:
- core/plugin_index.py and the plugin_catalog_refresh startup task;
- the plugins.index_url default, and migration 0035 deletes the stored
  plugins.index_url / plugins.repositories rows;
- the Settings -> Plugins repository editor, catalog, refresh, install and
  reload routes and their templates;
- the download/checksum/extract install path in plugin_manager, and the
  PLUGIN_INSTALL_DIR config it wrote to;
- plugins/index.yaml, docs/plugins/index.yaml.example and the guide's
  "Publishing to the Catalog" section (now "Shipping a Plugin").

Kept: enabling and configuring plugins, hot-reload on enable, restart, orphan
cleanup, and the external root (/data/plugins) for operator-mounted plugins.
tests/test_no_plugin_catalog.py guards the routes, default and task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:15:55 -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 %}
{% 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: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 %}