Files
rippy/rippy-windows/src/kern/dienste/benachrichtigung.ts
T
HitonabiandClaude Fable 5.1 3dc6798c79 feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie
veröffentlicht wurde):

Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in
kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge,
Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts,
Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts.

Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/,
uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte),
einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/
(ErstEinrichtung, PosterHintergrund).

Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs);
LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown
statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update;
Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die
Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown;
Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen
Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange
pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag;
Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem
Poster-Grid.

Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung,
fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit
29 Schritten inklusive Erst-Einrichtung (zweiter Start mit
RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:51:15 +02:00

95 lines
4.0 KiB
TypeScript

// Benachrichtigung aufs Handy (5.5.0, aus ARM: Apprise/Pushover/…) — zwei
// Wege, beide ohne Bibliothek, beide dokumentiert (Regel D):
// Discord-Webhook: POST <webhook-url> mit JSON { content } —
// discord.com/developers/docs/resources/webhook („Execute Webhook":
// content ist eine Zeichenkette bis 2000 Zeichen).
// Telegram-Bot: POST https://api.telegram.org/bot<TOKEN>/sendMessage mit
// JSON { chat_id, text } — core.telegram.org/bots/api#sendmessage.
// Ein Fehlschlag wird GESAGT (Ereignis-Zeile), kippt aber nie den Vorgang,
// der ihn ausgelöst hat.
import type { KernKontext } from '../kontext'
import type { EinstellungsDienst } from './einstellungen'
export interface Kanaele {
discordWebhook: string
telegramToken: string
telegramChatId: string
}
/** Was an Discord geht (pur, testbar). */
export function discordAnfrage(webhook: string, titel: string, text: string): { url: string; body: string } {
const inhalt = `**${titel}**\n${text}`.slice(0, 1990)
return { url: webhook, body: JSON.stringify({ content: inhalt, username: 'Rippy' }) }
}
/** Was an Telegram geht (pur, testbar). */
export function telegramAnfrage(token: string, chatId: string, titel: string, text: string): { url: string; body: string } {
return {
url: `https://api.telegram.org/bot${token}/sendMessage`,
body: JSON.stringify({ chat_id: chatId, text: `${titel}\n${text}`.slice(0, 4000) }),
}
}
export function kanaeleAus(lesen: (schluessel: string) => string): Kanaele {
return {
discordWebhook: lesen('discordWebhook').trim(),
telegramToken: lesen('telegramToken').trim(),
telegramChatId: lesen('telegramChatId').trim(),
}
}
export function kanaeleEingerichtet(k: Kanaele): boolean {
return k.discordWebhook.length > 0 || (k.telegramToken.length > 0 && k.telegramChatId.length > 0)
}
export class BenachrichtigungsDienst {
constructor(
private readonly kontext: KernKontext,
private readonly einstellungen: EinstellungsDienst,
private readonly holen: typeof fetch = fetch,
) {}
eingeschaltet(): boolean {
return this.einstellungen.lesen('benachrichtigungen', 'an') !== 'aus'
}
/** Schickt über alle eingerichteten Kanäle. Liefert die Fehler (leer = alles gut). */
async senden(titel: string, text: string, erzwingen = false): Promise<string[]> {
if (!erzwingen && !this.eingeschaltet()) return []
const k = kanaeleAus((s) => this.einstellungen.lesen(s))
const fehler: string[] = []
const anfragen: Array<{ name: string; url: string; body: string }> = []
if (k.discordWebhook.length > 0) anfragen.push({ name: 'Discord', ...discordAnfrage(k.discordWebhook, titel, text) })
if (k.telegramToken.length > 0 && k.telegramChatId.length > 0) {
anfragen.push({ name: 'Telegram', ...telegramAnfrage(k.telegramToken, k.telegramChatId, titel, text) })
}
for (const a of anfragen) {
try {
const antwort = await this.holen(a.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: a.body,
signal: AbortSignal.timeout(15_000),
})
if (antwort.status >= 300) fehler.push(`${a.name}: HTTP ${antwort.status}`)
} catch (e) {
fehler.push(`${a.name}: ${String((e as Error).message ?? e).slice(0, 120)}`)
}
}
for (const f of fehler) this.kontext.fehler(`Benachrichtigung ${f}`)
if (anfragen.length > 0 && fehler.length === 0) this.kontext.protokoll.schreiben(`Benachrichtigt (${anfragen.map((a) => a.name).join(', ')}): ${titel}`)
return fehler
}
/** Der Knopf „Testnachricht" — antwortet IMMER etwas. */
async testen(): Promise<void> {
const k = kanaeleAus((s) => this.einstellungen.lesen(s))
if (!kanaeleEingerichtet(k)) {
this.kontext.fehler('Benachrichtigung: kein Kanal eingerichtet — Discord-Webhook oder Telegram-Bot eintragen.')
return
}
const fehler = await this.senden('Rippy meldet sich', 'Testnachricht — die Verbindung steht.', true)
if (fehler.length === 0) this.kontext.hinweis('Testnachricht verschickt.')
}
}