From 71d2bb07f173cae92d160f281ddd8c1566010486 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 23 Jul 2026 01:16:44 +0200 Subject: [PATCH] =?UTF-8?q?Devlog-L=C3=B6schung:=20Discord-Delete-Sync=20+?= =?UTF-8?q?=20Admin-Button=20im=20Webinterface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MessageDelete-Listener (mit Partials): in Discord gelöschte Devlogs verschwinden automatisch aus dem Archiv, inkl. lokaler Bilder - DELETE /api/devlogs/:id (nur Admin) + ✕-Button auf den Karten (nur für Admin sichtbar) - Anlass: Test-Post mit Commit-Liste von vor der Prosa-Sperre stand noch im Archiv Co-Authored-By: Claude Opus 4.8 --- frontend/src/App.jsx | 2 +- frontend/src/api.js | 7 +++++-- frontend/src/pages/Devlogs.jsx | 34 +++++++++++++++++++++++++++++----- frontend/src/style.css | 9 +++++++++ src/bot/client.js | 18 ++++++++++++++++-- src/bot/devlog-archive.js | 16 ++++++++++++++-- src/db.js | 11 +++++++++++ src/web/api.js | 12 ++++++++++++ 8 files changed, 97 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 15f079b..bea3fdd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -89,7 +89,7 @@ export default function App() {
} /> - } /> + } /> } /> } /> diff --git a/frontend/src/api.js b/frontend/src/api.js index 6a38b9e..6e92076 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,6 +1,6 @@ // Kleiner Fetch-Wrapper — Cookies (Session) laufen automatisch mit (same-origin) -export async function apiGet(path) { - const res = await fetch(path, { headers: { Accept: 'application/json' } }); +async function api(path, options = {}) { + const res = await fetch(path, { headers: { Accept: 'application/json' }, ...options }); if (!res.ok) { const error = new Error(`API ${res.status}`); error.status = res.status; @@ -8,3 +8,6 @@ export async function apiGet(path) { } return res.json(); } + +export const apiGet = (path) => api(path); +export const apiDelete = (path) => api(path, { method: 'DELETE' }); diff --git a/frontend/src/pages/Devlogs.jsx b/frontend/src/pages/Devlogs.jsx index c2f4adf..4e4d5f2 100644 --- a/frontend/src/pages/Devlogs.jsx +++ b/frontend/src/pages/Devlogs.jsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react'; -import { apiGet } from '../api.js'; +import { useCallback, useEffect, useState } from 'react'; +import { apiGet, apiDelete } from '../api.js'; import { Markdown } from '../markdown.jsx'; const dateFmt = new Intl.DateTimeFormat('de-DE', { @@ -40,17 +40,32 @@ function splitContent(raw) { return { project, footer, body: blocks.join('\n\n') }; } -export default function Devlogs() { +export default function Devlogs({ me }) { const [data, setData] = useState(null); const [page, setPage] = useState(1); const [error, setError] = useState(false); - useEffect(() => { + const load = useCallback(() => { setError(false); apiGet(`/api/devlogs?page=${page}`).then(setData).catch(() => setError(true)); - window.scrollTo(0, 0); }, [page]); + useEffect(() => { + load(); + window.scrollTo(0, 0); + }, [load]); + + // Admin: Devlog aus dem Archiv entfernen (löscht auch die lokalen Bilder) + async function remove(id) { + if (!window.confirm('Dieses Devlog aus dem Archiv löschen?')) return; + try { + await apiDelete(`/api/devlogs/${id}`); + load(); + } catch { + window.alert('Löschen fehlgeschlagen.'); + } + } + const pages = data ? Math.ceil(data.total / data.pageSize) : 0; // Monats-Trenner in der Timeline @@ -92,6 +107,15 @@ export default function Devlogs() { {project && {project}} {relative(d.posted_at)} + {me?.admin && ( + + )}
diff --git a/frontend/src/style.css b/frontend/src/style.css index 8f5a3ba..b0b488e 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -246,6 +246,15 @@ body::after { padding: .1rem .5rem; white-space: nowrap; } +.card-delete { + font-family: var(--mono); + font-size: .7rem; + color: var(--muted2); + background: none; border: 1px solid transparent; + padding: .05rem .4rem; + transition: color .2s, border-color .2s; +} +.card-delete:hover { color: var(--neon2); border-color: rgba(255, 77, 0, .4); } .card-body { color: var(--text); font-weight: 300; font-size: 1.08rem; } .card-body p { margin: .55rem 0; } diff --git a/src/bot/client.js b/src/bot/client.js index 6cbed7a..438cef6 100644 --- a/src/bot/client.js +++ b/src/bot/client.js @@ -1,7 +1,7 @@ // Discord-Client: Commands laden, registrieren und Interactions verarbeiten -import { Client, Collection, Events, GatewayIntentBits, REST, Routes } from 'discord.js'; +import { Client, Collection, Events, GatewayIntentBits, Partials, REST, Routes } from 'discord.js'; import { config } from '../config.js'; -import { archiveDevlogMessage } from './devlog-archive.js'; +import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js'; import * as ping from './commands/ping.js'; import * as devlogBackfill from './commands/devlog-backfill.js'; @@ -17,6 +17,8 @@ export async function startBot() { GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, ], + // Partials, damit MessageDelete auch für ungecachte (ältere) Nachrichten feuert + partials: [Partials.Message], }); // Commands in Collection ablegen für schnellen Zugriff im Interaction-Handler @@ -42,6 +44,18 @@ 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; + try { + if (await removeDevlog(message.id)) { + console.log(`[devlog] Archiv-Eintrag entfernt (Discord-Nachricht ${message.id} gelöscht)`); + } + } catch (error) { + console.error('[devlog] Archiv-Löschung fehlgeschlagen:', error); + } + }); + client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; diff --git a/src/bot/devlog-archive.js b/src/bot/devlog-archive.js index 5837d03..89a925e 100644 --- a/src/bot/devlog-archive.js +++ b/src/bot/devlog-archive.js @@ -3,9 +3,9 @@ // Webserver unter /devlog-assets/ ausgeliefert. // Wird vom Live-Listener (MessageCreate) und vom /devlog-backfill-Command genutzt. import { mkdirSync } from 'node:fs'; -import { writeFile } from 'node:fs/promises'; +import { writeFile, rm } from 'node:fs/promises'; import { join, dirname, resolve } from 'node:path'; -import { saveDevlog, hasDevlog } from '../db.js'; +import { saveDevlog, hasDevlog, deleteDevlog } from '../db.js'; import { config } from '../config.js'; const MAX_IMAGES = 4; @@ -49,6 +49,18 @@ async function downloadImages(message) { return files; } +/** + * Devlog aus dem Archiv entfernen (inkl. lokal gespeicherter Bilder). + * @returns {Promise} true, wenn ein Eintrag gelöscht wurde + */ +export async function removeDevlog(messageId) { + const { deleted, images } = deleteDevlog(messageId); + for (const file of images) { + await rm(join(imagesDir, file), { force: true }); + } + return deleted; +} + /** * Nachricht archivieren, falls sie ein Devlog ist (Webhook-/Bot-Post mit Text). * @param {import('discord.js').Message} message diff --git a/src/db.js b/src/db.js index 3cc9b50..a7461cf 100644 --- a/src/db.js +++ b/src/db.js @@ -71,6 +71,17 @@ export function hasDevlog(messageId) { return Boolean(hasDevlogStmt.get(messageId)); } +const getDevlogImagesStmt = db.prepare('SELECT images FROM devlogs WHERE message_id = ?'); +const deleteDevlogStmt = db.prepare('DELETE FROM devlogs WHERE message_id = ?'); + +/** Devlog löschen; gibt { deleted, images } zurück (Dateinamen fürs Aufräumen) */ +export function deleteDevlog(messageId) { + const row = getDevlogImagesStmt.get(messageId); + if (!row) return { deleted: false, images: [] }; + deleteDevlogStmt.run(messageId); + return { deleted: true, images: JSON.parse(row.images || '[]') }; +} + // --- Lese-Queries für die Web-API (Feature 4) --- const selectDevlogs = db.prepare(` diff --git a/src/web/api.js b/src/web/api.js index a4c74c1..52db044 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -1,5 +1,6 @@ // REST-API fürs Webinterface: Devlogs öffentlich, Commits nur für den Admin import { listDevlogs, listCommits } from '../db.js'; +import { removeDevlog } from '../bot/devlog-archive.js'; import { getSessionUser, isAdmin } from './auth.js'; const PAGE_SIZE = 20; @@ -29,6 +30,17 @@ export function registerApiRoutes(app) { return { items: mapped, total, page, pageSize: PAGE_SIZE }; }); + // 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' }); + + const deleted = await removeDevlog(request.params.id); + request.log.info(`Devlog ${request.params.id} per Web-UI gelöscht: ${deleted}`); + return { deleted }; + }); + // Commit-Feed — nur für den Admin (spiegelt den privaten #-gitea-Kanal) app.get('/api/commits', async (request, reply) => { const user = getSessionUser(request);