fix(plugins): show load-failure reasons on the page the banner links to
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 45s
CI / integration (push) Successful in 2m24s
CI / publish (push) Successful in 1m14s

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>
This commit is contained in:
2026-08-13 08:36:35 -04:00
co-authored by Claude Opus 5
parent 8c50ff242c
commit 0da88c7076
4 changed files with 125 additions and 28 deletions
+28 -9
View File
@@ -95,31 +95,50 @@
<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
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 name in orphaned_plugins %}
{% 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%;background:var(--yellow);flex-shrink:0;"
title="Configured but not installed"></span>
<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);">{{ name }}</span>
<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;">
Settings key <code>plugin.{{ name }}</code> has no matching plugin.
{% 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>
<form method="post" action="/settings/plugins/orphans/{{ name }}/remove/"
onsubmit="return confirm('Remove stored settings for &quot;{{ name }}&quot;? Its saved configuration, including any credentials, will be discarded. This cannot be undone.');"
{% if orphan.removable %}
<form method="post" action="/settings/plugins/orphans/{{ orphan.name }}/remove/"
onsubmit="return 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;">
<button type="submit" class="btn btn-danger btn-sm" style="font-size:0.78rem;">Remove</button>
</form>
{% endif %}
</div>
{% endfor %}
</div>