feat(ui): Aenderungsverlauf hinter der Versionsnummer
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>
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
import { CHANGELOG, KIND_META } from "@/lib/changelog";
|
||||||
|
import { cn, formatDate } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Versionsnummer in der Fusszeile oeffnet den Aenderungsverlauf.
|
||||||
|
*
|
||||||
|
* Kein eigener Menuepunkt: gesucht wird so etwas erfahrungsgemaess dort, wo
|
||||||
|
* die Version steht – und einen Platz in der Navigation ist es nicht wert.
|
||||||
|
*/
|
||||||
|
export function ChangelogButton({ version }: { version: string }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const aktuell = CHANGELOG[0]?.version;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="rounded font-mono text-slate-600 underline decoration-dotted underline-offset-4 transition-colors hover:text-violet-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400/60"
|
||||||
|
title="Was ist neu?"
|
||||||
|
>
|
||||||
|
v{version}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
width="lg"
|
||||||
|
title="Änderungsverlauf"
|
||||||
|
description={
|
||||||
|
// Ein Hinweis, falls die laufende Fassung neuer ist als der Eintrag.
|
||||||
|
// Das passiert, wenn jemand die Version hochzaehlt und den Eintrag
|
||||||
|
// vergisst – dann soll es auffallen statt still danebenzustehen.
|
||||||
|
aktuell === version
|
||||||
|
? "Was in den letzten Fassungen dazugekommen ist."
|
||||||
|
: `Diese Instanz läuft auf v${version}; der Verlauf endet bei v${aktuell}.`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ol className="space-y-7">
|
||||||
|
{CHANGELOG.map((release) => (
|
||||||
|
<li key={release.version}>
|
||||||
|
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||||
|
<h3 className="font-mono text-base font-semibold text-white">
|
||||||
|
v{release.version}
|
||||||
|
</h3>
|
||||||
|
{release.version === version ? (
|
||||||
|
<span className="rounded-full bg-violet-500/15 px-2 py-0.5 text-[10px] uppercase tracking-wider text-violet-200 ring-1 ring-violet-400/30">
|
||||||
|
installiert
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span className="text-xs text-slate-500">{formatDate(release.date)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-1 text-sm text-slate-400">{release.headline}</p>
|
||||||
|
|
||||||
|
<ul className="mt-3 space-y-2">
|
||||||
|
{release.changes.map((change, index) => {
|
||||||
|
const meta = KIND_META[change.kind];
|
||||||
|
return (
|
||||||
|
<li key={index} className="flex gap-2.5 text-sm leading-snug text-slate-300">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"mt-0.5 h-fit shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider ring-1",
|
||||||
|
meta.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{meta.label}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">{change.text}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { CHANGELOG, KIND_META } from "./changelog";
|
||||||
|
import { APP_VERSION } from "./config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Verlauf und die Versionsnummer sind zwei Angaben, die zusammenpassen
|
||||||
|
* muessen. Genau daran ist dieses Projekt schon einmal gescheitert: die
|
||||||
|
* Version stand in der Fusszeile und in der package.json und zeigte
|
||||||
|
* verschiedene Werte.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("Aenderungsverlauf", () => {
|
||||||
|
it("beginnt mit der Fassung, die auch installiert ist", () => {
|
||||||
|
expect(CHANGELOG[0].version).toBe(APP_VERSION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nennt jede Fassung nur einmal", () => {
|
||||||
|
const versionen = CHANGELOG.map((release) => release.version);
|
||||||
|
expect(new Set(versionen).size).toBe(versionen.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zaehlt absteigend", () => {
|
||||||
|
const zahl = (version: string) =>
|
||||||
|
version.split(".").reduce((sum, teil) => sum * 1000 + Number(teil), 0);
|
||||||
|
|
||||||
|
for (let index = 1; index < CHANGELOG.length; index += 1) {
|
||||||
|
expect(
|
||||||
|
zahl(CHANGELOG[index - 1].version),
|
||||||
|
`${CHANGELOG[index - 1].version} muss ueber ${CHANGELOG[index].version} stehen`,
|
||||||
|
).toBeGreaterThan(zahl(CHANGELOG[index].version));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hat zu jeder Fassung ein Datum, eine Zeile und mindestens eine Aenderung", () => {
|
||||||
|
for (const release of CHANGELOG) {
|
||||||
|
expect(release.version, "Version im Format x.y.z").toMatch(/^\d+\.\d+\.\d+$/);
|
||||||
|
expect(Number.isNaN(Date.parse(release.date)), `Datum von ${release.version}`).toBe(false);
|
||||||
|
expect(release.headline.length, `Zeile zu ${release.version}`).toBeGreaterThan(0);
|
||||||
|
expect(release.changes.length, `Eintraege zu ${release.version}`).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kennt zu jeder Art eine Beschriftung", () => {
|
||||||
|
for (const release of CHANGELOG) {
|
||||||
|
for (const change of release.changes) {
|
||||||
|
expect(KIND_META[change.kind], `${change.kind} in v${release.version}`).toBeDefined();
|
||||||
|
expect(change.text.trim().length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* Was sich von Fassung zu Fassung geaendert hat.
|
||||||
|
*
|
||||||
|
* Bewusst hier und nicht als CHANGELOG.md: die Liste 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 stand und verschiedene Werte zeigte.
|
||||||
|
*
|
||||||
|
* Die oberste Fassung ist die aktuelle. Neue Eintraege kommen nach vorne.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ChangeKind = "neu" | "besser" | "behoben";
|
||||||
|
|
||||||
|
export interface ChangeEntry {
|
||||||
|
kind: ChangeKind;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Release {
|
||||||
|
version: string;
|
||||||
|
/** ISO-Datum, ohne Uhrzeit. */
|
||||||
|
date: string;
|
||||||
|
/** Ein Satz darueber, worum es in dieser Fassung ging. */
|
||||||
|
headline: string;
|
||||||
|
changes: ChangeEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KIND_META: Record<ChangeKind, { label: string; className: string }> = {
|
||||||
|
neu: {
|
||||||
|
label: "Neu",
|
||||||
|
className: "bg-violet-500/12 text-violet-200 ring-violet-400/30",
|
||||||
|
},
|
||||||
|
besser: {
|
||||||
|
label: "Besser",
|
||||||
|
className: "bg-cyan-500/12 text-cyan-200 ring-cyan-400/30",
|
||||||
|
},
|
||||||
|
behoben: {
|
||||||
|
label: "Behoben",
|
||||||
|
className: "bg-emerald-500/12 text-emerald-200 ring-emerald-400/30",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CHANGELOG: Release[] = [
|
||||||
|
{
|
||||||
|
version: "1.1.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Das Brett wird zur Arbeitsfläche.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Freie Reihenfolge im Brett: Karten lassen sich innerhalb einer Spalte anordnen, " +
|
||||||
|
"die Position bleibt erhalten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Mehrfachauswahl. Über die Kästchen mehrere Projekte wählen und Status, Tags oder " +
|
||||||
|
"Favoriten in einem Zug ändern — oder alle auf einmal löschen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Als App installierbar: Nebula bringt ein Web-Manifest und eigene Symbole mit und " +
|
||||||
|
"lässt sich auf dem Handy zum Startbildschirm hinzufügen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text: "Dieser Änderungsverlauf, erreichbar über die Versionsnummer in der Fußzeile.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Ansicht, Sortierung und der Archivschalter überdauern das Neuladen. Wer sein " +
|
||||||
|
"Brett von Hand anordnet, will es beim nächsten Aufruf auch sehen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Neu angelegte, importierte und verschobene Projekte bekamen keine Position und " +
|
||||||
|
"stapelten sich im Brett alle auf demselben Platz.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Umsortieren im Brett fasst das Änderungsdatum nicht mehr an. Vorher schob ein " +
|
||||||
|
"Aufräumen sämtliche Projekte in der Sortierung „Zuletzt geändert“ nach oben.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Die Sortierung im Kopf ist im Brett abgeschaltet — dort zählt die Anordnung von Hand. " +
|
||||||
|
"Sie stehen zu lassen, ohne dass sie wirkt, wäre irreführend.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Prisma liest seine Einstellungen aus prisma.config.ts statt aus der package.json. " +
|
||||||
|
"Damit ist die Warnung vor Prisma 7 weg.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "1.0.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Der Funktionsumfang ist beisammen.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Aktivitätsverlauf je Projekt: Statuswechsel, Notizen, Aufgaben und neue Commits " +
|
||||||
|
"in einer Zeitleiste.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Zwei-Faktor-Anmeldung per TOTP, mit QR-Code zum Einrichten und zehn " +
|
||||||
|
"Wiederherstellungscodes für den Fall, dass das Gerät verloren geht.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Mehrere Repositories je Projekt — Frontend und Backend gehören in eine Karte. " +
|
||||||
|
"Jedes mit eigenen Kennzahlen, eigener CI und eigenem Abgleich.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Benachrichtigungen merken sich den gemeldeten Zustand je Repository. Vorher " +
|
||||||
|
"verdeckte ein rotes Frontend die rote Pipeline des Backends.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Stillstand bemisst sich am jüngsten Commit über alle Repositories: an einem " +
|
||||||
|
"Vorhaben wird gearbeitet, sobald an einem seiner Teile gearbeitet wird.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Der Verlauf lieferte ohne ausdrückliche Angabe genau einen Eintrag statt dreißig " +
|
||||||
|
"— Number(null) ist 0, nicht „nicht angegeben“.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Die Issue-Übernahme antwortete mit „nicht gefunden“, obwohl gar kein bestimmtes " +
|
||||||
|
"Repository angefragt war.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.9.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Schneller ans Ziel.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Schnellsuche mit Strg+K: Projekte, Notizen und Bereiche, bedienbar allein mit " +
|
||||||
|
"der Tastatur.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Offene Git-Issues als Aufgaben übernehmen, mit Titel, Labels und Link zurück. " +
|
||||||
|
"Bereits übernommene werden übersprungen.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.8.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Der sanfte Stups.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Stillstehende Projekte melden sich, wenn länger kein Commit kam. Die Schwelle " +
|
||||||
|
"steht im Adminbereich.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Der neue Anlass fehlte im Prüfschema — die Oberfläche bot ihn an, der Server " +
|
||||||
|
"lehnte ihn ab. Ein Test hält beide Listen seitdem zusammen.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.7.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Bescheid bekommen, ohne nachzusehen.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Benachrichtigungen über ntfy, Webhook (HMAC-signiert) und E-Mail — bei " +
|
||||||
|
"fehlgeschlagenem Abgleich, roter CI oder neuer Version.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Gemeldet wird nur bei einer Änderung. Sonst käme bei einem Repo mit abgelaufenem " +
|
||||||
|
"Token mit jedem Durchlauf dieselbe Nachricht.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text: "Im ntfy-Betreff wurden Emojis zu Fragezeichen, weil die Kopfzeile nur Latin-1 kann.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.6.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Prüfen statt behaupten.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text: "Ende-zu-Ende-Tests mit Playwright gegen die gebaute Anwendung und eine eigene Datenbank.",
|
||||||
|
},
|
||||||
|
{ kind: "neu", text: "CI-Pipeline für Gitea Actions." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.5.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Aufgaben je Projekt.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text: "Aufgabenbrett mit sortierbaren Spalten, Fälligkeitsdatum und Labels.",
|
||||||
|
},
|
||||||
|
{ kind: "neu", text: "Fortschritt wahlweise aus dem Verhältnis erledigter Aufgaben." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.4.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Ziehen und ablegen.",
|
||||||
|
changes: [
|
||||||
|
{ kind: "neu", text: "Kanban-Ansicht: Status per Ziehen ändern." },
|
||||||
|
{ kind: "neu", text: "CI-Status je Branch und Commit-Aktivität als Verlaufslinie." },
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Die Verlaufslinie log: bei 100 Commits innerhalb weniger Tage zeigte sie für die " +
|
||||||
|
"übrigen Tage Nullen, obwohl schlicht keine Daten vorlagen.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.3.0",
|
||||||
|
date: "2026-07-26",
|
||||||
|
headline: "Text und Suche.",
|
||||||
|
changes: [
|
||||||
|
{ kind: "neu", text: "Markdown in Beschreibung und Notizen." },
|
||||||
|
{ kind: "neu", text: "Volltextsuche über Notizen, mit hervorgehobenen Fundstellen." },
|
||||||
|
{ kind: "neu", text: "Listen offener Pull Requests und Issues." },
|
||||||
|
{ kind: "behoben", text: "Die Versionsnummer stand an zwei Stellen und lief auseinander." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.2.0",
|
||||||
|
date: "2026-07-25",
|
||||||
|
headline: "Konten und Härtung.",
|
||||||
|
changes: [
|
||||||
|
{ kind: "neu", text: "Benutzerverwaltung mit Passwort und Passkeys, Einzel- und Mehrbenutzerbetrieb." },
|
||||||
|
{ kind: "neu", text: "Export, Import und vollständige Sicherung." },
|
||||||
|
{ kind: "neu", text: "Automatischer Abgleich nach Zeitplan, einstellbar im Adminbereich." },
|
||||||
|
{
|
||||||
|
kind: "besser",
|
||||||
|
text:
|
||||||
|
"Härtung: CSRF-Prüfung, Content-Security-Policy mit Nonce, SSRF-Schutz mit " +
|
||||||
|
"Namensauflösung, Ratenbegrenzung und harte Datentrennung je Konto.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Menüs verschwanden hinter den Karten. Ursache war der Weichzeichner der " +
|
||||||
|
"Glasoptik, der einen eigenen Bezugsrahmen aufspannt; Menüs hängen seitdem am " +
|
||||||
|
"Seitenende statt in der Karte.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "behoben",
|
||||||
|
text:
|
||||||
|
"Eingabefelder überlappten ihre Symbole, weil eigene CSS-Klassen außerhalb der " +
|
||||||
|
"Kaskadenschichten gegen die Hilfsklassen gewannen.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.1.0",
|
||||||
|
date: "2026-07-25",
|
||||||
|
headline: "Der Anfang.",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: "neu",
|
||||||
|
text:
|
||||||
|
"Projekte mit Status, Priorität, Fortschritt, Tags und Notizen — von der reinen " +
|
||||||
|
"Idee bis zum fertigen Repo.",
|
||||||
|
},
|
||||||
|
{ kind: "neu", text: "Git-Anbindung an GitHub, GitLab und Gitea über Access Token." },
|
||||||
|
{ kind: "neu", text: "Betrieb per Docker oder eigenständig, Daten in einer lokalen PostgreSQL." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user