Öffentliche Roadmap-Seite aus Gitea-Milestones

- GET /api/roadmap (public): Milestones des Setting roadmap_repo, 5-min-Cache,
  letzter guter Stand als Fallback bei Gitea-Ausfall
- /roadmap: Gruppen In Arbeit / Geplant / Fertig, Fortschrittsbalken im Brand-Look,
  Ziel-Monat, Markdown-Beschreibungen; Nav-Link öffentlich
- Setup-Seite: Roadmap-Repo konfigurierbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 12:24:31 +02:00
co-authored by Claude Opus 4.8
parent 39f9fd4f67
commit 6d8f383aed
7 changed files with 183 additions and 2 deletions
+34 -2
View File
@@ -6,7 +6,8 @@ import {
} from '../db.js';
import { config } from '../config.js';
import { removeDevlog } from '../bot/devlog-archive.js';
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl } from '../runtime-settings.js';
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo } from '../runtime-settings.js';
import { getMilestones } from '../gitea-api.js';
import { getSessionUser, isAdmin } from './auth.js';
const PAGE_SIZE = 20;
@@ -52,6 +53,36 @@ export function registerApiRoutes(app, client) {
return { items, total, page, pageSize: PAGE_SIZE };
});
// Roadmap — öffentlich, aus Gitea-Milestones (5-Minuten-Cache gegen API-Hammering)
let roadmapCache = { at: 0, repo: null, data: null };
app.get('/api/roadmap', async (request, reply) => {
const repo = roadmapRepo();
if (roadmapCache.data && roadmapCache.repo === repo && Date.now() - roadmapCache.at < 5 * 60_000) {
return roadmapCache.data;
}
try {
const milestones = await getMilestones(repo);
const data = {
repo,
milestones: milestones.map((m) => ({
title: m.title,
description: m.description ?? '',
state: m.state, // 'open' | 'closed'
open: m.open_issues ?? 0,
closed: m.closed_issues ?? 0,
due_on: m.due_on ?? null,
})),
};
roadmapCache = { at: Date.now(), repo, data };
return data;
} catch (error) {
request.log.error({ err: error }, 'Roadmap-Abruf fehlgeschlagen');
// Letzten guten Stand weiterreichen, falls vorhanden
if (roadmapCache.data && roadmapCache.repo === repo) return roadmapCache.data;
return reply.code(502).send({ error: 'Roadmap nicht abrufbar' });
}
});
// RSS-Feed der Devlogs — öffentlich abonnierbar
app.get('/feed.xml', async (request, reply) => {
const { items } = listDevlogs(20, 0);
@@ -144,6 +175,7 @@ ${rssItems}
weekly_recap_enabled: getSetting('weekly_recap_enabled') !== '0',
devlog_threads_enabled: getSetting('devlog_threads_enabled') !== '0',
bug_report_repo: getSetting('bug_report_repo') ?? 'D4rkst3r/EcoGame',
roadmap_repo: getSetting('roadmap_repo') ?? 'D4rkst3r/EcoGame',
watchdog_urls: getSetting('watchdog_urls') ?? '',
public_url: publicUrl(),
gitea_url: getSetting('gitea_url') ?? config.giteaUrl,
@@ -215,7 +247,7 @@ ${rssItems}
if (body.backup_enabled !== undefined) {
setSetting('backup_enabled', body.backup_enabled ? '1' : '0');
}
for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls']) {
for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls', 'roadmap_repo']) {
if (body[key] !== undefined) {
setSetting(key, String(body[key]).trim());
}