feat: Token-Grenzen, Speicher nach Art, Aufbewahrungsbericht, Discord-Meldungen
KEINE TOKEN-ARTEN, SONDERN TOKEN-GRENZEN. Fivemanage hat Token-Typen; wir haben schon ein Rechtesystem, es steht nur auf einer anderen Achse (Praefix und Loeschrecht). Was wirklich fehlte, sind die drei Schrauben, die die Frage beantworten "was passiert, wenn DIESER Token abhandenkommt": Groessengrenze ein Token fuer Fahrzeugbilder braucht keine 64 MB erlaubte Arten nur Bilder, kein Video Ablaufdatum fuer eine Anbindung, die man mal ausprobiert Alle drei am laufenden Dienst durchgemessen: 600 KB gegen 512 KB -> 413 mit beiden Zahlen; ein mp4 gegen einen Bild-Token -> 403 mit Art UND MIME im Text; ausserhalb des Praefix -> 403; abgelaufen -> 401 mit DEM DATUM. "Abgelaufen" und "unbekannt" auseinanderzuhalten spart eine halbe Stunde Suche nach einem Tippfehler, den es nicht gibt. Mehr als die Dienstgrenze zu erlauben wird abgelehnt statt still gekappt -- es waere eine Behauptung, denn durchgelassen wird ohnehin die schaerfere. SPEICHER NACH ART: ein Balken fuer die Verhaeltnisse, eine Tabelle fuer die Zahlen, dazu die groessten Dateien. AUFBEWAHRUNG VORBEREITET, NICHT GEBAUT. Automatisches Loeschen zu bauen, bevor man weiss, was da liegt, ist der Weg, wie man Daten verliert. Stattdessen der Bericht davor: wie viel ist aelter als 7/30/90/365 Tage, was liegt doppelt (gleicher SHA-256 unter zwei Pfaden) und wie viel Platz das kostet. "Alles aelter als 90 Tage loeschen" ist eine Behauptung, solange niemand weiss, wie viel das waere. Diese Seite loescht nichts. DISCORD-MELDUNGEN -- und der Grund ist nicht Discord, sondern eine Luecke, die wir heute selbst gebaut haben: die Sicherung laeuft nachts um halb fuenf, und wenn sie aufhoert zu laufen, merkt es niemand. Der Dienst sieht jetzt STUENDLICH nach und meldet, wenn der letzte Lauf gescheitert oder aelter als 26 Stunden ist (24 waeren zu knapp, 48 zu spaet). Hoechstens EINMAL je Zustand -- eine Meldung, die stuendlich wiederkommt, wird weggeklickt, und dann auch die, die zaehlt. Ein Webhook und keine Bot-Anbindung: eine Adresse, kein Token, keine Berechtigungen. Die Adresse wird geprueft (nur echte discord.com-Webhooks), und der Testknopf steht gleich daneben -- ein Webhook, den man eintraegt und erst in drei Wochen im Fehlerfall ausprobiert, ist einer, der dann nicht geht. Aus deren Doku mitgenommen und NICHT gebaut: presigned URLs (kurzlebige signierte Adressen fuer Client-Uploads). Waere der richtige Weg fuer Spieler-Screenshots -- solange keine Spieler hochladen, ist es Vorrat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// Meldungen nach Discord.
|
||||
//
|
||||
// WOZU. Die Sicherung laeuft nachts um halb fuenf, und wenn sie aufhoert zu
|
||||
// laufen, merkt es niemand -- das Panel zeigt es nur dem, der hinschaut. Genau
|
||||
// dafuer ist das hier: nicht fuer Jubelmeldungen, sondern fuer die zwei, drei
|
||||
// Faelle, in denen jemand etwas tun muss.
|
||||
//
|
||||
// Ein WEBHOOK und keine Bot-Anbindung: eine Adresse, kein Token, keine
|
||||
// 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.
|
||||
|
||||
import { getSetting, setSetting } from './settings.js'
|
||||
import { now } from './db.js'
|
||||
|
||||
export const MELDUNG_KEYS = {
|
||||
webhook: 'discord.webhook',
|
||||
/** Was gemeldet wird: Komma-Liste aus 'sicherung', 'verwaiste'. */
|
||||
anlaesse: 'discord.anlaesse',
|
||||
/** Womit zuletzt gemeldet wurde -- gegen Wiederholungen. */
|
||||
zuletzt: 'discord.zuletzt',
|
||||
} as const
|
||||
|
||||
export type MeldungConfig = {
|
||||
hatWebhook: boolean
|
||||
anlaesse: string[]
|
||||
}
|
||||
|
||||
export function meldungConfig(): MeldungConfig {
|
||||
const roh = getSetting(MELDUNG_KEYS.anlaesse)
|
||||
return {
|
||||
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'],
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 } = {},
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const webhook = getSetting(MELDUNG_KEYS.webhook)
|
||||
if (!webhook) return false
|
||||
|
||||
if (opt.anlass && !meldungConfig().anlaesse.includes(opt.anlass)) return false
|
||||
|
||||
if (opt.einmalig) {
|
||||
const zuletzt = getSetting(MELDUNG_KEYS.zuletzt)
|
||||
if (zuletzt === opt.einmalig) return false
|
||||
setSetting(MELDUNG_KEYS.zuletzt, opt.einmalig)
|
||||
}
|
||||
|
||||
const antwort = await fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: 'd4rk_media',
|
||||
embeds: [
|
||||
{
|
||||
description: text,
|
||||
color: opt.farbe ?? 0x4ea3ff,
|
||||
timestamp: new Date(now()).toISOString(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
// Discord antwortet auf einen Webhook mit 204.
|
||||
if (!antwort.ok) {
|
||||
console.error('[discord] Webhook antwortete mit', antwort.status)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
// Eine Meldung, die den Dienst mitreisst, waere die Buchhaltung, die
|
||||
// das Geschaeft verhindert.
|
||||
console.error('[discord] Meldung fehlgeschlagen:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Wacht ueber die Sicherung.
|
||||
*
|
||||
* Der eigentliche Punkt der ganzen Datei. Geprueft wird stuendlich, gemeldet
|
||||
* hoechstens einmal je Zustand:
|
||||
*
|
||||
* - der letzte Lauf ist gescheitert
|
||||
* - der letzte Lauf ist laenger als 26 Stunden her (taeglich um 04:30 plus
|
||||
* Luft; 24 waeren zu knapp, 48 zu spaet)
|
||||
* - es gab noch nie einen
|
||||
*/
|
||||
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 },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const letzte = JSON.parse(roh) as { at: number; ok: boolean; fehler?: string }
|
||||
const stunden = (now() - letzte.at) / 3_600_000
|
||||
|
||||
if (!letzte.ok) {
|
||||
await melde(
|
||||
`🔴 Die letzte Sicherung ist **fehlgeschlagen**.\n${letzte.fehler ?? 'kein Grund gemeldet'}`,
|
||||
{ anlass: 'sicherung', einmalig: `fehler:${letzte.at}`, farbe: 0xf85149 },
|
||||
)
|
||||
} 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 },
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
/* kaputter Eintrag -- beim naechsten Lauf wieder */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user