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:
+2
-1
@@ -5,9 +5,10 @@ import { devlogChannelId, devlogPingRoleId } from '../runtime-settings.js';
|
||||
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
||||
import * as ping from './commands/ping.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 }
|
||||
const commandModules = [ping, devlogBackfill];
|
||||
const commandModules = [ping, devlogBackfill, bug];
|
||||
|
||||
export async function startBot() {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user