Files
dogfather-universe/workspace/assets/js/automation.js
T
DogFatherGitandClaude Opus 5 1b2874299e Uhr und Glocke in die Begruessung, Teilen in die Leiste, ein echter Fehler weg
Neun Punkte aus Filipes Bildschirmfotos. Der wichtigste war kein
Aussehen, sondern ein Fehler:

UEBEREINANDERLIEGENDE TEXTE IN DER SICHERUNGSLISTE. Das Datum entstand
aus `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 Zeitstempel.
Heraus kam "160012438.02.09.2026", dreimal so lang wie die 96-px-Spalte,
und es lief ueber den Nachbartext. Ein Muster, das Bestandteile ZAEHLT
statt sie zu SUCHEN, bricht beim ersten Namen mit einem Teil mehr. Jetzt
ein Suchmuster nach vier-zwei-zwei Ziffern -- und in der CSS eine
Kuerzung, damit der NAECHSTE zu lange Text nur abgeschnitten wird. Eine
Spalte mit fester Breite ohne Kuerzung ist immer eine Zeitbombe.

DIE BEGRUESSUNGSKACHEL. Runde Digitaluhr rechts: zwei Ringe um dieselbe
Mitte -- innen die Sekunde, aussen der Stand der Stunde. Kein
setInterval(1000): Ein fester Takt laeuft mit der Zeit aus dem Tritt und
ueberspringt Sekunden; gewartet wird bis zur naechsten VOLLEN Sekunde.
Die Glocke ist aus der Kopfleiste hierhergezogen -- mit Rueckfall, denn
zwoelf andere Seiten haben diesen Platz nicht. Nebenbei ist die Leiste
damit um ein Element leichter; sie ist am 06.09. schon einmal an einem
sechsten zerbrochen.

TEILEN-KNOPF in der Leiste, nur fuer Scout, Manager und DogFather.
Geteilt wird der EINGANG, nicht die aktuelle Seite: Ein Link auf
bereich.html?b=schutz schickt jemanden auf eine Seite, die er nicht
sehen darf. Wo es navigator.share gibt, wird es benutzt; sonst
Zwischenablage; wo beides fehlt, erscheint der Knopf gar nicht -- ein
dritter Ausgang statt einer Schaltflaeche, die nichts tut.

WEITER: Kachelreihenfolge Steckbrief -> Profile -> Zahlen. Die
Tagesliste laesst sich zuklappen und zeigt dann SIEBEN Tage (die Woche,
nicht die vier von ueberall sonst). Dialoge, Automationen-Karten,
Sicherungsblock, KI-Kasten und Call-Karten bekommen dasselbe Material
wie die Kacheln -- Leuchtschiene, Materialstaerke, Glanz.

Profilbilder brauchten nichts: Der Weg gibt es fuer jede Rolle bereits
(steckbrief.html fuer die Betreuung, derselbe Block auf profil.html fuer
Creator, Hochladen schreibt immer auf req.person.id).

ZWEI EIGENE FEHLER, beide gemessen statt vermutet:
  - Die Koernung lag in vier neuen Bloecken auf DERSELBEN Ebene wie die
    Spiegelung und erbte deren 50 % Deckkraft. Gemessen rgb(56,44,58)
    statt rgb(20,26,38) -- die Karten sahen durchsichtig aus, obwohl sie
    zu 95 % decken. Derselbe Fehler wie heute Nachmittag an der
    Anmeldekarte. Koernung gehoert auf eine eigene Ebene mit overlay.
  - `.glocke-platz:empty { display: none }` liess die Kachel wachsen,
    sobald die Glocke geladen war. Layout-Sprung von start.html: 0,708.
    Platz wird jetzt reserviert -> 0,473.

pruef-struktur: `-breit` gehoert in die Stufen-Ausnahme. Schaerfe kostet
Bytes (hohe Frequenzen lassen sich nicht wegrechnen), die mittlere Stufe
wuchs auf 230-300 KB. Die Regel bleibt inhaltlich: gross nur, WENN eine
kleinere Stufe daneben steht. Die Verkettung der beiden replace() waere
ein stiller Fehler gewesen -- fuer uhd haette sie nach `-schmal` gesucht.

Gruen: kopf-messen (3), breiten (23), glocke (26), css-klassen (15),
struktur (32), buehne (38), start-ansicht (136), handy (59),
formulare (19), barrierefrei (18), lesbarkeit (14), tempo (8).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 03:40:49 +02:00

370 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===================================================================
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((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(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 { return; }
window.werZeigen?.(ich);
await Promise.all([kiZustand(), zustand(), hinweise(), verlauf()]);
})();
})();