Filipe hatte recht, und zwar zweifach: Der Husky war der FALSCHE (ein fremder Neon-Husky aus dem alten Bilderordner statt DogFather), und zwei in die Ecken geklebte Bilder ergeben noch kein Bild. DIE BUEHNE, zweiter Anlauf. Aus den zehn geschickten Bildern das Studio gewaehlt, in dem HasiDog und DogFather stehen -- nicht aus Geschmack, sondern wegen der ANORDNUNG: Die beiden stehen weit aussen, die Mitte ist dunkel. Genau dort steht der Inhalt. Bei den Bildern mit den Figuren in der Mitte waeren sie hinter dem Text gelandet. Ueber die ganze Flaeche, fest an der Scheibe, 138 % breit: Bei 100 % staenden die beiden bei 20 % und 78 % der Breite -- mitten unter der Textspalte. Vergroessert man das Bild ueber die Scheibe hinaus, wandern sie nach aussen (rund 9 % und 88 %) und der leere Hallenboden liegt dort, wo gelesen wird. Ein Bild groesser zu machen, damit WENIGER davon im Weg ist, klingt verkehrt und ist genau richtig. Fuer schmale Schirme ein eigener Hochkant-Ausschnitt derselben Szene -- ein auf 9:16 gequetschtes Breitbild zeigt nur noch Wand. 1,9 MB PNG -> 28 KB WebP. DIE VIER STELLEN aus den Bildschirmfotos: * Kopfleiste: der DogFather-Kopf davor. Als MASKE aus dem Logo gerechnet (Dunkelheit wird Deckkraft), nicht als Bild -- so laesst er sich im Stil einfaerben; ein fertig eingefaerbtes Bild muesste man fuer jede Farbe neu bauen. 114 KB -> 3 KB. * "Dogfather · DogFather" war der peinlichste Punkt: Name und Rolle lasen sich gleich, es sah nach einem Fehler aus. Jetzt drei unterscheidbare Dinge -- Zeichen mit dem Anfangsbuchstaben, Name, Rolle als Marke in ihrer Farbe. Gebaut in kopf.js, EINMAL statt vierzehnmal: Jede Seitendatei setzte diese Zeile bisher selbst zusammen. * Begruessung: leuchtender Strich, groesserer Gruss, auslaufende Trennlinie. Dieselben Striche vor "WAS IST DRAN" und "DEINE AUFGABEN" -- so gehoert sichtbar zusammen, was zusammengehoert. ZWEI EIGENE FEHLER, die die Pruefungen gefunden haben: 1. Ich hatte Verlaeufe IM TEXT gebaut (background-clip mit durchsichtiger Schrift). Sah gut aus, und der Kontrasttest meldete 1,05:1 -- zu Recht. Durchsichtige Schrift haengt an einer einzigen Technik; faellt sie aus (Kontrastmodus, Druck, aeltere Browser), ist der Text UNSICHTBAR statt nur anders gefaerbt. Das ist kein Schoenheitsfehler, das ist ein Ausfall. Jetzt feste Farbe mit einem Hauch Leuchten. 2. Auf dem Handy stand der Abmelden-Knopf 9 px aus dem Bild. Die Suche nach der Ursache ging zuerst in die Irre -- der Test nannte das Wasserzeichen, das aber von seiner Kachel sauber abgeschnitten wird und nur seine Masse meldet. Gemessen war es die Marke: 209 px + 139 px rechte Gruppe passten nicht in 390 px. Auf dem Handy bleibt jetzt nur der Kopf stehen; der Markenzug steht zwei Zeilen tiefer ohnehin als Ueberschrift. Fuer Vorleseprogramme bleibt er im Dokument. Und einmal habe ich mir beim Ersetzen eines CSS-Blocks das halbe Stylesheet geloescht (die Kachel-Regeln lagen zwischen den beiden Suchmarken). Aufgefallen sofort am Bildschirmfoto, zurueckgeholt aus Git, danach zeilengenau ersetzt. 118 Pruefungen, alle gruen. Der Kontrast wird weiterhin an echten Bildpunkten gemessen, jetzt an bis zu 35 Stellen je Seite. Co-Authored-By: Claude Opus 5 <[email protected]>
350 lines
16 KiB
JavaScript
350 lines
16 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((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;
|
||
}
|
||
for (const s of z.sicherungen) {
|
||
const zeile = el('div', 'verlauf__zeile');
|
||
zeile.append(el('span', 'verlauf__zeit', s.name.replace(/\.db$/, '').split('-').slice(1).reverse().join('.')));
|
||
zeile.append(el('span', 'verlauf__was', s.name.split('-')[0]));
|
||
zeile.append(el('span', 'verlauf__wer', bytes(s.groesse)));
|
||
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()]);
|
||
})();
|
||
})();
|