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>
This commit is contained in:
@@ -1,114 +0,0 @@
|
||||
{# settings/_catalog_partial.html — HTMX partial for plugin catalog #}
|
||||
<div id="plugin-catalog">
|
||||
|
||||
{% if error %}
|
||||
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0.75rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;margin-bottom:0.75rem;">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if catalog %}
|
||||
<div style="display:grid;gap:0.75rem;">
|
||||
{% for plugin in catalog %}
|
||||
{% set is_installed = plugin.name in installed_names %}
|
||||
{% set is_loaded = plugin.name in loaded_names %}
|
||||
{% set installed_ver = installed_versions.get(plugin.name, "") %}
|
||||
{# update_available: catalog version is strictly newer than what's on disk #}
|
||||
{% set update_available = is_installed and installed_ver and installed_ver != plugin.version %}
|
||||
|
||||
<div class="card" style="margin-bottom:0;padding:1rem 1.25rem;{% if update_available %}border-color:var(--gold);{% endif %}">
|
||||
<div style="display:flex;align-items:flex-start;gap:1rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;">
|
||||
<span style="font-weight:500;color:var(--text);font-size:0.9rem;">{{ plugin.name }}</span>
|
||||
<span style="font-size:0.75rem;color:var(--text-muted);">v{{ plugin.version }}</span>
|
||||
|
||||
{% if update_available %}
|
||||
<span class="badge badge-gold" style="font-size:0.68rem;">
|
||||
Update available — installed v{{ installed_ver }}
|
||||
</span>
|
||||
{% elif is_loaded %}
|
||||
<span class="badge badge-green" style="font-size:0.68rem;">Active</span>
|
||||
{% elif is_installed %}
|
||||
<span class="badge" style="background:var(--bg-elevated);color:var(--text-muted);font-size:0.68rem;">Installed</span>
|
||||
{% endif %}
|
||||
|
||||
{% for tag in plugin.tags %}
|
||||
<span style="font-size:0.68rem;padding:0.1em 0.4em;background:var(--bg-elevated);color:var(--text-muted);border-radius:3px;">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if plugin.description %}
|
||||
<div style="font-size:0.82rem;color:var(--text-muted);margin-top:0.25rem;">{{ plugin.description }}</div>
|
||||
{% endif %}
|
||||
<div style="font-size:0.75rem;color:var(--text-dim);margin-top:0.15rem;display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;">
|
||||
{% if plugin.author %}<span>by {{ plugin.author }}</span>{% endif %}
|
||||
{% if plugin.license %}<span style="color:var(--text-muted);">{{ plugin.license }}</span>{% endif %}
|
||||
{% if plugin.min_app_version %}<span style="color:var(--text-muted);">requires v{{ plugin.min_app_version }}+</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;flex-shrink:0;align-items:center;">
|
||||
{% if plugin.homepage %}
|
||||
<a href="{{ plugin.homepage }}" target="_blank" rel="noopener"
|
||||
class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Docs</a>
|
||||
{% elif plugin.repository_url %}
|
||||
<a href="{{ plugin.repository_url }}" target="_blank" rel="noopener"
|
||||
class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Source</a>
|
||||
{% endif %}
|
||||
|
||||
{% if (update_available or not is_installed) and not plugin.checksum_sha256 %}
|
||||
{# Steward installs only what it can verify against the catalog's checksum #}
|
||||
<span style="font-size:0.75rem;color:var(--text-muted);max-width:14rem;text-align:right;"
|
||||
title="Add checksum_sha256 to this plugin's entry in the repository's index.yaml">
|
||||
Can't {{ "update" if update_available else "install" }}: the catalog lists no checksum
|
||||
</span>
|
||||
|
||||
{% elif update_available %}
|
||||
{# Installed but outdated — offer update (download + restart) #}
|
||||
<form method="post"
|
||||
action="/settings/plugins/install/{{ plugin.name }}"
|
||||
hx-post="/settings/plugins/install/{{ plugin.name }}"
|
||||
hx-target="#catalog-result-{{ plugin.name }}"
|
||||
hx-swap="outerHTML"
|
||||
style="margin:0;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-warn btn-sm" style="font-size:0.78rem;">Update</button>
|
||||
</form>
|
||||
|
||||
{% elif is_loaded %}
|
||||
{# Active and up to date — no action needed #}
|
||||
|
||||
{% elif is_installed %}
|
||||
{# Installed but not active — offer hot-reload #}
|
||||
<form method="post"
|
||||
action="/settings/plugins/reload/{{ plugin.name }}"
|
||||
hx-post="/settings/plugins/reload/{{ plugin.name }}"
|
||||
hx-target="#catalog-result-{{ plugin.name }}"
|
||||
hx-swap="outerHTML"
|
||||
style="margin:0;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm" style="font-size:0.78rem;">Activate</button>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
{# Not installed — offer install #}
|
||||
<form method="post"
|
||||
action="/settings/plugins/install/{{ plugin.name }}"
|
||||
hx-post="/settings/plugins/install/{{ plugin.name }}"
|
||||
hx-target="#catalog-result-{{ plugin.name }}"
|
||||
hx-swap="outerHTML"
|
||||
style="margin:0;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm" style="font-size:0.78rem;">Install</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Inline result placeholder — replaced by _install_result.html after action #}
|
||||
<div id="catalog-result-{{ plugin.name }}"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif not error %}
|
||||
<div style="color:var(--text-muted);font-size:0.85rem;">No plugins listed in the catalog.</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
{# settings/_install_result.html — inline result after install/reload action #}
|
||||
<div id="catalog-result-{{ name }}"
|
||||
style="margin-top:0.6rem;padding:0.5rem 0.75rem;border-radius:5px;font-size:0.82rem;
|
||||
background:{% if success %}var(--green-dim){% else %}var(--red-dim){% endif %};
|
||||
color:{% if success %}var(--green){% else %}#ff8090{% endif %};">
|
||||
{{ message }}
|
||||
{% if restart_required %}
|
||||
·
|
||||
<form method="post" action="/settings/plugins/restart/"
|
||||
hx-post="/settings/plugins/restart/"
|
||||
hx-target="#restart-banner"
|
||||
hx-swap="outerHTML"
|
||||
style="display:inline;margin:0;">{{ csrf_field() }}
|
||||
<button type="submit"
|
||||
style="background:none;border:none;padding:0;color:var(--yellow);cursor:pointer;font-size:0.82rem;text-decoration:underline;">
|
||||
Restart now
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,99 +0,0 @@
|
||||
{# settings/_plugin_repos.html — HTMX partial for plugin repository management #}
|
||||
<div id="plugin-repos">
|
||||
|
||||
{% if repos %}
|
||||
<div style="display:grid;gap:0.6rem;margin-bottom:0.6rem;">
|
||||
{% for repo in repos %}
|
||||
{% set idx = loop.index0 %}
|
||||
|
||||
<div class="card" style="padding:0;overflow:visible;">
|
||||
|
||||
{# ── View row ────────────────────────────────────────────────────────── #}
|
||||
<div id="plugin-repo-view-{{ idx }}"
|
||||
style="display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:500;font-size:0.88rem;">{{ repo.name or repo.url }}</div>
|
||||
<div style="font-size:0.77rem;color:var(--text-muted);margin-top:0.1rem;
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ repo.url }}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.4rem;flex-shrink:0;">
|
||||
<button class="btn btn-ghost btn-sm" style="font-size:0.78rem;"
|
||||
onclick="toggleRepoEdit({{ idx|int }})">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" style="font-size:0.78rem;"
|
||||
hx-post="/settings/plugins/repos/{{ idx }}/remove"
|
||||
hx-target="#plugin-repos"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Remove repository '{{ repo.name or repo.url }}'?">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Inline edit form ──────────────────────────────────────────────── #}
|
||||
<div id="plugin-repo-edit-{{ idx }}"
|
||||
style="display:none;border-top:1px solid var(--border);padding:1rem;">
|
||||
<form hx-post="/settings/plugins/repos/{{ idx }}/save"
|
||||
hx-target="#plugin-repos"
|
||||
hx-swap="outerHTML">
|
||||
<div style="display:grid;grid-template-columns:1fr 2fr;gap:0.6rem;">
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label style="font-size:0.8rem;">Name</label>
|
||||
<input type="text" name="name" value="{{ repo.name }}" placeholder="My Plugins">
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label style="font-size:0.8rem;">Index URL</label>
|
||||
<input type="url" name="url" value="{{ repo.url }}"
|
||||
placeholder="https://example.com/plugins/index.yaml" required>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
|
||||
<button type="submit" class="btn btn-sm">Save</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm"
|
||||
onclick="toggleRepoEdit({{ idx|int }})">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="color:var(--text-muted);font-size:0.88rem;text-align:center;padding:1.5rem;margin-bottom:0.6rem;">
|
||||
No repositories charted. Add one below.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Add repository ────────────────────────────────────────────────────────── #}
|
||||
<details class="card" style="padding:0;overflow:hidden;">
|
||||
<summary style="display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;
|
||||
cursor:pointer;list-style:none;user-select:none;">
|
||||
<span style="flex:1;font-weight:500;font-size:0.875rem;color:var(--text);">Add Repository</span>
|
||||
<span class="btn btn-sm" style="pointer-events:none;">New ▾</span>
|
||||
</summary>
|
||||
<form hx-post="/settings/plugins/repos/add"
|
||||
hx-target="#plugin-repos"
|
||||
hx-swap="outerHTML"
|
||||
style="padding:1rem;padding-top:0;border-top:1px solid var(--border);">
|
||||
<div style="display:grid;grid-template-columns:1fr 2fr;gap:0.6rem;padding-top:1rem;">
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label style="font-size:0.8rem;">Name</label>
|
||||
<input type="text" name="name" placeholder="My Plugins">
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label style="font-size:0.8rem;">Index URL</label>
|
||||
<input type="url" name="url"
|
||||
placeholder="https://example.com/plugins/index.yaml" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm" style="margin-top:0.75rem;width:100%;">
|
||||
Add Repository
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleRepoEdit(idx) {
|
||||
var edit = document.getElementById('plugin-repo-edit-' + idx);
|
||||
edit.style.display = edit.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
@@ -76,7 +76,8 @@
|
||||
<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.
|
||||
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;">
|
||||
@@ -84,7 +85,7 @@
|
||||
</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.
|
||||
No integrations installed.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -97,8 +98,8 @@
|
||||
<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
|
||||
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;">
|
||||
@@ -145,40 +146,4 @@
|
||||
</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-post="/settings/plugins/catalog/refresh"
|
||||
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 %}
|
||||
|
||||
Reference in New Issue
Block a user