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
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
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
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.
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.
Seit v1.9.0 sind drei Features nach main gegangen, ohne dass die
Versionsnummer mitgezogen waere: Wochenrueckblick (#2), globale Zeitleiste
(#3) und die Suche ueber Aufgaben (#4).
Genau dieselbe Luecke wie vor v1.6.0. Der Test in changelog.test.ts prueft,
dass die oberste Fassung im Verlauf zur package.json passt - er greift aber
nicht, wenn beide gemeinsam zurueckhaengen. Ein Test, der das faende,
muesste den Verlauf gegen die Git-Historie halten; das waere mehr Maschinerie
als der Fehler wert, solange die Freigabe ohnehin von Hand laeuft.
Der Verlauf wird in der Anwendung angezeigt (Knopf an der Versionsnummer in
der Fusszeile), deshalb steht dort jetzt, was dazugekommen ist. Nachgesehen
auf 375px und 1280px: Eintraege vollstaendig, keine Beanstandung von
axe-core.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Luecken, die im Issue zusammen stehen.
**Im Aufgabendashboard fehlte ein Suchfeld.** Es gab Filter nach Status und
Projekt, aber keine Moeglichkeit, nach einem Wort zu suchen. Das Suchfeld
deckt Titel, Beschreibung, Labels und Projektname ab. Gefiltert wird im
Browser - die Aufgaben liegen dort ohnehin alle vor, es braucht also keine
zusaetzliche Abfrage und es gibt keine Verzoegerung beim Tippen.
Wer sucht, sucht dabei auch in Erledigtem: sonst faende man die Aufgabe von
letzter Woche nicht wieder, obwohl man genau sie meint. Der Schalter
"Erledigte einblenden" gilt nur ohne Suchbegriff.
**Die Volltextsuche kannte nur Notizen.** Task ist jetzt ebenfalls dabei,
nach demselben Muster: deutsche Textsuche, ts_headline fuer den Ausschnitt,
Hervorhebung als [[…]] statt als HTML. Deutsche Stammformen greifen damit
auch hier - "Rechnung" findet "Rechnungen".
Dazu ein Ausdrucksindex Task_fulltext_idx wie Note_fulltext_idx. Der
Ausdruck muss Zeichen fuer Zeichen dem in der Abfrage entsprechen, sonst
greift er nicht; nachgeprueft mit EXPLAIN, der Plan zeigt einen
Bitmap Index Scan darauf. "prisma migrate diff" laesst ihn in Ruhe -
Gegenprobe kommt leer heraus.
Das Antwortfeld heisst weiterhin "hits" und meint die Notizen. Umbenennen
haette nur Aufrufer gebrochen, ohne etwas zu verbessern; Aufgaben kommen als
eigenes Feld dazu. Beide Stellen, die Treffer anzeigen - Projektbrett und
Schnellsuche -, zeigen sie jetzt.
Geprueft: 261 Unit-Tests, 36 E2E-Tests. Die Volltextsuche zusaetzlich gegen
die laufende Anwendung: Treffer in Titel und Beschreibung, Erledigtes
inbegriffen, Stammformen greifen, fremde Konten finden nichts.
Refs #4
Eine Seite /verlauf, die den Aktivitaetsverlauf fortlaufend ueber alle
Projekte zeigt - ohne Wochengrenze, nach Kalendertagen gegliedert, mit
Nachladen aelterer Eintraege. Erste Seite serverseitig, alles Weitere ueber
/api/activity.
Geblaettert wird per Cursor ueber (createdAt, id), nicht per skip. Der
Verlauf schreibt mehrere Eintraege im selben Augenblick - beim Uebernehmen
von Issues entstehen zehn Zeilen mit identischem Zeitstempel. Ein Offset
ueber eine solche Grenze hinweg ueberspringt Zeilen oder zeigt sie zweimal,
und zwar lautlos. Der zweite Zweig der Bedingung (gleicher Zeitpunkt,
kleinere ID) faengt genau das ab.
Geprueft gegen 144 Eintraege mit absichtlichen Bloecken gleicher Zeitstempel:
15 Seiten, 144 Zeilen, keine Dublette, streng absteigend.
Die Bedingung selbst steht als pruefbare Funktion in lib/timeline, samt
Deckelung der Seitengroesse und Abweisen unbrauchbarer Cursor - eine ID aus
der Abfragezeichenkette geht nicht ungeprueft in die Abfrage.
Damit Rueckblick und Zeitleiste wirklich gleich aussehen, sind Zeile und
Tagesgruppen nach components/ActivityRow gewandert. Bewusst ohne "use
client" und ohne Hooks, damit beides sowohl aus der Server-Komponente
(Rueckblick) als auch aus der Client-Komponente (Zeitleiste) heraus
benutzbar ist. Die Buendelung im Browser ist unbedenklich, weil
tagesschluessel eine feste Zeitzone benutzt - Server und Browser kommen auf
denselben Kalendertag.
Erreichbar ueber die Schnellsuche und vom Wochenrueckblick aus.
Geprueft: 9 neue Unit-Tests zur Cursor-Logik, ein E2E-Test, der die ganze
Liste in Dreierseiten durchblaettert und gegen einen einzelnen grossen
Aufruf haelt, ein E2E-Test fuer die Seite, axe-core auf 375px und 1280px
je vor und nach dem Nachladen ohne Beanstandung.
Refs #3
Eine Seite, die aus dem vorhandenen Aktivitaetsverlauf zusammenfasst, was in
einer Woche ueber alle Projekte hinweg passiert ist: Kennzahlen oben, darunter
die Vorgaenge nach Kalendertagen gebuendelt, neuester Tag zuerst. Blaettern in
frueher Wochen ueber ?woche=-n.
Der Verlauf kennt keinen Besitzer - die Trennung laeuft ueber das Projekt,
"project: { ownerId }" steht als erste Bedingung.
Zwei Dinge, die sonst still falsch werden:
- **Wochengrenzen** stehen als pruefbare Funktion in lib/review, nicht in der
Seite. Gerechnet wird ueber Berliner Kalendertage, nicht ueber "sieben mal
24 Stunden": liegt eine Zeitumstellung in der Woche, hat sie 167 oder 169
Stunden. Und im Container laeuft die Anwendung nach UTC, ein Eintrag von
Sonntagabend fiele sonst in die falsche Woche.
- **"Aufgaben erledigt"** wird am meta-Feld abgelesen (to: "DONE") und nicht
am Anzeigetext. Der Text ist zum Lesen da und aendert sich mit der
Formulierung; eine Zaehlung daran wuerde beim naechsten umformulierten
Einzeiler unbemerkt kippen. Eintraege ohne meta zaehlen nicht mit - lieber
untertreiben als raten.
Damit completeTaskFromIssue dieselbe Grundlage liefert, schreibt es jetzt
ebenfalls from/to ins meta, wie es das Verschieben von Hand schon tut.
Die Zuordnung Symbol/Farbe je Eintragsart lag bisher in ActivityFeed und wird
jetzt von zwei Stellen gebraucht - herausgezogen nach components/activityKinds,
sonst laufen die Kopien beim naechsten neuen Eintragstyp auseinander.
Erreichbar ueber die Schnellsuche und einen Verweis vom Dashboard. Bewusst
kein vierter Punkt in der Kopfleiste - die ist auf dem Telefon schon eng.
Geprueft: 20 neue Unit-Tests (Wochengrenzen inklusive beider Zeitumstellungen,
Jahreswechsel, Zaehlung), ein E2E-Test ueber Aufruf und Blaettern, axe-core auf
375px und 1280px ueber drei Zustaende - auch die leere Woche - ohne
Beanstandung, kein seitlicher Ueberlauf.
Refs #2
Behebt florian.wilde/nebula#1.
Der Rueckweg "Issue geschlossen -> Aufgabe erledigt" gab es seit v1.7.0
schon, aber nur ueber den Webhook. Ist keiner eingerichtet - und am
Repository war keiner eingetragen -, blieb er ganz aus. Damit hing die
Funktion an einer Voraussetzung, die man ihr von aussen nicht ansieht.
Der Abgleich fragt jetzt den Zustand verknuepfter Issues nach und hakt ab,
was in Git geschlossen ist. Beides ergaenzt sich: der Webhook wirkt sofort,
der Abgleich verlaesslich.
Gespart wird, wo es geht: Nummern, die der gerade geholte Snapshot als offen
fuehrt, werden nicht einzeln nachgefragt. Umgekehrt gilt das nicht - die
Liste ist gedeckelt, aus dem Fehlen einer Nummer folgt nichts. Deshalb ein
Deckel von 25 Nachfragen je Durchgang.
**Nur abhaken, nie zuruecknehmen.** Der Abgleich sieht Zustaende, keine
Ereignisse. Ein offenes Issue kann auch bedeuten, dass das Schliessen von
Nebula aus fehlgeschlagen ist; ein symmetrischer Abgleich wuerde dann eine
bewusst erledigte Aufgabe wieder aufmachen. Das Zuruecknehmen bleibt dem
Webhook vorbehalten, der ein echtes "reopened" sieht. Ein geloeschtes Issue
ist ebenfalls kein erledigtes.
Nebenbei eine Unstimmigkeit aus v1.7.0 geradegezogen: der Webhook setzte nur
den Status und liess aus, was die Oberflaeche tut - naechste Fassung einer
wiederkehrenden Aufgabe, Eintrag im Verlauf. Beide Wege gehen jetzt durch
dieselbe Funktion.
Geprueft: 232 Unit-Tests, 31 E2E-Tests. Der neue E2E-Test laeuft als
einziger wirklich nach draussen - er haengt eine Aufgabe an ein seit Jahren
geschlossenes Issue und prueft, dass der Abgleich sie abhakt. Ist GitHub
nicht erreichbar, ueberspringt er sich statt rot zu werden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Geprueft gegen ASVS und die Cheat Sheets zu Authentication, Session
Management und Password Storage. Das meiste stand schon gut da – Token mit
256 Bit und nur als Hash in der Datenbank, serverseitige Pruefung bei jeder
Anfrage, Sitzungen weg beim Passwortwechsel, TOTP mit Replay-Schutz. Fuenf
Punkte blieben.
**Aufwand beim Hashen.** N war 2^15 bei p=1. Das Password Storage Cheat
Sheet nennt 2^17/p=1 als Minimum und listet als gleichwertig unter anderem
2^16/p=2 und 2^15/p=3 – mit p=1 lag 2^15 unter *allen* zulaessigen
Kombinationen und war rund dreimal billiger zu knacken als vorgesehen. Jetzt
2^17/p=1, gemessen 0,5 s und 128 MiB je Pruefung. Der Speicher ist gedeckelt:
Node rechnet scrypt im libuv-Threadpool, also hoechstens vier parallel.
**Neuhashen bei der Anmeldung.** Ohne das blieben bestehende Passwoerter fuer
immer auf den alten Parametern – anheben laesst er sich nur dort, wo das
Klartextpasswort vorliegt. needsRehash vergleicht den Gesamtaufwand N*r*p,
nicht Feld fuer Feld, damit gleichwertige Kombinationen nicht grundlos neu
gehasht werden.
**Aufzaehlbarkeit.** Drei Zweige beantworteten sich *vor* der Passwort-
pruefung und verrieten damit die Existenz eines Kontos: gesperrt (423),
deaktiviert (403) und "kein Passwort hinterlegt, nimm einen Passkey" (401).
Der einheitliche Text GENERIC war damit wirkungslos. Jetzt wird zuerst das
Passwort geprueft; Klartext gibt es erst danach. Reine Passkey-Konten
verbrennen dieselbe Rechenzeit, damit sich der Fall auch nicht an der Dauer
zeigt. Der Preis ist der verlorene Hinweis "nimm den Passkey" – bewusst
hingenommen.
**Leerlauffrist.** lastSeenAt wurde gefuehrt, aber nie ausgewertet: ein
abgegriffener Cookie galt bis zu 30 Tage, egal ob das Konto benutzt wurde.
ASVS 3.3.2 will beide Grenzen. Acht Stunden, ueber SESSION_IDLE_HOURS
einstellbar. Das Schreibintervall fuer lastSeenAt haengt jetzt an der Frist –
bei fester Stunde haette eine kuerzere Frist sich selbst ausgehebelt.
**Cookies.** SameSite Strict statt Lax, und bei https der Praefix "__Host-".
Der laesst den Browser erzwingen, dass kein Domain-Attribut gesetzt ist –
eine andere Subdomain kann den Cookie dann nicht ueberschreiben. Beides
kostet einmalig eine neue Anmeldung.
Dazu die Passwortpruefung: Wiederholungen, durchlaufende Zeichenfolgen und
Passwoerter, die den Benutzernamen enthalten, werden abgelehnt.
Zusammensetzungsregeln bleiben bewusst aus – ASVS 2.1.9 raet davon ab.
Geprueft: 232 Unit-Tests (davon 12 neue zu Aufwand, Neuhashen und Mustern)
und 30 E2E-Tests, darunter einer, der die Antwort auf ein stillgelegtes
Konto gegen die auf ein erfundenes haelt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auf dem Telefon war die Projektseite doppelt so breit wie der Bildschirm:
die Kopfleiste endete auf halber Strecke, "Jetzt synchronisieren" und der
Notiz-Knopf lagen ausserhalb.
Ursache sind die beiden Rasterspalten. Ein Rasterkind hat "min-width: auto"
und weigert sich damit, unter die Inhaltsbreite seines breitesten Kindes zu
schrumpfen. Breitestes Kind ist die Heatmap mit ueber 700px – sie zog die
Spalte auf und mit ihr die ganze Seite. Das overflow-x-auto um sie herum
kam gar nicht zum Zug, weil der Elter ihm die volle Breite zugestand.
Mit min-w-0 an beiden Spalten greift es wieder: gemessen auf 390px liegen
scrollWidth und clientWidth jetzt gleichauf, vorher 811 zu 390.
Warum das beim Umbau der mobilen Ansicht nicht auffiel: die Messung lief
gegen ein Projekt ohne Repository, ohne Snapshot und ohne README. Ohne
Heatmap gibt es nichts, was die Spalte aufziehen koennte. Nachgestellt
wurde das jetzt mit einem vollstaendigen Projekt – Snapshot, README mit
Codeblock und Tabelle, Notizen, Aufgaben.
Ein Regressionstest dafuer fehlt noch: die E2E-Tests legen Projekte ueber
die API an und kommen ohne echten Abgleich zu keinem Snapshot, also auch
zu keiner Heatmap. Ein Test ohne sie wuerde die Sicherheit vortaeuschen,
ohne den Fall zu treffen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Push aktualisiert den Stand sofort, statt bis zum naechsten Durchlauf
des Zeitgebers zu warten; ein geschlossenes Issue hakt seine Aufgabe ab.
Der Zeitplan bleibt als Rueckfalllosung.
Der Endpunkt ist der einzige ohne Anmeldung, der schreibt – entsprechend
eng gefasst:
- Ausgewiesen wird sich mit einer Signatur ueber den Rumpf. GitHub und
Gitea rechnen HMAC-SHA256, GitLab schickt nur den Token im Klartext;
dessen Verfahren ist schwaecher, mehr gibt die Plattform nicht her.
Verglichen wird durchweg zeitkonstant.
- Geprueft wird gegen den **rohen** Rumpf. Durch JSON.parse und
JSON.stringify gedreht kaeme eine andere Zeichenfolge heraus und die
Signatur passte nie.
- Der Rumpf ist nie Quelle fuer Inhalte. Wer das Geheimnis kennt, kann eine
Aufgabe auf- oder zumachen und einen Abgleich ausloesen – keine
beliebigen Daten einschleusen.
- Ratenbegrenzung je Repository, Deckel auf die Rumpfgroesse, und ein
unbekanntes Repository antwortet wie ein nicht eingerichtetes: 404,
damit die ID nicht bestaetigt wird.
- Unbekannte Ereignisse werden angenommen und liegen gelassen. Ein Fehler
brachte das Git-System zum Wiederholen und irgendwann zum Abschalten.
Das Geheimnis liegt verschluesselt am Repository, wie die Zugangstoken, und
ist genau einmal zu sehen. Damit es nicht doch nach draussen wandert, laedt
repositoryInclude die Repositories nicht mehr per include, sondern ueber
eine ausdrueckliche Feldliste: ein include haette das neue Feld still in
jede Projektantwort und jede Seite mitgenommen. Ob ein Haken eingerichtet
ist, holt die Projektseite getrennt.
Die Migration legt zwei nullbare Spalten an, mehr nicht; der Gegenlauf von
migrate diff kommt leer heraus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Zuordnung stand im Schema laengst – sourceIssue, sourceUrl und
sourceRepositoryId samt Eindeutigkeit je Repository, gefuellt von der
Uebernahme aus Issues seit v0.9.0. Es fehlte die Gegenrichtung.
Ein Knopf im Aufgabendialog legt das Issue an; Nummer und Adresse landen in
denselben Feldern, der Dublettenschutz gilt also fuer beide Wege. In den
Rumpf kommt ein Rueckverweis auf das Projekt.
Ausdruecklich per Knopf und nie nebenbei: was in ein fremdes System
geschrieben wird, soll jemand gewollt haben. Ist die Aufgabe erst
verknuepft, wandert der Zustand allerdings mit – erledigt schliesst das
Issue, ein Zurueckziehen oeffnet es wieder. Fehlertolerant: ein nicht
erreichbares Git-System darf das Abhaken nicht verhindern.
Die Zustaende passen nicht aufeinander, und das bleibt so. Nebula kennt
vier, ein Issue zwei; nach aussen geht nur die Grenze um ERLEDIGT. Ein
Rundlauf verliert damit IN ARBEIT und BLOCKIERT. Labels waeren die
Alternative, wuerden aber den Issue-Tracker mit Zustaenden fuellen, die
dort niemand pflegt.
Kein Widerspruch zu "keine eigenen Git-Operationen": Issues laufen ueber
die HTTP-API der Provider. Dafuer gibt es jetzt sendJson neben getJson –
durch dieselbe SSRF-Schleuse, aber ohne Umleitungen zu folgen. Ein 307 auf
eine interne Adresse naehme den Rumpf und den Token im Header mit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auf dem Telefon verdeckte die Tastatur den halben Dialog – bei der
Schnellerfassung genau das Eingabefeld, um das es geht.
Ursache ist der Unterschied zwischen Layout- und Sichtfenster: die Tastatur
schiebt sich darueber, ohne dass sich fuer die Seite etwas aendert.
"position: fixed" und "100vh" beziehen sich weiter auf das volle Fenster,
ein mittig stehender Dialog liegt damit zur Haelfte darunter.
Der Dialog richtet sich jetzt nach window.visualViewport – Hoehe und oberer
Rand kommen von dort und werden bei jeder Aenderung nachgefuehrt. Weil der
Behaelter scrollbar ist, holt der Browser das fokussierte Feld dann von
selbst ins Bild. Das gilt fuer alle Dialoge, nicht nur die Schnellerfassung.
Dazu, weil es beim Nachsehen auffiel: Aufgaben lassen sich in der
Uebersicht direkt bearbeiten. Bisher fuehrte von dort nur der Weg ins
Projekt, und wer eine Faelligkeit aendern wollte, musste die Aufgabe im
Brett wiederfinden. Der Dialog nimmt dafuer eine schlankere Form entgegen
als den vollstaendigen Prisma-Datensatz.
Geprueft mit einem nachgestellten Sichtfenster: Eingabefeld und Absende-
knopf liegen bei 375x667 und aufgeklappter Tastatur vollstaendig im freien
Bereich. Vorher lagen beide darunter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Server hat den neuen Stand ausgeliefert und in der Fusszeile weiter
"v1.5.0" angezeigt: Listenansicht, Schnellerfassung und die Aufgabenseite
waren drin, die Versionsnummer nicht.
Der Test in changelog.test.ts prueft, dass die oberste Fassung im Verlauf
zur package.json passt - er greift aber nicht, wenn beide gemeinsam
zurueckhaengen. Genau das war hier der Fall.
Der Verlauf wird in der Anwendung angezeigt (Knopf an der Versionsnummer in
der Fusszeile), deshalb steht dort jetzt, was sich geaendert hat: die
mobile Ansicht, die vierte Ansicht im Board, die Schnellerfassung, die
Aufgaben ueber alle Projekte, 16px in Eingabefeldern gegen den iOS-Zoom,
die fehlenden Beschriftungen der Symbolknoepfe und das beschnittene
Tag-Formular.
In der Roadmap ist v1.6.0 als erledigt eingetragen. Offen bleibt daraus
nur der dritte Teil, die persoenlichen API-Token - die sind
sicherheitsrelevant und bekommen einen eigenen Durchgang.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Wege, die zusammengehoeren: etwas festhalten, ohne den Rechner
aufzuklappen, und danach einen Ort haben, an dem alles zusammenlaeuft.
**Schnellerfassung.** Ein Blitzsymbol in der Kopfleiste, auf jeder Seite
erreichbar. Ein Feld, Enter, fertig - als Idee entsteht ein Projekt im
Status IDEA, als Aufgabe eine Aufgabe im gewaehlten Projekt. Beide Schemas
kommen mit einem einzigen Feld aus, an der API war deshalb nichts zu
aendern.
Der Dialog bleibt nach dem Anlegen offen und leert nur das Feld. Wer eine
Idee notiert, hat meistens gleich die naechste; ihn jedes Mal neu zu oeffnen
waere genau die Huerde, die hier wegsoll. Das zuletzt gewaehlte Projekt
wird gemerkt.
Der Einstieg sitzt in der Kopfleiste und nicht als schwebender Knopf unten
rechts - dort waere er auf schmalen Anzeigen mit der Auswahlleiste
kollidiert, die beim Markieren von Projekten einfaehrt.
**Aufgabenuebersicht.** /tasks sammelt jede Aufgabe aus jedem Projekt.
Geordnet wird nach Faelligkeit, nicht nach Spalte: projektuebergreifend
lautet die Frage "was ist ueberfaellig", nicht "was steht wo im Brett".
Abhaken geht direkt aus der Liste; den Rest - Zeitpunkt, abgeleiteter
Fortschritt, naechste Fassung wiederkehrender Aufgaben - erledigt der
bestehende Endpunkt.
Zwei Entscheidungen, die im Code begruendet sind:
- Gerechnet wird auf Kalendertage in fester Zeitzone, nicht auf Stunden und
nicht nach Serverzeit. Eine um 9 Uhr faellige Aufgabe ist um 17 Uhr nicht
ueberfaellig, sondern immer noch "heute". Und im Container laeuft die
Anwendung nach UTC - ohne feste Zeitzone kippte die Einteilung abends um
einen Tag. Das Fach bestimmt der Server, sonst liefen Server-Render und
Hydration auseinander.
- Bewusst nicht per Ziehen sortierbar. Die Position gilt innerhalb ihrer
Spalte in ihrem Projekt und waere hier ohne Bedeutung; eine Reihenfolge
anzubieten, die nirgends ankommt, waere ein leeres Versprechen.
Die Aufgabe kennt keinen Besitzer - die Trennung laeuft ueber das Projekt,
"project: { ownerId }" steht als einzige und erste Bedingung.
Nebenbei: der Untertitel in der Kopfleiste brach mit dem dritten
Navigationspunkt auf 375px mitten im Wort ab und bleibt unterhalb von sm
jetzt weg.
Geprueft: 200 Unit-Tests (davon 7 neue zu dueBucket, inklusive des
UTC-Falls), 27 E2E-Tests mit zwei neuen Wegen, axe-core auf 375px ueber
Uebersicht und beide Zustaende der Schnellerfassung ohne Beanstandung.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Vorhaben, die sich aus der Durchsicht der mobilen Ansicht ergeben haben.
v1.6.0 buendelt Schnellerfassung, eine projektuebergreifende Aufgabenseite
und die persoenlichen API-Token: erfassen am Telefon, ueberblicken an einer
Stelle, ansprechen per Shortcut. Die beiden Fallstricke der Aufgabenseite
stehen dabei: Task hat kein ownerId, und position gilt nur je Projekt.
v1.7.0 verknuepft Aufgaben mit Git-Issues. Die Zuordnung steht im Schema
laengst (sourceIssue, sourceUrl, sourceRepositoryId); es fehlt nur die
Gegenrichtung zur bestehenden Uebernahme. Bewusst ueber die HTTP-API der
Provider, nicht ueber eigene Git-Operationen - der Ausschluss unter
"Bewusst nicht" bleibt also unangetastet.
Ausdruecklich festgehalten, was NICHT kommt: eine echte beidseitige
Synchronisation. Die Zustaende passen ohnehin nicht aufeinander - ein Issue
kennt nur offen und geschlossen, DOING und BLOCKED gehen im Rundlauf
verloren. Das wird hingenommen, statt den Issue-Tracker mit Labels zu
fuellen, die dort niemand pflegt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auf 375px war das Dashboard 40px breiter als der Bildschirm. Ursache war die
Steuerleiste: Suchfeld, Sortierung, Ansichtsumschalter und "Neues Projekt"
lagen in einer Flex-Zeile ohne Umbruch, und weil das Sortierfeld ein
min-width mitbringt, wurde der Knopf am Ende schlicht hinausgeschoben. Das
overflow-x: hidden am body hat ihn dann abgeschnitten statt erreichbar zu
lassen. Dasselbe Muster im Projektkopf, wo "Loeschen" ausserhalb lag.
Die Zeilen brechen jetzt um, der Ansichtsumschalter steht auf schmalen
Anzeigen ueber die volle Breite. Gemessen ueber alle Seiten und beide
Testbreiten: scrollWidth == clientWidth.
Weil der Umschalter ohnehin umgebaut wurde, kommt die vierte Ansicht gleich
mit: eine Zeile je Projekt statt einer Karte. Auf dem Telefon ist das der
eigentliche Gewinn — eine Karte braucht rund 260px Hoehe und steht dort
zwangslaeufig allein in der Zeile, fuenf Projekte fuellen also zwei
Bildschirme. Als Liste passen sie auf einen, und weil alle Zeilen gleich
gebaut sind, liegen Name, Fortschritt und Status untereinander. Was nicht
mehr in die Breite passt, faellt der Reihe nach weg, statt sie zu sprengen.
Dazu die Kleinigkeiten, die auf dem Telefon dranhaengen:
- .field bekommt auf Beruehrungsgeraeten 16px. Darunter zoomt iOS Safari
beim Hineintippen die Seite heran und kehrt nicht von selbst zurueck.
- .btn mindestens 44px hoch, der Schieberegler einen 24px-Griff.
- Der Popover klemmt seine Breite ans Fenster. Unter 280px lieferte die
Rechnung eine negative linke Kante, das Menue stand halb ausserhalb.
- Das Tag-Formular der Auswahlleiste war 16px breiter als sein Popover und
wurde von dessen overflow-hidden beschnitten – auf jeder Breite.
- Die Heatmap scrollt wieder seitlich, statt sich zu stauchen.
- Die Kennzahlen stehen zweispaltig; einspaltig fuellten die fuenf Kacheln
den ganzen ersten Bildschirm, bevor das erste Projekt kam.
- viewportFit und die safe-area-Abstaende, damit im installierten Betrieb
nichts unter Statusleiste und Home-Anzeige verschwindet.
- Die beiden Verweise in der Kopfleiste und "Neues Projekt" tragen ein
aria-label. Unterhalb von sm sind sie reine Symbole und hiessen fuer einen
Screenreader gar nichts; axe-core meldete das als link-name.
axe-core laeuft auf 375px ueber alle vier Ansichten, den Dialog und die
Projektseite jetzt ohne Beanstandung.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Shell-Skripte lagen als 100644 im Index. Nach einem frischen Clone auf
dem Server scheiterte damit jedes "./scripts/setup.sh" und "./scripts/update.sh"
an fehlender Berechtigung; wer sich mit chmod +x behalf, hatte danach ein
schmutziges Arbeitsverzeichnis, und genau daran brach update.sh ab.
Unter Windows faellt das nicht auf, weil core.filemode dort auf false steht --
das Bit muss deshalb mit "git update-index --chmod=+x" gesetzt werden. Auch
docker/entrypoint.sh bekommt es; der Dockerfile-Aufruf chmod +x bleibt als
zweiter Riegel bestehen.
Ausserdem sagt update.sh jetzt, welche Dateien geaendert sind, statt nur zu
melden, dass etwas nicht sauber ist. Wer die Meldung liest, soll nicht erst
"git status" nachschlagen muessen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der letzte Anlauf hat das Problem nicht geloest. Die Freigabe stand weiter in
der Basisdatei, nur mit anderem Vorgabewert -- und Vorgabewerte greifen nicht,
wo die Variable schon gesetzt ist: in jeder .env, die aus der bisherigen
.env.example entstanden ist, steht POSTGRES_PORT="5432". Postgres band also
unveraendert auf 0.0.0.0 und kollidierte mit anderen Stacks auf demselben
Host; der Container kam gar nicht erst hoch. Das Overlay half nicht, weil es
erst greift, wenn man es dazunimmt.
Jetzt hat der db-Dienst keinen ports-Block mehr. Gebraucht wird er nur, wenn
die Anwendung ausserhalb des Containers laeuft, und dafuer gibt es
docker-compose.dev.yml (npm run docker:db). Damit ist der sichere Fall die
Vorgabe und der Sonderfall die Ausnahme, nicht umgekehrt.
Der App-Port bindet aus demselben Grund auf 127.0.0.1: Docker traegt seine
Freigaben direkt in iptables ein und umgeht dabei ufw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Datenbankport wurde in beiden Betriebsarten auf 0.0.0.0 gelegt, obwohl
ihn nur die Entwicklung braucht: im Container spricht die Anwendung "db:5432"
ueber das Compose-Netz an, und backup.sh geht ueber docker exec. Auf einem
Server lauschte Postgres damit im Netz, geschuetzt allein durch das Passwort
aus der .env. Die Anleitung riet zwar zu 127.0.0.1, aber als Handgriff statt
als Voreinstellung -- die falsche Richtung. Datenbank und Adminer binden
jetzt von Haus aus auf 127.0.0.1.
Liegen mehrere Stacks auf einem Host und regelt ein gemeinsamer Reverse Proxy
den Zugang, braucht Nebula gar keinen Host-Port. docker-compose.server.yml
nimmt beide Freigaben zurueck und haengt die Anwendung in ein geteiltes Netz.
Das braucht !reset: Compose fuehrt "ports" beim Zusammenfuehren anhaengend
statt ersetzend zusammen, eine leere Liste im Override wuerde also nichts
loeschen.
update.sh las die Adresse aus APP_PORT und haette in diesem Modus einen Port
gemeldet, den es nicht gibt. Es fragt jetzt Docker und sagt sonst, dass nur
das interne Netz erreichbar ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein reiner Docker-Host hat kein Node installiert. Die Anleitung verlangte
trotzdem "node scripts/setup.mjs", und update.sh las die Versionsnummer mit
"node -p" - beides waere dort gescheitert.
scripts/setup.sh macht dasselbe wie die .mjs-Fassung in reinem POSIX-Shell:
.env aus der Vorlage anlegen, ein APP_SECRET wuerfeln (openssl, sonst
/dev/urandom) und die Rechte auf 600 setzen. Ersetzt wird zeilenweise statt
mit einem sed-Ausdruck, weil base64 Schraegstriche enthaelt. update.sh liest
die Version jetzt selbst aus der package.json.
Dabei fielen zwei Fehler auf, die mein eigener Rat verursacht hat. Die
Anleitung empfiehlt APP_PORT="127.0.0.1:3000", damit die Anwendung nur hinter
dem Reverse Proxy erreichbar ist - die Gesundheitspruefung baute daraus aber
"http://127.0.0.1:127.0.0.1:3000". Und curl ist auf einem Docker-Host nicht
garantiert. Gefragt wird deshalb jetzt der Container selbst ueber seinen
HEALTHCHECK; curl liefert nur noch die Antwort zum Anzeigen, wenn es da ist.
Am Ende nannte das Skript den Sicherungspfad mit Anfuehrungszeichen - so
liess er sich nicht kopieren.
Nachgewiesen: dreimal gegen die laufende Instanz durchgelaufen, jedes Mal
mit Sicherung, Bau, Neustart und gesunder Antwort.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DEPLOYMENT.md fuehrt Schritt fuer Schritt vom leeren Server zur Instanz
hinter TLS: Docker, Konfiguration, Reverse Proxy mit Caddy oder nginx,
Firewall, Sicherungen per Cron. Dazu, was an Haertung schon eingebaut ist -
und was der Betreiber selbst beisteuern muss.
scripts/update.sh macht die Schritte in der richtigen Reihenfolge: pruefen,
holen, sichern, bauen, starten, Gesundheit pruefen. Erst bauen und dann
umschalten, damit ein gescheiterter Bau die laufende Fassung nicht
mitnimmt; die Sicherung liegt vor jeder Schemaaenderung. Antwortet
/api/health nach einer Minute nicht, zeigt es die letzten Logzeilen und
nennt den Weg zurueck.
Es weigert sich zu laufen, solange APP_SECRET leer oder noch der
Entwicklungswert ist - mit diesem Wert liegen die Git-Tokens faktisch offen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Elf Tests halten fest, was nicht durchkommen darf: script-Elemente,
Ereignis-Attribute aus rohem HTML, javascript: als Link und als Bildquelle,
data:text/html. Dazu die Aufloesung relativer Verweise und der Fall
"Bild im Link", der kein a im a erzeugen darf.
Gerendert wird serverseitig - fuer die Frage nach dem erzeugten HTML
braucht es kein DOM. Damit .tsx ueberhaupt laeuft, muss Vitest das JSX
selbst uebersetzen; die tsconfig setzt jsx auf "preserve", weil sonst Next
zustaendig ist. Vite nimmt inzwischen oxc statt esbuild, die esbuild-Option
wuerde stillschweigend ignoriert.
Umsortieren im Brett und das Anlegen von Vorlagen hatten als einzige
schreibende Endpunkte keine Ratenbegrenzung.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/_next/image war ohne Anmeldung erreichbar und antwortete mit 200. Die
Middleware nimmt den Pfad aus, weil dort sonst nur Rechenzeit verbraucht
wuerde - gedacht war das fuer ausgelieferte Dateien, nicht fuer einen
Endpunkt, der Bilder verarbeitet.
Er reicht jedes Bild durch sharp und damit durch libvips, fuer das es
regelmaessig Schwachstellen gibt; derzeit sind vier offen (CVE-2026-33327,
-33328, -35590, -35591). Ausnutzbar waere das hier nur mit einer Datei, die
schon auf dem Server liegt - hochladen kann man nichts. Aber Nebula benutzt
next/image ohnehin nirgends: eine Angriffsflaeche ohne Gegenwert.
Der Optimierer ist deshalb abgeschaltet, und der Pfad ist nicht mehr von der
Zugangsschranke ausgenommen. Ein Test haelt fest, dass er ohne Anmeldung
nicht mehr mit 200 antwortet.
Die Advisories zu postcss und sharp bleiben in npm audit stehen: Next pinnt
beide fest, auch in 16.2.12 - ein Versionssprung wuerde daran nichts
aendern. postcss laeuft nur zur Bauzeit ueber eigenes CSS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein TOTP-Code blieb rund 90 Sekunden gueltig und liess sich darin beliebig
oft verwenden. RFC 6238 verlangt in Abschnitt 5.2 ausdruecklich das
Gegenteil: derselbe Code darf kein zweites Mal angenommen werden.
Ohne diese Sperre kann ein abgefangener Code - ueber die Schulter gesehen,
von einer weiterleitenden Phishing-Seite, von Schadsoftware vom Bildschirm
gelesen - die restliche Gueltigkeitsdauer ueber wiederverwendet werden.
verifyTotp gibt dafuer jetzt ueber matchTotp das Zeitfenster zurueck, das
gepasst hat; angenommen wird nur, was neuer ist als das zuletzt eingeloeste.
Der Vergleich bleibt dabei in konstanter Zeit - es wird weiterhin nicht
fruehzeitig abgebrochen.
Auch die Aktivierung verbrennt ihr Fenster: der Code, der eben
scharfgeschaltet hat, soll nicht Sekunden spaeter noch zum Anmelden taugen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Elf Tests: welche Dateinamen als README gelten, dass Markdown Vorrang hat,
dass an einer Zeilengrenze gekuerzt wird und dass base64 mit Umlauten und
Zeilenumbruechen richtig ankommt.
Im Browser gegen vercel/swr geprueft, dazu ein erfundenes README fuer die
relativen Links, die swr nicht hergibt: aufgeloeste Pfade, unveraenderte
absolute Links, Anker bleiben Anker, kein img im Dokument und kein a im a.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lange READMEs sind eingeklappt, ein Knopf zeigt alles. Kurze stehen gleich
vollstaendig da - ein Knopf, der nichts freigibt, waere nur Ballast.
Relative Links werden gegen das Verzeichnis der Datei im Repository
aufgeloest. Ohne das liefen sie ins Leere, denn im README steht "docs/x.md"
und nicht die volle Adresse.
Bilder werden nicht geladen, sondern als anklickbare Marke gezeigt. Die
Content-Security-Policy erlaubt img-src nur von der eigenen Instanz; sie
dafuer zu oeffnen hiesse, jedem fremden README das Nachladen beliebiger
Bilder zu gestatten - also auch Zaehlpixeln. Fuer Abzeichen und
Bildschirmfotos, in einer Vorschau ohnehin Beiwerk, ist das kein guter
Tausch.
Der Umbau passiert im Baum und nicht ueber eine Komponente, weil es auf den
Elternknoten ankommt: "[](ziel)" ist in READMEs die Regel,
und ein a in einem a ist ungueltiges HTML. Der Browser zieht es beim Parsen
auseinander - der erste Versuch scheiterte prompt an einem
Hydrationsfehler. Innerhalb eines Links bleibt jetzt nur der Text stehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Erspart den Wechsel ins Git, nur um zu sehen, worum ein halbvergessenes
Projekt eigentlich ging.
GitHub hat einen eigenen Endpunkt, der jede Schreibweise findet - ein Aufruf
genuegt. GitLab und Gitea kennen keinen; dort wird erst das
Wurzelverzeichnis aufgelistet und dann die gefundene Datei geholt. Erkannt
werden README.md und die ueblichen Varianten, Gross- und Kleinschreibung
egal; Markdown hat Vorrang, weil nur das wirklich dargestellt werden kann.
Geholt wird hoechstens einmal am Tag, wie der Kalender, und mit eigenem
Zeitstempel: schlaegt das eine fehl, soll das andere nicht mit blockiert
werden.
Bei 60 000 Zeichen wird abgeschnitten, aber an einer Zeilengrenze - sonst
bliebe womoeglich ein halber Codeblock stehen. Dekodiert wird ueber Buffer
und nicht ueber atob, sonst kaemen Umlaute als Kauderwelsch heraus.
Kein README ist eine Auskunft, kein Fehlschlag: der Zeitstempel wird dann
trotzdem gesetzt, damit nicht jeder Abgleich erneut sucht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sieben Tests fuer die Terminlogik: Rechnen vom Faelligkeitsdatum,
Vorruecken bei langem Liegenbleiben, der Monatsuebergang ueber kurze
Monate, und dass die Schleife auch bei einem Datum von 1970 endet.
Dazu ein Ende-zu-Ende-Test ueber beide Wege nach ERLEDIGT und den Nachweis,
dass ein erneutes Speichern keine zweite Fassung erzeugt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eine Durchsicht mit axe-core ueber zehn Seitenzustaende - Dialoge einzeln,
sie stehen geschlossen nicht im DOM - fand 26 Verstoesse, alle derselben
Art: slate-500 kommt auf dem sehr dunklen Untergrund nur auf 3,8 zu 1,
slate-600 sogar auf 2,5. Verlangt sind 4,5 fuer normalen Text, und beide
Stufen werden reichlich fuer Hinweise und Nebenangaben benutzt.
Statt an hundert Stellen die Klasse zu tauschen, sind die beiden Stufen im
Theme aufgehellt. Der Abstand zwischen ihnen bleibt sichtbar, der Rest des
Entwurfs unangetastet. Geprueft gegen alle drei vorkommenden Untergruende;
der schlechteste Wert zaehlt.
Damit stimmt auch, was FEATURES.md ueber Barrierefreiheit behauptet - bisher
stand die Behauptung ungeprueft da.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Taeglich, woechentlich, alle zwei Wochen oder monatlich. Wandert die Aufgabe
nach ERLEDIGT, entsteht die naechste von selbst. Die erledigte bleibt
stehen - sonst ginge die Historie verloren.
Gerechnet wird vom Faelligkeitsdatum, nicht vom Abhaken: eine woechentliche
Aufgabe bleibt auf ihrem Montag, auch wenn sie einmal erst am Mittwoch
erledigt wurde. Lange Liegengebliebenes rutscht so weit vor, bis der Termin
in der Zukunft liegt; eine Aufgabe, die schon bei ihrer Entstehung
ueberfaellig ist, hilft niemandem.
Beim Monatsintervall bleibt der Tag im Monat erhalten. setMonth allein
rollt ueber: der 31. Januar wuerde zum 3. Maerz statt zum 28. Februar.
Ausgeloest wird an beiden Stellen, an denen eine Aufgabe erledigt werden
kann - im Dialog und durch Ziehen ins Brett. Nur der Uebergang zaehlt: eine
bereits erledigte Aufgabe erneut zu speichern erzeugt keine zweite Fassung.
Bewusst grob gehalten. Feinere Regeln wie "jeden zweiten Dienstag im Monat"
braucht ein Projektdashboard nicht, und sie waeren ohne Kalenderansicht
kaum nachvollziehbar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Elf Tests fuer die Seitenlogik des Kalenders: Deckel, Abbruch bei
unvollstaendiger Seite, Commits mit Datum in der Zukunft, und dass der
Zeitraum beim aeltesten bekannten Commit beginnt statt vor einem Jahr.
Dazu ein Ende-zu-Ende-Test fuer Vorlagen: sichern, anwenden, Datentrennung
gegen ein zweites Konto, und dass die Vorlage das Loeschen ihres
Ursprungsprojekts ueberlebt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Projekt, das sich bewaehrt hat, laesst sich im Projektkopf als Vorlage
sichern: Status, Prioritaet, Farbe, Tags, Notizen und offene Aufgaben. Beim
Anlegen steht sie oben im Dialog zur Auswahl.
Benutzereigen statt fest eingebaut. Welche Tags und welche Startaufgaben
sinnvoll sind, weiss nur, wer damit arbeitet - eine mitgelieferte Liste
waere geraten.
Zwei Dinge gehen bewusst nicht mit. Erledigte Aufgaben nicht: eine Vorlage
beschreibt, was zu tun ist, nicht was einmal getan wurde. Und Repositories
nicht: eine Vorlage, die jedes neue Projekt an dasselbe Repository haengt,
waere eine Falle.
Die Stammdaten uebernimmt der Dialog sichtbar in die Felder, damit man sie
vor dem Speichern noch aendern kann; bereits Getipptes bleibt stehen.
Notizen und Aufgaben legt erst der Server an - im Dialog liessen sie sich
nicht darstellen, und wer die Vorlage wieder abwaehlt, soll sie nicht
mitgeschleppt bekommen.
templateId gilt nur beim Anlegen. Ein bestehendes Projekt nachtraeglich aus
einer Vorlage zu befuellen wuerde Notizen und Aufgaben verdoppeln.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Feld je Tag auf der Projektseite, Wochen als Spalten, mit Farbskala und
Datum im Tooltip.
Woher die Zahlen kommen, unterscheidet sich je Anbieter. GitHub fuehrt die
Statistik selbst und liefert 52 Wochen mit Tageswerten in einem einzigen
Aufruf. GitLab und Gitea kennen nichts Vergleichbares; dort werden Commits
seitenweise geholt, gedeckelt auf fuenf Seiten - genug fuer die allermeisten
Repositories und wenig genug, um kein fremdes Kontingent zu sprengen.
Gezeichnet wird nur der Zeitraum, fuer den wirklich Daten vorliegen. Reichen
die Seiten nur ein paar Monate zurueck, faengt die Heatmap dort an und sagt
es dazu. Ein leeres Feld wuerde sonst behaupten, an dem Tag sei nicht
gearbeitet worden, obwohl darueber nichts bekannt ist - genau diesen Fehler
hatte die Verlaufslinie schon einmal.
Geholt wird hoechstens einmal am Tag und getrennt vom uebrigen Abgleich: an
einem Jahresueberblick aendert sich von Stunde zu Stunde nichts Sichtbares,
der Abgleich laeuft aber im Zweifel stuendlich.
Der Kalender benutzt getJson statt getJsonSafe. Waere ein Rate-Limit als
"keine Daten" durchgegangen, haette der gesetzte Zeitstempel die Heatmap
einen ganzen Tag lang unterdrueckt. Jetzt bleibt er bei einem Fehlschlag
ungesetzt, und der naechste Abgleich versucht es erneut - das deckt auch den
Fall ab, dass GitHub die Statistik gerade erst berechnet und mit 202
antwortet.
Nachgewiesen an vercel/swr: 49 Commits an 33 Tagen ueber ein volles Jahr aus
der Statistik, und mit kurzer Historie 90 statt 365 Felder samt Hinweis auf
den Deckel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prisma liest seine Einstellungen aus prisma.config.ts statt aus der
package.json; die Warnung vor Prisma 7 ist damit weg. Der dotenv-Aufruf
darin ist nicht optional: sobald die Datei existiert, laedt Prisma die .env
nicht mehr von selbst, und migrate deploy staende ohne DATABASE_URL da.
Vier neue Ende-zu-Ende-Tests: Reihenfolge und Mehrfachauswahl ueber die API,
Mehrfachauswahl in der Oberflaeche, der Aenderungsverlauf und das Manifest
samt Symbolen ohne Anmeldung. Damit sind es 21.
Der Test zum Ziehen von Aufgaben war gelegentlich rot, ohne dass etwas kaputt
war: das Neuladen brach den noch laufenden Speicheraufruf ab. Er wartet jetzt
auf die Antwort, statt auf eine feste Pause zu hoffen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Klick auf die Version in der Fusszeile zeigt, was in den bisherigen
Fassungen dazugekommen ist, was besser wurde und welche Fehler behoben sind.
Kein eigener Menuepunkt: gesucht wird so etwas dort, wo die Version steht.
Die Liste steht in lib/changelog.ts und nicht in einer CHANGELOG.md. Sie
wird in der Anwendung angezeigt, und zwei Quellen liefen prompt auseinander
- wie es der Versionsnummer schon einmal passiert ist, die eine Zeit lang in
der Fusszeile und in der package.json verschiedene Werte zeigte. Ein Test
haelt den obersten Eintrag und die package.json seitdem zusammen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Web-Manifest mit eigenen Symbolen; auf dem Handy laesst sich Nebula zum
Startbildschirm hinzufuegen und laeuft dann ohne Adressleiste.
Das Manifest entsteht wie die uebrigen Metadaten aus einer Funktion, nicht
aus einer Konstanten: nur so wird APP_NAME zur Laufzeit gelesen und das
fertige Docker-Image bleibt umbenennbar.
Manifest und Symbole muessen an der Zugangsschranke vorbei - der Browser
holt sie, bevor sich jemand angemeldet hat. Sonst landeten sie auf der
Anmeldeseite und die Anwendung waere nicht installierbar.
Die maskierbare Fassung hat mehr Rand: Android schneidet Symbole je nach
Geraet rund, eckig oder als Tropfen zu. Die PNGs sind aus dem vorhandenen
SVG mit Chrome erzeugt - das spart eine Bildbibliothek als Abhaengigkeit.
Beim Ergaenzen des Apple-Symbols ist mir aufgefallen, dass ein eigener
icons-Eintrag den Verweis ersetzt, den Next aus app/icon.svg von selbst
erzeugt: die Seite hatte danach gar kein Symbol mehr und der Browser fiel
auf /favicon.ico zurueck, das es hier nicht gibt. Beide Eintraege stehen
jetzt ausdruecklich da, ein Test prueft den Verweis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kaestchen auf jeder Karte, darunter eine Leiste am unteren Rand. Fest
verankert, weil sie bei einer langen Liste oben laengst aus dem Blick
gescrollt waere, waehrend man noch auswaehlt.
Ein eigener Endpunkt statt vieler einzelner PATCHes: eine Transaktion statt
zwanzig Rundgaengen, und der Verlauf bekommt je Projekt genau einen Eintrag
statt eines Schwalls.
Was der Filter ausblendet, faellt aus der Auswahl - sonst traefe eine
Massenaenderung Projekte, die man gar nicht mehr vor Augen hat. Fremde IDs
sammelt der Server wegen der Besitzerbedingung erst gar nicht ein und meldet
sie als uebergangen.
Das Kaestchen ist gedaempft, aber immer sichtbar. Erst beim Ueberfahren
einzublenden waere auf einem Touchgeraet nicht erreichbar - und wer es nie
sieht, weiss nicht, dass es die Auswahl gibt.
Ansicht, Sortierung und Archivschalter ueberdauern nun das Neuladen. Wer sein
Brett von Hand anordnet, will es beim naechsten Aufruf auch sehen. Der
Suchbegriff bleibt aussen vor: eine Suche von gestern wiederherzustellen
waere eher verwirrend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Karten lassen sich innerhalb einer Spalte anordnen, die Position bleibt am
Projekt. Das Aufgabenbrett kann das laengst - dass ausgerechnet das
Projektbrett der gewaehlten Sortierung folgte, war ein Bruch.
Die Sortierung im Kopf ist in dieser Ansicht deshalb abgeschaltet. Sie
stehen zu lassen, ohne dass sie wirkt, waere irrefuehrend.
Umsortieren fasst das Aenderungsdatum nicht an. Sonst schoebe ein Aufraeumen
im Brett saemtliche Projekte in "Zuletzt geaendert" nach oben - das Brett
wuerde die Liste umsortieren, ohne dass sich inhaltlich etwas geaendert hat.
Ein Statuswechsel dagegen ist eine echte Aenderung und darf das Datum
setzen. Nur er kommt auch in den Verlauf; ein blosser Rangwechsel nicht,
sonst bestuende der Verlauf bald aus nichts anderem.
Dabei fiel auf, dass neu angelegte, importierte und per Status verschobene
Projekte gar keine Position bekamen und sich alle auf 0 stapelten. Der Rang
wird jetzt an allen vier Stellen gesetzt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drei neue Ende-zu-Ende-Tests: das Verwalten mehrerer Repositories samt
Datentrennung, das Lesen von Exportdateien der Version 1, und die
Uebernahme ohne verknuepftes Repository.
Dabei fiel auf, dass der Uebernahme-Endpunkt ohne Repository mit 404
antwortete, obwohl gar keine Repository-ID angefragt war. Wer nichts angibt,
hat schlicht noch keines verknuepft - das ist kein "nicht gefunden", sondern
409 mit dem Hinweis, eines zu hinterlegen.
Version 1.0.0: der Funktionsumfang ist beisammen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eine Leiste mit allen Repositories des Projekts, sobald es mehr als eines
gibt - bei einem einzigen waere eine Leiste mit genau einem Knopf nur
Ballast. Jedes hat eigene Kennzahlen, Listen, CI und eigenen Abgleich.
Verknuepfen und Loesen laufen ueber einen eigenen Dialog mit derselben
Repo-Auswahl wie beim Projekt. Der Projektdialog bearbeitet weiterhin nur
das fuehrende: eine Liste darin wuerde den Dialog sprengen, in dem es
eigentlich um das Projekt geht.
Eine frei waehlbare Bezeichnung tritt an die Stelle von owner/name. Die
Karte zeigt "+2 Repos" und im Titel, welche es sind.
repoTitle steht in lib/git/meta statt bei den uebrigen Repository-Helfern:
lib/repositories zieht Prisma und damit node:-Module nach, die in einer
Client-Komponente nicht landen duerfen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Vorhaben besteht oft aus Frontend und Backend - zwei Repositories, aber
eine Karte. Die Git-Felder wandern deshalb vom Projekt in eine eigene
Tabelle, der Snapshot haengt jetzt am Repository.
Das erste Repository ist das fuehrende. Wo nur eines dargestellt werden
kann - Karte, Kanban -, ist es dieses. Ein Abgleichsfehler an *irgendeinem*
erscheint aber trotzdem auf der Karte, sonst bliebe er unbemerkt.
Der Merkstand fuer Benachrichtigungen liegt nun am Repository. Am Projekt
verdeckte ein rotes Frontend die rote Pipeline des Backends. Stillstand
bleibt dagegen eine Aussage ueber das Vorhaben und bemisst sich am juengsten
Commit ueber alle Repos: an einem Projekt wird gearbeitet, sobald an einem
seiner Teile gearbeitet wird.
Der Zeitgeber zaehlt und begrenzt jetzt Repositories statt Projekte. Sonst
zoege ein Projekt mit fuenf Repos fuenfmal so viele Anfragen nach sich, wie
sein Platz im Kontingent hergibt. "Alle abgleichen" laeuft nacheinander -
ein Schwung gleichzeitiger Anfragen an denselben Anbieter ist genau das,
worauf Rate-Limits reagieren.
Die Issue-Nummer allein reicht als Dublettenschutz nicht mehr: Frontend und
Backend haben beide ein Issue 5, und das sind zwei Aufgaben. Die Herkunft
gehoert deshalb mit in die Eindeutigkeit. In Postgres nachgewiesen: dieselbe
Nummer in zwei Repos geht durch, zweimal im selben Repo nicht, mehrere
Aufgaben ohne Herkunft bleiben erlaubt.
Die Projekt-API nimmt die Git-Felder weiterhin an und setzt damit das
fuehrende Repository - so kommt man mit einem Aufruf zu Projekt und Repo,
und der Import muss von der neuen Tabelle nichts wissen.
Die Exportdatei zaehlt auf 2 hoch und traegt eine Liste. Version 1 bleibt
lesbar: dort stand genau ein Repository flach am Projekt.
Die Datenmigration ist von Hand geschrieben - migrate diff kennt nur den
Zielzustand und haette die Spalten samt Snapshots ersatzlos geloescht.
Gegen eine Kopie mit echten Daten geprueft: Verknuepfung, Verbindung,
Fehlerzaehler und Snapshot kommen vollstaendig mit, Projekte ohne Repo
bekommen keine leere Zeile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Einmalkennwort zusaetzlich zum Passwort, mit QR-Code zum Einrichten und
zehn Wiederherstellungscodes fuer den Fall, dass das Geraet verloren geht.
Die Rechnung nach RFC 6238 steht in lib/auth/totp.ts statt in einer
Abhaengigkeit. Es sind wenige Zeilen, das Verfahren liegt seit 2011 fest, und
der RFC bringt Testvektoren mit - die Richtigkeit ist damit belegt und nicht
nur behauptet. Fuer den QR-Code kommt qrcode dazu; ihn selbst zu zeichnen
waere unverhaeltnismaessig.
Einrichtung in zwei Schritten: der Schluessel wird hinterlegt, aber erst ein
bestaetigter Code schaltet ihn scharf. Wer den QR-Code nicht scannen konnte
und abbricht, sperrt sich sonst beim naechsten Anmelden selbst aus.
Zwischen Passwort und Code steht ein eigenes Modell statt einer halbfertigen
Sitzung. Eine Sitzungszeile mit einem "noch nicht gueltig"-Merker waere eine
Zeile, die bei einem vergessenen Filter irgendwo doch als Anmeldung
durchgeht; dieses Versehen kann hier nicht passieren. Nach fuenf
Fehlversuchen ist der Vorgang verbrannt, damit sechs Stellen nicht in Ruhe
durchprobiert werden.
Die Wiederherstellungscodes liegen als SHA-256 statt als scrypt: sie sind
zufaellig erzeugt und lang genug, dass Raten aussichtslos ist - ein
langsames Verfahren brauchte bei jeder Anmeldung zehn teure Vergleiche und
kaufte nichts dafuer. Das Alphabet laesst O, I und B weg, weil die Codes von
Hand abgeschrieben werden.
Abschalten und Neuerzeugen verlangen das Passwort. Ein unbeaufsichtigter
Rechner mit offener Sitzung soll den zweiten Faktor nicht im Vorbeigehen
entfernen koennen.
Nachgewiesen im Browser: Einrichtung, Anmeldung mit Code, Anmeldung mit
Wiederherstellungscode, derselbe Code ein zweites Mal abgelehnt, Abschalten
mit falschem und richtigem Passwort.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Was ist hier eigentlich passiert - eine Zeitleiste auf der Projektseite
beantwortet das jetzt: Statuswechsel, Notizen, Aufgaben, neue Commits.
Der Verlauf soll erzaehlen, nicht protokollieren. Deshalb bekommen Status
und Repo-Verknuepfung einen eigenen Eintrag, waehrend alle uebrigen Felder
zu einem zusammengefasst werden - sonst ertraenkt eine Feinjustage der Farbe
alles Wichtige. Ein Titel, der sich aendert, kommt gar nicht vor.
Beim Abgleich ist Zurueckhaltung noch wichtiger: er laeuft im Zweifel
stuendlich. Vermerkt wird nur, wenn der letzte Commit ein anderer ist als
beim letzten Mal, und beim ersten Abgleich gar nicht - dass ein Repo Commits
hat, ist keine Neuigkeit. Fehlschlaege stehen nur beim Uebergang in den
Fehlerzustand drin, sonst schlaegt ein abgelaufenes Token stuendlich neu auf.
Der Text wird beim Schreiben formuliert, nicht beim Anzeigen. Ein Eintrag
soll auch dann lesbar bleiben, wenn es die Notiz, um die es ging, laengst
nicht mehr gibt.
Zweihundert Eintraege je Projekt bleiben erhalten, der taegliche Durchgang
stutzt den Rest. Der Export nimmt den Verlauf nicht mit - er beschriebe, was
in dieser Instanz passiert ist, und verwiese auf Konten, die es anderswo
nicht gibt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei neue Ende-zu-Ende-Tests: die Palette findet Projekt und Notiz und
oeffnet den Treffer mit Enter; die Uebernahme meldet ohne Repo-Stand einen
Hinweis statt still zu scheitern. Damit sind es 15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Klick im Git-Panel macht aus den offenen Issues Aufgaben - mit Titel,
Labels und einem Link zurueck ins Git-System.
Grundlage ist der zuletzt geholte Snapshot. Hier wird bewusst nichts
nachgeladen: ein Klick soll nicht ungefragt fremde APIs ansprechen und
womoeglich in ein Rate-Limit laufen. Wer den neuesten Stand will, gleicht
vorher ab; ohne Snapshot sagt der Server das statt still nichts zu tun.
Die Issue-Nummer bleibt an der Aufgabe und ist je Projekt eindeutig. Ein
zweiter Klick legt deshalb keine Dubletten an, auch wenn die Aufgabe
inzwischen verschoben oder umbenannt wurde. NULL gilt in Postgres als
verschieden, von Hand angelegte Aufgaben stoeren die Eindeutigkeit also
nicht.
Nachgewiesen an vercel/swr: 15 offene Issues uebernommen, beim zweiten
Aufruf 0 angelegt und 15 uebersprungen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Palette ueber die ganze Anwendung: Projekte nach Name, Kurzbeschreibung und
Tags, Notizen ueber die serverseitige Volltextsuche, dazu die Bereiche.
Ohne Eingabe stehen die zuletzt bearbeiteten Projekte bereit - danach sucht
man ueblicherweise.
Bedienung ueber Tastatur, wie man es erwartet: Pfeile waehlen, Enter oeffnet,
Escape schliesst, der ausgewaehlte Eintrag bleibt im Blick.
Die Projektliste wird beim ersten Oeffnen geholt und dann behalten. Sie bei
jedem Tastendruck neu zu laden waere Verschwendung, und veraltet ist sie
hoechstens um die Dauer einer Sitzung. Notizen sucht dagegen der Server,
entprellt ab zwei Zeichen - sie mitzuladen wuerde die Seite aufblaehen.
Wie Menue und Dialog haengt die Palette per Portal am body, aus denselben
Gruenden: overflow-hidden und backdrop-filter der Karten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FEATURES erklaert, warum der Massstab der letzte Commit ist und nicht das
Aenderungsdatum, wen die Pruefung auslaesst und wann erneut gemeldet wird.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eigener Abschnitt mit der Schwelle von 14 Tagen bis zu einem Jahr, dem
Zeitpunkt der letzten Pruefung und der Zahl der Empfaenger. Steht die auf
null, sagt der Abschnitt das und verweist auf "Mein Konto".
Eine Aenderung der Schwelle setzt lastStaleCheckAt zurueck, damit sie beim
naechsten Tick greift und nicht erst in 24 Stunden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vierter Anlass: seit laengerem kein Commit. Er haengt an keinem Ereignis -
es passiert ja gerade nichts - und braucht deshalb einen eigenen Durchlauf
im Zeitgeber, hoechstens einmal am Tag und unabhaengig vom Abgleich. Hoert
niemand auf den Anlass, laeuft die Pruefung gar nicht erst.
Massstab ist ausschliesslich der letzte Commit im verknuepften Repository.
Das Aenderungsdatum des Projekts taugt nicht: jeder Abgleich fasst es an und
wuerde den Stillstand immer wieder zuruecksetzen. Projekte ohne Repository
bleiben deshalb aussen vor, ebenso fertige und archivierte.
Gemeldet wird je Projekt einmal. Erneut erst, wenn danach wieder ein Commit
kam und es seitdem wieder still wurde - ein Projekt, das aufwacht und wieder
einschlaeft, meldet sich also ein zweites Mal.
Beim Testen kam heraus, dass ich den neuen Anlass zwar ins Prisma-Schema und
in die Oberflaeche eingetragen hatte, nicht aber ins Zod-Schema - die API
lehnte ihn mit 422 ab. Behoben, und ein Test haelt beide Listen kuenftig
zusammen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FEATURES beschreibt die drei Anlaesse, die Entprellung samt ihrer beiden
Feinheiten, die drei Zustellwege und die Absicherung. README bekommt die
neuen Endpunkte und Umgebungsvariablen, .env.example die SMTP-Vorlage.
ROADMAP: Benachrichtigungen erledigt. Neu an erster Stelle steht das Melden
stillstehender Projekte - das ist der einzige Anlass, der kein Ereignis hat,
an dem er haengt, und braucht deshalb einen eigenen Durchlauf. Ein doppelter
Eintrag zu Webhooks statt Polling ist dabei aufgefallen und entfernt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Beliebig viele Ziele je Konto, jedes mit eigenen Anlaessen. Jedes laesst sich
einzeln testen und pausieren; Ergebnis und Zeitpunkt der letzten Zustellung
stehen an der Karte.
Der Weg laesst sich nach dem Anlegen nicht mehr aendern - ein Webhook-Ziel zu
einer E-Mail-Adresse umzubiegen ergaebe keinen Sinn und muesste das
Geheimnis mitwandern lassen. Ist kein SMTP hinterlegt, ist die Auswahl
"E-Mail" gesperrt und erklaert, warum.
targetSafeSelect liegt in lib/notify/types statt in der Route: Next laesst in
route.ts nur bestimmte Exporte zu und bricht den Bau bei allem anderen ab.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drei Anlaesse: fehlgeschlagener Abgleich, rote CI, neues Release. Die Daten
dafuer liegen seit dem CI-Status vor, es fehlte nur der Weg nach draussen.
Der Kern ist die Entprellung. Gemeldet wird ausschliesslich bei einer
Aenderung gegenueber dem zuletzt gemeldeten Stand, der am Projekt vermerkt
ist. Ein Repo mit abgelaufenem Token meldet sich damit einmal statt bei
jedem stuendlichen Durchlauf. Zwei Feinheiten:
- Beim allerersten Abgleich wird kein Release gemeldet, sonst waere jede
bereits vorhandene Version eine "Neuigkeit".
- Der Merkstand wird auch ohne eingerichtetes Ziel fortgeschrieben, sonst
kaeme beim ersten Ziel die ganze Vergangenheit hinterher.
Zustellwege:
- ntfy mit Titel, Prioritaet und Symbol als Kopfzeilen, optionalem
Bearer-Token. Emojis werden aus den Kopfzeilen entfernt statt durch
Fragezeichen ersetzt - Kopfzeilen duerfen nur Latin-1 tragen, und das
Sinnbild liefert ohnehin der Tags-Header.
- Webhook mit JSON, Projektbezug und Tieflink. Mit Geheimnis wird der Rumpf
per HMAC-SHA256 signiert, damit der Empfaenger die Herkunft pruefen kann.
- E-Mail ueber SMTP, verfuegbar sobald SMTP_HOST gesetzt ist.
Zieladressen sind Benutzereingaben, die der Server abruft - sie laufen
deshalb durch denselben SSRF-Schutz wie die Git-Verbindungen. Redirects
werden nicht verfolgt, weil ein Umleitungsziel nicht geprueft waere.
Fehler beim Zustellen werden geschluckt: eine kaputte Benachrichtigung darf
den Abgleich nicht scheitern lassen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FEATURES beschreibt beide Ebenen - Unit-Tests ohne Datenbank und
Ende-zu-Ende-Tests gegen die gebaute Anwendung - samt der beiden
Vorkehrungen gegen einen Lauf auf der echten Datenbank. Dazu die
Voraussetzungen fuer Gitea Actions.
ROADMAP: der Testausbau war Punkt 1 und ist erledigt; Benachrichtigungen
ruecken auf.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prueft jeden Push und jeden Pull Request: Typen, Unit-Tests, Bau und
Ende-zu-Ende-Tests gegen einen frischen PostgreSQL-Dienst. Schlaegt etwas
fehl, wird der Playwright-Bericht als Artefakt abgelegt.
Der Datenbankname des Dienstes enthaelt "test", sonst verweigert
global-setup.ts den Dienst. In CI wird das mitgelieferte Chromium benutzt
statt des oertlich installierten Chrome.
Die Syntax ist GitHub-kompatibel - fuer GitHub genuegt es, die Datei nach
.github/workflows zu kopieren.
Nebenbei bekommt der Knopf des Benutzermenues eine Beschriftung. Ohne sie
las ein Screenreader auf schmalen Anzeigen nur das zweibuchstabige Kuerzel
vor, und im Test war er nicht eindeutig vom Datenmenue zu unterscheiden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 Tests gegen die gebaute Anwendung mit eigener Datenbank auf Port 3111 -
also durch Middleware, Authentifizierung, Validierung und Datenbank hindurch.
Genau die Schicht, die Unit-Tests nicht erreichen. Laufzeit rund 20 Sekunden.
api.spec.ts prueft, was bisher nur von Hand geprueft war:
- Einrichtung sperrt sich nach dem ersten Konto
- ohne Anmeldung liefert jeder Endpunkt 401
- fremdes Origin und falscher Content-Type werden abgewiesen
- SSRF-Sperren greifen
- Datentrennung: ein zweites Konto bekommt auf fremde Projekte, Aufgaben und
Adminwege durchgaengig 404 beziehungsweise 403, das Original bleibt
unveraendert
- Aufgaben: Reihenfolge, abgeleiteter Fortschritt, untergeschobene IDs
- Export ohne Geheimnisse, Import je Strategie
ui.spec.ts geht den Weg eines Menschen: anmelden, Projekt anlegen, Aufgaben
anlegen und ziehen, suchen, abmelden. Das Ziehen wird nach einem Neuladen
erneut geprueft - sonst waere nur die Anzeige getestet, nicht das Speichern.
Zwei Vorkehrungen:
- global-setup.ts bricht ab, wenn der Datenbankname nicht "test" enthaelt,
und zwar vor dem TRUNCATE. Der teuerste denkbare Fehler waere eine
Testreihe, die die Produktivdaten leert.
- Playwright startet den Server selbst mit eigener DATABASE_URL und eigenem
Port; eine laufende Instanz auf 3000 stoert nicht.
Beim Schreiben zeigte sich, dass synthetische Zieh-Gesten eine Pause vor dem
Loslassen brauchen - dnd-kit wertet die Bewegung in einem eigenen Frame aus
und faende sonst kein Ziel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Damit eine neue Sitzung in diesem Ordner nicht bei null anfaengt: die
Befehle, die nicht verhandelbaren Regeln (Datentrennung, Pruefung in jedem
Endpunkt, keine Geheimnisse nach aussen) und vor allem die Fallstricke, die
in diesem Projekt schon zugeschlagen haben - node-Module in
Client-Komponenten, instrumentation.ts im Edge-Bundle, CSS-Klassen ausserhalb
von @layer, Overlays in Karten mit backdrop-filter.
Dazu der Hinweis, dass Migrationen hier ueber "migrate diff" entstehen, weil
"migrate dev" interaktiv ist, und die Erinnerung, bei Oberflaechenaenderungen
tatsaechlich hinzusehen statt es zu behaupten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Schalter laesst sich ueber PATCH /api/projects/:id setzen. Beim
Einschalten wird der abgeleitete Wert sofort berechnet, damit er nicht bis
zur naechsten Aufgabenaenderung falsch steht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FEATURES bekommt ein eigenes Kapitel: die vier Spalten, das Ziehen mit
Sortierung, Faelligkeiten, der abgeleitete Fortschritt und die Erklaerung,
warum die Reihenfolge als vollstaendige Liste je Spalte gespeichert wird.
ROADMAP: "Aufgaben und Checklisten" wandert zu den erledigten Punkten. Neu
unter den Kleinigkeiten stehen wiederkehrende Aufgaben und das Uebernehmen
offener Git-Issues als Aufgaben - beides ergibt sich erst jetzt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der benannte Import aus der package.json loeste in Webpack die Warnung
"Should not import the named export 'version'" aus. Jetzt als Standardimport.
Version auf 0.5.0 gehoben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Aufgaben gehen mit - JSON-Export, Import und Instanz-Sicherung. Beim Import
wird die Position aus der Datei uebernommen, notfalls die Reihenfolge in ihr.
Wie bei den Notizen werden Aufgaben beim Ersetzen ausgetauscht statt
zusammengefuehrt, damit ein erneuter Import keine Dubletten erzeugt.
Der Markdown-Export bekommt einen Abschnitt "Aufgaben", nach Spalten
gegliedert und als Checkliste geschrieben - so bleibt der Stand auch
ausserhalb von Nebula lesbar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vier Spalten, Karten per Ziehen verschiebbar - zwischen den Spalten und
innerhalb einer Spalte. Waehrend des Ziehens fuehrt der oertliche Zustand,
danach wird die neue Ordnung als Ganzes gespeichert; scheitert das, springt
das Brett auf den Serverstand zurueck.
Details:
- Gezogen wird am Griff links, damit ein Klick auf die Karte weiter den
Dialog oeffnet.
- Der Spaltenwechsel passiert schon beim Darueberziehen, nicht erst beim
Ablegen - sonst wirkt das Brett traege.
- Der Serverstand wird nur uebernommen, wenn gerade nicht gezogen wird,
sonst spraenge die Karte mitten in der Bewegung zurueck.
- Faelligkeiten faerben sich: ueberfaellig rot, heute gelb, demnaechst
gedaempft.
Ist "Fortschritt aus Aufgaben" eingeschaltet, wird der Regler gesperrt statt
einen Wert anzubieten, der beim naechsten Aufgabenwechsel ueberschrieben
wuerde - im Dialog wie im Kopfbereich. Die Projektkarte zeigt das Verhaeltnis
als "3/8".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neues Modell Task: Titel, optionale Beschreibung, Spalte, Position,
Faelligkeit und Labels. Vier Spalten - offen, in Arbeit, blockiert,
erledigt.
Anders als beim Projektbrett ist die Reihenfolge innerhalb einer Spalte hier
bedeutsam und wird deshalb gespeichert. Der Reorder-Endpunkt nimmt je Spalte
die vollstaendige Liste in ihrer neuen Reihenfolge entgegen und leitet Status
und Position daraus ab. Das ist wiederholbar, kommt ohne Positionsrechnerei
im Browser aus, und ein verlorener Aufruf laesst sich einfach erneut senden.
Sicherheitsrelevant: der Endpunkt bekommt IDs aus dem Browser. Er
aktualisiert ausschliesslich Aufgaben, die wirklich zu diesem Projekt
gehoeren, und meldet uebergangene IDs zurueck - sonst liessen sich fremde
Aufgaben ueber untergeschobene IDs verschieben.
doneAt wird nur beim tatsaechlichen Wechsel nach "erledigt" gesetzt und beim
Zurueckziehen geleert. Ein Umsortieren innerhalb der Spalte laesst das Datum
in Ruhe.
Der Fortschritt eines Projekts laesst sich wahlweise aus dem Anteil
erledigter Aufgaben ableiten. Er wird nach jeder Aenderung neu berechnet und
auch sofort beim Einschalten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FEATURES beschreibt Markdown, Volltextsuche, PR- und Issue-Listen, den
CI-Status samt Herkunft je Git-System, die Aktivitaetskurve inklusive ihrer
Grenze, das Kanban-Board und das Testfundament.
ROADMAP: sieben Punkte wandern zu den erledigten. Ganz oben steht jetzt der
Ausbau der Tests auf Datenbank und Browser - das Fundament steht, die
Schicht darueber fehlt und ich pruefe weiterhin von Hand. Danach
Benachrichtigungen, die von CI-Status und Abgleich nun profitieren wuerden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Fusszeile zeigte v0.2.0, die package.json stand auf 0.1.0 - beim
Auth-Feature hatte ich nur die Anzeige hochgezaehlt. Jetzt kommt die Version
aus der package.json, kann also nicht mehr auseinanderlaufen. Stand: 0.4.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dritte Ansicht neben Raster und Gruppierung: eine Spalte je Status, Karten
per Ziehen verschiebbar. Der Wechsel wird sofort angezeigt und erst danach
gespeichert; scheitert das Speichern, springt die Karte zurueck.
Details, die den Unterschied machen:
- Gezogen wird am Griff links, nicht an der ganzen Karte - so bleibt der
Titel ein normaler Link.
- Das Ziehen startet erst nach sechs Pixeln Bewegung, sonst liesse sich die
Karte nicht mehr anklicken. Auf Touchgeraeten nach kurzem Halten.
- Die mitgezogene Karte haengt in einem DragOverlay am Cursor, das Original
bleibt blass stehen - sonst wuerde die Spalte unter dem Finger springen.
- Bestaetigt der Server die Verschiebung, wird die oertliche Ausnahme wieder
entfernt, damit sie nicht veraltet.
Die Reihenfolge innerhalb einer Spalte folgt der gewaehlten Sortierung.
Frei sortieren hiesse, eine Position je Projekt zu speichern - bewusst
nicht gebaut.
Nachgewiesen im Browser: Karte aus "Idee" nach "Fertig" gezogen, Zielspalte
hebt sich hervor und zeigt "Hier ablegen", danach steht das Projekt
serverseitig auf DONE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
51 Tests, unter einer Sekunde, ohne DOM, Datenbank oder laufenden Server.
Abgedeckt sind die Stellen, an denen ein Fehler teuer waere:
- Slug-Erzeugung mit Umlauten, Tag-Aufbereitung, Textkuerzung
- URL-Zerlegung fuer https, SSH, verschachtelte GitLab-Gruppen und
Oberflaechenpfade; Provider-Erkennung; Bildung der Aktivitaetskurve
- SSRF-Sperrlisten: Loopback, Link-Local samt Metadaten-Endpunkt, CGNAT,
Multicast, IPv4-gemappte IPv6-Adressen; Formpruefung; alle drei
Umgebungsschalter
- Passwoerter: scrypt-Rundlauf, unterschiedliches Salz je Hash, Ablageformat,
Verhalten bei kaputten Hashes, Unicode-Normalisierung, Passwortregeln
Bewusst ohne Datenbank- und Browsertests - die brauchen Infrastruktur und
gehoeren in eine Pipeline. Steht als naechster Punkt in der ROADMAP.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher standen nur Zahlen da. Jetzt kommen die Inhalte mit.
Pull Requests und Issues: bis zu 50 offene je Liste, mit Nummer, Titel,
Autor, Alter, Labels und Link. Bei GitHub und Gitea liefert der
Issue-Endpunkt auch Pull Requests mit - die werden herausgefiltert.
CI-Status des letzten Laufs auf dem Default-Branch:
- GitHub ueber actions/runs; dort sind Lauf-Status und Ergebnis getrennt und
werden auf einen Wert zusammengefuehrt
- GitLab ueber pipelines
- Gitea ueber den kombinierten Commit-Status. Das deckt Gitea Actions und
externe Systeme wie Woodpecker gleichermassen ab - ein reiner
Actions-Endpunkt existiert auf aelteren Instanzen nicht.
Commit-Aktivitaet als Balkenreihe. Berechnet aus den ohnehin geholten
Commits (jetzt 100 statt 15, angezeigt bleiben 15) statt ueber eigene
Statistik-Endpunkte: die gibt es nicht bei allen drei Systemen, und bei
GitHub antworten sie beim ersten Aufruf mit 202 und ohne Daten.
Beim Testen gegen gitlab-runner fiel auf, dass alle 100 Commits in die
letzten 30 Tage fielen - die frueheren Tage haetten faelschlich Null
gezeigt, obwohl wir ueber sie nichts wissen. Die Kurve wird deshalb auf den
tatsaechlich belegten Zeitraum gekuerzt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Suche im Board arbeitet auf den bereits geladenen Projekten und deckt
Name, Kurzbeschreibung, Tags und Repo-Pfad ab. Notizen liegen dort nicht vor
- sie mitzuladen wuerde die Seite unnoetig aufblaehen. Deshalb ein eigener
Endpunkt, der Postgres mit seiner deutschen Textsuche arbeiten laesst.
- websearch_to_tsquery versteht Eingaben wie "docker -compose" und wirft bei
Unsinn keinen Fehler
- ts_headline liefert den Ausschnitt gleich mit; die Hervorhebung kommt als
[[…]] zurueck, damit die Oberflaeche kein HTML aus der Datenbank einbetten
muss
- deutsche Stammformen greifen: "Webhook" findet auch "Webhooks"
- entprellt ab zwei Zeichen, laufende Abfragen werden abgebrochen
Dazu ein GIN-Index auf dem Suchausdruck. Prisma kann Ausdrucksindizes im
Schema nicht abbilden - nachgeprueft: "migrate diff" laesst ihn in Ruhe und
schlaegt kein DROP vor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher wurde alles als reiner Text ausgegeben. Jetzt werden Ueberschriften,
Listen, Tabellen, Codebloecke, Zitate und Links dargestellt, und aus
"- [ ] Aufgabe" werden echte Checklisten.
Sicherheitsrelevant: der Inhalt stammt vom Benutzer. Ohne rehype-sanitize
waere das ein XSS-Loch, sobald jemand rohes HTML in eine Notiz schreibt. Das
Standardschema erlaubt nur harmlose Elemente; ergaenzt wird nur, was fuer
Aufgabenlisten und Sprachklassen an Codebloecken noetig ist. Links bekommen
zusaetzlich target=_blank mit noopener und nofollow.
Die Formatierung steht in components.css: Tailwinds Preflight setzt alle
Abstaende und Schriftgroessen zurueck, Ueberschriften und Listen brauchen
also wieder Form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README bekommt einen eigenen Abschnitt mit den vier Wegen, den drei
Importstrategien und beiden Sicherungsarten - samt der Empfehlung, eine
Sicherung einmal in eine Wegwerf-Datenbank zurueckzuspielen. FEATURES
beschreibt dasselbe ausfuehrlicher.
In der ROADMAP wandert Export und Import zu den erledigten Punkten, mit den
getroffenen Entscheidungen: keine Tokens, keine Snapshots, Voll-Sicherung
nur fuer Administratoren. Die folgenden Punkte ruecken auf.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die JSON-Sicherung im Adminbereich eignet sich zum Umziehen. Zum
Wiederherstellen einer kaputten Instanz braucht es dagegen einen echten
Dump - Schema, Daten, Sequenzen, Indizes.
backup.ps1 und backup.sh fuehren pg_dump im Container aus, es muss also
nichts auf dem Rechner installiert sein. Benutzer und Datenbankname kommen
aus der .env, damit sie nicht doppelt gepflegt werden. Der Dump entsteht mit
--clean --if-exists und ist damit fuer sich allein einspielbar.
restore.* fragt vor dem Ueberschreiben nach und laesst sich ueber -Database
beziehungsweise NEBULA_DB_NAME auf eine andere Datenbank umlenken. Damit
laesst sich eine Sicherung pruefen, ohne die echte anzufassen - eine
Sicherung, die nie zurueckgespielt wurde, ist keine.
/backups liegt im .gitignore: die Dateien enthalten alle Daten der Instanz.
Geprueft am echten Bestand: Dump 30,9 KB mit allen neun Tabellen,
Rueckspielung in eine Pruefdatenbank ergab dieselben Zahlen, das Original
blieb unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dashboard: ein Datenmenue neben "Neues Projekt" mit Export aller Projekte
und Import. Projektseite: Ausklappmenue mit JSON und Markdown.
Adminbereich: Abschnitt "Sicherung" mit deutlichem Hinweis, was in der Datei
steht und wie vertraulich sie zu behandeln ist.
Der Import-Dialog fuehrt in zwei Schritten: Datei waehlen, Vorschau ansehen,
erst dann schreiben. Ein Wechsel der Strategie berechnet die Vorschau neu.
Der Ausfuehren-Knopf bleibt gesperrt, solange sich nichts aendern wuerde -
sonst waere er ein Angebot, das ins Leere laeuft. Fehlende Git-Verbindungen
werden gesondert hervorgehoben, mit dem Hinweis, dass Tokens nie in
Exportdateien stehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bislang lagen alle Daten ausschliesslich in der Datenbank - ein
"docker compose down -v" haette alles geloescht, ohne Rueckweg.
Export:
- alle eigenen Projekte oder ein einzelnes als JSON
- ein Projekt zusaetzlich als lesbares Markdown, inklusive letztem
bekannten Git-Stand
Was bewusst nicht in der Datei steht:
- Access Tokens. Die Verbindung wird nur beschrieben (Name, System,
Basis-URL), das Token muss beim Import neu hinterlegt werden. Eine
Exportdatei landet sonst schnell in einem Download-Ordner oder einem
Cloud-Abgleich.
- Repo-Snapshots. Reproduzierbar und gross; ein Abgleich holt sie zurueck.
- Datenbank-IDs und der Besitzer. Beides gilt nur in der Quellinstanz.
Import:
- die hochgeladene Datei ist Fremdinhalt und wird vor jeder Verwendung
streng gegen ein Zod-Schema geprueft
- Abgleich ueber den Slug, drei Strategien: ueberspringen, zusaetzlich
anlegen, ersetzen
- dryRun liefert dieselbe Auswertung, ohne zu schreiben - Grundlage der
Vorschau in der Oberflaeche
- Verbindungen werden ueber Name oder Provider samt Basis-URL zugeordnet;
fehlt eine, wird das Projekt ohne Verknuepfung angelegt und das gemeldet
- beim Ersetzen werden Notizen ausgetauscht statt zusammengefuehrt: das ist
vorhersagbar, und ein erneuter Import erzeugt keine Dubletten
Die Instanz-Sicherung fuer Administratoren enthaelt dagegen alles, was zum
Wiederherstellen noetig ist - Passwort-Hashes und Token-Chiffrate
eingeschlossen. Ohne dasselbe APP_SECRET sind die Tokens wertlos. Sitzungen
und Passkeys bleiben draussen: erstere sind kurzlebig, letztere haengen an
Geraet und rpID.
readJsonBody bekommt eine einstellbare Obergrenze - 256 KB reichen fuer eine
Exportdatei mit vielen Notizen nicht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README und FEATURES beschreiben den automatischen Abgleich samt
Zurueckhalten nach Fehlschlaegen und dem Weg ueber einen externen Zeitgeber.
Neu ist ROADMAP.md: die bisherige Liste "Ideen fuer spaeter" war eine lose
Aufzaehlung ohne Reihenfolge und ohne Aufwandsabschaetzung. Jetzt nach
Prioritaet gegliedert, jeder Punkt mit Aufwand und - wo es welche gibt -
offenen Fragen. Ganz oben steht Export und Import in vier Stufen, inklusive
der Frage, ob Git-Tokens mitgehen sollen (Vorschlag: nein).
Am Ende steht bewusst auch, was nicht kommt und warum - das erspart
spaetere Diskussionen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adminbereich, Abschnitt "Automatischer Abgleich":
- ein- und ausschalten
- Abstand von 15 Minuten bis taeglich
- Projekte je Durchlauf
- fertige und archivierte auslassen
- Zustand auf einen Blick: verknuepfte Projekte, letzter und naechster
Durchlauf, Kurzbericht des letzten Laufs
- Knopf "Jetzt abgleichen" fuer einen sofortigen Durchlauf
Der Endpunkt POST /api/admin/sync/run dient zugleich als Einstieg fuer einen
externen Zeitgeber, falls jemand lieber von aussen taktet als den eingebauten
Automatismus zu nutzen.
Im Projektdialog laesst sich ein einzelnes Projekt vom Abgleich ausnehmen -
nuetzlich bei einem Repo, dessen Token staendig Fehler wirft. Manuell
synchronisieren geht unabhaengig davon immer.
Die Zahl der in Frage kommenden Projekte wird ueber alle Konten gezaehlt: der
Abgleich ist ein Serverauftrag, keine Dateneinsicht. Ein Administrator sieht
dadurch weiterhin keine fremden Projekte.
Nachgewiesen: Durchlauf 1 aktualisiert das echte Repo und meldet das kaputte
als Fehlschlag, Durchlauf 2 stellt das kaputte zurueck, das ausgenommene
Projekt bleibt in beiden unberuehrt. Der Zeitplan loeste eigenstaendig aus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher musste jedes Projekt einzeln von Hand synchronisiert werden. Jetzt
gibt es einen Zeitgeber im Serverprozess, angestossen von
instrumentation.ts. Fuer eine Einzelinstanz ist das die einfachste
tragfaehige Loesung: kein zusaetzlicher Container, kein externer Cron, keine
zweite Konfiguration.
Ruecksicht auf fremde API-Kontingente war der bestimmende Gedanke:
- standardmaessig abgeschaltet - der Abgleich verbraucht fremdes Kontingent,
das soll eine bewusste Entscheidung sein
- je Durchlauf nur eine begrenzte Zahl Projekte, die am laengsten nicht
abgeglichenen zuerst; der Rest kommt beim naechsten Mal
- nach jedem Fehlschlag verdoppelt sich der Abstand bis zum naechsten
Versuch, gedeckelt beim Sechzehnfachen. Ein Repo mit abgelaufenem Token
blockiert damit nicht dauerhaft das Kontingent.
- kurze Pause zwischen zwei Projekten
- zwei Durchlaeufe ueberschneiden sich nie
- fertige und archivierte Projekte lassen sich auslassen
- einzelne Projekte lassen sich ausnehmen
Der Zaehler fuer Fehlschlaege wandert in syncProject: bei Erfolg zurueck auf
null, bei Fehlschlag hoch. So stimmt er auch nach einem manuellen Abgleich.
sync.ts wird zu sync/index.ts, damit Modul und Ordner nicht nebeneinander
stehen. Alle Importpfade bleiben unveraendert.
Zur Form der Abfrage in instrumentation.ts: Next uebersetzt die Datei auch
fuer den Edge-Runtime. Der Import muss deshalb *in* der positiven Bedingung
stehen, sonst bleibt er im Edge-Bundle stehen und der Build scheitert an
node:crypto. Mit einem frueh abbrechenden return passiert genau das.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Erstmals mit echtem Browser nachgesehen (headless Chrome ueber Playwright,
Screenshots aller Ansichten). Dabei fuenf Dinge gefunden:
1. Menue klebte am Fensterrand statt am Ausloeser. Bei einem Ausloeser weit
links wurde die bevorzugte rechtsbuendige Ausrichtung negativ und dann auf
den Rand geklemmt - das Menue stand quer ueber der Seite. Jetzt wird bei
Ueberlauf auf die andere Ausrichtung gewechselt und erst danach geklemmt.
2. .popover war mit 97 % nicht ganz deckend, wodurch "Bearbeiten" und
"Loeschen" schwach durchschienen. Jetzt vollstaendig deckend.
3. Der Schieberegler zeichnete die Spur rechts vom Griff in einem hellen
Standardton - ein grelles Band im dunklen Layout. Ersetzt durch eine
selbst gezeichnete Variante mit Farbverlauf und sichtbarem Griff. Auf der
Projektseite ersetzt sie zugleich das bisherige unsichtbare Eingabefeld
ueber einem gezeichneten Balken, das gar keinen Griff hatte.
4. Der Hinweis der Kachel "Ideen" wurde mitten im Wort abgeschnitten
("Noch ein Gedanke - nichts ge..."). Kurzer Text statt der langen
Statusbeschreibung.
5. Nach der Einrichtung stand im Adminbereich "noch nie angemeldet", obwohl
gerade eine Sitzung lief. Einrichtung und Registrierung setzen lastLoginAt
jetzt mit.
Die Konsole blieb ueber alle Ansichten hinweg frei von Fehlern und Warnungen
- die strikte CSP mit Nonce und strict-dynamic bricht also nichts.
Geprueft an einer separaten Wegwerf-Datenbank auf Port 3100, damit die
laufende Instanz unberuehrt blieb.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Das Statusmenue auf der Projektseite wurde von den Schaltflaechen
"Bearbeiten" und "Loeschen" ueberdeckt, war unten abgeschnitten und der
Fortschrittswert schien hindurch. Drei Ursachen, alle mit derselben Wurzel:
1. Die Karten setzen overflow-hidden fuer den Akzentstreifen und den
Farbverlaufsrand. Ein absolut positioniertes Kind wird daran
abgeschnitten.
2. .glass setzt backdrop-filter, die Projektkarte bekommt beim Hovern
zusaetzlich ein transform. Beides erzeugt einen Stacking-Context: das
z-index des Menues galt nur noch innerhalb der Karte und verlor gegen
spaeter gezeichnete Geschwister. Aus demselben Grund bezog sich das
"position: fixed" der Klickflaeche zum Schliessen auf die Karte statt
auf das Fenster - ein Klick daneben schloss das Menue nicht.
3. .glass hat 3,5 % Deckkraft. Als Kartenflaeche gut, als Overlay
unbrauchbar. Der eigene backdrop-filter half nicht, weil er innerhalb
eines Vorfahren mit backdrop-filter wirkungslos bleibt.
Behoben durch eine neue Popover-Komponente, die per createPortal am body
haengt, ihre Position aus dem Ankerelement berechnet und beim Scrollen und
bei Groessenaenderungen nachfuehrt. Reicht der Platz nach unten nicht, klappt
sie nach oben. Dazu die Klasse .popover mit nahezu deckendem Hintergrund.
Betrifft Statusauswahl, Benutzermenue und - aus demselben Grund - den
Modal-Dialog: der wird aus Karten heraus geoeffnet und haette sich sonst an
der Karte statt am Fenster ausgerichtet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README: Abschnitt zur ersten Anmeldung mit den beiden Betriebsarten und der
Einrichtung eines Passkeys, erweiterte Variablentabelle mit dem Hinweis, dass
Name und URL zur Laufzeit gelesen werden, vollstaendige API-Uebersicht sowie
ein Sicherheitsabschnitt, der auch die Grenzen benennt: kein HSTS, keine
TLS-Terminierung, Ratenbegrenzung nur im Arbeitsspeicher, kein
Passwort-Zuruecksetzen per E-Mail, und warum Anfragen ohne Origin- und
Sec-Fetch-Site-Header durchgelassen werden.
FEATURES: neues Kapitel zu Konten und Zugang, aktualisiertes Datenmodell und
ausgebauter Sicherheitsabschnitt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
APP_NAME, APP_TAGLINE und APP_URL sowie SESSION_TTL_DAYS und die drei
Schalter des SSRF-Schutzes werden an den App-Container durchgereicht.
Bei APP_URL steht ausdruecklich dabei, dass daraus die WebAuthn-rpID
abgeleitet wird, dass Passkeys einen sicheren Kontext brauchen und dass ein
spaeterer Hostnamenwechsel bestehende Passkeys ungueltig macht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher sah jede Anfrage alles. Jetzt traegt jede Abfrage eine
Besitzerbedingung, und zwar an erster Stelle in der where-Klausel statt als
nachtraeglicher Vergleich - so kann sie nicht versehentlich uebersprungen
werden.
- Lesen: findFirst statt findUnique, immer mit ownerId
- Schreiben und Loeschen: updateMany/deleteMany mit ownerId in der
where-Klausel. Fremde Datensaetze werden dadurch gar nicht erst getroffen,
und die betroffene Zeilenzahl sagt, ob es den Datensatz gab.
- Notizen haengen ueber project.ownerId am Besitzer
- Beim Verknuepfen eines Projekts wird geprueft, dass die Git-Verbindung dem
Aufrufer gehoert - sonst liesse sich eine fremde Verbindung und damit ein
fremder Token unterschieben
- Fremde IDs liefern 404 statt 403: die Antwort bestaetigt nicht einmal,
dass es den Datensatz gibt
- Ausgehende Endpunkte (Sync, Verbindungstest, Repo-Liste) zusaetzlich
ratenbegrenzt, Basis-URLs gegen den SSRF-Schutz geprueft
- Slug und Verbindungsname werden je Besitzer auf Eindeutigkeit geprueft
Nachgewiesen: ein zweites Konto erhaelt auf GET, PATCH, DELETE, Notiz
anlegen und Sync eines fremden Projekts durchgaengig 404, auf die
Admin-Endpunkte 403, und das Zielprojekt bleibt unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Mein Konto": Profil, Passwort setzen oder wechseln, Passkeys anlegen,
umbenennen und entfernen, aktive Sitzungen einsehen und ueberall abmelden.
Der letzte Passkey laesst sich nicht entfernen, solange kein Passwort
gesetzt ist - sonst waere das Konto ausgesperrt.
Admin-Einstellungen: Betriebsart umschalten, Selbstregistrierung freigeben,
Konten anlegen, bearbeiten, deaktivieren und loeschen. Die Instanzwerte aus
der .env werden angezeigt, damit sichtbar ist, warum Passkeys gegebenenfalls
nicht verfuegbar sind.
Schutzregeln:
- Der Wechsel in den Einzelbetrieb wird abgelehnt, solange mehr als ein
Konto existiert.
- Der letzte aktive Administrator kann weder herabgestuft noch deaktiviert
oder geloescht werden.
- Das eigene Konto laesst sich nicht loeschen oder deaktivieren.
- Deaktivieren oder ein neues Passwort beenden sofort alle Sitzungen des
Kontos.
Administratoren verwalten Konten, keine Inhalte: fremde Projekte sehen auch
sie nicht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- /setup fuehrt beim ersten Aufruf durch das Anlegen des Administratorkontos
und die Wahl der Betriebsart. Der Endpunkt funktioniert nur, solange es
keinen Benutzer gibt; danach kann sich niemand mehr nachtraeglich ein
Adminkonto erschleichen. Vorhandene Projekte ohne Besitzer werden dabei
uebernommen und im Formular vorher angekuendigt.
- /login mit Passwort oder Passkey. Der Passkey-Knopf entfaellt, wenn APP_URL
keinen sicheren Kontext ergibt - mit Hinweis, woran es liegt. Das Ziel nach
der Anmeldung wird auf relative Pfade begrenzt, sonst waere der
weiter-Parameter ein offener Redirect.
- /register nur im Mehrbenutzerbetrieb und nur, wenn ein Administrator die
Selbstregistrierung freigegeben hat.
- Kopfzeile mit Benutzermenue: eigenes Konto, Admin-Einstellungen, Abmelden.
Ohne Anmeldung bleibt sie auf das Logo reduziert.
- Layout liest Titel und Namen ueber generateMetadata, damit APP_NAME zur
Laufzeit greift statt beim Build eingefroren zu werden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kern der Benutzerverwaltung.
Passwoerter:
- scrypt aus der Node-Standardbibliothek (N=32768, r=8, p=1), speicherhart
und ohne zusaetzliches natives Modul
- Format scrypt$N$r$p$salt$hash, damit sich die Parameter spaeter anheben
lassen
- fakeVerifyDelay verbrennt bei unbekanntem Benutzernamen dieselbe
Rechenzeit, sonst liesse sich an der Antwortzeit ablesen, welche Konten
existieren
Passkeys ueber SimpleWebAuthn 13:
- Anmeldung ohne Benutzernamen ueber auffindbare Credentials
- Challenges liegen in der Datenbank und werden beim Einloesen geloescht,
koennen also nicht erneut eingespielt werden
- rpID und Origin kommen aus APP_URL
Sitzungen:
- Zufallstoken im HttpOnly-Cookie, in der Datenbank nur dessen Hash
- Abmelden und Sperren wirken sofort, weil bei jeder Anfrage nachgeschlagen
wird - anders als bei einem selbsttragenden Token
Middleware: CSRF-Pruefung ueber Sec-Fetch-Site bzw. Origin,
Sicherheitskopfzeilen inklusive CSP mit Per-Request-Nonce und
strict-dynamic, dazu eine grobe Zugangsschranke. Letztere ist ausdruecklich
keine Sicherheitsgrenze - im Edge-Runtime gibt es keine Datenbank.
Verbindlich entscheiden requireUserPage und requireUserApi, serverseitig,
in jeder Seite und jedem Endpunkt.
config.ts liest APP_NAME, APP_TAGLINE und APP_URL zur Laufzeit statt ueber
NEXT_PUBLIC_-Variablen. Die waeren beim Build fest ins Bundle geschrieben;
so laesst sich dasselbe Docker-Image ohne Neubau umbenennen und unter einer
anderen Adresse betreiben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neue Tabellen:
- User Konto mit Rolle, optionalem Passwort-Hash, Sperrzaehler.
Das Passwort ist optional, weil ein Konto auch ausschliesslich
per Passkey nutzbar sein darf.
- Session serverseitige Sitzung. Gespeichert wird nur der SHA-256-Hash
des Cookie-Tokens, damit ein Datenbankleck keine uebernehmbaren
Sitzungen ergibt.
- Credential ein Passkey je Zeile, mit oeffentlichem Schluessel, Transporten
und Signaturzaehler.
- WebAuthnChallenge kurzlebig und einmalig verwendbar, deshalb in der
Datenbank statt im Cookie.
- AppSettings Betriebsart und Registrierung, genau eine Zeile.
Project und GitConnection bekommen einen Besitzer. Bewusst nullable: so
ueberstehen Bestandsdaten die Migration und werden beim Anlegen des ersten
Kontos uebernommen. Slug und Verbindungsname sind dadurch nur noch je
Besitzer eindeutig - zwei Personen duerfen beide ein Projekt "dashboard"
haben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Basis-URL einer Git-Verbindung ist eine Benutzereingabe, die der Server
anschliessend selbst abruft. Ohne Pruefung liesse sich darueber jede vom
Server erreichbare Adresse ansprechen - Cloud-Metadaten unter
169.254.169.254, lokal gebundene Dienste oder ein Portscan im Heimnetz.
ssrf.ts prueft deshalb vor jedem ausgehenden Aufruf:
- Loopback, Link-Local samt Metadaten-Endpunkt, unspezifizierte Adressen,
Multicast und CGNAT sind gesperrt
- das private LAN bleibt erlaubt, weil dort ueblicherweise die eigene
Gitea-Instanz steht; abschaltbar ueber GIT_BLOCK_PRIVATE
- GIT_ALLOW_LOOPBACK fuer eine Instanz auf demselben Rechner,
GIT_ALLOWED_HOSTS als strengster Modus
- Hostnamen werden aufgeloest und *jede* zurueckgegebene Adresse geprueft,
sonst genuegte ein zweiter A-Record fuer DNS-Rebinding
- nach einem Redirect wird das tatsaechliche Ziel erneut geprueft
Dazu:
- rateLimit.ts, ein Zaehler im Arbeitsspeicher mit vorkonfigurierten
Kontingenten fuer Anmeldung, ausgehende Aufrufe und WebAuthn
- request.ts liest JSON-Bodies nur mit Content-Type application/json und
hoechstens 256 KB. Die Content-Type-Pflicht ist zugleich CSRF-Schutz in
der Tiefe: ein Cross-Site-Formular kommt ohne Preflight nur mit
text/plain, multipart oder urlencoded durch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Im Suchfeld lag das Lupensymbol auf dem Platzhaltertext. Ursache war die
Kaskade: .field steht mit der Kurzschreibweise "padding: .6rem .85rem" im
Stylesheet hinter Tailwinds Utilities, also gewann sie gegen "pl-9" und das
linke Padding blieb bei 13,6 Pixeln - zu wenig fuer das Symbol.
Die wiederkehrenden Bausteine liegen jetzt in einer eigenen Datei und dort in
@layer components. Tailwind sortiert die Layer als theme, base, components,
utilities; damit gewinnen Utilities wieder gegen die Komponentenklasse.
Das behebt nicht nur die Ueberlappung: auch alle Schaltflaechen mit
"btn ... px-2.5 py-1.5" wurden bisher in der Standardgroesse gerendert, weil
.btn ebenfalls eine padding-Kurzschreibweise setzt. Sie sind jetzt so
kompakt wie vorgesehen.
Geprueft am gebauten Stylesheet: .field liegt an Position 11107 im
components-Layer, .pl-9 an 31324 - die Utility gewinnt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Haelt fest, womit beide Betriebsarten tatsaechlich verifiziert wurden, und
erklaert die Deprecation-Warnung von Prisma 6.19 samt Grund, warum die
Umstellung auf prisma.config.ts noch aussteht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Anwendung nutzt ausschliesslich Route Handler, keine Server Actions. Die
Option bewirkte nichts, liess Next beim Build aber "Experiments (use with
caution)" ausgeben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der App-Container brach beim Start mit "Cannot find module 'effect'" ab und
lief in die Wiederholschleife des Entrypoints. Ursache: aus dem Builder
wurden nur node_modules/prisma und node_modules/@prisma uebernommen. Die CLI
laedt aber @prisma/config, und das zieht einen eigenen Abhaengigkeitsbaum
(effect, c12, …) nach, der ausserhalb dieser beiden Verzeichnisse liegt.
Statt einzelne Pakete nachzukopieren bekommt die CLI jetzt eine eigene,
minimale Installation unter /opt/prisma-cli. Die Version wird aus der
package.json des Builders gelesen, damit CLI und generierter Client nicht
auseinanderlaufen. Der Entrypoint bricht ausserdem mit klarer Meldung ab,
wenn die CLI fehlt, statt fuenfzehnmal auf eine vermeintlich nicht bereite
Datenbank zu warten.
Verifiziert: docker compose --profile full up -d --build spielt die
Migrationen ein, der Container meldet sich als healthy und erreicht die
Git-APIs nach aussen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>