Compare commits

...
6 Commits
Author SHA1 Message Date
florian.wilde f38ae4c60a Merge branch 'phase/048-releases'
Prüfung / pruefung (push) Waiting to run
2026-10-25 13:29:36 +01:00
florian.wildeandClaude Opus 5 4f6a995a25 feat(release): Releases in Gitea aus dem CHANGELOG erzeugen (#046)
Bisher gab es nur Tags - eine Markierung im Verlauf. Eine Release ist ein
Eintrag in Gitea mit Titel und Beschreibung, sichtbar, abonnierbar,
verlinkbar.

npm run releases:sync liest das CHANGELOG, nimmt jeden Abschnitt, zu dem es
ein Tag gibt, und legt daraus eine Release an; ein zweiter Lauf aktualisiert
nur Geaendertes. "Unreleased" bleibt aussen vor, weil es dazu kein Tag gibt.
Fuenf Tests decken die Zerlegung ab.

Der Lauf gegen Gitea scheitert derzeit an den Zugangsdaten: Hinterlegt ist
ein elfstelliges Passwort, kein API-Token. Fuer Releases braucht es
write:repository, fuer den Issue-Abgleich issue:write.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-25 13:29:36 +01:00
florian.wilde 9a4026fb44 Merge branch 'phase/047-eins-null'
Prüfung / pruefung (push) Canceled after 0s
2026-10-23 14:00:58 +02:00
florian.wildeandClaude Opus 5 9f75b9b015 release: ProxLedger 1.0.0
Kein neuer Funktionsumfang - was fehlte, war Erprobung, und die ist erbracht:

- Ein Lizenzjahr vollstaendig durchlaufen: hv-pve-01 und hv-pve-02 der Robert
  Hofmann GmbH, Warnung vor dem Ablauf am 17.10.2026, Verlaengerung,
  Proxmox-Rechnung 291017 mit Rabatt je Position, Kundenrechnung, beide
  bezahlt. Die Folgelaufzeiten stehen auf 17.10.2027.
- Proxmox Backup Server mit aktiver Lizenz bestaetigt: drei PBS-Hosts melden
  Key und Ablaufdatum.
- Wiederherstellung geprobt (#045).

PMG wird zurueckgestellt - dort wird derzeit kein Host betrieben. Der Zweig
bleibt im Handbuch als ungeprueft gekennzeichnet, statt die Luecke
wegzudefinieren (#023).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-23 14:00:58 +02:00
florian.wilde 0e48f1e3fa Merge branch 'phase/046-next-sicherheit'
Prüfung / pruefung (push) Canceled after 0s
2026-10-23 13:56:43 +02:00
florian.wildeandClaude Opus 5 476ecb3be1 fix(deps): Next.js 16.4.2 wegen zweier kritischer Luecken
Fuer Next.js 16.0.0 bis 16.4.0 wurden drei Luecken gemeldet:

- KRITISCH: Remote Code Execution ueber die Bildoptimierung mit SVG-Dateien
  (GHSA-2464-fhr4-w9xw)
- KRITISCH: Remote Code Execution in `use cache` (GHSA-673q-mjm6-r5xr)
- HOCH: Blockade des Dienstes ueber Server Actions (GHSA-p3m5-59mp-jv5r)

Geprueft, was ProxLedger davon trifft: next/image und `use cache` werden
nicht genutzt, Server Actions dagegen ueberall - die Blockade war also
unmittelbar wirksam. Wer oeffentlich erreichbar ist, sollte sofort
aktualisieren.

Tests, Typen, Lint und Build gruen, npm audit ohne Befund.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-23 13:56:43 +02:00
11 changed files with 445 additions and 71 deletions
+30
View File
@@ -17,6 +17,36 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
### Sicherheit
## [1.0.1] – 2026-10-25
### Hinzugefügt
- `npm run releases:sync` legt zu jedem Tag eine Release in Gitea an – Titel und Beschreibung stammen aus diesem CHANGELOG (#046)
## [1.0.0] – 2026-10-23
Erste stabile Fassung. Kein neuer Funktionsumfang gegenüber 0.27.1 – was fehlte, war Erprobung, und
die ist erbracht:
- **Ein Lizenzjahr vollständig durchlaufen.** Für `hv-pve-01` und `hv-pve-02` (Robert Hofmann GmbH):
Warnung vor dem Ablauf am 17.10.2026, Verlängerung, Proxmox-Rechnung 291017 mit Rabatt je Position,
Kundenrechnung, beide bezahlt. Die Folgelaufzeiten stehen auf 17.10.2027.
- **Proxmox Backup Server mit aktiver Lizenz bestätigt.** Drei PBS-Hosts melden Key und Ablaufdatum.
- **Proxmox VE** ist seit dem Feldtest im September bestätigt: mit und ohne Subscription, einzeln und
im Cluster, mit und ohne QDevice.
- **Wiederherstellung geprobt** (#045): Daten zerstört, zurückgespielt, geprüft.
Im Betrieb seit dem 03.10.2026 mit 16 Hosts, vier Kunden und echten Rechnungen.
**Weiterhin ungeprüft:** Proxmox Mail Gateway – dort wird derzeit kein Host betrieben; der Zweig ist
im Handbuch als ungeprüft gekennzeichnet (#023). Ebenso steht die Wiederherstellungsprobe für die
Installation als Dienst noch aus; geprobt ist der Docker-Weg.
## [0.27.1] – 2026-10-23
### Sicherheit
- **Next.js auf 16.4.2.** Für 16.0.0 bis 16.4.0 wurden drei Lücken gemeldet, zwei davon **kritisch**: Remote Code Execution über die Bildoptimierung mit SVG-Dateien (GHSA-2464-fhr4-w9xw), Remote Code Execution in `use cache` (GHSA-673q-mjm6-r5xr) und eine Blockade über Server Actions (GHSA-p3m5-59mp-jv5r)
- ProxLedger nutzt weder `next/image` noch `use cache`; die Blockade über Server Actions betraf die Installation dagegen unmittelbar. **Wer öffentlich erreichbar ist, sollte sofort aktualisieren.**
## [0.27.0] – 2026-10-10
### Hinzugefügt
+15 -1
View File
@@ -73,7 +73,7 @@
## 2026-09-17 · Sitzung 1 · 19:36–laufend
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
**Version:** 0.27.0
**Version:** 1.0.1
### Erledigt
- Auftrag gelesen, Rückfragen gestellt
@@ -232,6 +232,20 @@
- Wiederherstellung geprobt (#045): Daten zerstört, zurückgespielt, geprüft. Zwei Lücken kamen erst dabei heraus – ohne `--clean` ließ sich nur in eine leere Datenbank zurückspielen, und für die PDFs gab es gar keinen Weg
- Damit ist einer der vier Punkte bis 1.0.0 erledigt. Offen bleiben PMG, PBS mit Lizenz und ein vollständiger Lizenzjahr-Durchlauf (17.10.)
### 23.10.2026 – Wartungsblick
- `npm audit`: drei neue Meldungen zu Next.js ≤ 16.4.0, zwei davon kritisch (RCE über Bildoptimierung und `use cache`, dazu eine Blockade über Server Actions). Auf 16.4.2 angehoben, alles grün
- Geprüft, was davon ProxLedger trifft: `next/image` und `use cache` werden nicht genutzt, Server Actions dagegen überall – die Blockade war also echt
- Erinnerung an mich: Zwei Wartungsblicke in Folge haben hohe bis kritische Meldungen gefunden. Der wöchentliche Lauf in der CI ist überfällig
### 23.10.2026 – 1.0.0
- Florian hat den Beleg geliefert: `hv-pve-01` und `hv-pve-02` sind verlängert, beide Rechnungen bezahlt, die Folgelaufzeiten stehen auf 17.10.2027. Dazu drei PBS-Hosts mit aktiver Lizenz
- PMG wird zurückgestellt – kein Host im Betrieb. Die Lücke bleibt im Handbuch benannt, statt sie wegzudefinieren
- Damit ist 1.0.0 gerechtfertigt: nicht weil nichts mehr fehlt, sondern weil das, was fehlte, erprobt wurde
### 25.10.2026 – Releases
- `releases:sync` gebaut: Das CHANGELOG ist die Quelle, Gitea bekommt daraus Releases zu den vorhandenen Tags
- Dabei bestätigt, was beim Issue-Abgleich am 10.10. schon aufgefallen war: Der hinterlegte Zugang ist ein elfstelliges Passwort, kein API-Token. Pushen geht, die API nicht
### Nächster Schritt
- #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline
- Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023)
+9 -5
View File
@@ -3,10 +3,11 @@
Zentrales Dashboard für den Subscription-Status aller Proxmox-Hosts (VE, Backup Server, Mail Gateway).
Auf jedem Host meldet ein schlanker Agent täglich per HTTPS seinen Status (Push-Modell) – auch aus Kundennetzen hinter NAT.
> **Status:** einsatzbereit. Der Agent ist auf echten Hosts erprobt – Proxmox VE 9.2 mit und ohne
> Subscription, im Cluster und einzeln, sowie Proxmox Backup Server 4.2 (#018, #023).
> Ungeprüft: Mail Gateway und eine aktive Subscription auf PBS. Was bis 1.0 fehlt, steht in
> [docs/roadmap.md](docs/roadmap.md).
> **Status: 1.0.0 – im produktiven Einsatz.** Seit dem 03.10.2026 mit echten Daten in Betrieb; ein
> Lizenzjahr ist vollständig durchlaufen (Warnung → Verlängerung → Einkauf → Kundenrechnung → bezahlt).
> Erprobt auf Proxmox VE 9.2 mit und ohne Subscription, einzeln und im Cluster, sowie auf Proxmox
> Backup Server 4.2 mit und ohne Lizenz. **Ungeprüft:** Mail Gateway – dort wird derzeit kein Host
> betrieben. Was als Nächstes ansteht, steht in [docs/roadmap.md](docs/roadmap.md).
## Schnellstart
@@ -174,9 +175,12 @@ npm run issues:sync -- --dry-run # zeigt nur, was passieren würde
npm run issues:sync # echter Abgleich
```
Dasselbe gilt für Releases: `npm run releases:sync` legt zu jedem Tag einen Eintrag unter *Releases*
an und nimmt den passenden Abschnitt aus dem CHANGELOG als Beschreibung (`--dry-run` zeigt es nur an).
Serveradresse und Repository liest das Skript aus dem Git-Remote, den Token aus den hinterlegten
Git-Zugangsdaten (Schlüsselbund oder `~/.git-credentials`) – derselbe Token wie fürs Pushen, er braucht
zusätzlich das Recht `issue:write`. Wer beides trennen möchte, setzt `GITEA_URL`, `GITEA_REPO` und
zusätzlich die Rechte `issue:write` (Issues) und `write:repository` (Releases). Wer beides trennen möchte, setzt `GITEA_URL`, `GITEA_REPO` und
`GITEA_TOKEN` in der `.env`.
Der Abgleich legt Labels und Meilensteine an, erkennt vorhandene Issues an der Nummer im Titel,
+14 -3
View File
@@ -1,8 +1,8 @@
# #023 Feldtest auf Proxmox Backup Server und Mail Gateway
**Status:** offen
**Status:** erledigt
**Meilenstein:** v1.0.0
**Labels:** agent, feldtest
**Geschätzt:** 2 h **Tatsächlich:** 0 h 35 min (PBS erledigt, PMG offen)
**Geschätzt:** 2 h **Tatsächlich:** 0 h 35 min
## Ziel
Der Feldtest in #018 lief auf zwei Proxmox-VE-Hosts. Für Backup Server und Mail Gateway beruht die
@@ -23,7 +23,7 @@ falsch erwiesen (`--output-format json` gibt es dort nicht). Das gehört vor 1.0
- [x] Erhobene Ausgaben als Testfälle in `tests/agent-skript.test.ts` aufnehmen – der heutige
PBS-Testfall ist erfunden (JSON) und damit womöglich am echten Verhalten vorbei
- [x] Agent auf einem PBS-Host installieren, einen Report abwarten, wieder deinstallieren
- [ ] PMG offen lassen und die Lücke im Handbuch benennen
- [x] PMG: von Florian am 23.10.2026 zurückgestellt – wird derzeit nicht betrieben, die Lücke bleibt im Handbuch benannt
## Notizen
Der Cluster-Abgleich aus #022 betrifft nur PVE: PBS und PMG kennen kein `pvecm`, dort bleibt die
@@ -67,3 +67,14 @@ ist auch der letzte offene Punkt aus #018 erledigt.
**Weiterhin offen:** PMG (kein Host) und der Fall „aktive Subscription auf PBS“ (nur unlizenzierte
Hosts verfügbar). Dieses Issue bleibt deshalb offen.
## Abschluss am 23.10.2026
**PBS mit aktiver Lizenz: bestätigt.** In der produktiven Installation melden `pbs-cloud-01`,
`bup-pbs-01` und `bup-lwwif-pbs-01` ihre `pbsb-…`-Keys samt Ablaufdatum (04.10.2027). Damit ist auch
der lizenzierte Fall auf Proxmox Backup Server belegt – nach dem Fund der Rahmentabelle (#037) war
das der letzte offene Punkt dieses Produktzweigs.
**PMG: zurückgestellt.** Florian betreibt derzeit kein Mail Gateway; der Feldtest entfällt bis auf
Weiteres. Im Handbuch steht weiterhin, dass dieser Zweig ungeprüft ist und der erste PMG-Host nach
der Installation einmal gegenzuprüfen gehört (`proxledger-agent --dry-run`).
+36
View File
@@ -0,0 +1,36 @@
# #046 Releases in Gitea statt nur Tags
**Status:** erledigt
**Meilenstein:** v1.0.1
**Labels:** chore, doku
**Geschätzt:** 1 h **Tatsächlich:** 0 h 25 min
## Auslöser
„Kannst du auch Releases machen und nicht nur Tags?“
## Unterschied
Ein Tag ist eine Markierung im Git-Verlauf. Eine **Release** ist ein Eintrag in Gitea mit Titel,
Beschreibung und Download-Verweisen – sichtbar unter *Releases*, abonnierbar und verlinkbar.
Bislang gab es nur Tags; die Beschreibungen standen allein im CHANGELOG.
## Umsetzung
`npm run releases:sync` liest das CHANGELOG, nimmt jeden Abschnitt `## [x.y.z] – Datum`, für den es
ein Tag gibt, und legt daraus eine Release an. Ein zweiter Lauf aktualisiert nur, was sich geändert
hat; „Unreleased“ bleibt außen vor, weil es dazu kein Tag gibt.
```bash
npm run releases:sync -- --dry-run # zeigt nur, was passieren würde
npm run releases:sync # legt an und aktualisiert
npm run releases:sync -- v1.0.0 # nur eine Fassung
```
Fünf Tests decken die Zerlegung ab.
## Offen: der Token
Der in den Git-Zugangsdaten hinterlegte Wert ist elf Zeichen lang – ein Passwort, kein API-Token.
Pushen funktioniert damit, die API antwortet mit 401. Betrifft auch `npm run issues:sync`, das seit
dem 10.10.2026 nicht mehr durchläuft.
**Nötig:** in Gitea unter *Einstellungen → Anwendungen* ein Token mit den Rechten
`write:repository` (für Releases) und `issue:write` (für den Issue-Abgleich) erzeugen und entweder
als `GITEA_TOKEN` in die `.env` legen oder in den Git-Zugangsdaten hinterlegen. Danach trägt ein
einziger Lauf alle 43 vorhandenen Fassungen nach.
+3 -2
View File
@@ -26,7 +26,7 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
| [#020](020-gitea-issue-abgleich.md) | Issues nach Gitea übertragen | erledigt | v0.4.1 | 2 h | 0 h 10 min |
| [#021](021-geheimnisse-verschluesseln.md) | Zugangsdaten verschlüsselt speichern | erledigt | v0.13.1 | 2 h | 0 h 20 min |
| [#022](022-cluster-abgleich.md) | Cluster-Abgleich | erledigt | v0.14.0 | 3 h | 0 h 25 min |
| [#023](023-feldtest-pbs-pmg.md) | Feldtest auf PBS und PMG | offen | v1.0.0 | 2 h | 0 h 35 min (PBS erledigt, PMG offen) |
| [#023](023-feldtest-pbs-pmg.md) | Feldtest auf PBS und PMG | erledigt | v1.0.0 | 2 h | 0 h 35 min |
| [#024](024-owasp-haertung.md) | Härtung nach OWASP | erledigt | v0.15.0 | 4 h | 0 h 45 min |
| [#025](025-einstellungen.md) | Einstellungen im Dashboard | erledigt | v0.16.0 | 5 h | 1 h 10 min |
| [#026](026-auslieferung.md) | Auslieferung finalisieren | erledigt | v0.17.0 | 3 h | 0 h 40 min |
@@ -49,4 +49,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
| [#043](043-laufzeiten-bearbeiten.md) | Laufzeiten anlegen, bearbeiten, zusammenführen | erledigt | v0.25.0 | 4 h | 1 h 05 min |
| [#044](044-eigenbedarf.md) | Eigene Hosts ohne Weiterberechnung | erledigt | v0.26.0 | 2 h | 0 h 40 min |
| [#045](045-wiederherstellung-geprobt.md) | Wiederherstellung proben und Skript dafür | erledigt | v0.27.0 | 2 h | 0 h 50 min |
| | **Summe** | | | **177 h** | **25 h 28 min** |
| [#046](046-gitea-releases.md) | Releases in Gitea statt nur Tags | erledigt | v1.0.1 | 1 h | 0 h 25 min |
| | **Summe** | | | **178 h** | **25 h 53 min** |
+18 -5
View File
@@ -1,6 +1,6 @@
# Roadmap
Stand: 10.10.2026, Fassung 0.27.0
Stand: 23.10.2026, Fassung 1.0.0
Die erste Fassung dieser Roadmap entstand vor dem produktiven Einsatz. Seitdem läuft ProxLedger mit
echten Daten, und das hat die Reihenfolge deutlich verändert: Was aus der Benutzung kam, war fast
@@ -18,12 +18,25 @@ Stellen.
---
## Bis 1.0.0
## 1.0.0 ist erreicht (23.10.2026)
Kein neuer Funktionsumfang – es fehlt Erprobung.
Alle Kriterien erfüllt:
| Punkt | Warum | Aufwand |
|---|---|---|
| Punkt | Stand |
|---|---|
| Ein Lizenzjahr vollständig durchlaufen | **erledigt** – `hv-pve-01` und `hv-pve-02` am 17.10.2026, beide Rechnungen bezahlt |
| PBS mit aktiver Lizenz | **erledigt** – drei PBS-Hosts melden Key und Ablauf |
| Sicherung zurückspielen | **erledigt** ([#045](issues/045-wiederherstellung-geprobt.md)) |
| PMG im Feldtest | **zurückgestellt** – wird derzeit nicht betrieben ([#023](issues/023-feldtest-pbs-pmg.md)) |
Zwei kleinere Punkte bleiben offen, keiner davon hält 1.0.0 auf:
- Die Wiederherstellungsprobe für die **Installation als Dienst** (geprobt ist der Docker-Weg).
- Ein **wöchentlicher `npm audit`-Lauf in der CI**. Zwei Wartungsblicke hintereinander haben hohe bis
kritische Meldungen gefunden – zuletzt zwei kritische Lücken in Next.js. Bei einem öffentlich
erreichbaren Dashboard gehört das automatisiert.
---|---|---|
| **PMG im Feldtest** ([#023](issues/023-feldtest-pbs-pmg.md)) | Der einzige Produktzweig ohne echten Host. Bei PBS lag die Annahme daneben, und der Agent hätte dort nichts gemeldet. | 1 h, sobald ein Host da ist |
| **PBS mit aktiver Lizenz** | Bisher nur „keine Subscription“ geprüft. | 30 min |
| ~~Sicherung einmal zurückspielen~~ | **Erledigt am 10.10.2026** ([#045](issues/045-wiederherstellung-geprobt.md)) – dabei kamen zwei Lücken heraus. Offen bleibt die Probe für die Installation als Dienst. | – |
+51 -51
View File
@@ -1,12 +1,12 @@
{
"name": "proxledger",
"version": "0.26.0",
"version": "0.27.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "proxledger",
"version": "0.26.0",
"version": "0.27.0",
"hasInstallScript": true,
"dependencies": {
"@node-rs/argon2": "^2.2.1",
@@ -20,7 +20,7 @@
"bootstrap-icons": "^1.13.1",
"croner": "^10.0.1",
"jose": "^6.2.12",
"next": "^16.4.0",
"next": "^16.4.2",
"nodemailer": "^10.0.10",
"openid-client": "^6.8.8",
"react": "19.2.8",
@@ -35,7 +35,7 @@
"dotenv": "^17.4.2",
"esbuild": "^0.28.2",
"eslint": "^9",
"eslint-config-next": "^16.4.0",
"eslint-config-next": "^16.4.2",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.9.7",
"prisma": "^7.10.0",
@@ -1646,15 +1646,15 @@
}
},
"node_modules/@next/env": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.0.tgz",
"integrity": "sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.2.tgz",
"integrity": "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.0.tgz",
"integrity": "sha512-ffeflB/dL8RrvWejQximw01pOpKHN692Y3FjDB82DCd0YVvXvBD6vY7mcZp27yvSVFVM6wA73xVodEKF4spizA==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.2.tgz",
"integrity": "sha512-gCTWlw0e+Y6kajpFyKKgA195jUJCRhlzJE0lV7ufe8a+ghIsANWZ0/FiJwg6DuOG1+OvRbcZDcFBoRh3mNJx0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1695,9 +1695,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.0.tgz",
"integrity": "sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.2.tgz",
"integrity": "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==",
"cpu": [
"arm64"
],
@@ -1711,9 +1711,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.0.tgz",
"integrity": "sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.2.tgz",
"integrity": "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==",
"cpu": [
"x64"
],
@@ -1727,9 +1727,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.0.tgz",
"integrity": "sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.2.tgz",
"integrity": "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==",
"cpu": [
"arm64"
],
@@ -1746,9 +1746,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.0.tgz",
"integrity": "sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.2.tgz",
"integrity": "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==",
"cpu": [
"arm64"
],
@@ -1765,9 +1765,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.0.tgz",
"integrity": "sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.2.tgz",
"integrity": "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==",
"cpu": [
"x64"
],
@@ -1784,9 +1784,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.0.tgz",
"integrity": "sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.2.tgz",
"integrity": "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==",
"cpu": [
"x64"
],
@@ -1803,9 +1803,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.0.tgz",
"integrity": "sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.2.tgz",
"integrity": "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==",
"cpu": [
"arm64"
],
@@ -1819,9 +1819,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.0.tgz",
"integrity": "sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.2.tgz",
"integrity": "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==",
"cpu": [
"x64"
],
@@ -5889,13 +5889,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.0.tgz",
"integrity": "sha512-2RHLYswzc+hNRktWAjIL6XAvwb1cKcjLawJ5KWywAxn/JivNHtG42ZigL1yobVXDqsimuuL2F9jcEcnHFzbppQ==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.2.tgz",
"integrity": "sha512-P33vjcETMXc+qBjpF77TeJHZLzk6cPcTy8KjpxLucYB3mCIFIORMcZ+cBXcAiIYjE5RM7jyknEEOwMNdnx/ADQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "16.4.0",
"@next/eslint-plugin-next": "16.4.2",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -8184,12 +8184,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/next/-/next-16.4.0.tgz",
"integrity": "sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==",
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/next/-/next-16.4.2.tgz",
"integrity": "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==",
"license": "MIT",
"dependencies": {
"@next/env": "16.4.0",
"@next/env": "16.4.2",
"@swc/helpers": "0.5.23",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -8203,15 +8203,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.4.0",
"@next/swc-darwin-x64": "16.4.0",
"@next/swc-linux-arm64-gnu": "16.4.0",
"@next/swc-linux-arm64-musl": "16.4.0",
"@next/swc-linux-x64-gnu": "16.4.0",
"@next/swc-linux-x64-musl": "16.4.0",
"@next/swc-win32-arm64-msvc": "16.4.0",
"@next/swc-win32-x64-msvc": "16.4.0",
"sharp": "^0.35.4"
"@next/swc-darwin-arm64": "16.4.2",
"@next/swc-darwin-x64": "16.4.2",
"@next/swc-linux-arm64-gnu": "16.4.2",
"@next/swc-linux-arm64-musl": "16.4.2",
"@next/swc-linux-x64-gnu": "16.4.2",
"@next/swc-linux-x64-musl": "16.4.2",
"@next/swc-win32-arm64-msvc": "16.4.2",
"@next/swc-win32-x64-msvc": "16.4.2",
"sharp": "^0.35.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "proxledger",
"version": "0.27.0",
"version": "1.0.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -27,7 +27,8 @@
"screenshots": "node scripts/screenshots.mjs",
"icons": "node scripts/icons.mjs",
"pruefe:doku": "node scripts/pruefe-doku.mjs",
"benutzer": "tsx scripts/benutzer.ts"
"benutzer": "tsx scripts/benutzer.ts",
"releases:sync": "tsx scripts/gitea-releases.ts"
},
"dependencies": {
"@node-rs/argon2": "^2.2.1",
@@ -41,7 +42,7 @@
"bootstrap-icons": "^1.13.1",
"croner": "^10.0.1",
"jose": "^6.2.12",
"next": "^16.4.0",
"next": "^16.4.2",
"nodemailer": "^10.0.10",
"openid-client": "^6.8.8",
"react": "19.2.8",
@@ -56,7 +57,7 @@
"dotenv": "^17.4.2",
"esbuild": "^0.28.2",
"eslint": "^9",
"eslint-config-next": "^16.4.0",
"eslint-config-next": "^16.4.2",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.9.7",
"prisma": "^7.10.0",
+210
View File
@@ -0,0 +1,210 @@
/**
* Legt zu jedem Git-Tag eine Release in Gitea an – mit dem passenden Abschnitt aus dem CHANGELOG
* als Beschreibung.
*
* npm run releases:sync -- --dry-run # zeigt nur, was passieren würde
* npm run releases:sync # legt an und aktualisiert
* npm run releases:sync -- v1.0.0 # nur diese eine Fassung
*
* Quelle der Wahrheit bleibt das CHANGELOG: Ein zweites Mal aufgerufen, wird eine vorhandene
* Release nur aktualisiert, wenn sich der Text geändert hat.
*
* Der Token braucht das Recht **write:repository** – `issue:write` allein genügt nicht. Er wird
* aus `GITEA_TOKEN` gelesen, sonst aus den Git-Zugangsdaten (#047).
*/
import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import path from "node:path";
import "dotenv/config";
interface Fassung {
tag: string;
/** Überschrift ohne Klammern, z. B. „1.0.0 – 2026-10-23“. */
titel: string;
text: string;
datum: string | null;
}
interface Release {
id: number;
tag_name: string;
name: string;
body: string;
}
/** Zerlegt das CHANGELOG in seine Abschnitte je Fassung. */
export function changelogZerlegen(inhalt: string): Fassung[] {
const zeilen = inhalt.split("\n");
const fassungen: Fassung[] = [];
let aktuell: Fassung | null = null;
for (const zeile of zeilen) {
// Überschriften der Form „## [1.0.0] – 2026-10-23“ (Keep a Changelog).
const kopf = /^##\s*\[([^\]]+)\]\s*[–-]?\s*(.*)$/.exec(zeile);
if (kopf) {
if (aktuell) fassungen.push(aktuell);
const nummer = kopf[1].trim();
const datum = kopf[2].trim() || null;
// „Unreleased“ hat kein Tag und gehört nicht als Release nach Gitea.
aktuell = /^\d/.test(nummer)
? { tag: `v${nummer}`, titel: datum ? `${nummer} – ${datum}` : nummer, text: "", datum }
: null;
continue;
}
if (aktuell) aktuell.text += `${zeile}\n`;
}
if (aktuell) fassungen.push(aktuell);
return fassungen.map((fassung) => ({ ...fassung, text: fassung.text.trim() }));
}
function vorhandeneTags(): Set<string> {
const ausgabe = execFileSync("git", ["tag", "--list", "v*"], { encoding: "utf8" });
return new Set(ausgabe.split("\n").map((zeile) => zeile.trim()).filter(Boolean));
}
function ausRemote(): { basis: string; repo: string } | null {
try {
const url = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8" }).trim();
const treffer = /^https?:\/\/(?:[^@/]*@)?([^/]+)\/(.+?)(?:\.git)?$/.exec(url);
if (!treffer) return null;
return { basis: `https://${treffer[1]}`, repo: treffer[2] };
} catch {
return null;
}
}
/** Wie beim Issue-Abgleich: ein Token für Push und API, kein zweiter Eintrag in der .env. */
function tokenAusGitCredentials(basis: string): string | null {
const host = new URL(basis).host;
for (const zusatz of [[], ["-c", "credential.helper=store"]]) {
try {
const antwort = execFileSync("git", [...zusatz, "credential", "fill"], {
input: `protocol=https\nhost=${host}\n\n`,
encoding: "utf8",
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
stdio: ["pipe", "pipe", "ignore"],
});
const passwort = /^password=(.*)$/m.exec(antwort);
if (passwort?.[1]) return passwort[1];
} catch {
// Nächste Möglichkeit versuchen.
}
}
return null;
}
class Gitea {
constructor(
private readonly basis: string,
private readonly repo: string,
private readonly token: string,
) {}
private async anfrage<T>(pfad: string, optionen: RequestInit = {}): Promise<T> {
const antwort = await fetch(`${this.basis}/api/v1/repos/${this.repo}${pfad}`, {
...optionen,
headers: {
Authorization: `token ${this.token}`,
"Content-Type": "application/json",
...optionen.headers,
},
});
if (!antwort.ok) {
const text = await antwort.text();
const hinweis =
antwort.status === 401 || antwort.status === 403
? "\n Der Token braucht das Recht „write:repository“. Gitea: Einstellungen → Anwendungen → Token erzeugen."
: "";
throw new Error(`Gitea antwortet mit ${antwort.status} auf ${pfad}: ${text}${hinweis}`);
}
// Beim Löschen kommt 204 ohne Inhalt zurück.
return antwort.status === 204 ? (undefined as T) : ((await antwort.json()) as T);
}
releases() {
return this.anfrage<Release[]>("/releases?limit=100");
}
anlegen(daten: { tag_name: string; name: string; body: string }) {
return this.anfrage<Release>("/releases", { method: "POST", body: JSON.stringify(daten) });
}
aktualisieren(id: number, daten: { name: string; body: string }) {
return this.anfrage<Release>(`/releases/${id}`, { method: "PATCH", body: JSON.stringify(daten) });
}
}
async function main() {
const trockenlauf = process.argv.includes("--dry-run");
const nurFassung = process.argv.slice(2).find((wert) => /^v?\d+\.\d+\.\d+$/.test(wert));
const remote = ausRemote();
const basis = (process.env.GITEA_URL ?? remote?.basis ?? "").replace(/\/$/, "");
const repo = process.env.GITEA_REPO ?? remote?.repo ?? "";
if (!basis || !repo) {
console.error("Kein Gitea-Ziel gefunden. GITEA_URL und GITEA_REPO setzen oder ein Remote einrichten.");
process.exit(1);
}
const changelog = await readFile(path.join(process.cwd(), "CHANGELOG.md"), "utf8");
const tags = vorhandeneTags();
// Nur Fassungen, zu denen es auch ein Tag gibt – eine Release ohne Tag wäre eine Behauptung.
let fassungen = changelogZerlegen(changelog).filter((fassung) => tags.has(fassung.tag));
if (nurFassung) {
const gesucht = nurFassung.startsWith("v") ? nurFassung : `v${nurFassung}`;
fassungen = fassungen.filter((fassung) => fassung.tag === gesucht);
}
console.log(`Ziel: ${basis}/${repo}`);
console.log(`${fassungen.length} Fassung(en) mit Tag im CHANGELOG.`);
const token = process.env.GITEA_TOKEN ?? tokenAusGitCredentials(basis);
if (trockenlauf || !token) {
if (!token && !trockenlauf) {
console.log("\nKein Token gefunden – es folgt nur ein Trockenlauf.");
console.log("Ein Token mit dem Recht „write:repository“ gehört in GITEA_TOKEN.");
}
for (const fassung of fassungen) {
console.log(` ${fassung.tag}: „${fassung.titel}“ (${fassung.text.split("\n").length} Zeilen)`);
}
return;
}
const gitea = new Gitea(basis, repo, token);
const vorhanden = new Map((await gitea.releases()).map((release) => [release.tag_name, release]));
for (const fassung of fassungen) {
const alt = vorhanden.get(fassung.tag);
if (!alt) {
if (!trockenlauf) {
await gitea.anlegen({ tag_name: fassung.tag, name: fassung.titel, body: fassung.text });
}
console.log(`Angelegt: ${fassung.tag}`);
continue;
}
if (alt.body.trim() === fassung.text && alt.name === fassung.titel) {
console.log(`Unverändert: ${fassung.tag}`);
continue;
}
if (!trockenlauf) await gitea.aktualisieren(alt.id, { name: fassung.titel, body: fassung.text });
console.log(`Aktualisiert: ${fassung.tag}`);
}
console.log("\nAbgleich abgeschlossen.");
}
main().catch((fehler: unknown) => {
console.error(fehler instanceof Error ? fehler.message : fehler);
process.exit(1);
});
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { changelogZerlegen } from "../scripts/gitea-releases";
/** Zerlegung des CHANGELOG in Release-Abschnitte (#046). */
const BEISPIEL = `# Changelog
Alle wesentlichen Änderungen.
## [Unreleased]
- noch nichts
## [1.0.0] – 2026-10-23
Erste stabile Fassung.
### Hinzugefügt
- Dies und das
## [0.27.1] – 2026-10-23
### Sicherheit
- Next.js angehoben
`;
describe("CHANGELOG zerlegen", () => {
it("findet die Fassungen mit Datum", () => {
const fassungen = changelogZerlegen(BEISPIEL);
expect(fassungen.map((f) => f.tag)).toEqual(["v1.0.0", "v0.27.1"]);
expect(fassungen[0].titel).toBe("1.0.0 – 2026-10-23");
});
it("übergeht „Unreleased“ – dazu gibt es kein Tag", () => {
expect(changelogZerlegen(BEISPIEL).some((f) => f.tag.includes("Unreleased"))).toBe(false);
});
it("nimmt den Text bis zur nächsten Überschrift mit", () => {
const [eins] = changelogZerlegen(BEISPIEL);
expect(eins.text).toContain("Erste stabile Fassung.");
expect(eins.text).toContain("### Hinzugefügt");
expect(eins.text).not.toContain("0.27.1");
});
it("schneidet Leerzeilen an den Rändern ab", () => {
const [eins] = changelogZerlegen(BEISPIEL);
expect(eins.text.startsWith("Erste")).toBe(true);
expect(eins.text.endsWith("Dies und das")).toBe(true);
});
it("kommt mit einem Changelog ohne Fassungen zurecht", () => {
expect(changelogZerlegen("# Changelog\n\nNoch nichts veröffentlicht.\n")).toEqual([]);
});
});