Behebt florian.wilde/nebula#22. Gitea deckelt jede Listenantwort auf MAX_RESPONSE_ITEMS (Voreinstellung 50), egal was limit sagt. calendarByPaging verglich gegen die angefragten 100 und hielt damit bei jedem Gitea-Repository die erste Seite fuer die letzte. Die Heatmap kannte nur die juengsten 50 Commits - und gab deren aeltesten als Anfang des Zeitraums aus, ohne truncated zu setzen. Sie behauptete also Vollstaendigkeit, die sie nicht hatte. Maßstab ist jetzt die erste tatsaechlich gelieferte Seite. Kuerzer als die heisst letzte Seite, leer heisst Ende, und der Deckel gilt weiterhin nach CALENDAR_MAX_PAGES - dann aber ehrlich mit truncated. Am echten Repository nachgesehen, dem aus dem Issue: vorher Seite 1: 50 -> 50 Commits jetzt Seite 1: 50 | Seite 2: 26 -> 76 Commits Gitea selbst weist 76 aus. WAS DAS KOSTET Bei Gitea macht der Kalender jetzt bis zu fuenf Aufrufe statt einem, und ein Repository mit kurzer Historie fragt eine Seite mehr, um zu erfahren, dass nichts mehr kommt. Das laeuft hoechstens einmal am Tag; der Preis dafuer, dass die Zahl stimmt, ist ihn wert. VIER BESTEHENDE TESTS ANGEFASST Ihr Fixture lieferte fuer jede Seite dieselben Daten und hielt nur deshalb an, weil die alte Regel eine kurze Seite fuer die letzte hielt - also wegen genau des Fehlers, der hier behoben wird. Kein Server verhaelt sich so. Sie holen ihre Daten jetzt ueber eineSeite() und pruefen unveraendert das, worum es ihnen geht: Zaehlen und Filtern. Vier neue Tests stellen eine deckelnde Gegenseite nach. Gegengeprobt: mit der alten Zeile fallen genau diese vier. Refs #22
✦ Nebula
Das Kontrollzentrum für deine Vibe-Coding Projekte.
Next.js 15 · TypeScript · Tailwind CSS 4 · Prisma · PostgreSQL · Docker
Was ist Nebula?
Vibe Coding produziert schnell viele halbfertige Projekte: ein Repo hier, eine Idee dort, irgendwo eine Notiz in einer Textdatei. Nebula bündelt das an einem Ort.
Jedes Projekt bekommt einen Status (von der rohen Idee bis „Fertig"), beliebig viele Notizen und optional ein verknüpftes Repository. Nebula holt sich von dort per API Branches, Tags, Commits, Releases und offene Issues/PRs — egal ob GitHub, GitLab oder eine selbst gehostete Gitea-Instanz.
Neben dem Projektbrett gibt es Aufgaben über alle Projekte hinweg (nach Fälligkeit geordnet), einen Wochenrückblick und eine fortlaufende Zeitleiste aus dem Aktivitätsverlauf. Aufgaben lassen sich als Git-Issue anlegen; wird das Issue geschlossen, hakt Nebula die Aufgabe ab — sofort per Webhook oder verzögert beim nächsten Abgleich.
Die Anmeldung läuft über Benutzername und Passwort oder Passkey (WebAuthn). Nebula kennt zwei Betriebsarten: Einzelbetrieb für ein Konto oder Mehrbenutzerbetrieb, in dem jede Person ihren eigenen, streng getrennten Bereich hat. Umschalten geht jederzeit in den Admin-Einstellungen.
Alle Daten liegen in deiner lokalen PostgreSQL. Es gibt keinen Cloud-Dienst dahinter, Access Tokens werden verschlüsselt abgelegt.
Eine vollständige Auflistung aller Funktionen steht in FEATURES.md, geplante Erweiterungen in ROADMAP.md. Für den Dauerbetrieb auf einem Server — TLS, Firewall, Sicherungen und Updates — gibt es DEPLOYMENT.md.
Schnellstart
Zwei Betriebsarten, dieselbe Codebasis. Beide brauchen zuerst eine .env:
node scripts/setup.mjs # legt .env an und erzeugt ein zufälliges APP_SECRET
Ohne Node zur Hand? Einfach
.env.examplenach.envkopieren undAPP_SECRETvon Hand auf einen langen Zufallswert setzen.
Variante A — Standalone (empfohlen zum Entwickeln)
Die App läuft nativ auf dem Rechner, nur PostgreSQL kommt aus dem Container:
npm run docker:db # Postgres 17 auf 127.0.0.1:5432
npm install
npm run db:migrate # Schema anlegen
npm run db:seed # optional: drei Beispielprojekte
npm run dev # → http://localhost:3000
Für den Produktivbetrieb ohne Docker:
npm run build
npm start # → http://localhost:3000
Variante B — Alles in Docker
docker compose --profile full up -d --build # → http://localhost:3000
Der App-Container spielt beim Start automatisch die Migrationen ein und wartet dabei auf die Datenbank. Logs mitlesen:
docker compose --profile full logs -f app
Stoppen:
docker compose --profile full down # Container weg, Daten bleiben
docker compose --profile full down -v # inkl. Datenbank-Volume
Variante C — Eigene, bereits laufende PostgreSQL
Docker wird gar nicht gebraucht. In der .env nur den Connection-String setzen:
DATABASE_URL="postgresql://benutzer:passwort@dbhost:5432/nebula?schema=public"
Dann npm install && npm run db:migrate && npm run dev.
Voraussetzungen
| Variante | Benötigt |
|---|---|
| Standalone | Node.js ≥ 20.11, npm, PostgreSQL ≥ 14 (lokal oder via Docker) |
| Docker | Docker Engine ≥ 24 mit Compose v2 |
Konfiguration
Alle Variablen sind in .env.example dokumentiert.
| Variable | Bedeutung | Default |
|---|---|---|
APP_NAME |
Anzeigename: Seitentitel, Kopfzeile, Passkey-Dialog | Nebula |
APP_TAGLINE |
Kleine Zeile unter dem Namen | Vibe Coding |
APP_URL |
Öffentliche Adresse — bestimmt die WebAuthn-RP-ID | http://localhost:3000 |
DATABASE_URL |
PostgreSQL-Verbindung (Prisma) | postgresql://nebula:nebula@localhost:5432/nebula |
APP_SECRET |
Schlüssel für die Token-Verschlüsselung (AES-256-GCM) | — unbedingt setzen |
SESSION_TTL_DAYS |
Absolute Lebensdauer einer Anmeldung in Tagen | 30 |
SESSION_IDLE_HOURS |
Nach so vielen Stunden ohne Nutzung läuft die Sitzung ab (0 = aus) |
8 |
PORT |
Port der Anwendung | 3000 |
APP_PORT |
Host-Port-Mapping des App-Containers | 127.0.0.1:3000 |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
Zugangsdaten des DB-Containers | nebula |
POSTGRES_PORT |
Host-Port der Datenbank — wirkt nur mit docker-compose.dev.yml, sonst hat die DB keinen |
127.0.0.1:5432 |
ADMINER_PORT |
Host-Port von Adminer (--profile tools) |
127.0.0.1:8080 |
GIT_API_TIMEOUT_MS |
Timeout für Git-API-Aufrufe, in Millisekunden. Alles unter 1000 oder Unlesbares wird verworfen (mit Warnung im Log) — sonst bräche jeder Aufruf sofort ab | 15000 |
GIT_ALLOW_LOOPBACK |
Git-Instanz auf demselben Rechner erlauben | false |
GIT_BLOCK_PRIVATE |
Zugriff ins private LAN sperren | false |
GIT_ALLOWED_HOSTS |
Strengster Modus: nur diese Hosts erlauben | leer |
NOTIFY_TIMEOUT_MS |
Timeout beim Zustellen von Benachrichtigungen | 10000 |
SMTP_HOST … SMTP_FROM |
SMTP-Zugang; ohne SMTP_HOST steht der Weg „E-Mail" nicht zur Verfügung |
leer |
APP_NAME, APP_TAGLINE und APP_URL werden zur Laufzeit gelesen. Dasselbe
Docker-Image lässt sich also ohne Neubau umbenennen oder unter einer anderen Adresse
betreiben — Container neu starten genügt.
APP_SECRETnachträglich ändern macht bereits gespeicherte Git-Tokens unlesbar. Sie müssen dann in den Verbindungen neu hinterlegt werden — Konten, Passwörter und Passkeys bleiben unberührt.
APP_URLnachträglich ändern (anderer Hostname) macht bestehende Passkeys ungültig, weil sie an die RP-ID gebunden sind. Passwörter funktionieren weiter.
Erste Anmeldung
Beim ersten Aufruf landest du auf /setup. Dort legst du das Administratorkonto an und
wählst die Betriebsart:
- Einzelbetrieb — genau ein Konto, keine Registrierung, keine Benutzerliste.
- Mehrbenutzerbetrieb — beliebig viele Konten. Projekte, Notizen und Git-Verbindungen sind pro Konto strikt getrennt; auch ein Administrator sieht fremde Projekte nicht.
Nach der Einrichtung ist /setup gesperrt. Bereits vorhandene Projekte ohne Besitzer
(etwa aus den Seed-Daten) werden dem neuen Konto zugeordnet.
Passkey einrichten: unter Mein Konto → Passkeys → Passkey hinzufügen. Danach genügt auf der Anmeldeseite ein Klick auf „Mit Passkey anmelden" — ohne Benutzernamen, weil Nebula auffindbare Credentials nutzt.
Passkeys brauchen einen sicheren Kontext. Sie funktionieren über
https://und überhttp://localhost, nicht über eine LAN-IP ohne TLS.APP_URLmuss dabei exakt der Adresse entsprechen, unter der du die App aufrufst.
Git-Verbindung einrichten
- Im Git-System einen Personal Access Token erzeugen — mit den Rechten aus dem Abschnitt Welche Rechte der Token braucht.
- In Nebula unter Git-Verbindungen → Verbindung hinzufügen eintragen:
System wählen, Basis-URL (z. B.
https://git.meine-firma.de, ohne/api) und Token. - Mit Testen prüfen — Nebula meldet den erkannten Benutzer zurück.
- Beim Anlegen oder Bearbeiten eines Projekts die Verbindung auswählen und über Repos laden das Repository aussuchen (oder die URL direkt eintippen).
- Auf der Projektseite Jetzt synchronisieren — Branches, Tags, Commits und Releases werden geholt und als Snapshot gespeichert.
Der Snapshot bleibt in der Datenbank, das Dashboard funktioniert also auch offline. Ohne Token gehen nur öffentliche Repositories, mit strengem Rate-Limit.
Welche Rechte der Token braucht
Nebula liest fast alles nur. Geschrieben wird an genau zwei Stellen: wenn du eine
Aufgabe als Issue anlegst, und wenn eine Aufgabe auf DONE wandert und Nebula das
verknüpfte Issue schließt. Alles andere sind GET-Aufrufe. Deshalb lohnt es, zwei Fälle
zu unterscheiden:
Fall A — nur Abgleich. Branches, Tags, Commits, Releases, CI-Zustand, README, Heatmap, offene Issues und PRs anzeigen. Rein lesend.
Fall B — zusätzlich die Issue-Anbindung. Eine Aufgabe als Issue anlegen; wird das Issue geschlossen, hakt Nebula die Aufgabe ab und schließt sie umgekehrt mit.
| System | Fall A (nur lesen) | Fall B zusätzlich |
|---|---|---|
| GitHub | Fine-grained Token: Metadata: Read, Contents: Read, Issues: Read, Pull requests: Read, Actions: Read | Issues: Read and write |
| GitLab | read_api |
api — es gibt nichts dazwischen |
| Gitea / Forgejo | read:repository, read:issue, read:user |
write:issue |
Jeder Aufruf einzeln
Die folgenden Tabellen sind vollständig: mehr als das ruft Nebula nicht nach draußen.
Sie sind aus src/lib/git/github.ts, gitlab.ts und gitea.ts erhoben — wer prüfen
will, ob hier etwas fehlt, greppt dort nach getJson, getJsonSafe und sendJson.
GitHub — https://api.github.com, Header Authorization: Bearer …
| Aufruf | Wofür | Benötigtes Recht |
|---|---|---|
GET /rate_limit |
Verbindungstest | keins |
GET /user |
Kontoname beim Testen zurückmelden | keins |
GET /user/repos |
Auswahlliste hinter Repos laden | Metadata: Read |
GET /repos/{o}/{r} |
Stammdaten, Default-Branch, Sichtbarkeit | Metadata: Read |
GET …/languages |
Sprachverteilung | Metadata: Read |
GET …/tags |
Tags | Metadata: Read |
GET …/stats/commit_activity |
Commit-Heatmap über ein Jahr | Metadata: Read |
GET …/branches |
Branch-Liste | Contents: Read |
GET …/commits |
die letzten 100 Commits | Contents: Read |
GET …/releases |
Releases | Contents: Read |
GET …/readme |
README-Vorschau | Contents: Read |
GET …/pulls?state=open |
offene Pull Requests | Pull requests: Read |
GET …/issues?state=open |
offene Issues | Issues: Read |
GET …/issues/{nr} |
Abgleich: ist das verknüpfte Issue noch offen? | Issues: Read |
GET …/actions/runs |
CI-Zustand des Default-Branch | Actions: Read |
POST …/issues |
Aufgabe als Issue anlegen | Issues: Read and write |
PATCH …/issues/{nr} |
Issue schließen oder wieder öffnen | Issues: Read and write |
Dass /tags und /stats/commit_activity unter Metadata fallen und nicht unter
Contents, ist nicht zu erraten — es steht so in
GitHubs Berechtigungsübersicht.
GitLab — {Basis-URL}/api/v4, Header PRIVATE-TOKEN: …
| Aufruf | Wofür | Benötigtes Recht |
|---|---|---|
GET /projects?per_page=1 |
Verbindungstest | read_api |
GET /user |
Kontoname beim Testen zurückmelden | read_user (in read_api enthalten) |
GET /projects?membership=true |
Auswahlliste hinter Repos laden | read_api |
GET /projects/{id}?statistics=true |
Stammdaten, Default-Branch, Sichtbarkeit | read_api |
GET …/repository/branches · /tags · /commits |
Snapshot und Heatmap | read_api |
GET …/repository/tree · /repository/files/{datei} |
README (zwei Aufrufe, GitLab hat keinen README-Endpunkt) | read_api |
GET …/releases · /languages |
Releases, Sprachverteilung | read_api |
GET …/merge_requests?state=opened |
offene Merge Requests | read_api |
GET …/issues?state=opened |
offene Issues | read_api |
GET …/issues/{nr} |
Abgleich: ist das verknüpfte Issue noch offen? | read_api |
GET …/pipelines |
CI-Zustand des Default-Branch | read_api |
POST …/issues |
Aufgabe als Issue anlegen | api |
PUT …/issues/{nr} (state_event) |
Issue schließen oder wieder öffnen | api |
Gitea / Forgejo — {Basis-URL}/api/v1, Header Authorization: token …
| Aufruf | Wofür | Benötigtes Recht |
|---|---|---|
GET /version |
Verbindungstest | keins — der Aufruf darf fehlschlagen, Nebula wertet ihn nur ergänzend aus |
GET /user |
Kontoname beim Testen zurückmelden | read:user |
GET /user/repos bzw. GET /repos/search |
Auswahlliste hinter Repos laden | read:repository |
GET /repos/{o}/{r} |
Stammdaten, Default-Branch, Sichtbarkeit | read:repository |
GET …/branches · /tags · /commits · /releases · /languages |
Snapshot und Heatmap | read:repository |
GET …/contents · /contents/{datei} |
README (zwei Aufrufe, wie bei GitLab) | read:repository |
GET …/commits/{branch}/status |
CI-Zustand des Default-Branch | read:repository |
GET …/pulls?state=open |
offene Pull Requests | read:repository — Gitea zählt PRs zum Repository-Scope, nicht zu den Issues |
GET …/issues?state=open&type=issues |
offene Issues | read:issue |
GET …/issues/{nr} |
Abgleich: ist das verknüpfte Issue noch offen? | read:issue |
POST …/issues |
Aufgabe als Issue anlegen | write:issue |
PATCH …/issues/{nr} |
Issue schließen oder wieder öffnen | write:issue |
write:issue schließt read:issue mit ein — Gitea versteht write: als „lesen und
schreiben". Beide anzuhaken schadet nicht, nötig ist es nicht.
Was Nebula nicht tut
- Keine Kommentare. Nebula schreibt nie einen Issue-Kommentar, weder beim Anlegen noch beim Schließen. Was im Rumpf des angelegten Issues steht, ist die Aufgabenbeschreibung plus eine Zeile Rückverweis — danach fasst Nebula den Text nicht mehr an.
- Kein Code. Nichts wird committet, gepusht, gemergt oder getaggt. Nebula ist eine Übersicht, keine Entwicklungsumgebung.
- Keine Webhooks anlegen. Den Haken trägst du selbst im Git-System ein — genau deshalb verlangt Nebula dafür keine Administrationsrechte.
- Keine Labels, keine Meilensteine, keine Zuweisungen.
- Keine Adminrechte, keine Organisations- oder Benutzerverwaltung.
Kommentare und Labels, die an einem Issue hängen, stammen also entweder aus der Weboberfläche deines Git-Systems oder von einem anderen Werkzeug mit eigenem Token — nicht von Nebula.
Der GitLab-Sonderfall
GitLab kennt keinen schmalen Schreib-Scope für Issues. Laut
Access token scopes
gibt es nur api — „complete read and write access to the API" — und darunter direkt
read_api. Wer bei GitLab Aufgaben als Issue anlegen will, vergibt damit ein Token, das
auch Code ändern darf.
Wenn dir das zu weit geht: bleib bei read_api und verzichte auf die Issue-Anbindung.
Der Abgleich läuft vollständig, und geschlossene Issues werden weiterhin erkannt und
haken ihre Aufgabe ab — nur der Rückweg von Nebula ins Issue fällt weg. Ein eigenes
Dienstkonto, das nur Zugriff auf die betroffenen Projekte hat, ist der andere Ausweg.
Im Zweifel klein anfangen
Ein fehlendes Recht führt zu einem 403 mit dem Hinweis „Token hat nicht die noetigen
Scopes"; die Meldung steht am Repository auf der Projektseite. Fang mit Fall A an und
leg gezielt nach, wenn du die Issue-Anbindung wirklich benutzt — statt vorsorglich
alles zu vergeben.
Automatisch statt auf Knopfdruck
Unter Admin-Einstellungen → Automatischer Abgleich lässt sich der Sync in den Hintergrund verlegen: Abstand von 15 Minuten bis täglich, Anzahl Projekte je Durchlauf, und wahlweise ohne fertige und archivierte Projekte. Standardmäßig ist er aus, weil jeder Durchlauf API-Kontingent verbraucht.
Der Zeitgeber läuft im Serverprozess — kein zusätzlicher Container, kein externer Cron. Nach Fehlschlägen wird ein Repository mit wachsendem Abstand zurückgehalten, damit ein abgelaufener Token nicht dauerhaft Kontingent verbrennt. Einzelne Projekte lassen sich im Projektdialog ausnehmen; manuell synchronisieren geht immer.
Wer lieber von außen taktet, lässt den Automatismus aus und ruft
POST /api/admin/sync/run per eigenem Cron auf.
Export, Import und Sicherung
Projekte mitnehmen
| Weg | Wo | Inhalt |
|---|---|---|
| Alle Projekte als JSON | Dashboard → Datenbank-Symbol | Stammdaten, Notizen, Repo-Verknüpfung |
| Ein Projekt als JSON | Projektseite → Download-Symbol | dasselbe, für ein Projekt |
| Ein Projekt als Markdown | Projektseite → Download-Symbol | lesbar, inklusive letztem Git-Stand |
| Import | Dashboard → Datenbank-Symbol | JSON einlesen, mit Vorschau |
Access Tokens sind nie enthalten. Die Git-Verbindung wird nur beschrieben (Name, System, Basis-URL); nach dem Import muss das Token neu hinterlegt werden. Verweist eine importierte Datei auf eine hier unbekannte Verbindung, wird das Projekt ohne Verknüpfung angelegt und in der Vorschau angezeigt.
Beim Import wird über den Slug abgeglichen. Für Treffer gibt es drei Strategien:
- Überspringen — Vorhandene bleiben unverändert
- Zusätzlich anlegen — Kopie mit freiem Slug (
nebula→nebula-2) - Ersetzen — Stammdaten überschreiben, Notizen austauschen
Vor dem Schreiben zeigt eine Vorschau, was passieren würde. Erst dann wird geschrieben.
Sicherung
Zwei Wege, mit unterschiedlichem Zweck:
JSON-Sicherung (Admin-Einstellungen → Sicherung) enthält Einstellungen, Konten,
Verbindungen, Projekte und Notizen. Gut zum Umziehen. Passwörter liegen als scrypt-Hash
vor, Git-Tokens als Chiffrat — ohne dasselbe APP_SECRET sind sie wertlos. Trotzdem
vertraulich behandeln.
Datenbank-Sicherung über pg_dump — das, was man zum Wiederherstellen einer kaputten
Instanz braucht:
.\scripts\backup.ps1 # → .\backups\nebula-<datum>.sql
.\scripts\restore.ps1 -File .\backups\nebula-....sql
./scripts/backup.sh # → ./backups/nebula-<datum>.sql
./scripts/restore.sh backups/nebula-....sql
Die Skripte lesen Benutzer und Datenbankname aus der .env und führen pg_dump im
Container aus — auf dem Rechner selbst muss nichts installiert sein.
Eine Sicherung ist erst dann eine Sicherung, wenn du sie einmal zurückgespielt hast. Das geht ohne Risiko in eine Wegwerf-Datenbank:
docker exec nebula-db psql -U nebula -d postgres -c "CREATE DATABASE nebula_test" .\scripts\restore.ps1 -File .\backups\nebula-....sql -Database nebula_test -Force
Projektstruktur
nebula/
├── docs/banner.png Titelbild dieser Datei
├── docker/entrypoint.sh Migrationen + Serverstart im Container
├── docker-compose.yml db (immer) · app (--profile full) · adminer (--profile tools)
├── Dockerfile Multi-Stage-Build auf Basis des Next-Standalone-Outputs
├── prisma/
│ ├── schema.prisma Datenmodell
│ ├── migrations/ versionierte SQL-Migrationen
│ └── seed.ts Beispieldaten
├── scripts/setup.mjs .env erzeugen inkl. APP_SECRET
└── src/
├── app/
│ ├── page.tsx Dashboard (Board, Filter, Kennzahlen)
│ ├── projects/[id]/ Projektdetail: Beschreibung, Notizen, Aufgaben, Git
│ ├── tasks/ Aufgaben über alle Projekte, nach Fälligkeit
│ ├── rueckblick/ Wochenrückblick
│ ├── verlauf/ Fortlaufende Zeitleiste
│ ├── connections/ Verwaltung der Git-Verbindungen
│ ├── account/ admin/ Eigenes Konto · Adminbereich
│ └── api/ REST-Endpunkte (siehe Abschnitt „API")
├── components/ UI-Komponenten (Board, Karten, Panels, Dialoge)
├── lib/
│ ├── auth/ Sitzungen, Passwörter, TOTP, WebAuthn, Zugriffsschutz
│ ├── crypto.ts AES-256-GCM für Access Tokens
│ ├── db.ts Prisma-Client
│ ├── git/ Provider-Adapter: github.ts · gitlab.ts · gitea.ts
│ ├── sync/ Repo-Snapshot holen, speichern, Zeitgeber
│ ├── tasks/ Aufgabenlogik, Brücke zu Git-Issues
│ ├── webhooks/ Signaturprüfung eingehender Webhooks
│ ├── review.ts Wochengrenzen und Kennzahlen
│ ├── timeline.ts Cursor-Blättern für den Verlauf
│ ├── security/ SSRF-Schutz, Ratenbegrenzung, Body-Prüfung
│ └── validation.ts Zod-Schemas für alle Eingaben
└── types/ gemeinsame Typen für Server und Client
npm-Skripte
| Skript | Zweck |
|---|---|
npm run dev |
Entwicklungsserver mit Hot Reload |
npm run build / npm start |
Produktionsbuild und -start (ohne Docker) |
npm run setup |
.env anlegen inkl. generiertem APP_SECRET |
npm run db:migrate |
Migrationen einspielen (prisma migrate deploy) |
npm run db:migrate:dev |
Neue Migration aus Schemaänderungen erzeugen |
npm run db:seed |
Beispielprojekte anlegen |
npm run db:studio |
Prisma Studio öffnen |
npm run db:reset |
Datenbank zurücksetzen (löscht alles) |
npm test |
Unit-Tests (Vitest), unter einer Sekunde |
npm run test:watch |
Tests im Beobachtungsmodus |
npm run typecheck |
TypeScript prüfen |
npm test |
Unit-Tests (Vitest, ohne Datenbank) |
npm run test:e2e |
Ende-zu-Ende-Tests (Playwright, baut vorher) |
npm run test:all |
Beides nacheinander |
npm run docker:db |
Nur die Datenbank starten, mit Host-Port für npm run dev |
npm run docker:up / docker:down |
Kompletten Stack starten / stoppen |
npm run docker:tools |
Adminer auf http://localhost:8080 |
API
Alle Endpunkte liefern und erwarten JSON.
Bis auf /api/health und /api/auth/* erfordern alle Endpunkte eine Anmeldung und liefern
ausschließlich Daten des angemeldeten Kontos.
| Methode | Pfad | Zweck |
|---|---|---|
GET |
/api/health |
Status inkl. Datenbankverbindung (Docker-Healthcheck) |
POST |
/api/auth/setup |
Administratorkonto anlegen (nur beim ersten Mal) |
POST |
/api/auth/login /api/auth/logout /api/auth/register |
Anmeldung |
POST |
/api/auth/mfa |
Zweiter Faktor: Einmalkennwort oder Wiederherstellungscode |
GET |
/api/auth/session |
Aktuelle Sitzung und Instanzzustand |
POST |
/api/auth/passkey/{login,register}/{options,verify} |
WebAuthn |
PATCH |
/api/account/profile |
Eigenes Profil |
POST |
/api/account/password |
Eigenes Passwort setzen |
GET DELETE |
/api/account/sessions |
Eigene Sitzungen / überall abmelden |
PATCH DELETE |
/api/account/passkeys/:id |
Passkey umbenennen / entfernen |
GET POST PUT DELETE |
/api/account/totp |
Zwei-Faktor: Zustand, einrichten, aktivieren, abschalten |
POST |
/api/account/totp/recovery |
Neue Wiederherstellungscodes |
GET PATCH |
/api/admin/settings |
Betriebsart, Registrierung, Abgleich (Admin) |
POST |
/api/admin/sync/run |
Abgleich sofort anstoßen (Admin) |
GET POST |
/api/admin/users |
Konten auflisten / anlegen (Admin) |
PATCH DELETE |
/api/admin/users/:id |
Konto ändern / löschen (Admin) |
GET POST |
/api/projects |
Projekte auflisten / anlegen |
GET |
/api/projects/export |
Alle eigenen Projekte als JSON |
GET |
/api/projects/:id/export |
Ein Projekt (?format=md für Markdown) |
POST |
/api/projects/import |
Import, mit dryRun für die Vorschau |
GET POST |
/api/templates |
Projektvorlagen auflisten / anlegen |
PATCH DELETE |
/api/templates/:id |
Vorlage ändern / löschen |
POST |
/api/templates/from-project |
Aus einem Projekt eine Vorlage machen |
GET |
/api/admin/backup |
Vollständige Sicherung als JSON (Admin) |
GET |
/api/search?q= |
Volltextsuche über eigene Notizen und Aufgaben |
GET |
/api/activity |
Verlauf über alle Projekte, per Cursor (?vor=&id=&limit=) |
GET POST |
/api/notifications |
Benachrichtigungsziele |
PATCH DELETE |
/api/notifications/:id |
Ziel ändern / entfernen |
POST |
/api/notifications/:id/test |
Testnachricht senden |
GET PATCH DELETE |
/api/projects/:id |
Projekt lesen / ändern / löschen |
POST |
/api/projects/:id/sync |
Alle Repositories des Projekts abgleichen |
PATCH |
/api/projects/reorder |
Neue Reihenfolge im Brett |
POST |
/api/projects/bulk |
Status, Tags oder Löschen für mehrere |
GET |
/api/projects/:id/activity |
Verlauf des Projekts (?limit=) |
GET POST |
/api/projects/:id/repositories |
Repositories des Projekts |
PATCH DELETE |
/api/repositories/:id |
Repository ändern / vom Projekt lösen |
POST |
/api/repositories/:id/sync |
Ein einzelnes Repository abgleichen |
POST DELETE |
/api/repositories/:id/webhook |
Webhook einrichten / zurücknehmen |
POST |
/api/webhooks/:repositoryId |
Eingehend von GitHub/GitLab/Gitea, per Signatur geprüft |
GET POST |
/api/projects/:id/tasks |
Aufgaben des Projekts |
PATCH |
/api/projects/:id/tasks/reorder |
Neue Reihenfolge nach dem Ziehen |
POST |
/api/projects/:id/tasks/from-issues |
Offene Issues als Aufgaben übernehmen |
PATCH DELETE |
/api/tasks/:id |
Aufgabe ändern / löschen |
POST |
/api/tasks/:id/issue |
Aufgabe als Issue im verknüpften Repository anlegen |
GET POST |
/api/projects/:id/notes |
Notizen des Projekts |
PATCH DELETE |
/api/notes/:id |
Notiz ändern / löschen |
GET POST |
/api/connections |
Git-Verbindungen |
PATCH DELETE |
/api/connections/:id |
Verbindung ändern / löschen |
POST |
/api/connections/:id/check |
Verbindung und Token testen |
GET |
/api/connections/:id/repos |
Zugängliche Repositories auflisten |
Beispiel — Idee per Kommandozeile anlegen:
curl -X POST http://localhost:3000/api/projects \
-H "Content-Type: application/json" \
-d '{"name":"Neue Idee","status":"IDEA","tags":["experiment"]}'
Sicherheit
| Bereich | Umsetzung |
|---|---|
| Anmeldung | Passwort-Hashing mit scrypt (N=32768, r=8, p=1), serverseitige Sitzungen — in der Datenbank liegt nur der SHA-256-Hash des Cookie-Tokens |
| Passkeys | WebAuthn über SimpleWebAuthn, Challenges einmalig verwendbar und in der DB abgelegt, Signaturzähler wird fortgeschrieben |
| Cookies | HttpOnly, SameSite=Lax, Secure automatisch bei https |
| CSRF | Jede schreibende Anfrage muss vom eigenen Origin kommen (Sec-Fetch-Site / Origin); zusätzlich wird Content-Type: application/json erzwungen, was den Formular-Trick ohne Preflight ausschließt |
| CSP | Strikte Policy mit Per-Request-Nonce und strict-dynamic, dazu frame-ancestors 'none', object-src 'none', base-uri 'none' |
| Weitere Header | X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy |
| SSRF | Die Basis-URL einer Git-Verbindung ist Benutzereingabe. Vor jedem ausgehenden Aufruf werden Hostname und alle aufgelösten IPs geprüft (auch nach Redirects); Loopback, Link-Local samt Cloud-Metadaten, Multicast und CGNAT sind gesperrt |
| Datentrennung | Jede Abfrage filtert auf den Besitzer. Fremde IDs liefern 404, nicht 403 — die Antwort verrät also nicht einmal, dass es den Datensatz gibt |
| Rate-Limiting | Anmeldung pro IP und Konto, ausgehende Git-Aufrufe, WebAuthn-Optionen; Konto sperrt sich nach 8 Fehlversuchen für 15 Minuten |
| Eingaben | Zod-Schemas an jeder API-Grenze, Body auf 256 KB begrenzt |
| Git-Tokens | AES-256-GCM, Schlüssel per scrypt aus APP_SECRET. Werden nie an den Browser ausgeliefert — die API zeigt nur ghp_…9f3a |
| Sitzungsende | Deaktivieren, Löschen oder ein neues Passwort beenden sofort alle Sitzungen des Kontos |
Grenzen, die du kennen solltest:
- Ist
APP_SECRETnicht gesetzt, warnt die App beim Start und nutzt einen unsicheren Entwicklungsschlüssel. - Ohne
Origin- undSec-Fetch-Site-Header wird eine schreibende Anfrage durchgelassen. Das betrifft nur Nicht-Browser-Clients (curl, Skripte) und ist bewusst so, damit die API skriptbar bleibt — ein CSRF-Vektor ist es nicht, weil jeder Browser mindestens einen der beiden Header sendet. - Das Rate-Limiting liegt im Arbeitsspeicher und wird beim Neustart zurückgesetzt.
- Es gibt kein HSTS und keine TLS-Terminierung. Für den Betrieb im Internet gehört ein Reverse Proxy mit gültigem Zertifikat davor — der ist ohnehin nötig, damit Passkeys funktionieren.
- Passwort-Zurücksetzen per E-Mail gibt es nicht; ein Administrator setzt Passwörter neu.
Fehlersuche
| Symptom | Ursache / Lösung |
|---|---|
Can't reach database server |
Läuft npm run docker:db? Ein blosses docker compose up -d db startet die DB ohne Host-Port — dann ist sie von npm run dev aus nicht erreichbar. Stimmt DATABASE_URL? |
The table ... does not exist |
Migrationen fehlen: npm run db:migrate |
Sync meldet 401 |
Token abgelaufen oder falsche Scopes — unter Git-Verbindungen neu setzen |
Sync meldet 404 |
Owner/Repo prüfen; bei GitLab gehört die volle Gruppenkette in „Owner" |
Sync meldet 403 Rate-Limit |
Ohne Token ist das Limit sehr niedrig — Token hinterlegen |
| Tokens plötzlich ungültig | APP_SECRET wurde geändert — Tokens neu eintragen |
| Port 3000 belegt | APP_PORT in der .env ändern (Docker) bzw. npm run dev -- -p 3001 |
package.json#prisma is deprecated |
Harmlose Warnung von Prisma 6.19. Die Umstellung auf prisma.config.ts steht aus, weil dann die .env nicht mehr automatisch geladen wird — vor Prisma 7 zu erledigen |
| Passkey-Knopf fehlt / „kein sicherer Kontext" | APP_URL zeigt weder auf https:// noch auf http://localhost |
| Passkey wird nicht akzeptiert | APP_URL weicht von der aufgerufenen Adresse ab, oder der Hostname wurde geändert (RP-ID) |
Nach docker compose down -v ist alles weg |
Das Volume enthielt Konten und Projekte — -v löscht es |
| Kein Zugriff auf die eigene Gitea im LAN | Läuft sie auf demselben Rechner? Dann GIT_ALLOW_LOOPBACK=true setzen |
Getestet mit
Am 25.07.2026 gegen echte Systeme verifiziert — beide Betriebsarten, alle drei Git-Adapter (GitHub, GitLab und Gitea jeweils gegen die öffentliche Instanz). Seitdem laufen die Ende-zu-Ende-Tests bei jeder Änderung mit; live nach draußen geht davon nur ein Test gegen die GitHub-API.
Werkzeugstand vom 18.08.2026:
| Version | |
|---|---|
| Node.js | 24.19.0 |
| npm | 11.17.0 |
| Next.js | 15.5.21 |
| Prisma | 6.19.3 |
| PostgreSQL | 17.11 (Alpine) |
| Docker | 29.7.2 |
Lizenz
MIT — siehe LICENSE.
