From 01884f4747522dbca7bab65f85ffa430693dbd51 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 26 Jul 2026 23:20:40 +0200 Subject: [PATCH] test: wiederkehrende Aufgaben abdecken, Doku, Version 1.3.0 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) --- FEATURES.md | 30 +++++++++++++++++-- ROADMAP.md | 35 ++++++++++++++++++++-- e2e/api.spec.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 15 ++++++++-- package.json | 3 +- src/lib/changelog.ts | 33 +++++++++++++++++++++ 6 files changed, 178 insertions(+), 8 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index ce94c65..020844b 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -207,6 +207,23 @@ Blockiert · Erledigt**. - Erledigtes wird durchgestrichen; der Zeitpunkt wird festgehalten und beim Zurückziehen wieder gelöscht. +### Wiederkehrende Aufgaben + +Täglich, wöchentlich, alle zwei Wochen oder monatlich. Wandert die Aufgabe nach +**Erledigt** — im Dialog oder durch Ziehen —, entsteht die nächste von selbst. Die +erledigte bleibt stehen, sonst ginge die Historie verloren. + +- **Gerechnet wird vom Fälligkeitsdatum, nicht vom Abhaken.** Eine wöchentliche 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 überfällig ist, hilft niemandem. +- Beim Monatsintervall bleibt der Tag im Monat erhalten. Der 31. Januar wird zum + 28. Februar, nicht zum 3. März. +- Nur der **Übergang** nach Erledigt löst aus. Eine bereits erledigte Aufgabe erneut zu + speichern erzeugt keine zweite Fassung. +- Bewusst grob gehalten: feinere Regeln („jeden zweiten Dienstag im Monat") braucht ein + Projektdashboard nicht und wären ohne Kalenderansicht kaum nachvollziehbar. + ### Aus Git-Issues übernehmen Im Git-Panel, Reiter *Issues*: ein Klick macht aus den offenen Issues Aufgaben — Titel, @@ -502,8 +519,14 @@ letzten Zustellung stehen an der Karte. - **Animationen**: gestaffelt einblendende Karten, Hover-Lift mit Farbverlaufsrand, animierte Fortschrittsbalken. - **Responsiv** von Handy bis Ultrawide (bis zu vier Kartenspalten). -- **Barrierearm**: durchgehende ARIA-Labels, Escape schließt Dialoge, sichtbarer - Fokusring, `prefers-reduced-motion` schaltet Animationen ab. +- **Barrierefrei nach WCAG 2.1 AA**, und zwar geprüft statt behauptet: axe-core läuft über + zehn Seitenzustände einschließlich der Dialoge und meldet keine Verstöße. Durchgehende + ARIA-Labels, Escape schließt Dialoge, sichtbarer Fokusring, `prefers-reduced-motion` + schaltet Animationen ab. + Bei der ersten Durchsicht fielen 26 Kontrastverstöße auf: gedämpfter Text erreichte auf + dem sehr dunklen Untergrund nur 3,8 statt 4,5 zu 1. Statt an hundert Stellen die Klasse + zu tauschen, sind die beiden gedämpften Stufen im Theme aufgehellt — der Abstand + zwischen ihnen bleibt sichtbar. - **Deutschsprachig**, Datums- und Zahlenformate in `de-DE` mit fester Zeitzone Europe/Berlin, damit Server- und Browser-Darstellung übereinstimmen. - **Als App installierbar.** Ein Web-Manifest mit eigenen Symbolen — auf dem Handy @@ -532,7 +555,8 @@ letzten Zustellung stehen an der Karte. | `AppSettings` | Betriebsart und Registrierung (eine Zeile) | | `Project` | Projektstammdaten, Status, Fortschritt, Besitzer | | `Repository` | Repositories eines Projekts inkl. Abgleichstand (n:1 zu Projekt) | -| `Task` | Aufgaben mit Spalte, Position, Fälligkeit, Labels und Issue-Herkunft | +| `Task` | Aufgaben mit Spalte, Position, Fälligkeit, Labels, Wiederholung und Issue-Herkunft | +| `ProjectTemplate` | Vorlagen je Konto: Stammdaten, Tags, Notizen und Aufgaben | | `Activity` | Verlauf je Projekt, neueste zuerst, auf 200 Einträge begrenzt | | `NotificationTarget` | Zustellwege je Konto, mit verschlüsseltem Geheimnis | | `Note` | Notizen (n:1 zu Projekt, Cascade-Delete) | diff --git a/ROADMAP.md b/ROADMAP.md index 2d356ec..db8b3aa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,6 +20,28 @@ erreichbar ist. Der Zeitplan bleibt als Rückfalllösung. --- +### 1.2 README-Vorschau `S` + +Das README aus dem Repo holen und auf der Projektseite anzeigen. Ein +zusätzlicher API-Aufruf, Markdown kann Nebula schon. Erspart den Wechsel ins +Git, nur um zu sehen, worum ein halbvergessenes Projekt eigentlich ging. + +### 1.3 Läuft es noch? `M` + +Eine Health-Check-URL je Projekt, die der Zeitgeber regelmäßig anpingt. +Nebula wüsste dann, ob ein Projekt nicht nur existiert, sondern auch läuft. +Nutzt Zeitgeber und Benachrichtigungen, die es schon gibt. Der SSRF-Schutz +greift dabei wie bei den Git-Aufrufen. + +### 1.4 Persönliche API-Token `M` + +Derzeit geht die API nur mit Sitzungscookie. Ein Token je Konto würde +`curl`-Aufrufe und Handy-Shortcuts erlauben — Idee aus dem Terminal anlegen, +ohne den Browser zu öffnen. Nur der Hash in der Datenbank, wie bei den +Sitzungen. + +--- + ## 2 · Größere Brocken ### 2.1 Passwort-Zurücksetzen per E-Mail `M` @@ -38,11 +60,14 @@ Voraussetzung, falls du das Projekt veröffentlichen willst. | Idee | Aufwand | Nutzen | | --- | --- | --- | +| Wochenrückblick | `S` | „Was ist diese Woche passiert" über alle Projekte, aus dem Aktivitätsverlauf | +| Globale Zeitleiste | `S` | Derselbe Verlauf, aber projektübergreifend als eigene Seite | +| Suche über Aufgaben | `S` | Die Volltextsuche deckt bisher nur Notizen ab | | Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste | -| Wiederkehrende Aufgaben | `M` | Etwa „jeden Montag Abhängigkeiten prüfen" | +| Aufgabe ↔ Commit verknüpfen | `M` | Enthält ein Commit `#5`, wird die übernommene Aufgabe verlinkt | +| Tastaturkürzel-Übersicht mit `?` | `S` | Es gibt inzwischen genug davon | | Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel | | Eigenes Logo und Farbe | `S` | Ergänzt `APP_NAME` | -| Barrierefreiheits-Durchsicht | `M` | Fokusreihenfolge, Screenreader, Kontraste | | Audit-Log für Adminaktionen | `M` | Nachvollziehbar, wer Konten geändert hat | --- @@ -54,6 +79,11 @@ Damit klar ist, was **nicht** kommt und warum: - **Geteilte Projekte zwischen Konten.** Die Trennung ist derzeit hart und einfach zu prüfen. Freigaben bringen eine Rechteverwaltung mit sich — nur sinnvoll, wenn wirklich mehrere Leute zusammenarbeiten. +- **Veraltete Abhängigkeiten prüfen.** Klingt verlockend, aber Renovate und + Dependabot machen das besser, und es wären viele Aufrufe an fremde + Registries — für eine Zahl, die im Repo selbst schon steht. +- **Anhänge an Notizen.** Bräuchte Dateispeicher und damit eine zweite Sorge + bei jeder Sicherung. Ein Link ins Git tut es auch. - **Eigene Git-Operationen** (klonen, committen, pushen). Nebula ist eine Übersicht, keine Entwicklungsumgebung. - **Zeiterfassung, Rechnungen, Kundenverwaltung.** Andere Baustelle. @@ -84,6 +114,7 @@ Damit klar ist, was **nicht** kommt und warum: | **v1.0.0** Aktivitätsverlauf je Projekt, Zwei-Faktor per TOTP, mehrere Repositories je Projekt | ✓ | | **v1.1.0** Freie Reihenfolge im Brett, Mehrfachauswahl, installierbar als App, Änderungsverlauf | ✓ | | **v1.2.0** Commit-Heatmap über ein Jahr, Projektvorlagen | ✓ | +| **v1.3.0** Wiederkehrende Aufgaben, Barrierefreiheit nach WCAG 2.1 AA | ✓ | Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben diff --git a/e2e/api.spec.ts b/e2e/api.spec.ts index c3c63f2..b795283 100644 --- a/e2e/api.spec.ts +++ b/e2e/api.spec.ts @@ -374,6 +374,76 @@ test("Brett: Reihenfolge und Mehrfachauswahl", async ({ request, playwright }) = expect((await geloescht.json()).changed).toBe(3); }); +test("Wiederkehrende Aufgaben legen die naechste Fassung an", async ({ request }) => { + await login(request, ADMIN); + + const projekt = ( + await (await request.post("/api/projects", { data: { name: "Wiederkehrend" } })).json() + ).project; + + const woechentlich = ( + await ( + await request.post(`/api/projects/${projekt.id}/tasks`, { + data: { + title: "Abhaengigkeiten pruefen", + dueDate: "2026-07-06T12:00:00", + recurrence: "WEEKLY", + labels: ["wartung"], + }, + }) + ).json() + ).task; + + // Ein Zwischenschritt loest noch nichts aus. + await request.patch(`/api/tasks/${woechentlich.id}`, { data: { status: "DOING" } }); + let alle = (await (await request.get(`/api/projects/${projekt.id}/tasks`)).json()).tasks; + expect(alle).toHaveLength(1); + + // Erst der Uebergang nach ERLEDIGT. + const erledigt = await request.patch(`/api/tasks/${woechentlich.id}`, { + data: { status: "DONE" }, + }); + expect((await erledigt.json()).next).not.toBeNull(); + + alle = (await (await request.get(`/api/projects/${projekt.id}/tasks`)).json()).tasks; + expect(alle).toHaveLength(2); + + const folge = alle.find((task: { id: string }) => task.id !== woechentlich.id); + expect(folge.status).toBe("TODO"); + expect(folge.title).toBe("Abhaengigkeiten pruefen"); + expect(folge.labels).toEqual(["wartung"]); + expect(folge.recurrence).toBe("WEEKLY"); + expect(folge.recurredFrom).toBe(woechentlich.id); + // Der Termin liegt in der Zukunft und auf demselben Wochentag. + expect(new Date(folge.dueDate).getTime()).toBeGreaterThan(Date.now()); + expect(new Date(folge.dueDate).getUTCDay()).toBe(1); + + // Erneut speichern erzeugt keine dritte. + await request.patch(`/api/tasks/${woechentlich.id}`, { data: { title: "Umbenannt" } }); + alle = (await (await request.get(`/api/projects/${projekt.id}/tasks`)).json()).tasks; + expect(alle).toHaveLength(2); + + // Der zweite Weg: durch Ziehen in die Spalte ERLEDIGT. + const gezogen = await request.patch(`/api/projects/${projekt.id}/tasks/reorder`, { + data: { columns: { DONE: [folge.id] } }, + }); + expect(gezogen.ok()).toBeTruthy(); + + alle = (await (await request.get(`/api/projects/${projekt.id}/tasks`)).json()).tasks; + expect(alle).toHaveLength(3); + expect(alle.filter((task: { status: string }) => task.status === "TODO")).toHaveLength(1); + + // Eine einmalige Aufgabe bleibt einmalig. + const einmalig = ( + await ( + await request.post(`/api/projects/${projekt.id}/tasks`, { data: { title: "Nur einmal" } }) + ).json() + ).task; + await request.patch(`/api/tasks/${einmalig.id}`, { data: { status: "DONE" } }); + alle = (await (await request.get(`/api/projects/${projekt.id}/tasks`)).json()).tasks; + expect(alle).toHaveLength(4); +}); + test("Projektvorlagen: aus Projekt sichern und anwenden", async ({ request, playwright }) => { await login(request, ADMIN); diff --git a/package-lock.json b/package-lock.json index 2b48e5e..44888e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nebula", - "version": "1.0.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nebula", - "version": "1.0.0", + "version": "1.2.0", "hasInstallScript": true, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -34,6 +34,7 @@ "@types/qrcode": "^1.5.6", "@types/react": "^19.1.5", "@types/react-dom": "^19.1.5", + "axe-core": "^4.12.1", "dotenv": "^17.4.2", "prisma": "^6.8.2", "tailwindcss": "^4.1.7", @@ -2536,6 +2537,16 @@ "node": ">=12" } }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", diff --git a/package.json b/package.json index 8f3737d..f5bd137 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula", - "version": "1.2.0", + "version": "1.3.0", "private": true, "description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)", "scripts": { @@ -56,6 +56,7 @@ "@types/qrcode": "^1.5.6", "@types/react": "^19.1.5", "@types/react-dom": "^19.1.5", + "axe-core": "^4.12.1", "dotenv": "^17.4.2", "prisma": "^6.8.2", "tailwindcss": "^4.1.7", diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index 1a321c5..a2b088e 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -41,6 +41,39 @@ export const KIND_META: Record }; export const CHANGELOG: Release[] = [ + { + version: "1.3.0", + date: "2026-07-26", + headline: "Was sich wiederholt, und was besser lesbar ist.", + changes: [ + { + kind: "neu", + text: + "Wiederkehrende Aufgaben: täglich, wöchentlich, alle zwei Wochen oder monatlich. " + + "Wandert die Aufgabe nach „Erledigt“, entsteht die nächste von selbst.", + }, + { + kind: "behoben", + text: + "Gedämpfter Text erreichte auf dem dunklen Untergrund nur 3,8 statt der " + + "geforderten 4,5 zu 1 — an 26 Stellen. Beide Stufen sind jetzt aufgehellt, " + + "der Abstand zwischen ihnen bleibt sichtbar.", + }, + { + kind: "besser", + text: + "Die Barrierefreiheit ist mit axe-core über zehn Seitenzustände geprüft, " + + "einschließlich der Dialoge. Keine Verstöße mehr gegen WCAG 2.1 AA.", + }, + { + kind: "besser", + text: + "Der Folgetermin rechnet vom Fälligkeitsdatum, nicht vom Abhaken — eine " + + "wöchentliche Aufgabe bleibt auf ihrem Wochentag. Lange Liegengebliebenes " + + "rutscht so weit vor, bis der Termin in der Zukunft liegt.", + }, + ], + }, { version: "1.2.0", date: "2026-07-26",