Bug-Reports, Devlog-Threads und Watchdog
- /bug (alle Member): erstellt Gitea-Issue inkl. Screenshot-Upload als Asset; bug_reports-Tabelle für den Rückkanal — Issue geschlossen (issues-Webhook) → DM an den Reporter - Auto-Thread '💬 Devlog <Datum>' unter jedem Devlog-Post (Setting, Default an) - Watchdog: prüft Setting watchdog_urls alle 2 min, DM an Admin nach 2 Fails in Folge + Entwarnung mit Downtime-Dauer - Setup-Seite: Bug-Repo, Threads-Toggle, Watchdog-URLs; Env GITEA_API_TOKEN/GITEA_URL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,15 @@ ADMIN_DISCORD_ID=
|
|||||||
# Lokal zum Testen: http://localhost:3080 (auch im Developer Portal eintragen!)
|
# Lokal zum Testen: http://localhost:3080 (auch im Developer Portal eintragen!)
|
||||||
PUBLIC_URL=https://bot.d4rkst3r.de
|
PUBLIC_URL=https://bot.d4rkst3r.de
|
||||||
|
|
||||||
|
# --- Bug-Reports (/bug → Gitea-Issue) ---
|
||||||
|
|
||||||
|
# Gitea → Einstellungen → Anwendungen → Token erzeugen (Scope: write:issue)
|
||||||
|
# Ohne Token ist /bug deaktiviert (Bot läuft trotzdem)
|
||||||
|
GITEA_API_TOKEN=
|
||||||
|
|
||||||
|
# Optional: Gitea-Basis-URL (Default: https://git.d4rkst3r.de)
|
||||||
|
# GITEA_URL=https://git.d4rkst3r.de
|
||||||
|
|
||||||
# Optional: HTTP-Port des Webservers (Default: 3080)
|
# Optional: HTTP-Port des Webservers (Default: 3080)
|
||||||
# HTTP_PORT=3080
|
# HTTP_PORT=3080
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,31 @@ Unter **/settings** (Nav: „Setup", nur als Admin sichtbar) lassen sich zur Lau
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Bug-Reports, Threads & Watchdog
|
||||||
|
|
||||||
|
**`/bug` (alle Member):** Titel + Beschreibung + optional Screenshot → der Bot
|
||||||
|
erstellt ein Gitea-Issue im konfigurierten Repo (Setup-Seite), lädt den
|
||||||
|
Screenshot als Issue-Attachment hoch und merkt sich den Reporter.
|
||||||
|
Wird das Issue in Gitea **geschlossen**, bekommt der Reporter automatisch
|
||||||
|
eine DM („Dein Bug ist behoben ✅").
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
1. Gitea → Einstellungen → Anwendungen → **Token erzeugen** (Scope `write:issue`)
|
||||||
|
→ als `GITEA_API_TOKEN` in die Env
|
||||||
|
2. Im Gitea-Webhook zusätzlich das Trigger-Event **„Issues"** aktivieren
|
||||||
|
(für die Rückkanal-DMs)
|
||||||
|
|
||||||
|
**Auto-Threads:** Unter jedem Devlog-Post öffnet der Bot einen Diskussions-Thread
|
||||||
|
(„💬 Devlog 23.07.", Auto-Archiv nach 3 Tagen). Abschaltbar auf der Setup-Seite.
|
||||||
|
Der Bot braucht im Devlog-Kanal die Berechtigung **„Öffentliche Threads erstellen"**.
|
||||||
|
|
||||||
|
**Watchdog:** Auf der Setup-Seite URLs eintragen (kommagetrennt, z. B.
|
||||||
|
`https://git.d4rkst3r.de, https://bot.d4rkst3r.de/health`) — der Bot prüft sie
|
||||||
|
alle 2 Minuten und schickt dir bei Ausfall (2 Fehlschläge in Folge) eine
|
||||||
|
**Discord-DM**, inkl. Entwarnung mit Downtime-Dauer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Setup: Webinterface
|
## Setup: Webinterface
|
||||||
|
|
||||||
**Zugriffsmodell:** Devlog-Archiv ist öffentlich (wie der Discord-Kanal),
|
**Zugriffsmodell:** Devlog-Archiv ist öffentlich (wie der Discord-Kanal),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ services:
|
|||||||
COMMIT_CHANNEL_ID: ${COMMIT_CHANNEL_ID}
|
COMMIT_CHANNEL_ID: ${COMMIT_CHANNEL_ID}
|
||||||
DEVLOG_CHANNEL_ID: ${DEVLOG_CHANNEL_ID}
|
DEVLOG_CHANNEL_ID: ${DEVLOG_CHANNEL_ID}
|
||||||
DEVLOG_POST_SECRET: ${DEVLOG_POST_SECRET}
|
DEVLOG_POST_SECRET: ${DEVLOG_POST_SECRET}
|
||||||
|
GITEA_API_TOKEN: ${GITEA_API_TOKEN:-}
|
||||||
|
GITEA_URL: ${GITEA_URL:-https://git.d4rkst3r.de}
|
||||||
GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET}
|
GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET}
|
||||||
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET}
|
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET}
|
||||||
SESSION_SECRET: ${SESSION_SECRET}
|
SESSION_SECRET: ${SESSION_SECRET}
|
||||||
|
|||||||
@@ -178,11 +178,55 @@ export default function Settings({ me }) {
|
|||||||
<span className="field-hint">Commits pro Tag, Devlogs & Top-Projekte in den Devlog-Kanal</span>
|
<span className="field-hint">Commits pro Tag, Devlogs & Top-Projekte in den Devlog-Kanal</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.devlog_threads_enabled}
|
||||||
|
onChange={(e) => setForm({ ...form, devlog_threads_enabled: e.target.checked })}
|
||||||
|
/>
|
||||||
|
<span>Diskussions-Thread unter jedem Devlog</span>
|
||||||
|
<span className="field-hint">Bot öffnet automatisch „💬 Devlog <Datum>"</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<button className="btn" onClick={() => sendTest('recap')}>Rückblick jetzt testen</button>
|
<button className="btn" onClick={() => sendTest('recap')}>Rückblick jetzt testen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bug-Reports */}
|
||||||
|
<div className="settings-section">
|
||||||
|
<h2 className="settings-title">// Bug-Reports (/bug)</h2>
|
||||||
|
{!data.status.giteaTokenConfigured && (
|
||||||
|
<p className="notice" style={{ padding: '0 0 .8rem' }}>
|
||||||
|
⚠️ Kein <code>GITEA_API_TOKEN</code> gesetzt — /bug ist deaktiviert.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="field">
|
||||||
|
<label>Ziel-Repo <span className="field-hint">hier landen die Issues aus /bug</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="D4rkst3r/EcoGame"
|
||||||
|
value={form.bug_report_repo}
|
||||||
|
onChange={(e) => setForm({ ...form, bug_report_repo: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Watchdog */}
|
||||||
|
<div className="settings-section">
|
||||||
|
<h2 className="settings-title">// Watchdog</h2>
|
||||||
|
<div className="field">
|
||||||
|
<label>Überwachte URLs <span className="field-hint">kommagetrennt; bei Ausfall gibt's eine DM — leer = aus</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="https://git.d4rkst3r.de, https://bot.d4rkst3r.de/health"
|
||||||
|
value={form.watchdog_urls}
|
||||||
|
onChange={(e) => setForm({ ...form, watchdog_urls: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Commit-Feed */}
|
{/* Commit-Feed */}
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Commit-Feed</h2>
|
<h2 className="settings-title">// Commit-Feed</h2>
|
||||||
|
|||||||
+2
-1
@@ -5,9 +5,10 @@ import { devlogChannelId, devlogPingRoleId } from '../runtime-settings.js';
|
|||||||
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
||||||
import * as ping from './commands/ping.js';
|
import * as ping from './commands/ping.js';
|
||||||
import * as devlogBackfill from './commands/devlog-backfill.js';
|
import * as devlogBackfill from './commands/devlog-backfill.js';
|
||||||
|
import * as bug from './commands/bug.js';
|
||||||
|
|
||||||
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
||||||
const commandModules = [ping, devlogBackfill];
|
const commandModules = [ping, devlogBackfill, bug];
|
||||||
|
|
||||||
export async function startBot() {
|
export async function startBot() {
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// /bug — Bug-Report aus Discord: erstellt ein Gitea-Issue (inkl. Screenshot-Upload)
|
||||||
|
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||||
|
import { config } from '../../config.js';
|
||||||
|
import { bugReportRepo } from '../../runtime-settings.js';
|
||||||
|
import { createIssue, uploadIssueAsset } from '../../gitea-api.js';
|
||||||
|
import { saveBugReport } from '../../db.js';
|
||||||
|
|
||||||
|
export const data = new SlashCommandBuilder()
|
||||||
|
.setName('bug')
|
||||||
|
.setDescription('Bug melden — landet direkt beim Entwickler')
|
||||||
|
.addStringOption((o) =>
|
||||||
|
o.setName('titel').setDescription('Kurze Zusammenfassung').setRequired(true).setMaxLength(120)
|
||||||
|
)
|
||||||
|
.addStringOption((o) =>
|
||||||
|
o.setName('beschreibung')
|
||||||
|
.setDescription('Was ist passiert? Was hast du erwartet?')
|
||||||
|
.setRequired(true)
|
||||||
|
.setMaxLength(1500)
|
||||||
|
)
|
||||||
|
.addAttachmentOption((o) =>
|
||||||
|
o.setName('screenshot').setDescription('Optional: Screenshot des Problems')
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function execute(interaction) {
|
||||||
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
if (!config.giteaApiToken) {
|
||||||
|
await interaction.editReply('❌ Bug-Reports sind gerade nicht konfiguriert (kein Gitea-Token).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = bugReportRepo();
|
||||||
|
const titel = interaction.options.getString('titel');
|
||||||
|
const beschreibung = interaction.options.getString('beschreibung');
|
||||||
|
const screenshot = interaction.options.getAttachment('screenshot');
|
||||||
|
|
||||||
|
const body = `${beschreibung}\n\n---\n🎮 Gemeldet via Discord von **${interaction.user.tag}**`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issue = await createIssue(repo, `[Bug] ${titel}`, body);
|
||||||
|
|
||||||
|
// Screenshot als Issue-Attachment hochladen (Discord-CDN-Links laufen ab)
|
||||||
|
if (screenshot?.contentType?.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(screenshot.url);
|
||||||
|
if (res.ok) {
|
||||||
|
await uploadIssueAsset(
|
||||||
|
repo,
|
||||||
|
issue.number,
|
||||||
|
screenshot.name ?? 'screenshot.png',
|
||||||
|
Buffer.from(await res.arrayBuffer())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[bug] Screenshot-Upload fehlgeschlagen:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für den Rückkanal merken: Issue geschlossen → DM an den Reporter
|
||||||
|
saveBugReport({
|
||||||
|
repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
discord_user_id: interaction.user.id,
|
||||||
|
title: titel,
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.editReply(
|
||||||
|
`✅ Danke! Dein Bug ist erfasst als [#${issue.number}](${issue.html_url}) — ` +
|
||||||
|
`du bekommst eine DM, sobald er behoben ist.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[bug] Issue-Erstellung fehlgeschlagen:', error);
|
||||||
|
await interaction.editReply('❌ Konnte das Issue nicht anlegen — bitte später nochmal versuchen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Watchdog: prüft konfigurierte URLs und schickt dem Admin eine DM bei Ausfall.
|
||||||
|
// URLs kommen aus dem Setting watchdog_urls (Setup-Seite), leer = deaktiviert.
|
||||||
|
import { getSetting } from '../db.js';
|
||||||
|
import { config } from '../config.js';
|
||||||
|
|
||||||
|
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||||
|
const FAILS_BEFORE_ALERT = 2; // 2 Fehlschläge in Folge → Alarm (gegen Flattern)
|
||||||
|
const TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
|
// URL → { fails, down, since }
|
||||||
|
const state = new Map();
|
||||||
|
|
||||||
|
function watchedUrls() {
|
||||||
|
return (getSetting('watchdog_urls') ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => /^https?:\/\//.test(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dmAdmin(client, content) {
|
||||||
|
const user = await client.users.fetch(config.adminDiscordId);
|
||||||
|
await user.send(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ein Prüfdurchlauf — exportiert für Tests und den Intervall-Timer */
|
||||||
|
export async function watchdogTick(client, { failsBeforeAlert = FAILS_BEFORE_ALERT } = {}) {
|
||||||
|
for (const url of watchedUrls()) {
|
||||||
|
const s = state.get(url) ?? { fails: 0, down: false, since: null };
|
||||||
|
|
||||||
|
let ok = false;
|
||||||
|
let detail = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
redirect: 'follow',
|
||||||
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||||
|
headers: { 'User-Agent': 'ecobot-watchdog/1.0' },
|
||||||
|
});
|
||||||
|
ok = res.status < 500;
|
||||||
|
detail = `HTTP ${res.status}`;
|
||||||
|
} catch (error) {
|
||||||
|
detail = error.cause?.code ?? error.name ?? 'Fehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
if (s.down) {
|
||||||
|
const minutes = Math.max(1, Math.round((Date.now() - s.since) / 60000));
|
||||||
|
await dmAdmin(client, `✅ **${url}** ist wieder erreichbar (Downtime: ~${minutes} min).`).catch(() => {});
|
||||||
|
console.log(`[watchdog] ${url} wieder ok`);
|
||||||
|
}
|
||||||
|
state.set(url, { fails: 0, down: false, since: null });
|
||||||
|
} else {
|
||||||
|
const fails = s.fails + 1;
|
||||||
|
if (!s.down && fails >= failsBeforeAlert) {
|
||||||
|
await dmAdmin(client, `🚨 **${url}** ist DOWN (${detail}) — seit ${failsBeforeAlert} Checks nicht erreichbar!`).catch(() => {});
|
||||||
|
console.warn(`[watchdog] ${url} down (${detail})`);
|
||||||
|
state.set(url, { fails, down: true, since: Date.now() });
|
||||||
|
} else {
|
||||||
|
state.set(url, { ...s, fails });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startWatchdog(client) {
|
||||||
|
setInterval(() => watchdogTick(client), CHECK_INTERVAL_MS);
|
||||||
|
}
|
||||||
@@ -47,4 +47,8 @@ export const config = {
|
|||||||
|
|
||||||
// SQLite-Datei (im Container: /app/data → ecobot_data-Volume)
|
// SQLite-Datei (im Container: /app/data → ecobot_data-Volume)
|
||||||
dbPath: process.env.DB_PATH || './data/ecobot.db',
|
dbPath: process.env.DB_PATH || './data/ecobot.db',
|
||||||
|
|
||||||
|
// Gitea-API (für /bug → Issues). Ohne Token ist /bug deaktiviert.
|
||||||
|
giteaUrl: (process.env.GITEA_URL || 'https://git.d4rkst3r.de').replace(/\/$/, ''),
|
||||||
|
giteaApiToken: process.env.GITEA_API_TOKEN || null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,40 @@ db.exec(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bug-Reports aus Discord (/bug) — für den Rückkanal (Issue geschlossen → DM)
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS bug_reports (
|
||||||
|
repo TEXT NOT NULL,
|
||||||
|
issue_number INTEGER NOT NULL,
|
||||||
|
discord_user_id TEXT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
PRIMARY KEY (repo, issue_number)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertBugReport = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO bug_reports (repo, issue_number, discord_user_id, title)
|
||||||
|
VALUES (@repo, @issue_number, @discord_user_id, @title)
|
||||||
|
`);
|
||||||
|
const getBugReportStmt = db.prepare(
|
||||||
|
'SELECT * FROM bug_reports WHERE repo = ? AND issue_number = ?'
|
||||||
|
);
|
||||||
|
const deleteBugReportStmt = db.prepare(
|
||||||
|
'DELETE FROM bug_reports WHERE repo = ? AND issue_number = ?'
|
||||||
|
);
|
||||||
|
|
||||||
|
export function saveBugReport(report) {
|
||||||
|
insertBugReport.run(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Report holen und direkt entfernen (einmalige Benachrichtigung) */
|
||||||
|
export function takeBugReport(repo, issueNumber) {
|
||||||
|
const row = getBugReportStmt.get(repo, issueNumber);
|
||||||
|
if (row) deleteBugReportStmt.run(repo, issueNumber);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
// Laufzeit-Einstellungen (Settings-Seite im Webinterface) — überschreiben Env-Defaults
|
// Laufzeit-Einstellungen (Settings-Seite im Webinterface) — überschreiben Env-Defaults
|
||||||
db.exec('CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
db.exec('CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
||||||
const getSettingStmt = db.prepare('SELECT value FROM settings WHERE key = ?');
|
const getSettingStmt = db.prepare('SELECT value FROM settings WHERE key = ?');
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Minimaler Gitea-API-Client (Token-Auth) — für /bug → Issues
|
||||||
|
import { config } from './config.js';
|
||||||
|
|
||||||
|
function headers(extra = {}) {
|
||||||
|
return {
|
||||||
|
Authorization: `token ${config.giteaApiToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Issue anlegen; gibt { number, html_url } zurück */
|
||||||
|
export async function createIssue(repo, title, body) {
|
||||||
|
const res = await fetch(`${config.giteaUrl}/api/v1/repos/${repo}/issues`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers({ 'Content-Type': 'application/json' }),
|
||||||
|
body: JSON.stringify({ title, body }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Gitea createIssue ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
const issue = await res.json();
|
||||||
|
return { number: issue.number, html_url: issue.html_url };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Datei (z. B. Screenshot) als Issue-Attachment hochladen */
|
||||||
|
export async function uploadIssueAsset(repo, issueNumber, filename, buffer) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('attachment', new Blob([buffer]), filename);
|
||||||
|
const res = await fetch(
|
||||||
|
`${config.giteaUrl}/api/v1/repos/${repo}/issues/${issueNumber}/assets?name=${encodeURIComponent(filename)}`,
|
||||||
|
{ method: 'POST', headers: headers(), body: form }
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Gitea uploadIssueAsset ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { startBot } from './bot/client.js';
|
import { startBot } from './bot/client.js';
|
||||||
import { startWebServer } from './web/server.js';
|
import { startWebServer } from './web/server.js';
|
||||||
import { scheduleWeeklyRecap } from './bot/weekly-recap.js';
|
import { scheduleWeeklyRecap } from './bot/weekly-recap.js';
|
||||||
|
import { startWatchdog } from './bot/watchdog.js';
|
||||||
|
|
||||||
process.on('unhandledRejection', (error) => {
|
process.on('unhandledRejection', (error) => {
|
||||||
console.error('[main] Unhandled Rejection:', error);
|
console.error('[main] Unhandled Rejection:', error);
|
||||||
@@ -11,6 +12,7 @@ try {
|
|||||||
const client = await startBot();
|
const client = await startBot();
|
||||||
await startWebServer(client);
|
await startWebServer(client);
|
||||||
scheduleWeeklyRecap(client);
|
scheduleWeeklyRecap(client);
|
||||||
|
startWatchdog(client);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[main] Start fehlgeschlagen:', error);
|
console.error('[main] Start fehlgeschlagen:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ export function releaseChannelId() {
|
|||||||
return getSetting('release_channel_id') || null;
|
return getSetting('release_channel_id') || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Diskussions-Thread unter jedem Devlog-Post (Default: an) */
|
||||||
|
export function devlogThreadsEnabled() {
|
||||||
|
return getSetting('devlog_threads_enabled') !== '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ziel-Repo für /bug-Reports (Default: EcoGame) */
|
||||||
|
export function bugReportRepo() {
|
||||||
|
return getSetting('bug_report_repo') || 'D4rkst3r/EcoGame';
|
||||||
|
}
|
||||||
|
|
||||||
/** Rolle, die bei Devlog-Posts gepingt wird — leer = kein Ping */
|
/** Rolle, die bei Devlog-Posts gepingt wird — leer = kein Ping */
|
||||||
export function devlogPingRoleId() {
|
export function devlogPingRoleId() {
|
||||||
return getSetting('devlog_ping_role_id') || null;
|
return getSetting('devlog_ping_role_id') || null;
|
||||||
|
|||||||
+8
-1
@@ -139,6 +139,9 @@ ${rssItems}
|
|||||||
commit_branch_filter: getSetting('commit_branch_filter') ?? '',
|
commit_branch_filter: getSetting('commit_branch_filter') ?? '',
|
||||||
ignored_repos: getSetting('ignored_repos') ?? '',
|
ignored_repos: getSetting('ignored_repos') ?? '',
|
||||||
weekly_recap_enabled: getSetting('weekly_recap_enabled') !== '0',
|
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',
|
||||||
|
watchdog_urls: getSetting('watchdog_urls') ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +157,7 @@ ${rssItems}
|
|||||||
botTag: client.user?.tag ?? null,
|
botTag: client.user?.tag ?? null,
|
||||||
uptimeSeconds: Math.floor(process.uptime()),
|
uptimeSeconds: Math.floor(process.uptime()),
|
||||||
guilds: client.guilds.cache.size,
|
guilds: client.guilds.cache.size,
|
||||||
|
giteaTokenConfigured: Boolean(config.giteaApiToken),
|
||||||
...stats,
|
...stats,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -197,7 +201,10 @@ ${rssItems}
|
|||||||
if (body.weekly_recap_enabled !== undefined) {
|
if (body.weekly_recap_enabled !== undefined) {
|
||||||
setSetting('weekly_recap_enabled', body.weekly_recap_enabled ? '1' : '0');
|
setSetting('weekly_recap_enabled', body.weekly_recap_enabled ? '1' : '0');
|
||||||
}
|
}
|
||||||
for (const key of ['commit_branch_filter', 'ignored_repos']) {
|
if (body.devlog_threads_enabled !== undefined) {
|
||||||
|
setSetting('devlog_threads_enabled', body.devlog_threads_enabled ? '1' : '0');
|
||||||
|
}
|
||||||
|
for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls']) {
|
||||||
if (body[key] !== undefined) {
|
if (body[key] !== undefined) {
|
||||||
setSetting(key, String(body[key]).trim());
|
setSetting(key, String(body[key]).trim());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|||||||
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { saveDevlog } from '../db.js';
|
import { saveDevlog } from '../db.js';
|
||||||
import { devlogChannelId, devlogPingRoleId } from '../runtime-settings.js';
|
import { devlogChannelId, devlogPingRoleId, devlogThreadsEnabled } from '../runtime-settings.js';
|
||||||
import { imagesDir } from '../bot/devlog-archive.js';
|
import { imagesDir } from '../bot/devlog-archive.js';
|
||||||
|
|
||||||
const BRAND_YELLOW = 0xf5c518;
|
const BRAND_YELLOW = 0xf5c518;
|
||||||
@@ -154,6 +154,21 @@ export function registerDevlogEndpoint(app, client) {
|
|||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Diskussions-Thread unterm Devlog (Fehler nicht fatal — z. B. fehlende Rechte)
|
||||||
|
if (devlogThreadsEnabled() && typeof message.startThread === 'function') {
|
||||||
|
try {
|
||||||
|
const threadDate = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
day: '2-digit', month: '2-digit',
|
||||||
|
}).format(new Date());
|
||||||
|
await message.startThread({
|
||||||
|
name: `💬 Devlog ${threadDate}`,
|
||||||
|
autoArchiveDuration: 4320, // 3 Tage
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
request.log.warn(`Devlog-Thread fehlgeschlagen: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Direkt archivieren (der Live-Listener ignoriert eigene Bot-Posts bewusst)
|
// Direkt archivieren (der Live-Listener ignoriert eigene Bot-Posts bewusst)
|
||||||
const imageFiles = [];
|
const imageFiles = [];
|
||||||
for (const [i, f] of files.entries()) {
|
for (const [i, f] of files.entries()) {
|
||||||
|
|||||||
+23
-1
@@ -8,7 +8,7 @@ import { existsSync } from 'node:fs';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { saveCommits, saveRelease } from '../db.js';
|
import { saveCommits, saveRelease, takeBugReport } from '../db.js';
|
||||||
import { commitFeedEnabled, branchAllowed, repoIgnored } from '../runtime-settings.js';
|
import { commitFeedEnabled, branchAllowed, repoIgnored } from '../runtime-settings.js';
|
||||||
import { postPushEmbed } from '../bot/commit-feed.js';
|
import { postPushEmbed } from '../bot/commit-feed.js';
|
||||||
import { postReleaseEmbed } from '../bot/release-feed.js';
|
import { postReleaseEmbed } from '../bot/release-feed.js';
|
||||||
@@ -140,6 +140,28 @@ export async function startWebServer(client) {
|
|||||||
return { ok: true, release: release.tag, posted };
|
return { ok: true, release: release.tag, posted };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue geschlossen → Rückkanal-DM an den Discord-Reporter (/bug)
|
||||||
|
if (event === 'issues') {
|
||||||
|
const payload = request.body;
|
||||||
|
if (payload.action === 'closed' && payload.issue?.number) {
|
||||||
|
const repo = payload.repository?.full_name ?? '';
|
||||||
|
const report = takeBugReport(repo, payload.issue.number);
|
||||||
|
if (report) {
|
||||||
|
try {
|
||||||
|
const user = await client.users.fetch(report.discord_user_id);
|
||||||
|
await user.send(
|
||||||
|
`✅ Dein gemeldeter Bug **„${report.title}"** wurde behoben!\n` +
|
||||||
|
`Issue [#${payload.issue.number}](${payload.issue.html_url ?? ''}) ist geschlossen — danke für den Report! 🙌`
|
||||||
|
);
|
||||||
|
request.log.info(`Bug-Report-DM an ${report.discord_user_id} (Issue #${payload.issue.number})`);
|
||||||
|
} catch (error) {
|
||||||
|
request.log.warn(`Bug-Report-DM fehlgeschlagen: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: true, event: 'issues' };
|
||||||
|
}
|
||||||
|
|
||||||
// Andere Events (Tag, Issue, …) freundlich ignorieren
|
// Andere Events (Tag, Issue, …) freundlich ignorieren
|
||||||
if (event !== 'push') {
|
if (event !== 'push') {
|
||||||
return { ok: true, ignored: event };
|
return { ok: true, ignored: event };
|
||||||
|
|||||||
Reference in New Issue
Block a user