diff --git a/CLAUDE.md b/CLAUDE.md index de9da75..eea4abe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,31 @@ SSRF-Schutz (`lib/security/ssrf.ts`). - **Datumsangaben** über `lib/utils` formatieren (feste Zeitzone), sonst weichen Server-Render und Hydration voneinander ab. Relative Zeiten bekommen `suppressHydrationWarning`. +- **Die feste Zeitzone gilt auch beim *Rechnen*, nicht nur beim Formatieren.** + Im Container läuft die Anwendung nach UTC; „welcher Kalendertag" und + „welche Woche" kippen dort abends um einen Tag. `lib/tasks` (`dueBucket`) + und `lib/review` (`wocheUm`) rechnen deshalb über Berliner Kalendertage. + Und eine Woche ist **nicht** „7 × 24 Stunden" — bei einer Zeitumstellung + hat sie 167 oder 169. +- **`min-width: auto` bei Flex- und Grid-Kindern.** Hat hier zweimal + zugeschlagen: ein Kind weigert sich, unter die Inhaltsbreite seines + breitesten Kindes zu schrumpfen, und drückt stattdessen die Nachbarn aus + dem Bild. Einmal war es ein Sortierfeld mit `min-width`, das „Neues + Projekt" hinausschob; einmal die Heatmap, die die ganze Projektseite + doppelt so breit machte. Heilmittel ist beide Male `min-w-0` am Kind — + ohne das greift auch ein `overflow-x-auto` darin nicht. +- **`body { overflow-x: hidden }` versteckt genau diese Fehler.** Es steht in + `globals.css` als Notnagel; was es abschneidet, ist auf dem Telefon + schlicht nicht mehr erreichbar. Ursachen gehören in die Komponente. +- **Dialoge müssen der Bildschirmtastatur ausweichen.** Sie verkleinert nur + das *sichtbare* Fenster, nicht das Layoutfenster — `position: fixed` und + `100vh` beziehen sich weiter auf das volle. `components/ui/Modal.tsx` hängt + dafür an `window.visualViewport`; wer ein eigenes Overlay baut, braucht + dasselbe. +- **Blättern über Cursor, nicht über `skip`.** Der Aktivitätsverlauf schreibt + mehrere Zeilen im selben Augenblick; ein Offset über so eine Grenze hinweg + überspringt oder verdoppelt lautlos. Muster in `lib/timeline.ts`: + `(createdAt, id)` als Cursor, Sortierung identisch zur Bedingung. ## Migrationen @@ -83,9 +108,24 @@ vergleicht mit der *laufenden* Datenbank. Ist eine eben erzeugte Migration dort noch nicht eingespielt, steht ihr Inhalt in der nächsten gleich noch einmal. -**Die erzeugte SQL vor dem Anwenden lesen.** Der Volltextindex -`Note_fulltext_idx` ist ein Ausdrucksindex, den Prisma nicht kennt (er wird -bislang in Ruhe gelassen, aber das ist nicht garantiert). +**Das Zielverzeichnis vorher *nicht* anlegen.** Ein leerer Ordner unter +`prisma/migrations/` lässt `--from-migrations` mit `P3015` abbrechen +(„Could not find the migration file"). Erst die SQL woandershin erzeugen, +dann Ordner anlegen und hineinkopieren. + +**Die erzeugte SQL vor dem Anwenden lesen.** Die Volltextindizes +`Note_fulltext_idx` und `Task_fulltext_idx` sind Ausdrucksindizes, die Prisma +nicht kennt (sie werden bislang in Ruhe gelassen, aber das ist nicht +garantiert). + +**Ein Ausdrucksindex greift nur bei Zeichengleichheit.** Weicht der Ausdruck +in der Abfrage auch nur in einem `coalesce` vom Index ab, rechnet Postgres +`to_tsvector` je Zeile neu — ohne Fehlermeldung. Nachsehen statt annehmen: + +```bash +docker exec nebula-db psql -U nebula -d -c "SET enable_seqscan = off; +EXPLAIN (COSTS OFF) SELECT …" # muss einen Bitmap Index Scan zeigen +``` **Datenmigrationen von Hand schreiben.** `migrate diff` kennt nur den Zielzustand: Spalten, die woandershin wandern, würde es ersatzlos löschen. @@ -103,6 +143,18 @@ Bei Änderungen an der Oberfläche **hinsehen**. Chrome ist installiert, ein Skript mit `playwright-core` und `channel: "chrome"` genügt für Screenshots. Mehrere Anzeigefehler dieses Projekts sind ausschließlich so aufgefallen. +**Seitlichen Überlauf misst man.** `document.documentElement.scrollWidth` +gegen `clientWidth` auf 375 px — das findet in Sekunden, was beim Ansehen +durchrutscht. Ein Skript, das dabei nur die *obersten* Verursacher meldet +(Eltern, die selbst schon überlaufen, herausfiltern), ist deutlich brauchbarer +als eine Liste aller betroffenen Knoten. + +**Die Testdaten müssen die Bausteine enthalten, um die es geht.** Eine +Messung gegen ein Projekt ohne Repository, ohne Snapshot und ohne README +bescheinigte hier einmal „alles in Ordnung", während die echte Projektseite +doppelt so breit war wie der Bildschirm. Erst mit vollständigen Daten +nachgestellt trat der Fehler zutage. + **Barrierefreiheit misst man, statt sie zu behaupten.** `axe-core` liegt als Entwicklungsabhängigkeit bereit; ein Skript hängt es per `addScriptTag` in die laufende Seite und ruft `axe.run` mit den WCAG-Tags auf. Jeder diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index af1071a..65c9ec1 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -283,6 +283,23 @@ Zurückspielen: ## Updates +Es sind zwei getrennte Orte, und das ist der Punkt, an dem es leicht +durcheinandergeht: + +| | Was dort liegt | +| --- | --- | +| **Das Git-Repository** | Der Quellcode. Dorthin wird entwickelt. | +| **Diese Instanz** | Die laufende Anwendung, die du im Browser benutzt. | + +Ein Commit im Git ändert am laufenden Nebula **nichts**. Der Server holt sich +von allein nichts ab; er betreibt weiter die Fassung, die dort zuletzt gebaut +wurde. Ein Update ist der Schritt dazwischen: holen, bauen, umschalten. + +**Woran du siehst, was gerade läuft:** die Versionsnummer unten in der +Fußzeile. Ein Klick darauf öffnet den Änderungsverlauf. Steht dort eine +ältere Nummer als im Git, fehlen dir die neueren Funktionen — sie sind +geschrieben, aber noch nicht auf diesem Server. + Dafür gibt es `scripts/update.sh`. Es macht alles in der richtigen Reihenfolge und bricht ab, sobald ein Schritt scheitert: @@ -308,7 +325,7 @@ nennt den Weg zurück. ```bash ./scripts/update.sh --yes # ohne Rückfrage, für Cron -./scripts/update.sh --ref v1.4.0 # auf eine bestimmte Fassung +./scripts/update.sh --ref v1.9.0 # auf eine bestimmte Fassung ./scripts/update.sh --no-pull # nur neu bauen, ohne git pull ``` @@ -354,6 +371,23 @@ docker system df docker image prune -f # alte Abbilder nach mehreren Updates ``` +### Webhooks + +Der Abgleich holt den Stand in seinem Takt. Wer ihn **sofort** haben will — +ein Push ist durch, ein Issue wurde geschlossen —, trägt im Git-System einen +Webhook ein. Nebula erzeugt URL und Geheimnis am Repository (Funksymbol im +Git-Bereich der Projektseite), eingetragen wird der Haken von Hand. + +Voraussetzung ist, dass **dein Git-System diese Instanz erreicht**. Hinter +dem Reverse Proxy ist das gegeben; in einem abgeschotteten Netz nicht. Der +Endpunkt liegt unter `/api/webhooks/` und ist der einzige ohne +Anmeldung — ausgewiesen wird sich über eine Signatur, die gegen das +hinterlegte Geheimnis geprüft wird. + +**Ohne Webhook geht nichts verloren.** Der Abgleich prüft den Zustand +verknüpfter Issues ohnehin mit und hakt erledigte Aufgaben nach. Der Webhook +macht es nur sofort statt beim nächsten Durchlauf. + ### Automatischer Abgleich Standardmäßig **aus**. Einzuschalten unter *Admin-Einstellungen → @@ -383,6 +417,13 @@ Damit du weißt, worum du dich **nicht** kümmern musst: Zugriffe; Kontosperre nach acht Fehlversuchen. - **Git-Tokens** liegen AES-256-GCM-verschlüsselt in der Datenbank und verlassen den Server nie — auch nicht im Export. +- **Sitzungen** laufen nach 8 Stunden ohne Nutzung ab und spätestens nach + 30 Tagen (`SESSION_IDLE_HOURS`, `SESSION_TTL_DAYS`). In der Datenbank liegt + nur der Hash des Tokens; das Cookie ist `HttpOnly`, `SameSite=Strict` und + bei HTTPS zusätzlich mit dem Präfix `__Host-`. +- **Passwörter** werden mit scrypt nach der Empfehlung des OWASP Password + Storage Cheat Sheet gehasht (N = 2^17). Rechnet mit rund einer halben + Sekunde je Anmeldung — das ist gewollt. - Der **Bildoptimierer** von Next ist abgeschaltet: Nebula benutzt ihn nicht, und `/_next/image` wäre sonst eine Angriffsfläche ohne Gegenwert. diff --git a/FEATURES.md b/FEATURES.md index 2478792..b957c34 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -145,12 +145,17 @@ dort wäre er auf schmalen Anzeigen mit der Auswahlleiste kollidiert. Favoriten bleiben stets oben. - **Vier Ansichten**: freies Raster, **kompakte Liste**, nach Status gruppierte Abschnitte oder **Kanban**. -- **Volltextsuche über Notizen.** Die Suche im Board arbeitet auf den geladenen Projekten - (Name, Kurzbeschreibung, Tags, Repo-Pfad). Notizen liegen dort nicht vor — sie - mitzuladen würde die Seite aufblähen. Stattdessen sucht Postgres serverseitig mit - seiner deutschen Textsuche, entprellt ab zwei Zeichen. Treffer erscheinen als eigene - Liste mit hervorgehobener Fundstelle, die betroffenen Projekte werden zusätzlich - eingeblendet. Deutsche Stammformen greifen: „Webhook" findet auch „Webhooks". +- **Volltextsuche über Notizen und Aufgaben.** Die Suche im Board arbeitet auf den + geladenen Projekten (Name, Kurzbeschreibung, Tags, Repo-Pfad). Notizen und Aufgaben + liegen dort nicht vor — sie mitzuladen würde die Seite aufblähen. Stattdessen sucht + Postgres serverseitig mit seiner deutschen Textsuche, entprellt ab zwei Zeichen. + Treffer erscheinen als zwei eigene Listen mit hervorgehobener Fundstelle, die + betroffenen Projekte werden zusätzlich eingeblendet. Deutsche Stammformen greifen: + „Webhook" findet auch „Webhooks", „Rechnung" auch „Rechnungen". + + Beide Seiten haben je einen **Ausdrucksindex** (`Note_fulltext_idx`, + `Task_fulltext_idx`). Der Ausdruck darin muss Zeichen für Zeichen dem in der Abfrage + entsprechen, sonst greift er nicht und Postgres rechnet `to_tsvector` je Zeile neu. - **Archiv** ist standardmäßig ausgeblendet und per Schalter einblendbar. - **Leerzustände** mit direktem Einstieg statt einer leeren Fläche. @@ -341,6 +346,54 @@ Die Aufgabe kennt keinen Besitzer — die Trennung läuft über das Projekt --- +## 2c. Rückblick und Verlauf + +Zwei Sichten auf denselben Aktivitätsverlauf, über alle Projekte hinweg. Beide teilen +sich Zeilendarstellung und Tagesgruppierung (`components/ActivityRow`) — zwei Kopien +wären beim nächsten Feinschliff auseinandergelaufen. + +Der Verlauf kennt keinen Besitzer; die Trennung läuft über das Projekt +(`project: { ownerId }` als erste Bedingung). + +### Wochenrückblick + +`/rueckblick` beantwortet „was ist diese Woche passiert". Oben Kennzahlen — Vorgänge, +berührte Projekte, erledigte und neue Aufgaben —, darunter die Vorgänge nach +Kalendertagen gebündelt, neuester Tag zuerst. Über `?woche=-n` geht es bis zu 25 Wochen +zurück; weiter lohnt nicht, weil der Verlauf ohnehin nur 200 Einträge je Projekt hält. + +**Gerechnet wird auf Kalendertage in fester Zeitzone** (Europe/Berlin), nicht auf +„sieben mal 24 Stunden": liegt eine Zeitumstellung in der Woche, hat sie 167 oder 169 +Stunden. Und im Container läuft die Anwendung nach UTC — ein Eintrag von Sonntagabend +fiele sonst in die falsche Woche. Das Fach bestimmt der **Server**, damit Server-Render +und Hydration nicht auseinanderlaufen. + +**„Aufgaben erledigt" wird am `meta`-Feld abgelesen** (`to: "DONE"`), nicht am +Anzeigetext. Der Text ist zum Lesen da und ändert sich mit der Formulierung; eine +Zählung daran würde beim nächsten umformulierten Einzeiler unbemerkt kippen. Einträge +ohne `meta` zählen nicht mit — lieber untertreiben als raten. + +### Globale Zeitleiste + +`/verlauf` zeigt denselben Verlauf **fortlaufend**: ohne Wochengrenze, ohne Fazit, mit +„Ältere laden". Die erste Seite kommt serverseitig mit, alles Weitere über +`/api/activity`. + +**Geblättert wird per Cursor** über `(createdAt, id)`, nicht per `skip`. Der Verlauf +schreibt mehrere Einträge im selben Augenblick — beim Übernehmen von Issues entstehen +zehn Zeilen mit identischem Zeitstempel. Ein Offset über eine solche Grenze hinweg +überspringt Zeilen oder zeigt sie zweimal, und zwar lautlos. Der zweite Zweig der +Bedingung (gleicher Zeitpunkt, kleinere ID) fängt genau das ab. + +Nachgerechnet gegen 144 Einträge mit absichtlichen Blöcken gleicher Zeitstempel: 15 +Seiten, 144 Zeilen, keine Dublette, streng absteigend. + +Beide Seiten sind über die Schnellsuche erreichbar, der Rückblick zusätzlich über einen +Verweis vom Dashboard. Bewusst **kein** weiterer Punkt in der Kopfleiste — die ist auf +dem Telefon schon eng. + +--- + ## 3. Notizen - **Beliebig viele Notizen pro Projekt**, mit optionalem Titel und mehrzeiligem Text. diff --git a/README.md b/README.md index 7e56091..4e7b4ab 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ Jedes Projekt bekommt einen **Status** (von der rohen Idee bis „Fertig"), beli 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 @@ -275,15 +281,25 @@ nebula/ └── src/ ├── app/ │ ├── page.tsx Dashboard (Board, Filter, Kennzahlen) - │ ├── projects/[id]/ Projektdetail: Beschreibung, Notizen, Git + │ ├── 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 - │ └── api/ REST-Endpunkte (Projekte, Notizen, Verbindungen, Sync) + │ ├── 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.ts Repo-Snapshot holen und speichern + │ ├── 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 ``` @@ -347,7 +363,8 @@ ausschließlich Daten des angemeldeten Kontos. | `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 die eigenen Notizen | +| `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 | @@ -359,10 +376,13 @@ ausschließlich Daten des angemeldeten Kontos. | `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 | @@ -434,17 +454,21 @@ Grenzen, die du kennen solltest: ## Getestet mit -Stand 25.07.2026 gegen echte Systeme verifiziert — beide Betriebsarten, alle drei -Git-Adapter (GitHub, GitLab und Gitea jeweils gegen die öffentliche Instanz). +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 | 26.5.0 | +| Node.js | 24.19.0 | | npm | 11.17.0 | | Next.js | 15.5.21 | | Prisma | 6.19.3 | -| PostgreSQL | 17 (Alpine) | -| Docker / Compose | 29.6.2 / v5.3.1 | +| PostgreSQL | 17.11 (Alpine) | +| Docker | 29.7.2 | ---