Nach der Korrektur von CPUQuota (50% -> 200%) ist die KI benutzbar: nr_throttled steht bei 0 (vorher 1752 von 1801 Zeitfenstern). Gemessen NACH der Korrektur: To-dos aus einem Protokoll : 12,7 s (vorher: Abbruch nach 45 s) Website waehrend der Arbeit: 0,063 s -- Basis war 0,068 s Kein Einfluss auf die Website, wie bei der ersten Messung mit zwei echten Kernen vorhergesagt. DREI KNOEPFE, alle nach demselben Muster: - Calls: "To-dos vorschlagen" liest, was im Protokoll schon steht, und fuellt LEERE To-do-Zeilen. Bereits Getipptes wird nie ueberschrieben. - Start-Check: "Besser formulieren" macht aus einer Notiz einen Aufgabentitel. Der einfache Vorschlag (erster Satz der Notiz) bleibt daneben bestehen -- er funktioniert auch ohne KI. - Report: "In Worte fassen" fasst die Zahlen in drei Saetzen zusammen. IST DIE KI AUS, GIBT ES DEN KNOPF NICHT. Jede Seite fragt beim Laden einmal nach. Ein Knopf, der immer eine Fehlermeldung bringt, ist schlimmer als gar kein Knopf. Die Absicherung steht in EINER Datei (assets/js/ki.js), nicht dreimal. Der Knopf sagt waehrend der Arbeit "Die KI denkt ..." und der Funke pulst -- zwoelf Sekunden ohne Rueckmeldung fuehlen sich sonst an wie ein Fehler. Optisch bewusst ANDERS als die normalen Knoepfe: gestrichelter Rand statt geschliffener Kante. Ein KI-Knopf tut nichts Endgueltiges, er schlaegt vor -- das soll man auf den ersten Blick sehen. ZWEI VERBESSERUNGEN AUS DEM TEST: 1. Der Bereichsname wurde dem Modell mitgegeben und klebte prompt in der Antwort: "LIVE-Struktur auf 18 und 21 Uhr festlegen". Ohne ihn kommt eine echte Handlung heraus. Der Befund allein reicht -- er steht ja ohnehin im richtigen Bereich. Variable entfernt, kein toter Code. 2. Der Report gab dem Modell "ueberfaellig" ohne Umlaut vor und bekam es genauso zurueck. Jetzt richtig geschrieben. Geprueft: Calls 7 s und drei brauchbare To-dos, Start-Check erzeugt einen Titel, Report drei Saetze, 0 Konsolenfehler.
258 lines
10 KiB
JavaScript
258 lines
10 KiB
JavaScript
/* ===================================================================
|
||
Reports & Review.
|
||
|
||
Die Fragen der Abschnitte stammen woertlich aus dem Konzept, Seite 16:
|
||
"Was wurde erledigt? Was blockiert? Was hat funktioniert? Was kommt
|
||
naechste Woche?"
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const BEREICHSNAME = {
|
||
live: 'LIVE', content: 'Content', technik: 'Technik',
|
||
community: 'Community', schutz: 'Schutz',
|
||
};
|
||
|
||
let ich = null;
|
||
let kiDa = false;
|
||
|
||
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 || ''; };
|
||
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—');
|
||
|
||
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'); }
|
||
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('kein Zugriff'); }
|
||
return a;
|
||
}
|
||
|
||
/* Vorher/Nachher. `gut` sagt, ob ein Anstieg erfreulich ist -- bei
|
||
offenen Problemen ist er das nicht. */
|
||
function trend(jetzt, davor, gut = true) {
|
||
const t = el('span', 'trend');
|
||
t.dataset.gut = gut ? 'ja' : 'nein';
|
||
const diff = jetzt - davor;
|
||
if (!davor && !jetzt) { t.dataset.richtung = 'gleich'; t.textContent = '–'; return t; }
|
||
if (diff === 0) { t.dataset.richtung = 'gleich'; t.textContent = 'unverändert'; return t; }
|
||
t.dataset.richtung = diff > 0 ? 'hoch' : 'runter';
|
||
t.textContent = (diff > 0 ? '▲ +' : '▼ ') + diff + ' ggü. davor';
|
||
return t;
|
||
}
|
||
|
||
function kachel(zahl, name, { trendEl = null, warn = false } = {}) {
|
||
const k = el('div', 'kachel');
|
||
if (warn) k.dataset.warn = 'ja';
|
||
const wert = el('div', 'kachel__wert');
|
||
wert.append(el('span', 'kachel__zahl', String(zahl)));
|
||
if (trendEl) wert.append(trendEl);
|
||
k.append(wert, el('span', 'kachel__name', name));
|
||
return k;
|
||
}
|
||
|
||
function block(titel, frage, inhalt) {
|
||
const b = el('section', 'block');
|
||
b.append(el('h2', 'block__titel', titel));
|
||
if (frage) b.append(el('p', 'block__frage', frage));
|
||
b.append(inhalt);
|
||
return b;
|
||
}
|
||
|
||
/* ---------- Bericht zeichnen ------------------------------------------- */
|
||
|
||
function zeichne(d) {
|
||
const ziel = $('bericht');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
$('unterzeile').textContent =
|
||
`${d.creator_name ? d.creator_name + ' · ' : 'Alle Creator · '}`
|
||
+ `${datum(d.zeitraum.ab)} bis ${datum(d.zeitraum.bis)}`
|
||
+ ` · Vergleich mit den ${d.zeitraum.tage} Tagen davor`;
|
||
|
||
/* Was wurde erledigt? */
|
||
const k1 = el('div', 'kacheln');
|
||
k1.append(kachel(d.aufgaben.erledigt, 'Aufgaben erledigt',
|
||
{ trendEl: trend(d.aufgaben.erledigt, d.aufgaben.erledigt_davor) }));
|
||
k1.append(kachel(d.aufgaben.angelegt, 'neu angelegt',
|
||
{ trendEl: trend(d.aufgaben.angelegt, d.aufgaben.angelegt_davor) }));
|
||
k1.append(kachel(d.termine.gewesen, 'Termine gewesen'));
|
||
k1.append(kachel(d.dateien.freigegeben, 'Dateien freigegeben'));
|
||
ziel.append(block('Was wurde erledigt?', null, k1));
|
||
|
||
/* Was blockiert? */
|
||
const k2 = el('div', 'kacheln');
|
||
k2.append(kachel(d.aufgaben.offen, 'Aufgaben offen'));
|
||
k2.append(kachel(d.aufgaben.ueberfaellig, 'überfällig', { warn: d.aufgaben.ueberfaellig > 0 }));
|
||
const dringend = Object.values(d.bereiche).reduce((s, b) => s + b.dringend, 0);
|
||
k2.append(kachel(dringend, 'dringende Punkte', { warn: dringend > 0 }));
|
||
k2.append(kachel(d.dateien.offen_review, 'Dateien im Review'));
|
||
const b2 = block('Was blockiert?', 'Die drei Aufgaben, die am längsten offen liegen:', k2);
|
||
if (d.aufgaben.aeltester.length) {
|
||
const liste = el('ul', 'liste-klein');
|
||
for (const a of d.aufgaben.aeltester) {
|
||
const li = el('li');
|
||
li.append(document.createTextNode(a.titel));
|
||
li.append(el('span', 'klein',
|
||
`offen seit ${datum(a.erstellt)}${a.frist ? ' · Frist ' + datum(a.frist) : ''}`));
|
||
liste.append(li);
|
||
}
|
||
b2.append(liste);
|
||
} else {
|
||
b2.append(el('p', 'leer-hinweis', 'Nichts offen – sauber.'));
|
||
}
|
||
ziel.append(b2);
|
||
|
||
/* Was hat funktioniert? */
|
||
const k3 = el('div', 'kacheln');
|
||
const jetztS = d.live.jetzt.schnitt;
|
||
const davorS = d.live.davor.schnitt;
|
||
const kLive = kachel(jetztS === null ? '–' : String(jetztS).replace('.', ','),
|
||
`LIVE-Schnitt aus ${d.live.jetzt.anzahl} Bewertung(en)`);
|
||
if (jetztS !== null && davorS !== null) {
|
||
kLive.querySelector('.kachel__wert').append(
|
||
trend(Math.round(jetztS * 10), Math.round(davorS * 10)));
|
||
}
|
||
k3.append(kLive);
|
||
for (const [b, w] of Object.entries(d.bereiche)) {
|
||
k3.append(kachel(w.neu, BEREICHSNAME[b] + ': neue Einträge',
|
||
{ trendEl: trend(w.neu, w.neu_davor) }));
|
||
}
|
||
ziel.append(block('Was hat funktioniert?',
|
||
'Bewegung in den Betreuungsbereichen im Vergleich zum Zeitraum davor:', k3));
|
||
|
||
/* Was kommt als Nächstes? */
|
||
const k4 = el('div', 'kacheln');
|
||
k4.append(kachel(d.termine.kommend, 'Termine stehen an'));
|
||
for (const [b, w] of Object.entries(d.bereiche)) {
|
||
if (w.offen) k4.append(kachel(w.offen, BEREICHSNAME[b] + ': offen',
|
||
{ warn: w.dringend > 0 }));
|
||
}
|
||
const b4 = block('Was kommt als Nächstes?', null, k4);
|
||
if (d.profil && (d.profil.plan_prio1 || d.profil.plan_prio2 || d.profil.plan_prio3)) {
|
||
const liste = el('ul', 'liste-klein');
|
||
for (const [i, t] of [d.profil.plan_prio1, d.profil.plan_prio2, d.profil.plan_prio3].entries()) {
|
||
if (!t) continue;
|
||
const li = el('li');
|
||
li.append(document.createTextNode(t));
|
||
li.append(el('span', 'klein', `Priorität ${i + 1} aus dem 90-Tage-Plan`));
|
||
liste.append(li);
|
||
}
|
||
b4.append(liste);
|
||
}
|
||
if (d.profil?.naechster_review) {
|
||
b4.append(el('p', 'block__frage', 'Nächster Review-Termin: ' + datum(d.profil.naechster_review)));
|
||
}
|
||
ziel.append(b4);
|
||
|
||
$('entscheidung-block').hidden = false;
|
||
|
||
/* Zahlen in Worte -- erst auf Knopfdruck, nie von selbst. Der
|
||
Report ist ohne KI vollstaendig; das hier ist eine Zugabe. */
|
||
if (kiDa) {
|
||
const reihe = el('div', 'ki-reihe');
|
||
reihe.append(window.KI.knopf('In Worte fassen', async () => {
|
||
const zahlen = `Zeitraum ${d.zeitraum.tage} Tage`
|
||
+ `, ${d.aufgaben.erledigt} Aufgaben erledigt (davor ${d.aufgaben.erledigt_davor})`
|
||
+ `, ${d.aufgaben.angelegt} neu angelegt (davor ${d.aufgaben.angelegt_davor})`
|
||
+ `, ${d.aufgaben.offen} offen, ${d.aufgaben.ueberfaellig} überfällig`
|
||
+ `, ${Object.values(d.bereiche).reduce((s, b) => s + b.dringend, 0)} dringende Punkte`
|
||
+ `, LIVE-Schnitt ${d.live.jetzt.schnitt ?? '-'} aus ${d.live.jetzt.anzahl} Bewertungen`
|
||
+ `, ${d.termine.gewesen} Termine gewesen, ${d.termine.kommend} stehen an.`;
|
||
const r = await window.KI.frage('reporttext', { zahlen });
|
||
const alt = reihe.querySelector('.ki-text');
|
||
if (alt) alt.remove();
|
||
const text = el('p', 'ki-text', r.text);
|
||
reihe.append(text);
|
||
}));
|
||
ziel.append(reihe);
|
||
}
|
||
}
|
||
|
||
async function laden() {
|
||
melde('');
|
||
const creator = $('f-creator').value || 'alle';
|
||
const tage = $('f-tage').value;
|
||
try {
|
||
const a = await hole(`/workspace/api/report?creator=${encodeURIComponent(creator)}&tage=${tage}`);
|
||
if (!a.ok) { melde('Report konnte nicht erstellt werden.'); return; }
|
||
zeichne(await a.json());
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ---------- Entscheidung ------------------------------------------------ */
|
||
|
||
$('entscheidung').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
$('e-fehler').textContent = '';
|
||
const knopf = $('e-speichern');
|
||
knopf.disabled = true;
|
||
try {
|
||
const a = await hole('/workspace/api/report/entscheidung', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
titel: $('e-titel').value,
|
||
frist: $('e-frist').value,
|
||
creator_id: $('f-creator').value || null,
|
||
}),
|
||
});
|
||
if (!a.ok) {
|
||
$('e-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.';
|
||
return;
|
||
}
|
||
$('entscheidung').reset();
|
||
$('e-stand').textContent = 'Als Aufgabe angelegt ✓';
|
||
setTimeout(() => { $('e-stand').textContent = ''; }, 3200);
|
||
await laden();
|
||
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
|
||
});
|
||
|
||
/* ---------- Start -------------------------------------------------------- */
|
||
|
||
(async () => {
|
||
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);
|
||
kiDa = await window.KI.verfuegbar();
|
||
|
||
try {
|
||
const { creator } = await (await hole('/workspace/api/report/creator')).json();
|
||
if (ich.rolle === 'admin') {
|
||
$('creator-block').hidden = false;
|
||
const alle = document.createElement('option');
|
||
alle.value = ''; alle.textContent = 'Alle Creator';
|
||
$('f-creator').append(alle);
|
||
} else if (ich.rolle === 'scout') {
|
||
/* Ein Scout waehlt zwischen SEINEN Creator -- "alle" gibt es
|
||
fuer ihn nicht, das waere die Gesamtuebersicht. */
|
||
if (!creator.length) {
|
||
$('unterzeile').textContent =
|
||
'Dir ist noch kein Creator zugeteilt. DogFather legt fest, wen du betreust.';
|
||
return;
|
||
}
|
||
$('creator-block').hidden = false;
|
||
}
|
||
for (const c of creator) {
|
||
const o = document.createElement('option');
|
||
o.value = String(c.id); o.textContent = c.name;
|
||
$('f-creator').append(o);
|
||
}
|
||
if (ich.rolle === 'creator') $('f-creator').value = String(ich.id);
|
||
} catch { return; }
|
||
|
||
$('f-creator').addEventListener('change', laden);
|
||
$('f-tage').addEventListener('change', laden);
|
||
await laden();
|
||
})();
|
||
})();
|