feat: Versionierung mit Changelog, Version 1.0.0

Bis jetzt stand nirgends, welcher Stand laeuft. Beim Aktualisieren auf
einem Server ist das die erste Frage.

CHANGELOG.md nach Keep a Changelog, Versionsnummern nach SemVer. Der
Changelog sagt, was sich beim Arbeiten aendert - nicht, welche Datei
angefasst wurde. Dazu eine Tabelle, was Major, Minor und Patch fuer den
Betreiber praktisch bedeuten, und der Hinweis, dass sich eine
Datenbank-Migration nicht zurueckrollen laesst.

Die Version steht bewusst an zwei Stellen: package.json und
src/lib/version.ts. Ein Import der package.json waere im
Standalone-Build eine andere Datei als die des Projekts - die Zahl haenge
dann an der Laufzeit. Ein Test haelt beide zusammen, und
"npm run version" setzt sie gemeinsam samt Changelog-Abschnitt.

Committet und getaggt wird nicht automatisch. Ein Changelog-Eintrag, den
niemand gegengelesen hat, ist keinen Deut besser als keiner.

Sichtbar wird die Version unten in der Seitenleiste und unter
/api/gesundheit - dort mit Git-Stand, den der Docker-Build hineinreicht.
Auf der Anmeldeseite steht sie nicht: wer nicht angemeldet ist, muss
nicht wissen, welche Fassung hier laeuft.

