Bisher standen nur Zahlen da. Jetzt kommen die Inhalte mit. Pull Requests und Issues: bis zu 50 offene je Liste, mit Nummer, Titel, Autor, Alter, Labels und Link. Bei GitHub und Gitea liefert der Issue-Endpunkt auch Pull Requests mit - die werden herausgefiltert. CI-Status des letzten Laufs auf dem Default-Branch: - GitHub ueber actions/runs; dort sind Lauf-Status und Ergebnis getrennt und werden auf einen Wert zusammengefuehrt - GitLab ueber pipelines - Gitea ueber den kombinierten Commit-Status. Das deckt Gitea Actions und externe Systeme wie Woodpecker gleichermassen ab - ein reiner Actions-Endpunkt existiert auf aelteren Instanzen nicht. Commit-Aktivitaet als Balkenreihe. Berechnet aus den ohnehin geholten Commits (jetzt 100 statt 15, angezeigt bleiben 15) statt ueber eigene Statistik-Endpunkte: die gibt es nicht bei allen drei Systemen, und bei GitHub antworten sie beim ersten Aufruf mit 202 und ohne Daten. Beim Testen gegen gitlab-runner fiel auf, dass alle 100 Commits in die letzten 30 Tage fielen - die frueheren Tage haetten faelschlich Null gezeigt, obwohl wir ueber sie nichts wissen. Die Kurve wird deshalb auf den tatsaechlich belegten Zeitraum gekuerzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
322 lines
10 KiB
Plaintext
322 lines
10 KiB
Plaintext
// ─────────────────────────────────────────────────────────────
|
||
// Nebula – Datenmodell
|
||
// Datenbank: PostgreSQL (lokal oder via docker compose)
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
// linux-musl-openssl-3.0.x wird fuer das Alpine-Docker-Image benoetigt.
|
||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
/// Lebenszyklus eines Projekts. IDEA ist der Startzustand fuer
|
||
/// frisch notierte Einfaelle, ARCHIVED blendet Projekte aus dem
|
||
/// Standard-Board aus, ohne sie zu loeschen.
|
||
enum ProjectStatus {
|
||
IDEA
|
||
OPEN
|
||
PLANNING
|
||
IN_DEVELOPMENT
|
||
DONE
|
||
ARCHIVED
|
||
}
|
||
|
||
/// Unterstuetzte Git-Systeme.
|
||
enum GitProvider {
|
||
GITHUB
|
||
GITLAB
|
||
GITEA
|
||
}
|
||
|
||
/// ADMIN darf Benutzer und Einstellungen verwalten. Auf fremde Projekte hat
|
||
/// auch ein Admin keinen Zugriff – die Trennung ist bewusst hart.
|
||
enum UserRole {
|
||
ADMIN
|
||
USER
|
||
}
|
||
|
||
/// SINGLE_USER: genau ein Konto, keine Registrierung, keine Benutzerliste.
|
||
/// MULTI_USER: mehrere Konten, jedes mit streng getrennten Daten.
|
||
enum AppMode {
|
||
SINGLE_USER
|
||
MULTI_USER
|
||
}
|
||
|
||
/// Ein Benutzerkonto. Das Passwort ist optional, weil ein Konto auch
|
||
/// ausschliesslich per Passkey nutzbar sein darf.
|
||
model User {
|
||
id String @id @default(cuid())
|
||
/// Immer kleingeschrieben gespeichert, damit die Anmeldung
|
||
/// gross-/kleinschreibungsunabhaengig funktioniert.
|
||
username String @unique
|
||
displayName String?
|
||
email String? @unique
|
||
/// scrypt-Hash im Format scrypt$N$r$p$<salt>$<hash>; null = nur Passkey.
|
||
passwordHash String?
|
||
role UserRole @default(USER)
|
||
isActive Boolean @default(true)
|
||
|
||
lastLoginAt DateTime?
|
||
/// Zaehler und Sperre gegen Passwortraten.
|
||
failedLoginCount Int @default(0)
|
||
lockedUntil DateTime?
|
||
|
||
sessions Session[]
|
||
credentials Credential[]
|
||
projects Project[]
|
||
connections GitConnection[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([role])
|
||
}
|
||
|
||
/// Serverseitige Sitzung. In der Datenbank liegt nur der SHA-256-Hash des
|
||
/// Cookie-Tokens, damit ein Datenbankleck keine uebernehmbaren Sitzungen ergibt.
|
||
model Session {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
tokenHash String @unique
|
||
expiresAt DateTime
|
||
lastSeenAt DateTime @default(now())
|
||
userAgent String?
|
||
ip String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId])
|
||
@@index([expiresAt])
|
||
}
|
||
|
||
/// Ein registrierter Passkey (WebAuthn-Credential).
|
||
model Credential {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
/// base64url-kodierte Credential-ID des Authenticators.
|
||
credentialId String @unique
|
||
publicKey Bytes
|
||
counter Int @default(0)
|
||
transports String[] @default([])
|
||
deviceType String?
|
||
backedUp Boolean @default(false)
|
||
/// Frei waehlbarer Name, z. B. "MacBook Touch ID".
|
||
name String?
|
||
lastUsedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId])
|
||
}
|
||
|
||
/// Kurzlebige WebAuthn-Challenge. Wird nach Gebrauch geloescht, damit sie
|
||
/// kein zweites Mal verwendet werden kann.
|
||
model WebAuthnChallenge {
|
||
id String @id @default(cuid())
|
||
challenge String @unique
|
||
/// null bei der Anmeldung ueber auffindbare Passkeys.
|
||
userId String?
|
||
/// "registration" oder "authentication"
|
||
purpose String
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([expiresAt])
|
||
}
|
||
|
||
/// Einstellungen der Instanz. Genau eine Zeile mit der id "singleton".
|
||
model AppSettings {
|
||
id String @id @default("singleton")
|
||
mode AppMode @default(SINGLE_USER)
|
||
/// Nur im Mehrbenutzerbetrieb wirksam.
|
||
allowRegistration Boolean @default(false)
|
||
/// Anzeigename gegenueber dem Authenticator beim Anlegen eines Passkeys.
|
||
rpName String @default("Nebula")
|
||
|
||
// ── Automatischer Abgleich mit den Git-Systemen ────────────
|
||
/// Standardmaessig aus: der Abgleich verbraucht fremde API-Kontingente,
|
||
/// das soll eine bewusste Entscheidung sein.
|
||
autoSyncEnabled Boolean @default(false)
|
||
/// Abstand zwischen zwei Durchlaeufen in Minuten.
|
||
autoSyncIntervalMinutes Int @default(60)
|
||
/// Fertige und archivierte Projekte auslassen – dort aendert sich selten
|
||
/// noch etwas.
|
||
autoSyncActiveOnly Boolean @default(true)
|
||
/// Hoechstzahl Projekte je Durchlauf, damit ein grosses Board nicht auf
|
||
/// einen Schlag in ein Rate-Limit laeuft.
|
||
autoSyncBatchSize Int @default(25)
|
||
|
||
lastAutoSyncAt DateTime?
|
||
/// Kurzbericht des letzten Durchlaufs: { synced, failed, skipped, ms }
|
||
lastAutoSyncReport Json?
|
||
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
/// Eine Verbindung zu einem Git-System inkl. verschluesseltem Token.
|
||
model GitConnection {
|
||
id String @id @default(cuid())
|
||
name String
|
||
provider GitProvider
|
||
/// Basis-URL der Instanz, z. B. https://github.com, https://gitlab.com,
|
||
/// https://git.meine-domain.de
|
||
baseUrl String
|
||
/// Optionaler Benutzername – rein informativ fuer die Anzeige.
|
||
username String?
|
||
|
||
/// Personal Access Token, AES-256-GCM-verschluesselt (siehe src/lib/crypto.ts).
|
||
tokenCipher String?
|
||
/// Gekuerzte Darstellung des Tokens fuer die UI, z. B. "ghp_…4f2a".
|
||
tokenHint String?
|
||
|
||
lastCheckedAt DateTime?
|
||
lastCheckOk Boolean?
|
||
lastCheckMessage String?
|
||
|
||
/// Besitzer. Nullable, damit Bestandsdaten aus der Zeit vor der
|
||
/// Benutzerverwaltung erhalten bleiben; beim Einrichten des ersten
|
||
/// Kontos werden sie diesem zugeordnet.
|
||
ownerId String?
|
||
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||
|
||
projects Project[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([ownerId, name])
|
||
@@index([provider])
|
||
@@index([ownerId])
|
||
}
|
||
|
||
/// Ein Vibe-Coding-Projekt – von der reinen Idee bis zum fertigen Repo.
|
||
model Project {
|
||
id String @id @default(cuid())
|
||
name String
|
||
/// Pro Besitzer eindeutig – siehe @@unique unten.
|
||
slug String
|
||
/// Einzeiler fuer die Karte im Dashboard.
|
||
summary String?
|
||
/// Ausfuehrliche Beschreibung / Ideenskizze (Markdown-freundlich).
|
||
description String?
|
||
status ProjectStatus @default(IDEA)
|
||
/// 1 = niedrig, 2 = normal, 3 = hoch, 4 = kritisch
|
||
priority Int @default(2)
|
||
/// Fortschritt in Prozent (0–100)
|
||
progress Int @default(0)
|
||
/// Akzentfarbe der Karte: violet | cyan | emerald | amber | rose | blue
|
||
accent String @default("violet")
|
||
tags String[] @default([])
|
||
favorite Boolean @default(false)
|
||
|
||
// ── Git-Anbindung ──────────────────────────────────────────
|
||
repoUrl String?
|
||
/// Owner bzw. Namespace/Gruppe (bei GitLab auch verschachtelt: "gruppe/subgruppe")
|
||
repoOwner String?
|
||
repoName String?
|
||
defaultBranch String?
|
||
connectionId String?
|
||
connection GitConnection? @relation(fields: [connectionId], references: [id], onDelete: SetNull)
|
||
|
||
lastSyncedAt DateTime?
|
||
syncError String?
|
||
/// Am automatischen Abgleich teilnehmen. Einzeln abschaltbar, etwa bei
|
||
/// einem Repo, dessen Token staendig Fehler wirft.
|
||
autoSync Boolean @default(true)
|
||
/// Aufeinanderfolgende Fehlversuche. Steuert das Zurueckhalten: nach
|
||
/// jedem Fehlschlag wird der naechste Versuch weiter hinausgezoegert.
|
||
syncFailureCount Int @default(0)
|
||
|
||
/// Besitzer. Nullable wie bei GitConnection, damit Bestandsdaten beim
|
||
/// Einrichten des ersten Kontos uebernommen werden koennen.
|
||
ownerId String?
|
||
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||
|
||
notes Note[]
|
||
snapshot RepoSnapshot?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([ownerId, slug])
|
||
@@index([status])
|
||
@@index([connectionId])
|
||
@@index([updatedAt])
|
||
@@index([ownerId])
|
||
}
|
||
|
||
/// Freie Notizen zu einem Projekt.
|
||
model Note {
|
||
id String @id @default(cuid())
|
||
projectId String
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
title String?
|
||
content String
|
||
pinned Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([projectId])
|
||
@@index([pinned])
|
||
}
|
||
|
||
/// Zwischengespeicherter Stand des verknuepften Repositories. Wird beim
|
||
/// Sync ueberschrieben, damit das Dashboard auch ohne Netz Daten zeigt.
|
||
model RepoSnapshot {
|
||
id String @id @default(cuid())
|
||
projectId String @unique
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
||
fullName String?
|
||
description String?
|
||
defaultBranch String?
|
||
htmlUrl String?
|
||
visibility String?
|
||
|
||
stars Int @default(0)
|
||
forks Int @default(0)
|
||
openIssues Int @default(0)
|
||
openPullRequests Int @default(0)
|
||
sizeKb Int @default(0)
|
||
|
||
lastCommitSha String?
|
||
lastCommitMessage String?
|
||
lastCommitAuthor String?
|
||
lastCommitAt DateTime?
|
||
|
||
latestTag String?
|
||
latestRelease String?
|
||
latestReleaseAt DateTime?
|
||
|
||
branchCount Int @default(0)
|
||
tagCount Int @default(0)
|
||
|
||
/// Rohdaten der Listen – bewusst als JSON, damit providerspezifische
|
||
/// Zusatzfelder nicht verloren gehen.
|
||
branches Json @default("[]")
|
||
tags Json @default("[]")
|
||
commits Json @default("[]")
|
||
releases Json @default("[]")
|
||
languages Json @default("{}")
|
||
pullRequests Json @default("[]")
|
||
issues Json @default("[]")
|
||
/// Commits je Tag der letzten 30 Tage, aeltester zuerst.
|
||
commitActivity Json @default("[]")
|
||
|
||
// ── Letzter CI-Lauf auf dem Default-Branch ─────────────────
|
||
/// success | failure | running | pending | cancelled
|
||
ciState String?
|
||
ciName String?
|
||
ciBranch String?
|
||
ciUrl String?
|
||
ciFinishedAt DateTime?
|
||
|
||
pushedAt DateTime?
|
||
fetchedAt DateTime @default(now())
|
||
}
|