diff --git a/prisma/migrations/20260725090000_init/migration.sql b/prisma/migrations/20260725090000_init/migration.sql new file mode 100644 index 0000000..8c92edb --- /dev/null +++ b/prisma/migrations/20260725090000_init/migration.sql @@ -0,0 +1,132 @@ +-- CreateEnum +CREATE TYPE "ProjectStatus" AS ENUM ('IDEA', 'OPEN', 'PLANNING', 'IN_DEVELOPMENT', 'DONE', 'ARCHIVED'); + +-- CreateEnum +CREATE TYPE "GitProvider" AS ENUM ('GITHUB', 'GITLAB', 'GITEA'); + +-- CreateTable +CREATE TABLE "GitConnection" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "provider" "GitProvider" NOT NULL, + "baseUrl" TEXT NOT NULL, + "username" TEXT, + "tokenCipher" TEXT, + "tokenHint" TEXT, + "lastCheckedAt" TIMESTAMP(3), + "lastCheckOk" BOOLEAN, + "lastCheckMessage" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GitConnection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "summary" TEXT, + "description" TEXT, + "status" "ProjectStatus" NOT NULL DEFAULT 'IDEA', + "priority" INTEGER NOT NULL DEFAULT 2, + "progress" INTEGER NOT NULL DEFAULT 0, + "accent" TEXT NOT NULL DEFAULT 'violet', + "tags" TEXT[] DEFAULT ARRAY[]::TEXT[], + "favorite" BOOLEAN NOT NULL DEFAULT false, + "repoUrl" TEXT, + "repoOwner" TEXT, + "repoName" TEXT, + "defaultBranch" TEXT, + "connectionId" TEXT, + "lastSyncedAt" TIMESTAMP(3), + "syncError" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Note" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "title" TEXT, + "content" TEXT NOT NULL, + "pinned" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Note_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RepoSnapshot" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "fullName" TEXT, + "description" TEXT, + "defaultBranch" TEXT, + "htmlUrl" TEXT, + "visibility" TEXT, + "stars" INTEGER NOT NULL DEFAULT 0, + "forks" INTEGER NOT NULL DEFAULT 0, + "openIssues" INTEGER NOT NULL DEFAULT 0, + "openPullRequests" INTEGER NOT NULL DEFAULT 0, + "sizeKb" INTEGER NOT NULL DEFAULT 0, + "lastCommitSha" TEXT, + "lastCommitMessage" TEXT, + "lastCommitAuthor" TEXT, + "lastCommitAt" TIMESTAMP(3), + "latestTag" TEXT, + "latestRelease" TEXT, + "latestReleaseAt" TIMESTAMP(3), + "branchCount" INTEGER NOT NULL DEFAULT 0, + "tagCount" INTEGER NOT NULL DEFAULT 0, + "branches" JSONB NOT NULL DEFAULT '[]', + "tags" JSONB NOT NULL DEFAULT '[]', + "commits" JSONB NOT NULL DEFAULT '[]', + "releases" JSONB NOT NULL DEFAULT '[]', + "languages" JSONB NOT NULL DEFAULT '{}', + "pushedAt" TIMESTAMP(3), + "fetchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RepoSnapshot_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GitConnection_name_key" ON "GitConnection"("name"); + +-- CreateIndex +CREATE INDEX "GitConnection_provider_idx" ON "GitConnection"("provider"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); + +-- CreateIndex +CREATE INDEX "Project_status_idx" ON "Project"("status"); + +-- CreateIndex +CREATE INDEX "Project_connectionId_idx" ON "Project"("connectionId"); + +-- CreateIndex +CREATE INDEX "Project_updatedAt_idx" ON "Project"("updatedAt"); + +-- CreateIndex +CREATE INDEX "Note_projectId_idx" ON "Note"("projectId"); + +-- CreateIndex +CREATE INDEX "Note_pinned_idx" ON "Note"("pinned"); + +-- CreateIndex +CREATE UNIQUE INDEX "RepoSnapshot_projectId_key" ON "RepoSnapshot"("projectId"); + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "GitConnection"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RepoSnapshot" ADD CONSTRAINT "RepoSnapshot_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..0e2c50d --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,163 @@ +// ───────────────────────────────────────────────────────────── +// 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 +} + +/// 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? + + projects Project[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([name]) + @@index([provider]) +} + +/// Ein Vibe-Coding-Projekt – von der reinen Idee bis zum fertigen Repo. +model Project { + id String @id @default(cuid()) + name String + slug String @unique + /// 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? + + notes Note[] + snapshot RepoSnapshot? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status]) + @@index([connectionId]) + @@index([updatedAt]) +} + +/// 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()) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..0ec60bd --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,118 @@ +import { PrismaClient, ProjectStatus } from "@prisma/client"; + +const prisma = new PrismaClient(); + +interface SampleNote { + title?: string | null; + content: string; + pinned?: boolean; +} + +interface SampleProject { + name: string; + slug: string; + summary: string; + description: string; + status: ProjectStatus; + priority: number; + progress: number; + accent: string; + tags: string[]; + favorite: boolean; + notes: SampleNote[]; +} + +/** + * Legt ein paar Beispielprojekte an, damit das Dashboard nach dem ersten + * Start nicht leer ist. Idempotent: bereits vorhandene Slugs werden + * uebersprungen. + */ +async function main() { + const samples: SampleProject[] = [ + { + name: "Nebula", + slug: "nebula", + summary: "Dieses Dashboard – Uebersicht ueber alle Vibe-Coding Projekte.", + description: + "Meta: Nebula verwaltet sich selbst. Notizen, Status und Git-Infos an einem Ort.", + status: ProjectStatus.IN_DEVELOPMENT, + priority: 3, + progress: 65, + accent: "violet", + tags: ["next.js", "prisma", "dashboard"], + favorite: true, + notes: [ + { + title: "Naechste Schritte", + content: + "- Kanban-Ansicht ergaenzen\n- Webhook-Endpoint fuer Push-Events\n- Export als Markdown", + pinned: true, + }, + ], + }, + { + name: "Synthwave CLI", + slug: "synthwave-cli", + summary: "Terminal-Tool, das Projektgeruest inkl. CI in Sekunden erzeugt.", + description: + "Idee: ein `npx synthwave init` das Repo, Lint, Tests, Container und Pipeline vorkonfiguriert.", + status: ProjectStatus.PLANNING, + priority: 2, + progress: 15, + accent: "cyan", + tags: ["cli", "typescript", "dx"], + favorite: false, + notes: [ + { + title: null, + content: "Konkurrenz pruefen: create-t3-app, degit, plop.", + pinned: false, + }, + ], + }, + { + name: "Pixelgarten", + slug: "pixelgarten", + summary: "Browser-Spiel: ein Garten, der aus Commit-Aktivitaet waechst.", + description: + "Rohe Idee. Jeder Commit pflanzt eine Pflanze, Streaks lassen sie bluehen.", + status: ProjectStatus.IDEA, + priority: 1, + progress: 0, + accent: "emerald", + tags: ["spiel", "canvas", "fun"], + favorite: false, + notes: [], + }, + ]; + + for (const sample of samples) { + const existing = await prisma.project.findUnique({ + where: { slug: sample.slug }, + }); + if (existing) { + console.log(` · uebersprungen (existiert): ${sample.slug}`); + continue; + } + + const { notes, ...project } = sample; + await prisma.project.create({ + data: { + ...project, + notes: notes.length ? { create: notes } : undefined, + }, + }); + console.log(` ✓ angelegt: ${sample.slug}`); + } +} + +main() + .then(async () => { + console.log("Seed abgeschlossen."); + await prisma.$disconnect(); + }) + .catch(async (error) => { + console.error("Seed fehlgeschlagen:", error); + await prisma.$disconnect(); + process.exit(1); + });