Files
FabledSteward/fabledscryer/templates/settings/plugins.html
T
bvandeusen 230b542015 feat: rename to FabledScryer, multi-dashboard system, plugin management, branding
- Rename package fablednetmon → fabledscryer throughout
- Multi-dashboard: ownership, per-user defaults, HTMX edit (add/remove/reorder)
- Read-only share tokens scoped to individual dashboards
- Dashboard edit is HTMX-driven (no page reloads)
- Plugin management system: remote catalog, download/install, hot-reload, in-app restart
- plugin_index.py: fetch/cache remote index.yaml; default URL → bvandeusen/fabledscryer-plugins
- plugin_manager.py: download_and_install_plugin, hot_reload_plugin, restart_app
  - ZIP extraction handles GitHub archive formats (name-v1.0.0/, name-main/)
- Settings split into tabbed sections: General, Notifications, Ansible, Plugins
- Plugins tab: catalog browser (HTMX), install/activate/update/restart actions
- UI/branding: dark palette (#07071a), crystal ball SVG logo, animated star field,
  Libertinus Serif applied to headings, nav, labels, and section titles
- Widget registry (core/widgets.py) for dashboard plugin integration
- UPS widget.html (dashboard card) and settings/_tabs.html include
- Migrations 0005–0008: dashboards, is_default, ownership, share tokens
- docs/plugins/: writing-a-plugin.md updated with publishing guide,
  index.yaml.example template for fabledscryer-plugins repo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:27:56 -04:00

162 lines
7.6 KiB
HTML

{# fabledscryer/templates/settings/plugins.html #}
{% extends "base.html" %}
{% block title %}Settings — Plugins — Fabled Scryer{% endblock %}
{% block content %}
{% set active_tab = "plugins" %}
{% include "settings/_tabs.html" %}
{# ── Restart banner (replaced by _restart_pending.html after action) #}
<div id="restart-banner"></div>
{# ── Installed Plugins ─────────────────────────────────────────────────────── #}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
<div class="section-title">Installed Plugins</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>
{% if not discovered_plugins %}
<div class="card" style="color:var(--text-muted);font-size:0.9rem;">
No plugins found in the plugin directory.
</div>
{% else %}
<form method="post" action="/settings/plugins/">
<div style="display:grid;gap:1rem;max-width:720px;">
{# Plugin index URL #}
<div class="card" style="padding:1rem 1.25rem;">
<div class="section-title" style="margin-bottom:0.6rem;">Plugin Index URL</div>
<div class="form-group" style="margin:0;">
<input type="url" name="plugins.index_url"
value="{{ settings.get('plugins.index_url', '') }}"
placeholder="https://raw.githubusercontent.com/your-org/fabledscryer-plugins/main/index.yaml"
style="font-size:0.85rem;">
</div>
<div style="font-size:0.77rem;color:var(--text-muted);margin-top:0.4rem;">
URL to the remote <code>index.yaml</code> used by the plugin catalog below.
</div>
</div>
{% for plugin in discovered_plugins %}
<div class="card" style="padding:1.25rem;">
{# Plugin header: enable toggle + name/description #}
<div style="display:flex;align-items:flex-start;gap:0.75rem;margin-bottom:{% if plugin.get('config') %}1rem{% else %}0{% endif %};">
<input type="checkbox"
name="plugin.{{ plugin._dir }}.enabled"
id="plugin-{{ plugin._dir }}-enabled"
{% if plugin._enabled %}checked{% endif %}
style="margin-top:0.2rem;flex-shrink:0;">
<label for="plugin-{{ plugin._dir }}-enabled" style="margin:0;cursor:pointer;">
<span style="font-weight:600;color:var(--text);">{{ plugin.get('name', plugin._dir) }}</span>
<span style="color:var(--text-muted);font-size:0.78rem;margin-left:0.4rem;">v{{ plugin.get('version', '?') }}</span>
{% if plugin.get('description') %}
<div style="color:var(--text-muted);font-size:0.83rem;margin-top:0.15rem;font-weight:normal;">{{ plugin.description }}</div>
{% endif %}
</label>
</div>
{# Config fields — only shown when there are config keys #}
{% if plugin.get('config') %}
<div style="border-top:1px solid var(--border);padding-top:1rem;display:grid;gap:0.6rem;">
{% for cfg_key, cfg_default in plugin.config.items() %}
{% if cfg_default is mapping %}
{# Nested dict group #}
<div style="padding:0.6rem 0.75rem;background:var(--bg);border-radius:5px;border:1px solid var(--border);">
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.6rem;">{{ cfg_key }}</div>
{% set sub_cfg = plugin._config.get(cfg_key, cfg_default) if plugin._config.get(cfg_key, cfg_default) is mapping else cfg_default %}
<div style="display:grid;gap:0.5rem;">
{% for sub_key, sub_default in cfg_default.items() %}
{% if sub_default is sameas false or sub_default is sameas true %}
<div style="display:flex;align-items:center;gap:0.5rem;">
<input type="checkbox"
name="plugin.{{ plugin._dir }}.{{ cfg_key }}.{{ sub_key }}"
id="plugin-{{ plugin._dir }}-{{ cfg_key }}-{{ sub_key }}"
{% if sub_cfg.get(sub_key, sub_default) %}checked{% endif %}>
<label for="plugin-{{ plugin._dir }}-{{ cfg_key }}-{{ sub_key }}"
style="margin:0;font-size:0.85rem;color:var(--text);">{{ sub_key }}</label>
</div>
{% else %}
<div class="form-group" style="margin:0;">
<label style="font-size:0.82rem;">{{ sub_key }}</label>
<input type="text"
name="plugin.{{ plugin._dir }}.{{ cfg_key }}.{{ sub_key }}"
value="{{ sub_cfg.get(sub_key, sub_default) }}">
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% elif cfg_default is sameas false or cfg_default is sameas true %}
<div style="display:flex;align-items:center;gap:0.5rem;">
<input type="checkbox"
name="plugin.{{ plugin._dir }}.{{ cfg_key }}"
id="plugin-{{ plugin._dir }}-{{ cfg_key }}"
{% if plugin._config.get(cfg_key, cfg_default) %}checked{% endif %}>
<label for="plugin-{{ plugin._dir }}-{{ cfg_key }}"
style="margin:0;font-size:0.85rem;color:var(--text);">{{ cfg_key }}</label>
</div>
{% else %}
<div class="form-group" style="margin:0;">
<label style="font-size:0.82rem;">{{ cfg_key }}</label>
<input type="{% if 'password' in cfg_key or 'secret' in cfg_key %}password{% else %}text{% endif %}"
name="plugin.{{ plugin._dir }}.{{ cfg_key }}"
value="{% if 'password' in cfg_key or 'secret' in cfg_key %}{% else %}{{ plugin._config.get(cfg_key, cfg_default) }}{% endif %}"
{% if 'password' in cfg_key or 'secret' in cfg_key %}placeholder="••••••••"{% endif %}>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<div style="margin-top:1rem;display:flex;align-items:center;gap:1rem;">
<button type="submit" class="btn">Save</button>
<span style="font-size:0.82rem;color:var(--text-muted);">Enable/disable changes take effect after a restart.</span>
</div>
</form>
{% endif %}
{# ── Plugin Catalog ────────────────────────────────────────────────────────── #}
<div style="margin-top:2rem;">
<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>
{% set index_url = settings.get('plugins.index_url', '') %}
{% if index_url %}
<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;">
Configure a Plugin Index URL above to browse available plugins.
</div>
{% endif %}
</div>
{% endblock %}