Merge branch 'phase/003-datenmodell'
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
# Datenbank (lokal über docker-compose.dev.yml, Port 5433)
|
||||||
|
DATABASE_URL="postgresql://proxledger:proxledger@localhost:5433/proxledger?schema=public"
|
||||||
|
|
||||||
|
# Eigene Datenbank für automatisierte Tests
|
||||||
|
TEST_DATABASE_URL="postgresql://proxledger:proxledger@localhost:5433/proxledger_test?schema=public"
|
||||||
|
|
||||||
|
# Lokaler Administrator, wird vom Seed angelegt
|
||||||
|
ADMIN_EMAIL="admin@example.de"
|
||||||
|
ADMIN_NAME="Administrator"
|
||||||
|
ADMIN_PASSWORD="bitte-aendern"
|
||||||
|
|
||||||
|
# Öffentlich erreichbare Adresse des Dashboards (für install.sh und E-Mails)
|
||||||
|
APP_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# Demo-Daten beim Seed anlegen (1 = ja)
|
||||||
|
SEED_DEMO="0"
|
||||||
@@ -51,3 +51,5 @@ Thumbs.db
|
|||||||
# Vercel / Sonstiges
|
# Vercel / Sonstiges
|
||||||
.vercel
|
.vercel
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
/src/generated/prisma
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ coverage
|
|||||||
docs
|
docs
|
||||||
*.md
|
*.md
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
src/generated
|
||||||
|
prisma/migrations
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
- Klickbarer Prototyp mit Beispieldaten: Dashboard, Kunden- und Host-Ansichten, Rechnungen, Jahresbahn (#002)
|
- Klickbarer Prototyp mit Beispieldaten: Dashboard, Kunden- und Host-Ansichten, Rechnungen, Jahresbahn (#002)
|
||||||
- ProxLedger-Logo als SVG-Komponente, Farbschema nach Vorgabe inklusive Dark-Mode (#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)
|
- Globale Suche über Key, Host, Kunde und Rechnungsnummer mit Strg+K (#002)
|
||||||
|
- Datenmodell mit Prisma: Kunden, Standorte, Hosts, Reports, Key- und Server-ID-Historie, Laufzeiten, Proxmox- und Kundenrechnungen mit Positionen, Tokens, Warnungen, Benachrichtigungen, Benutzer und Audit-Log (#003)
|
||||||
|
- Erste Migration, Seed für Administrator, Warnregeln und Demo-Daten (#003)
|
||||||
|
- Docker-Compose für die Entwicklungsdatenbank und Testdatenbank-Setup für Vitest (#003)
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,22 @@
|
|||||||
|
|
||||||
## Zeitübersicht
|
## Zeitübersicht
|
||||||
|
|
||||||
**Gesamt:** 0 h 28 min
|
**Gesamt:** 0 h 53 min
|
||||||
|
|
||||||
| Issue | Zeit |
|
| Issue | Zeit |
|
||||||
|---|---|
|
|---|---|
|
||||||
| #001 Projekt-Setup | 0 h 08 min |
|
| #001 Projekt-Setup | 0 h 08 min |
|
||||||
| #002 Klickbarer Prototyp | 0 h 20 min |
|
| #002 Klickbarer Prototyp | 0 h 20 min |
|
||||||
|
| #003 Datenmodell und Migrationen | 0 h 25 min |
|
||||||
|
|
||||||
| Meilenstein | Zeit |
|
| Meilenstein | Zeit |
|
||||||
|---|---|
|
|---|---|
|
||||||
| v0.1.0 | 0 h 28 min |
|
| v0.1.0 | 0 h 53 min |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2026-09-17 · Sitzung 1 · 19:36–laufend
|
## 2026-09-17 · Sitzung 1 · 19:36–laufend
|
||||||
**Issue:** #001 Projekt-Setup, #002 Klickbarer Prototyp
|
**Issue:** #001 Projekt-Setup, #002 Klickbarer Prototyp, #003 Datenmodell
|
||||||
**Version:** 0.1.0-dev
|
**Version:** 0.1.0-dev
|
||||||
|
|
||||||
### Erledigt
|
### Erledigt
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
- #001 abgeschlossen: Next.js 16 (App Router, TypeScript strict), AdminLTE-CSS, Bootstrap Icons, zod, ESLint/Prettier/Vitest; Lint, Typecheck, Test und Build grün
|
- #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
|
- #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
|
- Screenshots aller Seiten aufgenommen und in `docs/anleitung/bilder/` abgelegt
|
||||||
|
- #003 abgeschlossen: Prisma-Schema mit 23 Modellen, erste Migration, Seed (Administrator, 10 Warnregeln, Demo-Daten), Testdatenbank-Setup für Vitest, 3 Schema-Tests
|
||||||
|
|
||||||
### Entscheidungen
|
### 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).
|
- **Screenshots mit Firefox headless** statt Playwright: Firefox ist vorhanden, es muss kein Browser samt Systembibliotheken nachinstalliert werden (dafür wäre root nötig).
|
||||||
@@ -36,11 +38,14 @@
|
|||||||
- **Rechnungen mit Positionen:** Proxmox-Sammelbestellungen und Sammelrechnungen an Kunden sollen ohne doppelte Rechnungsnummern abbildbar sein.
|
- **Rechnungen mit Positionen:** Proxmox-Sammelbestellungen und Sammelrechnungen an Kunden sollen ohne doppelte Rechnungsnummern abbildbar sein.
|
||||||
- **Eigener Worker-Prozess** für zeitgesteuerte Warnregeln und Benachrichtigungen, unabhängig von Seitenaufrufen.
|
- **Eigener Worker-Prozess** für zeitgesteuerte Warnregeln und Benachrichtigungen, unabhängig von Seitenaufrufen.
|
||||||
- **Prototyp direkt im Next.js-Projekt**, damit die Komponenten weiterverwendet werden.
|
- **Prototyp direkt im Next.js-Projekt**, damit die Komponenten weiterverwendet werden.
|
||||||
|
- **Prisma 7.10.0 fest gesetzt:** Der npm-Tag `latest` zeigt derzeit auf 8.0.0-rc, ein Release Candidate gehört nicht in ein neues Projekt.
|
||||||
|
- **Seed nutzt die Prototyp-Beispieldaten**, damit die Oberfläche ab #006 dieselben Fälle zeigt wie der Prototyp.
|
||||||
|
- **Keine Nur-Lese-Rolle** (Rückmeldung Florian): alle Benutzer sind Administratoren.
|
||||||
|
|
||||||
### Probleme / offene Fragen
|
### Probleme / offene Fragen
|
||||||
- Docker ist nicht installiert und `sudo` erfordert ein Passwort → Installation muss manuell erfolgen (wird ab #003 gebraucht).
|
- Docker wurde von Florian installiert; die Entwicklungsdatenbank läuft auf Port 5433 (5432 bleibt für lokale Instanzen frei).
|
||||||
- Favicon-Dateien (.ico/PNG) fehlen noch, weil kein SVG-Konverter installiert ist.
|
- Favicon-Dateien (.ico/PNG) fehlen noch, weil kein SVG-Konverter installiert ist.
|
||||||
- Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen)
|
- Offen: Rolle „readonly“ zusätzlich zu Admin? (vorläufig vorgesehen)
|
||||||
|
|
||||||
### Nächster Schritt
|
### Nächster Schritt
|
||||||
- Rückmeldung zum Prototyp abwarten, dann #003 Datenmodell (braucht PostgreSQL per Docker)
|
- #004 Agent-API: Enrollment, Report-Endpunkt, Token-Hashing, Rate-Limit, Tests
|
||||||
|
|||||||
@@ -11,12 +11,19 @@ Voraussetzungen: Node.js ≥ 24, Docker (für PostgreSQL ab #003).
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
npm run db:up # PostgreSQL im Container (Port 5433)
|
||||||
|
cp .env.example .env
|
||||||
|
npm run db:migrate # Schema anlegen
|
||||||
|
npm run db:seed # Administrator, Warnregeln, mit SEED_DEMO=1 auch Demo-Daten
|
||||||
npm run dev # http://localhost:3000
|
npm run dev # http://localhost:3000
|
||||||
|
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
npm test
|
npm test # legt bei Bedarf die Testdatenbank an
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Weitere Skripte: `npm run db:studio` (Datenbank-Oberfläche), `npm run db:down` (Container stoppen).
|
||||||
|
|
||||||
## Dokumentation
|
## Dokumentation
|
||||||
|
|
||||||
- [Architektur- und Umsetzungsplan](docs/architektur.md)
|
- [Architektur- und Umsetzungsplan](docs/architektur.md)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# PostgreSQL für die lokale Entwicklung. Die vollständige Compose-Datei
|
||||||
|
# (App + Worker + Datenbank) entsteht in #014.
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: proxledger-postgres-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: proxledger
|
||||||
|
POSTGRES_PASSWORD: proxledger
|
||||||
|
POSTGRES_DB: proxledger
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- proxledger-dev-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U proxledger"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
proxledger-dev-db:
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
# #003 Datenmodell und Migrationen
|
# #003 Datenmodell und Migrationen
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Meilenstein:** v0.1.0
|
**Meilenstein:** v0.1.0
|
||||||
**Labels:** backend, datenbank
|
**Labels:** backend, datenbank
|
||||||
**Geschätzt:** 5 h **Tatsächlich:** –
|
**Geschätzt:** 5 h **Tatsächlich:** 0 h 25 min
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
Vollständiges Prisma-Schema gemäß `docs/architektur.md` Abschnitt 3, erste Migration, Seed mit Admin-User und optionalen Demo-Daten.
|
Vollständiges Prisma-Schema gemäß `docs/architektur.md` Abschnitt 3, erste Migration, Seed mit Admin-User und optionalen Demo-Daten.
|
||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
- [ ] Docker-Compose für lokale Entwicklung (nur PostgreSQL)
|
- [x] Docker-Compose für lokale Entwicklung (nur PostgreSQL)
|
||||||
- [ ] Prisma-Schema: Customer, Site, Host, Tag, Report, SubscriptionKey, ServerIdHistory, SubscriptionPeriod, PurchaseInvoice(+Item), SalesInvoice(+Item), Attachment, AgentToken, EnrollmentToken, Alert, Notification, AlertRuleSetting, NotificationChannel, User, AuditLog, Setting
|
- [x] Prisma-Schema: Customer, Site, Host, Tag, Report, SubscriptionKey, ServerIdHistory, SubscriptionPeriod, PurchaseInvoice(+Item), SalesInvoice(+Item), Attachment, AgentToken, EnrollmentToken, Alert, Notification, AlertRuleSetting, NotificationChannel, User, AuditLog, Setting
|
||||||
- [ ] Indizes für Suche (Key, Server-ID, Hostname, IP, Rechnungsnummer)
|
- [x] Indizes für Suche (Key, Server-ID, Hostname, IP, Rechnungsnummer)
|
||||||
- [ ] Erste Migration
|
- [x] Erste Migration
|
||||||
- [ ] Seed: Admin-User aus Umgebungsvariablen, Standard-Warnregeln, optional Demo-Daten (`SEED_DEMO=1`)
|
- [x] Seed: Admin-User aus Umgebungsvariablen, Standard-Warnregeln, optional Demo-Daten (`SEED_DEMO=1`)
|
||||||
- [ ] DB-Client-Singleton, Test-Datenbank-Setup für Vitest
|
- [x] DB-Client-Singleton, Test-Datenbank-Setup für Vitest
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
- [ ] `npx prisma migrate dev` läuft auf leerer DB durch
|
- [x] `npx prisma migrate dev` läuft auf leerer DB durch
|
||||||
- [ ] `npx prisma db seed` legt Admin und Standard-Regeln an, idempotent
|
- [x] `npx prisma db seed` legt Admin und Standard-Regeln an, idempotent
|
||||||
- [ ] Beträge als Decimal, Zeitstempel in UTC
|
- [x] Beträge als Decimal, Zeitstempel in UTC
|
||||||
|
|
||||||
## Abhängigkeiten
|
## Abhängigkeiten
|
||||||
#001
|
#001
|
||||||
@@ -26,3 +26,7 @@ Vollständiges Prisma-Schema gemäß `docs/architektur.md` Abschnitt 3, erste Mi
|
|||||||
## Notizen / Entscheidungen
|
## Notizen / Entscheidungen
|
||||||
- Host-Identität über Agent-Token/`agentId`, nicht über Server-ID (siehe Architektur).
|
- Host-Identität über Agent-Token/`agentId`, nicht über Server-ID (siehe Architektur).
|
||||||
- Rechnungen als eigene Entitäten mit Positionen, damit Sammelbestellungen und Sammelrechnungen abbildbar sind.
|
- Rechnungen als eigene Entitäten mit Positionen, damit Sammelbestellungen und Sammelrechnungen abbildbar sind.
|
||||||
|
- Prisma 7.10.0 statt der npm-Kennzeichnung „latest“: dort liegt derzeit ein Release Candidate (8.0.0-rc).
|
||||||
|
- Prisma 7 benötigt einen Treiber-Adapter; im Einsatz ist `@prisma/adapter-pg`.
|
||||||
|
- Der Seed legt Demo-Daten aus `src/mock/data.ts` an, damit Prototyp und Datenbank dieselben Beispiele zeigen. Die Sammelrechnung PX-2025-88213 deckt bewusst zwei Laufzeiten ab.
|
||||||
|
- Passwörter werden mit argon2id gehasht (`@node-rs/argon2`, vorkompiliert – kein Build-Toolchain nötig).
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| [#001](001-projekt-setup.md) | Projekt-Setup | erledigt | v0.1.0 | 3 h | 0 h 08 min |
|
| [#001](001-projekt-setup.md) | Projekt-Setup | erledigt | v0.1.0 | 3 h | 0 h 08 min |
|
||||||
| [#002](002-prototyp.md) | Klickbarer Prototyp | erledigt | v0.1.0 | 8 h | 0 h 20 min |
|
| [#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 | – |
|
| [#003](003-datenmodell.md) | Datenmodell und Migrationen | erledigt | v0.1.0 | 5 h | 0 h 25 min |
|
||||||
| [#004](004-agent-api.md) | Agent-API | offen | v0.1.0 | 6 h | – |
|
| [#004](004-agent-api.md) | Agent-API | in Arbeit | v0.1.0 | 6 h | – |
|
||||||
| [#005](005-agent-skript.md) | Agent-Skript | offen | v0.2.0 | 8 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 | – |
|
| [#006](006-kunden-hostzuweisung.md) | Kunden, Standorte und Hostzuweisung | offen | v0.3.0 | 6 h | – |
|
||||||
| [#007](007-hosts-detailansichten.md) | Host-Tabelle, Suche und Detailansichten | offen | v0.4.0 | 8 h | – |
|
| [#007](007-hosts-detailansichten.md) | Host-Tabelle, Suche und Detailansichten | offen | v0.4.0 | 8 h | – |
|
||||||
@@ -20,4 +20,4 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
|||||||
| [#014](014-deployment.md) | Deployment und Dokumentation | offen | v0.10.0 | 8 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 | – |
|
| [#015](015-feinschliff-v1.md) | Feinschliff für 1.0 | offen | v1.0.0 | 6 h | – |
|
||||||
| [#016](016-anleitung-screenshots.md) | Benutzerhandbuch mit Screenshots | offen | v0.10.0 | 5 h | – |
|
| [#016](016-anleitung-screenshots.md) | Benutzerhandbuch mit Screenshots | offen | v0.10.0 | 5 h | – |
|
||||||
| | **Summe** | | | **106 h** | **0 h 28 min** |
|
| | **Summe** | | | **106 h** | **0 h 53 min** |
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ const eslintConfig = defineConfig([
|
|||||||
...nextVitals,
|
...nextVitals,
|
||||||
...nextTs,
|
...nextTs,
|
||||||
prettier,
|
prettier,
|
||||||
globalIgnores([".next/**", "out/**", "build/**", "coverage/**", "next-env.d.ts"]),
|
globalIgnores([".next/**", "out/**", "build/**", "coverage/**", "next-env.d.ts", "src/generated/**"]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|||||||
Generated
+2461
-14
File diff suppressed because it is too large
Load Diff
+14
-1
@@ -10,9 +10,19 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"db:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||||
|
"db:down": "docker compose -f docker-compose.dev.yml down",
|
||||||
|
"db:migrate": "prisma migrate dev",
|
||||||
|
"db:seed": "prisma db seed",
|
||||||
|
"db:studio": "prisma studio",
|
||||||
|
"db:generate": "prisma generate",
|
||||||
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@node-rs/argon2": "^2.2.1",
|
||||||
|
"@prisma/adapter-pg": "^7.10.0",
|
||||||
|
"@prisma/client": "^7.10.0",
|
||||||
"admin-lte": "^4.9.1",
|
"admin-lte": "^4.9.1",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
@@ -25,10 +35,13 @@
|
|||||||
"@types/node": "^24.13.5",
|
"@types/node": "^24.13.5",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.5",
|
"eslint-config-next": "16.3.5",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.9.7",
|
"prettier": "^3.9.7",
|
||||||
|
"prisma": "^7.10.0",
|
||||||
|
"tsx": "^4.23.13",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vitest": "^5.0.1"
|
"vitest": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,580 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Produkt" AS ENUM ('PVE', 'PBS', 'PMG');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'NOTFOUND', 'INVALID', 'EXPIRED', 'SUSPENDED', 'UNKNOWN');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "LaufzeitStatus" AS ENUM ('VORSCHLAG', 'BESTAETIGT');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AlertSchwere" AS ENUM ('INFO', 'WARNUNG', 'GEFAHR');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "NotificationKanal" AS ENUM ('EMAIL', 'WEBHOOK');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "NotificationStatus" AS ENUM ('GESENDET', 'FEHLGESCHLAGEN', 'WARTEND');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Kunde" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"kundennummer" TEXT NOT NULL,
|
||||||
|
"ansprechpartner" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
"telefon" TEXT,
|
||||||
|
"notizen" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Kunde_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Standort" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"kundeId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"strasse" TEXT,
|
||||||
|
"plz" TEXT,
|
||||||
|
"ort" TEXT,
|
||||||
|
"notizen" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Standort_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Host" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"agentId" TEXT NOT NULL,
|
||||||
|
"hostname" TEXT NOT NULL,
|
||||||
|
"fqdn" TEXT,
|
||||||
|
"kundeId" TEXT,
|
||||||
|
"standortId" TEXT,
|
||||||
|
"produkt" "Produkt" NOT NULL,
|
||||||
|
"produktVersion" TEXT,
|
||||||
|
"kernel" TEXT,
|
||||||
|
"serverId" TEXT,
|
||||||
|
"clusterName" TEXT,
|
||||||
|
"nodeName" TEXT,
|
||||||
|
"ips" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"sockets" INTEGER,
|
||||||
|
"cpuModell" TEXT,
|
||||||
|
"uptimeSekunden" BIGINT,
|
||||||
|
"paketUpdates" INTEGER,
|
||||||
|
"agentVersion" TEXT,
|
||||||
|
"notizen" TEXT,
|
||||||
|
"subKey" TEXT,
|
||||||
|
"subStatus" "SubscriptionStatus" NOT NULL DEFAULT 'UNKNOWN',
|
||||||
|
"subLevel" TEXT,
|
||||||
|
"subProduktname" TEXT,
|
||||||
|
"subLizenzierteSockets" INTEGER,
|
||||||
|
"subRegdate" DATE,
|
||||||
|
"subNextduedate" DATE,
|
||||||
|
"subChecktime" TIMESTAMP(3),
|
||||||
|
"erstmalsGesehenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"letzterReportAm" TIMESTAMP(3),
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Host_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Tag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"farbe" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Report" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"hostId" TEXT NOT NULL,
|
||||||
|
"reportId" TEXT NOT NULL,
|
||||||
|
"erhobenAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
"empfangenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"subStatus" "SubscriptionStatus" NOT NULL,
|
||||||
|
"subKey" TEXT,
|
||||||
|
"subNextduedate" DATE,
|
||||||
|
"serverId" TEXT,
|
||||||
|
"hostname" TEXT,
|
||||||
|
"produktVersion" TEXT,
|
||||||
|
"kernel" TEXT,
|
||||||
|
"sockets" INTEGER,
|
||||||
|
"paketUpdates" INTEGER,
|
||||||
|
"agentVersion" TEXT,
|
||||||
|
"ips" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"quellIp" TEXT,
|
||||||
|
"rohdaten" JSONB NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SubscriptionKeyHistorie" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"hostId" TEXT NOT NULL,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"ersteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"letzteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SubscriptionKeyHistorie_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ServerIdHistorie" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"hostId" TEXT NOT NULL,
|
||||||
|
"serverId" TEXT NOT NULL,
|
||||||
|
"ersteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"letzteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ServerIdHistorie_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Laufzeit" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"hostId" TEXT NOT NULL,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"start" DATE NOT NULL,
|
||||||
|
"ende" DATE NOT NULL,
|
||||||
|
"status" "LaufzeitStatus" NOT NULL DEFAULT 'VORSCHLAG',
|
||||||
|
"notizen" TEXT,
|
||||||
|
"proxmoxPositionId" TEXT,
|
||||||
|
"kundenPositionId" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Laufzeit_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ProxmoxRechnung" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"nummer" TEXT NOT NULL,
|
||||||
|
"datum" DATE NOT NULL,
|
||||||
|
"faelligAm" DATE,
|
||||||
|
"nettoBetrag" DECIMAL(12,2) NOT NULL,
|
||||||
|
"ustSatz" DECIMAL(5,2) NOT NULL DEFAULT 19.00,
|
||||||
|
"rabatt" DECIMAL(12,2),
|
||||||
|
"zahlungsart" TEXT,
|
||||||
|
"bezahlt" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"bezahltAm" DATE,
|
||||||
|
"notizen" TEXT,
|
||||||
|
"anhangId" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ProxmoxRechnung_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ProxmoxRechnungPosition" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"rechnungId" TEXT NOT NULL,
|
||||||
|
"bezeichnung" TEXT NOT NULL,
|
||||||
|
"menge" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"nettoBetrag" DECIMAL(12,2) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ProxmoxRechnungPosition_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Kundenrechnung" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"kundeId" TEXT NOT NULL,
|
||||||
|
"erstellt" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"nummer" TEXT,
|
||||||
|
"datum" DATE,
|
||||||
|
"nettoBetrag" DECIMAL(12,2),
|
||||||
|
"ustSatz" DECIMAL(5,2) NOT NULL DEFAULT 19.00,
|
||||||
|
"bezahlt" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"bezahltAm" DATE,
|
||||||
|
"notizen" TEXT,
|
||||||
|
"anhangId" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Kundenrechnung_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "KundenrechnungPosition" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"rechnungId" TEXT NOT NULL,
|
||||||
|
"bezeichnung" TEXT NOT NULL,
|
||||||
|
"menge" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"nettoBetrag" DECIMAL(12,2) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "KundenrechnungPosition_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Anhang" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"dateiname" TEXT NOT NULL,
|
||||||
|
"pfad" TEXT NOT NULL,
|
||||||
|
"mimeTyp" TEXT NOT NULL,
|
||||||
|
"groesse" INTEGER NOT NULL,
|
||||||
|
"sha256" TEXT NOT NULL,
|
||||||
|
"hochgeladenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Anhang_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AgentToken" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"hostId" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"praefix" TEXT NOT NULL,
|
||||||
|
"bezeichnung" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"letzteNutzungAm" TIMESTAMP(3),
|
||||||
|
"widerrufenAm" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "AgentToken_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "EnrollmentToken" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"praefix" TEXT NOT NULL,
|
||||||
|
"bezeichnung" TEXT,
|
||||||
|
"gueltigBis" TIMESTAMP(3),
|
||||||
|
"maxNutzungen" INTEGER,
|
||||||
|
"nutzungen" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"kundeId" TEXT,
|
||||||
|
"standortId" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"widerrufenAm" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "EnrollmentToken_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Alert" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"regel" TEXT NOT NULL,
|
||||||
|
"fingerprint" TEXT NOT NULL,
|
||||||
|
"schwere" "AlertSchwere" NOT NULL,
|
||||||
|
"titel" TEXT NOT NULL,
|
||||||
|
"beschreibung" TEXT NOT NULL,
|
||||||
|
"hostId" TEXT,
|
||||||
|
"kundeId" TEXT,
|
||||||
|
"laufzeitId" TEXT,
|
||||||
|
"geoeffnetAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"quittiertAm" TIMESTAMP(3),
|
||||||
|
"erledigtAm" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "Alert_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Benachrichtigung" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"alertId" TEXT,
|
||||||
|
"kanal" "NotificationKanal" NOT NULL,
|
||||||
|
"empfaenger" TEXT NOT NULL,
|
||||||
|
"betreff" TEXT,
|
||||||
|
"status" "NotificationStatus" NOT NULL DEFAULT 'WARTEND',
|
||||||
|
"fehler" TEXT,
|
||||||
|
"versendetAm" TIMESTAMP(3),
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Benachrichtigung_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WarnregelEinstellung" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"regel" TEXT NOT NULL,
|
||||||
|
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"schwellwerte" JSONB NOT NULL DEFAULT '{}',
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WarnregelEinstellung_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Benachrichtigungskanal" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"typ" "NotificationKanal" NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"konfiguration" JSONB NOT NULL,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Benachrichtigungskanal_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Benutzer" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"passwortHash" TEXT,
|
||||||
|
"oidcSubject" TEXT,
|
||||||
|
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"letzterLoginAm" TIMESTAMP(3),
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Benutzer_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AuditEintrag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"benutzerId" TEXT,
|
||||||
|
"aktion" TEXT NOT NULL,
|
||||||
|
"entitaet" TEXT NOT NULL,
|
||||||
|
"entitaetId" TEXT,
|
||||||
|
"aenderung" JSONB,
|
||||||
|
"ipAdresse" TEXT,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AuditEintrag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Einstellung" (
|
||||||
|
"schluessel" TEXT NOT NULL,
|
||||||
|
"wert" JSONB NOT NULL,
|
||||||
|
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Einstellung_pkey" PRIMARY KEY ("schluessel")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_HostTags" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_HostTags_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Kunde_kundennummer_key" ON "Kunde"("kundennummer");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Kunde_name_idx" ON "Kunde"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Standort_kundeId_idx" ON "Standort"("kundeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Host_agentId_key" ON "Host"("agentId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_kundeId_idx" ON "Host"("kundeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_standortId_idx" ON "Host"("standortId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_hostname_idx" ON "Host"("hostname");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_serverId_idx" ON "Host"("serverId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_subKey_idx" ON "Host"("subKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_subNextduedate_idx" ON "Host"("subNextduedate");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Host_subStatus_idx" ON "Host"("subStatus");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Report_reportId_key" ON "Report"("reportId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Report_hostId_erhobenAm_idx" ON "Report"("hostId", "erhobenAm");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Report_empfangenAm_idx" ON "Report"("empfangenAm");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SubscriptionKeyHistorie_key_idx" ON "SubscriptionKeyHistorie"("key");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "SubscriptionKeyHistorie_hostId_key_key" ON "SubscriptionKeyHistorie"("hostId", "key");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ServerIdHistorie_serverId_idx" ON "ServerIdHistorie"("serverId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ServerIdHistorie_hostId_serverId_key" ON "ServerIdHistorie"("hostId", "serverId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Laufzeit_proxmoxPositionId_key" ON "Laufzeit"("proxmoxPositionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Laufzeit_kundenPositionId_key" ON "Laufzeit"("kundenPositionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Laufzeit_ende_idx" ON "Laufzeit"("ende");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Laufzeit_key_idx" ON "Laufzeit"("key");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Laufzeit_hostId_start_key" ON "Laufzeit"("hostId", "start");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ProxmoxRechnung_nummer_key" ON "ProxmoxRechnung"("nummer");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ProxmoxRechnung_bezahlt_faelligAm_idx" ON "ProxmoxRechnung"("bezahlt", "faelligAm");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ProxmoxRechnung_datum_idx" ON "ProxmoxRechnung"("datum");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ProxmoxRechnungPosition_rechnungId_idx" ON "ProxmoxRechnungPosition"("rechnungId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Kundenrechnung_nummer_key" ON "Kundenrechnung"("nummer");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Kundenrechnung_kundeId_idx" ON "Kundenrechnung"("kundeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Kundenrechnung_erstellt_bezahlt_idx" ON "Kundenrechnung"("erstellt", "bezahlt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "KundenrechnungPosition_rechnungId_idx" ON "KundenrechnungPosition"("rechnungId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "AgentToken_tokenHash_key" ON "AgentToken"("tokenHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AgentToken_hostId_idx" ON "AgentToken"("hostId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "EnrollmentToken_tokenHash_key" ON "EnrollmentToken"("tokenHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Alert_fingerprint_key" ON "Alert"("fingerprint");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Alert_erledigtAm_idx" ON "Alert"("erledigtAm");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Alert_hostId_idx" ON "Alert"("hostId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Alert_kundeId_idx" ON "Alert"("kundeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Alert_regel_idx" ON "Alert"("regel");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Benachrichtigung_alertId_idx" ON "Benachrichtigung"("alertId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Benachrichtigung_erstelltAm_idx" ON "Benachrichtigung"("erstelltAm");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WarnregelEinstellung_regel_key" ON "WarnregelEinstellung"("regel");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Benutzer_email_key" ON "Benutzer"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Benutzer_oidcSubject_key" ON "Benutzer"("oidcSubject");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AuditEintrag_entitaet_entitaetId_idx" ON "AuditEintrag"("entitaet", "entitaetId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AuditEintrag_erstelltAm_idx" ON "AuditEintrag"("erstelltAm");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_HostTags_B_index" ON "_HostTags"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Standort" ADD CONSTRAINT "Standort_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Host" ADD CONSTRAINT "Host_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Host" ADD CONSTRAINT "Host_standortId_fkey" FOREIGN KEY ("standortId") REFERENCES "Standort"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Report" ADD CONSTRAINT "Report_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SubscriptionKeyHistorie" ADD CONSTRAINT "SubscriptionKeyHistorie_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ServerIdHistorie" ADD CONSTRAINT "ServerIdHistorie_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Laufzeit" ADD CONSTRAINT "Laufzeit_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Laufzeit" ADD CONSTRAINT "Laufzeit_proxmoxPositionId_fkey" FOREIGN KEY ("proxmoxPositionId") REFERENCES "ProxmoxRechnungPosition"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Laufzeit" ADD CONSTRAINT "Laufzeit_kundenPositionId_fkey" FOREIGN KEY ("kundenPositionId") REFERENCES "KundenrechnungPosition"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProxmoxRechnung" ADD CONSTRAINT "ProxmoxRechnung_anhangId_fkey" FOREIGN KEY ("anhangId") REFERENCES "Anhang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProxmoxRechnungPosition" ADD CONSTRAINT "ProxmoxRechnungPosition_rechnungId_fkey" FOREIGN KEY ("rechnungId") REFERENCES "ProxmoxRechnung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Kundenrechnung" ADD CONSTRAINT "Kundenrechnung_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Kundenrechnung" ADD CONSTRAINT "Kundenrechnung_anhangId_fkey" FOREIGN KEY ("anhangId") REFERENCES "Anhang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "KundenrechnungPosition" ADD CONSTRAINT "KundenrechnungPosition_rechnungId_fkey" FOREIGN KEY ("rechnungId") REFERENCES "Kundenrechnung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AgentToken" ADD CONSTRAINT "AgentToken_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EnrollmentToken" ADD CONSTRAINT "EnrollmentToken_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EnrollmentToken" ADD CONSTRAINT "EnrollmentToken_standortId_fkey" FOREIGN KEY ("standortId") REFERENCES "Standort"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_laufzeitId_fkey" FOREIGN KEY ("laufzeitId") REFERENCES "Laufzeit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Benachrichtigung" ADD CONSTRAINT "Benachrichtigung_alertId_fkey" FOREIGN KEY ("alertId") REFERENCES "Alert"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AuditEintrag" ADD CONSTRAINT "AuditEintrag_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_HostTags" ADD CONSTRAINT "_HostTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_HostTags" ADD CONSTRAINT "_HostTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
// ProxLedger – Datenmodell
|
||||||
|
// Entwurfsentscheidungen siehe docs/architektur.md, Abschnitt 3.
|
||||||
|
// Kurz: Ein Host wird über seine agentId identifiziert (nicht über die Server-ID, die sich bei
|
||||||
|
// Hardwaretausch ändert), Rechnungen sind eigene Entitäten mit Positionen, damit Sammelbestellungen
|
||||||
|
// bei Proxmox und Sammelrechnungen an Kunden ohne doppelte Rechnungsnummern abbildbar sind.
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../src/generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Aufzählungen
|
||||||
|
|
||||||
|
enum Produkt {
|
||||||
|
PVE
|
||||||
|
PBS
|
||||||
|
PMG
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionStatus {
|
||||||
|
ACTIVE
|
||||||
|
NOTFOUND
|
||||||
|
INVALID
|
||||||
|
EXPIRED
|
||||||
|
SUSPENDED
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LaufzeitStatus {
|
||||||
|
VORSCHLAG
|
||||||
|
BESTAETIGT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AlertSchwere {
|
||||||
|
INFO
|
||||||
|
WARNUNG
|
||||||
|
GEFAHR
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationKanal {
|
||||||
|
EMAIL
|
||||||
|
WEBHOOK
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationStatus {
|
||||||
|
GESENDET
|
||||||
|
FEHLGESCHLAGEN
|
||||||
|
WARTEND
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Kunden und Standorte
|
||||||
|
|
||||||
|
model Kunde {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
kundennummer String @unique
|
||||||
|
ansprechpartner String?
|
||||||
|
email String?
|
||||||
|
telefon String?
|
||||||
|
notizen String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
standorte Standort[]
|
||||||
|
hosts Host[]
|
||||||
|
kundenrechnungen Kundenrechnung[]
|
||||||
|
enrollmentTokens EnrollmentToken[]
|
||||||
|
alerts Alert[]
|
||||||
|
|
||||||
|
@@index([name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Standort {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
kundeId String
|
||||||
|
name String
|
||||||
|
strasse String?
|
||||||
|
plz String?
|
||||||
|
ort String?
|
||||||
|
notizen String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||||
|
hosts Host[]
|
||||||
|
enrollmentTokens EnrollmentToken[]
|
||||||
|
|
||||||
|
@@index([kundeId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Hosts
|
||||||
|
|
||||||
|
model Host {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
/// Stabile Kennung, die der Agent bei der Registrierung erzeugt (aus /etc/machine-id abgeleitet).
|
||||||
|
/// Sie bleibt auch dann gleich, wenn sich die Proxmox-Server-ID durch Hardwaretausch ändert.
|
||||||
|
agentId String @unique
|
||||||
|
|
||||||
|
hostname String
|
||||||
|
fqdn String?
|
||||||
|
kundeId String?
|
||||||
|
standortId String?
|
||||||
|
produkt Produkt
|
||||||
|
produktVersion String?
|
||||||
|
kernel String?
|
||||||
|
|
||||||
|
/// Aktuelle Server-ID laut letztem Report; die Historie steht in ServerIdHistorie.
|
||||||
|
serverId String?
|
||||||
|
clusterName String?
|
||||||
|
nodeName String?
|
||||||
|
ips String[] @default([])
|
||||||
|
sockets Int?
|
||||||
|
cpuModell String?
|
||||||
|
uptimeSekunden BigInt?
|
||||||
|
paketUpdates Int?
|
||||||
|
agentVersion String?
|
||||||
|
notizen String?
|
||||||
|
|
||||||
|
// Momentaufnahme der Subscription aus dem letzten Report (für Listen und Filter).
|
||||||
|
subKey String?
|
||||||
|
subStatus SubscriptionStatus @default(UNKNOWN)
|
||||||
|
subLevel String?
|
||||||
|
subProduktname String?
|
||||||
|
subLizenzierteSockets Int?
|
||||||
|
subRegdate DateTime? @db.Date
|
||||||
|
subNextduedate DateTime? @db.Date
|
||||||
|
subChecktime DateTime?
|
||||||
|
|
||||||
|
erstmalsGesehenAm DateTime @default(now())
|
||||||
|
letzterReportAm DateTime?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: SetNull)
|
||||||
|
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
||||||
|
reports Report[]
|
||||||
|
keyHistorie SubscriptionKeyHistorie[]
|
||||||
|
serverIdHistorie ServerIdHistorie[]
|
||||||
|
laufzeiten Laufzeit[]
|
||||||
|
tokens AgentToken[]
|
||||||
|
alerts Alert[]
|
||||||
|
tags Tag[] @relation("HostTags")
|
||||||
|
|
||||||
|
@@index([kundeId])
|
||||||
|
@@index([standortId])
|
||||||
|
@@index([hostname])
|
||||||
|
@@index([serverId])
|
||||||
|
@@index([subKey])
|
||||||
|
@@index([subNextduedate])
|
||||||
|
@@index([subStatus])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
farbe String?
|
||||||
|
|
||||||
|
hosts Host[] @relation("HostTags")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Report {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
hostId String
|
||||||
|
|
||||||
|
/// Vom Agent erzeugte UUID; verhindert doppelte Verarbeitung nachgesendeter Reports.
|
||||||
|
reportId String @unique
|
||||||
|
|
||||||
|
/// Zeitpunkt der Datenerhebung auf dem Host (kann bei nachgesendeten Reports älter sein).
|
||||||
|
erhobenAm DateTime
|
||||||
|
empfangenAm DateTime @default(now())
|
||||||
|
|
||||||
|
subStatus SubscriptionStatus
|
||||||
|
subKey String?
|
||||||
|
subNextduedate DateTime? @db.Date
|
||||||
|
serverId String?
|
||||||
|
hostname String?
|
||||||
|
produktVersion String?
|
||||||
|
kernel String?
|
||||||
|
sockets Int?
|
||||||
|
paketUpdates Int?
|
||||||
|
agentVersion String?
|
||||||
|
ips String[] @default([])
|
||||||
|
quellIp String?
|
||||||
|
|
||||||
|
/// Vollständiger Report, damit auch künftige Felder nachträglich auswertbar sind.
|
||||||
|
rohdaten Json
|
||||||
|
|
||||||
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([hostId, erhobenAm])
|
||||||
|
@@index([empfangenAm])
|
||||||
|
}
|
||||||
|
|
||||||
|
model SubscriptionKeyHistorie {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
hostId String
|
||||||
|
key String
|
||||||
|
ersteMeldung DateTime @default(now())
|
||||||
|
letzteMeldung DateTime @default(now())
|
||||||
|
|
||||||
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([hostId, key])
|
||||||
|
@@index([key])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ServerIdHistorie {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
hostId String
|
||||||
|
serverId String
|
||||||
|
ersteMeldung DateTime @default(now())
|
||||||
|
letzteMeldung DateTime @default(now())
|
||||||
|
|
||||||
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([hostId, serverId])
|
||||||
|
@@index([serverId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Laufzeiten und Rechnungen
|
||||||
|
|
||||||
|
model Laufzeit {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
hostId String
|
||||||
|
key String
|
||||||
|
start DateTime @db.Date
|
||||||
|
ende DateTime @db.Date
|
||||||
|
status LaufzeitStatus @default(VORSCHLAG)
|
||||||
|
notizen String?
|
||||||
|
|
||||||
|
/// Position der Proxmox-Rechnung (Einkauf), die diese Laufzeit bezahlt.
|
||||||
|
proxmoxPositionId String? @unique
|
||||||
|
|
||||||
|
/// Position der Kundenrechnung (Verkauf), mit der die Laufzeit weiterberechnet wird.
|
||||||
|
kundenPositionId String? @unique
|
||||||
|
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
|
proxmoxPosition ProxmoxRechnungPosition? @relation(fields: [proxmoxPositionId], references: [id], onDelete: SetNull)
|
||||||
|
kundenPosition KundenrechnungPosition? @relation(fields: [kundenPositionId], references: [id], onDelete: SetNull)
|
||||||
|
alerts Alert[]
|
||||||
|
|
||||||
|
@@unique([hostId, start])
|
||||||
|
@@index([ende])
|
||||||
|
@@index([key])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rechnung von Proxmox (Einkauf). Eine Bestellung kann mehrere Keys umfassen.
|
||||||
|
model ProxmoxRechnung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
nummer String @unique
|
||||||
|
datum DateTime @db.Date
|
||||||
|
faelligAm DateTime? @db.Date
|
||||||
|
nettoBetrag Decimal @db.Decimal(12, 2)
|
||||||
|
ustSatz Decimal @default(19.00) @db.Decimal(5, 2)
|
||||||
|
rabatt Decimal? @db.Decimal(12, 2)
|
||||||
|
zahlungsart String?
|
||||||
|
bezahlt Boolean @default(false)
|
||||||
|
bezahltAm DateTime? @db.Date
|
||||||
|
notizen String?
|
||||||
|
anhangId String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
anhang Anhang? @relation(fields: [anhangId], references: [id], onDelete: SetNull)
|
||||||
|
positionen ProxmoxRechnungPosition[]
|
||||||
|
|
||||||
|
@@index([bezahlt, faelligAm])
|
||||||
|
@@index([datum])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ProxmoxRechnungPosition {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
rechnungId String
|
||||||
|
bezeichnung String
|
||||||
|
menge Int @default(1)
|
||||||
|
nettoBetrag Decimal @db.Decimal(12, 2)
|
||||||
|
|
||||||
|
rechnung ProxmoxRechnung @relation(fields: [rechnungId], references: [id], onDelete: Cascade)
|
||||||
|
laufzeit Laufzeit?
|
||||||
|
|
||||||
|
@@index([rechnungId])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rechnung an den Kunden (Verkauf). `erstellt = false` bedeutet: steht noch aus.
|
||||||
|
model Kundenrechnung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
kundeId String
|
||||||
|
erstellt Boolean @default(false)
|
||||||
|
nummer String? @unique
|
||||||
|
datum DateTime? @db.Date
|
||||||
|
nettoBetrag Decimal? @db.Decimal(12, 2)
|
||||||
|
ustSatz Decimal @default(19.00) @db.Decimal(5, 2)
|
||||||
|
bezahlt Boolean @default(false)
|
||||||
|
bezahltAm DateTime? @db.Date
|
||||||
|
notizen String?
|
||||||
|
anhangId String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||||
|
anhang Anhang? @relation(fields: [anhangId], references: [id], onDelete: SetNull)
|
||||||
|
positionen KundenrechnungPosition[]
|
||||||
|
|
||||||
|
@@index([kundeId])
|
||||||
|
@@index([erstellt, bezahlt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model KundenrechnungPosition {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
rechnungId String
|
||||||
|
bezeichnung String
|
||||||
|
menge Int @default(1)
|
||||||
|
nettoBetrag Decimal @db.Decimal(12, 2)
|
||||||
|
|
||||||
|
rechnung Kundenrechnung @relation(fields: [rechnungId], references: [id], onDelete: Cascade)
|
||||||
|
laufzeit Laufzeit?
|
||||||
|
|
||||||
|
@@index([rechnungId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Anhang {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
dateiname String
|
||||||
|
pfad String
|
||||||
|
mimeTyp String
|
||||||
|
groesse Int
|
||||||
|
sha256 String
|
||||||
|
hochgeladenAm DateTime @default(now())
|
||||||
|
|
||||||
|
proxmoxRechnungen ProxmoxRechnung[]
|
||||||
|
kundenrechnungen Kundenrechnung[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Tokens
|
||||||
|
|
||||||
|
/// Host-spezifischer API-Token. In der Datenbank liegt nur der SHA-256-Hash.
|
||||||
|
model AgentToken {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
hostId String
|
||||||
|
tokenHash String @unique
|
||||||
|
praefix String
|
||||||
|
bezeichnung String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
letzteNutzungAm DateTime?
|
||||||
|
widerrufenAm DateTime?
|
||||||
|
|
||||||
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([hostId])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token für die Erstregistrierung, optional mit Kunden- und Standortzuordnung.
|
||||||
|
model EnrollmentToken {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
praefix String
|
||||||
|
bezeichnung String?
|
||||||
|
gueltigBis DateTime?
|
||||||
|
maxNutzungen Int?
|
||||||
|
nutzungen Int @default(0)
|
||||||
|
kundeId String?
|
||||||
|
standortId String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
widerrufenAm DateTime?
|
||||||
|
|
||||||
|
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: SetNull)
|
||||||
|
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Warnungen und Benachrichtigungen
|
||||||
|
|
||||||
|
model Alert {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
regel String
|
||||||
|
|
||||||
|
/// Regel + betroffenes Objekt (+ Schwelle). Verhindert doppelte offene Warnungen.
|
||||||
|
fingerprint String @unique
|
||||||
|
|
||||||
|
schwere AlertSchwere
|
||||||
|
titel String
|
||||||
|
beschreibung String
|
||||||
|
|
||||||
|
hostId String?
|
||||||
|
kundeId String?
|
||||||
|
laufzeitId String?
|
||||||
|
|
||||||
|
geoeffnetAm DateTime @default(now())
|
||||||
|
quittiertAm DateTime?
|
||||||
|
erledigtAm DateTime?
|
||||||
|
|
||||||
|
host Host? @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
|
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||||
|
laufzeit Laufzeit? @relation(fields: [laufzeitId], references: [id], onDelete: Cascade)
|
||||||
|
benachrichtigungen Benachrichtigung[]
|
||||||
|
|
||||||
|
@@index([erledigtAm])
|
||||||
|
@@index([hostId])
|
||||||
|
@@index([kundeId])
|
||||||
|
@@index([regel])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Benachrichtigung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
alertId String?
|
||||||
|
kanal NotificationKanal
|
||||||
|
empfaenger String
|
||||||
|
betreff String?
|
||||||
|
status NotificationStatus @default(WARTEND)
|
||||||
|
fehler String?
|
||||||
|
versendetAm DateTime?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
|
||||||
|
alert Alert? @relation(fields: [alertId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([alertId])
|
||||||
|
@@index([erstelltAm])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schwellwerte je Warnregel, z. B. { "tage": [30, 14, 7] }.
|
||||||
|
model WarnregelEinstellung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
regel String @unique
|
||||||
|
aktiv Boolean @default(true)
|
||||||
|
schwellwerte Json @default("{}")
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Benachrichtigungskanal {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
typ NotificationKanal
|
||||||
|
name String
|
||||||
|
aktiv Boolean @default(true)
|
||||||
|
konfiguration Json
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Benutzer und Protokoll
|
||||||
|
|
||||||
|
/// Alle Benutzer sind Administratoren; eine Nur-Lese-Rolle ist bewusst nicht vorgesehen.
|
||||||
|
model Benutzer {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String
|
||||||
|
passwortHash String?
|
||||||
|
oidcSubject String? @unique
|
||||||
|
aktiv Boolean @default(true)
|
||||||
|
letzterLoginAm DateTime?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
auditEintraege AuditEintrag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model AuditEintrag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
benutzerId String?
|
||||||
|
aktion String
|
||||||
|
entitaet String
|
||||||
|
entitaetId String?
|
||||||
|
aenderung Json?
|
||||||
|
ipAdresse String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
|
||||||
|
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([entitaet, entitaetId])
|
||||||
|
@@index([erstelltAm])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Freie Systemeinstellungen (z. B. SMTP-Konfiguration, Anzeigeoptionen).
|
||||||
|
model Einstellung {
|
||||||
|
schluessel String @id
|
||||||
|
wert Json
|
||||||
|
geaendertAm DateTime @updatedAt
|
||||||
|
}
|
||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
import { hash } from "@node-rs/argon2";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { PrismaClient } from "../src/generated/prisma/client";
|
||||||
|
import type { Prisma } from "../src/generated/prisma/client";
|
||||||
|
import type { Produkt, SubscriptionStatus } from "../src/generated/prisma/enums";
|
||||||
|
import { alerts, hosts, kunden, laufzeiten, reports } from "../src/mock/data";
|
||||||
|
import type { Laufzeit as MockLaufzeit } from "../src/mock/types";
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
if (!connectionString) throw new Error("DATABASE_URL ist nicht gesetzt.");
|
||||||
|
|
||||||
|
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }) });
|
||||||
|
|
||||||
|
/** Standardwerte der Warnregeln; Schwellwerte sind später in der Oberfläche änderbar (#011). */
|
||||||
|
const WARNREGELN: { regel: string; schwellwerte: Prisma.InputJsonValue }[] = [
|
||||||
|
{ regel: "subscription-laeuft-ab", schwellwerte: { tage: [30, 14, 7] } },
|
||||||
|
{ regel: "status-nicht-aktiv", schwellwerte: {} },
|
||||||
|
{ regel: "sockets-abweichung", schwellwerte: {} },
|
||||||
|
{ regel: "keine-meldung", schwellwerte: { stunden: 48 } },
|
||||||
|
{ regel: "serverid-geaendert", schwellwerte: {} },
|
||||||
|
{ regel: "key-mehrfach-gemeldet", schwellwerte: {} },
|
||||||
|
{ regel: "host-ohne-kunde", schwellwerte: {} },
|
||||||
|
{ regel: "proxmox-rechnung-faellig", schwellwerte: { tage: 7 } },
|
||||||
|
{ regel: "kundenrechnung-fehlt", schwellwerte: {} },
|
||||||
|
{ regel: "kundenrechnung-unbezahlt", schwellwerte: { tage: 30 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function seedAdmin() {
|
||||||
|
const email = process.env.ADMIN_EMAIL ?? "admin@example.de";
|
||||||
|
const name = process.env.ADMIN_NAME ?? "Administrator";
|
||||||
|
const passwort = process.env.ADMIN_PASSWORD;
|
||||||
|
|
||||||
|
if (!passwort) {
|
||||||
|
console.warn("ADMIN_PASSWORD ist nicht gesetzt – der Administrator wird ohne Passwort angelegt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwortHash = passwort ? await hash(passwort) : null;
|
||||||
|
|
||||||
|
await prisma.benutzer.upsert({
|
||||||
|
where: { email },
|
||||||
|
// Ein vorhandenes Passwort wird nicht überschrieben, damit der Seed gefahrlos wiederholbar ist.
|
||||||
|
update: { name },
|
||||||
|
create: { email, name, passwortHash },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Administrator: ${email}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedWarnregeln() {
|
||||||
|
for (const regel of WARNREGELN) {
|
||||||
|
await prisma.warnregelEinstellung.upsert({
|
||||||
|
where: { regel: regel.regel },
|
||||||
|
update: {},
|
||||||
|
create: { regel: regel.regel, aktiv: true, schwellwerte: regel.schwellwerte },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`${WARNREGELN.length} Warnregeln vorbereitet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRODUKT: Record<string, Produkt> = { pve: "PVE", pbs: "PBS", pmg: "PMG" };
|
||||||
|
const STATUS: Record<string, SubscriptionStatus> = {
|
||||||
|
active: "ACTIVE",
|
||||||
|
notfound: "NOTFOUND",
|
||||||
|
invalid: "INVALID",
|
||||||
|
expired: "EXPIRED",
|
||||||
|
suspended: "SUSPENDED",
|
||||||
|
};
|
||||||
|
|
||||||
|
function datum(wert: string | null | undefined): Date | null {
|
||||||
|
return wert ? new Date(wert) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedDemo() {
|
||||||
|
for (const kunde of kunden) {
|
||||||
|
await prisma.kunde.upsert({
|
||||||
|
where: { id: kunde.id },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: kunde.id,
|
||||||
|
name: kunde.name,
|
||||||
|
kundennummer: kunde.kundennummer,
|
||||||
|
ansprechpartner: kunde.ansprechpartner,
|
||||||
|
email: kunde.email,
|
||||||
|
telefon: kunde.telefon,
|
||||||
|
notizen: kunde.notizen,
|
||||||
|
standorte: {
|
||||||
|
create: kunde.standorte.map((standort) => ({
|
||||||
|
id: standort.id,
|
||||||
|
name: standort.name,
|
||||||
|
ort: standort.ort,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const host of hosts) {
|
||||||
|
await prisma.host.upsert({
|
||||||
|
where: { id: host.id },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: host.id,
|
||||||
|
agentId: `demo-${host.id}`,
|
||||||
|
hostname: host.hostname,
|
||||||
|
fqdn: host.fqdn,
|
||||||
|
kundeId: host.kundeId,
|
||||||
|
standortId: host.standortId,
|
||||||
|
produkt: PRODUKT[host.produkt],
|
||||||
|
produktVersion: host.produktVersion,
|
||||||
|
kernel: host.kernel,
|
||||||
|
serverId: host.serverId,
|
||||||
|
clusterName: host.clusterName,
|
||||||
|
nodeName: host.nodeName,
|
||||||
|
ips: host.ips,
|
||||||
|
sockets: host.sockets,
|
||||||
|
cpuModell: host.cpuModell,
|
||||||
|
uptimeSekunden: host.uptimeSekunden === null ? null : BigInt(host.uptimeSekunden),
|
||||||
|
paketUpdates: host.paketUpdates,
|
||||||
|
agentVersion: host.agentVersion,
|
||||||
|
notizen: host.notizen,
|
||||||
|
subKey: host.subscription.key,
|
||||||
|
subStatus: STATUS[host.subscription.status],
|
||||||
|
subLevel: host.subscription.level,
|
||||||
|
subProduktname: host.subscription.produktname,
|
||||||
|
subLizenzierteSockets: host.subscription.lizenzierteSockets,
|
||||||
|
subRegdate: datum(host.subscription.regdate),
|
||||||
|
subNextduedate: datum(host.subscription.nextduedate),
|
||||||
|
subChecktime: datum(host.subscription.checktime),
|
||||||
|
letzterReportAm: datum(host.letzterReport),
|
||||||
|
tags: {
|
||||||
|
connectOrCreate: host.tags.map((tag) => ({ where: { name: tag }, create: { name: tag } })),
|
||||||
|
},
|
||||||
|
keyHistorie: host.subscription.key
|
||||||
|
? { create: [{ key: host.subscription.key }] }
|
||||||
|
: undefined,
|
||||||
|
serverIdHistorie: { create: [{ serverId: host.serverId }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rechnungen entstehen aus den Laufzeiten: gleiche Rechnungsnummer = eine Rechnung mit mehreren Positionen.
|
||||||
|
const proxmoxRechnungen = new Map<string, MockLaufzeit[]>();
|
||||||
|
const kundenrechnungen = new Map<string, MockLaufzeit[]>();
|
||||||
|
|
||||||
|
for (const laufzeit of laufzeiten) {
|
||||||
|
if (laufzeit.einkauf?.nummer) {
|
||||||
|
const bisher = proxmoxRechnungen.get(laufzeit.einkauf.nummer) ?? [];
|
||||||
|
proxmoxRechnungen.set(laufzeit.einkauf.nummer, [...bisher, laufzeit]);
|
||||||
|
}
|
||||||
|
if (laufzeit.verkauf) {
|
||||||
|
const schluessel = laufzeit.verkauf.nummer ?? `ohne-nummer-${laufzeit.id}`;
|
||||||
|
const bisher = kundenrechnungen.get(schluessel) ?? [];
|
||||||
|
kundenrechnungen.set(schluessel, [...bisher, laufzeit]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxmoxPositionen = new Map<string, string>();
|
||||||
|
for (const [nummer, zugehoerig] of proxmoxRechnungen) {
|
||||||
|
const vorlage = zugehoerig[0].einkauf!;
|
||||||
|
const rechnung = await prisma.proxmoxRechnung.upsert({
|
||||||
|
where: { nummer },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
nummer,
|
||||||
|
datum: new Date(vorlage.datum!),
|
||||||
|
faelligAm: datum(vorlage.faelligAm),
|
||||||
|
nettoBetrag: zugehoerig.reduce((summe, l) => summe + (l.einkauf?.nettoBetrag ?? 0), 0),
|
||||||
|
ustSatz: vorlage.ustSatz,
|
||||||
|
rabatt: vorlage.rabatt ?? null,
|
||||||
|
zahlungsart: vorlage.zahlungsart ?? null,
|
||||||
|
bezahlt: vorlage.bezahlt,
|
||||||
|
bezahltAm: datum(vorlage.bezahltAm),
|
||||||
|
positionen: {
|
||||||
|
create: zugehoerig.map((l) => ({
|
||||||
|
id: `prp-${l.id}`,
|
||||||
|
bezeichnung: `${l.key} · ${l.start} bis ${l.ende}`,
|
||||||
|
nettoBetrag: l.einkauf?.nettoBetrag ?? 0,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const l of zugehoerig) proxmoxPositionen.set(l.id, `prp-${l.id}`);
|
||||||
|
console.log(`Proxmox-Rechnung ${rechnung.nummer} mit ${zugehoerig.length} Position(en)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kundenPositionen = new Map<string, string>();
|
||||||
|
for (const [schluessel, zugehoerig] of kundenrechnungen) {
|
||||||
|
const vorlage = zugehoerig[0].verkauf!;
|
||||||
|
const host = hosts.find((h) => h.id === zugehoerig[0].hostId);
|
||||||
|
if (!host?.kundeId) continue;
|
||||||
|
|
||||||
|
const id = `kr-${schluessel}`;
|
||||||
|
await prisma.kundenrechnung.upsert({
|
||||||
|
where: { id },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id,
|
||||||
|
kundeId: host.kundeId,
|
||||||
|
erstellt: vorlage.erstellt,
|
||||||
|
nummer: vorlage.nummer,
|
||||||
|
datum: datum(vorlage.datum),
|
||||||
|
nettoBetrag: vorlage.erstellt
|
||||||
|
? zugehoerig.reduce((summe, l) => summe + (l.verkauf?.nettoBetrag ?? 0), 0)
|
||||||
|
: null,
|
||||||
|
ustSatz: vorlage.ustSatz,
|
||||||
|
bezahlt: vorlage.bezahlt,
|
||||||
|
bezahltAm: datum(vorlage.bezahltAm),
|
||||||
|
positionen: {
|
||||||
|
create: zugehoerig.map((l) => ({
|
||||||
|
id: `krp-${l.id}`,
|
||||||
|
bezeichnung: `${l.key} · ${l.start} bis ${l.ende}`,
|
||||||
|
nettoBetrag: l.verkauf?.nettoBetrag ?? 0,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const l of zugehoerig) kundenPositionen.set(l.id, `krp-${l.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const laufzeit of laufzeiten) {
|
||||||
|
await prisma.laufzeit.upsert({
|
||||||
|
where: { id: laufzeit.id },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: laufzeit.id,
|
||||||
|
hostId: laufzeit.hostId,
|
||||||
|
key: laufzeit.key,
|
||||||
|
start: new Date(laufzeit.start),
|
||||||
|
ende: new Date(laufzeit.ende),
|
||||||
|
status: laufzeit.vorschlag ? "VORSCHLAG" : "BESTAETIGT",
|
||||||
|
proxmoxPositionId: proxmoxPositionen.get(laufzeit.id) ?? null,
|
||||||
|
kundenPositionId: kundenPositionen.get(laufzeit.id) ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const report of reports) {
|
||||||
|
await prisma.report.upsert({
|
||||||
|
where: { reportId: report.id },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
reportId: report.id,
|
||||||
|
hostId: report.hostId,
|
||||||
|
erhobenAm: new Date(report.zeitpunkt),
|
||||||
|
subStatus: STATUS[report.status],
|
||||||
|
subKey: report.key,
|
||||||
|
subNextduedate: datum(report.nextduedate),
|
||||||
|
agentVersion: report.agentVersion,
|
||||||
|
paketUpdates: report.paketUpdates,
|
||||||
|
rohdaten: { hinweis: "Demo-Report ohne Rohdaten" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const alert of alerts) {
|
||||||
|
await prisma.alert.upsert({
|
||||||
|
where: { fingerprint: `${alert.typ}:${alert.hostId ?? alert.kundeId ?? alert.id}` },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: alert.id,
|
||||||
|
fingerprint: `${alert.typ}:${alert.hostId ?? alert.kundeId ?? alert.id}`,
|
||||||
|
regel: alert.typ,
|
||||||
|
schwere: alert.schwere === "gefahr" ? "GEFAHR" : alert.schwere === "warnung" ? "WARNUNG" : "INFO",
|
||||||
|
titel: alert.titel,
|
||||||
|
beschreibung: alert.beschreibung,
|
||||||
|
hostId: alert.hostId,
|
||||||
|
kundeId: alert.kundeId,
|
||||||
|
laufzeitId: alert.laufzeitId,
|
||||||
|
geoeffnetAm: new Date(alert.seit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Demo-Daten: ${kunden.length} Kunden, ${hosts.length} Hosts, ${laufzeiten.length} Laufzeiten, ${alerts.length} Warnungen`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await seedAdmin();
|
||||||
|
await seedWarnregeln();
|
||||||
|
|
||||||
|
if (process.env.SEED_DEMO === "1") {
|
||||||
|
await seedDemo();
|
||||||
|
} else {
|
||||||
|
console.log("Demo-Daten übersprungen (SEED_DEMO=1 setzen, um sie anzulegen).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => prisma.$disconnect())
|
||||||
|
.catch(async (fehler) => {
|
||||||
|
console.error(fehler);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
|
// npm install --save-dev prisma dotenv
|
||||||
|
import "dotenv/config";
|
||||||
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
seed: "tsx prisma/seed.ts",
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: process.env["DATABASE_URL"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { PrismaClient } from "@/generated/prisma/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma-Client als Singleton. Im Entwicklungsmodus lädt Next.js Module neu,
|
||||||
|
* ohne den Prozess zu beenden – ohne Singleton entstünden dabei immer neue Verbindungspools.
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
var proxledgerPrisma: PrismaClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function erzeugeClient(): PrismaClient {
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error("DATABASE_URL ist nicht gesetzt. Beispielwerte stehen in .env.example.");
|
||||||
|
}
|
||||||
|
return new PrismaClient({ adapter: new PrismaPg({ connectionString }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prisma: PrismaClient = globalThis.proxledgerPrisma ?? erzeugeClient();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
globalThis.proxledgerPrisma = prisma;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||||
|
|
||||||
|
describe.runIf(datenbankVerfuegbar)("Datenmodell", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await datenbankLeeren();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testPrisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verteilt eine Proxmox-Sammelrechnung auf mehrere Laufzeiten", async () => {
|
||||||
|
const kunde = await testPrisma.kunde.create({
|
||||||
|
data: { name: "Testkunde GmbH", kundennummer: "K-9001" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostDaten = [
|
||||||
|
{ agentId: "test-1", hostname: "pve-test-01", key: "pve1c-aaaa111111" },
|
||||||
|
{ agentId: "test-2", hostname: "pve-test-02", key: "pve1c-bbbb222222" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rechnung = await testPrisma.proxmoxRechnung.create({
|
||||||
|
data: {
|
||||||
|
nummer: "PX-TEST-1",
|
||||||
|
datum: new Date("2026-09-01"),
|
||||||
|
faelligAm: new Date("2026-09-15"),
|
||||||
|
nettoBetrag: 230,
|
||||||
|
bezahlt: false,
|
||||||
|
positionen: {
|
||||||
|
create: hostDaten.map((h) => ({ bezeichnung: h.key, nettoBetrag: 115 })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { positionen: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [index, daten] of hostDaten.entries()) {
|
||||||
|
const host = await testPrisma.host.create({
|
||||||
|
data: {
|
||||||
|
agentId: daten.agentId,
|
||||||
|
hostname: daten.hostname,
|
||||||
|
produkt: "PVE",
|
||||||
|
kundeId: kunde.id,
|
||||||
|
subKey: daten.key,
|
||||||
|
subStatus: "ACTIVE",
|
||||||
|
subNextduedate: new Date("2027-09-01"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await testPrisma.laufzeit.create({
|
||||||
|
data: {
|
||||||
|
hostId: host.id,
|
||||||
|
key: daten.key,
|
||||||
|
start: new Date("2026-09-01"),
|
||||||
|
ende: new Date("2027-08-31"),
|
||||||
|
status: "BESTAETIGT",
|
||||||
|
proxmoxPositionId: rechnung.positionen[index].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const laufzeiten = await testPrisma.laufzeit.findMany({
|
||||||
|
include: { proxmoxPosition: { include: { rechnung: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(laufzeiten).toHaveLength(2);
|
||||||
|
expect(laufzeiten.every((l) => l.proxmoxPosition?.rechnung.nummer === "PX-TEST-1")).toBe(true);
|
||||||
|
expect(
|
||||||
|
laufzeiten.reduce((summe, l) => summe + Number(l.proxmoxPosition?.nettoBetrag ?? 0), 0),
|
||||||
|
).toBe(230);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("erlaubt Hosts ohne Kunde und setzt die Zuordnung beim Löschen des Kunden zurück", async () => {
|
||||||
|
const kunde = await testPrisma.kunde.create({
|
||||||
|
data: { name: "Wegfallend GmbH", kundennummer: "K-9002" },
|
||||||
|
});
|
||||||
|
const host = await testPrisma.host.create({
|
||||||
|
data: { agentId: "test-3", hostname: "pve-test-03", produkt: "PVE", kundeId: kunde.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await testPrisma.kunde.delete({ where: { id: kunde.id } });
|
||||||
|
|
||||||
|
const danach = await testPrisma.host.findUnique({ where: { id: host.id } });
|
||||||
|
expect(danach?.kundeId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verhindert zwei Laufzeiten mit gleichem Start je Host", async () => {
|
||||||
|
const host = await testPrisma.host.create({
|
||||||
|
data: { agentId: "test-4", hostname: "pve-test-04", produkt: "PBS" },
|
||||||
|
});
|
||||||
|
const daten = {
|
||||||
|
hostId: host.id,
|
||||||
|
key: "pbsc-1111111111",
|
||||||
|
start: new Date("2026-01-01"),
|
||||||
|
ende: new Date("2026-12-31"),
|
||||||
|
};
|
||||||
|
|
||||||
|
await testPrisma.laufzeit.create({ data: daten });
|
||||||
|
await expect(testPrisma.laufzeit.create({ data: daten })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { Client } from "pg";
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legt die Testdatenbank an (falls nötig) und spielt alle Migrationen ein.
|
||||||
|
* Ohne `TEST_DATABASE_URL` werden die datenbankgestützten Tests übersprungen.
|
||||||
|
*/
|
||||||
|
export default async function setup() {
|
||||||
|
const url = process.env.TEST_DATABASE_URL;
|
||||||
|
if (!url) {
|
||||||
|
console.warn("TEST_DATABASE_URL ist nicht gesetzt – Datenbanktests werden übersprungen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ziel = new URL(url);
|
||||||
|
const datenbank = ziel.pathname.replace(/^\//, "");
|
||||||
|
const verwaltung = new URL(url);
|
||||||
|
verwaltung.pathname = "/postgres";
|
||||||
|
|
||||||
|
const client = new Client({ connectionString: verwaltung.toString() });
|
||||||
|
await client.connect();
|
||||||
|
const vorhanden = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [datenbank]);
|
||||||
|
if (vorhanden.rowCount === 0) {
|
||||||
|
// Datenbanknamen lassen sich nicht parametrisieren; er stammt aus der eigenen Konfiguration.
|
||||||
|
await client.query(`CREATE DATABASE "${datenbank.replace(/"/g, "")}"`);
|
||||||
|
}
|
||||||
|
await client.end();
|
||||||
|
|
||||||
|
execFileSync("npx", ["prisma", "migrate", "deploy"], {
|
||||||
|
env: { ...process.env, DATABASE_URL: url },
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { PrismaClient } from "@/generated/prisma/client";
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
|
const url = process.env.TEST_DATABASE_URL;
|
||||||
|
|
||||||
|
/** Datenbankgestützte Tests laufen nur, wenn eine Testdatenbank konfiguriert ist. */
|
||||||
|
export const datenbankVerfuegbar = Boolean(url);
|
||||||
|
|
||||||
|
export const testPrisma: PrismaClient = new PrismaClient({
|
||||||
|
adapter: new PrismaPg({ connectionString: url ?? "postgresql://localhost:5432/proxledger_test" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Leert alle Tabellen; Reihenfolge spielt dank CASCADE keine Rolle. */
|
||||||
|
export async function datenbankLeeren() {
|
||||||
|
const tabellen = await testPrisma.$queryRaw<{ tablename: string }[]>`
|
||||||
|
SELECT tablename FROM pg_tables
|
||||||
|
WHERE schemaname = 'public' AND tablename NOT LIKE '_prisma%'
|
||||||
|
`;
|
||||||
|
if (tabellen.length === 0) return;
|
||||||
|
const liste = tabellen.map((t) => `"public"."${t.tablename}"`).join(", ");
|
||||||
|
await testPrisma.$executeRawUnsafe(`TRUNCATE TABLE ${liste} RESTART IDENTITY CASCADE`);
|
||||||
|
}
|
||||||
@@ -8,5 +8,8 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
include: ["tests/**/*.test.ts", "src/**/*.test.ts"],
|
include: ["tests/**/*.test.ts", "src/**/*.test.ts"],
|
||||||
environment: "node",
|
environment: "node",
|
||||||
|
globalSetup: ["tests/global-setup.ts"],
|
||||||
|
// Datenbanktests teilen sich eine Datenbank und dürfen sich nicht überholen.
|
||||||
|
fileParallelism: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user