CI / pruefen (pull_request) Canceled after 0s
Behebt florian.wilde/nebula#19 und florian.wilde/nebula#20.
getJsonSafe machte aus jedem Fehlschlag eine leere Liste. Damit war ein
403 nicht mehr von "das Repository hat keine Branches" zu unterscheiden:
Der Abgleich lief durch, meldete "Snapshot aktualisiert: 0 Branches,
0 Tags", setzte kein syncError - und ueberschrieb dabei den letzten guten
Stand mit Nullen. Ein Repository mit 65 Commits sah danach aus wie ein
leeres, und die Ursache war nicht zu ermitteln, weil die Auskunft, die
sie benannt haette, gerade weggeworfen worden war.
Teilabrufe sammelt jetzt je Feld, was schiefging - mit HTTP-Status, denn
daran haengt die Ursache: 403 heisst Scopes, 429 Rate-Limit, 408
Zeitueberschreitung, 0 nicht erreicht. Der Fallback bleibt, ein einzelner
Fehlschlag darf den ganzen Abgleich nicht abbrechen.
Daraus folgt der zweite Teil: persistSnapshot laesst stehen, was nicht
geholt werden konnte. Der Snapshot ist ein Zwischenspeicher - ein alter
Stand mit sichtbarem Datum ist immer brauchbarer als eine leere Liste,
die aussieht wie eine Aussage. Die abgeleiteten Felder gehen mit ihrer
Liste: lastCommit* haengt an commits, latestTag an tags UND releases,
sonst waeren sie in sich widerspruechlich.
Bewusst kein Fehlschlag: syncError bleibt leer und der Zaehler fuers
Zurueckhalten steigt nicht. Der Rest des Standes ist frisch, und ein
Repository deswegen stundenlang auszusetzen waere die falsche Antwort.
GIT_API_TIMEOUT_MS wird geprueft. Number("") ist 0 und Number("abc") ist
NaN; setTimeout loest bei beidem sofort aus, und dann bricht jeder Aufruf
ab, bevor er beginnt. Eine verrutschte Zeile in der .env legte damit den
ganzen Abgleich still - lautlos, versteht sich.
NACHGEWIESEN, NICHT BEHAUPTET
Gegen einen nachgebauten Gitea-Server, der genau den beobachteten Fall
erzeugt - Basis-Aufruf antwortet, Teillisten geben 403 - und eine
Wegwerf-Datenbank:
gesund branchCount=2 tagCount=3 commits=2 releases=1 missing={}
kaputt branchCount=2 tagCount=3 commits=2 releases=1
missing={8 Felder mit HTTP 403}, completeAt unveraendert
wieder gut missing={} , completeAt fortgeschrieben
Nichts verloren, die Luecke steht in der Meldung und am Repository, und
nach dem naechsten gesunden Durchlauf raeumt sie sich von selbst weg.
axe-core 0 Verstoesse auf 1280 und 375 px, kein seitlicher Ueberlauf.
Der eigentliche Grund der Ausfaelle auf der Instanz ist damit noch nicht
gefunden - aber ab jetzt steht er auf der Projektseite.
Refs #19
Refs #20
668 lines
23 KiB
Plaintext
668 lines
23 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?
|
||
|
||
// ── Aufgabenbrett ──────────────────────────────────────────
|
||
/// Hoechstzahl Karten je Spalte. Was darueber hinausgeht, kommt hinter
|
||
/// einen Knopf – verborgen wird nichts. 0 schaltet die Begrenzung ab.
|
||
taskColumnLimit Int @default(15)
|
||
|
||
// ── 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?
|
||
|
||
/// Gemeinsames Geheimnis fuer eingehende Webhooks, verschluesselt wie die
|
||
/// Zugangstoken. Je Repository, weil der Haken im Git-System dort
|
||
/// eingetragen wird und sich einzeln zuruecknehmen laesst.
|
||
/// NULL heisst: fuer dieses Repository ist kein Webhook eingerichtet, und
|
||
/// eingehende Aufrufe werden abgewiesen.
|
||
webhookSecret String?
|
||
/// Wann zuletzt ein gueltiger Webhook ankam – die einzige Rueckmeldung
|
||
/// darueber, ob der Haken im Git-System wirklich funktioniert.
|
||
webhookLastAt DateTime?
|
||
|
||
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?
|
||
|
||
/// Teil-Abrufe, die beim letzten Abgleich nicht geklappt haben:
|
||
/// { "branches": "HTTP 403: Zugriff verweigert …" }. Leeres Objekt heisst,
|
||
/// dass alle Listen wirklich geholt wurden.
|
||
///
|
||
/// Ohne diese Angabe ist eine leere Liste zweideutig – "nicht geholt" sieht
|
||
/// aus wie "nichts vorhanden". Genau daran ist eine Fehlersuche einmal
|
||
/// haengen geblieben.
|
||
missing Json @default("{}")
|
||
|
||
pushedAt DateTime?
|
||
fetchedAt DateTime @default(now())
|
||
/// Wann zuletzt ein Abgleich lief, bei dem alle Teile ankamen. Steht der
|
||
/// weiter zurueck als fetchedAt, ist ein Teil der Anzeige aelter als sie
|
||
/// vorgibt.
|
||
completeAt DateTime?
|
||
}
|