Files
dogfather-universe/workspace/assets/js/ampel.js
T
DogFatherGitandClaude Opus 5 1afb38258c Anfuehrungszeichen: drei Loecher in meiner eigenen Wache
Die zweite Sitzung hat in den Praesentationen des Vaults 83 Stellen
gefunden — und damit auf Loecher in MEINER Wache gezeigt. Ich habe
sie nachgemessen, alle drei waren echt.

LOCH 1 — SIE SAH NUR `server/workspace-*.js`

Also ausgerechnet nicht `workspace.js`, die groesste Datei des
Hauses. Der weitere Filter fand dort sofort drei Stellen:

    workspace.js:3404  Kanal „Chat-Moderation"     (Protokoll)
    workspace.js:3422  „${titel}" -> ${haus}       (Protokoll)
    workspace.js:3304  „${… || "ohne Titel"}"      (siehe Loch 3)

Das ist heute der DRITTE Fall von „die Wache kennt nur die halbe
Menge" — nach pruef-struktur (sah nur die Pruefdateien, nicht die
Anwendung) und pruef-fingermass (sah keine variable Breite).

LOCH 2 — IHR AUSDRUCK GILT JE ZEILE

Ein `„`, das erst in der naechsten Zeile mit einem geraden `"`
schliesst, fiel durch. Gemessen: drei echte Faelle, zwei davon
sichtbarer Seitentext.

    workspace/leistung.html       „nicht / zugeordnet"
    workspace/leistung.html       „Backstage-Tabelle / einfuegen"
    workspace/assets/js/ampel.js  „noch ' + 'verbessern"

Beim letzten laeuft das Zitat ueber eine Zeichenketten-Verkettung.

Neue Regel ohne Fehlalarme: erst alle RICHTIG geschlossenen Paare
entfernen (auch mehrzeilig), dann die einzeilig falschen (die meldet
die alte Regel). Was danach noch ein `„` hat und binnen drei Zeilen
ein gerades `"`, ist ein Fund. Ausgenommen bleibt das Zeichen ALS
WERT — in `content: "„";` und in den Sprachtabellen ist das `„` der
Inhalt und das `"` die Begrenzung.

LOCH 3 — EINE AUSNAHME, DIE EINEN FUND VERSCHLUCKT HAT

Gefunden durch die ZAHL, nicht durch den Blick: Der weitere Filter
liess die Ausnahmen von 12 auf 13 steigen. Die dreizehnte war

    `… „${zeile[titelSpalte] || "ohne Titel"}"`

Der Ausdruck blieb am `"` von `"ohne Titel"` haengen; als Inhalt kam
`${zeile[titelSpalte] || ` heraus, das endet auf ein Leerzeichen,
und damit galt „der Satz geht in der naechsten Zeile weiter". Das
echte Schlusszeichen stand hinter dem `}` und war gerade.

Dass die Ausnahmen gezaehlt UND genannt werden, hat ihn gefunden.
Genau dafuer steht die Zeile dort.

Behoben an der Wurzel: Anfuehrungszeichen innerhalb einer Einsetzung
`${…}` werden vorher durch X ersetzt, bei gleicher Laenge und
gleichen Zeilenumbruechen. Das nimmt einer ganzen Sorte
Fehlklassifizierung die Grundlage — die Ausnahmen fielen danach von
13 auf 11.

UND EIN VIERTES, BEIM MESSEN AUFGEFALLEN

`„Van-Van”` in assets/js/data-modis.js schliesst mit U+201D, dem
ENGLISCHEN Zeichen. Zweimal, beide Male in einer DEUTSCHEN Zeile
(`de:` und `"de-CH":`) — nicht einmal eine Uebersetzung, in der es
richtig waere. Eigene Regel dafuer, die keine Ausnahme braucht: Wer
mit `„` oeffnet, schliesst deutsch; in fremdsprachigen Zeilen steht
gar kein `„` am Anfang.

DIE AUSNAHMEZAHL IST JETZT STRENG. Sie stand auf „hoechstens 12" —
damit waere der Anstieg auf 13 nicht aufgefallen. Jetzt genau 11,
wie bei den Grundlinien in pruef-fingermass.

GEPRUEFT: pruef-struktur 68 -> 75 Pruefungen, 0 Fehler. Dazu
leistung-optik 59/0, ampel 47/0, treff 85/0, modi-verborgen 87/0 —
die Seiten, deren Text ich angefasst habe. Steuerzeichen: 830
Dateien, keines. Beide Stempel gesetzt.

Sechs echte Stellen berichtigt, vier neue Gegenproben.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 19:28:38 +02:00

246 lines
9.3 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') {
const ag = await frageNach({
titel: 'Noch verbessern',
was: 'Der Creator sieht diesen Satz an seinem Eintrag.',
bleibt: 'Der Eintrag bleibt stehen – er wird nur als „noch '
+ 'verbessern“ markiert und kann überarbeitet werden.',
ja: 'Zurückgeben',
grund: 'Was genau soll verbessert werden?', grundPflicht: true,
platzhalter: 'z. B. Das Licht ist zu dunkel – bitte heller nachdrehen.',
});
if (!ag) return;
grund = ag.grund;
}
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 (!await frageNach({
titel: 'Nachricht zurücknehmen?',
was: 'Der Text verschwindet für alle, die ihn noch nicht gelesen haben.',
bleibt: 'Wer ihn schon gelesen hat, erinnert sich trotzdem daran.',
endgueltig: true, ja: '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: {} }; }
};
})();