Files
tesm/srv/tesm/templates/papierkorb.html
T
alientim 7b17c952ba Papierkorb: Sortieren/Suchen; Wartung: Einzel-Update-Button + Bulk-Neustart
Papierkorb: jede der fuenf Kategorien (Clients/Switche/Zugangsdaten/
Benutzer/Gruppen) bekommt jetzt dasselbe Sortieren/Suchen wie die
uebrigen Listenseiten (data-sortable + Such-Eingabe je Tabelle, nur
wenn die Kategorie tatsaechlich Eintraege hat).

Wartung: Update liess sich bisher nur per Bulk (Checkbox + 'Update
starten') ausloesen. Jedes Geraet bekommt jetzt zusaetzlich einen
eigenen 'Update'-Button (analog zum bereits vorhandenen 'Neustart'-
Button, ueber ein eigenstaendiges <form> + form="update-<mac>"-
Referenz -- kein verschachteltes <form>). Zusaetzlich ein 'Bulk-
Neustart'-Button neben 'Update starten', der dieselben Checkboxen/
dasselbe #updateForm nutzt wie das Bulk-Update (Ziel-Route wird per JS
nur fuer diesen einen Submit umgebogen), inkl. Bestaetigungsdialog wie
beim Einzel-Neustart.

Serverseitig war dafuer keine Aenderung noetig -- _maintenance_dispatch()
verarbeitete beliebig viele MACs schon immer generisch, die bisherigen
Formulare haben nur zufaellig immer genau eine bzw. alle uebermittelt.

Live auf POETEST verifiziert: Papierkorb zeigt Sortieren/Suchen nur bei
tatsaechlich befuellten Kategorien; der neue Einzel-Update-Button loest
korrekt einen Job fuer genau dieses eine Geraet aus (SSH-Verbindungs-
versuch zum richtigen Ziel bestaetigt, sauber behandelter Fehler, da das
Testsystem gerade offline war).
2026-08-22 09:53:28 +02:00

274 lines
14 KiB
HTML

