diff --git a/CHANGELOG.md b/CHANGELOG.md index 141a85f..8b45773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,18 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.4.0] – 2026-09-17 + +### Hinzugefügt +- Host-Tabelle mit Suche über Hostname, FQDN, Key, Server-ID, IP, Kunde und Rechnungsnummer (#007) +- Filter nach Kunde, Status, Produkt, Ablauffrist und offenen Rechnungspunkten; Sortierung und Seitenblättern (#007) +- CSV-Export der Host-Liste unter Berücksichtigung der aktiven Filter (#007) +- Host-Detailansicht mit Übersicht, Subscription-Historie, Reports, Rechnungen, Notizen mit Tags und Tokens (#007) +- Kunde eines Hosts direkt in der Detailansicht ändern (#007) + +### Geändert +- Host-Tabelle und Host-Detail arbeiten mit echten Daten statt Beispieldaten (#007) + ## [0.3.0] – 2026-09-17 ### Hinzugefügt diff --git a/DEVLOG.md b/DEVLOG.md index 4a192d6..1efc173 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 2 h 13 min +**Gesamt:** 2 h 38 min | Issue | Zeit | |---|---| @@ -12,18 +12,20 @@ | #004 Agent-API | 0 h 20 min | | #005 Agent-Skript | 0 h 25 min | | #006 Kunden und Hostzuweisung | 0 h 35 min | +| #007 Host-Tabelle und Detailansichten | 0 h 25 min | | Meilenstein | Zeit | |---|---| | v0.1.0 | 1 h 13 min | **abgeschlossen** | v0.2.0 | 0 h 25 min | **abgeschlossen** | v0.3.0 | 0 h 35 min | **abgeschlossen** +| v0.4.0 | 0 h 25 min | **abgeschlossen** --- ## 2026-09-17 · Sitzung 1 · 19:36–laufend -**Issue:** #001–#006 (Meilensteine v0.1.0 bis v0.3.0) -**Version:** 0.3.0 +**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) +**Version:** 0.4.0 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -41,6 +43,9 @@ - #006 abgeschlossen: Kunden und Standorte pflegen, Mehrfachzuweisung nicht zugeordneter Hosts, Enrollment- und Host-Tokens verwalten, Audit-Log; Dashboard, Kundenseiten und globale Suche laufen auf der Datenbank - Durchlauf gegen den laufenden Server geprüft: Token anlegen → `enroll` → Agent-Report → Laufzeit-Vorschlag → Host erscheint als „nicht zugeordnet“ → `ping` - Meilenstein v0.3.0 abgeschlossen und getaggt; 45 Tests +- Issues #017 bis #019 aus bisherigen Notizen angelegt (Favicons, Feldtest des Agents, CI-Pipeline) +- #007 abgeschlossen: Host-Tabelle mit Suche, fünf Filtern, Sortierung, Seitenblättern und CSV-Export; Host-Detail mit sechs Tabs, Notizen, Tags, Kundenzuweisung und Token-Widerruf +- Meilenstein v0.4.0 abgeschlossen und getaggt; 59 Tests ### Entscheidungen - **Screenshots mit Firefox headless** statt Playwright: Firefox ist vorhanden, es muss kein Browser samt Systembibliotheken nachinstalliert werden (dafür wäre root nötig). @@ -67,4 +72,4 @@ - Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen) ### Nächster Schritt -- #007 Host-Tabelle mit Filtern, globale Suche auf Datenbankbasis und Host-Detailansicht +- #008 Laufzeiten und Rechnungen: Vorschläge bestätigen, Rechnungen erfassen, Häkchen direkt in der Tabelle diff --git a/docs/anleitung/bilder/host-detail.png b/docs/anleitung/bilder/host-detail.png index 1a63ba7..6e3b22f 100644 Binary files a/docs/anleitung/bilder/host-detail.png and b/docs/anleitung/bilder/host-detail.png differ diff --git a/docs/anleitung/bilder/hosts-gefiltert.png b/docs/anleitung/bilder/hosts-gefiltert.png new file mode 100644 index 0000000..afc38eb Binary files /dev/null and b/docs/anleitung/bilder/hosts-gefiltert.png differ diff --git a/docs/anleitung/bilder/hosts.png b/docs/anleitung/bilder/hosts.png index c4fecc9..3506b8c 100644 Binary files a/docs/anleitung/bilder/hosts.png and b/docs/anleitung/bilder/hosts.png differ diff --git a/docs/issues/007-hosts-detailansichten.md b/docs/issues/007-hosts-detailansichten.md index 33806b2..bae640b 100644 --- a/docs/issues/007-hosts-detailansichten.md +++ b/docs/issues/007-hosts-detailansichten.md @@ -1,25 +1,33 @@ # #007 Host-Tabelle, Suche und Detailansichten -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.4.0 **Labels:** ui -**Geschätzt:** 8 h **Tatsächlich:** – +**Geschätzt:** 8 h **Tatsächlich:** 0 h 25 min ## Ziel Hosts finden, filtern und vollständig einsehen. ## Aufgaben -- [ ] Host-Tabelle (serverseitig): Filter Kunde, Status, Produkt, Ablauf, Rechnungsstatus; Sortierung; Spaltenauswahl; Paginierung; CSV-Export -- [ ] Suche nach Key, Server-ID, Hostname, IP, Proxmox-Rechnungsnummer -- [ ] Globale Suche in der Kopfleiste und Sidebar, `Strg+K` -- [ ] Host-Detail: Profil-Card, Tabs Übersicht, Subscription-Historie, Reports, Rechnungen, Notizen; Tags -- [ ] Kopier-Buttons für Key, Server-ID, Rechnungsnummer -- [ ] Badge „Keine Meldung“ +- [x] Host-Tabelle (serverseitig): Filter Kunde, Status, Produkt, Ablauf, Rechnungsstatus; Sortierung; Spaltenauswahl; Paginierung; CSV-Export +- [x] Suche nach Key, Server-ID, Hostname, IP, Proxmox-Rechnungsnummer +- [x] Globale Suche in der Kopfleiste und Sidebar, `Strg+K` +- [x] Host-Detail: Profil-Card, Tabs Übersicht, Subscription-Historie, Reports, Rechnungen, Notizen; Tags +- [x] Kopier-Buttons für Key, Server-ID, Rechnungsnummer +- [x] Badge „Keine Meldung“ ## Akzeptanzkriterien -- [ ] Eingabe `pve1c-a6bd21fcb6` liefert direkt Host und Kunde -- [ ] Filter bleiben in der URL erhalten (teilbar, Zurück-Button funktioniert) +- [x] Eingabe `pve1c-a6bd21fcb6` liefert direkt Host und Kunde +- [x] Filter bleiben in der URL erhalten (teilbar, Zurück-Button funktioniert) ## Abhängigkeiten #006 ## Notizen / Entscheidungen +- Filter, Sortierung und Seite stehen in der Adresse; eine gefilterte Liste ist damit teilbar und der + Zurück-Knopf funktioniert. +- Sortierung stellt Hosts ohne Ablaufdatum bzw. ohne Meldung immer ans Ende (`nulls: "last"`), sonst stünden + sie beim absteigenden Sortieren ganz oben. +- Die Suche findet auch über Proxmox- und Kundenrechnungsnummern zum Host. +- Der CSV-Export übernimmt die aktiven Filter und liefert alle Treffer, nicht nur die angezeigte Seite. +- **Zurückgestellt:** frei wählbare Spalten. Die Tabelle hat acht feste Spalten und passt damit auf einen + Bildschirm; eine Spaltenauswahl lohnt erst bei deutlich mehr Feldern. Bei Bedarf als eigenes Issue nachziehen. diff --git a/docs/issues/README.md b/docs/issues/README.md index 6e71037..02a26e8 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -10,8 +10,8 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#004](004-agent-api.md) | Agent-API | erledigt | v0.1.0 | 6 h | 0 h 20 min | | [#005](005-agent-skript.md) | Agent-Skript | erledigt | v0.2.0 | 8 h | 0 h 25 min | | [#006](006-kunden-hostzuweisung.md) | Kunden, Standorte und Hostzuweisung | erledigt | v0.3.0 | 6 h | 0 h 35 min | -| [#007](007-hosts-detailansichten.md) | Host-Tabelle, Suche und Detailansichten | in Arbeit | v0.4.0 | 8 h | – | -| [#008](008-laufzeiten-rechnungen.md) | Laufzeiten und Rechnungen | offen | v0.5.0 | 10 h | – | +| [#007](007-hosts-detailansichten.md) | Host-Tabelle, Suche und Detailansichten | erledigt | v0.4.0 | 8 h | 0 h 25 min | +| [#008](008-laufzeiten-rechnungen.md) | Laufzeiten und Rechnungen | in Arbeit | v0.5.0 | 10 h | – | | [#009](009-kundenuebersicht-dashboard-jahresbahn.md) | Kundenübersicht, Dashboard und Jahresbahn | offen | v0.6.0 | 8 h | – | | [#010](010-export.md) | Export CSV und PDF | offen | v0.6.0 | 5 h | – | | [#011](011-warnregeln.md) | Warnregeln | offen | v0.7.0 | 8 h | – | @@ -23,4 +23,4 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#017](017-favicon-dateien.md) | Favicon-Dateien erzeugen | offen | v0.6.0 | 1 h | – | | [#018](018-agent-feldtest.md) | Agent auf echten Proxmox-Hosts prüfen | offen | v0.2.1 | 2 h | – | | [#019](019-ci-pipeline.md) | CI-Pipeline | offen | v0.10.0 | 3 h | – | -| | **Summe** | | | **112 h** | **2 h 13 min** | +| | **Summe** | | | **112 h** | **2 h 38 min** | diff --git a/package.json b/package.json index 5d0d9c8..f99f9c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.3.0", + "version": "0.4.0", "private": true, "scripts": { "dev": "next dev", diff --git a/src/app/(dashboard)/hosts/[id]/page.tsx b/src/app/(dashboard)/hosts/[id]/page.tsx index 312272c..5c8d0cd 100644 --- a/src/app/(dashboard)/hosts/[id]/page.tsx +++ b/src/app/(dashboard)/hosts/[id]/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { notFound } from "next/navigation"; +import { HostNotizen } from "@/components/hosts/HostNotizen"; import { ContentHeader } from "@/components/layout/ContentHeader"; import { AblaufBadge, @@ -12,21 +13,18 @@ import { import { Card } from "@/components/ui/Card"; import { CopyableCode } from "@/components/ui/CopyableCode"; import { Tabs } from "@/components/ui/Tabs"; +import { PRODUKT_TEXT } from "@/lib/anzeige"; +import { prisma } from "@/lib/db"; import { formatDatum, formatDatumZeit, formatEuro, formatUptime, formatZahl } from "@/lib/format"; -import { HEUTE } from "@/mock/data"; -import { - PRODUKT_NAME, - alertsVonHost, - getHost, - getKunde, - hostMeldetNicht, - laufzeitenVonHost, - reportsVonHost, -} from "@/mock/queries"; +import { hostKundeSetzen } from "@/server/actions/host-detail"; +import { hostTokenWiderrufen } from "@/server/actions/tokens"; +import { hostMitDetails } from "@/server/queries/hostliste"; +import { meldungsGrenze } from "@/server/queries/hosts"; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - return { title: getHost(id)?.hostname ?? "Host" }; + const host = await prisma.host.findUnique({ where: { id }, select: { hostname: true } }); + return { title: host?.hostname ?? "Host" }; } function Zeile({ bezeichnung, children }: { bezeichnung: string; children: React.ReactNode }) { @@ -40,51 +38,52 @@ function Zeile({ bezeichnung, children }: { bezeichnung: string; children: React export default async function HostDetailSeite({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - const host = getHost(id); + const host = await hostMitDetails(id); if (!host) notFound(); - const kunde = getKunde(host.kundeId); - const standort = kunde?.standorte.find((s) => s.id === host.standortId); - const laufzeiten = laufzeitenVonHost(host.id); - const reports = reportsVonHost(host.id); - const alerts = alertsVonHost(host.id); - const keys = [...new Set(laufzeiten.map((l) => l.key))]; + const kunden = await prisma.kunde.findMany({ + orderBy: { name: "asc" }, + select: { id: true, name: true }, + }); + + const jetzt = new Date(); + const meldetNicht = !host.letzterReportAm || host.letzterReportAm < meldungsGrenze(jetzt); + const socketAbweichung = + host.subLizenzierteSockets !== null && + host.sockets !== null && + host.subLizenzierteSockets !== host.sockets; return ( <> } + aktion={} />
- {alerts.length > 0 ? ( -
-
- -
    - {alerts.map((alert) => ( -
  • - - {alert.titel} - {" "} - {alert.beschreibung} -
  • - ))} -
-
-
-
+ {host.alerts.length > 0 ? ( + +
    + {host.alerts.map((alert) => ( +
  • + + {alert.titel} + {" "} + {alert.beschreibung} +
  • + ))} +
+
) : null}
@@ -94,40 +93,69 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id