feat: Aktionen-Spalte in der Geräteliste (/geraete)

Hardware-Informationen, Installations-/Aktions-Log, De-/Reprovisionieren,
Gerät löschen (Soft Delete, mit Bestätigungsseite) - via Pico-Dropdown,
kein zusätzliches JS. Spricht die neuen anode-Endpunkte aus
provisioning-server an.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-10 13:04:58 +02:00
parent d0cf308c0a
commit dd12dd7b74
5 changed files with 239 additions and 1 deletions

View File

@ -120,3 +120,78 @@ def auftrag_deselect(device_id: str, merkmal_key: str, user: dict = Depends(get_
)
return RedirectResponse(f"/geraete/{device_id}", status_code=303)
@router.get("/geraete/{device_id}/hardware")
def hardware_ansicht(request: Request, device_id: str, user: dict = Depends(get_current_user)):
device = find_device_in_organization(device_id, user.organization_id)
if device is None:
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
result = anode_request("GET", f"/api/v1/devices/{device_id}/hardware")
hardware = result.get("hardware") if result.get("success") else None
fehler = None if result.get("success") else result.get("message")
return templates.TemplateResponse(
request,
"geraete_hardware.html",
{"user": user, "device": device, "hardware": hardware, "fehler": fehler},
)
@router.get("/geraete/{device_id}/log")
def log_ansicht(request: Request, device_id: str, user: dict = Depends(get_current_user)):
device = find_device_in_organization(device_id, user.organization_id)
if device is None:
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
result = anode_request("GET", f"/api/v1/devices/{device_id}/events")
events = result.get("events", []) if result.get("success") else []
return templates.TemplateResponse(
request, "geraete_log.html", {"user": user, "device": device, "events": events}
)
@router.post("/geraete/{device_id}/deprovisionieren")
def deprovisionieren(device_id: str, user: dict = Depends(get_current_user)):
if find_device_in_organization(device_id, user.organization_id) is None:
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
anode_request("POST", f"/api/v1/devices/{device_id}/deprovision")
return RedirectResponse("/geraete", status_code=303)
@router.post("/geraete/{device_id}/reprovisionieren")
def reprovisionieren(device_id: str, user: dict = Depends(get_current_user)):
if find_device_in_organization(device_id, user.organization_id) is None:
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
anode_request("POST", f"/api/v1/devices/{device_id}/reprovision")
return RedirectResponse("/geraete", status_code=303)
@router.get("/geraete/{device_id}/loeschen")
def loeschen_bestaetigen(request: Request, device_id: str, user: dict = Depends(get_current_user)):
device = find_device_in_organization(device_id, user.organization_id)
if device is None:
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
return templates.TemplateResponse(
request, "geraete_loeschen.html", {"user": user, "device": device}
)
@router.post("/geraete/{device_id}/loeschen")
def loeschen(device_id: str, user: dict = Depends(get_current_user)):
if find_device_in_organization(device_id, user.organization_id) is None:
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
anode_request("POST", f"/api/v1/devices/{device_id}/archive")
return RedirectResponse("/geraete", status_code=303)

View File

@ -0,0 +1,77 @@
{% extends "base.html" %}
{% block title %}Hardware {{ device.hostname or device.device_fingerprint }} — Tuxflotte{% endblock %}
{% block content %}
<p><a href="/geraete">← Geräteliste</a></p>
<h2>Hardware-Informationen: {{ device.hostname or device.device_fingerprint }}</h2>
{% if fehler %}
<p><mark>{{ fehler }}</mark></p>
{% elif not hardware %}
<p>Für dieses Gerät liegt noch kein Hardware-Snapshot vor (Gerät wurde noch nicht aktiviert).</p>
{% else %}
<section>
<h3>System</h3>
<figure>
<table>
<tbody>
<tr><td>Hersteller</td><td>{{ hardware.manufacturer or "—" }}</td></tr>
<tr><td>Produkt</td><td>{{ hardware.product_name or "—" }} {{ hardware.product_version or "" }}</td></tr>
<tr><td>Architektur</td><td>{{ hardware.architecture }}</td></tr>
<tr><td>System-UUID</td><td><code>{{ hardware.system_uuid or "—" }}</code></td></tr>
<tr><td>Seriennummer</td><td><code>{{ hardware.system_serial or "—" }}</code></td></tr>
<tr><td>CPU</td><td>{{ hardware.cpu_model or "—" }} ({{ hardware.cpu_logical_count or "?" }} Logikkerne)</td></tr>
<tr><td>Arbeitsspeicher</td><td>{{ (hardware.memory_bytes / (1024**3)) | round(1) if hardware.memory_bytes else "—" }} GB</td></tr>
<tr><td>Mainboard</td><td>{{ hardware.board_vendor or "—" }} {{ hardware.board_name or "" }}</td></tr>
<tr><td>BIOS</td><td>{{ hardware.bios_vendor or "—" }} {{ hardware.bios_version or "" }}</td></tr>
<tr><td>Boot-Modus</td><td>{{ hardware.boot_mode or "—" }}</td></tr>
<tr><td>Secure Boot</td><td>{{ hardware.secure_boot or "—" }}</td></tr>
<tr><td>TPM</td><td>{{ hardware.tpm_version or "—" }}</td></tr>
<tr><td>Erfasst am</td><td>{{ hardware.collected_at }}</td></tr>
</tbody>
</table>
</figure>
</section>
<section>
<h3>Netzwerk-Interfaces</h3>
{% if not hardware.network_interfaces %}
<p>Keine erfasst.</p>
{% else %}
<figure>
<table>
<thead><tr><th>Name</th><th>Typ</th><th>MAC-Adresse</th></tr></thead>
<tbody>
{% for interface in hardware.network_interfaces %}
<tr><td>{{ interface.name }}</td><td>{{ interface.type }}</td><td><code>{{ interface.mac_address }}</code></td></tr>
{% endfor %}
</tbody>
</table>
</figure>
{% endif %}
</section>
<section>
<h3>Datenträger</h3>
{% if not hardware.storage_devices %}
<p>Keine erfasst.</p>
{% else %}
<figure>
<table>
<thead><tr><th>Name</th><th>Modell</th><th>Seriennummer</th><th>Größe</th><th>Transport</th></tr></thead>
<tbody>
{% for storage in hardware.storage_devices %}
<tr>
<td>{{ storage.name }}</td>
<td>{{ storage.model or "—" }}</td>
<td><code>{{ storage.serial or "—" }}</code></td>
<td>{{ (storage.size_bytes / (1024**3)) | round(1) }} GB</td>
<td>{{ storage.transport or "—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</figure>
{% endif %}
</section>
{% endif %}
{% endblock %}

View File

@ -8,7 +8,7 @@
<figure>
<table>
<thead>
<tr><th>Hostname</th><th>Kennung</th><th>Letzter Check-in</th><th></th></tr>
<tr><th>Hostname</th><th>Kennung</th><th>Letzter Check-in</th><th>Status</th><th></th><th></th></tr>
</thead>
<tbody>
{% for device in devices %}
@ -16,7 +16,35 @@
<td>{{ device.hostname or "—" }}</td>
<td><code>{{ device.device_fingerprint }}</code></td>
<td>{{ device.agent_last_checkin or "noch nie" }}</td>
<td>
{% if device.deprovisioned_at %}
<span class="state-absent">deprovisioniert</span>
{% else %}
<span class="state-present">aktiv</span>
{% endif %}
</td>
<td><a href="/geraete/{{ device.id }}" role="button" class="outline">Auftragskatalog →</a></td>
<td>
<details class="dropdown">
<summary role="button" class="secondary outline">Aktionen</summary>
<ul>
<li><a href="/geraete/{{ device.id }}/hardware">Hardware-Informationen</a></li>
<li><a href="/geraete/{{ device.id }}/log">Installations-/Aktions-Log</a></li>
<li>
{% if device.deprovisioned_at %}
<form method="post" action="/geraete/{{ device.id }}/reprovisionieren">
<button type="submit" class="outline">Erneut provisionieren</button>
</form>
{% else %}
<form method="post" action="/geraete/{{ device.id }}/deprovisionieren">
<button type="submit" class="outline secondary">De-Provisionieren</button>
</form>
{% endif %}
</li>
<li><a href="/geraete/{{ device.id }}/loeschen">Gerät löschen</a></li>
</ul>
</details>
</td>
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Gerät löschen — Tuxflotte{% endblock %}
{% block content %}
<p><a href="/geraete">← Geräteliste</a></p>
<h2>Gerät löschen</h2>
<p>Soll dieses Gerät wirklich gelöscht werden?</p>
<figure>
<table>
<tbody>
<tr><td>Hostname</td><td>{{ device.hostname or "—" }}</td></tr>
<tr><td>Kennung</td><td><code>{{ device.device_fingerprint }}</code></td></tr>
</tbody>
</table>
</figure>
<p>Das Gerät verschwindet danach aus Ihrer Geräteliste. Bereits erfasste Hardware-Daten und das Aktions-Log gehen dabei nicht verloren.</p>
<form method="post" action="/geraete/{{ device.id }}/loeschen">
<button type="submit">Ja, Gerät löschen</button>
<a href="/geraete" role="button" class="secondary outline">Abbrechen</a>
</form>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Log {{ device.hostname or device.device_fingerprint }} — Tuxflotte{% endblock %}
{% block content %}
<p><a href="/geraete">← Geräteliste</a></p>
<h2>Installations-/Aktions-Log: {{ device.hostname or device.device_fingerprint }}</h2>
{% if not events %}
<p>Für dieses Gerät liegen noch keine Einträge vor.</p>
{% else %}
<figure>
<table>
<thead>
<tr><th>Zeitpunkt</th><th>Merkmal</th><th>Ereignis</th><th>Detail</th></tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ event.occurred_at }}</td>
<td>{{ event.merkmal_name }}</td>
<td>
{% if event.event_type in ("applied", "selected") %}
<span class="state-present">{{ event.event_type }}</span>
{% elif event.event_type in ("apply_failed", "remove_failed") %}
<mark>{{ event.event_type }}</mark>
{% else %}
<span class="state-absent">{{ event.event_type }}</span>
{% endif %}
</td>
<td>{% if event.detail %}<code>{{ event.detail }}</code>{% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</figure>
{% endif %}
{% endblock %}