{% extends "base.html" %}
{% set active_page = "papierkorb" %}
{% block page_title %}Papierkorb{% endblock %}
{% block page_sub %}<div class="topbar-sub">Gelöschte Clients, Switche, Zugangsdaten, Benutzer und Gruppen</div>{% endblock %}
{% block content %}
<div class="section-head">
<div>
<h2 style="font-size:16px;">Papierkorb</h2>
<div class="hint">
Gelöschte Clients, Switche, Zugangsdaten, Benutzer und Gruppen — werden nach
{{ trash_retention_days }} Tag{{ 'en' if trash_retention_days != 1 else '' }} automatisch endgültig gelöscht
(einstellbar unter Systemeinstellungen). AD/LDAP-Benutzer landen nie hier, da sie sich bei jedem gültigen
Login automatisch neu anlegen.
</div>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:16px;">
<div class="card card-pad">
<h3 style="font-size:14px; margin:0 0 12px;">Clients ({{ trash.devices|length }})</h3>
{% if trash.devices %}
<div class="table-wrap">
<div class="table-toolbar">
<div class="search-input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="devicesTrashSearch" placeholder="Clients durchsuchen…" oninput="filterTable('devicesTrashSearch','devicesTrashTable')">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" id="devicesTrashTable" data-sortable>
<thead><tr>
<th data-sort-key="name">Name</th>
<th data-sort-key="ip">IP</th>
<th data-sort-key="deleted">Gelöscht am</th>
<th style="width:1%;">Aktionen</th>
</tr></thead>
<tbody>
{% for d in trash.devices %}
<tr data-sort-name="{{ d['name']|lower }}" data-sort-ip="{{ d['ip']|lower }}" data-sort-deleted="{{ d['deleted_at']|lower }}">
<td class="cell-name">{{ d['name'] }}</td>
<td class="mono">{{ d['ip'] }}</td>
<td class="text-faint" style="font-size:12px;">{{ d['deleted_at'] }}</td>
<td>
{% if current_user.has_permission('papierkorb.edit') %}
<div class="row-actions">
<form method="post" action="{{ url_for('restore_device', mac=d['mac']) }}">
<button type="submit" class="btn btn-sm btn-secondary">Wiederherstellen</button>
</form>
<form method="post" action="{{ url_for('purge_device', mac=d['mac']) }}" data-confirm="„{{ d['name'] }}“ endgültig löschen? Das kann nicht rückgängig gemacht werden.">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Endgültig löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div></div>
{% else %}
<p class="text-faint" style="font-size:12px;">Papierkorb ist leer.</p>
{% endif %}
</div>
<div class="card card-pad">
<h3 style="font-size:14px; margin:0 0 12px;">Switche ({{ trash.switches|length }})</h3>
{% if trash.switches %}
<div class="table-wrap">
<div class="table-toolbar">
<div class="search-input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="switchesTrashSearch" placeholder="Switche durchsuchen…" oninput="filterTable('switchesTrashSearch','switchesTrashTable')">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" id="switchesTrashTable" data-sortable>
<thead><tr>
<th data-sort-key="hostname">Hostname</th>
<th data-sort-key="ip">IP</th>
<th data-sort-key="deleted">Gelöscht am</th>
<th style="width:1%;">Aktionen</th>
</tr></thead>
<tbody>
{% for s in trash.switches %}
<tr data-sort-hostname="{{ s['hostname']|lower }}" data-sort-ip="{{ s['ip']|lower }}" data-sort-deleted="{{ s['deleted_at']|lower }}">
<td class="cell-name">{{ s['hostname'] }}</td>
<td class="mono">{{ s['ip'] }}</td>
<td class="text-faint" style="font-size:12px;">{{ s['deleted_at'] }}</td>
<td>
{% if current_user.has_permission('papierkorb.edit') %}
<div class="row-actions">
<form method="post" action="{{ url_for('restore_switch', hostname=s['hostname']) }}">
<button type="submit" class="btn btn-sm btn-secondary">Wiederherstellen</button>
</form>
<form method="post" action="{{ url_for('purge_switch', hostname=s['hostname']) }}" data-confirm="„{{ s['hostname'] }}“ endgültig löschen? Das kann nicht rückgängig gemacht werden.">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Endgültig löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div></div>
{% else %}
<p class="text-faint" style="font-size:12px;">Papierkorb ist leer.</p>
{% endif %}
</div>
<div class="card card-pad">
<h3 style="font-size:14px; margin:0 0 12px;">Zugangsdaten ({{ trash.credentials|length }})</h3>
{% if trash.credentials %}
<div class="table-wrap">
<div class="table-toolbar">
<div class="search-input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="credentialsTrashSearch" placeholder="Zugangsdaten durchsuchen…" oninput="filterTable('credentialsTrashSearch','credentialsTrashTable')">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" id="credentialsTrashTable" data-sortable>
<thead><tr>
<th data-sort-key="name">Name</th>
<th data-sort-key="username">Username</th>
<th data-sort-key="deleted">Gelöscht am</th>
<th style="width:1%;">Aktionen</th>
</tr></thead>
<tbody>
{% for c in trash.credentials %}
<tr data-sort-name="{{ c['name']|lower }}" data-sort-username="{{ c['username']|lower }}" data-sort-deleted="{{ c['deleted_at']|lower }}">
<td class="cell-name">{{ c['name'] }}</td>
<td class="mono">{{ c['username'] }}</td>
<td class="text-faint" style="font-size:12px;">{{ c['deleted_at'] }}</td>
<td>
{% if current_user.has_permission('papierkorb.edit') %}
<div class="row-actions">
<form method="post" action="{{ url_for('restore_credential', cred_id=c['id']) }}">
<button type="submit" class="btn btn-sm btn-secondary">Wiederherstellen</button>
</form>
<form method="post" action="{{ url_for('purge_credential', cred_id=c['id']) }}" data-confirm="„{{ c['name'] }}“ endgültig löschen? Das kann nicht rückgängig gemacht werden.">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Endgültig löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div></div>
{% else %}
<p class="text-faint" style="font-size:12px;">Papierkorb ist leer.</p>
{% endif %}
</div>
<div class="card card-pad">
<h3 style="font-size:14px; margin:0 0 12px;">Benutzer ({{ trash.users|length }})</h3>
{% if trash.users %}
<div class="table-wrap">
<div class="table-toolbar">
<div class="search-input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="usersTrashSearch" placeholder="Benutzer durchsuchen…" oninput="filterTable('usersTrashSearch','usersTrashTable')">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" id="usersTrashTable" data-sortable>
<thead><tr>
<th data-sort-key="username">Username</th>
<th data-sort-key="name">Name</th>
<th data-sort-key="deleted">Gelöscht am</th>
<th style="width:1%;">Aktionen</th>
</tr></thead>
<tbody>
{% for u in trash.users %}
{% set full_name = [u['first_name'], u['last_name']]|select|join(' ') %}
<tr data-sort-username="{{ u['username']|lower }}" data-sort-name="{{ full_name|lower }}" data-sort-deleted="{{ u['deleted_at']|lower }}">
<td class="cell-name">{{ u['username'] }}</td>
<td class="text-dim">{{ full_name or '—' }}</td>
<td class="text-faint" style="font-size:12px;">{{ u['deleted_at'] }}</td>
<td>
{% if current_user.has_permission('papierkorb.edit') %}
<div class="row-actions">
<form method="post" action="{{ url_for('restore_user', user_id=u['id']) }}">
<button type="submit" class="btn btn-sm btn-secondary">Wiederherstellen</button>
</form>
<form method="post" action="{{ url_for('purge_user', user_id=u['id']) }}" data-confirm="„{{ u['username'] }}“ endgültig löschen? Das kann nicht rückgängig gemacht werden.">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Endgültig löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div></div>
{% else %}
<p class="text-faint" style="font-size:12px;">Papierkorb ist leer.</p>
{% endif %}
</div>
<div class="card card-pad">
<h3 style="font-size:14px; margin:0 0 12px;">Gruppen ({{ trash.groups|length }})</h3>
{% if trash.groups %}
<div class="table-wrap">
<div class="table-toolbar">
<div class="search-input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="text" id="groupsTrashSearch" placeholder="Gruppen durchsuchen…" oninput="filterTable('groupsTrashSearch','groupsTrashTable')">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" id="groupsTrashTable" data-sortable>
<thead><tr>
<th data-sort-key="name">Name</th>
<th data-sort-key="deleted">Gelöscht am</th>
<th style="width:1%;">Aktionen</th>
</tr></thead>
<tbody>
{% for g in trash.groups %}
<tr data-sort-name="{{ g['name']|lower }}" data-sort-deleted="{{ g['deleted_at']|lower }}">
<td class="cell-name">{{ g['name'] }}</td>
<td class="text-faint" style="font-size:12px;">{{ g['deleted_at'] }}</td>
<td>
{% if current_user.has_permission('papierkorb.edit') %}
<div class="row-actions">
<form method="post" action="{{ url_for('restore_group', group_id=g['id']) }}">
<button type="submit" class="btn btn-sm btn-secondary">Wiederherstellen</button>
</form>
<form method="post" action="{{ url_for('purge_group', group_id=g['id']) }}" data-confirm="„{{ g['name'] }}“ endgültig löschen? Das kann nicht rückgängig gemacht werden.">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Endgültig löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div></div>
{% else %}
<p class="text-faint" style="font-size:12px;">Papierkorb ist leer.</p>
{% endif %}
</div>
</div>
<script>
function filterTable(inputId, tableId) {
const q = document.getElementById(inputId).value.trim().toLowerCase();
document.querySelectorAll(`#${tableId} tbody tr`).forEach(row => {
if (row.classList.contains("empty-row")) return;
row.style.display = row.innerText.toLowerCase().includes(q) ? "" : "none";
});
}
</script>
{% endblock %}