Versionshinweis mit Aenderungsverlauf
Bisher liess sich der laufenden Anwendung nicht ansehen, welche Fassung sie ist. Nach einem Update war damit auch nicht zu erkennen, ob es angekommen ist — die haeufigste Frage im Betrieb. Die Fussleiste nennt jetzt die Version und fuehrt auf /version mit dem vollstaendigen Verlauf. Zusammengetragen ist er aus dem, was es schon gab: dem Abschnitt „Gebaut" der Roadmap und der Git-Historie. Gepflegt wird der Verlauf an genau einer Stelle, src/lib/changelog.ts. Daraus entsteht sowohl die Seite als auch die CHANGELOG.md (npm run changelog); eine Pruefung mit --check wacht darueber, dass beides zusammenpasst. Zwei gepflegte Listen liefen unweigerlich auseinander, und ein Changelog, der nicht stimmt, ist schlimmer als keiner. Die Historie hatte keine Tags. Die Eintraege sind deshalb nach Erscheinungstag zu Versionen zusammengefasst — die Nummern sind nachtraeglich vergeben, die Daten stammen aus den Commits. Zu jeder Version gibt es jetzt einen annotierten Tag im Repo; v1.6.0 folgt beim Merge. Die Roadmap fuehrt den Verlauf nicht mehr doppelt, sondern verweist darauf und behaelt den Blick nach vorn. Refs #9
This commit is contained in:
@@ -6,6 +6,7 @@ import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { actorLabel, getActor, isAuthEnabled } from "@/lib/auth/actor";
|
||||
import { can } from "@/lib/auth/policy";
|
||||
import { getBranding } from "@/lib/branding";
|
||||
import { APP_VERSION } from "@/lib/changelog";
|
||||
|
||||
// Der Kopfbereich hängt von der Anmeldung ab und darf nicht vorgerendert
|
||||
// werden.
|
||||
@@ -128,9 +129,20 @@ export default async function AppLayout({ children }: LayoutProps<"/">) {
|
||||
<main className="flex-1">{children}</main>
|
||||
|
||||
<footer className="border-t border-slate-200 bg-surface">
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 text-sm text-slate-500 sm:px-6">
|
||||
{branding.appName}
|
||||
{branding.footerText ? ` · ${branding.footerText}` : null}
|
||||
<div className="mx-auto flex max-w-6xl flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-4 py-6 text-sm text-slate-500 sm:px-6">
|
||||
<span>
|
||||
{branding.appName}
|
||||
{branding.footerText ? ` · ${branding.footerText}` : null}
|
||||
</span>
|
||||
{/* Welche Fassung hier läuft — nach einem Update die schnellste
|
||||
Antwort auf die Frage, ob das Update auch angekommen ist. */}
|
||||
<Link
|
||||
href="/version"
|
||||
title="Version und Änderungen"
|
||||
className="rounded font-mono text-xs tabular-nums transition-colors hover:text-slate-900"
|
||||
>
|
||||
v{APP_VERSION}
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Icon } from "@/components/icons";
|
||||
import { Badge, Card } from "@/components/ui";
|
||||
import { requirePageActor } from "@/lib/auth/guard";
|
||||
import {
|
||||
APP_VERSION,
|
||||
CHANGELOG,
|
||||
formatChangelogDate,
|
||||
} from "@/lib/changelog";
|
||||
|
||||
export const metadata: Metadata = { title: "Version und Änderungen" };
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function VersionPage() {
|
||||
await requirePageActor();
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-10">
|
||||
<Link
|
||||
href="/"
|
||||
className="mb-5 inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-slate-600 transition-colors hover:text-slate-900"
|
||||
>
|
||||
<Icon name="arrowLeft" className="size-4" />
|
||||
Zur Typ-Auswahl
|
||||
</Link>
|
||||
|
||||
<div className="mb-7">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
|
||||
Version und Änderungen
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Hier läuft Version{" "}
|
||||
<span className="font-semibold text-slate-900">{APP_VERSION}</span>.
|
||||
Darunter steht, was in welcher Fassung dazugekommen ist.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Nach einem Update lohnt ein Blick hierher: Steht oben noch die alte
|
||||
Nummer, läuft der Container noch mit dem alten Abbild.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ol className="space-y-4">
|
||||
{CHANGELOG.map((eintrag) => {
|
||||
const laeuft = eintrag.version === APP_VERSION;
|
||||
return (
|
||||
<li key={eintrag.version}>
|
||||
<Card>
|
||||
<div className="mb-3 flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h2 className="text-lg font-semibold text-slate-900">
|
||||
{eintrag.version}
|
||||
</h2>
|
||||
<span className="text-sm text-slate-500 tabular-nums">
|
||||
{formatChangelogDate(eintrag.date)}
|
||||
</span>
|
||||
{laeuft ? (
|
||||
<Badge tone="accent">läuft hier</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="mb-3 text-slate-600">{eintrag.summary}</p>
|
||||
|
||||
<ul className="space-y-1.5 text-sm text-slate-700">
|
||||
{eintrag.changes.map((zeile) => (
|
||||
<li key={zeile} className="flex gap-2.5">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mt-2 size-1.5 shrink-0 rounded-full bg-slate-300"
|
||||
/>
|
||||
<span>{zeile}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import pkg from "../../package.json";
|
||||
|
||||
/**
|
||||
* Änderungsverlauf der Anwendung — die einzige Quelle dafür.
|
||||
*
|
||||
* Hieraus entsteht beides: die Seite unter `/version` und die `CHANGELOG.md`
|
||||
* im Wurzelverzeichnis (`npm run changelog`). Zwei gepflegte Listen liefen
|
||||
* unweigerlich auseinander; eine Prüfung wacht darüber, dass die erzeugte
|
||||
* Datei zu dieser Quelle passt.
|
||||
*
|
||||
* Die Einträge sind aus dem Abschnitt „Gebaut“ der Roadmap und der
|
||||
* Git-Historie zusammengetragen. Versionsnummern gab es dabei nie — es
|
||||
* existieren keine Tags —, deshalb ist je **Erscheinungstag** eine Version
|
||||
* gebildet. Das ist nachträglich vergeben, aber die Daten selbst stimmen.
|
||||
*/
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
/** ISO-Datum, an dem der Stand entstanden ist. */
|
||||
date: string;
|
||||
/** Eine Zeile zur Einordnung — was diese Version ausmacht. */
|
||||
summary: string;
|
||||
changes: string[];
|
||||
};
|
||||
|
||||
export const CHANGELOG: readonly ChangelogEntry[] = [
|
||||
{
|
||||
version: "1.6.0",
|
||||
date: "2026-08-21",
|
||||
summary: "Sichtbare Version mit Änderungsverlauf.",
|
||||
changes: [
|
||||
"Die laufende Version steht in der Fußzeile und führt auf eine Seite mit dem vollständigen Änderungsverlauf.",
|
||||
"Der Verlauf wird an einer Stelle gepflegt; die CHANGELOG.md im Projekt entsteht daraus.",
|
||||
"Die Roadmap verweist für Vergangenes auf den Changelog und behält den Blick nach vorn.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.5.0",
|
||||
date: "2026-08-20",
|
||||
summary: "Statische Codes lassen sich benennen und ablegen.",
|
||||
changes: [
|
||||
"Das Feld „Name“ gilt in beiden Betriebsarten, nicht mehr nur bei dynamischen Codes.",
|
||||
"Der Name bestimmt den Dateinamen beim Herunterladen.",
|
||||
"Ein statischer Code lässt sich in „Meine Codes“ ablegen — als Schnappschuss ohne Kurz-Link und ohne Scan-Zähler.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.4.0",
|
||||
date: "2026-08-17",
|
||||
summary: "Sprechende Kurz-Links und eine früh erkannte Fehlkonfiguration.",
|
||||
changes: [
|
||||
"Kurz-Links selbst vergeben und nachträglich ändern, mit „/“ als Trenner.",
|
||||
"Die vorherige Adresse bleibt gültig und lässt sich einzeln freigeben — gedruckte Codes bleiben gültig.",
|
||||
"Vergeben und Ändern ist Administratoren vorbehalten und in der Verwaltung für Bearbeiter freigebbar.",
|
||||
"Weicht BASE_URL von der aufgerufenen Adresse ab, meldet die Anmeldung das sofort statt erst am Ende.",
|
||||
"Auf der Anmeldeseite steht nicht mehr, wie die Anmeldung konfiguriert ist.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.3.0",
|
||||
date: "2026-08-16",
|
||||
summary: "Eigenes Erscheinungsbild und ein Dark Mode.",
|
||||
changes: [
|
||||
"Name, Logo, Favicon und Akzentfarbe sind im Admin-Bereich einstellbar.",
|
||||
"Hell und dunkel folgen dem Betriebssystem, mit Umschalter für eine eigene Wahl.",
|
||||
"Das Update-Skript erklärt, warum ein Vorspulen scheitert, statt nur abzubrechen.",
|
||||
"Die Authentik-Anleitung sagt deutlicher, wie die Redirect URI zu bilden ist.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.2.0",
|
||||
date: "2026-08-15",
|
||||
summary: "Single Sign-on, Dokumentation und eine Sicherheitsdurchsicht.",
|
||||
changes: [
|
||||
"Anmeldung über Authentik und andere OIDC-Anbieter, mit PKCE und geprüftem ID-Token.",
|
||||
"Bedienungsanleitung mit Bildern, Funktionsübersicht und Roadmap.",
|
||||
"Anmeldung und Sitzungen nach OWASP durchgesehen und gehärtet: stärkere Passwort-Parameter, Längengrenze, Herkunftsprüfung, Schutzheader, absolute Sitzungsdauer.",
|
||||
"Auf der Startseite steht „nur dynamisch“ statt eines nichtssagenden Abzeichens an jeder Kachel.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.1.0",
|
||||
date: "2026-08-14",
|
||||
summary: "Alle fünfzehn QR-Typen, Anmeldung, Gruppen und mehrere Domains.",
|
||||
changes: [
|
||||
"Vierzehn weitere QR-Typen — von vCard und WLAN bis PDF, Bild und Feedback.",
|
||||
"Landingpages als eigene Gattung für Typen ohne fremdes Ziel.",
|
||||
"Datei-Upload mit Inhaltsprüfung, Größengrenze und Aufräumen.",
|
||||
"Anmeldung, Benutzer, Gruppen und die Rollen Administrator, Bearbeiter und Betrachter.",
|
||||
"Mehrere Domains für Kurz-URLs, den Gruppen zugeteilt.",
|
||||
"„Meine Codes“ als Liste mit Suche, Sortierung und Gruppenfilter.",
|
||||
"Einzelne QR-Typen lassen sich in der Verwaltung ausblenden.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0",
|
||||
date: "2026-08-13",
|
||||
summary: "Erste lauffähige Fassung.",
|
||||
changes: [
|
||||
"Next.js-Anwendung und PostgreSQL als Docker-Compose-Stack, Migrationen beim Start.",
|
||||
"QR-Typ Website, statisch und dynamisch, mit Kurz-URL, Weiterleitung und Scan-Zähler.",
|
||||
"Gestaltung mit Farben, Kontrastwarnung, Größe, Fehlerkorrektur und Rand.",
|
||||
"Download als PNG und SVG, serverseitig gerendert.",
|
||||
"Update-Skript für Linux, macOS und Windows.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Die laufende Version.
|
||||
*
|
||||
* Aus der `package.json`, damit es genau eine Stelle gibt, an der sie steht.
|
||||
* Der Wert wird beim Bauen eingesetzt — im Container liegt kein Git, aus dem
|
||||
* sich sonst etwas ableiten ließe.
|
||||
*/
|
||||
export const APP_VERSION: string = pkg.version;
|
||||
|
||||
/** Der Eintrag zur laufenden Version, falls er im Verlauf steht. */
|
||||
export function currentEntry(): ChangelogEntry | undefined {
|
||||
return CHANGELOG.find((eintrag) => eintrag.version === APP_VERSION);
|
||||
}
|
||||
|
||||
/** Datum als „21.08.2026“ — ohne Abhängigkeit von der Zeitzone des Servers. */
|
||||
export function formatChangelogDate(iso: string): string {
|
||||
const [jahr, monat, tag] = iso.split("-");
|
||||
return `${tag}.${monat}.${jahr}`;
|
||||
}
|
||||
Reference in New Issue
Block a user