11 Commits
Author SHA1 Message Date
florian.wilde a2547b9022 Merge: Release v1.10.1
CI / pruefen (push) Canceled after 0s
2026-08-18 12:47:12 +00:00
florian.wilde fd633983b8 chore(release): v1.10.1
CI / pruefen (pull_request) Canceled after 0s
Zwei Aenderungen seit 1.10.0, beide bereits auf main: das Kennzeichen fuer
ein verknuepftes Git-Issue auf der Aufgabenkarte (#12) und die Token-Rechte,
Aufruf fuer Aufruf belegt (#10). Dazu die Korrektur am Hinweis unter dem
Token-Feld, der die Rechte fuer Actions nicht nannte - ohne die sieht bei
GitHub niemand einen CI-Zustand.

Verlauf und Versionsnummer wandern zusammen, sonst laufen sie auseinander;
changelog.test.ts haelt das fest. Die package-lock.json stand noch auf 1.2.0
und beschrieb damit ein Paket, das es so nie gab - mitgezogen.

Refs #10
Refs #12
2026-08-18 14:46:35 +02:00
florian.wilde 1b5f3fa1b2 Merge #12: Karte zeigt ein verknuepftes Git-Issue
CI / pruefen (push) Canceled after 0s
2026-08-18 12:39:33 +00:00
florian.wilde 6ee529576d feat(tasks): Karte zeigt, wenn ein Git-Issue daranhaengt
CI / pruefen (pull_request) Canceled after 0s
Die Verknuepfung stand bisher nur im Bearbeiten-Dialog und in der
projektuebergreifenden Uebersicht. Auf dem Brett eines Projekts - dort,
wo man die Aufgaben im Vorbeigehen sieht - war ihr nichts anzusehen.
Wer wissen wollte, welche Aufgabe schon ein Issue hat, musste jede
einzeln aufmachen.

Das Kennzeichen steht vorn in der Fusszeile der Karte, damit sich eine
Spalte danach ueberfliegen laesst, und traegt dasselbe Symbol wie der
Dialog: ein Sachverhalt, ein Zeichen.

Bewusst kein Link. Die Karte steckt in einem <button>, ein <a> darin
waere ungueltiges Markup; der Weg zum Issue fuehrt ueber den Dialog, der
ihn ohnehin anbietet. Die Nummer allein ergibt vorgelesen keinen Sinn,
deshalb steht "Verknuepft mit" als sr-only davor.

Der Fallstrick lag in der Bedingung darueber: die Fusszeile erschien nur
bei Beschreibung, Frist, Wiederholung oder Labels. Gerade eine aus einem
Issue erzeugte Aufgabe hat davon oft nichts - sie waere leer geblieben.

Abzudunkeln, sobald die Aufgabe erledigt ist, war versucht und wieder
verworfen: axe-core rechnete text-violet-300/50 bei 10px als
Kontrastverstoss ab.

Nachgesehen statt angenommen: Brett mit sechs Aufgaben (verknuepft,
unverknuepft, nackt, vollbepackt, erledigt, vierstellige Nummer) auf
1280 und 375 px - kein seitlicher Ueberlauf, axe-core ohne Verstoss.

Refs #12
2026-08-18 14:04:45 +02:00
florian.wilde 77567c06cd Merge #10: Token-Rechte vollstaendig dokumentiert
CI / pruefen (push) Canceled after 0s
2026-08-18 11:51:06 +00:00
florian.wilde e9f01ba8fa docs(git): jeden ausgehenden Aufruf einzeln mit seinem Recht belegen
CI / pruefen (pull_request) Canceled after 0s
Die Faelle A und B beantworteten die Frage auf Ebene der Scopes, nicht
auf Ebene der Aufrufe. Wer nachpruefen wollte, ob ein Recht wirklich
noetig ist, musste die drei Clients selbst lesen.

Jetzt steht in der README je System eine vollstaendige Tabelle: Aufruf,
Zweck, benoetigtes Recht. Die Zuordnung bei GitHub folgt der offiziellen
Berechtigungsuebersicht - dass /tags und /stats/commit_activity unter
Metadata fallen und nicht unter Contents, ist nicht zu erraten. Fuer
GitLab ist mit Quellenangabe belegt, dass es unterhalb von 'api' keinen
Schreibzugriff auf Issues gibt.

Ausserdem war offen, woher die Kommentare an den Issues stammen: Nebula
schreibt keine. Der Schreibzugriff besteht aus genau zwei Aufrufen. Ein
Test klopft dafuer die Clients ab - kaeme ein dritter dazu, wuerde die
Empfehlung 'nimm ein lesendes Token' still falsch, ohne dass jemand
einen Fehler saehe.

Der Hinweis am Eingabefeld nannte Actions nicht, obwohl der CI-Zustand
bei GitHub genau daran haengt.

Refs #10
2026-08-18 12:57:02 +02:00
florian.wilde 7920b6a20f Token-Rechte vollstaendig dokumentiert
CI / pruefen (pull_request) Canceled after 0s
Die Angaben stammten aus der Zeit, als Nebula nur gelesen hat. Seit v1.7.0
legt es Issues an und schliesst sie - wer sich an README oder den Hinweis im
Verbindungsdialog hielt, bekam dabei einen 403.

Erhoben wurde es nicht aus dem Gedaechtnis, sondern aus dem Code: jeder
ausgehende Aufruf in lib/git/gitea.ts, github.ts und gitlab.ts, abgebildet
auf das noetige Recht.

Aufgeschluesselt nach Funktion statt nach System, damit erkennbar ist, was
sich weglassen laesst: der Abgleich allein kommt rein lesend aus, nur die
Issue-Anbindung schreibt.

**GitLab ist der unangenehme Fall.** Es kennt keinen schmalen Schreib-Scope
fuer Issues - dort geht nur "api", also alles, was das Konto darf. Das steht
jetzt ausdruecklich da, samt der Alternative: bei read_api bleiben und auf
die Issue-Anbindung verzichten. Der Abgleich laeuft dann vollstaendig, und
geschlossene Issues werden weiterhin erkannt, nur nicht zurueckgeschrieben.

Ebenfalls festgehalten, was Nebula *nicht* braucht: Schreibrechte auf Code,
Adminrechte, Rechte zum Anlegen von Webhooks. Letzteres ist der Grund, warum
der Haken von Hand eingetragen wird - so bleibt der Token davon frei.

Der Hinweis in lib/git/meta.ts steht direkt am Eingabefeld und ist die
Stelle, die tatsaechlich gelesen wird; er ist deshalb mitgezogen. Dazu ein
Waechter in meta.test.ts: jeder Provider muss in GitClient createIssue und
setIssueState koennen, folglich muss auch jeder Hinweis sagen, was das an
Rechten kostet. Genau diese Kopplung war hier gerissen.

Geprueft: 267 Unit-Tests (6 neue), 36 E2E-Tests, Hinweistext je System gegen
die laufende Anwendung gelesen, axe-core auf 375px und 1280px ohne
Beanstandung.

Refs #10
2026-08-18 12:10:17 +02:00
florian.wilde 6c3f2d5bb6 Merge: Dokumentation auf Stand v1.10.0
CI / pruefen (push) Canceled after 0s
2026-08-18 06:23:04 +00:00
florian.wilde 3617222cc1 docs: alle Markdown-Dokumente auf Stand v1.10.0
CI / pruefen (pull_request) Canceled after 0s
Die Dokumente waren unterschiedlich weit zurueck: FEATURES bei v1.9.0,
README bei v1.8.0, DEPLOYMENT und CLAUDE.md noch bei v1.5.0.

**README.** Die Einleitung nannte weder die projektuebergreifenden Aufgaben
noch Rueckblick, Zeitleiste oder die Issue-Anbindung. In der API-Tabelle
fehlten vier Routen (/api/activity, /api/tasks/:id/issue,
/api/repositories/:id/webhook und der eingehende /api/webhooks/:id); die
Suche war noch als "ueber die eigenen Notizen" beschrieben. Die
Projektstruktur zeigte ein lib/sync.ts, das laengst ein Verzeichnis ist, und
kannte weder tasks/ noch rueckblick/ oder verlauf/.

Unter "Getestet mit" stand Node 26.5.0, tatsaechlich laeuft 24.19.0. Die
Tabelle nennt jetzt den Werkzeugstand mit Datum und trennt ihn von der
Aussage, wann zuletzt gegen echte Git-Systeme geprueft wurde - das war im
Juli und ist seitdem nicht wiederholt worden.

**FEATURES.** Neues Kapitel 2c zu Rueckblick und Zeitleiste, samt der beiden
Entscheidungen, die sonst niemand nachvollziehen kann: Kalendertage in fester
Zeitzone statt "7 x 24 Stunden", und "erledigt" aus dem meta-Feld statt aus
dem Anzeigetext. Das Suchkapitel deckt jetzt Aufgaben mit ab.

**DEPLOYMENT.** Der Abschnitt "Updates" sprang bisher direkt ins Skript,
ohne zu erklaeren, was ein Update ueberhaupt ist - genau daran ist die Frage
"was meinst du mit Deploy?" entstanden. Jetzt steht vorneweg, dass Git und
laufende Instanz zwei Orte sind und woran man sieht, welche Fassung laeuft.
Dazu ein Abschnitt zu Webhooks (setzt Erreichbarkeit voraus, ohne sie geht
aber nichts verloren) und die Sitzungsfristen in der Haertungsliste.

**CLAUDE.md.** Sechs Fallstricke aus den letzten Runden, die sonst beim
naechsten Mal erneut Zeit kosten - allen voran "min-width: auto" bei Flex-
und Grid-Kindern, das hier zweimal zugeschlagen hat. Ausserdem: dass die
feste Zeitzone auch beim Rechnen gilt, dass Dialoge der Bildschirmtastatur
ausweichen muessen, dass ein Ausdrucksindex nur bei Zeichengleichheit
greift (mit EXPLAIN nachsehen), dass ein leeres Migrationsverzeichnis
"migrate diff" abbrechen laesst - und dass eine Ueberlaufmessung gegen
unvollstaendige Testdaten faelschlich gruen meldet.

Geprueft: Querverweise zwischen den Dokumenten aufgeloest, alle in README
und DEPLOYMENT genannten Umgebungsvariablen gegen Code und .env.example
gehalten, API-Tabelle gegen die tatsaechlich vorhandenen Routen.
2026-08-18 08:20:57 +02:00
florian.wilde 43a4c70053 docs(roadmap): Stand auf v1.10.0 gebracht
CI / pruefen (pull_request) Canceled after 0s
Zuletzt angefasst bei v1.7.0. Seitdem sind drei Fassungen und vier Issues
durchgelaufen, ohne dass die Roadmap es mitbekommen haette.

Erledigt und deshalb aus den offenen Abschnitten entfernt:
Wochenrueckblick, globale Zeitleiste und die Suche ueber Aufgaben - alle
drei mit v1.10.0. Webhooks standen noch als Vorschlag drin, obwohl sie seit
v1.7.0 laufen. In der Erledigt-Tabelle fehlten v1.8.0 bis v1.10.0.

Die Nummerierung war ausserdem lueckenhaft: 1.0, dann 1.2 - der Punkt 1.1
war beim Umsetzen herausgeloescht worden. Jetzt wieder fortlaufend, und
1.0 (Rest aus v1.6.0) ist mit 1.3 zusammengefasst, weil beide dasselbe
meinten.

Neu ist ein Abschnitt "Aufgefallen beim Bauen": fuenf Beobachtungen aus der
Arbeit an v1.6.0 bis v1.10.0, die sonst nur in Issue-Kommentaren und im
Chat gestanden haetten - der abgeschnittene Anwendungsname auf schmalen
Anzeigen, die fehlenden Indizes fuer die inzwischen vier
projektuebergreifenden Seiten, der fehlende Projektfilter auf der
Zeitleiste, der offene ASVS-Punkt zu geleakten Passwoertern und die zwei
Fassungen ohne Release-Anker.

Nichts davon ist dringend; festgehalten, damit es nicht verloren geht.
2026-08-18 08:10:32 +02:00
florian.wilde 65a31b7cc9 Merge: v1.10.0
CI / pruefen (push) Canceled after 0s
2026-08-18 05:49:55 +00:00
13 changed files with 701 additions and 69 deletions
+55 -3
View File
@@ -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 <db> -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
+42 -1
View File
@@ -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/<repositoryId>` 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.
+97 -6
View File
@@ -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.
@@ -250,6 +255,10 @@ Blockiert · Erledigt**.
Fälligkeitsdatum und Labels.
- **Fälligkeiten** färben sich mit: überfällig rot, heute gelb, in den nächsten Tagen
gedämpft.
- **Hängt ein Git-Issue daran**, trägt die Karte dessen Nummer — vorn in der Fußzeile,
vor Beschreibung, Wiederholung, Frist und Labels, damit sich eine Spalte danach
überfliegen lässt. Bewusst kein Link: der Weg zum Issue führt über den Dialog (4.6),
weil ein `<a>` innerhalb der anklickbaren Karte ungültiges Markup wäre.
- Erledigtes wird durchgestrichen; der Zeitpunkt wird festgehalten und beim Zurückziehen
wieder gelöscht.
@@ -341,6 +350,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.
@@ -432,6 +489,40 @@ Firmen-Gitea nebeneinander.
Kurzbeschreibung.
- **Löschen** trennt nur die Verknüpfung; die Projekte selbst bleiben erhalten.
#### Nötige Token-Rechte
Nebula liest fast alles nur. Geschrieben wird an **genau zwei Stellen**: `POST …/issues`
beim Anlegen und `PATCH`/`PUT …/issues/{nr}` beim Schließen oder Wiederöffnen (4.6). Wer
darauf verzichtet, kommt mit einem rein lesenden Token aus.
| System | Nur Abgleich | Zusätzlich für die Issue-Anbindung |
| --- | --- | --- |
| GitHub | *Metadata*, *Contents*, *Issues*, *Pull requests*, *Actions* — je lesend | *Issues: Read and write* |
| GitLab | `read_api` | `api` |
| Gitea / Forgejo | `read:repository`, `read:issue`, `read:user` | `write:issue` |
Welcher Aufruf welches Recht verlangt, steht Zeile für Zeile in der
[README](README.md#welche-rechte-der-token-braucht) — dort ist jeder ausgehende Aufruf
der drei Clients aufgeführt.
**GitLab kennt keinen schmalen Schreib-Scope.** `api` erlaubt alles, was das Konto darf,
Code eingeschlossen. Wem das zu weit geht, bleibt bei `read_api`: der Abgleich läuft
vollständig, geschlossene Issues werden weiterhin erkannt — nur schreibt Nebula dann
nicht zurück.
**Nicht verlangt** werden Schreibrechte auf Code, Adminrechte und Rechte zum Anlegen von
Webhooks. Der Haken wird von Hand im Git-System eingetragen (4.7); das erspart dem Token
genau die Administrationsrechte, die er sonst bräuchte.
**Kommentare schreibt Nebula nie** — weder beim Anlegen noch beim Schließen. In den
Rumpf des angelegten Issues wandert die Aufgabenbeschreibung plus eine Zeile Rückverweis
auf das Projekt; danach bleibt der Text unangetastet. Was sonst an Kommentaren und
Labels an einem Issue hängt, kommt aus der Weboberfläche oder von einem anderen Werkzeug.
Ein fehlendes Recht endet in einem `403` mit „Token hat nicht die noetigen Scopes",
sichtbar am Repository auf der Projektseite. Klein anfangen und gezielt nachlegen ist
deshalb ohne Risiko.
### 4.3 Was beim Sync geholt wird
| Bereich | Details |
+161 -17
View File
@@ -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
@@ -167,14 +173,8 @@ Nebula auffindbare Credentials nutzt.
## Git-Verbindung einrichten
1. Im Git-System einen **Personal Access Token** mit Leserechten erzeugen:
| System | Scopes |
| --- | --- |
| GitHub | Fine-grained Token: *Contents: Read*, *Metadata: Read*, *Issues: Read*, *Pull requests: Read* |
| GitLab | `read_api` (alternativ `read_repository`) |
| Gitea / Forgejo | `repository:read`, `issue:read`, `user:read` |
1. Im Git-System einen **Personal Access Token** erzeugen — mit den Rechten aus dem
Abschnitt [Welche Rechte der Token braucht](#welche-rechte-der-token-braucht).
2. 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.
3. Mit **Testen** prüfen — Nebula meldet den erkannten Benutzer zurück.
@@ -186,6 +186,132 @@ Nebula auffindbare Credentials nutzt.
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](https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens).
**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](https://docs.gitlab.com/security/tokens/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
@@ -275,15 +401,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 +483,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 +496,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 +574,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 |
---
+75 -34
View File
@@ -7,25 +7,21 @@ Stück für Stück abarbeiten. Reihenfolge ist ein Vorschlag, keine Festlegung.
Was bereits steht, listet [FEATURES.md](FEATURES.md).
**Stand:** v1.10.1. Alle Issues aus dem Dashboard sind abgearbeitet.
---
## 1 · Als Nächstes empfohlen
### 1.0 Rest aus v1.6.0: persönliche API-Token `M`
### 1.1 Persönliche API-Token `M`
Schnellerfassung und die projektübergreifende Aufgabenseite sind mit v1.6.0
ausgeliefert. Offen bleibt der dritte Teil — siehe 1.3. Er gehört sachlich
dazu: erfassen am Telefon, überblicken an einer Stelle, ansprechen per
Shortcut.
Der letzte offene Teil aus v1.6.0 („unterwegs erfassen"). Derzeit geht die
API nur mit Sitzungscookie. Ein Token je Konto würde `curl`-Aufrufe und
Handy-Shortcuts erlauben — eine Idee aus dem Terminal anlegen, ohne den
Browser zu öffnen. Nur der Hash in der Datenbank, wie bei den Sitzungen.
Bewusst nicht mitgeliefert wurde ein **Index für die übergreifende
Aufgabenabfrage**. `Task` hat kein `ownerId`, die Trennung läuft über
`project: { ownerId }`, und `[projectId, status, position]` greift dafür
nicht. Bei einigen hundert Aufgaben fällt das nicht auf; wird die Seite
träge, ist ein Index auf `dueDate` der erste Griff — und dann eine
Migration wert.
---
Bewusst noch nicht angefasst, weil sicherheitsrelevant: Geltungsbereich,
Widerruf und Ablauf wollen entschieden sein, nicht nebenbei gebaut.
### 1.2 Läuft es noch? `M`
@@ -34,24 +30,17 @@ Nebula wüsste dann, ob ein Projekt nicht nur existiert, sondern auch läuft.
Nutzt Zeitgeber und Benachrichtigungen, die es schon gibt. Der SSRF-Schutz
greift dabei wie bei den Git-Aufrufen.
### 1.3 Persönliche API-Token `M`
### 1.3 Aufgaben ↔ Issues: der Rest `S`
Derzeit geht die API nur mit Sitzungscookie. Ein Token je Konto würde
`curl`-Aufrufe und Handy-Shortcuts erlauben — Idee aus dem Terminal anlegen,
ohne den Browser zu öffnen. Nur der Hash in der Datenbank, wie bei den
Sitzungen.
---
### 1.4 Aufgaben ↔ Issues: was noch fehlt `S`
Mit v1.7.0 stehen beide Richtungen: „als Issue anlegen“ und der Rückweg über
Webhooks. Offen geblieben ist Kleinkram:
Beide Richtungen stehen — „als Issue anlegen" seit v1.7.0, der Rückweg über
Webhook (v1.7.0) und zusätzlich über den Abgleich (v1.9.0). Offen ist
Kleinkram:
- **Mehrere Repositories.** Der Knopf nimmt immer das führende. Bei einem
Projekt aus Frontend und Backend fehlt die Wahl.
- **Beim Übernehmen aus Issues** wird der Rückweg nicht automatisch
eingerichtet — der Webhook ist getrennt zu setzen.
- **Webhook beim Verknüpfen anbieten.** Er ist getrennt einzurichten, und
wer das nicht tut, merkt nur an der Verzögerung, dass etwas fehlt. Ein
Hinweis an der richtigen Stelle würde reichen.
Was **nicht** kommt, bleibt wie besprochen: eine echte beidseitige
Synchronisation mit Konfliktauflösung. Die Zustände passen ohnehin nicht
@@ -75,15 +64,60 @@ Voraussetzung, falls du das Projekt veröffentlichen willst.
---
## 3 · Kleinigkeiten
## 3 · Aufgefallen beim Bauen
Beobachtungen aus der Arbeit an v1.6.0 bis v1.10.1 — festgehalten, damit sie
nicht verloren gehen. Nichts davon ist dringend.
### 3.1 Anwendungsname bricht in der Kopfleiste ab `S`
Zwischen 320 und 375 px wird aus „Nebula" ein „Neb…". Gemessen über fünf
Breiten und zwei Seiten: betrifft alle Seiten gleichermaßen. Seit die
Aufgaben als dritter Punkt danebenstehen, ist es enger geworden.
### 3.2 Indizes für die projektübergreifenden Seiten `S`
Weder `Task` noch `Activity` haben einen Besitzer — die Trennung läuft über
`project: { ownerId }`. Die vorhandenen Indizes
(`[projectId, status, position]` bzw. `[projectId, createdAt]`) greifen für
Abfragen über alle Projekte deshalb nicht.
Betroffen sind inzwischen vier Seiten: Aufgabenübersicht, Wochenrückblick,
Zeitleiste und die Volltextsuche. Bei einigen hundert Datensätzen fällt das
nicht auf. Wird eine der Seiten träge, sind Indizes auf `Task.dueDate` und
`Activity.createdAt` der erste Griff — und dann eine Migration wert.
### 3.3 Projektfilter auf der Zeitleiste `S`
Über alle Projekte wird die Liste schnell lang. Der Wochenrückblick hat
Wochengrenzen als natürliche Bremse, die Zeitleiste nicht. Bewusst
weggelassen, weil das Issue nur „derselbe Verlauf, projektübergreifend"
verlangte.
### 3.4 Abgleich gegen geleakte Passwörter `M`
ASVS 2.1.7 verlangt es; umgesetzt ist bisher nur eine lokale Sperrliste plus
Mustererkennung (Wiederholungen, Zeichenfolgen, Benutzername im Passwort).
Have-I-Been-Pwned scheidet aus — Nebula ruft grundsätzlich nichts von außen,
und ein Passwort-Präfix nach draußen zu geben widerspräche dem erst recht.
Eine lokale Top-10000-Liste als Datei wäre der Kompromiss.
### 3.5 v0.2.0 und v0.3.0 ohne Release `S`
Beide stehen im Änderungsverlauf, existierten aber nie in der
`package.json` — dort springt die Historie von 0.1.0 auf 0.4.0. Nachgetragen
wurden sie in dem Commit, der den Verlauf einführte, und der liegt *nach*
0.4.0. Es gibt also keinen ehrlichen Commit, an dem ein Tag hängen könnte.
Die übrigen 18 Fassungen haben seit dem 18.08.2026 ein Release in Gitea.
---
## 4 · Kleinigkeiten
| Idee | Aufwand | Nutzen |
| --- | --- | --- |
| Wochenrückblick | `S` | „Was ist diese Woche passiert" über alle Projekte, aus dem Aktivitätsverlauf |
| Globale Zeitleiste | `S` | Derselbe Verlauf, aber projektübergreifend als eigene Seite |
| Suche über Aufgaben | `S` | Die Volltextsuche deckt bisher nur Notizen ab |
| Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste |
| Aufgabe ↔ Commit verknüpfen | `M` | Enthält ein Commit `#5`, wird die übernommene Aufgabe verlinkt — fällt weitgehend mit 1.4 ab |
| Aufgabe ↔ Commit verknüpfen | `M` | Enthält ein Commit `#5`, wird die übernommene Aufgabe verlinkt — fällt weitgehend mit 1.3 ab |
| Tastaturkürzel-Übersicht mit `?` | `S` | Es gibt inzwischen genug davon |
| Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel |
| Eigenes Logo und Farbe | `S` | Ergänzt `APP_NAME` |
@@ -91,7 +125,7 @@ Voraussetzung, falls du das Projekt veröffentlichen willst.
---
## 4 · Bewusst nicht
## 5 · Bewusst nicht
Damit klar ist, was **nicht** kommt und warum:
@@ -104,7 +138,10 @@ Damit klar ist, was **nicht** kommt und warum:
- **Anhänge an Notizen.** Bräuchte Dateispeicher und damit eine zweite Sorge
bei jeder Sicherung. Ein Link ins Git tut es auch.
- **Eigene Git-Operationen** (klonen, committen, pushen). Nebula ist eine
Übersicht, keine Entwicklungsumgebung.
Übersicht, keine Entwicklungsumgebung. Issues über die HTTP-API der
Provider sind davon nicht berührt — die gibt es seit v1.7.0.
- **Beidseitige Synchronisation von Aufgaben und Issues** mit
Konfliktauflösung — siehe 1.3.
- **Zeiterfassung, Rechnungen, Kundenverwaltung.** Andere Baustelle.
- **Cloud-Synchronisation.** Die Daten bleiben in deiner Datenbank; das ist
der Punkt.
@@ -138,6 +175,10 @@ Damit klar ist, was **nicht** kommt und warum:
| **v1.5.0** Sicherheitsdurchsicht, Server-Anleitung und Update-Skript | ✓ |
| **v1.6.0** Mobile Ansicht, Listenansicht, Schnellerfassung, Aufgaben über alle Projekte | ✓ |
| **v1.7.0** Aufgabe als Git-Issue anlegen, Webhooks für Push und Issues | ✓ |
| **v1.8.0** Anmeldung und Sitzungen nach OWASP: scrypt-Aufwand, Aufzählbarkeit, Leerlauffrist, Cookie-Härtung | ✓ |
| **v1.9.0** Geschlossene Issues haken ihre Aufgabe auch ohne Webhook ab | ✓ |
| **v1.10.0** Wochenrückblick, globale Zeitleiste, Suche über Aufgaben | ✓ |
| **v1.10.1** Issue-Kennzeichen auf der Aufgabenkarte, Token-Rechte Aufruf für Aufruf belegt | ✓ |
Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur
beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "nebula",
"version": "1.2.0",
"version": "1.10.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nebula",
"version": "1.2.0",
"version": "1.10.1",
"hasInstallScript": true,
"dependencies": {
"@dnd-kit/core": "^6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nebula",
"version": "1.10.0",
"version": "1.10.1",
"private": true,
"description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)",
"scripts": {
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import type { Task } from "@prisma/client";
import { TaskCard } from "./TaskCard";
/**
* Die Karte ist die einzige Stelle im Projekt, an der eine Aufgabe im
* Vorbeigehen zu sehen ist – der Bearbeiten-Dialog verlangt einen Klick.
* Steht die Verknuepfung nur dort, weiss niemand vorher, welche Aufgabe
* ein Issue hat.
*
* Serverseitig gerendert wie bei Markdown.test.tsx: es geht um das
* erzeugte Markup, nicht um Verhalten im Browser.
*/
function aufgabe(felder: Partial<Task> = {}): Task {
return {
id: "t1",
projectId: "p1",
title: "Etwas tun",
description: null,
status: "TODO",
position: 0,
dueDate: null,
labels: [],
recurrence: null,
sourceIssue: null,
sourceUrl: null,
sourceRepositoryId: null,
createdAt: new Date("2026-08-18T10:00:00Z"),
updatedAt: new Date("2026-08-18T10:00:00Z"),
...felder,
} as Task;
}
const markup = (task: Task) => renderToStaticMarkup(<TaskCard task={task} />);
describe("Kennzeichen fuer ein verknuepftes Issue", () => {
it("zeigt die Nummer, sobald eine Aufgabe an einem Issue haengt", () => {
const html = markup(aufgabe({ sourceIssue: 42 }));
expect(html).toContain("#42");
expect(html).toContain("Verkn");
});
it("erscheint auch ohne Beschreibung, Datum und Labels", () => {
// Der eigentliche Fallstrick: die Fusszeile der Karte wird nur
// gerendert, wenn es etwas zu zeigen gibt. Faellt die Verknuepfung aus
// dieser Bedingung heraus, bleibt genau die nackte Aufgabe unbeschriftet
// – und das ist der haeufige Fall bei einer aus einem Issue erzeugten.
const html = markup(
aufgabe({ sourceIssue: 7, description: null, dueDate: null, labels: [], recurrence: null }),
);
expect(html).toContain("#7");
});
it("bleibt weg, wenn keine Verknuepfung besteht", () => {
const html = markup(aufgabe({ description: "Text" }));
expect(html).not.toContain("Verkn");
});
it("bleibt auf einer erledigten Aufgabe sichtbar", () => {
// Die erledigte Karte streicht ihren Titel durch; das Kennzeichen bleibt
// in voller Helligkeit. Abzudunkeln war versucht und wieder verworfen –
// axe-core rechnete es als Kontrastverstoss ab (10px Text, 3,2:1).
// Dass hier ein Issue mitgeschlossen wurde, ist ohnehin gerade dann die
// interessante Auskunft.
const html = markup(aufgabe({ sourceIssue: 5, status: "DONE" }));
expect(html).toContain("#5");
});
it("baut keinen Link in die Karte", () => {
// Die Karte steckt in einem <button>; ein <a> darin waere ungueltiges
// Markup und im Browser nicht verlaesslich anklickbar.
const html = markup(aufgabe({ sourceIssue: 42, sourceUrl: "https://example.com/issues/42" }));
expect(html).not.toContain("<a ");
});
it("nennt die Verknuepfung auch fuer Vorlesesoftware", () => {
// "#42" allein ergibt vorgelesen keinen Sinn.
const html = markup(aufgabe({ sourceIssue: 42 }));
expect(html).toMatch(/sr-only[^>]*>Verkn/);
});
});
+26 -2
View File
@@ -1,6 +1,12 @@
"use client";
import { AlignLeft, CalendarClock, GripVertical, Repeat } from "lucide-react";
import {
AlignLeft,
CalendarClock,
GitPullRequestArrow,
GripVertical,
Repeat,
} from "lucide-react";
import type { Task } from "@prisma/client";
import { RECURRENCE_META } from "@/lib/tasks";
import { cn, formatDate } from "@/lib/utils";
@@ -70,8 +76,26 @@ export function TaskCard({
{task.title}
</p>
{(task.description || task.dueDate || task.recurrence || task.labels.length > 0) && (
{(task.description ||
task.dueDate ||
task.recurrence ||
task.sourceIssue ||
task.labels.length > 0) && (
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px]">
{/* Steht vorn, damit sich eine Spalte danach ueberfliegen laesst.
Bewusst kein Link: die Karte steckt in einem <button>, und ein
<a> darin waere ungueltig. Der Weg zum Issue fuehrt ueber den
Bearbeiten-Dialog, der ihn ohnehin anbietet. */}
{task.sourceIssue ? (
<span
className="inline-flex items-center gap-1 text-violet-300"
title={`Verknüpft mit Issue #${task.sourceIssue}`}
>
<GitPullRequestArrow className="h-2.5 w-2.5" aria-hidden />
<span className="sr-only">Verknüpft mit </span>#{task.sourceIssue}
</span>
) : null}
{task.description ? (
<AlignLeft className="h-2.5 w-2.5 text-slate-600" aria-label="hat Beschreibung" />
) : null}
+31
View File
@@ -41,6 +41,37 @@ export const KIND_META: Record<ChangeKind, { label: string; className: string }>
};
export const CHANGELOG: Release[] = [
{
version: "1.10.1",
date: "2026-08-18",
headline: "Sichtbar, welche Aufgabe ein Issue hat — und nachlesbar, was ein Token dafür braucht.",
changes: [
{
kind: "neu",
text:
"Auf dem Aufgabenbrett eines Projekts trägt eine Karte die Nummer ihres " +
"verknüpften Git-Issues. Bisher stand die Verknüpfung nur im Bearbeiten-Dialog " +
"und in der projektübergreifenden Übersicht — welche Aufgabe schon ein Issue " +
"hat, war im Vorbeigehen nicht zu sehen.",
},
{
kind: "besser",
text:
"Die Rechte einer Git-Verbindung stehen jetzt Aufruf für Aufruf in der README: " +
"je System eine Tabelle aus Endpunkt, Zweck und benötigtem Recht. Getrennt nach " +
"reinem Abgleich und zusätzlicher Issue-Anbindung, damit erkennbar ist, was sich " +
"weglassen lässt. Bei GitLab ist belegt, dass es unterhalb von „api“ keinen " +
"Schreibzugriff auf Issues gibt.",
},
{
kind: "behoben",
text:
"Der Hinweis unter dem Token-Feld nannte die Rechte für Actions nicht, obwohl " +
"der CI-Zustand bei GitHub genau daran hängt. Wer sich daran hielt, sah keinen " +
"Build-Status.",
},
],
},
{
version: "1.10.0",
date: "2026-08-18",
+60
View File
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { PROVIDER_META, guessProvider } from "./meta";
const PROVIDER = ["GITHUB", "GITLAB", "GITEA"] as const;
describe("PROVIDER_META", () => {
it("kennt jedes unterstuetzte System", () => {
for (const provider of PROVIDER) {
expect(PROVIDER_META[provider], provider).toBeDefined();
}
// Und nichts darueber hinaus – ein Eintrag ohne Gegenstueck im Schema
// waere ein Ueberbleibsel.
expect(Object.keys(PROVIDER_META).sort()).toEqual([...PROVIDER].sort());
});
it("hat zu jedem System Beschriftung, Basis-URL und Dokuverweis", () => {
for (const provider of PROVIDER) {
const meta = PROVIDER_META[provider];
expect(meta.label.length, provider).toBeGreaterThan(0);
expect(meta.defaultBaseUrl, provider).toMatch(/^https:\/\//);
expect(meta.docsUrl, provider).toMatch(/^https:\/\//);
}
});
/**
* Der eigentliche Waechter.
*
* Der Hinweis am Eingabefeld war eine Zeit lang rein lesend formuliert,
* obwohl Nebula seit v1.7.0 Issues anlegt und schliesst – wer sich daran
* hielt, bekam einen 403. Jeder Provider in GitClient muss createIssue und
* setIssueState koennen; folglich muss auch jeder Hinweis sagen, was das
* an Rechten kostet.
*/
it("sagt bei jedem System etwas ueber die Rechte fuer Issues", () => {
for (const provider of PROVIDER) {
expect(PROVIDER_META[provider].tokenHelp, provider).toMatch(/issue/i);
}
});
it("nennt einen konkreten Scope statt nur 'Leserechte'", () => {
// GitLab nennt Scopes in Anfuehrungszeichen, GitHub und Gitea
// Berechtigungsnamen – gemeinsam ist, dass etwas Nachschlagbares dasteht.
expect(PROVIDER_META.GITLAB.tokenHelp).toMatch(/read_api/);
expect(PROVIDER_META.GITEA.tokenHelp).toMatch(/read:repository/);
expect(PROVIDER_META.GITHUB.tokenHelp).toMatch(/Metadata/);
});
});
describe("guessProvider", () => {
it("erkennt die oeffentlichen Instanzen am Hostnamen", () => {
expect(guessProvider("github.com")).toBe("GITHUB");
expect(guessProvider("gitlab.com")).toBe("GITLAB");
});
it("faellt bei allem anderen auf Gitea zurueck", () => {
// Selbst gehostet ist hier der haeufigste Fall, und Gitea die
// wahrscheinlichste Wahl dafuer.
expect(guessProvider("git.wilde-server.de")).toBe("GITEA");
});
});
+9 -3
View File
@@ -12,19 +12,25 @@ export const PROVIDER_META: Record<
label: "GitHub",
defaultBaseUrl: "https://github.com",
tokenHelp:
"Fine-grained Token mit Lesezugriff auf Contents, Metadata, Issues und Pull Requests.",
"Fine-grained Token, alle nur lesend: Metadata, Contents, Issues, Pull requests " +
"und Actions. Nur wenn Aufgaben als Issue angelegt werden sollen, Issues " +
"zusaetzlich schreiben.",
docsUrl: "https://github.com/settings/tokens",
},
GITLAB: {
label: "GitLab",
defaultBaseUrl: "https://gitlab.com",
tokenHelp: "Personal Access Token mit Scope 'read_api' (bzw. 'read_repository').",
tokenHelp:
"Scope 'read_api' genuegt fuer den Abgleich. Aufgaben als Issue anzulegen verlangt " +
"'api' – GitLab kennt keinen schmaleren Schreibzugriff.",
docsUrl: "https://gitlab.com/-/user_settings/personal_access_tokens",
},
GITEA: {
label: "Gitea / Forgejo",
defaultBaseUrl: "https://git.example.com",
tokenHelp: "Access Token mit Leserechten auf repository, issue und user.",
tokenHelp:
"read:repository, read:issue und read:user. Nur wenn Aufgaben als Issue angelegt " +
"werden sollen, zusaetzlich write:issue.",
docsUrl: "https://docs.gitea.com/development/api-usage",
},
};
+60
View File
@@ -0,0 +1,60 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
/**
* Waechter ueber eine Zusage in der Dokumentation.
*
* README und FEATURES sagen zu, dass Nebula an genau zwei Stellen nach
* draussen schreibt: ein Issue anlegen und seinen Zustand umsetzen. Auf
* dieser Zusage steht die Empfehlung, ein rein lesendes Token zu vergeben,
* solange die Issue-Anbindung nicht gebraucht wird.
*
* Kaeme ein dritter Schreibaufruf dazu, wuerde die Empfehlung still falsch:
* niemand bekaeme einen Fehler, nur ein 403 an einer Stelle, die laut
* Dokumentation gar nicht schreibt. Deshalb wird hier der Quelltext selbst
* abgeklopft – sendJson ist der einzige Weg aus lib/git heraus, der eine
* andere Methode als GET benutzt (siehe http.ts).
*/
const CLIENTS = ["gitea.ts", "github.ts", "gitlab.ts"] as const;
/** Die Zeile ueber dem Methoden-Argument nennt den Endpunkt. */
function schreibaufrufe(quelltext: string): string[] {
const zeilen = quelltext.split("\n");
const gefunden: string[] = [];
zeilen.forEach((zeile, i) => {
if (!/\bsendJson[<(]/.test(zeile)) return;
// sendJson(url, METHODE, …) – Endpunkt und Methode stehen darunter.
const block = zeilen.slice(i, i + 4).join(" ");
const methode = block.match(/"(POST|PATCH|PUT)"/)?.[1];
const pfad = block.match(/\/issues[^`"']*/)?.[0] ?? "?";
gefunden.push(`${methode ?? "?"} ${pfad}`);
});
return gefunden;
}
describe("Schreibzugriffe nach draussen", () => {
it("beschraenken sich auf Issues anlegen und Zustand setzen", () => {
for (const datei of CLIENTS) {
const quelltext = readFileSync(join(__dirname, datei), "utf8");
const aufrufe = schreibaufrufe(quelltext);
expect(aufrufe.length, `${datei}: Anzahl Schreibaufrufe`).toBe(2);
for (const aufruf of aufrufe) {
expect(aufruf, `${datei}: ${aufruf}`).toMatch(/^(POST|PATCH|PUT) \/issues/);
}
// Genau einer legt an, genau einer setzt um.
expect(aufrufe.filter((a) => a.startsWith("POST")).length, datei).toBe(1);
expect(aufrufe.filter((a) => /^(PATCH|PUT)/.test(a)).length, datei).toBe(1);
}
});
it("laufen ueber sendJson, nicht an http.ts vorbei", () => {
// Ein direkter fetch in einem Client haette weder SSRF-Schutz noch
// Zeitbegrenzung – und wuerde am Waechter oben vorbeilaufen.
for (const datei of CLIENTS) {
const quelltext = readFileSync(join(__dirname, datei), "utf8");
expect(quelltext, datei).not.toMatch(/\bfetch\s*\(/);
}
});
});