Feature: HTMX + Jinja2 Frontend ersetzt Next.js komplett

- Kein Node.js, kein npm, kein Build-Schritt mehr
- HTMX 2.0.4 + PicoCSS 2 vendored in backend/static/
- Jinja2 Templates für alle 9 Seiten (Dashboard, ZFS, Snapshots,
  Shares, Identities, Logs, Services, Navigator, Login)
- HTMX Fragments für Live-Updates (30s Polling Dashboard)
- JWT als httpOnly Cookie statt localStorage
- Disk Usage zeigt TB/PB korrekt (Jinja2 serverseitig formatiert)
- Update-safe: nur Python-Deps, keine npm-Abhängigkeiten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 18:45:46 +02:00
parent 654df5b98f
commit 5ecd143535
44 changed files with 1123 additions and 6129 deletions
+89
View File
@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% block title %}Shares ZMB Webui{% endblock %}
{% block content %}
<h2>File Sharing</h2>
<!-- Tabs via CSS -->
<div role="tablist" style="display:flex;gap:0.5rem;margin-bottom:1.5rem;border-bottom:1px solid var(--pico-muted-border-color);padding-bottom:0.5rem">
<button role="tab" onclick="showTab('samba')" id="tab-samba" class="outline">Samba (SMB)</button>
<button role="tab" onclick="showTab('nfs')" id="tab-nfs" class="outline secondary">NFS</button>
<button role="tab" onclick="showTab('config')" id="tab-config" class="outline secondary">Samba Config</button>
</div>
<!-- Samba Shares -->
<div id="panel-samba">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h3 style="margin:0">Samba Shares</h3>
<button onclick="document.getElementById('new-samba-form').style.display='block'">&#43; Neu</button>
</div>
<div id="new-samba-form" style="display:none;margin-bottom:1rem">
<article>
<h4>Neuer Samba Share</h4>
<form hx-post="/api/shares/samba" hx-target="#samba-list" hx-swap="innerHTML"
hx-on::after-request="this.reset();document.getElementById('new-samba-form').style.display='none'">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
<div><label>Name</label><input name="name" required placeholder="myshare"></div>
<div><label>Pfad</label><input name="path" required placeholder="/tank/share"></div>
<div><label>Kommentar</label><input name="comment" placeholder="Beschreibung"></div>
</div>
<div class="actions"><button type="submit">Erstellen</button><button type="button" class="outline secondary" onclick="document.getElementById('new-samba-form').style.display='none'">Abbrechen</button></div>
</form>
</article>
</div>
<div id="samba-list"
hx-get="/fragments/samba-shares"
hx-trigger="load">
<p aria-busy="true">Lade...</p>
</div>
</div>
<!-- NFS -->
<div id="panel-nfs" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h3 style="margin:0">NFS Exports</h3>
<button onclick="document.getElementById('new-nfs-form').style.display='block'">&#43; Neu</button>
</div>
<div id="new-nfs-form" style="display:none;margin-bottom:1rem">
<article>
<h4>Neuer NFS Export</h4>
<form hx-post="/api/shares/nfs" hx-target="#nfs-list" hx-swap="innerHTML"
hx-on::after-request="this.reset();document.getElementById('new-nfs-form').style.display='none'">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
<div><label>Pfad</label><input name="path" required placeholder="/tank/share"></div>
<div><label>Clients</label><input name="clients" required placeholder="*(rw,sync,no_subtree_check)"></div>
<div><label>Kommentar</label><input name="comment" placeholder="Beschreibung"></div>
</div>
<div class="actions"><button type="submit">Erstellen</button><button type="button" class="outline secondary" onclick="document.getElementById('new-nfs-form').style.display='none'">Abbrechen</button></div>
</form>
</article>
</div>
<div id="nfs-list"
hx-get="/fragments/nfs-shares"
hx-trigger="load">
<p aria-busy="true">Lade...</p>
</div>
</div>
<!-- Samba Config -->
<div id="panel-config" style="display:none">
<div id="samba-config"
hx-get="/fragments/samba-config"
hx-trigger="load">
<p aria-busy="true">Lade...</p>
</div>
</div>
<script>
function showTab(name) {
['samba','nfs','config'].forEach(t => {
document.getElementById('panel-' + t).style.display = t === name ? 'block' : 'none';
const btn = document.getElementById('tab-' + t);
btn.className = t === name ? 'outline' : 'outline secondary';
});
}
</script>
{% endblock %}