The admin banner counts every plugin load failure, but settings/plugins.html only rendered failure reasons inside the plugin card macro -- which iterates discovered plugins. A plugin that failed BECAUSE it has no directory was therefore counted in the banner and shown nowhere on the page the banner links to. That dead end is what the operator hit: "1 plugin failed to load" on the dashboard, nothing wrong on the details page, and no way to clean it up. Folds the reason into the "Configured but not installed" rows rather than adding a competing section, and drives that list from the UNION of stored plugin.* settings and recorded load failures instead of stored settings alone. A failure with no stored row -- a plugin enabled by a DEFAULTS entry, which is exactly what plugin.http was -- would otherwise still be counted and still be invisible. Rows carry `removable`: only a plugin with a real stored row can be cleaned up, so a default-declared one shows the reason and an explanation instead of a Remove button that would delete nothing while the default reasserts it. Rows with a reason read as an error (red, "Failed to load"); rows that merely have leftover config stay a warning. A discovered plugin that failed is excluded -- its own card already shows the reason. Replaces find_orphaned_plugin_names outright rather than adding a second overlapping helper; it had one caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
10 KiB
HTML
185 lines
10 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;">
|
|
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 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 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:600;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/"
|
|
onsubmit="return confirm('Remove stored settings for "{{ orphan.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>
|
|
{% endif %}
|
|
</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 %}
|