Etappe: 8
Phase: Haertung
This commit is contained in:
Florian Wilde
2026-08-22 10:41:33 +02:00
parent 4b953e7887
commit 60255e7319
10 changed files with 442 additions and 8 deletions
+192
View File
@@ -0,0 +1,192 @@
# Änderungen
Alle nennenswerten Änderungen an dieser Anwendung, in umgekehrter
Reihenfolge. Aufbau nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
**Was hier steht:** was sich für dich beim Arbeiten ändert — nicht, welche
Datei angefasst wurde. Wer den technischen Verlauf braucht, findet ihn im
Git-Log; jeder Commit trägt seine Etappe.
**Was die Zahlen bedeuten:**
| Stelle | Wann sie steigt | Was du tun musst |
|---|---|---|
| **Major** (1.x.x → 2.0.0) | Etwas verhält sich anders als vorher, oder eine Migration lässt sich nicht zurücknehmen | Changelog lesen, **vorher sichern**, Zeit einplanen |
| **Minor** (1.0.x → 1.1.0) | Neue Funktionen, alles Bisherige bleibt wie es war | Changelog überfliegen, normal aktualisieren |
| **Patch** (1.0.0 → 1.0.1) | Fehler behoben, sonst nichts | Einfach aktualisieren |
Eine Datenbank-Migration lässt sich **nicht** zurückrollen. Vor jedem
Update ein Backup anstoßen — siehe README, Abschnitt *Sicherung und
Wiederherstellung*.
---
## [Unveröffentlicht]
_Noch nichts._
## [1.0.0] – 2026-08-22
Erste vollständige Fassung. Alle acht Etappen umgesetzt, alle
Abnahmekriterien als Test hinterlegt.
### Hinzugefügt
**Der Alltag**
- **Dashboard „Heute"** — beantwortet morgens, was ansteht: Fristen-Ampel
(überfällig / diese Woche / diesen Monat), wer gerade am Zug ist,
offene Nachforderungen des Bezirks, ungeprüfter Posteingang,
unverbuchte Belege, Kennzahlen zu Vermögen und Heimkosten.
- **Fristen** — die gesetzlichen Pflichtfristen entstehen von selbst aus
dem Bestellungsdatum: Vermögensverzeichnis und Anfangsbericht nach drei
Monaten, Jahresbericht und Vermögensübersicht zum Ende jedes
Betreuungsjahres, Aufwandspauschale, Landespflegegeld,
Zuzahlungsbefreiung, Rückkaufswerte. Liste, Monatskalender und ein
ICS-Feed zum Abonnieren im eigenen Kalender.
- **Vorgänge** als Fallakte mit Zeitstrahl über Dokumente, Protokolle,
Fristen und Buchungen. Neun Vorlagen mit vorbefüllter Aufgabenliste,
darunter Heimeinzug, Antrag Hilfe zur Pflege, Wohnungsauflösung,
Hausverkauf, Darlehen nach § 91 SGB XII und Widerspruch gegen einen
Bescheid.
- **Kontakte und Kontaktprotokoll** — Verzeichnis aller Stellen mit
Aktenzeichen, dazu eine Schnellerfassung, die am Handy in unter zwanzig
Sekunden zu bedienen ist.
**Papier**
- **Dokumentenablage** mit Drag & Drop, Sammel-Upload und Handy-Foto.
Texterkennung auf Deutsch, Volltextsuche über alle Schreiben,
Posteingangsbuch mit den Stufen neu → gesichtet → zugeordnet →
beantwortet, Feld für den Ablageort des Papieroriginals.
- Beim Import werden **Absender, Datum, Aktenzeichen, Dokumentart und
eine im Text genannte Frist vorgeschlagen** — mit der Textstelle
daneben, damit man prüfen kann. Übernommen wird nur auf Klick.
- **Belegmappe**: alle Dokumente eines Vorgangs oder Zeitraums als eine
chronologisch sortierte, paginierte PDF mit Deckblatt und
Inhaltsverzeichnis.
**Geld**
- **Kassenbuch** mit Belegzwang, Kategorien, CSV- und CAMT.053-Import.
Harte Trennung zwischen dem Vermögen der betreuten Person und eigenen
Auslagen, mit Warnung bei Buchungen auf ein als privat markiertes Konto.
- **Konten** mit verschlüsselter IBAN und Saldoständen je Stichtag,
**Vermögensposten** mit vollständiger Wert-Historie und Pflichtangabe
zur Wertquelle. Werte werden nie überschrieben.
- **Vermögensverzeichnis (§ 1835 BGB)** und **Vermögensübersicht bzw.
Rechnungslegung (§§ 1859, 1865 BGB)** als druckfertige PDF, in zwei
Ausgabevarianten für befreite und nicht befreite Betreuer.
**Behörde**
- **Sozialhilfe-Modul** für das Verfahren beim Bezirk: Antragsakte,
Nachforderungs-Tracker mit Fortschrittsbalken, vorbefüllte Checkliste
der Standardunterlagen mit Begründung, warum der Träger sie will.
- **Bescheid-Prüfung** mit Berechnung der Rechtsbehelfsfrist ab dem
Zugangsdatum, einschließlich Wochenende, bayerischer Feiertage und dem
Sonderfall fehlender Rechtsbehelfsbelehrung. Der Rechenweg wird
ausgegeben.
- **Heimkosten-Gegenüberstellung** Monat für Monat: Kosten gegen
Pflegekasse, Leistungszuschlag nach § 43c SGB XI, Rente und
Sozialhilfeanteil — die Deckungslücke wird sichtbar.
- **Schonvermögens-Bewertung** je Posten (verwertbar / geschützt /
strittig) mit Begründungsfeld und Textbausteinen zu § 90 SGB XII.
- **Katalog weiterer Leistungen**: Landespflegegeld, Zuzahlungsbefreiung,
Rundfunkbeitragsbefreiung, Schwerbehindertenausweis, Grundsicherung.
**Substanz**
- **Immobilie** mit Grundbuchdaten, Belastungen nach Abteilung II und
III, Wert-Historie, Status im Sozialhilfeverfahren und einer
Pflichten-Checkliste bei Leerstand — zehn Punkte, jeder mit Begründung.
- **Versicherungen und Verträge** mit Kündigungsfristen-Rechner, der den
spätesten Absendetag nennt, Anschreiben zur Rückkaufswert-Anfrage mit
automatischer Wiedervorlage und einem Kündigungs-Assistenten.
- **Heim und Pflege** mit Heimvertrag, Leistungszuschlag-Staffel,
Pflegegrad-Historie, Abwesenheitstagen und einem Gesundheitsverlauf mit
COPD-Bezug: Exazerbationen, Sauerstoff, Stürze, Krankenhaus,
Hilfsmittel, Impfungen nach STIKO-Plan.
**Ausgabe**
- **Jahresbericht (§ 1863 BGB)** als Entwurf zum Überarbeiten, gespeist
aus Kontaktprotokollen, Gesundheitsereignissen und Vorgangsstatus.
Ausgabe als PDF und als DOCX.
- **Unterlagenpaket für den Bezirk**: die im Tracker markierten
Dokumente als eine PDF mit Anschreiben und Inhaltsverzeichnis.
- **Vollexport** als ZIP mit allen Originaldateien, allen Daten als
entschlüsseltes JSON, den erzeugten Berichten, einer LIESMICH und
SHA-256-Prüfsummen.
- **Wissensbasis** mit 37 Hinweiskarten, pflegbar als JSON-Datei ohne
Neustart. Jede mit Rechtsgrundlage, Link und dem stehenden Vermerk,
dass sie keine Rechtsberatung ersetzt.
**Verwaltung**
- **Mitleser-Rolle** mit modulweiser Freigabe und ausdrücklicher Warnung
bei Gesundheit, Finanzen und Sozialhilfe.
- **Papierkorb** mit 30 Tagen Aufbewahrung.
- **Wartungslauf** über `/api/wartung`: Fristerinnerungen per E-Mail,
Papierkorb leeren, alte Anmeldeversuche entfernen.
### Sicherheit
- **Pflicht-Zweitfaktor** (TOTP nach RFC 6238). Lässt sich nicht
abschalten. Acht Wiederherstellungscodes, einzeln Argon2id-gehasht.
- **Anwendungsseitige Feldverschlüsselung** mit AES-256-GCM für
Diagnosen, den ermittelten Willen, IBANs, Versicherungs-, Renten- und
Steuernummern sowie alle Freitext-Notizen. Der Feldname wird
mitauthentifiziert — ein Geheimtext lässt sich nicht von einem Feld in
ein anderes umhängen. Schlüsselrotation ohne Datenverlust vorgesehen.
- **Argon2id** nach OWASP-Empfehlung, mit automatischem Nachziehen bei
späterer Parametererhöhung.
- **Rate-Limiting** am Login nach E-Mail und Anschluss. Die Sperre wird
geprüft, bevor überhaupt gerechnet wird, und die Antwortzeit ist
gestreckt — sie verrät nicht, welche Adressen existieren.
- **Unveränderliches Audit-Log**: ein Datenbank-Trigger weist UPDATE und
DELETE ab, auch bei direktem Zugriff mit `psql`.
- **Strenge Content-Security-Policy** mit Nonce pro Anfrage. Keine
externen Schriften, kein CDN, keine Tracker — nachgewiesen durch einen
Test, der jede Anfrage an einen fremden Host als Fehler wertet.
- **Uploads** werden an den Magic Bytes geprüft, nicht an Endung oder
gemeldetem MIME-Typ. Ausgeliefert wird nur über kurzlebige signierte
URLs, immer als Anhang mit `nosniff`.
- **Erneute Anmeldung** vor Export, endgültigem Löschen und
Rollenänderungen. Ein Passwortwechsel beendet alle offenen Sitzungen.
### Betrieb
- Docker Compose mit Anwendung, PostgreSQL, MinIO und Sicherungsdienst.
**Ohne Reverse Proxy** — der ist auf dem Zielserver vorhanden. Die
Datenbank hat keine Portfreigabe.
- **Tägliche verschlüsselte Sicherung** um 03:15 Uhr mit Selbstprüfung:
lässt sich die Datei nicht wieder entschlüsseln, endet der Lauf mit
Fehler statt mit einem stillen Scheinerfolg.
- Verarbeitungsverzeichnis und Datenschutzhinweise als ausfüllbare
Textbausteine.
- Bauprotokoll aus den Commit-Trailern.
### Bekannte Einschränkungen
- Die Texterkennung ist gegen ein echtes Tesseract **nicht** end-to-end
geprüft. Der Weg „nicht verfügbar, Upload funktioniert trotzdem" ist
getestet.
- Der CAMT.053-Import ist nur gegen selbst gebaute Beispiele geprüft, nicht
gegen einen echten Bankauszug.
- Das Kostenkonto der Immobilie wertet über Buchungskategorien aus und
wäre bei einem **zweiten** Grundstück falsch. Die Oberfläche sagt das
an der Auswertung dazu.
- Eine Aufgabe aus einer Vorgangs-Vorlage lässt sich nicht entfernen —
passt sie im Einzelfall nicht, bleibt sie offen stehen.
- `@react-pdf/renderer` braucht in diesem Abhängigkeitsbaum eine Krücke
(`src/lib/pdf/standardschrift.ts`), weil seine Unterabhängigkeiten für
Standardschriften kein Schriftobjekt mehr liefern.
Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md).
---
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.0...HEAD
[1.0.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/releases/tag/v1.0.0
+10
View File
@@ -43,6 +43,16 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV SKIP_ENV_VALIDATION=1
# Git-Stand und Bauzeitpunkt landen in der Oberflaeche und im
# Gesundheitsendpunkt. Beim Aktualisieren sieht man damit auf einen Blick,
# ob wirklich der neue Stand laeuft:
#
# docker build --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) .
ARG GIT_COMMIT=""
ARG BUILD_ZEIT=""
ENV NEXT_PUBLIC_GIT_COMMIT=$GIT_COMMIT
ENV NEXT_PUBLIC_BUILD_ZEIT=$BUILD_ZEIT
RUN npm run build
# --- Stufe 3: Laufzeit -------------------------------------------------------
+5
View File
@@ -70,6 +70,11 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
# In der Oberflaeche und unter /api/gesundheit sichtbar. Setzen mit:
# GIT_COMMIT=$(git rev-parse --short HEAD) docker compose up -d --build
GIT_COMMIT: ${GIT_COMMIT:-}
BUILD_ZEIT: ${BUILD_ZEIT:-}
restart: unless-stopped
depends_on:
db:
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "betreuungs-cockpit",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
"engines": {
@@ -28,7 +28,8 @@
"protokoll": "node scripts/bauprotokoll.mjs",
"test:e2e": "playwright test",
"protokoll:md": "node scripts/bauprotokoll.mjs --md",
"wartungsschluessel": "node --env-file=.env scripts/wartungsschluessel.mjs"
"wartungsschluessel": "node --env-file=.env scripts/wartungsschluessel.mjs",
"version": "node scripts/version.mjs"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env node
/**
* Version anheben.
*
* npm run version -- patch 1.0.0 -> 1.0.1
* npm run version -- minor 1.0.0 -> 1.1.0
* npm run version -- major 1.0.0 -> 2.0.0
* npm run version -- 1.2.3 genau diese Version
*
* Was passiert:
* 1. package.json und src/lib/version.ts werden gesetzt - beide, damit
* sie nicht auseinanderlaufen.
* 2. Im CHANGELOG wird aus "## [Unveroeffentlicht]" ein Abschnitt mit
* Versionsnummer und heutigem Datum, darueber ein neuer leerer
* Abschnitt.
* 3. Der Vergleichslink am Dateiende wird nachgezogen.
*
* Was NICHT passiert: committen und taggen. Das machst du selbst, nachdem
* du den Changelog-Eintrag gegengelesen hast. Ein automatisch erzeugter
* Changelog, den niemand liest, ist keinen Deut besser als keiner.
*
* Danach:
* git add -A && git commit -m "chore: Version 1.0.1"
* git tag -a v1.0.1 -m "..." && git push --follow-tags
*/
import { readFileSync, writeFileSync } from 'node:fs'
const WURZEL = new URL('..', import.meta.url)
const pfad = (name) => new URL(name, WURZEL)
function lesen(name) {
return readFileSync(pfad(name), 'utf8')
}
function schreiben(name, inhalt) {
writeFileSync(pfad(name), inhalt, 'utf8')
console.log(` ${name}`)
}
// --- Ziel bestimmen ---------------------------------------------------------
const argument = process.argv[2]
if (!argument) {
console.error('Aufruf: npm run version -- <patch|minor|major|x.y.z>')
process.exit(1)
}
const paket = JSON.parse(lesen('package.json'))
const alt = paket.version
function naechste(aktuell, art) {
const treffer = aktuell.match(/^(\d+)\.(\d+)\.(\d+)$/)
if (!treffer) throw new Error(`Version "${aktuell}" ist kein x.y.z`)
const [haupt, neben, fehler] = treffer.slice(1).map(Number)
switch (art) {
case 'major':
return `${haupt + 1}.0.0`
case 'minor':
return `${haupt}.${neben + 1}.0`
case 'patch':
return `${haupt}.${neben}.${fehler + 1}`
default:
if (!/^\d+\.\d+\.\d+$/.test(art)) {
throw new Error(`"${art}" ist weder patch/minor/major noch x.y.z`)
}
return art
}
}
const neu = naechste(alt, argument)
if (neu === alt) {
console.error(`Version ist bereits ${alt}.`)
process.exit(1)
}
console.log(`\n${alt} -> ${neu}\n`)
console.log('Geaendert:')
// --- 1. package.json --------------------------------------------------------
paket.version = neu
schreiben('package.json', `${JSON.stringify(paket, null, 2)}\n`)
// --- 2. src/lib/version.ts --------------------------------------------------
const versionsdatei = lesen('src/lib/version.ts')
const ersetzt = versionsdatei.replace(
/export const VERSION = '[^']+'/,
`export const VERSION = '${neu}'`,
)
if (ersetzt === versionsdatei) {
console.error('FEHLER: VERSION in src/lib/version.ts nicht gefunden.')
process.exit(1)
}
schreiben('src/lib/version.ts', ersetzt)
// --- 3. CHANGELOG.md --------------------------------------------------------
const heute = new Date().toISOString().slice(0, 10)
let changelog = lesen('CHANGELOG.md')
const UNVEROEFFENTLICHT = '## [Unveröffentlicht]'
if (!changelog.includes(UNVEROEFFENTLICHT)) {
console.error(`FEHLER: "${UNVEROEFFENTLICHT}" steht nicht im CHANGELOG.`)
process.exit(1)
}
const leererAbschnitt = [
UNVEROEFFENTLICHT,
'',
'_Noch nichts._',
'',
`## [${neu}] – ${heute}`,
].join('\n')
changelog = changelog.replace(UNVEROEFFENTLICHT, leererAbschnitt)
// Vergleichslinks am Dateiende nachziehen.
const basis = 'https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare'
changelog = changelog.replace(
/^\[Unveröffentlicht\]:.*$/m,
`[Unveröffentlicht]: ${basis}/v${neu}...HEAD\n[${neu}]: ${basis}/v${alt}...v${neu}`,
)
schreiben('CHANGELOG.md', changelog)
// --- Hinweis ----------------------------------------------------------------
console.log(`
Jetzt:
1. CHANGELOG.md gegenlesen - steht dort, was sich fuer den Nutzer
tatsaechlich aendert? Nicht, welche Datei angefasst wurde.
2. git add -A && git commit -m "chore: Version ${neu}"
3. git tag -a v${neu} -m "Version ${neu}"
4. git push --follow-tags
`)
+2
View File
@@ -7,6 +7,7 @@ import { aktiveBetreute, erfordereBenutzer, erfordereZweitenFaktor, meineBetreut
import { darf } from '@/lib/auth/rollen'
import { ROLLEN_BESCHRIFTUNG } from '@/lib/auth/rollen'
import { db, nichtGeloescht } from '@/lib/db'
import { versionstext } from '@/lib/version'
/**
* Rahmen aller Fachseiten.
@@ -64,6 +65,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
return (
<AppHuelle
navigation={navigation}
version={versionstext()}
kopfzeile={
<BetreuteAuswahl
aktiveId={aktive.id}
+13 -5
View File
@@ -1,12 +1,17 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { COMMIT, VERSION } from '@/lib/version'
/**
* Betriebsbereitschaft fuer den Healthcheck in docker-compose.
*
* Bewusst ohne Anmeldung, aber auch ohne jede Auskunft ueber Inhalte:
* geprueft wird nur, ob die Datenbank antwortet. Version und Datenbestand
* gehen niemanden etwas an, der die Adresse errät.
* Bewusst ohne Anmeldung, aber ohne jede Auskunft ueber Inhalte: geprueft
* wird nur, ob die Datenbank antwortet.
*
* Die Version steht dabei: sie wird beim Aktualisieren gebraucht, um zu
* sehen, ob der neue Stand tatsaechlich laeuft. Das ist keine Auskunft
* ueber die betreuten Daten, und die Adresse ist ohnehin nur ueber den
* eigenen Reverse Proxy erreichbar.
*/
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
@@ -14,10 +19,13 @@ export const dynamic = 'force-dynamic'
export async function GET() {
try {
await db.$queryRaw`SELECT 1`
return NextResponse.json({ status: 'bereit' }, { headers: { 'Cache-Control': 'no-store' } })
return NextResponse.json(
{ status: 'bereit', version: VERSION, commit: COMMIT || undefined },
{ headers: { 'Cache-Control': 'no-store' } },
)
} catch {
return NextResponse.json(
{ status: 'datenbank nicht erreichbar' },
{ status: 'datenbank nicht erreichbar', version: VERSION },
{ status: 503, headers: { 'Cache-Control': 'no-store' } },
)
}
+11 -1
View File
@@ -21,12 +21,15 @@ export function AppHuelle({
kopfzeile,
schnellerfassung,
benutzerbereich,
version,
children,
}: {
navigation: NavEintrag[]
kopfzeile: React.ReactNode
schnellerfassung?: React.ReactNode
benutzerbereich: React.ReactNode
/** Wird unten in der Seitenleiste angezeigt - hilft beim Aktualisieren. */
version?: string
children: React.ReactNode
}) {
const [menueOffen, setMenueOffen] = React.useState(false)
@@ -73,7 +76,14 @@ export function AppHuelle({
<Navigation eintraege={navigation} />
</div>
<div className="border-t p-3">{benutzerbereich}</div>
<div className="border-t p-3">
{benutzerbereich}
{version && (
<p className="px-2 pt-2 text-xs text-muted-foreground">
Version {version}
</p>
)}
</div>
</aside>
{/* --- Kopfzeile am Handy ------------------------------------------- */}
+38
View File
@@ -0,0 +1,38 @@
/**
* Versionsangaben der Anwendung.
*
* VERSION wird vom Skript `npm run version` zusammen mit der package.json
* gesetzt - es gibt also genau eine Quelle, aus der beide gespeist werden.
* Ein Test (tests/version.test.ts) prueft, dass sie nicht auseinanderlaufen.
*
* Warum nicht einfach die package.json importieren? Im Standalone-Build
* liegt eine andere package.json neben dem Server als die des Projekts.
* Die Zahl waere dann je nach Laufzeit eine andere - genau das, was man
* bei einer Versionsangabe nicht gebrauchen kann.
*
* Die Angabe ist kein Geheimnis, aber auch keine oeffentliche Auskunft:
* sie steht in den Einstellungen und im Gesundheitsendpunkt, nicht auf
* der Anmeldeseite. Wer nicht angemeldet ist, muss nicht wissen, welche
* Fassung hier laeuft.
*/
/** Muss zur "version" in der package.json passen. */
export const VERSION = '1.0.0'
/**
* Kurzer Git-Stand, beim Bauen hineingereicht:
*
* docker build --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) .
*
* Fehlt die Angabe (etwa im Entwicklungsmodus), bleibt sie leer. Dann
* zeigt die Oberflaeche nur die Version.
*/
export const COMMIT = process.env.NEXT_PUBLIC_GIT_COMMIT ?? ''
/** Zeitpunkt des Bauens, ebenfalls beim Bauen hineingereicht. */
export const GEBAUT_AM = process.env.NEXT_PUBLIC_BUILD_ZEIT ?? ''
/** "1.0.0" oder "1.0.0 · 4b953e7" - fuer die Anzeige. */
export function versionstext(): string {
return COMMIT ? `${VERSION} · ${COMMIT}` : VERSION
}
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { VERSION, versionstext } from '@/lib/version'
/**
* Die Version steht an zwei Stellen: in der package.json und in
* src/lib/version.ts. Das ist Absicht - im Standalone-Build liegt eine
* andere package.json neben dem Server als die des Projekts, ein Import
* waere also je nach Laufzeit eine andere Zahl.
*
* Damit die beiden nicht auseinanderlaufen, gibt es dieses Netz. Wer die
* Version von Hand aendert statt mit "npm run version", merkt es hier.
*/
describe('Version', () => {
const paket = JSON.parse(readFileSync('package.json', 'utf8')) as { version: string }
it('stimmt mit der package.json ueberein', () => {
expect(
VERSION,
'src/lib/version.ts und package.json weichen ab - benutze "npm run version"',
).toBe(paket.version)
})
it('ist eine gueltige Versionsnummer nach SemVer', () => {
expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/)
})
it('haengt den Git-Stand an, wenn er bekannt ist', () => {
expect(versionstext().startsWith(VERSION)).toBe(true)
})
})