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>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user