Files
dogfather-universe/workspace/assets/js/ampel.js
T
DogFatherGitandClaude Opus 5 461d41943a Keine Maschinensprache mehr auf dem Bildschirm
Filipe: "keine kryptischen oder technischen fehlermeldungen, sondern
klare aussagen darueber, was passiert ist und wie man das problem
loesen kann."

DAS PROBLEM, NACHGEMESSEN.

Der Server antwortet im Fehlerfall mit { fehler: "..." }. Darin stehen
ZWEI verschiedene Dinge, und von aussen sehen sie gleich aus:
Maschinenkennungen (nicht_verfuegbar, nicht_gefunden, ungueltig -- 43
verschiedene) und fertige deutsche Saetze.

An 84 Stellen stand `textContent = d.fehler` -- ungefiltert. Wer beim
Hochladen einer Datei Pech hatte, las woertlich "nicht_verfuegbar" auf
dem Bildschirm und wusste nicht einmal, ob er selbst schuld war.

Gezaehlt: 474 Antworten mit "nicht_verfuegbar", 392 mit
"nicht_gefunden", 125 mit "ungueltig".

DREI AUSGAENGE, NICHT ZWEI.

`sagWas()` in workspace/assets/js/meldung.js:
  bekannte Kennung   -> ihr Satz
  unbekannte Kennung -> der Ersatzsatz der Stelle, NIE die Kennung
                        (sie geht in die Konsole, wo sie jemandem
                        auffaellt, der sie beheben kann)
  fertiger Satz      -> unveraendert durch

Erkannt wird eine Kennung daran, dass sie nur aus Kleinbuchstaben,
Ziffern und Unterstrichen besteht. Ein deutscher Satz hat immer
Leerzeichen; eine Kennung nie. Die Unterscheidung ist entschieden,
nicht geraten.

UND WARUM DIE TABELLE NICHT ALTERN KANN.

Eine von Hand gepflegte Liste ist hier schon zweimal teuer geworden
(die abgeschriebene Spaltenliste, die feste Umbruchschwelle). Deshalb
rechnet pruef-meldungen.mjs die Kennungen AUS DEM SERVER aus statt sie
zu kennen -- eine neue ohne Satz macht sie rot. Man kann es nicht mehr
vergessen.

Sie prueft drei Dinge und beweist zu jedem, dass sie auch "nicht in
Ordnung" sagen kann:
  1. vollstaendig -- jede Kennung hat einen Satz
  2. angeschlossen -- keine Rohanzeige, und jede Seite, deren Skript
     sagWas benutzt, laedt auch meldung.js
  3. lesbar -- kein Satz enthaelt Kennung, Zahl oder englisches Wort

GEFUNDEN HAT SIE SOFORT EINEN ECHTEN FEHLER: leistung.html laedt seine
Skripte ohne `defer` und fiel damit durch meinen Einbau. Dort waere
sagWas nicht definiert gewesen -- aus einer Fehlermeldung waere ein
Absturz geworden, also schlimmer als vorher.

NEBENBEFUND, DER MICH FAST EINE FALSCHE MELDUNG GEKOSTET HAETTE:
"alter_offen" heisst NICHT "etwas Aelteres ist offen", sondern
"Altersbestaetigung steht noch aus" (workspace.js:4864). Geraten
haette ich es falsch uebersetzt.

GEMESSEN: pruef-meldungen 8/0, alle 46 Skripte syntaktisch in Ordnung.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 14:46:13 +02:00

