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:
@@ -0,0 +1,29 @@
|
||||
{% macro fmt_bytes(b) %}
|
||||
{%- if b == 0 %}0 B
|
||||
{%- elif b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB
|
||||
{%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB
|
||||
{%- elif b < 1099511627776 %}{{ "%.1f"|format(b/1073741824) }} GB
|
||||
{%- else %}{{ "%.1f"|format(b/1099511627776) }} TB
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% if not filesystems %}
|
||||
<p>Keine Dateisysteme gefunden.</p>
|
||||
{% else %}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem">
|
||||
{% for fs in filesystems %}
|
||||
<article style="margin:0;padding:1rem">
|
||||
<strong>{{ fs.mountpoint }}</strong>
|
||||
<div class="progress-bar-wrap" style="margin:0.4rem 0">
|
||||
<div class="progress-bar {% if fs.capacity > 85 %}bar-red{% elif fs.capacity > 70 %}bar-yellow{% else %}bar-blue{% endif %}" style="width:{{ fs.capacity }}%"></div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;font-size:0.8rem;gap:0.25rem">
|
||||
<div><div style="color:var(--pico-muted-color)">Total</div><strong>{{ fmt_bytes(fs.total) }}</strong></div>
|
||||
<div><div style="color:var(--pico-muted-color)">Belegt</div><strong>{{ fmt_bytes(fs.used) }}</strong></div>
|
||||
<div><div style="color:var(--pico-muted-color)">Frei</div><strong>{{ fmt_bytes(fs.available) }}</strong></div>
|
||||
</div>
|
||||
<small style="color:var(--pico-muted-color)">{{ fs.filesystem }}</small>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if not groups %}
|
||||
<p>Keine Gruppen gefunden.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Gruppe</th><th>GID</th><th>Mitglieder</th></tr></thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td><strong>{{ group.name }}</strong></td>
|
||||
<td>{{ group.gid }}</td>
|
||||
<td style="font-size:0.85rem">{{ group.members | join(', ') if group.members else '–' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div style="background:var(--pico-code-background,#1a1a2e);border-radius:6px;padding:1rem;overflow-x:auto;max-height:70vh;overflow-y:auto;font-family:monospace;font-size:0.8rem;line-height:1.5">
|
||||
{% if logs %}
|
||||
{% for line in logs %}
|
||||
<div style="{% if 'ERROR' in line or 'error' in line %}color:#fca5a5{% elif 'WARN' in line or 'warn' in line %}color:#fde68a{% elif 'INFO' in line %}color:#a5f3fc{% else %}color:var(--pico-muted-color){% endif %}">{{ line | e }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span style="color:var(--pico-muted-color)">Keine Logs verfügbar.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
{% if error %}
|
||||
<p style="color:#fca5a5">⚠ {{ error }}</p>
|
||||
{% else %}
|
||||
{% if parent_path %}
|
||||
<div style="margin-bottom:0.5rem">
|
||||
<button class="outline secondary"
|
||||
hx-get="/fragments/navigator?path={{ parent_path | urlencode }}"
|
||||
hx-target="#nav-content"
|
||||
hx-swap="innerHTML">↑ Übergeordnet</button>
|
||||
<span style="font-family:monospace;margin-left:0.5rem;color:var(--pico-muted-color)">{{ current_path }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Größe</th><th>Geändert</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if item.type == 'directory' %}
|
||||
<button class="outline secondary" style="padding:0.1rem 0.4rem;font-size:0.85rem"
|
||||
hx-get="/fragments/navigator?path={{ item.path | urlencode }}"
|
||||
hx-target="#nav-content"
|
||||
hx-swap="innerHTML">
|
||||
📁 {{ item.name }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span style="font-family:monospace;font-size:0.85rem">📄 {{ item.name }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="badge">{{ item.type }}</span></td>
|
||||
<td style="font-size:0.85rem">{{ item.size }}</td>
|
||||
<td style="font-size:0.8rem;color:var(--pico-muted-color)">{{ item.modified }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not items %}
|
||||
<p style="color:var(--pico-muted-color)">Verzeichnis ist leer.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% macro fmt_bytes(b) %}
|
||||
{%- if b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB
|
||||
{%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB
|
||||
{%- else %}{{ "%.2f"|format(b/1073741824) }} GB
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1rem">
|
||||
{% for iface in interfaces %}
|
||||
{% if iface.name not in ['lo'] %}
|
||||
<article style="margin:0;padding:1rem">
|
||||
<strong>{{ iface.name }}</strong>
|
||||
<table style="margin:0.5rem 0;font-size:0.85rem">
|
||||
<tr><td style="color:var(--pico-muted-color)">RX</td><td><strong>{{ fmt_bytes(iface.rx_bytes) }}</strong></td><td style="color:var(--pico-muted-color)">{{ "{:,}".format(iface.rx_packets) }} pkt</td></tr>
|
||||
<tr><td style="color:var(--pico-muted-color)">TX</td><td><strong>{{ fmt_bytes(iface.tx_bytes) }}</strong></td><td style="color:var(--pico-muted-color)">{{ "{:,}".format(iface.tx_packets) }} pkt</td></tr>
|
||||
{% if iface.rx_drops > 0 or iface.tx_drops > 0 %}
|
||||
<tr><td colspan="3" style="color:#fca5a5;font-size:0.75rem">⚠ {{ iface.rx_drops + iface.tx_drops }} drops</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% if not exports %}
|
||||
<p>Keine NFS Exports konfiguriert.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Pfad</th><th>Clients</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for exp in exports %}
|
||||
<tr id="nfs-{{ loop.index }}">
|
||||
<td style="font-family:monospace">{{ exp.path }}</td>
|
||||
<td style="font-size:0.85rem">{{ exp.clients }}</td>
|
||||
<td>
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/shares/nfs/{{ exp.path | urlencode }}"
|
||||
hx-confirm="NFS Export '{{ exp.path }}' löschen?"
|
||||
hx-target="#nfs-{{ loop.index }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% macro fmt_bytes(b) %}
|
||||
{%- if b == 0 %}0 B
|
||||
{%- elif b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB
|
||||
{%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB
|
||||
{%- elif b < 1099511627776 %}{{ "%.1f"|format(b/1073741824) }} GB
|
||||
{%- else %}{{ "%.1f"|format(b/1099511627776) }} TB
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% if not pools %}
|
||||
<p>Keine ZFS Pools verfügbar.</p>
|
||||
{% else %}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem">
|
||||
{% for pool in pools %}
|
||||
{% set pct = (pool.allocated / pool.size * 100)|int if pool.size else 0 %}
|
||||
<article style="margin:0;padding:1rem">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<strong>{{ pool.name }}</strong>
|
||||
<span class="badge {% if pool.health == 'ONLINE' %}badge-green{% elif pool.health == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}">{{ pool.health }}</span>
|
||||
</div>
|
||||
<div class="progress-bar-wrap" style="margin:0.5rem 0">
|
||||
<div class="progress-bar {% if pct > 85 %}bar-red{% elif pct > 70 %}bar-yellow{% else %}bar-blue{% endif %}" style="width:{{ pct }}%"></div>
|
||||
</div>
|
||||
<small style="color:var(--pico-muted-color)">
|
||||
{{ fmt_bytes(pool.allocated) }} / {{ fmt_bytes(pool.size) }} ({{ pct }}%)
|
||||
</small>
|
||||
{% if pool.status and pool.status != 'ONLINE' %}
|
||||
<p style="margin:0.5rem 0 0;font-size:0.8rem;color:#fca5a5">{{ pool.status }}</p>
|
||||
{% endif %}
|
||||
<div class="actions" style="margin-top:0.75rem">
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/pools/{{ pool.name }}/scrub"
|
||||
hx-confirm="Scrub für {{ pool.name }} starten?"
|
||||
hx-target="closest article"
|
||||
hx-swap="none">Scrub</button>
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/pools/{{ pool.name }}/clear"
|
||||
hx-confirm="Fehler für {{ pool.name }} löschen?"
|
||||
hx-swap="none">Clear</button>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% macro fmt_bytes(b) %}
|
||||
{%- if b == 0 %}0 B
|
||||
{%- elif b < 1024 %}{{ b }} B
|
||||
{%- elif b < 1048576 %}{{ "%.1f"|format(b/1024) }} KB
|
||||
{%- elif b < 1073741824 %}{{ "%.1f"|format(b/1048576) }} MB
|
||||
{%- elif b < 1099511627776 %}{{ "%.1f"|format(b/1073741824) }} GB
|
||||
{%- else %}{{ "%.1f"|format(b/1099511627776) }} TB
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>CPU Load</h3>
|
||||
<p class="value">{{ cpu.load_average[0] if cpu.load_average else '–' }}</p>
|
||||
<small>{{ cpu.count }} Kerne</small>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>RAM</h3>
|
||||
<p class="value">{{ fmt_bytes(mem.used) }}</p>
|
||||
<small>von {{ fmt_bytes(mem.total) }}</small>
|
||||
{% set pct = (mem.used / mem.total * 100)|int if mem.total else 0 %}
|
||||
<div class="progress-bar-wrap" style="margin-top:0.4rem">
|
||||
<div class="progress-bar {% if pct > 85 %}bar-red{% elif pct > 70 %}bar-yellow{% else %}bar-blue{% endif %}" style="width:{{ pct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Uptime</h3>
|
||||
<p class="value">{{ uptime.uptime_string if uptime else '–' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Pools</h3>
|
||||
<p class="value">{{ pools|length }}</p>
|
||||
<small>
|
||||
{% for p in pools %}
|
||||
<span class="badge {% if p.health == 'ONLINE' %}badge-green{% elif p.health == 'DEGRADED' %}badge-yellow{% else %}badge-red{% endif %}">{{ p.name }}</span>
|
||||
{% endfor %}
|
||||
</small>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
{% set fruit_keys = ['fruit:encoding','fruit:metadata','fruit:zero_file_id','fruit:nfs_aces'] %}
|
||||
{% set has_macos = params | selectattr('key', 'in', fruit_keys) | list | length == 4 %}
|
||||
|
||||
<h3>Samba Global Config</h3>
|
||||
|
||||
<!-- MacOS Toggle -->
|
||||
<article style="margin-bottom:1rem">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div>
|
||||
<strong>MacOS Shares (Fruit)</strong>
|
||||
<p style="margin:0;font-size:0.85rem;color:var(--pico-muted-color)">Optimiert alle Shares für macOS (fruit, catia, streams_xattr)</p>
|
||||
</div>
|
||||
<button class="{% if has_macos %}outline secondary{% else %}outline{% endif %}"
|
||||
hx-post="/fragments/samba-config/toggle-macos?enable={{ 'false' if has_macos else 'true' }}"
|
||||
hx-target="#samba-config"
|
||||
hx-swap="innerHTML">
|
||||
{% if has_macos %}MacOS deaktivieren{% else %}MacOS aktivieren{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% if has_macos %}
|
||||
<div style="margin-top:0.5rem">
|
||||
{% for p in params if p.key in fruit_keys %}
|
||||
<span class="badge badge-blue" style="margin:0.1rem">{{ p.key }} = {{ p.value }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<!-- Parameter Tabelle -->
|
||||
<table>
|
||||
<thead><tr><th>Parameter</th><th>Wert</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in params %}
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ p.key }}</td>
|
||||
<td style="font-size:0.85rem;word-break:break-all">{{ p.value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,23 @@
|
||||
{% if not shares %}
|
||||
<p>Keine Samba Shares konfiguriert.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Pfad</th><th>Kommentar</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for share in shares %}
|
||||
<tr id="samba-{{ share.name }}">
|
||||
<td><strong>{{ share.name }}</strong></td>
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ share.path }}</td>
|
||||
<td>{{ share.comment }}</td>
|
||||
<td>
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/shares/samba/{{ share.name }}"
|
||||
hx-confirm="Share '{{ share.name }}' löschen?"
|
||||
hx-target="#samba-{{ share.name }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if not services %}
|
||||
<p>Keine Dienste gefunden.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Dienst</th><th>Status</th><th>Beschreibung</th></tr></thead>
|
||||
<tbody>
|
||||
{% for svc in services %}
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:0.85rem">{{ svc.name }}</td>
|
||||
<td><span class="badge {% if svc.state == 'active' or svc.active == 'active' %}badge-green{% else %}badge-red{% endif %}">{{ svc.state or svc.active }}</span></td>
|
||||
<td style="font-size:0.85rem;color:var(--pico-muted-color)">{{ svc.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% if not users %}
|
||||
<p>Keine Benutzer gefunden.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead><tr><th>Benutzer</th><th>UID</th><th>Shell</th><th>Status</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr id="user-{{ user.username }}">
|
||||
<td><strong>{{ user.username }}</strong></td>
|
||||
<td>{{ user.uid }}</td>
|
||||
<td style="font-family:monospace;font-size:0.8rem">{{ user.shell }}</td>
|
||||
<td><span class="badge {% if user.locked %}badge-red{% else %}badge-green{% endif %}">{{ 'Gesperrt' if user.locked else 'Aktiv' }}</span></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="outline secondary"
|
||||
hx-post="/api/identities/users/{{ user.username }}/samba-password"
|
||||
hx-prompt="Samba Passwort für {{ user.username }}:"
|
||||
hx-swap="none">Samba PW</button>
|
||||
<button class="outline secondary"
|
||||
hx-delete="/api/identities/users/{{ user.username }}"
|
||||
hx-confirm="Benutzer '{{ user.username }}' löschen?"
|
||||
hx-target="#user-{{ user.username }}"
|
||||
hx-swap="outerHTML">Löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user