faed0d760f
- 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>
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
/**
|
|
* nextcloud-alert-mail — reiner Baustein der Benachrichtigungsmail
|
|
* (quick-261002-kxc, L-05, D-K6). Kein Nest, kein Zugriff auf die Uhr oder
|
|
* den Versand: Betreff und Klartext entstehen aus den Eingaben. Die Mail ist
|
|
* wie die Erinnerungsmails nur Deutsch, Zeit in Europe/Berlin.
|
|
*/
|
|
|
|
import type { NextcloudRating } from './nextcloud-rating';
|
|
|
|
export interface NextcloudAlertMailInput {
|
|
kind: 'down' | 'up';
|
|
customerName: string;
|
|
baseUrl: string;
|
|
rating: NextcloudRating;
|
|
errorKind: string | null;
|
|
errorDetail: string | null;
|
|
at: Date;
|
|
}
|
|
|
|
const SUBJECT_MAX = 150;
|
|
|
|
const CERT_NAME_CODES = new Set(['ERR_TLS_CERT_ALTNAME_INVALID', 'HOSTNAME_MISMATCH']);
|
|
const CERT_EXPIRED_CODES = new Set(['CERT_HAS_EXPIRED']);
|
|
const CERT_UNTRUSTED_CODES = new Set([
|
|
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
|
'CERT_UNTRUSTED',
|
|
'CERT_NOT_YET_VALID',
|
|
'CERT_REVOKED',
|
|
]);
|
|
const DNS_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN']);
|
|
const TIMEOUT_CODES = new Set(['ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT']);
|
|
|
|
/**
|
|
* Uebersetzt Fehlerart und Kurzkennung eines fehlgeschlagenen Abrufs in einen
|
|
* lesbaren deutschen Hinweis. Die Rohkennung bleibt in der Datenbank; die
|
|
* Kachel (Web) bildet dieselben Regeln ab.
|
|
*/
|
|
export function describeCheckError(
|
|
errorKind: string | null | undefined,
|
|
errorDetail: string | null | undefined,
|
|
): string {
|
|
const detail = errorDetail ?? '';
|
|
if (errorKind === 'tls') {
|
|
if (CERT_NAME_CODES.has(detail)) return 'Zertifikat passt nicht zur Adresse';
|
|
if (CERT_EXPIRED_CODES.has(detail)) return 'Zertifikat abgelaufen';
|
|
if (CERT_UNTRUSTED_CODES.has(detail)) return 'Zertifikat nicht vertrauenswürdig';
|
|
return 'Verbindungsfehler';
|
|
}
|
|
if (errorKind === 'timeout') return 'Zeitüberschreitung';
|
|
if (errorKind === 'http-status') {
|
|
const match = /^HTTP (\d{3})$/.exec(detail);
|
|
return match ? `Server antwortet mit Fehler ${match[1]}` : 'Verbindungsfehler';
|
|
}
|
|
if (errorKind === 'network') {
|
|
if (DNS_CODES.has(detail)) return 'Adresse nicht gefunden';
|
|
if (detail === 'ECONNREFUSED') return 'Verbindung abgelehnt';
|
|
if (TIMEOUT_CODES.has(detail)) return 'Zeitüberschreitung';
|
|
}
|
|
return 'Verbindungsfehler';
|
|
}
|
|
|
|
function subjectWording(input: NextcloudAlertMailInput): string {
|
|
if (input.kind === 'up') return 'wieder in Ordnung';
|
|
switch (input.rating.reason) {
|
|
case 'invalid-response':
|
|
return 'keine gültige Antwort';
|
|
case 'maintenance':
|
|
return 'im Wartungsmodus';
|
|
case 'needs-db-upgrade':
|
|
return 'Datenbank-Aktualisierung ausstehend';
|
|
case 'eol-passed':
|
|
return 'Support abgelaufen';
|
|
default:
|
|
return 'nicht erreichbar';
|
|
}
|
|
}
|
|
|
|
/** 'YYYY-MM-DD' als 'TT.MM.JJJJ'. */
|
|
function germanDay(day: string): string {
|
|
const [y, m, d] = day.split('-');
|
|
return `${d}.${m}.${y}`;
|
|
}
|
|
|
|
function reasonLine(input: NextcloudAlertMailInput): string {
|
|
const { rating } = input;
|
|
if (input.kind === 'up') {
|
|
if (rating.reason === 'update-available' && rating.updateTo) {
|
|
return `Aktueller Stand: Update auf ${rating.updateTo} verfügbar`;
|
|
}
|
|
if (rating.reason === 'eol-soon' && rating.eolDate) {
|
|
return `Aktueller Stand: Support endet am ${germanDay(rating.eolDate)}`;
|
|
}
|
|
return 'Aktueller Stand: Aktuell';
|
|
}
|
|
switch (rating.reason) {
|
|
case 'invalid-response':
|
|
return 'Grund: Keine gültige Nextcloud-Antwort';
|
|
case 'maintenance':
|
|
return 'Grund: Wartungsmodus eingeschaltet';
|
|
case 'needs-db-upgrade':
|
|
return 'Grund: Datenbank-Aktualisierung ausstehend';
|
|
case 'eol-passed':
|
|
return rating.eolDate
|
|
? `Grund: Support abgelaufen seit ${germanDay(rating.eolDate)}`
|
|
: 'Grund: Support abgelaufen';
|
|
default:
|
|
return `Grund: Nicht erreichbar (${describeCheckError(input.errorKind, input.errorDetail)})`;
|
|
}
|
|
}
|
|
|
|
export function buildNextcloudAlertMail(
|
|
input: NextcloudAlertMailInput,
|
|
appUrl: string,
|
|
): { subject: string; text: string } {
|
|
const subject = `Nextcloud ${input.customerName}: ${subjectWording(input)}`
|
|
.replace(/[\r\n]+/g, ' ')
|
|
.slice(0, SUBJECT_MAX);
|
|
const when = `${new Intl.DateTimeFormat('de-DE', {
|
|
timeZone: 'Europe/Berlin',
|
|
dateStyle: 'full',
|
|
timeStyle: 'short',
|
|
}).format(input.at)} Uhr`;
|
|
const name = input.customerName.replace(/[\r\n]+/g, ' ');
|
|
const sentence =
|
|
input.kind === 'down'
|
|
? `die Nextcloud "${name}" hat seit ${when} ein Problem.`
|
|
: `die Nextcloud "${name}" ist seit ${when} wieder in Ordnung.`;
|
|
const lines = [
|
|
'Guten Tag,',
|
|
'',
|
|
sentence,
|
|
'',
|
|
reasonLine(input),
|
|
`Adresse: ${input.baseUrl}`,
|
|
`Zeitpunkt: ${when}`,
|
|
'',
|
|
`Zum Modul: ${appUrl}/modules/nextcloud-status`,
|
|
'',
|
|
'Sie erhalten diese Nachricht, weil für diese Cloud "Benachrichtigen" eingeschaltet ist. Mit der Glocke auf der Kachel können Sie das jederzeit ausschalten.',
|
|
];
|
|
return { subject, text: lines.join('\n') };
|
|
}
|