Files
tessera-ctl/apps/api/src/nextcloud-status/nextcloud-alert-rules.ts
T
schalli faed0d760f feat(nextcloud-status): Benachrichtigung abonnieren und Mail bei Störung
- Glocke je Kachel (persönlich, Benutzen-Ebene), Tabelle NextcloudAlertSubscription mit RLS
- Zwei-Fehlschläge-Regel und gemeldeter Zustand auf der Zeile, Anspruch vor dem Mailversand
- Mail (nur Deutsch) über MailService, bis zu drei Versuche, Zugriff beim Senden erneut geprüft
- Fehlercodes in der Mail als lesbarer Hinweis

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 15:23:34 +02:00

93 lines
3.3 KiB
TypeScript

/**
* nextcloud-alert-rules — reine Regeln der Benachrichtigung
* (quick-261002-kxc, L-02, L-03, D-K1, D-K2). Kein Nest, kein Prisma, kein
* Zugriff auf die Uhr: das Datum wird hereingereicht.
*
* ZWEI-FEHLSCHLAEGE-REGEL: ein einzelner fehlgeschlagener Abruf kann eine
* kurze Stoerung sein (Neustart hinter dem Proxy, Netzwackler). Deshalb gilt
* eine Cloud erst nach zwei aufeinanderfolgenden Fehlschlaegen als "nicht
* erreichbar". Der ERSTE Fehlschlag schreibt nur den Zaehler und den
* Zeitpunkt, alle Statusfelder und `lastCheckedAt` bleiben unangetastet — die
* Bewertung wird aus den gespeicherten Feldern berechnet und zeigt dadurch
* weiter den letzten guten Stand. Wartungsmodus, ausstehende
* Datenbankaktualisierung und abgelaufener Support kommen aus einer
* erfolgreichen Antwort bzw. dem Datum und zaehlen sofort.
*
* UEBERGANG: `decideAlert` vergleicht den zuletzt gemeldeten Zustand
* ('ok' | 'red') mit der Ampel. Grau aendert nichts.
*/
import type { RatingLevel } from './nextcloud-rating';
import type { NextcloudCheckResult } from './nextcloud-status-fetch';
/** Anzahl aufeinanderfolgender Fehlschlaege, ab der "nicht erreichbar" gilt. */
export const FAILURES_FOR_RED = 2;
/** Wartezeit bis zur Wiederholung nach dem ersten Fehlschlag. */
export const RETRY_DELAY_MS = 5 * 60 * 1000;
/** Zuletzt gemeldeter Zustand einer Cloud. */
export type AlertState = 'ok' | 'red';
/** 'down' = eben rot geworden, 'up' = wieder in Ordnung, null = nichts zu melden. */
export type AlertTransition = 'down' | 'up' | null;
export function decideAlert(prev: AlertState, level: RatingLevel): AlertTransition {
if (level === 'unknown') return null;
if (prev === 'ok' && level === 'red') return 'down';
if (prev === 'red' && level !== 'red') return 'up';
return null;
}
export interface StatusWritePlan {
/** 'ok' = Abruf gelungen, 'pending' = erster Fehlschlag, 'confirmed' = bestaetigter Fehlschlag. */
outcome: 'ok' | 'pending' | 'confirmed';
data: Record<string, unknown>;
}
export function planStatusWrite(
prevFailures: number,
result: NextcloudCheckResult,
now: Date,
): StatusWritePlan {
if (result.reachable) {
return {
outcome: 'ok',
data: {
reachable: result.reachable,
maintenance: result.maintenance,
needsDbUpgrade: result.needsDbUpgrade,
versionString: result.versionString,
edition: result.edition,
productName: result.productName,
errorKind: result.errorKind,
errorDetail: result.errorDetail,
lastCheckedAt: now,
consecutiveFailures: 0,
firstFailureAt: null,
},
};
}
const failures = prevFailures + 1;
if (failures < FAILURES_FOR_RED) {
// Erster Fehlschlag: Statusfelder und lastCheckedAt bleiben unberuehrt (D-K2).
return { outcome: 'pending', data: { consecutiveFailures: failures, firstFailureAt: now } };
}
return {
outcome: 'confirmed',
data: {
reachable: result.reachable,
maintenance: result.maintenance,
needsDbUpgrade: result.needsDbUpgrade,
versionString: result.versionString,
edition: result.edition,
productName: result.productName,
errorKind: result.errorKind,
errorDetail: result.errorDetail,
lastCheckedAt: now,
// Gedeckelt: eine dauerhaft rote Cloud zaehlt nicht endlos weiter.
consecutiveFailures: FAILURES_FOR_RED,
},
};
}