Ein TOTP-Code blieb rund 90 Sekunden gueltig und liess sich darin beliebig oft verwenden. RFC 6238 verlangt in Abschnitt 5.2 ausdruecklich das Gegenteil: derselbe Code darf kein zweites Mal angenommen werden. Ohne diese Sperre kann ein abgefangener Code - ueber die Schulter gesehen, von einer weiterleitenden Phishing-Seite, von Schadsoftware vom Bildschirm gelesen - die restliche Gueltigkeitsdauer ueber wiederverwendet werden. verifyTotp gibt dafuer jetzt ueber matchTotp das Zeitfenster zurueck, das gepasst hat; angenommen wird nur, was neuer ist als das zuletzt eingeloeste. Der Vergleich bleibt dabei in konstanter Zeit - es wird weiterhin nicht fruehzeitig abgebrochen. Auch die Aktivierung verbrennt ihr Fenster: der Code, der eben scharfgeschaltet hat, soll nicht Sekunden spaeter noch zum Anmelden taugen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
640 lines
21 KiB
Plaintext
640 lines
21 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
|
||
}
|
||
|
||
/// Zustellwege fuer Benachrichtigungen.
|
||
enum NotificationChannel {
|
||
WEBHOOK
|
||
NTFY
|
||
EMAIL
|
||
}
|
||
|
||
/// Anlaesse, ueber die benachrichtigt werden kann.
|
||
enum NotificationEvent {
|
||
/// Ein Abgleich mit dem Git-System ist fehlgeschlagen.
|
||
SYNC_FAILED
|
||
/// Der letzte CI-Lauf auf dem Default-Branch ist rot.
|
||
CI_FAILED
|
||
/// Im Repository ist ein neuer Tag oder ein neues Release aufgetaucht.
|
||
NEW_RELEASE
|
||
/// Seit laengerem kein Commit mehr – der sanfte Stups.
|
||
PROJECT_STALE
|
||
}
|
||
|
||
/// Was im Verlauf eines Projekts festgehalten wird. Bewusst grob: der
|
||
/// Verlauf soll erzaehlen, was passiert ist, und nicht jede Feldaenderung
|
||
/// mitschreiben.
|
||
enum ActivityKind {
|
||
PROJECT_CREATED
|
||
PROJECT_UPDATED
|
||
STATUS_CHANGED
|
||
NOTE_ADDED
|
||
NOTE_UPDATED
|
||
NOTE_DELETED
|
||
TASK_ADDED
|
||
TASK_MOVED
|
||
TASK_DELETED
|
||
TASKS_IMPORTED
|
||
REPO_LINKED
|
||
REPO_COMMITS
|
||
SYNC_FAILED
|
||
}
|
||
|
||
/// Wie oft sich eine Aufgabe wiederholt. Bewusst grob: feinere Regeln
|
||
/// (jeden zweiten Dienstag im Monat) braucht ein Projektdashboard nicht,
|
||
/// und sie waeren ohne Kalenderansicht kaum nachvollziehbar.
|
||
enum Recurrence {
|
||
DAILY
|
||
WEEKLY
|
||
BIWEEKLY
|
||
MONTHLY
|
||
}
|
||
|
||
/// Spalten des Aufgabenbretts innerhalb eines Projekts.
|
||
enum TaskStatus {
|
||
TODO
|
||
DOING
|
||
BLOCKED
|
||
DONE
|
||
}
|
||
|
||
/// 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?
|
||
|
||
/// TOTP-Schluessel, AES-256-GCM-verschluesselt (siehe src/lib/crypto.ts).
|
||
/// Hinterlegt heisst noch nicht scharf: erst ein bestaetigter Code setzt
|
||
/// totpEnabledAt. Bis dahin darf die Anmeldung nicht danach fragen, sonst
|
||
/// sperrt sich aus, wer die Einrichtung abbricht.
|
||
totpSecretCipher String?
|
||
totpEnabledAt DateTime?
|
||
/// Zuletzt eingeloestes Zeitfenster. RFC 6238 verlangt, denselben Code
|
||
/// kein zweites Mal anzunehmen – ohne diesen Merker liesse sich ein
|
||
/// abgefangener Code die restliche Gueltigkeit ueber wiederverwenden.
|
||
totpLastCounter Int?
|
||
|
||
sessions Session[]
|
||
credentials Credential[]
|
||
projects Project[]
|
||
connections GitConnection[]
|
||
notificationTargets NotificationTarget[]
|
||
activities Activity[]
|
||
recoveryCodes RecoveryCode[]
|
||
pendingLogins PendingLogin[]
|
||
templates ProjectTemplate[]
|
||
|
||
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])
|
||
}
|
||
|
||
/// Ein Wiederherstellungscode fuer den Fall, dass der Authenticator weg ist.
|
||
/// Jeder Code gilt genau einmal.
|
||
model RecoveryCode {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
/// SHA-256 des Codes. Kein langsames Verfahren wie bei Passwoertern: die
|
||
/// Codes sind zufaellig erzeugt und lang genug, dass Raten aussichtslos
|
||
/// ist – und bei einer Anmeldung waeren sonst zehn teure Vergleiche noetig.
|
||
codeHash String @unique
|
||
|
||
usedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId])
|
||
}
|
||
|
||
/// Zwischenzustand einer Anmeldung: Passwort stimmt, der zweite Faktor fehlt
|
||
/// noch. Bewusst eine eigene Zeile und keine halbfertige Sitzung – so kann
|
||
/// aus diesem Zustand nichts entstehen, was einer Anmeldung aehnelt.
|
||
model PendingLogin {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
/// Wie bei der Sitzung liegt nur der SHA-256-Hash des Cookie-Tokens hier.
|
||
tokenHash String @unique
|
||
/// Fehlversuche. Nach zu vielen wird der Vorgang verworfen, damit die
|
||
/// sechs Stellen nicht in Ruhe durchprobiert werden koennen.
|
||
attempts Int @default(0)
|
||
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId])
|
||
@@index([expiresAt])
|
||
}
|
||
|
||
/// 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?
|
||
|
||
// ── Stillstand ─────────────────────────────────────────────
|
||
/// Ab wie vielen Tagen ohne Commit ein Projekt als stillstehend gilt.
|
||
staleAfterDays Int @default(60)
|
||
/// Die Pruefung laeuft hoechstens einmal am Tag.
|
||
lastStaleCheckAt DateTime?
|
||
|
||
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)
|
||
|
||
repositories Repository[]
|
||
|
||
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)
|
||
/// Rang innerhalb der Statusspalte im Brett. Nur dort wirksam – die
|
||
/// uebrigen Ansichten folgen der gewaehlten Sortierung.
|
||
position 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)
|
||
|
||
/// Fortschritt aus dem Verhaeltnis erledigter Aufgaben berechnen statt
|
||
/// von Hand zu schieben. Nur sinnvoll, wenn es ueberhaupt Aufgaben gibt.
|
||
progressFromTasks Boolean @default(false)
|
||
|
||
/// Wann zuletzt wegen Stillstands gemeldet wurde. Erneut gemeldet wird
|
||
/// erst, wenn danach wieder ein Commit kam und es dann wieder still wurde.
|
||
/// Bleibt am Projekt: Stillstand ist eine Aussage ueber das Vorhaben, nicht
|
||
/// ueber ein einzelnes Repository.
|
||
notifiedStaleAt DateTime?
|
||
|
||
notes Note[]
|
||
tasks Task[]
|
||
activities Activity[]
|
||
repositories Repository[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([ownerId, slug])
|
||
@@index([status])
|
||
@@index([updatedAt])
|
||
@@index([ownerId])
|
||
@@index([ownerId, status, position])
|
||
}
|
||
|
||
/// Ein Repository eines Projekts. Ein Vorhaben besteht oft aus mehreren –
|
||
/// Frontend und Backend etwa –, gehoert aber in eine Karte.
|
||
///
|
||
/// Das erste Repository (position 0) ist das fuehrende: seine Kennzahlen
|
||
/// stehen auf der Projektkarte, wo fuer mehrere kein Platz ist.
|
||
model Repository {
|
||
id String @id @default(cuid())
|
||
projectId String
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
||
/// Frei waehlbare Bezeichnung wie "Frontend". Leer heisst: der Repo-Name
|
||
/// steht fuer sich.
|
||
label String?
|
||
|
||
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)
|
||
|
||
/// Reihenfolge in der Anzeige.
|
||
position Int @default(0)
|
||
|
||
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)
|
||
|
||
/// Woruber zuletzt benachrichtigt wurde. Gemeldet wird nur bei einer
|
||
/// Aenderung – sonst kaeme bei einem dauerhaft kaputten Repo mit jedem
|
||
/// Abgleich dieselbe Nachricht. Je Repository, nicht je Projekt: sonst
|
||
/// verdeckt ein rotes Frontend die rote Pipeline des Backends.
|
||
notifiedSyncError String?
|
||
notifiedCiState String?
|
||
notifiedRelease String?
|
||
|
||
snapshot RepoSnapshot?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([projectId, position])
|
||
@@index([connectionId])
|
||
}
|
||
|
||
/// Ein Zustellweg fuer Benachrichtigungen, gehoert genau einem Konto.
|
||
model NotificationTarget {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
name String
|
||
channel NotificationChannel
|
||
|
||
/// Webhook-URL, ntfy-Themen-URL oder E-Mail-Adresse.
|
||
target String
|
||
/// Optionales Geheimnis, AES-256-GCM verschluesselt: Bearer-Token fuer
|
||
/// ntfy oder ein Signaturschluessel fuer den Webhook.
|
||
secretCipher String?
|
||
secretHint String?
|
||
|
||
/// Worueber dieses Ziel benachrichtigt werden will.
|
||
events NotificationEvent[] @default([])
|
||
isActive Boolean @default(true)
|
||
|
||
lastSentAt DateTime?
|
||
lastOk Boolean?
|
||
lastMessage String?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([userId, name])
|
||
@@index([userId])
|
||
}
|
||
|
||
/// Eine Aufgabe innerhalb eines Projekts – die Karten des Aufgabenbretts.
|
||
model Task {
|
||
id String @id @default(cuid())
|
||
projectId String
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
title String
|
||
/// Optionale Notiz zur Aufgabe, Markdown wird dargestellt.
|
||
description String?
|
||
status TaskStatus @default(TODO)
|
||
/// Rang innerhalb der Spalte. Anders als beim Projektbrett braucht es ihn
|
||
/// hier wirklich: Aufgaben haben eine Reihenfolge, die der Benutzer setzt.
|
||
/// Beim Umsortieren werden die Positionen einer Spalte neu durchnummeriert.
|
||
position Int @default(0)
|
||
dueDate DateTime?
|
||
labels String[] @default([])
|
||
/// Zeitpunkt, zu dem die Aufgabe nach DONE gewandert ist.
|
||
doneAt DateTime?
|
||
|
||
/// Wiederholt sich die Aufgabe? Wandert sie nach ERLEDIGT, entsteht die
|
||
/// naechste Fassung mit fortgeschriebenem Faelligkeitsdatum. Die erledigte
|
||
/// bleibt stehen – sonst ginge die Historie verloren.
|
||
recurrence Recurrence?
|
||
/// Aus welcher Aufgabe diese hervorgegangen ist. Nur zur Nachvollziehbarkeit;
|
||
/// bewusst ohne Fremdschluessel, damit das Loeschen der Vorgaengerin die
|
||
/// Nachfolgerin nicht mitnimmt.
|
||
recurredFrom String?
|
||
|
||
/// Stammt die Aufgabe aus einem Git-Issue? Nummer, Repository und Link
|
||
/// merken, damit dasselbe Issue nicht zweimal uebernommen wird.
|
||
sourceIssue Int?
|
||
sourceUrl String?
|
||
/// Aus welchem Repository. Muss mit in die Eindeutigkeit: Frontend und
|
||
/// Backend haben beide ein Issue Nummer 5, und das sind zwei Aufgaben.
|
||
sourceRepositoryId String?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
/// Verhindert Dubletten beim erneuten Uebernehmen. Mehrere Aufgaben ohne
|
||
/// Herkunft sind erlaubt: NULL gilt in Postgres als verschieden.
|
||
@@unique([projectId, sourceRepositoryId, sourceIssue])
|
||
@@index([projectId, status, position])
|
||
}
|
||
|
||
/// Eine Vorlage fuer neue Projekte: Stammdaten, Tags, Notizen und Aufgaben.
|
||
///
|
||
/// Bewusst benutzereigen und nicht fest eingebaut. Welche Tags und welche
|
||
/// Startaufgaben sinnvoll sind, weiss nur, wer damit arbeitet – eine
|
||
/// mitgelieferte Liste waere geraten.
|
||
model ProjectTemplate {
|
||
id String @id @default(cuid())
|
||
ownerId String
|
||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||
|
||
name String
|
||
/// Wozu die Vorlage gut ist – erscheint in der Auswahl.
|
||
description String?
|
||
|
||
// ── Was die Vorlage in das neue Projekt schreibt ───────────
|
||
summary String?
|
||
projectDescription String?
|
||
status ProjectStatus @default(IDEA)
|
||
priority Int @default(2)
|
||
accent String @default("violet")
|
||
tags String[] @default([])
|
||
progressFromTasks Boolean @default(false)
|
||
|
||
/// [{ title, content, pinned }]
|
||
notes Json @default("[]")
|
||
/// [{ title, description, status, labels }]
|
||
tasks Json @default("[]")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([ownerId, name])
|
||
@@index([ownerId])
|
||
}
|
||
|
||
/// Ein Eintrag im Verlauf eines Projekts: was ist wann passiert.
|
||
model Activity {
|
||
id String @id @default(cuid())
|
||
projectId String
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
||
/// Wer. null bedeutet: die Anwendung selbst, also der Zeitgeber beim
|
||
/// automatischen Abgleich.
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||
|
||
kind ActivityKind
|
||
/// Fertig formulierter Einzeiler. Die Formulierung beim Schreiben
|
||
/// festzuhalten spart der Anzeige das Nachschlagen von Namen, die es
|
||
/// spaeter womoeglich nicht mehr gibt – etwa den einer geloeschten Notiz.
|
||
summary String
|
||
/// Zusatzangaben wie { from: "IDEA", to: "DONE" }. Fuer die Anzeige nicht
|
||
/// noetig, aber nuetzlich, falls spaeter ausgewertet werden soll.
|
||
meta Json?
|
||
|
||
createdAt DateTime @default(now())
|
||
|
||
/// Absteigend gelesen, immer je Projekt.
|
||
@@index([projectId, createdAt])
|
||
}
|
||
|
||
/// 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 eines Repositories. Wird beim Sync
|
||
/// ueberschrieben, damit das Dashboard auch ohne Netz Daten zeigt.
|
||
model RepoSnapshot {
|
||
id String @id @default(cuid())
|
||
repositoryId String @unique
|
||
repository Repository @relation(fields: [repositoryId], 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("[]")
|
||
|
||
/// Commits je Tag ueber bis zu ein Jahr – die Heatmap.
|
||
/// { from, to, days: { "2026-07-26": 3 }, source, truncated }
|
||
/// Getrennt vom uebrigen Stand, weil das Holen zusaetzliche Aufrufe kostet
|
||
/// und deshalb hoechstens einmal am Tag geschieht.
|
||
commitCalendar Json @default("{}")
|
||
commitCalendarAt DateTime?
|
||
|
||
/// Das README des Repositories, roh als Markdown. Wie der Kalender
|
||
/// hoechstens einmal am Tag geholt – es aendert sich selten, und der
|
||
/// Abgleich laeuft im Zweifel stuendlich.
|
||
readme String?
|
||
/// Dateiname, wie er im Repository steht ("README.md", "readme.rst" …).
|
||
readmePath String?
|
||
/// Wogegen relative Links im README aufzuloesen sind.
|
||
readmeBaseUrl String?
|
||
readmeAt DateTime?
|
||
|
||
// ── 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())
|
||
}
|