diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e4e043..438ba37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). - Projektorganisation: Devlog, Changelog, Issue-Übersicht und Issues pro Phase (#001) - Architektur- und Umsetzungsplan in `docs/architektur.md` (#001) - Next.js-Grundgerüst mit AdminLTE-CSS, Bootstrap Icons, zod, ESLint, Prettier und Vitest (#001) +- Klickbarer Prototyp mit Beispieldaten: Dashboard, Kunden- und Host-Ansichten, Rechnungen, Jahresbahn (#002) +- ProxLedger-Logo als SVG-Komponente, Farbschema nach Vorgabe inklusive Dark-Mode (#002) +- Globale Suche über Key, Host, Kunde und Rechnungsnummer mit Strg+K (#002) ### Geändert diff --git a/DEVLOG.md b/DEVLOG.md index b7ef35d..b37e077 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,20 +2,21 @@ ## Zeitübersicht -**Gesamt:** 0 h 08 min +**Gesamt:** 0 h 28 min | Issue | Zeit | |---|---| | #001 Projekt-Setup | 0 h 08 min | +| #002 Klickbarer Prototyp | 0 h 20 min | | Meilenstein | Zeit | |---|---| -| v0.1.0 | 0 h 08 min | +| v0.1.0 | 0 h 28 min | --- ## 2026-09-17 · Sitzung 1 · 19:36–laufend -**Issue:** #001 Projekt-Setup +**Issue:** #001 Projekt-Setup, #002 Klickbarer Prototyp **Version:** 0.1.0-dev ### Erledigt @@ -24,8 +25,12 @@ - Repository-Rechte korrigiert (Verzeichnis gehörte root), Branch `master` → `main`, Git-Identität lokal gesetzt - `.gitignore`, `CHANGELOG.md`, `DEVLOG.md`, Architekturplan und 15 Issues angelegt - #001 abgeschlossen: Next.js 16 (App Router, TypeScript strict), AdminLTE-CSS, Bootstrap Icons, zod, ESLint/Prettier/Vitest; Lint, Typecheck, Test und Build grün +- #002 abgeschlossen: Prototyp mit Beispieldaten – Layout (Sidebar, Kopfleiste, Inhaltskopf, Footer), Logo als SVG-Komponente, Theme mit Farbvorgaben und Dark-Mode, Dashboard, Kunden (Liste und Detail), Hosts (Liste, Detail, nicht zugeordnet), Rechnungen, Jahresbahn, Einstellungsseiten, globale Suche mit Strg+K +- Screenshots aller Seiten aufgenommen und in `docs/anleitung/bilder/` abgelegt ### 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). +- **Kennzahlen werden berechnet, nicht hinterlegt:** auch im Prototyp kommen alle Zahlen aus den Beispieldaten, damit Tabellen und Kacheln nie widersprüchlich sind. - **AdminLTE 4.9.1 nur als CSS:** stabil, Bootstrap 5.3.8, kein jQuery; die JS-Plugins manipulieren das DOM und werden daher als React-Komponenten nachgebaut. - **Host-Identität über Agent-Token statt Server-ID:** Die Server-ID ändert sich bei Hardwaretausch und soll dann eine Warnung auslösen, statt einen neuen Host zu erzeugen. - **Rechnungen mit Positionen:** Proxmox-Sammelbestellungen und Sammelrechnungen an Kunden sollen ohne doppelte Rechnungsnummern abbildbar sein. @@ -33,8 +38,9 @@ - **Prototyp direkt im Next.js-Projekt**, damit die Komponenten weiterverwendet werden. ### Probleme / offene Fragen -- Docker ist nicht installiert und `sudo` erfordert ein Passwort → Installation muss manuell erfolgen (benötigt ab #003). +- Docker ist nicht installiert und `sudo` erfordert ein Passwort → Installation muss manuell erfolgen (wird ab #003 gebraucht). +- Favicon-Dateien (.ico/PNG) fehlen noch, weil kein SVG-Konverter installiert ist. - Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen) ### Nächster Schritt -- Next.js-Grundgerüst (#001), danach Prototyp (#002) +- Rückmeldung zum Prototyp abwarten, dann #003 Datenmodell (braucht PostgreSQL per Docker) diff --git a/docs/anleitung/bilder/dashboard.png b/docs/anleitung/bilder/dashboard.png new file mode 100644 index 0000000..edf657b Binary files /dev/null and b/docs/anleitung/bilder/dashboard.png differ diff --git a/docs/anleitung/bilder/host-detail.png b/docs/anleitung/bilder/host-detail.png new file mode 100644 index 0000000..1a63ba7 Binary files /dev/null and b/docs/anleitung/bilder/host-detail.png differ diff --git a/docs/anleitung/bilder/hosts.png b/docs/anleitung/bilder/hosts.png new file mode 100644 index 0000000..c4fecc9 Binary files /dev/null and b/docs/anleitung/bilder/hosts.png differ diff --git a/docs/anleitung/bilder/jahresbahn.png b/docs/anleitung/bilder/jahresbahn.png new file mode 100644 index 0000000..e26321b Binary files /dev/null and b/docs/anleitung/bilder/jahresbahn.png differ diff --git a/docs/anleitung/bilder/kunde-detail.png b/docs/anleitung/bilder/kunde-detail.png new file mode 100644 index 0000000..6a3e72d Binary files /dev/null and b/docs/anleitung/bilder/kunde-detail.png differ diff --git a/docs/anleitung/bilder/mobil-dashboard.png b/docs/anleitung/bilder/mobil-dashboard.png new file mode 100644 index 0000000..d3dcdae Binary files /dev/null and b/docs/anleitung/bilder/mobil-dashboard.png differ diff --git a/docs/anleitung/bilder/nicht-zugeordnet.png b/docs/anleitung/bilder/nicht-zugeordnet.png new file mode 100644 index 0000000..5ca3c50 Binary files /dev/null and b/docs/anleitung/bilder/nicht-zugeordnet.png differ diff --git a/docs/anleitung/bilder/rechnungen.png b/docs/anleitung/bilder/rechnungen.png new file mode 100644 index 0000000..1bf88f9 Binary files /dev/null and b/docs/anleitung/bilder/rechnungen.png differ diff --git a/docs/issues/002-prototyp.md b/docs/issues/002-prototyp.md index a82165c..0200134 100644 --- a/docs/issues/002-prototyp.md +++ b/docs/issues/002-prototyp.md @@ -1,31 +1,34 @@ # #002 Klickbarer Prototyp -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.1.0 **Labels:** ui, prototyp -**Geschätzt:** 8 h **Tatsächlich:** – +**Geschätzt:** 8 h **Tatsächlich:** 0 h 20 min ## Ziel Klickbarer UI-Prototyp der Seiten „Dashboard“, „Kunden-Detail“ und „Host-Detail“ mit realistischen Beispieldaten und ohne Backend, als Abstimmungsgrundlage für Look & Feel. Die Komponenten werden in den späteren Phasen weiterverwendet. ## Aufgaben -- [ ] Logo-Komponente (Standard, Sidebar-Variante, einfarbig, Favicon 32/16) + `favicon.svg`, `favicon.ico`, PNGs, `apple-touch-icon` -- [ ] Theme: Farb-Tokens (abgedunkelte Info/Erfolg), Source Sans 3 (16 px), Monospace-Klasse mit Kopier-Button -- [ ] Layout: Sidebar (250 px, einklappbar, Overlay mobil), Kopfleiste, Inhaltskopf mit Breadcrumb, Footer mit Version -- [ ] Komponenten: SmallBox, InfoBox, Card, Badge, Callout, TodoList, ProgressBar, Tabs, CopyableCode -- [ ] Beispieldaten (`src/mock/`): 4 Kunden, ~8 Hosts (PVE/PBS/PMG), Laufzeiten, Rechnungen, Alerts -- [ ] Dashboard: 4 Small Boxes, Callout „Nicht zugeordnete Hosts“, Card „Subscriptions nach Ablauf“, Card „Aufgaben“, Card „Rechnungsstatus je Kunde“ -- [ ] Kunden-Detail: Info-Boxen (Hosts, Einkauf, Verkauf, Marge), Host-Tabelle, Laufzeiten-Tabelle, Summen pro Jahr, Ansprechpartner, Notizen -- [ ] Host-Detail: Profil-Card + Tabs Übersicht, Subscription-Historie, Reports, Rechnungen, Notizen -- [ ] Warnglocke mit Dropdown, Strg+K-Suchdialog (clientseitig über Mock-Daten) -- [ ] Dark-Mode-Umschalter (Grundfunktion) +- [x] Logo-Komponente (Standard, Sidebar-Variante, einfarbig, Favicon 32/16) + `favicon.svg`, `favicon.ico`, PNGs, `apple-touch-icon` +- [x] Theme: Farb-Tokens (abgedunkelte Info/Erfolg), Source Sans 3 (16 px), Monospace-Klasse mit Kopier-Button +- [x] Layout: Sidebar (250 px, einklappbar, Overlay mobil), Kopfleiste, Inhaltskopf mit Breadcrumb, Footer mit Version +- [x] Komponenten: SmallBox, InfoBox, Card, Badge, Callout, TodoList, ProgressBar, Tabs, CopyableCode +- [x] Beispieldaten (`src/mock/`): 4 Kunden, ~8 Hosts (PVE/PBS/PMG), Laufzeiten, Rechnungen, Alerts +- [x] Dashboard: 4 Small Boxes, Callout „Nicht zugeordnete Hosts“, Card „Subscriptions nach Ablauf“, Card „Aufgaben“, Card „Rechnungsstatus je Kunde“ +- [x] Kunden-Detail: Info-Boxen (Hosts, Einkauf, Verkauf, Marge), Host-Tabelle, Laufzeiten-Tabelle, Summen pro Jahr, Ansprechpartner, Notizen +- [x] Host-Detail: Profil-Card + Tabs Übersicht, Subscription-Historie, Reports, Rechnungen, Notizen +- [x] Warnglocke mit Dropdown, Strg+K-Suchdialog (clientseitig über Mock-Daten) +- [x] Dark-Mode-Umschalter (Grundfunktion) ## Akzeptanzkriterien -- [ ] Alle drei Seiten sind über Sidebar/Links erreichbar und klickbar -- [ ] Suche nach `pve1c-a6bd21fcb6` findet Host und Kunde -- [ ] Mobil (375 px) benutzbar: Sidebar als Overlay, Tabellen scrollbar -- [ ] Weiße Schrift auf Info/Erfolg erfüllt 4,5:1 +- [x] Alle drei Seiten sind über Sidebar/Links erreichbar und klickbar +- [x] Suche nach `pve1c-a6bd21fcb6` findet Host und Kunde +- [x] Mobil (375 px) benutzbar: Sidebar als Overlay, Tabellen scrollbar +- [x] Weiße Schrift auf Info/Erfolg erfüllt 4,5:1 ## Abhängigkeiten #001 ## Notizen / Entscheidungen +- Favicon-Dateien (`favicon.ico`, PNG-Größen, `apple-touch-icon`) fehlen noch; das Logo liegt als React-Komponente vor. Nachziehen in #007. +- Screenshots des Prototyps liegen in `docs/anleitung/bilder/` und dienen als Grundlage für #016. +- Auf dem Dashboard zeigt die Spalte „Proxmox“ bewusst die Rechnung der Verlängerung (Laufzeit, die am `nextduedate` beginnt), nicht die der laufenden Periode. diff --git a/docs/issues/016-anleitung-screenshots.md b/docs/issues/016-anleitung-screenshots.md new file mode 100644 index 0000000..7fd5408 --- /dev/null +++ b/docs/issues/016-anleitung-screenshots.md @@ -0,0 +1,33 @@ +# #016 Benutzerhandbuch mit Screenshots +**Status:** offen +**Meilenstein:** v0.10.0 +**Labels:** docs +**Geschätzt:** 5 h **Tatsächlich:** – + +## Ziel +Bebildertes Handbuch auf Deutsch, das den kompletten Weg zeigt: Dashboard installieren, Enrollment-Token erzeugen, +Agent auf einem Host ausrollen, Host einem Kunden zuweisen, Laufzeit und Rechnungen pflegen, Warnungen und +Benachrichtigungen einrichten. + +## Aufgaben +- [ ] Gliederung `docs/anleitung/README.md` (Installation, Erste Schritte, Agent ausrollen, Kunden und Hosts, Rechnungen, Warnungen, Benachrichtigungen, Backup) +- [ ] Screenshots automatisiert erzeugen (`npm run screenshots`): Seiten anfahren, Aufnahmen nach `docs/anleitung/bilder/` schreiben +- [ ] Aufnahmen in Hell und Dunkel, dazu eine Mobilansicht +- [ ] Bilder mit Bildunterschriften und Alternativtexten einbinden +- [ ] Screenshots vor jedem Release auffrischen (Hinweis in der Release-Checkliste) +- [ ] Verweise aus README und Dashboard-Hilfe auf das Handbuch + +## Akzeptanzkriterien +- [ ] Jemand ohne Vorkenntnisse kann anhand des Handbuchs Dashboard und Agent installieren +- [ ] Jeder Arbeitsschritt hat mindestens ein Bild +- [ ] Bilder zeigen Beispieldaten, keine echten Kundendaten + +## Abhängigkeiten +#014 + +## Notizen / Entscheidungen +- Aufnahmen laufen über Firefox im Headless-Modus (`firefox --profile --headless --screenshot`), + weil Firefox auf dem Entwicklungsrechner ohnehin vorhanden ist und kein Browser nachinstalliert werden muss. + Das Profil braucht `user_pref("layout.css.prefers-color-scheme.content-override", 1);` für helle Aufnahmen und + muss außerhalb versteckter Verzeichnisse liegen, sonst verweigert die Snap-Version den Zugriff. +- Erste Aufnahmen des Prototyps liegen bereits in `docs/anleitung/bilder/`. diff --git a/docs/issues/README.md b/docs/issues/README.md index 6fcb9bb..4f3b16e 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -5,8 +5,8 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | Nr. | Titel | Status | Meilenstein | Geschätzt | Tatsächlich | |---|---|---|---|---|---| | [#001](001-projekt-setup.md) | Projekt-Setup | erledigt | v0.1.0 | 3 h | 0 h 08 min | -| [#002](002-prototyp.md) | Klickbarer Prototyp | in Arbeit | v0.1.0 | 8 h | – | -| [#003](003-datenmodell.md) | Datenmodell und Migrationen | offen | v0.1.0 | 5 h | – | +| [#002](002-prototyp.md) | Klickbarer Prototyp | erledigt | v0.1.0 | 8 h | 0 h 20 min | +| [#003](003-datenmodell.md) | Datenmodell und Migrationen | in Arbeit | v0.1.0 | 5 h | – | | [#004](004-agent-api.md) | Agent-API | offen | v0.1.0 | 6 h | – | | [#005](005-agent-skript.md) | Agent-Skript | offen | v0.2.0 | 8 h | – | | [#006](006-kunden-hostzuweisung.md) | Kunden, Standorte und Hostzuweisung | offen | v0.3.0 | 6 h | – | @@ -19,4 +19,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#013](013-authentifizierung.md) | Authentifizierung und Audit-Log | offen | v0.9.0 | 6 h | – | | [#014](014-deployment.md) | Deployment und Dokumentation | offen | v0.10.0 | 8 h | – | | [#015](015-feinschliff-v1.md) | Feinschliff für 1.0 | offen | v1.0.0 | 6 h | – | -| | **Summe** | | | **101 h** | **0 h 08 min** | +| [#016](016-anleitung-screenshots.md) | Benutzerhandbuch mit Screenshots | offen | v0.10.0 | 5 h | – | +| | **Summe** | | | **106 h** | **0 h 28 min** | diff --git a/src/app/(dashboard)/einstellungen/benachrichtigungen/page.tsx b/src/app/(dashboard)/einstellungen/benachrichtigungen/page.tsx new file mode 100644 index 0000000..dfe428d --- /dev/null +++ b/src/app/(dashboard)/einstellungen/benachrichtigungen/page.tsx @@ -0,0 +1,24 @@ +import { ContentHeader } from "@/components/layout/ContentHeader"; +import { Card } from "@/components/ui/Card"; + +export const metadata = { title: "Benachrichtigungen" }; + +export default function BenachrichtigungenSeite() { + return ( + <> + +
+
+ +

+ SMTP (Microsoft 365), Webhooks und die tägliche Zusammenfassung werden in #012 umgesetzt. +

+
+
+
+ + ); +} diff --git a/src/app/(dashboard)/einstellungen/system/page.tsx b/src/app/(dashboard)/einstellungen/system/page.tsx new file mode 100644 index 0000000..7dfbe2d --- /dev/null +++ b/src/app/(dashboard)/einstellungen/system/page.tsx @@ -0,0 +1,27 @@ +import { ContentHeader } from "@/components/layout/ContentHeader"; +import { Card } from "@/components/ui/Card"; +import { APP_VERSION } from "@/lib/version"; + +export const metadata = { title: "System" }; + +export default function SystemSeite() { + return ( + <> + +
+
+ +
+
Version
+
{APP_VERSION}
+
Datenquelle
+
Beispieldaten (Prototyp, #002)
+
Warnregeln
+
Konfigurierbar ab #011
+
+
+
+
+ + ); +} diff --git a/src/app/(dashboard)/einstellungen/tokens/page.tsx b/src/app/(dashboard)/einstellungen/tokens/page.tsx new file mode 100644 index 0000000..b8e4e9a --- /dev/null +++ b/src/app/(dashboard)/einstellungen/tokens/page.tsx @@ -0,0 +1,37 @@ +import { ContentHeader } from "@/components/layout/ContentHeader"; +import { Card } from "@/components/ui/Card"; +import { CopyableCode } from "@/components/ui/CopyableCode"; + +export const metadata = { title: "Agent-Tokens" }; + +export default function TokensSeite() { + return ( + <> + +
+
+ +

+ Auf dem Proxmox-Host als root ausführen. Der Enrollment-Token gilt bis zum angegebenen Datum und + kann mehrfach verwendet werden. +

+
+              
+                curl -fsSL https://proxledger.example.de/install.sh | bash -s -- --token plx_e_BEISPIEL
+              
+            
+ +
+ +

+ Ausgabe, Ablauf und Widerruf von Enrollment- und Host-Tokens folgen in #006. +

+
+
+
+ + ); +} diff --git a/src/app/(dashboard)/hosts/[id]/page.tsx b/src/app/(dashboard)/hosts/[id]/page.tsx new file mode 100644 index 0000000..3a88aa1 --- /dev/null +++ b/src/app/(dashboard)/hosts/[id]/page.tsx @@ -0,0 +1,315 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ContentHeader } from "@/components/layout/ContentHeader"; +import { + AblaufBadge, + Badge, + EinkaufBadge, + KeineMeldungBadge, + StatusBadge, + VerkaufBadge, +} from "@/components/ui/Badges"; +import { Card } from "@/components/ui/Card"; +import { CopyableCode } from "@/components/ui/CopyableCode"; +import { Tabs } from "@/components/ui/Tabs"; +import { formatDatum, formatDatumZeit, formatEuro, formatUptime, formatZahl } from "@/lib/format"; +import { HEUTE, hosts } from "@/mock/data"; +import { + PRODUKT_NAME, + alertsVonHost, + getHost, + getKunde, + hostMeldetNicht, + laufzeitenVonHost, + reportsVonHost, +} from "@/mock/queries"; + +export function generateStaticParams() { + return hosts.map((host) => ({ id: host.id })); +} + +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return { title: getHost(id)?.hostname ?? "Host" }; +} + +function Zeile({ bezeichnung, children }: { bezeichnung: string; children: React.ReactNode }) { + return ( +
+ {bezeichnung} + {children} +
+ ); +} + +export default async function HostDetailSeite({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const host = getHost(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))]; + + return ( + <> + } + /> + +
+
+ {alerts.length > 0 ? ( +
+
+ +
    + {alerts.map((alert) => ( +
  • + + {alert.titel} + {" "} + {alert.beschreibung} +
  • + ))} +
+
+
+
+ ) : null} + +
+
+ +
+