Fuenf Suchlaeufe parallel, ihre Funde selbst nachgemessen und behoben.
DER GROESSTE: 13 VON 21 SEITEN SCHLUCKTEN JEDEN NETZFEHLER.
Jede Seite beginnt mit `try { await hole('/api/ich') } catch { return; }`.
Faellt das Netz aus -- Aufzug, U-Bahn, Funkloch --, bricht der Block ab,
und die Seite bleibt fuer immer auf "wird geladen …" stehen. Gemessen:
0 von 32 Stellen setzten `aria-busy` im Fehlerfall zurueck, und es gab
KEINEN EINZIGEN "Nochmal versuchen" auf allen 21 Seiten.
Im Code stand der richtige Satz dazu -- an genau einer Stelle:
"Ein Platzhalter, der nie ersetzt wird, ist eine Luege mit
Fortschrittsanzeige." Er galt ueberall ausser dort.
Neu: window.ladefehler() in meldung.js. Sie ersetzt alles, was gerade
"laedt", durch drei Dinge: was los ist, was das bedeutet, und einen
Knopf, der es noch einmal versucht -- ohne die Seite neu zu laden.
21 Dateien angeschlossen. Neu: server/pruef-ladefehler.mjs, 26/0, mit
abgeklemmtem Netz im echten Browser.
UND DIE STARTSEITE WARF BEI EINEM FUNKLOCH AUF DIE ANMELDUNG.
`catch { location.assign('/workspace/') }` -- der Mensch glaubt, er sei
rausgeworfen, und tippt seinen Code neu. Dabei haelt seine Sitzung 12
Stunden; nur die Anfrage kam nicht durch. Umgeleitet wird jetzt nur
noch bei einer ANTWORT, die das sagt (401).
DER ANMELDEHINWEIS FUEHRTE MODIS IN DIE SPERRE.
Nach zwei Fehlversuchen stand: "Stimmt die Auswahl oben? Ein Code
gehoert immer zu genau einer davon." Auf der Crew-Wand ist das FALSCH
-- `stillerZugang()` sucht ueber die Codekennung, die angetippte
Kachel spielt fuer hand/modi keine Rolle. Sie probieren alle vier
durch, sammeln vier Fehlversuche, und nach acht in zehn Minuten ist
ihre Adresse gesperrt. Ein Hinweis, der die Sperre herbeifuehrt, gegen
die er helfen soll. Auf der Agenturwand stimmt der Satz weiter --
deshalb wird gefragt, auf welcher Wand man steht.
Dazu: Die Crew-Wand nannte nur der Community einen Weg ("Frag im Live
nach"). Wer zum Team gehoert und dessen Code nicht geht, fand dort
niemanden.
SACKGASSE AUF DER KERNSEITE EINES MODI.
treff-moderation sagte: "Zugaenge legst du in 'Personen & Zugaenge'
an" -- eine Seite, die ein Modi nicht oeffnen darf. Am ersten Tag, bei
leerer Community, war das der einzige Satz im Abschnitt. Jetzt fragt
die Seite ueber `window.__ich.seiten`, ob es den Weg fuer DIESEN
Menschen gibt. Und ein 404 wirft ihn nicht mehr wortlos auf die
Startseite.
TOTE KNOEPFE AN FREMDEN KARTEN.
"◀ zurueck" und "▶ In Arbeit" standen an JEDER Aufgabenkarte, auch an
fremden. Ein Modi sieht das Brett des ganzen Teams; er tippt, der
Server lehnt mit 403 ab, und die Meldung erscheint GANZ OBEN. Bei
einer Karte weiter unten sieht er nichts. Zwei Zeilen tiefer stand die
Regel im Klartext: "niemandem etwas anzubieten, das dann abgelehnt
wird." Der Server schickt jetzt `darf_aendern` mit.
13 STELLEN ROLLTEN GEGEN DEN WILLEN DES NUTZERS.
`scrollIntoView({ behavior: 'smooth' })` beachtet "Animationen
reduzieren" NICHT. Wer das eingestellt hat, hat es meist wegen
Schwindel getan. Zwei Stellen fragten vorher, dreizehn nicht.
Jetzt `window.sanft()`, einmal statt dreizehnmal.
DAS WORT UEBER DEN BRETTERN EINES MODI HIESS "BETREUUNG".
Fest im HTML, ueberschrieben nur bei Brettern mit eigenem `ober` --
sechs haben keines. Jetzt faellt es auf die GRUPPE der Kachel zurueck,
ueber die er hergekommen ist. Je Rolle richtig, ohne zweite Liste.
DER HINWEIS-ZU-KACHEL-WEG WAR DOPPELT KAPUTT.
`bereichZu` suchte nur in GRUPPEN -- der Liste der AGENTUR. Die
Kacheln von Team Dogi schickt der Server; fuer einen Modi fand die
Zeile entweder nichts oder eine fremde Kachel und uebernahm deren
Farbe. Und sie suchte ueber den NAMEN: "LIVE-Analyse" heisst auf der
Crew-Adresse "Live-Ablauf". Beim Beheben erst den Namen umgedreht --
und damit die Agenturseite kaputt gemacht (pruef-start-ansicht sofort
rot). Jetzt ueber das ZIEL, das in beiden Haeusern dasselbe ist.
UND EIN BRETT WAR SEIT GESTERN GESPERRT.
`TREFF_BRETTER` wird aus Kachelzielen abgeleitet. Als die Kachel
"Regeln & Hilfe" am 19.09. auf `treff-regeln.html` umgelenkt wurde,
fiel `regeln` heraus -- und `treff-regeln.html` verweist weiterhin
darauf ("Haeufige Fragen stehen auf dem Brett Regeln & Hilfe").
Gefunden hat es pruef-treff, die seit gestern rot war. 72/0.
NEBENBEI 7 SEITEN LEICHTER: meldung.js wird jetzt abgeleitet
eingebunden -- nur dort, wo sagWas/ladefehler/sanft wirklich
gebraucht werden. Die Anmeldewand traegt es nicht mehr.
Gruen: pruef-ladefehler 26/0, pruef-sackgassen 11/0 (582 Wege, 0 ins
Leere), pruef-start-ansicht, pruef-treff 72/0, pruef-treffchat,
pruef-community-sicht 10/0, pruef-aufgabenbrett, pruef-code 17/0,
pruef-chat-optik, pruef-nachfrage 49/0, pruef-meldungen 8/0,
pruef-css-klassen, pruef-tippziele 11/0, pruef-leerzustand 13/0,
pruef-rechtetafel.
470 lines
21 KiB
JavaScript
470 lines
21 KiB
JavaScript
/* ===================================================================
|
||
Automationen.
|
||
|
||
Die Kachel trug lange "Phase 4", obwohl die Automationen laengst
|
||
liefen -- sie waren nur verteilt und hatten keinen Ort. Diese Seite
|
||
ist dieser Ort.
|
||
|
||
Sie fuehrt bewusst NICHTS Eigenes: Was hier steht, kommt aus den
|
||
Bereichen selbst. Eine Seite, die Automationen doppelt abbildet,
|
||
waere die naechste Stelle, die irgendwann etwas anderes behauptet als
|
||
die Wirklichkeit.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (tag, klasse, text) => {
|
||
const k = document.createElement(tag);
|
||
if (klasse) k.className = klasse;
|
||
if (text !== undefined) k.textContent = text;
|
||
return k;
|
||
};
|
||
const melde = (t) => { $('fehler').textContent = t || ''; };
|
||
|
||
async function hole(pfad, optionen = {}) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||
return a;
|
||
}
|
||
|
||
/* Was dauerhaft laeuft. Steht hier als Text, weil es Verhalten ist und
|
||
keine Daten -- aber jede Zeile beschreibt etwas, das tatsaechlich im
|
||
Code steht, nicht etwas Gewuenschtes. */
|
||
const REGELN = [
|
||
{ name: 'Hinweise auf der Übersicht',
|
||
text: 'Überfälliges, heute Fälliges, Gespräche ohne Protokoll, fällige Follow-ups und dringende Punkte werden bei jedem Aufruf neu ermittelt. Sie führen keine eigene Liste – ein Hinweis verschwindet von selbst, sobald die Sache erledigt ist.' },
|
||
{ name: '„Neu" in der Bibliothek',
|
||
text: 'Eine frisch eingestellte Anleitung steht 48 Stunden unter „Neu & wichtig“ und ist danach nur noch in ihrer Kategorie zu finden. Angepinntes bleibt, bis es jemand abschaltet.' },
|
||
{ name: 'Zuständigkeit bei der Übergabe',
|
||
text: 'Wird aus einem übergebenen Lead ein Creator angelegt, betreut ihn automatisch der Scout, der ihn gefunden hat. Die Angaben aus der Pipeline wandern dabei ins Creator-Profil.' },
|
||
{ name: 'Aufgaben aus Gesprächen',
|
||
text: 'To-dos aus einem Protokoll, Entscheidungen aus einem Review und Befunde aus dem Start-Check werden zu echten Aufgaben im Brett – in einem Zug, nicht als Text, den man später abtippt.' },
|
||
{ name: 'Sitzungen laufen ab',
|
||
text: 'Eine Anmeldung gilt zwölf Stunden. Nach einem neuen Zugangscode oder einer Sperre enden alle offenen Sitzungen der Person sofort.' },
|
||
{ name: 'Schutz vor Rateversuchen',
|
||
text: 'Acht Fehlversuche je Anschluss in zehn Minuten, danach ist zu. Gezählt wird die echte Adresse, nicht die von Cloudflare.' },
|
||
{ name: 'Sicherung vor Umbauten',
|
||
text: 'Bevor die Datenbank umgestellt wird, schreibt der Server eine vollständige Sicherung. Ohne Sicherung wird nicht umgestellt.' },
|
||
{ name: 'Nächtliche Sicherung',
|
||
text: 'Jede Nacht eine vollständige Sicherung, die sofort wieder geöffnet und geprüft wird – auf Unversehrtheit und darauf, dass wirklich Daten drinstehen. Aufgehoben werden 7 tägliche, 4 wöchentliche und 6 monatliche. Geschrieben wird nie eine Dateikopie, sondern immer von SQLite selbst.' },
|
||
{ name: 'Kopie außer Haus',
|
||
text: 'Filipes Rechner holt sich die Sicherungen täglich von allein – zusammen mit den hochgeladenen Dateien und den PDFs der Bibliothek, die nicht in der Datenbank stecken. War der Rechner aus, wird der Lauf beim nächsten Hochfahren nachgeholt. Nach jedem Lauf meldet er sich hier zurück, damit auffällt, wenn er es nicht mehr tut.' },
|
||
];
|
||
|
||
const ART = {
|
||
ki_gefragt: 'KI um einen Vorschlag gebeten',
|
||
einstellung_geaendert: 'Einstellung geändert',
|
||
betreuung_gesetzt: 'Zuständigkeit gesetzt',
|
||
onboarding_gestartet: 'Creator aus einem Lead angelegt',
|
||
protokoll_geschrieben: 'Gesprächsprotokoll festgehalten',
|
||
startcheck_aufgabe: 'Aufgabe aus dem Start-Check',
|
||
review_entscheidung: 'Aufgabe aus dem Review',
|
||
anmeldung_gesperrt: 'Anmeldung wegen zu vieler Versuche gesperrt',
|
||
};
|
||
|
||
const zeitpunkt = (iso) => {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
const p = (n) => String(n).padStart(2, '0');
|
||
return `${p(d.getDate())}.${p(d.getMonth() + 1)}. · ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||
};
|
||
|
||
/* ---------- KI ---------------------------------------------------------- */
|
||
|
||
async function kiZustand() {
|
||
let d;
|
||
try { d = await (await hole('/workspace/api/ki/status')).json(); } catch { return; }
|
||
|
||
$('ki-schalter-block').hidden = false;
|
||
$('ki-schalter').checked = d.angeschaltet !== false;
|
||
$('ki-schalter-text').textContent = d.angeschaltet !== false ? 'Angeschaltet' : 'Abgeschaltet';
|
||
$('ki-kasten').dataset.zustand = d.da ? 'laeuft' : (d.angeschaltet === false ? 'aus' : 'weg');
|
||
|
||
$('ki-text').textContent = d.angeschaltet === false
|
||
? 'Abgeschaltet. Die Vorschlags-Knöpfe erscheinen nirgends, und der Dienst wird nicht gefragt.'
|
||
: d.da
|
||
? 'Läuft. Die Vorschlags-Knöpfe erscheinen in Calls, Start-Check und Report.'
|
||
: 'Angeschaltet, aber gerade nicht erreichbar. Alles andere funktioniert normal weiter.';
|
||
|
||
const daten = $('ki-daten');
|
||
daten.textContent = '';
|
||
for (const [name, wert] of [
|
||
['Modell', 'Qwen3.5-4B, Apache 2.0'],
|
||
['Ort', 'auf diesem Server, nur über 127.0.0.1'],
|
||
['Kosten', '0 € – nichts verlässt die Maschine'],
|
||
['Grenzen', 'zwei von vier Kernen, eine Anfrage gleichzeitig, fünf je Person in zehn Minuten'],
|
||
['Zustand', d.beschaeftigt ? 'denkt gerade' : (d.da ? 'bereit' : '–')],
|
||
]) {
|
||
const li = el('li');
|
||
li.append(el('span', 'ki-kasten__schild', name), el('span', null, wert));
|
||
daten.append(li);
|
||
}
|
||
}
|
||
|
||
$('ki-schalter').addEventListener('change', async () => {
|
||
const an = $('ki-schalter').checked;
|
||
$('ki-schalter').disabled = true;
|
||
try {
|
||
const a = await hole('/workspace/api/ki/schalter', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ an }),
|
||
});
|
||
if (!a.ok) {
|
||
melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.'));
|
||
$('ki-schalter').checked = !an;
|
||
return;
|
||
}
|
||
melde('');
|
||
await kiZustand();
|
||
} catch { /* umgeleitet */ } finally { $('ki-schalter').disabled = false; }
|
||
});
|
||
|
||
/* ---------- Hinweise ----------------------------------------------------- */
|
||
|
||
const HINWEISTEXT = {
|
||
aufgaben_ueberfaellig: 'überfällige Aufgaben', aufgaben_heute: 'heute fällige Aufgaben',
|
||
aufgaben_review: 'Aufgaben warten auf Freigabe', termine_heute: 'Termine heute',
|
||
protokoll_fehlt: 'Gespräche ohne Protokoll', dateien_review: 'Dateien im Review',
|
||
bereiche_dringend: 'dringende Punkte in den Bereichen', startcheck_handlung: 'Punkte im Start-Check',
|
||
followup_faellig: 'fällige Follow-ups', uebergabe_offen: 'übergebene Leads ohne Creator',
|
||
review_faellig: 'überfällige Review-Termine', review_bald: 'Reviews in den nächsten 7 Tagen',
|
||
ohne_betreuung: 'Creator ohne zuständige Person', ohne_profil: 'leere Creator-Profile',
|
||
ohne_startcheck: 'Creator ohne Start-Check',
|
||
};
|
||
|
||
async function hinweise() {
|
||
let d;
|
||
try { d = await (await hole('/workspace/api/hinweise')).json(); } catch { return; }
|
||
const ziel = $('hinweise');
|
||
ziel.textContent = '';
|
||
$('hinweis-zahl').textContent = String(d.hinweise.length);
|
||
if (!d.hinweise.length) {
|
||
ziel.append(el('p', 'leer-hinweis', 'Nichts offen. Alles läuft.'));
|
||
return;
|
||
}
|
||
const liste = el('div', 'hinweise');
|
||
for (const h of d.hinweise) {
|
||
const a = el('a', 'hinweis');
|
||
a.href = h.ziel;
|
||
a.dataset.stufe = h.stufe;
|
||
a.append(el('span', 'hinweis__zahl', String(h.anzahl)));
|
||
a.append(el('span', 'hinweis__text', HINWEISTEXT[h.art] || h.text));
|
||
a.append(el('span', 'hinweis__pfeil', '→'));
|
||
liste.append(a);
|
||
}
|
||
ziel.append(liste);
|
||
}
|
||
|
||
/* ---------- Sicherung & Zustand ------------------------------------------
|
||
|
||
Die einzige Automation, deren Ausfall ALLES auf einmal kostet -- und
|
||
die einzige, die man nicht bemerkt, wenn sie ausbleibt. Deshalb steht
|
||
hier nicht nur "läuft", sondern wann zuletzt, wie viel, wie viele
|
||
Personen darin standen und wie alt das ist.
|
||
------------------------------------------------------------------------ */
|
||
|
||
const bytes = (n) => {
|
||
if (n === null || n === undefined) return '–';
|
||
if (n < 1024) return n + ' B';
|
||
if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB';
|
||
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB';
|
||
return (n / 1024 / 1024 / 1024).toFixed(1) + ' GB';
|
||
};
|
||
|
||
function herZeit(iso) {
|
||
if (!iso) return null;
|
||
const min = Math.floor((Date.now() - Date.parse(iso)) / 60000);
|
||
if (!Number.isFinite(min)) return null;
|
||
if (min < 60) return `vor ${Math.max(min, 0)} Minuten`;
|
||
const std = Math.floor(min / 60);
|
||
if (std < 48) return `vor ${std} Stunden`;
|
||
return `vor ${Math.floor(std / 24)} Tagen`;
|
||
}
|
||
|
||
async function zustand() {
|
||
const kasten = $('zustand-kasten');
|
||
let z;
|
||
try {
|
||
const a = await hole('/workspace/api/zustand');
|
||
/* 404 heisst hier: diese Rolle soll davon gar nichts wissen. Dann
|
||
verschwindet der ganze Kasten, statt leer dazustehen. */
|
||
if (!a.ok) { kasten.hidden = true; return; }
|
||
z = await a.json();
|
||
} catch { kasten.hidden = true; return; }
|
||
|
||
const her = herZeit(z.letzte?.zeit);
|
||
/* Älter als 36 Stunden heisst: mindestens eine Nacht ist ausgefallen. */
|
||
const alt = z.letzte?.zeit
|
||
? (Date.now() - Date.parse(z.letzte.zeit)) > 36 * 3600_000 : true;
|
||
/* Die Kopie auf dem Rechner läuft täglich, der Rechner ist aber nicht
|
||
täglich an. Erst nach zehn Tagen ist das ein echtes Zeichen. */
|
||
const kopieAlt = z.kopie?.zeit
|
||
? (Date.now() - Date.parse(z.kopie.zeit)) > 10 * 86400_000 : true;
|
||
|
||
kasten.dataset.zustand = z.fehler ? 'fehler' : ((alt || kopieAlt) ? 'alt' : 'gut');
|
||
|
||
/* Die Überschrift muss den GRUND nennen, aus dem der Kasten gelb ist.
|
||
Stand dort nur "Zuletzt gesichert vor 2 Minuten", während die Farbe
|
||
wegen der fehlenden Kopie außer Haus warnte, widersprach sich die
|
||
Anzeige selbst -- und man sucht den Fehler an der falschen Stelle. */
|
||
const [titel, text] =
|
||
z.fehler
|
||
? ['Die letzte Sicherung ist fehlgeschlagen', z.fehler.text]
|
||
: !z.letzte
|
||
? ['Noch keine Sicherung gelaufen',
|
||
'Die erste läuft in der kommenden Nacht – oder jetzt gleich auf Knopfdruck.']
|
||
: alt
|
||
? [`Die letzte Sicherung ist ${her}`,
|
||
'Mindestens eine Nacht ist ausgefallen. Läuft der Dienst durch?']
|
||
: kopieAlt
|
||
? ['Es fehlt eine Kopie außerhalb des Servers',
|
||
z.kopie
|
||
? `Zuletzt ${herZeit(z.kopie.zeit)} geholt. Auf dem Server ist alles frisch `
|
||
+ `(${z.letzte.datei}, ${bytes(z.letzte.groesse)}), aber eine Sicherung auf `
|
||
+ `derselben Platte hilft nicht gegen einen Plattenausfall.`
|
||
: `Auf dem Server ist alles frisch (${z.letzte.datei}, ${bytes(z.letzte.groesse)}, `
|
||
+ `${z.letzte.personen} Personen). Aber eine Sicherung auf derselben Platte hilft `
|
||
+ `nicht gegen einen Plattenausfall – und geholt hat sie noch niemand.`]
|
||
: [`Zuletzt gesichert ${her}`,
|
||
`${z.letzte.datei} · ${bytes(z.letzte.groesse)} · ${z.letzte.personen} Personen · `
|
||
+ `geschrieben und sofort wieder geprüft in ${z.letzte.dauer_ms} ms`];
|
||
|
||
$('zustand-titel').textContent = titel;
|
||
$('zustand-text').textContent = text;
|
||
|
||
const daten = $('zustand-daten');
|
||
daten.textContent = '';
|
||
const zeilen = [
|
||
['Aufgehoben', `${z.anzahl} Sicherungen · ${bytes(z.belegt)} · `
|
||
+ `${z.behalten.taeglich} täglich, ${z.behalten.woechentlich} wöchentlich, ${z.behalten.monatlich} monatlich`],
|
||
['Datenbank', `${bytes(z.dateien?.db)} · Prüfung: ${z.heil || '–'} · Betrieb: ${z.journal || '–'}`],
|
||
/* Die WAL-Grösse steht hier, weil genau daran der Fehler vom
|
||
31.08.2026 aufgefallen wäre: Datenbank 4 KB, WAL 2,1 MB. */
|
||
['Wartende Schreibvorgänge', `${bytes(z.dateien?.wal)} (WAL – wird bei jeder Sicherung zusammengeführt)`],
|
||
['Platz', z.platte ? `${bytes(z.platte.frei)} frei von ${bytes(z.platte.gesamt)}` : '–'],
|
||
/* Die wichtigste Zeile im Kasten. Eine Sicherung auf derselben
|
||
Platte hilft gegen versehentliches Löschen, nicht gegen einen
|
||
Plattenausfall. Und ohne diese Rückmeldung würde niemand merken,
|
||
wenn die Kopie still aufhört zu laufen. */
|
||
['Kopie außer Haus', z.kopie
|
||
? `${herZeit(z.kopie.zeit)} · ${z.kopie.rechner || 'ein Rechner'} · `
|
||
+ `${z.kopie.sicherungen} Sicherungen, ${z.kopie.dateien} Dateien`
|
||
: 'noch nie – läuft dieser Rechner die Aufgabe?'],
|
||
['Dienst läuft', `${Math.floor(z.seit / 3600)} h ${Math.floor((z.seit % 3600) / 60)} min`],
|
||
];
|
||
for (const [name, wert] of zeilen) {
|
||
const li = el('li');
|
||
li.append(el('span', 'ki-kasten__schild', name), el('span', null, wert));
|
||
daten.append(li);
|
||
}
|
||
|
||
const liste = $('sicherungen');
|
||
liste.textContent = '';
|
||
if (!z.sicherungen.length) {
|
||
liste.append(el('p', 'leise', 'Noch keine.'));
|
||
$('sicherungen-schalter').hidden = true;
|
||
return;
|
||
}
|
||
/* DAS DATUM WIRD GESUCHT, NICHT ZUSAMMENGESETZT (07.09.2026).
|
||
|
||
Hier stand `name.split('-').slice(1).reverse().join('.')`. Bei
|
||
"woechentlich-2026-09-06.db" ging das gut. Die Sicherungen vor
|
||
einem Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit
|
||
einem Zeitstempel hinten dran. Aus derselben Rechnung wurde dann
|
||
"160012438.02.09.2026": eine Zeichenkette, die dreimal so lang
|
||
ist wie die Spalte und ueber den Nachbartext lief. Genau das hat
|
||
Filipe im Bildschirmfoto gesehen.
|
||
|
||
Ein Muster, das die Bestandteile ZAEHLT statt sie zu SUCHEN,
|
||
bricht beim ersten Namen, der einen Teil mehr hat. Deshalb jetzt
|
||
ein Suchmuster nach vier-zwei-zwei Ziffern -- egal, was davor
|
||
oder dahinter steht. */
|
||
for (const s of z.sicherungen) {
|
||
const roh = s.name.replace(/\.db$/, '');
|
||
const t = roh.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||
const zeile = el('div', 'verlauf__zeile');
|
||
/* Findet sich kein Datum, wird das GESAGT und nicht geraten --
|
||
ein erfundenes Datum an einer Sicherung waere schlimmer als
|
||
ein Strich. */
|
||
zeile.append(el('span', 'verlauf__zeit', t ? `${t[3]}.${t[2]}.${t[1]}` : '—'));
|
||
zeile.append(el('span', 'verlauf__was', roh.split('-')[0]));
|
||
zeile.append(el('span', 'verlauf__wer', bytes(s.groesse)));
|
||
zeile.title = s.name; // der volle Name bleibt erreichbar
|
||
liste.append(zeile);
|
||
}
|
||
window.klappe?.(liste, $('sicherungen-schalter'), $('sicherungen-schalter-text'));
|
||
}
|
||
|
||
$('jetzt-sichern').addEventListener('click', async () => {
|
||
const knopf = $('jetzt-sichern');
|
||
knopf.disabled = true;
|
||
const vorher = knopf.textContent;
|
||
knopf.textContent = 'Sichert …';
|
||
try {
|
||
const a = await hole('/workspace/api/zustand/sichern', { method: 'POST' });
|
||
const d = await a.json().catch(() => ({}));
|
||
if (!a.ok) { melde(sagWas(d.fehler, 'Die Sicherung ist fehlgeschlagen.')); return; }
|
||
melde('');
|
||
await zustand();
|
||
} catch { /* umgeleitet */ } finally {
|
||
knopf.disabled = false; knopf.textContent = vorher;
|
||
}
|
||
});
|
||
|
||
/* ---------- Verlauf ------------------------------------------------------ */
|
||
|
||
async function verlauf() {
|
||
const ziel = $('verlauf');
|
||
let d;
|
||
try {
|
||
const a = await hole('/workspace/api/verwaltung/protokoll?n=40');
|
||
if (!a.ok) { ziel.textContent = ''; ziel.append(el('p', 'leise', 'Kein Zugriff auf das Protokoll.')); return; }
|
||
d = await a.json();
|
||
} catch { return; }
|
||
|
||
/* Nur die Eintraege, die tatsaechlich aus einer Automation stammen --
|
||
Anmeldungen und Handarbeit gehoeren hier nicht hin. */
|
||
const eintraege = (d.eintraege || d.protokoll || []).filter((e) => ART[e.aktion]).slice(0, 12);
|
||
ziel.textContent = '';
|
||
if (!eintraege.length) {
|
||
ziel.append(el('p', 'leer-hinweis', 'Noch nichts passiert, was von allein lief.'));
|
||
return;
|
||
}
|
||
const liste = el('div', 'verlauf');
|
||
for (const e of eintraege) {
|
||
const z = el('div', 'verlauf__zeile');
|
||
z.append(el('span', 'verlauf__zeit', zeitpunkt(e.zeitpunkt)));
|
||
z.append(el('span', 'verlauf__was', ART[e.aktion]));
|
||
z.append(el('span', 'verlauf__wer', [e.wer, e.detail].filter(Boolean).join(' · ')));
|
||
liste.append(z);
|
||
}
|
||
ziel.append(liste);
|
||
/* Geklappt wird die INNERE Liste, nicht #verlauf -- dessen einziges
|
||
Kind ist diese Liste, da gaebe es nichts zu verbergen. */
|
||
window.klappe?.(liste, $('verlauf-schalter'), $('verlauf-schalter-text'));
|
||
}
|
||
|
||
/* ---------- Start --------------------------------------------------------- */
|
||
|
||
const regeln = $('regeln');
|
||
for (const r of REGELN) {
|
||
const k = el('div', 'regel');
|
||
k.append(el('h3', 'regel__name', r.name));
|
||
k.append(el('p', 'regel__text', r.text));
|
||
regeln.append(k);
|
||
}
|
||
|
||
(async () => {
|
||
let ich;
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Hier stand `catch { return; }`
|
||
-- und damit blieb die Seite bei jedem Netzaussetzer fuer
|
||
immer auf "wird geladen …" stehen. Gemessen auf 13 von 21
|
||
Seiten. `ladefehler()` sagt, was los ist, und bietet einen
|
||
Knopf an, der es noch einmal versucht. */
|
||
window.ladefehler?.();
|
||
return;
|
||
}
|
||
window.werZeigen?.(ich);
|
||
/* Ueber window und mit ?. -- die Funktion steht weiter unten in
|
||
derselben Datei. Als blanker Name haenge die Seite an der
|
||
Reihenfolge des Einlesens; so nicht. */
|
||
window.aufbewahrungLaden?.();
|
||
await Promise.all([kiZustand(), zustand(), hinweise(), verlauf()]);
|
||
})();
|
||
})();
|
||
|
||
/* =====================================================================
|
||
DAS LOESCHKONZEPT (15.09.2026)
|
||
|
||
Drei Sorten Zeile, und die Zahl daneben bedeutet je nach Sorte etwas
|
||
anderes -- deshalb wird sie auch anders beschriftet:
|
||
|
||
raeumen "0 warten" was noch zu tun ist
|
||
fremd "0 warten" dasselbe, nur raeumt eine andere Stelle
|
||
bleibt "13 gespeichert" was absichtlich daliegt
|
||
|
||
Eine Zahl ohne Beschriftung waere hier die schlechteste Loesung:
|
||
"13" unter einer Loeschregel liest sich wie ein Rueckstand.
|
||
===================================================================== */
|
||
(() => {
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (t, k, text) => {
|
||
const n = document.createElement(t);
|
||
if (k) n.className = k;
|
||
if (text !== undefined) n.textContent = text;
|
||
return n;
|
||
};
|
||
|
||
const SCHILD = { raeumen: 'wird gelöscht', fremd: 'wird anderswo gelöscht', bleibt: 'bleibt' };
|
||
|
||
function zeichne(daten) {
|
||
const ziel = $('aufbewahrung');
|
||
if (!ziel) return;
|
||
ziel.textContent = '';
|
||
|
||
for (const f of daten.fristen || []) {
|
||
const z = el('article', 'aufb');
|
||
z.dataset.art = f.art;
|
||
|
||
const kopf = el('div', 'aufb__kopf');
|
||
kopf.append(el('h3', 'aufb__was', f.was));
|
||
kopf.append(el('span', 'aufb__art', SCHILD[f.art] || f.art));
|
||
kopf.append(el('span', 'aufb__frist', f.frist));
|
||
z.append(kopf);
|
||
|
||
if (f.zweck && f.zweck !== '—') z.append(el('p', 'aufb__zeile', 'Wofür: ' + f.zweck));
|
||
z.append(el('p', 'aufb__zeile', f.wirkung));
|
||
z.append(el('p', 'aufb__grund', f.grundlage));
|
||
|
||
/* DIE ZAHL. `null` heisst "konnte nicht nachsehen" und wird auch
|
||
so geschrieben -- nicht als 0. */
|
||
const n = el('p', 'aufb__zahl');
|
||
if (f.anzahl === null || f.anzahl === undefined) {
|
||
n.textContent = 'konnte nicht nachsehen';
|
||
n.dataset.unklar = 'ja';
|
||
} else if (f.art === 'bleibt') {
|
||
n.textContent = `${f.anzahl} gespeichert`;
|
||
} else {
|
||
n.textContent = f.anzahl === 0 ? 'nichts offen' : `${f.anzahl} warten auf das Löschen`;
|
||
if (f.anzahl > 0) n.dataset.offen = 'ja';
|
||
}
|
||
z.append(n);
|
||
ziel.append(z);
|
||
}
|
||
|
||
if (daten.letzte?.zeitpunkt) {
|
||
const d = new Date(daten.letzte.zeitpunkt);
|
||
const getan = (daten.letzte.getan || []).reduce((s, g) => s + g.anzahl, 0);
|
||
ziel.append(el('p', 'aufb__letzte',
|
||
`Zuletzt gelaufen: ${d.toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' })}`
|
||
+ (getan ? ` – ${getan} Datensätze geräumt.` : ' – nichts zu tun.')));
|
||
}
|
||
}
|
||
|
||
window.aufbewahrungLaden = async function () {
|
||
if (!$('aufbewahrung')) return;
|
||
try {
|
||
const a = await fetch('/workspace/api/aufbewahrung', { credentials: 'same-origin' });
|
||
if (!a.ok) { $('aufbewahrung').textContent = ''; return; }
|
||
zeichne(await a.json());
|
||
} catch { /* die Seite ist auch ohne vollstaendig */ }
|
||
};
|
||
|
||
$('aufb-jetzt')?.addEventListener('click', async (e) => {
|
||
const k = e.currentTarget;
|
||
k.disabled = true;
|
||
try {
|
||
await fetch('/workspace/api/aufbewahrung/jetzt',
|
||
{ method: 'POST', credentials: 'same-origin' });
|
||
await window.aufbewahrungLaden();
|
||
} finally { k.disabled = false; }
|
||
});
|
||
})();
|