docs: README und .env.example auf den Stand -- und Embeds im Stil des Bots
DIE README BESCHRIEB NOCH DEN VORGAENGER: "Fivemanage Lite mit MySQL und MinIO", Ausprobieren ueber docker-compose.local.yml, vier Geheimnisse, davon drei fuer Dienste, die es nicht mehr gibt. Jetzt beschreibt sie, was da ist: ein Container, SQLite, Dateien auf einer Platte -- Starten, was der Dienst kann, die eine Regel und wie sie im Code durchgesetzt wird, Betrieb (Sicherung, Wiki, Zurueckspielen), was bewusst fehlt, und die zwei Dinge, die Zeit gekostet haben. Und die .env.example nannte DB_PASSWORD, MINIO_ROOT_PASSWORD und API_TOKEN_HMAC_SECRET -- drei Werte fuer Dienste, die seit Wochen weg sind, und KEINEN der acht, die das Compose wirklich liest. Nachgezaehlt: jetzt sind alle acht genannt, zwei als Pflicht, sechs als Optional mit dem Grund dahinter. EMBEDS IM STIL DES BOTS. Auf den Hinweis hin in d4rkbot/src/embeds.js nachgesehen: dort gibt es eine zentrale Embed-Fabrik mit Markenfarbe (#f5c518), Fusszeile "D4RKST3R // <TAG>" und Zeitstempel. Zwei Dienste desselben Hauses sollen in einem Kanal nicht wie zwei Fremde aussehen -- also uebernommen statt neu erfunden, samt Feldern statt Fliesstext und Discords Zeitmarken (<t:...:R>), die "vor 3 Stunden" in der Zeitzone des Lesers anzeigen. EINE ABWEICHUNG, mit Absicht: der Bot faerbt alles in der Markenfarbe, hier faerbt die SCHWERE. Eine Warnung, die aussieht wie jede andere Nachricht, ist eine Warnung, die man ueberliest -- und diese Meldungen gibt es nur, weil jemand sie sehen soll. Die Testnachricht schickt jetzt echte Zahlen mit. So sieht man nicht nur, DASS etwas ankommt, sondern auch, ob es lesbar ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+106
-21
@@ -9,9 +9,15 @@
|
||||
// Berechtigungen, kein zweites Geheimnis, das ablaeuft. Wer ihn zurueckziehen
|
||||
// will, loescht ihn in Discord.
|
||||
//
|
||||
// Der Dienst schickt HOECHSTENS EINMAL je Anlass. Eine Meldung, die stuendlich
|
||||
// wiederkommt, wird nach dem dritten Mal weggeklickt -- und dann auch die, die
|
||||
// zaehlt.
|
||||
// DER AUSSEHEN FOLGT DEM d4rkbot. Dessen src/embeds.js hat eine zentrale
|
||||
// Embed-Fabrik: Markenfarbe, Fusszeile "D4RKST3R // <TAG>", Zeitstempel. Zwei
|
||||
// Dienste desselben Hauses sollen in einem Kanal nicht wie zwei Fremde
|
||||
// aussehen, also uebernehmen wir das Muster statt ein eigenes zu erfinden.
|
||||
//
|
||||
// EINE ABWEICHUNG, mit Absicht: der Bot faerbt alles in der Markenfarbe, hier
|
||||
// faerbt die SCHWERE. Eine Warnung, die aussieht wie jede andere Nachricht,
|
||||
// ist eine Warnung, die man ueberliest -- und diese Meldungen gibt es nur,
|
||||
// weil jemand sie sehen soll.
|
||||
|
||||
import { getSetting, setSetting } from './settings.js'
|
||||
import { now } from './db.js'
|
||||
@@ -22,8 +28,22 @@ export const MELDUNG_KEYS = {
|
||||
anlaesse: 'discord.anlaesse',
|
||||
/** Womit zuletzt gemeldet wurde -- gegen Wiederholungen. */
|
||||
zuletzt: 'discord.zuletzt',
|
||||
/** Der Name in der Fusszeile. Wie beim Bot einstellbar, mit demselben
|
||||
* Standard. */
|
||||
marke: 'marke.name',
|
||||
} as const
|
||||
|
||||
/** Dieselben Werte wie im d4rkbot (src/runtime-settings.js). */
|
||||
const MARKE_STANDARD = 'D4RKST3R'
|
||||
const FARBEN = {
|
||||
marke: 0xf5c518,
|
||||
gut: 0x3fb950,
|
||||
warnung: 0xd29922,
|
||||
schlecht: 0xf85149,
|
||||
} as const
|
||||
|
||||
export type Schwere = keyof typeof FARBEN
|
||||
|
||||
export type MeldungConfig = {
|
||||
hatWebhook: boolean
|
||||
anlaesse: string[]
|
||||
@@ -35,17 +55,28 @@ export function meldungConfig(): MeldungConfig {
|
||||
hatWebhook: Boolean(getSetting(MELDUNG_KEYS.webhook)),
|
||||
// Ohne Eintrag gilt: alles, was es gibt. Wer einen Webhook eintraegt,
|
||||
// will Meldungen -- sonst haette er es gelassen.
|
||||
anlaesse: roh ? roh.split(',').map((a) => a.trim()).filter(Boolean) : ['sicherung', 'verwaiste'],
|
||||
anlaesse: roh
|
||||
? roh.split(',').map((a) => a.trim()).filter(Boolean)
|
||||
: ['sicherung', 'verwaiste'],
|
||||
}
|
||||
}
|
||||
|
||||
type Feld = { name: string; value: string; inline?: boolean }
|
||||
|
||||
/** Eine Nachricht schicken. Wirft nie.
|
||||
*
|
||||
* `einmalig` ist der Schluessel gegen Wiederholungen: dieselbe Zeichenkette
|
||||
* wird nur einmal gemeldet, bis sich der Zustand aendert. */
|
||||
export async function melde(
|
||||
text: string,
|
||||
opt: { anlass?: string; einmalig?: string; farbe?: number } = {},
|
||||
titel: string,
|
||||
opt: {
|
||||
text?: string
|
||||
felder?: Feld[]
|
||||
schwere?: Schwere
|
||||
anlass?: string
|
||||
einmalig?: string
|
||||
tag?: string
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const webhook = getSetting(MELDUNG_KEYS.webhook)
|
||||
@@ -59,6 +90,8 @@ export async function melde(
|
||||
setSetting(MELDUNG_KEYS.zuletzt, opt.einmalig)
|
||||
}
|
||||
|
||||
const marke = getSetting(MELDUNG_KEYS.marke) || MARKE_STANDARD
|
||||
|
||||
const antwort = await fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
@@ -66,8 +99,14 @@ export async function melde(
|
||||
username: 'd4rk_media',
|
||||
embeds: [
|
||||
{
|
||||
description: text,
|
||||
color: opt.farbe ?? 0x4ea3ff,
|
||||
title: titel,
|
||||
description: opt.text,
|
||||
color: FARBEN[opt.schwere ?? 'marke'],
|
||||
// Felder statt Fliesstext, wo es Zahlen sind: "228
|
||||
// Dateien / 11,5 MB / vor 3 Stunden" liest sich in
|
||||
// einer Zeile schlechter als nebeneinander.
|
||||
fields: opt.felder,
|
||||
footer: { text: `${marke} // ${opt.tag ?? 'MEDIA'}` },
|
||||
timestamp: new Date(now()).toISOString(),
|
||||
},
|
||||
],
|
||||
@@ -87,6 +126,9 @@ export async function melde(
|
||||
}
|
||||
}
|
||||
|
||||
const groesse = (n: number) =>
|
||||
n < 1024 * 1024 ? `${Math.round(n / 1024)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||
|
||||
/** Wacht ueber die Sicherung.
|
||||
*
|
||||
* Der eigentliche Punkt der ganzen Datei. Geprueft wird stuendlich, gemeldet
|
||||
@@ -99,30 +141,73 @@ export async function melde(
|
||||
*/
|
||||
export async function pruefeSicherung(): Promise<void> {
|
||||
const roh = getSetting('sicherung.letzte')
|
||||
|
||||
if (!roh) {
|
||||
await melde(
|
||||
'⚠️ Es wurde noch **nie** eine Sicherung gemeldet. Läuft die geplante Aufgabe?',
|
||||
{ anlass: 'sicherung', einmalig: 'nie', farbe: 0xd29922 },
|
||||
)
|
||||
await melde('Noch keine Sicherung gemeldet', {
|
||||
text: 'Es wurde noch **nie** eine Sicherung eingetragen. Läuft die geplante Aufgabe?',
|
||||
schwere: 'warnung',
|
||||
anlass: 'sicherung',
|
||||
einmalig: 'nie',
|
||||
tag: 'SICHERUNG',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const letzte = JSON.parse(roh) as { at: number; ok: boolean; fehler?: string }
|
||||
const letzte = JSON.parse(roh) as {
|
||||
at: number
|
||||
ok: boolean
|
||||
dateien?: number
|
||||
groesse?: number
|
||||
hoch?: boolean
|
||||
fehler?: string
|
||||
}
|
||||
const stunden = (now() - letzte.at) / 3_600_000
|
||||
const wann = `<t:${Math.floor(letzte.at / 1000)}:R>`
|
||||
|
||||
if (!letzte.ok) {
|
||||
await melde(
|
||||
`🔴 Die letzte Sicherung ist **fehlgeschlagen**.\n${letzte.fehler ?? 'kein Grund gemeldet'}`,
|
||||
{ anlass: 'sicherung', einmalig: `fehler:${letzte.at}`, farbe: 0xf85149 },
|
||||
)
|
||||
await melde('Die Sicherung ist fehlgeschlagen', {
|
||||
text: letzte.fehler ?? 'Es wurde kein Grund gemeldet.',
|
||||
felder: [
|
||||
{ name: 'Wann', value: wann, inline: true },
|
||||
{
|
||||
name: 'Zuletzt gut',
|
||||
value: letzte.dateien ? `${letzte.dateien} Dateien` : '—',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
schwere: 'schlecht',
|
||||
anlass: 'sicherung',
|
||||
einmalig: `fehler:${letzte.at}`,
|
||||
tag: 'SICHERUNG',
|
||||
})
|
||||
} else if (stunden > 26) {
|
||||
await melde(
|
||||
`⚠️ Die letzte Sicherung ist **${Math.floor(stunden)} Stunden** her. Läuft die geplante Aufgabe noch?`,
|
||||
{ anlass: 'sicherung', einmalig: `alt:${letzte.at}`, farbe: 0xd29922 },
|
||||
)
|
||||
await melde('Die Sicherung ist überfällig', {
|
||||
text: 'Läuft die geplante Aufgabe noch? Erwartet wird sie täglich um 04:30.',
|
||||
felder: [
|
||||
{ name: 'Letzter Lauf', value: wann, inline: true },
|
||||
{ name: 'Das ist', value: `${Math.floor(stunden)} Stunden her`, inline: true },
|
||||
],
|
||||
schwere: 'warnung',
|
||||
anlass: 'sicherung',
|
||||
einmalig: `alt:${letzte.at}`,
|
||||
tag: 'SICHERUNG',
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
/* kaputter Eintrag -- beim naechsten Lauf wieder */
|
||||
}
|
||||
}
|
||||
|
||||
/** Die Testnachricht. Zeigt bewusst, WIE eine echte aussieht — sonst prueft
|
||||
* man nur, ob irgendetwas ankommt, und nicht, ob es lesbar ist. */
|
||||
export async function testMeldung(wer: string, bestand: { files: number; bytes: number }) {
|
||||
return melde('Testnachricht', {
|
||||
text: `Ausgelöst von **${wer}**. So sieht eine Meldung von hier aus.`,
|
||||
felder: [
|
||||
{ name: 'Bestand', value: `${bestand.files} Dateien`, inline: true },
|
||||
{ name: 'Belegt', value: groesse(bestand.bytes), inline: true },
|
||||
],
|
||||
schwere: 'gut',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
type Vorlage,
|
||||
} from '../settings.js'
|
||||
import { discordRoutes, redirectUri } from './discord.js'
|
||||
import { MELDUNG_KEYS, melde, meldungConfig } from '../meldung.js'
|
||||
import { MELDUNG_KEYS, meldungConfig, testMeldung } from '../meldung.js'
|
||||
import {
|
||||
absolutePath,
|
||||
checkPath,
|
||||
@@ -1526,10 +1526,12 @@ dashRoutes.delete('/tokens/:id', (c) => {
|
||||
* Ein Webhook, den man eintraegt und erst in drei Wochen im Fehlerfall
|
||||
* ausprobiert, ist ein Webhook, der dann nicht funktioniert. */
|
||||
dashRoutes.post('/maintenance/melden', async (c) => {
|
||||
const ok = await melde(
|
||||
`✅ Testnachricht von d4rk_media — ausgelöst von **${c.get('user').username}**.`,
|
||||
{ farbe: 0x3fb950 },
|
||||
)
|
||||
// Mit echten Zahlen statt "Hallo Welt": so sieht man nicht nur, DASS etwas
|
||||
// ankommt, sondern auch, ob es lesbar ist.
|
||||
const b = db
|
||||
.prepare('SELECT COUNT(*) AS files, COALESCE(SUM(size),0) AS bytes FROM media')
|
||||
.get() as { files: number; bytes: number }
|
||||
const ok = await testMeldung(c.get('user').username, b)
|
||||
if (!ok) {
|
||||
return c.json(
|
||||
{ error: 'Nichts geschickt. Ist ein Webhook eingetragen, und stimmt die Adresse?' },
|
||||
|
||||
Reference in New Issue
Block a user