Release-Ankündigungen + öffentliche Changelog-Seite

- Gitea-Release-Webhook: Releases werden archiviert (releases-Tabelle) und neue
  'published'-Releases als oranges 🚀-Embed mit Buttons angekündigt
- Release-Kanal auf der Setup-Seite wählbar (leer = Feature aus) + Test-Button
- /changelog: öffentliche Seite mit Tag-Chips, Pre-Release-Badge, Release-Notes
- /api/releases öffentlich; Updates/Redeliveries posten nicht doppelt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 10:37:28 +02:00
co-authored by Claude Opus 4.8
parent 5a57108230
commit be1fc227d0
9 changed files with 273 additions and 15 deletions
+41
View File
@@ -41,6 +41,20 @@ if (!devlogCols.some((c) => c.name === 'images')) {
db.exec(`ALTER TABLE devlogs ADD COLUMN images TEXT NOT NULL DEFAULT '[]'`);
}
// Releases (Gitea-Release-Webhook) — für Ankündigungen + Changelog-Seite
db.exec(`
CREATE TABLE IF NOT EXISTS releases (
repo TEXT NOT NULL,
tag TEXT NOT NULL,
name TEXT,
body TEXT,
url TEXT,
prerelease INTEGER NOT NULL DEFAULT 0,
published_at TEXT NOT NULL,
PRIMARY KEY (repo, tag)
);
`);
// Volltextsuche (FTS5) über Devlogs — Trigger halten den Index synchron
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS devlogs_fts USING fts5(content, message_id UNINDEXED);
@@ -170,6 +184,33 @@ export function listCommits(limit, offset) {
return { items: selectCommits.all(limit, offset), total: countCommitsStmt.get().n };
}
const upsertRelease = db.prepare(`
INSERT INTO releases (repo, tag, name, body, url, prerelease, published_at)
VALUES (@repo, @tag, @name, @body, @url, @prerelease, @published_at)
ON CONFLICT(repo, tag) DO UPDATE SET
name = excluded.name, body = excluded.body, url = excluded.url,
prerelease = excluded.prerelease
`);
/** Release speichern/aktualisieren. Gibt true zurück, wenn neu (→ ankündigen). */
export function saveRelease(release) {
const existed = db
.prepare('SELECT 1 FROM releases WHERE repo = ? AND tag = ?')
.get(release.repo, release.tag);
upsertRelease.run(release);
return !existed;
}
const selectReleases = db.prepare(`
SELECT repo, tag, name, body, url, prerelease, published_at
FROM releases ORDER BY published_at DESC LIMIT ? OFFSET ?
`);
const countReleasesStmt = db.prepare('SELECT count(*) AS n FROM releases');
export function listReleases(limit, offset) {
return { items: selectReleases.all(limit, offset), total: countReleasesStmt.get().n };
}
/** Statistiken fürs Status-Panel der Settings-Seite */
export function archiveStats() {
return {