diff --git a/FEATURES.md b/FEATURES.md index fc4c275..ce94c65 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -138,6 +138,24 @@ Bedienung über Tastatur: Pfeile wählen, `Enter` öffnet, `Esc` schließt. Die wird beim ersten Öffnen geholt und dann behalten — sie bei jedem Tastendruck neu zu laden wäre Verschwendung. +### Projektvorlagen + +Ein Projekt, das sich bewährt hat, lässt sich im Projektkopf über **Als Vorlage sichern** +festhalten: Status, Priorität, Farbe, Tags, Notizen und offene Aufgaben. Beim Anlegen eines +neuen Projekts steht die Vorlage oben im Dialog zur Auswahl. + +Bewusst benutzereigen statt fest eingebaut — welche Tags und welche Startaufgaben sinnvoll +sind, weiß nur, wer damit arbeitet. Eine mitgelieferte Liste wäre geraten. + +- **Erledigte Aufgaben kommen nicht mit.** Eine Vorlage beschreibt, was zu tun ist, nicht + was einmal getan wurde. +- **Repositories gehen nicht mit.** Eine Vorlage, die jedes neue Projekt an dasselbe + Repository hängt, wäre eine Falle. +- Die Stammdaten füllt der Dialog **sichtbar** in die Felder, damit sie vor dem Speichern + noch änderbar sind; bereits Getipptes wird nicht überschrieben. Notizen und Aufgaben legt + erst der Server an — im Dialog ließen sie sich nicht darstellen. +- Vorlagen überleben das Löschen ihres Ursprungsprojekts. + ### Mehrfachauswahl Ein Kästchen auf jeder Karte — es erscheint erst beim Überfahren, damit nicht auf jeder @@ -301,6 +319,7 @@ Firmen-Gitea nebeneinander. | Tags | bis zu 25, mit SHA, Nachricht und Datum | | Commits | die letzten 15 zur Anzeige; geholt werden 100 für die Aktivitätskurve | | **Commit-Aktivität** | Balkenreihe der letzten Tage, auf Karte und Projektseite | +| **Commit-Heatmap** | Ein Feld je Tag über bis zu ein Jahr, auf der Projektseite | | Releases | bis zu 10, inklusive Pre-Release- und Draft-Kennzeichnung | | Sprachen | Verteilung als Balken mit Prozentwerten | @@ -327,9 +346,19 @@ bewusste Entscheidung sein. Einschalten in den Admin-Einstellungen. | Einstellung | Bedeutung | | --- | --- | | Abstand | 15 Minuten bis täglich | -| Projekte je Durchlauf | 1–100, Voreinstellung 25 | +| Repositories je Durchlauf | 1–100, Voreinstellung 25 | | Fertige und archivierte auslassen | Dort ändert sich selten noch etwas | +Gezählt und begrenzt wird je **Repository**, nicht je Projekt: sonst zöge ein Projekt mit +fünf Repos fünfmal so viele Anfragen nach sich, wie sein Platz im Kontingent hergibt. + +Die Daten für die **Heatmap** kommen getrennt und höchstens einmal am Tag — an einem +Jahresüberblick ändert sich von Stunde zu Stunde nichts Sichtbares. Bei GitHub genügt dafür +ein Aufruf der Jahresstatistik; GitLab und Gitea kennen keine, dort werden bis zu fünf +Seiten Commits geholt. Reicht das nicht ein Jahr zurück, **beginnt die Heatmap beim +ältesten bekannten Commit** und sagt es dazu — leere Felder für Zeiträume zu zeichnen, über +die nichts bekannt ist, wäre eine Lüge. + Wie der Durchlauf arbeitet: - Der Zeitgeber läuft im Serverprozess selbst (`instrumentation.ts`), prüft minütlich, ob diff --git a/README.md b/README.md index 1653160..33a6737 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,9 @@ ausschließlich Daten des angemeldeten Kontos. | `GET` | `/api/projects/export` | Alle eigenen Projekte als JSON | | `GET` | `/api/projects/:id/export` | Ein Projekt (`?format=md` für Markdown) | | `POST` | `/api/projects/import` | Import, mit `dryRun` für die Vorschau | +| `GET` `POST` | `/api/templates` | Projektvorlagen auflisten / anlegen | +| `PATCH` `DELETE` | `/api/templates/:id` | Vorlage ändern / löschen | +| `POST` | `/api/templates/from-project` | Aus einem Projekt eine Vorlage machen | | `GET` | `/api/admin/backup` | Vollständige Sicherung als JSON (Admin) | | `GET` | `/api/search?q=` | Volltextsuche über die eigenen Notizen | | `GET` `POST` | `/api/notifications` | Benachrichtigungsziele | @@ -348,6 +351,8 @@ ausschließlich Daten des angemeldeten Kontos. | `POST` | `/api/notifications/:id/test` | Testnachricht senden | | `GET` `PATCH` `DELETE` | `/api/projects/:id` | Projekt lesen / ändern / löschen | | `POST` | `/api/projects/:id/sync` | Alle Repositories des Projekts abgleichen | +| `PATCH` | `/api/projects/reorder` | Neue Reihenfolge im Brett | +| `POST` | `/api/projects/bulk` | Status, Tags oder Löschen für mehrere | | `GET` | `/api/projects/:id/activity` | Verlauf des Projekts (`?limit=`) | | `GET` `POST` | `/api/projects/:id/repositories` | Repositories des Projekts | | `PATCH` `DELETE` | `/api/repositories/:id` | Repository ändern / vom Projekt lösen | diff --git a/ROADMAP.md b/ROADMAP.md index d356b3e..2d356ec 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,37 +20,26 @@ erreichbar ist. Der Zeitplan bleibt als Rückfalllösung. --- -## 2 · Danach +## 2 · Größere Brocken -### 2.1 Commit-Aktivität als Heatmap `S` - -Die Sparkline gibt es auf Karte und Projektseite. Eine Heatmap über ein Jahr -wäre die nächste Ausbaustufe — dafür braucht es allerdings mehr als eine -Seite Commits, also Paginierung oder die Statistik-Endpunkte. - ---- - -## 3 · Größere Brocken - -### 3.1 Passwort-Zurücksetzen per E-Mail `M` +### 2.1 Passwort-Zurücksetzen per E-Mail `M` Heute setzt ein Administrator das Passwort neu. Selbstbedienung braucht SMTP und damit eine weitere Konfiguration — lohnt erst im Mehrbenutzerbetrieb. -### 3.2 Mehrsprachigkeit `L` +### 2.2 Mehrsprachigkeit `L` Die Oberfläche ist durchgängig deutsch verdrahtet. Englisch dazu wäre eine Voraussetzung, falls du das Projekt veröffentlichen willst. --- -## 4 · Kleinigkeiten +## 3 · Kleinigkeiten | Idee | Aufwand | Nutzen | | --- | --- | --- | | Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste | | Wiederkehrende Aufgaben | `M` | Etwa „jeden Montag Abhängigkeiten prüfen" | -| Projektvorlagen | `S` | Neues Projekt mit vorgefüllten Tags und Notizen | | Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel | | Eigenes Logo und Farbe | `S` | Ergänzt `APP_NAME` | | Barrierefreiheits-Durchsicht | `M` | Fokusreihenfolge, Screenreader, Kontraste | @@ -58,7 +47,7 @@ Voraussetzung, falls du das Projekt veröffentlichen willst. --- -## 5 · Bewusst nicht +## 4 · Bewusst nicht Damit klar ist, was **nicht** kommt und warum: @@ -94,6 +83,7 @@ Damit klar ist, was **nicht** kommt und warum: | **v0.9.0** Schnellsuche per `Strg+K`, Aufgaben aus Git-Issues | ✓ | | **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 | ✓ | 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 77ffa0d..c3c63f2 100644 --- a/e2e/api.spec.ts +++ b/e2e/api.spec.ts @@ -374,6 +374,94 @@ test("Brett: Reihenfolge und Mehrfachauswahl", async ({ request, playwright }) = expect((await geloescht.json()).changed).toBe(3); }); +test("Projektvorlagen: aus Projekt sichern und anwenden", async ({ request, playwright }) => { + await login(request, ADMIN); + + // Ein Projekt herrichten, das als Vorlage taugt. + const quelle = ( + await ( + await request.post("/api/projects", { + data: { name: "Vorlagenquelle", status: "PLANNING", priority: 3, tags: ["web", "docker"] }, + }) + ).json() + ).project; + + await request.post(`/api/projects/${quelle.id}/notes`, { + data: { content: "Checkliste vor dem Start." }, + }); + const erledigt = ( + await ( + await request.post(`/api/projects/${quelle.id}/tasks`, { data: { title: "Schon fertig" } }) + ).json() + ).task; + await request.patch(`/api/tasks/${erledigt.id}`, { data: { status: "DONE" } }); + await request.post(`/api/projects/${quelle.id}/tasks`, { data: { title: "Repo anlegen" } }); + await request.post(`/api/projects/${quelle.id}/tasks`, { data: { title: "CI einrichten" } }); + + const gesichert = await request.post("/api/templates/from-project", { + data: { projectId: quelle.id, name: "Web-App", description: "Mit Docker und CI" }, + }); + expect(gesichert.status()).toBe(201); + const vorlage = (await gesichert.json()).template; + + expect(vorlage.tags).toEqual(["web", "docker"]); + expect(vorlage.status).toBe("PLANNING"); + expect(vorlage.notes).toHaveLength(1); + // Erledigte Aufgaben kommen nicht mit: eine Vorlage beschreibt, was zu tun ist. + expect(vorlage.tasks).toHaveLength(2); + expect(vorlage.tasks.map((task: { title: string }) => task.title)).not.toContain("Schon fertig"); + + // Derselbe Name ein zweites Mal wird abgewiesen. + const doppelt = await request.post("/api/templates/from-project", { + data: { projectId: quelle.id, name: "Web-App" }, + }); + expect(doppelt.status()).toBe(409); + + // Anwenden: Notizen und Aufgaben entstehen mit dem Projekt. + const neu = ( + await ( + await request.post("/api/projects", { + data: { name: "Aus Vorlage", status: "IDEA", templateId: vorlage.id }, + }) + ).json() + ).project; + + const geladen = (await (await request.get(`/api/projects/${neu.id}`)).json()).project; + expect(geladen.notes).toHaveLength(1); + expect(geladen.tasks).toHaveLength(2); + // Die Reihenfolge der Vorlage bleibt erhalten. + expect(geladen.tasks.map((task: { title: string }) => task.title)).toEqual([ + "Repo anlegen", + "CI einrichten", + ]); + // Der ausdrueckliche Status gewinnt gegen den der Vorlage. + expect(geladen.status).toBe("IDEA"); + + // Der Verlauf haelt die Herkunft fest. + const verlauf = (await (await request.get(`/api/projects/${neu.id}/activity`)).json()).activities; + expect(verlauf.some((entry: { summary: string }) => entry.summary.includes("Web-App"))).toBe(true); + + // Eine fremde Vorlage laesst sich nicht unterschieben. + const fremd = await playwright.request.newContext({ + baseURL: test.info().project.use.baseURL, + }); + await login(fremd, ZWEITER); + expect((await (await fremd.get("/api/templates")).json()).templates).toHaveLength(0); + const untergeschoben = await fremd.post("/api/projects", { + data: { name: "Klau", templateId: vorlage.id }, + }); + expect(untergeschoben.status()).toBe(404); + expect((await fremd.delete(`/api/templates/${vorlage.id}`)).status()).toBe(404); + await fremd.dispose(); + + // Und die Vorlage ueberlebt das Loeschen ihres Ursprungsprojekts. + await request.delete(`/api/projects/${quelle.id}`); + const nochDa = (await (await request.get("/api/templates")).json()).templates; + expect(nochDa).toHaveLength(1); + + expect((await request.delete(`/api/templates/${vorlage.id}`)).ok()).toBeTruthy(); +}); + test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async ({ request }) => { await login(request, ADMIN); diff --git a/package.json b/package.json index 872b2f4..8f3737d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)", "scripts": { diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index 6ffdaf3..1a321c5 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -41,6 +41,38 @@ export const KIND_META: Record }; export const CHANGELOG: Release[] = [ + { + version: "1.2.0", + date: "2026-07-26", + headline: "Ein Jahr auf einen Blick, und ein Start ohne Leerlauf.", + changes: [ + { + kind: "neu", + text: + "Commit-Heatmap auf der Projektseite: ein Feld je Tag über bis zu ein Jahr, " + + "mit Farbskala und Datum im Tooltip.", + }, + { + kind: "neu", + text: + "Projektvorlagen. Ein bewährtes Projekt über „Als Vorlage sichern“ festhalten — " + + "Tags, Notizen und offene Aufgaben stehen dann beim nächsten Projekt bereit.", + }, + { + kind: "besser", + text: + "Die Heatmap-Daten kommen höchstens einmal am Tag und getrennt vom übrigen " + + "Abgleich. Bei GitHub genügt ein Aufruf der Jahresstatistik, bei GitLab und " + + "Gitea werden bis zu fünf Seiten Commits geholt.", + }, + { + kind: "besser", + text: + "Reicht die Historie nicht ein Jahr zurück, beginnt die Heatmap beim ältesten " + + "bekannten Commit — leere Felder für unbekannte Zeiträume wären eine Lüge.", + }, + ], + }, { version: "1.1.0", date: "2026-07-26",