diff --git a/README.md b/README.md index fa214b6..7890181 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,24 @@ Bilder werden lokal gespeichert (`data/devlog_images/`), weil Discord-CDN-Links --- +## Setup-Seite im Webinterface (Admin) + +Unter **/settings** (Nav: „Setup", nur als Admin sichtbar) lassen sich zur Laufzeit +ändern — gespeichert in SQLite, **Env-Variablen sind nur noch der Fallback**, +Änderungen greifen sofort ohne Redeploy: + +- **Devlog-Kanal** und **Commit-Kanal** als Dropdown (alle Textkanäle, die der + Bot sieht) — mit **Test senden**-Button pro Kanal +- **Commit-Feed an/aus** (aus = Commits werden weiter archiviert, nur nicht gepostet) +- **Branch-Filter** (kommagetrennt, leer = alle) — gefilterte Branches werden + archiviert, aber nicht gepostet +- **Ignorierte Repos** (kommagetrennt, z. B. `D4rkst3r/ecobot`) — komplett übersprungen +- **Status-Panel:** Bot-Account, Uptime, Anzahl Devlogs/Commits, DB-Größe + +`COMMIT_CHANNEL_ID` und `DEVLOG_CHANNEL_ID` sind damit optional geworden. + +--- + ## Setup: Webinterface **Zugriffsmodell:** Devlog-Archiv ist öffentlich (wie der Discord-Kanal), diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bea3fdd..9ce2cb9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import { Routes, Route, NavLink, Navigate } from 'react-router-dom'; import { apiGet } from './api.js'; import Devlogs from './pages/Devlogs.jsx'; import Commits from './pages/Commits.jsx'; +import Settings from './pages/Settings.jsx'; function DiscordMark() { return ( @@ -69,6 +70,7 @@ export default function App() {
@@ -91,6 +93,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/api.js b/frontend/src/api.js index 6e92076..d2c2e94 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -11,3 +11,15 @@ async function api(path, options = {}) { export const apiGet = (path) => api(path); export const apiDelete = (path) => api(path, { method: 'DELETE' }); +export const apiPut = (path, body) => + api(path, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(body), + }); +export const apiPost = (path, body = {}) => + api(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(body), + }); diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx new file mode 100644 index 0000000..6d007c1 --- /dev/null +++ b/frontend/src/pages/Settings.jsx @@ -0,0 +1,185 @@ +import { useEffect, useState } from 'react'; +import { apiGet, apiPut, apiPost } from '../api.js'; + +/** Sekunden → "2d 4h 13m" */ +function fmtUptime(s) { + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor((s % 3600) / 60); + return [d && `${d}d`, (d || h) && `${h}h`, `${m}m`].filter(Boolean).join(' '); +} + +function fmtBytes(b) { + if (b > 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`; + return `${(b / 1024).toFixed(0)} KB`; +} + +export default function Settings({ me }) { + const [data, setData] = useState(null); + const [form, setForm] = useState(null); + const [error, setError] = useState(null); + const [feedback, setFeedback] = useState(null); + + useEffect(() => { + if (!me.admin) return; + apiGet('/api/settings') + .then((d) => { + setData(d); + setForm(d.settings); + }) + .catch((e) => setError(e.status)); + }, [me.admin]); + + function flash(msg) { + setFeedback(msg); + setTimeout(() => setFeedback(null), 3000); + } + + async function save() { + try { + const res = await apiPut('/api/settings', form); + setForm(res.settings); + flash('✓ Gespeichert'); + } catch { + flash('✗ Speichern fehlgeschlagen'); + } + } + + async function sendTest(target) { + try { + await apiPost(`/api/settings/test/${target}`); + flash('✓ Test-Nachricht gesendet'); + } catch { + flash('✗ Test fehlgeschlagen — Kanal/Rechte prüfen'); + } + } + + const header = ( +
+
SETUP
+
+ ); + + if (me.loading) return <>{header}

Lade …

; + if (!me.user || !me.admin) { + return ( + <> + {header} +
+
+

🔒 Nur für den Admin.

+ {!me.user && Login mit Discord} +
+
+ + ); + } + if (error) return <>{header}

Fehler beim Laden ({error}).

; + if (!data || !form) return <>{header}

Lade …

; + + const channelOptions = data.channels.map((c) => ( + + )); + + return ( + <> + {header} +
+ {/* Status */} +
+

// Status

+
+
{data.status.botTag ?? '—'}Bot
+
{fmtUptime(data.status.uptimeSeconds)}Uptime
+
{data.status.devlogs}Devlogs
+
{data.status.commits}Commits
+
{fmtBytes(data.status.dbSizeBytes)}Datenbank
+
+
+ + {/* Kanäle */} +
+

// Kanäle

+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ + {/* Commit-Feed */} +
+

// Commit-Feed

+ +
+ +
+ +
+ + setForm({ ...form, commit_branch_filter: e.target.value })} + /> +
+ +
+ + setForm({ ...form, ignored_repos: e.target.value })} + /> +
+
+ +
+ + {feedback && {feedback}} +
+
+ + ); +} diff --git a/frontend/src/style.css b/frontend/src/style.css index b0b488e..aa8e49e 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -354,6 +354,86 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } font-size: .68rem; color: var(--muted2); white-space: nowrap; } +/* ── SETTINGS ──────────────────────────────────────── */ +.settings-section { + background: var(--bg2); + border: 1px solid rgba(255, 255, 255, .06); + padding: 1.4rem 1.6rem; + margin-bottom: 1.5rem; +} +.settings-title { + font-family: var(--mono); + font-size: .72rem; letter-spacing: .3em; text-transform: uppercase; + color: var(--neon2); + margin-bottom: 1.2rem; +} + +.stats { display: flex; flex-wrap: wrap; gap: 2.5rem; } +.stat { display: flex; flex-direction: column; gap: .1rem; } +.stat-value { + font-family: var(--display); + font-size: 1.5rem; letter-spacing: .05em; color: var(--text); +} +.stat-label { + font-family: var(--mono); + font-size: .6rem; letter-spacing: .25em; text-transform: uppercase; + color: var(--muted2); +} + +.field { margin-bottom: 1.1rem; } +.field:last-child { margin-bottom: 0; } +.field > label { + display: block; + font-family: var(--mono); + font-size: .68rem; letter-spacing: .15em; text-transform: uppercase; + color: var(--muted); + margin-bottom: .4rem; +} +.field-hint { + text-transform: none; letter-spacing: .03em; + color: var(--muted2); margin-left: .5rem; +} +.field-row { display: flex; gap: .6rem; } +.field-row select { flex: 1; } + +.field select, .field input[type="text"] { + width: 100%; + background: #0d0d0d; + border: 1px solid rgba(255, 255, 255, .12); + color: var(--text); + font-family: var(--body); + font-size: .95rem; + padding: .5rem .7rem; + outline: none; + transition: border-color .2s; +} +.field select:focus, .field input[type="text"]:focus { border-color: var(--neon); } +.field select option { background: #0d0d0d; } + +.toggle { display: flex; align-items: center; gap: .6rem; cursor: pointer; } +.toggle input { + appearance: none; + width: 34px; height: 18px; flex-shrink: 0; + background: #222; border: 1px solid rgba(255, 255, 255, .15); + position: relative; cursor: pointer; + transition: background .2s, border-color .2s; +} +.toggle input::after { + content: ''; + position: absolute; top: 2px; left: 2px; + width: 12px; height: 12px; + background: var(--muted); + transition: transform .2s, background .2s; +} +.toggle input:checked { background: rgba(245, 197, 24, .2); border-color: var(--neon); } +.toggle input:checked::after { transform: translateX(16px); background: var(--neon); } +.toggle > span:first-of-type { color: var(--text); font-size: .95rem; } + +.settings-actions { display: flex; align-items: center; gap: 1rem; } +.btn-save { background: var(--neon); color: #0a0a0a; border-color: var(--neon); font-weight: 600; } +.btn-save:hover:not(:disabled) { background: var(--text); border-color: var(--text); } +.feedback { font-family: var(--mono); font-size: .75rem; letter-spacing: .1em; color: var(--success); } + /* ── PAGINATION ────────────────────────────────────── */ .pager { display: flex; align-items: center; justify-content: center; diff --git a/src/bot/client.js b/src/bot/client.js index 438cef6..e8f2fe7 100644 --- a/src/bot/client.js +++ b/src/bot/client.js @@ -1,6 +1,7 @@ // Discord-Client: Commands laden, registrieren und Interactions verarbeiten import { Client, Collection, Events, GatewayIntentBits, Partials, REST, Routes } from 'discord.js'; import { config } from '../config.js'; +import { devlogChannelId } 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'; @@ -34,7 +35,7 @@ export async function startBot() { // Live-Archivierung: neue Devlogs (Webhook-Posts im Devlog-Kanal) sofort sichern client.on(Events.MessageCreate, async (message) => { - if (message.channelId !== config.devlogChannelId) return; + if (message.channelId !== devlogChannelId()) return; try { if (await archiveDevlogMessage(message)) { console.log(`[devlog] Neues Devlog archiviert (${message.id})`); @@ -46,7 +47,7 @@ export async function startBot() { // In Discord gelöscht = im Archiv gelöscht (inkl. lokaler Bilder) client.on(Events.MessageDelete, async (message) => { - if (message.channelId !== config.devlogChannelId) return; + if (message.channelId !== devlogChannelId()) return; try { if (await removeDevlog(message.id)) { console.log(`[devlog] Archiv-Eintrag entfernt (Discord-Nachricht ${message.id} gelöscht)`); diff --git a/src/bot/commands/devlog-backfill.js b/src/bot/commands/devlog-backfill.js index 53adc67..3d3a1ec 100644 --- a/src/bot/commands/devlog-backfill.js +++ b/src/bot/commands/devlog-backfill.js @@ -1,6 +1,6 @@ // /devlog-backfill — komplette Kanal-Historie scannen und alte Devlogs nacharchivieren (Admin only) import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; -import { config } from '../../config.js'; +import { devlogChannelId } from '../../runtime-settings.js'; import { archiveDevlogMessage } from '../devlog-archive.js'; export const data = new SlashCommandBuilder() @@ -11,7 +11,12 @@ export const data = new SlashCommandBuilder() export async function execute(interaction) { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const channel = await interaction.client.channels.fetch(config.devlogChannelId); + const targetChannel = devlogChannelId(); + if (!targetChannel) { + await interaction.editReply('❌ Kein Devlog-Kanal konfiguriert (Settings-Seite im Webinterface).'); + return; + } + const channel = await interaction.client.channels.fetch(targetChannel); if (!channel?.isTextBased()) { await interaction.editReply('❌ Devlog-Kanal nicht gefunden oder kein Textkanal.'); return; diff --git a/src/bot/commit-feed.js b/src/bot/commit-feed.js index 030effa..d8302c3 100644 --- a/src/bot/commit-feed.js +++ b/src/bot/commit-feed.js @@ -1,6 +1,6 @@ // Baut aus einem Gitea-Push ein hübsches Embed und postet es in den Commit-Kanal import { EmbedBuilder } from 'discord.js'; -import { config } from '../config.js'; +import { commitChannelId } from '../runtime-settings.js'; const GITEA_GREEN = 0x609926; const MAX_COMMITS_SHOWN = 10; @@ -19,9 +19,13 @@ function firstLine(message, maxLen = 72) { * commits: Array<{sha: string, message: string, url: string, author_name: string}> }} push */ export async function postPushEmbed(client, push) { - const channel = await client.channels.fetch(config.commitChannelId); + const channelId = commitChannelId(); + if (!channelId) { + throw new Error('Kein Commit-Kanal konfiguriert (Settings-Seite oder COMMIT_CHANNEL_ID)'); + } + const channel = await client.channels.fetch(channelId); if (!channel?.isTextBased()) { - throw new Error(`Commit-Kanal ${config.commitChannelId} nicht gefunden oder kein Textkanal`); + throw new Error(`Commit-Kanal ${channelId} nicht gefunden oder kein Textkanal`); } const count = push.commits.length; diff --git a/src/config.js b/src/config.js index a3e5699..d37896f 100644 --- a/src/config.js +++ b/src/config.js @@ -19,15 +19,16 @@ export const config = { discordGuildId: process.env.DISCORD_GUILD_ID || null, // Devlog-Archiv (Feature 3) - // Kanal, in den der Bot die Devlogs postet (und den er mitliest) - devlogChannelId: required('DEVLOG_CHANNEL_ID'), + // Kanal-Defaults — können über die Settings-Seite im Webinterface + // überschrieben werden (DB gewinnt, Env ist Fallback) + devlogChannelId: process.env.DEVLOG_CHANNEL_ID || null, // Secret im Pfad des Devlog-Endpoints: /webhooks/devlog/ // (steckt in der URL in tools/.devlog_webhook im EcoGame-Repo) devlogPostSecret: required('DEVLOG_POST_SECRET'), // Commit-Feed (Feature 2) - // Kanal, in den Push-Embeds gepostet werden - commitChannelId: required('COMMIT_CHANNEL_ID'), + // Kanal, in den Push-Embeds gepostet werden (Default, siehe oben) + commitChannelId: process.env.COMMIT_CHANNEL_ID || null, // Shared Secret — muss identisch im Gitea-Webhook eingetragen sein giteaWebhookSecret: required('GITEA_WEBHOOK_SECRET'), diff --git a/src/db.js b/src/db.js index a7461cf..29587ea 100644 --- a/src/db.js +++ b/src/db.js @@ -1,6 +1,6 @@ // SQLite-Anbindung (better-sqlite3, synchron & schnell) — Schema wird beim Start angelegt import Database from 'better-sqlite3'; -import { mkdirSync } from 'node:fs'; +import { mkdirSync, statSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { config } from './config.js'; @@ -41,6 +41,22 @@ if (!devlogCols.some((c) => c.name === 'images')) { db.exec(`ALTER TABLE devlogs ADD COLUMN images TEXT NOT NULL DEFAULT '[]'`); } +// 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)'); +const getSettingStmt = db.prepare('SELECT value FROM settings WHERE key = ?'); +const setSettingStmt = db.prepare(` + INSERT INTO settings (key, value) VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value +`); + +export function getSetting(key) { + return getSettingStmt.get(key)?.value ?? null; +} + +export function setSetting(key, value) { + setSettingStmt.run(key, value); +} + const insertCommit = db.prepare(` INSERT OR IGNORE INTO commits (sha, repo, branch, message, author_name, author_user, url, committed_at) VALUES (@sha, @repo, @branch, @message, @author_name, @author_user, @url, @committed_at) @@ -103,3 +119,12 @@ const countCommitsStmt = db.prepare('SELECT COUNT(*) AS n FROM commits'); export function listCommits(limit, offset) { return { items: selectCommits.all(limit, offset), total: countCommitsStmt.get().n }; } + +/** Statistiken fürs Status-Panel der Settings-Seite */ +export function archiveStats() { + return { + devlogs: countDevlogsStmt.get().n, + commits: countCommitsStmt.get().n, + dbSizeBytes: statSync(dbFile).size, + }; +} diff --git a/src/runtime-settings.js b/src/runtime-settings.js new file mode 100644 index 0000000..844e8b0 --- /dev/null +++ b/src/runtime-settings.js @@ -0,0 +1,38 @@ +// Effektive Laufzeit-Konfiguration: DB-Setting (Webinterface) vor Env-Variable. +// Wird bei jedem Zugriff frisch aufgelöst — Änderungen greifen ohne Neustart. +import { getSetting } from './db.js'; +import { config } from './config.js'; + +export function commitChannelId() { + return getSetting('commit_channel_id') || config.commitChannelId; +} + +export function devlogChannelId() { + return getSetting('devlog_channel_id') || config.devlogChannelId; +} + +/** Commit-Feed global an/aus (Default: an). Aus = weiter archivieren, nur nicht posten. */ +export function commitFeedEnabled() { + return getSetting('commit_feed_enabled') !== '0'; +} + +/** Kommagetrennte Liste in Array übersetzen ('' → leer) */ +function csv(value) { + return (value ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean); +} + +/** Branch-Filter: leer = alle Branches werden gepostet */ +export function branchAllowed(branch) { + const allowed = csv(getSetting('commit_branch_filter')); + return allowed.length === 0 || allowed.includes(branch); +} + +/** Ignorierte Repos (z. B. "D4rkst3r/ecobot") — werden komplett übersprungen */ +export function repoIgnored(repo) { + return csv(getSetting('ignored_repos')).some( + (r) => r.toLowerCase() === repo.toLowerCase() + ); +} diff --git a/src/web/api.js b/src/web/api.js index 52db044..930d56a 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -1,6 +1,8 @@ -// REST-API fürs Webinterface: Devlogs öffentlich, Commits nur für den Admin -import { listDevlogs, listCommits } from '../db.js'; +// REST-API fürs Webinterface: Devlogs öffentlich, Commits + Settings nur für den Admin +import { EmbedBuilder } from 'discord.js'; +import { listDevlogs, listCommits, archiveStats, getSetting, setSetting } from '../db.js'; import { removeDevlog } from '../bot/devlog-archive.js'; +import { commitChannelId, devlogChannelId } from '../runtime-settings.js'; import { getSessionUser, isAdmin } from './auth.js'; const PAGE_SIZE = 20; @@ -11,7 +13,15 @@ function paging(request) { return { limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE, page }; } -export function registerApiRoutes(app) { +/** Admin-Guard: null = okay, sonst wurde bereits eine Fehler-Antwort gesendet */ +function requireAdmin(request, reply) { + const user = getSessionUser(request); + if (!user) return reply.code(401).send({ error: 'login required' }); + if (!isAdmin(user)) return reply.code(403).send({ error: 'admin only' }); + return null; +} + +export function registerApiRoutes(app, client) { // Wer bin ich? (fürs Frontend: Login-Status + Admin-Flag) app.get('/api/me', async (request) => { const user = getSessionUser(request); @@ -32,9 +42,7 @@ export function registerApiRoutes(app) { // Devlog aus dem Archiv löschen — nur Admin (z. B. alte Test-Posts) app.delete('/api/devlogs/:id', async (request, reply) => { - const user = getSessionUser(request); - if (!user) return reply.code(401).send({ error: 'login required' }); - if (!isAdmin(user)) return reply.code(403).send({ error: 'admin only' }); + if (requireAdmin(request, reply)) return; const deleted = await removeDevlog(request.params.id); request.log.info(`Devlog ${request.params.id} per Web-UI gelöscht: ${deleted}`); @@ -43,12 +51,108 @@ export function registerApiRoutes(app) { // Commit-Feed — nur für den Admin (spiegelt den privaten #-gitea-Kanal) app.get('/api/commits', async (request, reply) => { - const user = getSessionUser(request); - if (!user) return reply.code(401).send({ error: 'login required' }); - if (!isAdmin(user)) return reply.code(403).send({ error: 'admin only' }); + if (requireAdmin(request, reply)) return; const { limit, offset, page } = paging(request); const { items, total } = listCommits(limit, offset); return { items, total, page, pageSize: PAGE_SIZE }; }); + + // --- Settings (Admin) --- + + /** Alle Textkanäle, die der Bot sehen kann (für die Dropdowns) */ + function listChannels() { + const channels = []; + for (const guild of client.guilds.cache.values()) { + for (const ch of guild.channels.cache.values()) { + if (ch.isTextBased?.() && ch.viewable !== false) { + channels.push({ id: ch.id, name: ch.name, guild: guild.name }); + } + } + } + return channels.sort((a, b) => a.name.localeCompare(b.name)); + } + + function currentSettings() { + return { + commit_channel_id: commitChannelId(), + devlog_channel_id: devlogChannelId(), + commit_feed_enabled: getSetting('commit_feed_enabled') !== '0', + commit_branch_filter: getSetting('commit_branch_filter') ?? '', + ignored_repos: getSetting('ignored_repos') ?? '', + }; + } + + app.get('/api/settings', async (request, reply) => { + if (requireAdmin(request, reply)) return; + + const stats = archiveStats(); + return { + channels: listChannels(), + settings: currentSettings(), + status: { + botTag: client.user?.tag ?? null, + uptimeSeconds: Math.floor(process.uptime()), + guilds: client.guilds.cache.size, + ...stats, + }, + }; + }); + + app.put('/api/settings', async (request, reply) => { + if (requireAdmin(request, reply)) return; + + const body = request.body ?? {}; + + // Kanäle: müssen existierende Textkanäle sein + for (const key of ['commit_channel_id', 'devlog_channel_id']) { + if (body[key] !== undefined) { + const ch = client.channels.cache.get(String(body[key])); + if (!ch?.isTextBased?.()) { + return reply.code(400).send({ error: `${key}: Kanal nicht gefunden` }); + } + setSetting(key, String(body[key])); + } + } + if (body.commit_feed_enabled !== undefined) { + setSetting('commit_feed_enabled', body.commit_feed_enabled ? '1' : '0'); + } + for (const key of ['commit_branch_filter', 'ignored_repos']) { + if (body[key] !== undefined) { + setSetting(key, String(body[key]).trim()); + } + } + + request.log.info('Settings per Web-UI aktualisiert'); + return { ok: true, settings: currentSettings() }; + }); + + // Test-Embed in den konfigurierten Kanal senden (prüft die Kanal-Wahl ohne Push/Devlog) + app.post('/api/settings/test/:target', async (request, reply) => { + if (requireAdmin(request, reply)) return; + + const target = request.params.target; + const channelId = target === 'commit' ? commitChannelId() : target === 'devlog' ? devlogChannelId() : null; + if (!channelId) { + return reply.code(400).send({ error: 'Kein Kanal konfiguriert' }); + } + try { + const channel = await client.channels.fetch(channelId); + await channel.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xf5c518) + .setTitle('🔧 Test') + .setDescription( + `Test-Nachricht für den **${target === 'commit' ? 'Commit' : 'Devlog'}-Kanal** — von der Settings-Seite ausgelöst.` + ) + .setFooter({ text: 'D4RKST3R // SETUP' }), + ], + }); + return { ok: true }; + } catch (error) { + request.log.error({ err: error }, 'Test-Nachricht fehlgeschlagen'); + return reply.code(502).send({ error: 'Senden fehlgeschlagen — Rechte im Kanal prüfen' }); + } + }); } diff --git a/src/web/devlog-endpoint.js b/src/web/devlog-endpoint.js index 80a8500..8cffd09 100644 --- a/src/web/devlog-endpoint.js +++ b/src/web/devlog-endpoint.js @@ -10,6 +10,7 @@ import { join } from 'node:path'; import { AttachmentBuilder, EmbedBuilder } from 'discord.js'; import { config } from '../config.js'; import { saveDevlog } from '../db.js'; +import { devlogChannelId } from '../runtime-settings.js'; import { imagesDir } from '../bot/devlog-archive.js'; const BRAND_YELLOW = 0xf5c518; @@ -64,7 +65,11 @@ export function registerDevlogEndpoint(app, client) { return reply.code(400).send({ error: 'empty devlog' }); } - const channel = await client.channels.fetch(config.devlogChannelId); + const targetChannel = devlogChannelId(); + if (!targetChannel) { + return reply.code(500).send({ error: 'no devlog channel configured' }); + } + const channel = await client.channels.fetch(targetChannel); if (!channel?.isTextBased()) { return reply.code(500).send({ error: 'devlog channel not found' }); } diff --git a/src/web/server.js b/src/web/server.js index 746488a..7c291cb 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { config } from '../config.js'; import { saveCommits } from '../db.js'; +import { commitFeedEnabled, branchAllowed, repoIgnored } from '../runtime-settings.js'; import { postPushEmbed } from '../bot/commit-feed.js'; import { registerAuthRoutes } from './auth.js'; import { registerApiRoutes } from './api.js'; @@ -59,7 +60,7 @@ export async function startWebServer(client) { await app.register(fastifyMultipart, { limits: { fileSize: 10 * 1024 * 1024, files: 8 } }); registerAuthRoutes(app); - registerApiRoutes(app); + registerApiRoutes(app, client); registerDevlogEndpoint(app, client); // Healthcheck (für Portainer/NPM) @@ -118,6 +119,11 @@ export async function startWebServer(client) { const repo = payload.repository?.full_name ?? 'unbekannt'; const branch = (payload.ref ?? '').replace('refs/heads/', ''); + // Ignorierte Repos komplett überspringen (Settings-Seite) + if (repoIgnored(repo)) { + return { ok: true, ignored: `repo ${repo}` }; + } + const rows = commits.map((c) => ({ sha: c.id, repo, @@ -131,6 +137,12 @@ export async function startWebServer(client) { const inserted = saveCommits(rows); request.log.info(`Push auf ${repo}@${branch}: ${rows.length} Commit(s), ${inserted} neu gespeichert`); + // Posten nur wenn Feed aktiv und Branch erlaubt (archiviert wird immer) + if (!commitFeedEnabled() || !branchAllowed(branch)) { + request.log.info(`Embed übersprungen (Feed aus oder Branch ${branch} gefiltert)`); + return { ok: true, commits: rows.length, new: inserted, posted: false }; + } + // Embed posten — Fehler hier sollen den Webhook nicht scheitern lassen (Gitea würde sonst retrien) try { await postPushEmbed(client, {