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]>
246 lines
9.3 KiB
JavaScript
246 lines
9.3 KiB
JavaScript
/* ===================================================================
|
||
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: {} }; }
|
||
};
|
||
})();
|