feat(db): Datenmodell, erste Migration und Seed (#003)
Prisma 7.10 mit Postgres-Adapter, 23 Modelle inklusive Rechnungen mit Positionen, Seed fuer Administrator, Warnregeln und Demo-Daten, Testdatenbank fuer Vitest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user