Wochen-Rückblick: sonntags 20:00 automatisch in den Devlog-Kanal
- Scheduler (5-Minuten-Check, dedupe über last_weekly_recap-Setting) - Embed: Commits pro Tag als Balken-Grafik, Commit-/Devlog-Zahlen, Top-Projekte - Setting weekly_recap_enabled + 'Rückblick jetzt testen'-Button auf der Setup-Seite - TZ Europe/Berlin im Compose (Scheduler + Datumsformate) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// Wochen-Rückblick: sonntags 20:00 (Container-TZ, siehe compose) postet der Bot
|
||||
// eine Zusammenfassung der Woche in den Devlog-Kanal.
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { weeklyStats, getSetting, setSetting } from '../db.js';
|
||||
import { devlogChannelId } from '../runtime-settings.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const BRAND_YELLOW = 0xf5c518;
|
||||
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
/** Balken-Zeile: `Mo 21.07 ████████ 12` */
|
||||
function buildBars(perDay) {
|
||||
const byDay = new Map(perDay.map((r) => [r.day, r.n]));
|
||||
const rows = [];
|
||||
let max = 1;
|
||||
// Letzte 7 Tage, älteste zuerst
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date(Date.now() - i * 86400000);
|
||||
const key = date.toISOString().slice(0, 10);
|
||||
const n = byDay.get(key) ?? 0;
|
||||
max = Math.max(max, n);
|
||||
rows.push({ date, n });
|
||||
}
|
||||
const dayFmt = new Intl.DateTimeFormat('de-DE', { weekday: 'short' });
|
||||
const dateFmt = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
return rows
|
||||
.map((r) => {
|
||||
const bar = '█'.repeat(Math.round((r.n / max) * 14)) || '·';
|
||||
return `${dayFmt.format(r.date).padEnd(3)}${dateFmt.format(r.date)} ${bar} ${r.n}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/** Rückblick-Embed bauen und in den Devlog-Kanal posten */
|
||||
export async function postWeeklyRecap(client) {
|
||||
const channelId = devlogChannelId();
|
||||
if (!channelId) throw new Error('Kein Devlog-Kanal konfiguriert');
|
||||
|
||||
const channel = await client.channels.fetch(channelId);
|
||||
if (!channel?.isTextBased()) throw new Error('Devlog-Kanal nicht gefunden');
|
||||
|
||||
const stats = weeklyStats();
|
||||
const totalCommits = stats.perDay.reduce((sum, r) => sum + r.n, 0);
|
||||
const repoLine = stats.topRepos
|
||||
.map((r) => `**${r.repo.split('/').pop()}** (${r.n})`)
|
||||
.join(' · ');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setTitle('📊 Wochen-Rückblick')
|
||||
.setURL(`${config.publicUrl}/devlogs`)
|
||||
.setDescription(
|
||||
`\`\`\`\n${buildBars(stats.perDay)}\n\`\`\``
|
||||
)
|
||||
.addFields(
|
||||
{ name: 'Commits', value: String(totalCommits), inline: true },
|
||||
{ name: 'Devlogs', value: String(stats.devlogs), inline: true },
|
||||
...(repoLine ? [{ name: 'Aktive Projekte', value: repoLine, inline: false }] : [])
|
||||
)
|
||||
.setFooter({ text: 'D4RKST3R // WEEKLY' })
|
||||
.setTimestamp();
|
||||
|
||||
await channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
/** Scheduler starten: prüft alle 5 Minuten, ob Sonntag ≥ 20:00 und noch nicht gepostet */
|
||||
export function scheduleWeeklyRecap(client) {
|
||||
const check = async () => {
|
||||
if (getSetting('weekly_recap_enabled') === '0') return;
|
||||
|
||||
const now = new Date();
|
||||
if (now.getDay() !== 0 || now.getHours() < 20) return; // Sonntag ab 20:00 (lokale TZ)
|
||||
|
||||
const today = now.toISOString().slice(0, 10);
|
||||
if (getSetting('last_weekly_recap') === today) return;
|
||||
|
||||
try {
|
||||
await postWeeklyRecap(client);
|
||||
setSetting('last_weekly_recap', today);
|
||||
console.log('[weekly] Wochen-Rückblick gepostet');
|
||||
} catch (error) {
|
||||
console.error('[weekly] Rückblick fehlgeschlagen:', error);
|
||||
}
|
||||
};
|
||||
setInterval(check, CHECK_INTERVAL_MS);
|
||||
check();
|
||||
}
|
||||
@@ -211,6 +211,28 @@ export function listReleases(limit, offset) {
|
||||
return { items: selectReleases.all(limit, offset), total: countReleasesStmt.get().n };
|
||||
}
|
||||
|
||||
/** Zahlen der letzten 7 Tage für den Wochen-Rückblick */
|
||||
export function weeklyStats() {
|
||||
const perDay = db
|
||||
.prepare(`
|
||||
SELECT date(committed_at) AS day, count(*) AS n
|
||||
FROM commits WHERE committed_at >= datetime('now', '-7 days')
|
||||
GROUP BY day
|
||||
`)
|
||||
.all();
|
||||
const topRepos = db
|
||||
.prepare(`
|
||||
SELECT repo, count(*) AS n
|
||||
FROM commits WHERE committed_at >= datetime('now', '-7 days')
|
||||
GROUP BY repo ORDER BY n DESC LIMIT 3
|
||||
`)
|
||||
.all();
|
||||
const devlogs = db
|
||||
.prepare(`SELECT count(*) AS n FROM devlogs WHERE posted_at >= datetime('now', '-7 days')`)
|
||||
.get().n;
|
||||
return { perDay, topRepos, devlogs };
|
||||
}
|
||||
|
||||
/** Statistiken fürs Status-Panel der Settings-Seite */
|
||||
export function archiveStats() {
|
||||
return {
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
// Einstiegspunkt — startet Discord-Bot und Webserver (Webhooks)
|
||||
// Einstiegspunkt — startet Discord-Bot, Webserver und den Wochen-Rückblick-Scheduler
|
||||
import { startBot } from './bot/client.js';
|
||||
import { startWebServer } from './web/server.js';
|
||||
import { scheduleWeeklyRecap } from './bot/weekly-recap.js';
|
||||
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error('[main] Unhandled Rejection:', error);
|
||||
@@ -9,6 +10,7 @@ process.on('unhandledRejection', (error) => {
|
||||
try {
|
||||
const client = await startBot();
|
||||
await startWebServer(client);
|
||||
scheduleWeeklyRecap(client);
|
||||
} catch (error) {
|
||||
console.error('[main] Start fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -138,6 +138,7 @@ ${rssItems}
|
||||
commit_feed_enabled: getSetting('commit_feed_enabled') !== '0',
|
||||
commit_branch_filter: getSetting('commit_branch_filter') ?? '',
|
||||
ignored_repos: getSetting('ignored_repos') ?? '',
|
||||
weekly_recap_enabled: getSetting('weekly_recap_enabled') !== '0',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,6 +194,9 @@ ${rssItems}
|
||||
if (body.commit_feed_enabled !== undefined) {
|
||||
setSetting('commit_feed_enabled', body.commit_feed_enabled ? '1' : '0');
|
||||
}
|
||||
if (body.weekly_recap_enabled !== undefined) {
|
||||
setSetting('weekly_recap_enabled', body.weekly_recap_enabled ? '1' : '0');
|
||||
}
|
||||
for (const key of ['commit_branch_filter', 'ignored_repos']) {
|
||||
if (body[key] !== undefined) {
|
||||
setSetting(key, String(body[key]).trim());
|
||||
@@ -208,6 +212,18 @@ ${rssItems}
|
||||
if (requireAdmin(request, reply)) return;
|
||||
|
||||
const target = request.params.target;
|
||||
|
||||
// Sonderfall: echten Wochen-Rückblick sofort posten
|
||||
if (target === 'recap') {
|
||||
try {
|
||||
const { postWeeklyRecap } = await import('../bot/weekly-recap.js');
|
||||
await postWeeklyRecap(client);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
request.log.error({ err: error }, 'Wochen-Rückblick-Test fehlgeschlagen');
|
||||
return reply.code(502).send({ error: 'Senden fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
const channelId =
|
||||
target === 'commit' ? commitChannelId()
|
||||
: target === 'devlog' ? devlogChannelId()
|
||||
|
||||
Reference in New Issue
Block a user