Filipe: "da sind aber noch zwei kategorien zu". Er hat recht, und es waren zwei VERSCHIEDENE Fehler. 1) "DASHBOARD" war eine Kachel, die man nicht oeffnen kann -- man steht ja schon darauf. Sie stand seit dem ersten Tag als Platzhalter da. Ein Eintrag, der nirgendwohin fuehrt, sieht aus wie etwas Unfertiges. Entfernt. 2) "AUTOMATIONEN" trug noch "Phase 4", obwohl die Automationen laengst liefen -- sie waren nur verteilt (Hinweise auf der Uebersicht, Suche ueberall, KI-Vorschlaege an drei Stellen) und hatten keinen Ort. Jetzt haben sie einen. DIE SEITE FUEHRT BEWUSST NICHTS EIGENES. Was dort steht, kommt aus den Bereichen selbst -- eine Seite, die Automationen doppelt abbildet, waere die naechste Stelle, die irgendwann etwas anderes behauptet als die Wirklichkeit. Vier Abschnitte: - Die lokale KI mit Zustand, Modell, Ort, Kosten und Grenzen -- und dem einzigen Schalter der Seite. Sie ist die einzige Automation, die Rechenzeit kostet, die der Website gehoert. - "Was gerade anliegt": dieselben Hinweise wie auf der Uebersicht, hier vollstaendig statt gekuerzt. - "Laeuft dauerhaft im Hintergrund": sieben Regeln im Klartext. Jede beschreibt etwas, das tatsaechlich im Code steht. - "Zuletzt automatisch passiert": aus dem Protokoll, gefiltert auf das, was ohne Zutun geschah. DER KI-SCHALTER WIRD AN ZWEI STELLEN GEPRUEFT: beim Anzeigen der Knoepfe UND in jedem Endpunkt. Eine Oberflaeche, die etwas versteckt, ist keine Sperre. Geprueft: abgeschaltet liefert der Endpunkt 403, ein Creator darf gar nicht schalten (403), die Einstellung ueberdauert einen Neustart (eigene Tabelle, damit sie dieselbe Sicherung bekommt wie alles andere). NEBENBEI DERSELBE FEHLER WIE FRUEHER: Der Schalter-Baustein lag in wissen.css und fehlte prompt auf der neuen Seite -- dort erschien er als nacktes Kaestchen. Wie damals bei .knopf-still. Liegt jetzt in gate.css. Geprueft: 17 Kacheln, KEINE mehr ohne Ziel. Creator wird von der Seite weggeleitet (302). Handy ohne Ueberlauf, 0 Konsolenfehler.
207 lines
9.0 KiB
JavaScript
207 lines
9.0 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.' },
|
||
];
|
||
|
||
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);
|
||
}
|
||
|
||
/* ---------- 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);
|
||
}
|
||
|
||
/* ---------- 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; }
|
||
$('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle);
|
||
await Promise.all([kiZustand(), hinweise(), verlauf()]);
|
||
})();
|
||
})();
|