Willkommens-Karten, Geburtstage, Devlog-Permalinks mit OG-Vorschau, Auto-Publish

- Willkommens-Karten: gerendertes PNG (SVG → sharp) mit Avatar im
  Neon-Ring, Willkommens-Schriftzug und Member-Nummer; Fallback aufs
  bisherige Embed wenn das Rendering scheitert; Dockerfile installiert
  fonts-dejavu-core für die Text-Darstellung
- Geburtstags-System: /geburtstag setzen|entfernen (birthdays-Tabelle),
  tägliche Runde ab 09:00 Europe/Berlin (Doppel-Post-Schutz über
  last_birthday_run), Gratulations-Embed + Tages-Rolle (wird am
  nächsten Morgen wieder abgeräumt); Kanal + Rolle im Community-Tab
- Devlog-Permalinks: /devlogs/:id als eigene Seite (GET /api/devlogs/:id),
  Link-Symbol an jeder Karte, Link-kopieren-Button; der Server injiziert
  Open-Graph-Tags ins SPA-HTML — Devlog-Links zeigen Titel, Anriss und
  Bild, alle anderen Seiten bekommen Default-Tags (auch die Startseite)
- Auto-Publish: maybeCrosspost() veröffentlicht Devlog-, Release-,
  Composer- und geplante Posts automatisch in Ankündigungs-Kanälen
- brandEmbed: leere Avatar-URL crasht nicht mehr die Footer-Validierung

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:43:15 +02:00
co-authored by Claude Fable 5
parent ef0fb066f7
commit 676fc45bb1
23 changed files with 1080 additions and 21 deletions
+19 -2
View File
@@ -1,7 +1,7 @@
// REST-API fürs Webinterface: Devlogs öffentlich, Commits + Settings nur für den Admin
import { EmbedBuilder } from 'discord.js';
import {
listDevlogs, searchDevlogs, listCommits, listReleases, archiveStats,
listDevlogs, searchDevlogs, getDevlog, listCommits, listReleases, archiveStats,
getSetting, setSetting, createApiKey, listApiKeys, deleteApiKey,
listGallery, listPlaytesters, topWishes, commitHeatmap,
createRoleMenu, updateRoleMenu, getRoleMenu, listRoleMenus, deleteRoleMenu,
@@ -89,6 +89,18 @@ export function registerApiRoutes(app, client) {
return { items: mapped, total, page, pageSize: PAGE_SIZE };
});
// Einzelnes Devlog (Permalink-Seite /devlogs/:id)
app.get('/api/devlogs/:id', async (request, reply) => {
const item = getDevlog(String(request.params.id));
if (!item) return reply.code(404).send({ error: 'Devlog nicht gefunden' });
return {
item: {
...item,
images: JSON.parse(item.images || '[]').map((f) => `/devlog-assets/${f}`),
},
};
});
// Changelog — öffentlich (Releases aller Repos)
app.get('/api/releases', async (request) => {
const { limit, offset, page } = paging(request);
@@ -339,6 +351,8 @@ ${rssItems}
server_alert_channel_id: getSetting('server_alert_channel_id') ?? '',
member_gate_enabled: getSetting('member_gate_enabled') !== '0',
discord_invite_url: getSetting('discord_invite_url') ?? '',
birthday_channel_id: getSetting('birthday_channel_id') ?? '',
birthday_role_id: getSetting('birthday_role_id') ?? '',
};
}
@@ -385,6 +399,7 @@ ${rssItems}
'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id',
'modlog_channel_id', 'status_channel_id', 'voting_channel_id', 'ticket_channel_id',
'events_announce_channel_id', 'social_announce_channel_id', 'server_alert_channel_id',
'birthday_channel_id',
];
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
if (body[key] === undefined) continue;
@@ -400,7 +415,7 @@ ${rssItems}
setSetting(key, value);
}
// Rollen: müssen existieren ('' = Feature aus)
for (const key of ['devlog_ping_role_id', 'playtester_role_id', 'autorole_id']) {
for (const key of ['devlog_ping_role_id', 'playtester_role_id', 'autorole_id', 'birthday_role_id']) {
if (body[key] === undefined) continue;
const value = String(body[key]);
if (value !== '') {
@@ -1005,6 +1020,8 @@ ${rssItems}
}
const message = await channel.send(payload);
const { maybeCrosspost } = await import('../embeds.js');
await maybeCrosspost(message);
request.log.info(`Composer: Nachricht ${message.id} gesendet`);
logAudit(getSessionUser(request), 'composer gesendet', `#${channel.name ?? channelId}`);
return { ok: true, edited: false, message_id: message.id };