waechter.log wuchs unbegrenzt: alle 5 Minuten eine Zeile, ~288/Tag. Ohne Grenze irgendwann zu groß zum Durchsehen -- dann verliert das Protokoll seinen Zweck. Der Wächter kürzt jetzt selbst, statt logrotate: Das bräuchte eine Datei in /etc (kein Schreibrecht) und einen Extra-Dienst. Vor dem Anhängen wird nur die Größe abgefragt (billig); erst über 1 MB (~45 Tage) wird die Datei einmal gelesen und auf die jüngsten 2000 Zeilen (~1 Woche) gestutzt. Beim Bauen einen eigenen Fehler gefangen: statSync war in waechter.mjs nicht importiert (beim Auslagern der Sicherungsprüfung mit entfernt worden). node --check meldet das nicht -- es hätte erst zur Laufzeit im nächsten Cron-Lauf gekracht. Import ergänzt. Test pruef-protokoll-kuerzen.mjs: klein bleibt unangetastet, groß wird auf die JÜNGSTEN Zeilen gestutzt (älteste fallen weg), Grenzfall und fehlende Datei sauber. 8/8. Co-Authored-By: Claude Opus 5 <[email protected]>
496 lines
19 KiB
JavaScript
496 lines
19 KiB
JavaScript
/* =====================================================================
|
|
waechter.mjs — merkt, wenn etwas ausfaellt, und sagt Bescheid
|
|
|
|
Aufruf (als root, per Cron alle fuenf Minuten):
|
|
/usr/local/bin/node /home/dogiweb/dogfather-universe/server-internal/waechter.mjs
|
|
|
|
WARUM ALS ROOT UND NICHT ALS DIENST
|
|
|
|
Ein Waechter, der ueber den internen Dienst meldet, hat einen
|
|
Konstruktionsfehler mit Ansage: Ausgerechnet wenn DIESER Dienst das
|
|
Problem ist, kommt keine Meldung mehr durch -- also genau im
|
|
wichtigsten Fall.
|
|
|
|
Deshalb laeuft er eigenstaendig. Er liest die Zugangsdaten selbst aus
|
|
der .env und der Datenbank und verschickt selbst. Ob irgendein Dienst
|
|
noch laeuft, ist ihm gleich.
|
|
|
|
KEINE FREMDPAKETE
|
|
|
|
Bewusst nur Bordmittel: Node-eigene Kryptographie, sqlite3 von der
|
|
Kommandozeile, systemctl, fetch. Ein Waechter, der wegen einer
|
|
fehlenden Abhaengigkeit nicht startet, ist schlimmer als keiner --
|
|
man verlaesst sich auf ihn.
|
|
|
|
⚠️ WAS ER NICHT KANN
|
|
|
|
Ist der Server als Ganzes weg -- Netz, Strom, Hardware --, meldet
|
|
auch er nichts. Dagegen hilft nur eine Ueberwachung ausserhalb dieser
|
|
Maschine. Das ist eine bewusste Grenze, keine Nachlaessigkeit.
|
|
|
|
NUR BEI ZUSTANDSWECHSEL
|
|
|
|
Gemeldet wird, wenn etwas KIPPT: heil -> kaputt und kaputt -> heil.
|
|
Nicht alle fuenf Minuten dasselbe. Wer staendig Meldungen bekommt,
|
|
sieht irgendwann keine mehr hin -- und uebersieht dann die eine, auf
|
|
die es ankam.
|
|
===================================================================== */
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, statSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import tls from "node:tls";
|
|
import { senden } from "./lib/webpush.js";
|
|
import { sicherungZustand } from "./lib/sicherung-pruefen.js";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DB = "/var/lib/dogfather-internal/dogfather-internal.db";
|
|
const ENV_DATEI = "/home/dogiintern/dogfather-universe/server-internal/.env";
|
|
const ZUSTAND_DATEI = "/var/lib/dogfather-waechter/zustand.json";
|
|
const PROTOKOLL = "/var/lib/dogfather-waechter/waechter.log";
|
|
|
|
/* ---------------------------------------------------------------------
|
|
WAS UEBERWACHT WIRD
|
|
--------------------------------------------------------------------- */
|
|
|
|
const DIENSTE = [
|
|
"caddy", "dogiweb", "dogiintern", "vandiy-shop", "safeaddress", "gitea",
|
|
];
|
|
|
|
/* WAS ALS "HEIL" GILT
|
|
|
|
Grundregel: alles unter 500.
|
|
|
|
Das ist bewusst so tief angesetzt, und der Grund ist der Zweck dieses
|
|
Waechters: Er fragt "lebt der Dienst?", nicht "ist der Inhalt
|
|
richtig?". Aus dieser Sicht sind 401, 403 und 404 GUTE Antworten --
|
|
sie beweisen, dass jemand da ist, zuhoert und Rechte prueft. Nur 5xx
|
|
und Schweigen bedeuten, dass dahinter nichts mehr laeuft; genau das
|
|
meldet Caddy als 502, wenn ein Dienst weg ist.
|
|
|
|
Auch dieser Wert stammt aus einem Fehlschlag im Test: Mit "unter 400"
|
|
galt das Postfach als ausgefallen, weil die geprueften Adresse 404
|
|
lieferte. Der Dienst lief einwandfrei -- die Adresse war schlicht
|
|
falsch gewaehlt. Eine Ueberwachung, die bei einer falsch getippten
|
|
Adresse "Ausfall" ruft, erzieht einen dazu, ihre Meldungen zu
|
|
ignorieren.
|
|
|
|
Weiterleitungen sind damit ebenfalls in Ordnung -- hinter einer
|
|
Zugangswand sind sie das RICHTIGE Ergebnis, nicht der Fehler.
|
|
|
|
⚠️ Zuerst stand hier je Adresse eine Liste erlaubter Antwortcodes,
|
|
von Hand gepflegt. Schon der erste Testlauf meldete VanVans Shop als
|
|
ausgefallen -- er war es nicht, er steht ebenfalls hinter einer
|
|
Zugangswand und antwortet mit 302. Ich war damit genau in die Falle
|
|
gelaufen, vor der der alte Kommentar an dieser Stelle gewarnt hatte.
|
|
|
|
Eine handgepflegte Liste ist hier die falsche Bauweise: Sie muss bei
|
|
jeder Aenderung an irgendeiner Seite nachgezogen werden, und wer das
|
|
vergisst, bekommt einen Fehlalarm. Nach dem zweiten oder dritten
|
|
schaltet man den Waechter ab -- und dann ist er auch beim echten
|
|
Ausfall still.
|
|
|
|
Was ein echter Ausfall ist, sieht dagegen immer gleich aus: 5xx (der
|
|
Dienst dahinter antwortet nicht mehr, Caddy meldet 502) oder gar
|
|
keine Antwort. Genau darauf wird geprueft.
|
|
|
|
"genau" gibt es nur da, wo mehr verlangt ist: Die Widerrufsseite muss
|
|
nach § 356a BGB erreichbar sein. Eine Weiterleitung waere dort schon
|
|
ein Mangel, kein Normalzustand. */
|
|
const SEITEN = [
|
|
{ name: "Dogfather Universe", url: "https://dogfather-universe.com/" },
|
|
{ name: "Postfach", url: "https://postfach.dogfather-universe.com/api/team" },
|
|
{ name: "VanVans Shop", url: "https://vans-diy-bastelbedarf.com/" },
|
|
{ name: "Buchhaltung", url: "https://buchhaltung.vans-diy-bastelbedarf.com/" },
|
|
{ name: "SafeAddress", url: "https://safeaddress.dogfather-universe.com/" },
|
|
{ name: "ZockerAnstalt", url: "https://zockeranstalt.dogfather-universe.com/" },
|
|
{ name: "Gitea", url: "https://git.dogfather-universe.com/" },
|
|
{ name: "Widerruf (Pflichtseite)", url: "https://dogfather-universe.com/webdesign/widerruf.html", genau: [200] },
|
|
{ name: "Impressum (Pflichtseite)", url: "https://dogfather-universe.com/webdesign/rechtliches.html", genau: [200] },
|
|
];
|
|
|
|
/* Ab hier wird es eng. 90 Prozent klingt nach viel Reserve -- bei einer
|
|
Datenbank, die taeglich gesichert wird, ist es das nicht. */
|
|
const PLATTE_WARNUNG = 88;
|
|
|
|
/* Caddy erneuert Zertifikate selbst, meist 30 Tage vorher. Meldet der
|
|
Waechter erst bei 7 Tagen, ist bereits dreimal etwas schiefgegangen. */
|
|
const ZERTIFIKAT_WARNUNG_TAGE = 14;
|
|
|
|
/* ---------------------------------------------------------------------
|
|
PRUEFUNGEN
|
|
--------------------------------------------------------------------- */
|
|
|
|
function dienstZustand(name) {
|
|
try {
|
|
const aus = execFileSync("systemctl", ["is-active", name + ".service"],
|
|
{ encoding: "utf8", timeout: 10000 }).trim();
|
|
return aus === "active";
|
|
} catch {
|
|
/* is-active gibt bei "inactive" einen Fehlercode zurueck -- das ist
|
|
kein Absturz, sondern die Antwort. */
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function seiteZustand(s) {
|
|
try {
|
|
const a = await fetch(s.url, {
|
|
/* Weiterleitungen NICHT verfolgen: Sonst prueft man am Ende die
|
|
Zugangswand statt der Seite und merkt nicht, wenn dahinter
|
|
nichts mehr ist. */
|
|
redirect: "manual",
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
const heil = s.genau ? s.genau.includes(a.status) : a.status < 500;
|
|
return { heil, status: a.status };
|
|
} catch (e) {
|
|
/* Keine Antwort ist der eindeutigste Ausfall ueberhaupt. */
|
|
return { heil: false, status: String(e?.message || e).slice(0, 60) };
|
|
}
|
|
}
|
|
|
|
function platteZustand() {
|
|
try {
|
|
const aus = execFileSync("df", ["--output=pcent", "/"], { encoding: "utf8", timeout: 10000 });
|
|
const belegt = Number(aus.split("\n")[1].replace("%", "").trim());
|
|
return { heil: belegt < PLATTE_WARNUNG, belegt };
|
|
} catch {
|
|
return { heil: true, belegt: -1 };
|
|
}
|
|
}
|
|
|
|
/* Restlaufzeit des Zertifikats. Direkt ueber die TLS-Verbindung, ohne
|
|
openssl -- eine Abhaengigkeit weniger. */
|
|
function zertifikatTage(host) {
|
|
return new Promise((f) => {
|
|
const zeit = setTimeout(() => { try { v.destroy(); } catch {} f(null); }, 12000);
|
|
const v = tls.connect({ host, port: 443, servername: host, rejectUnauthorized: false }, () => {
|
|
const z = v.getPeerCertificate();
|
|
clearTimeout(zeit);
|
|
v.end();
|
|
if (!z || !z.valid_to) return f(null);
|
|
f(Math.floor((new Date(z.valid_to).getTime() - Date.now()) / 86400000));
|
|
});
|
|
v.on("error", () => { clearTimeout(zeit); f(null); });
|
|
});
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
MELDEN
|
|
--------------------------------------------------------------------- */
|
|
|
|
function envLesen() {
|
|
const raus = {};
|
|
if (!existsSync(ENV_DATEI)) return raus;
|
|
for (const zeile of readFileSync(ENV_DATEI, "utf8").split("\n")) {
|
|
const t = zeile.trim();
|
|
if (!t || t.startsWith("#")) continue;
|
|
const i = t.indexOf("=");
|
|
if (i < 0) continue;
|
|
raus[t.slice(0, i).trim()] = t.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
}
|
|
return raus;
|
|
}
|
|
|
|
/* Als Trennzeichen ein Steuerzeichen, das in keiner Adresse und keinem
|
|
Schluessel vorkommen kann. "|" oder ";" waeren naheliegender und
|
|
irgendwann falsch -- Push-Adressen sind lange, undurchsichtige
|
|
Zeichenketten, und darauf, was darin nie auftaucht, moechte man nicht
|
|
wetten.
|
|
|
|
Erzeugt ueber String.fromCharCode statt als rohes Zeichen: Ein
|
|
unsichtbares Steuerzeichen im Quelltext ueberlebt kein Kopieren, kein
|
|
Zusammenfuehren und keinen Editor, der Zeilen "aufraeumt". Es
|
|
verschwindet lautlos, und dann steht hier ein leerer Trenner, an dem
|
|
sich nichts mehr aufteilen laesst. */
|
|
const TRENNER = String.fromCharCode(1);
|
|
|
|
function dbLesen(sql) {
|
|
try {
|
|
return execFileSync("sqlite3", ["-separator", TRENNER, DB, sql],
|
|
{ encoding: "utf8", timeout: 15000 }).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/* Entschluesselt im selben Format wie lib/crypto.js: "iv.ciphertext",
|
|
beides base64, AES-GCM. */
|
|
async function entschluesseln(wert, schluesselB64) {
|
|
const [ivB64, ctB64] = String(wert).split(".");
|
|
const roh = Uint8Array.from(Buffer.from(schluesselB64, "base64"));
|
|
const key = await crypto.subtle.importKey("raw", roh, "AES-GCM", false, ["decrypt"]);
|
|
const klar = await crypto.subtle.decrypt(
|
|
{ name: "AES-GCM", iv: Uint8Array.from(Buffer.from(ivB64, "base64")) },
|
|
key,
|
|
Uint8Array.from(Buffer.from(ctB64, "base64"))
|
|
);
|
|
return new TextDecoder().decode(klar);
|
|
}
|
|
|
|
async function meldung(titel, text) {
|
|
const env = envLesen();
|
|
if (!env.ENCRYPTION_KEY) return { gesendet: 0, grund: "kein ENCRYPTION_KEY lesbar" };
|
|
|
|
const oeffentlich = dbLesen("SELECT value FROM app_settings WHERE key='wd_push_vapid_oeffentlich';");
|
|
const privatRoh = dbLesen("SELECT value FROM app_settings WHERE key='wd_push_vapid_privat';");
|
|
if (!oeffentlich || !privatRoh) return { gesendet: 0, grund: "keine Push-Schluessel in der Datenbank" };
|
|
|
|
let privat;
|
|
try {
|
|
privat = await entschluesseln(privatRoh, env.ENCRYPTION_KEY);
|
|
} catch (e) {
|
|
return { gesendet: 0, grund: "Schluessel nicht entschluesselbar" };
|
|
}
|
|
|
|
/* ⚠️ Hier standen zwei sehr verschiedene Ursachen unter derselben
|
|
Meldung: eine leere Geraeteliste und eine fehlende Tabelle.
|
|
|
|
dbLesen gibt in beiden Faellen "" zurueck -- einmal, weil niemand
|
|
angemeldet ist (harmlos, ein Klick behebt es), einmal, weil die
|
|
Migration nicht gelaufen ist (ein Fehler, der Suchen an ganz
|
|
anderer Stelle ausloest). Wer "kein Geraet angemeldet" liest, klickt
|
|
auf Einschalten; wenn dann nichts passiert, sucht er beim Browser
|
|
statt bei der Datenbank.
|
|
|
|
Deshalb wird jetzt zuerst gefragt, ob es die Tabelle ueberhaupt
|
|
gibt. Aufgefallen am 26.08.2026 beim ersten echten Probelauf. */
|
|
const tabelleDa = dbLesen(
|
|
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='wd_push_geraete';"
|
|
);
|
|
if (tabelleDa !== "1") {
|
|
return { gesendet: 0, grund: "Tabelle wd_push_geraete fehlt — Migration 0019 nicht gelaufen" };
|
|
}
|
|
|
|
const zeilen = dbLesen("SELECT endpunkt, p256dh, auth FROM wd_push_geraete;");
|
|
if (!zeilen) {
|
|
return {
|
|
gesendet: 0,
|
|
grund: "kein Geraet angemeldet — in der Verwaltung unter Uebersicht einschalten",
|
|
};
|
|
}
|
|
|
|
const vapid = {
|
|
oeffentlich, privat,
|
|
kontakt: env.PUSH_KONTAKT || "mailto:[email protected]",
|
|
};
|
|
const nachricht = JSON.stringify({
|
|
titel, text, url: "/webdesign/verwaltung.html", art: "waechter",
|
|
});
|
|
|
|
let gesendet = 0;
|
|
for (const z of zeilen.split("\n")) {
|
|
const [endpunkt, p256dh, auth] = z.split(TRENNER);
|
|
if (!endpunkt) continue;
|
|
try {
|
|
const r = await senden({ endpunkt, p256dh, auth }, nachricht, vapid,
|
|
/* Eine Stoerungsmeldung ist dringend und soll auch ein
|
|
schlafendes Geraet wecken. */
|
|
{ dringlichkeit: "high", ttl: 3600 });
|
|
if (r.zustand === "ok") gesendet++;
|
|
} catch { /* ein totes Geraet darf die uebrigen nicht aufhalten */ }
|
|
}
|
|
return { gesendet };
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
ABLAUF
|
|
--------------------------------------------------------------------- */
|
|
|
|
/* 700/600 statt der Vorgabe 755/644. Hier stehen keine
|
|
personenbezogenen Daten, aber eine vollstaendige Liste aller Dienste,
|
|
Adressen und ihres Zustands -- fuer jemanden, der einen Angriff
|
|
vorbereitet, eine bequeme Landkarte. Sie kostet nichts, also gibt es
|
|
auch keinen Grund, sie herzugeben. */
|
|
/* ⚠️ mode: 0o700 beim Anlegen REICHT NICHT (nachgemessen 26.08.2026).
|
|
|
|
Zwei Gruende, und beide sind still:
|
|
|
|
1. "mode" gilt nur, wenn das Verzeichnis dabei NEU entsteht. Existiert
|
|
es schon -- und beim zweiten Lauf existiert es immer --, laesst
|
|
mkdirSync die Rechte unberuehrt. Das Verzeichnis war beim allerersten
|
|
Lauf entstanden, bevor diese Zeile ueberhaupt eingebaut war, und
|
|
blieb deshalb auf 755.
|
|
|
|
2. Selbst beim Neuanlegen zieht die umask des Prozesses noch Bits ab.
|
|
|
|
Gemessen: /var/backups/dogfather steht auf 700, weil dort ein
|
|
ausdrueckliches chmod im Skript steht. /var/lib/dogfather-waechter
|
|
stand auf 755 -- weltweit lesbar, mit einer vollstaendigen Liste
|
|
aller Dienste, Adressen und ihres Zustands darin.
|
|
|
|
Die Absicht stand also im Code, die Wirkung fehlte. Deshalb jetzt
|
|
ausdruecklich chmod, bei jedem Lauf, fuer Verzeichnis UND Datei. */
|
|
function verzeichnisSichern(pfad) {
|
|
const ordner = dirname(pfad);
|
|
mkdirSync(ordner, { recursive: true, mode: 0o700 });
|
|
try { chmodSync(ordner, 0o700); } catch { /* nicht Eigentuemer: dann eben nicht */ }
|
|
}
|
|
|
|
function dateiSichern(pfad) {
|
|
try { chmodSync(pfad, 0o600); } catch { /* Datei noch nicht da */ }
|
|
}
|
|
|
|
/* Das Protokoll hält sich selbst klein, statt unbegrenzt zu wachsen.
|
|
|
|
Der Wächter schreibt alle fünf Minuten eine Zeile -- rund 288 am Tag.
|
|
Ohne Grenze wüchse die Datei still vor sich hin, bis sie irgendwann zu
|
|
groß zum Durchsehen ist und damit ihren Zweck verliert.
|
|
|
|
Bewusst kein logrotate: Das bräuchte eine Datei in /etc (kein
|
|
Schreibrecht) und einen zusätzlichen Dienst. Der Wächter kürzt lieber
|
|
selbst -- er schreibt die Datei ohnehin.
|
|
|
|
Der Schnitt kostet fast nie etwas: Erst wird nur die Größe abgefragt
|
|
(billig). Nur wenn sie die Grenze übersteigt, wird die Datei einmal
|
|
gelesen und auf die jüngsten Zeilen gestutzt. Bei ~80 Byte pro Zeile
|
|
sind 1 MB rund 45 Tage; danach bleiben die letzten 2000 Zeilen (gut
|
|
eine Woche Verlauf) stehen. */
|
|
const PROTOKOLL_MAX_BYTE = 1024 * 1024;
|
|
const PROTOKOLL_BEHALTEN = 2000;
|
|
|
|
function protokollKuerzenFallsNoetig() {
|
|
let groesse;
|
|
try { groesse = statSync(PROTOKOLL).size; } catch { return; } // noch keine Datei
|
|
if (groesse <= PROTOKOLL_MAX_BYTE) return;
|
|
try {
|
|
const zeilen = readFileSync(PROTOKOLL, "utf8").split("\n");
|
|
/* Die letzte, meist leere Zeile nach dem abschließenden \n fällt beim
|
|
Zusammensetzen ohnehin wieder an -- deshalb slice auf die Nutzzeilen. */
|
|
const jung = zeilen.filter((z) => z.length).slice(-PROTOKOLL_BEHALTEN);
|
|
writeFileSync(PROTOKOLL, jung.join("\n") + "\n", { mode: 0o600 });
|
|
dateiSichern(PROTOKOLL);
|
|
} catch { /* Misslingt das Kürzen, wird eben weiter angehängt -- kein Drama. */ }
|
|
}
|
|
|
|
function protokoll(text) {
|
|
try {
|
|
verzeichnisSichern(PROTOKOLL);
|
|
protokollKuerzenFallsNoetig();
|
|
writeFileSync(PROTOKOLL,
|
|
`${new Date().toISOString()} ${text}\n`, { flag: "a", mode: 0o600 });
|
|
} catch { /* Protokoll ist Beiwerk, nicht der Zweck */ }
|
|
console.log(text);
|
|
}
|
|
|
|
function zustandLesen() {
|
|
try {
|
|
return JSON.parse(readFileSync(ZUSTAND_DATEI, "utf8"));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function zustandSchreiben(z) {
|
|
verzeichnisSichern(ZUSTAND_DATEI);
|
|
writeFileSync(ZUSTAND_DATEI, JSON.stringify(z, null, 1), { encoding: "utf8", mode: 0o600 });
|
|
dateiSichern(ZUSTAND_DATEI);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
PROBELAUF: node waechter.mjs --probe
|
|
|
|
Verschickt eine Meldung, ohne dass etwas kaputt sein muss.
|
|
|
|
Das ist nicht Spielerei, sondern die einzige Moeglichkeit, den
|
|
MELDEWEG zu pruefen, ohne auf eine echte Stoerung zu warten. Eine
|
|
Ueberwachung, deren Zustellung stillschweigend nicht funktioniert,
|
|
ist schlimmer als gar keine: Man haelt Stille faelschlich fuer
|
|
"alles in Ordnung" -- dabei ist sie nur Stille.
|
|
--------------------------------------------------------------------- */
|
|
if (process.argv.includes("--probe")) {
|
|
const r = await meldung(
|
|
"Probe vom Waechter",
|
|
"Wenn du das liest, kommen Stoerungsmeldungen an."
|
|
);
|
|
protokoll(`Probelauf: ${r.gesendet ? r.gesendet + " Geraet(e) erreicht" : "NICHT zugestellt (" + r.grund + ")"}`);
|
|
process.exit(r.gesendet ? 0 : 1);
|
|
}
|
|
|
|
const jetzt = {};
|
|
const beschreibung = {};
|
|
|
|
for (const d of DIENSTE) {
|
|
const heil = dienstZustand(d);
|
|
jetzt["dienst:" + d] = heil;
|
|
beschreibung["dienst:" + d] = heil
|
|
? `Dienst ${d} laeuft wieder`
|
|
: `Dienst ${d} laeuft NICHT`;
|
|
}
|
|
|
|
for (const s of SEITEN) {
|
|
const r = await seiteZustand(s);
|
|
jetzt["seite:" + s.name] = r.heil;
|
|
beschreibung["seite:" + s.name] = r.heil
|
|
? `${s.name} ist wieder erreichbar`
|
|
: `${s.name} antwortet nicht (${r.status})`;
|
|
}
|
|
|
|
const p = platteZustand();
|
|
jetzt["platte"] = p.heil;
|
|
beschreibung["platte"] = p.heil
|
|
? `Plattenplatz wieder in Ordnung (${p.belegt}%)`
|
|
: `Platte zu ${p.belegt}% belegt`;
|
|
|
|
/* Kein Urteil (null) heisst: alten Zustand stehen lassen, nicht melden --
|
|
dasselbe Vorgehen wie beim Zertifikat weiter unten. */
|
|
const sich = sicherungZustand();
|
|
if (sich) {
|
|
jetzt["sicherung"] = sich.heil;
|
|
beschreibung["sicherung"] = sich.heil
|
|
? `Sicherung laeuft wieder (${sich.grund})`
|
|
: `SICHERUNG FEHLT: ${sich.grund}`;
|
|
}
|
|
|
|
for (const host of ["dogfather-universe.com", "vans-diy-bastelbedarf.com"]) {
|
|
const tage = await zertifikatTage(host);
|
|
/* Kein Ergebnis heisst NICHT "kaputt" -- ein kurzer Netzhaenger darf
|
|
keinen Alarm ausloesen. Dann bleibt der alte Zustand stehen. */
|
|
if (tage === null) continue;
|
|
jetzt["zert:" + host] = tage > ZERTIFIKAT_WARNUNG_TAGE;
|
|
beschreibung["zert:" + host] = tage > ZERTIFIKAT_WARNUNG_TAGE
|
|
? `Zertifikat ${host} wieder gueltig (${tage} Tage)`
|
|
: `Zertifikat ${host} laeuft in ${tage} Tagen ab`;
|
|
}
|
|
|
|
const vorher = zustandLesen();
|
|
const neuKaputt = [];
|
|
const wiederHeil = [];
|
|
|
|
for (const [k, heil] of Object.entries(jetzt)) {
|
|
const alt = vorher[k];
|
|
if (alt === undefined) {
|
|
/* Beim allerersten Lauf wird nur aufgezeichnet. Sonst kaeme sofort
|
|
eine Meldung fuer jeden Punkt, der gerade zufaellig nicht
|
|
antwortet -- und der erste Eindruck waere ein Fehlalarm. */
|
|
continue;
|
|
}
|
|
if (alt && !heil) neuKaputt.push(beschreibung[k]);
|
|
if (!alt && heil) wiederHeil.push(beschreibung[k]);
|
|
}
|
|
|
|
zustandSchreiben(jetzt);
|
|
|
|
const kaputtGesamt = Object.values(jetzt).filter((x) => !x).length;
|
|
|
|
if (!neuKaputt.length && !wiederHeil.length) {
|
|
protokoll(`ruhig (${Object.keys(jetzt).length} Punkte, davon ${kaputtGesamt} auffaellig)`);
|
|
process.exit(0);
|
|
}
|
|
|
|
let titel, text;
|
|
if (neuKaputt.length) {
|
|
titel = neuKaputt.length === 1 ? "Stoerung" : `${neuKaputt.length} Stoerungen`;
|
|
text = neuKaputt.join(" · ").slice(0, 300);
|
|
} else {
|
|
titel = "Wieder in Ordnung";
|
|
text = wiederHeil.join(" · ").slice(0, 300);
|
|
}
|
|
|
|
protokoll(`WECHSEL: ${titel} — ${text}`);
|
|
const r = await meldung(titel, text);
|
|
protokoll(`Meldung: ${r.gesendet ? r.gesendet + " Geraet(e)" : "nicht zugestellt (" + r.grund + ")"}`);
|
|
process.exit(0);
|