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).
274 lines
14 KiB
HTML
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 %}
|