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:
parent
d0cf308c0a
commit
dd12dd7b74
@ -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)
|
||||
|
||||
77
templates/geraete_hardware.html
Normal file
77
templates/geraete_hardware.html
Normal 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 %}
|
||||
@ -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>
|
||||
|
||||
22
templates/geraete_loeschen.html
Normal file
22
templates/geraete_loeschen.html
Normal 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 %}
|
||||
36
templates/geraete_log.html
Normal file
36
templates/geraete_log.html
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user