233 lines
8.6 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.
/* ===================================================================
Ampel und Rückmeldungen an einem Eintrag.
Zwei Dinge, bewusst getrennt gehalten:
DIE AMPEL ist eine Beurteilung. Sie gehört dem Betreuer.
Ein Creator sieht sie, kann sie aber nicht setzen --
sonst stünde alles auf grün.
DIE NACHRICHT ist ein Gespräch. Sie gehört beiden. Ein Creator,
der auf eine Bewertung nicht antworten kann, bekommt
ein Urteil statt einer Betreuung.
Drei Stufen, nicht fünf. "Reicht das schon?" hat drei ehrliche
Antworten: ja, noch nicht, noch nicht angesehen. Eine Zahl von 1 bis 5
klingt genauer und ist es nicht -- am Ende steht überall die 3.
=================================================================== */
(() => {
'use strict';
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
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;
}
const wann = (iso) => (iso
? new Date(iso).toLocaleString('de-DE',
{ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: '');
const STUFEN = [
['gut', 'Passt so'],
['verbessern', 'Noch verbessern'],
];
/** Hängt Ampel und Rückmeldungen an eine Eintragskarte.
* karte: das Element, an das angehängt wird
* id: die Nummer des Eintrags
* vorschau: { ampel, anzahl } aus der Sammelabfrage -- damit die
* Liste nicht für jeden Eintrag einzeln nachfragt */
window.ampelAnhaengen = function (karte, id, vorschau = {}) {
const zeile = el('div', 'ampel');
const stufe = vorschau.ampel?.stufe || 'offen';
zeile.dataset.stufe = stufe;
/* Der Zustand, immer sichtbar -- auch für den Creator. Das ist der
ganze Sinn: Er soll sehen, wo er steht, ohne fragen zu müssen. */
const marke = el('span', 'ampel__marke',
stufe === 'gut' ? 'Passt so'
: stufe === 'verbessern' ? 'Noch verbessern' : 'Noch nicht angesehen');
zeile.append(marke);
if (vorschau.ampel?.von_name) {
zeile.append(el('span', 'ampel__von',
`${vorschau.ampel.von_name} · ${wann(vorschau.ampel.am)}`));
}
/* Die Begründung steht DANEBEN, nicht versteckt. "Noch verbessern"
ohne Grund ist nicht umsetzbar, nur entmutigend. */
if (vorschau.ampel?.begruendung) {
zeile.append(el('p', 'ampel__grund', vorschau.ampel.begruendung));
}
const auf = el('button', 'schritt',
vorschau.anzahl ? `Rückmeldung (${vorschau.anzahl})` : 'Rückmeldung');
auf.type = 'button';
auf.addEventListener('click', () => oeffnen(karte, id, auf));
zeile.append(auf);
karte.append(zeile);
};
/* ---------- Der ausklappbare Bereich ---------------------------------- */
async function oeffnen(karte, id, knopf) {
const vorhanden = karte.querySelector('.rueckmeldung');
if (vorhanden) { vorhanden.remove(); knopf.dataset.offen = ''; return; }
knopf.dataset.offen = 'ja';
let daten;
try {
const a = await hole(`/workspace/api/eintrag/${id}/rueckmeldung`);
if (!a.ok) return;
daten = await a.json();
} catch { return; }
const kasten = el('div', 'rueckmeldung');
/* ---- Bewerten: nur die Betreuung ---- */
if (daten.darf_bewerten) {
const b = el('div', 'rueckmeldung__bewerten');
b.append(el('p', 'feldschild', 'Bewertung'));
const knoepfe = el('div', 'ampel__wahl');
for (const [wert, name] of STUFEN) {
const k = el('button', 'ampel__knopf', name);
k.type = 'button';
k.dataset.stufe = wert;
if (daten.ampel?.stufe === wert) k.dataset.an = 'ja';
k.addEventListener('click', async () => {
let grund = daten.ampel?.begruendung || '';
/* "Noch verbessern" verlangt einen Grund -- der Server lehnt
es sonst ab, und das ist richtig so. Hier wird deshalb
gleich danach gefragt statt eine Fehlermeldung zu zeigen. */
if (wert === 'verbessern') {
grund = prompt('Was genau soll verbessert werden?\n\n'
+ 'Ohne diesen Satz kann der Creator nichts damit anfangen.', grund) || '';
if (grund.trim().length < 5) return;
}
k.disabled = true;
try {
const a = await hole(`/workspace/api/eintrag/${id}/ampel`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe: wert, begruendung: grund }),
});
if (!a.ok) {
alert(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.'));
k.disabled = false;
return;
}
/* Die Karte neu aufbauen lassen -- die Seite weiss selbst
am besten, wie sie aussieht. */
document.dispatchEvent(new CustomEvent('ampel-geaendert', { detail: { id } }));
} catch { k.disabled = false; }
});
knoepfe.append(k);
}
b.append(knoepfe);
kasten.append(b);
} else {
kasten.append(el('p', 'rueckmeldung__hinweis',
'Die Bewertung setzt deine Betreuung. Hier kannst du antworten und Fragen stellen.'));
}
/* ---- Das Gespräch ---- */
const verlauf = el('div', 'nachrichten');
zeichneNachrichten(verlauf, daten.nachrichten, daten.ich, id);
kasten.append(verlauf);
/* ---- Schreiben: JEDER Beteiligte ---- */
const form = el('form', 'nachricht-neu');
const feld = el('textarea');
feld.rows = 2;
feld.maxLength = 2000;
feld.placeholder = daten.darf_bewerten
? 'Rückmeldung an den Creator …'
: 'Frage oder Antwort an deine Betreuung …';
form.append(feld);
const senden = el('button', 'knopf knopf--klein', 'Senden');
senden.type = 'submit';
form.append(senden);
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!feld.value.trim()) return;
senden.disabled = true;
try {
const a = await hole(`/workspace/api/eintrag/${id}/nachricht`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: feld.value }),
});
if (!a.ok) {
alert(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.'));
return;
}
const { nachrichten } = await a.json();
feld.value = '';
zeichneNachrichten(verlauf, nachrichten, daten.ich, id);
} catch { /* umgeleitet */ } finally { senden.disabled = false; }
});
kasten.append(form);
karte.append(kasten);
}
function zeichneNachrichten(ziel, liste, ichId, eintragId) {
ziel.textContent = '';
if (!liste.length) {
ziel.append(el('p', 'nachrichten__leer', 'Noch keine Rückmeldung.'));
return;
}
for (const n of liste) {
const z = el('div', 'nachricht');
if (n.von === ichId) z.dataset.eigen = 'ja';
z.dataset.rolle = n.von_rolle || '';
const kopf = el('div', 'nachricht__kopf');
kopf.append(el('span', 'nachricht__von', n.von_name || 'Unbekannt'));
kopf.append(el('span', 'nachricht__wann', wann(n.am)));
/* Nur die eigene lässt sich zurücknehmen. Fremde zu löschen hieße,
ein Gespräch nachträglich umzuschreiben. */
if (n.von === ichId) {
const weg = el('button', 'nachricht__weg', '×');
weg.type = 'button';
weg.title = 'Eigene Nachricht zurücknehmen';
weg.addEventListener('click', async () => {
if (!confirm('Diese Nachricht zurücknehmen?')) return;
try {
const a = await hole(`/workspace/api/eintrag/${eintragId}/nachricht/${n.id}`,
{ method: 'DELETE' });
if (a.ok) z.remove();
} catch { /* umgeleitet */ }
});
kopf.append(weg);
}
z.append(kopf);
z.append(el('p', 'nachricht__text', n.text));
ziel.append(z);
}
}
/** Holt Ampeln und Nachrichtenzahlen für eine ganze Liste auf einmal.
* Zwanzig Einträge einzeln abzufragen wären zwanzig Anfragen. */
window.ampelnHolen = async function (ids) {
if (!ids.length) return { ampeln: {}, anzahlNachrichten: {} };
try {
const a = await hole('/workspace/api/eintrag/ampeln?ids=' + ids.join(','));
if (!a.ok) return { ampeln: {}, anzahlNachrichten: {} };
return await a.json();
} catch { return { ampeln: {}, anzahlNachrichten: {} }; }
};
})();