Files
nebula/prisma/schema.prisma
T
florian.wildeandClaude Opus 5 ed2637877e feat(sync): automatischer Abgleich nach Zeitplan
Bisher musste jedes Projekt einzeln von Hand synchronisiert werden. Jetzt
gibt es einen Zeitgeber im Serverprozess, angestossen von
instrumentation.ts. Fuer eine Einzelinstanz ist das die einfachste
tragfaehige Loesung: kein zusaetzlicher Container, kein externer Cron, keine
zweite Konfiguration.

Ruecksicht auf fremde API-Kontingente war der bestimmende Gedanke:

- standardmaessig abgeschaltet - der Abgleich verbraucht fremdes Kontingent,
  das soll eine bewusste Entscheidung sein
- je Durchlauf nur eine begrenzte Zahl Projekte, die am laengsten nicht
  abgeglichenen zuerst; der Rest kommt beim naechsten Mal
- nach jedem Fehlschlag verdoppelt sich der Abstand bis zum naechsten
  Versuch, gedeckelt beim Sechzehnfachen. Ein Repo mit abgelaufenem Token
  blockiert damit nicht dauerhaft das Kontingent.
- kurze Pause zwischen zwei Projekten
- zwei Durchlaeufe ueberschneiden sich nie
- fertige und archivierte Projekte lassen sich auslassen
- einzelne Projekte lassen sich ausnehmen

Der Zaehler fuer Fehlschlaege wandert in syncProject: bei Erfolg zurueck auf
null, bei Fehlschlag hoch. So stimmt er auch nach einem manuellen Abgleich.

sync.ts wird zu sync/index.ts, damit Modul und Ordner nicht nebeneinander
stehen. Alle Importpfade bleiben unveraendert.

Zur Form der Abfrage in instrumentation.ts: Next uebersetzt die Datei auch
fuer den Edge-Runtime. Der Import muss deshalb *in* der positiven Bedingung
stehen, sonst bleibt er im Edge-Bundle stehen und der Build scheitert an
node:crypto. Mit einem frueh abbrechenden return passiert genau das.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:37:54 +02:00

310 lines
9.9 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─────────────────────────────────────────────────────────────
// 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("{}")
pushedAt DateTime?
fetchedAt DateTime @default(now())
}