From 4ac3d8d53d947f55f415881191660ac823f46a44 Mon Sep 17 00:00:00 2001 From: Erik Dunin Date: Mon, 2 Nov 2026 22:08:20 +0100 Subject: [PATCH] =?UTF-8?q?Kommentare=20je=20Gewerk=20klar=20getrennt,=20a?= =?UTF-8?q?bgew=C3=A4hlte=20Gewerke=20ausgeblendet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kommentare mehrerer Gewerke mit Überschrift und Linie: Gewerk in seiner Farbe, rechts Verfasser und Zeit, darunter der Text (Projekt-Dashboard, Projektliste, Wandtafel, Verlauf-Kacheln) - Behoben: Im Projekt-Dashboard erschienen Kommentare von Gewerken, die das Projekt nicht mehr hat (z. B. HLS bei 21-4030); Anzeige und Verlauf zeigen nur noch Gewerke des Projekts - Sitzungswechsel übernimmt nur noch Kommentare der Gewerke des Projekts Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 25 ++++++++++++++++++++ public/App.html | 8 +++---- public/js/board/board.js | 2 +- public/js/plan/plan-info.js | 14 +++++++---- public/js/utils.js | 47 ++++++++++++++++++++++++++++++------- public/styles.css | 19 ++++++++++----- server.js | 4 ++++ 7 files changed, 95 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a4dcd4a..62b0cbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4871,6 +4871,31 @@ Kommentare nach dreimaligem Bereinigen unverändert). `entitiesReparieren()` `project_kommentare.status` und `protokoll_punkte.text` aller Sitzungen über `entitiesEntstapeln()` zurück. +## Behobener Fehler: Kommentare abgewählter Gewerke (11/2026) + +Eriks Meldung zu 21-4030: Im Projekt-Dashboard stand ein HLS-Kommentar, +obwohl das Projekt seit KW 31 nur noch ELT hat. Ursache: Die einmalige +Umstellung `kommentareInAktuelleSitzungUebernehmen()` hatte je Gewerk den +jüngsten früheren Kommentar übernommen, ohne die aktuellen Gewerke des +Projekts zu prüfen; der Sitzungswechsel trug ihn weiter. Jetzt: +`kommentareDesProjekts()` (js/utils.js) — die Gesamtanzeige +(`kommentarVon(p, null)`, `kommentarAnzeigeHtml()`) und der Verlauf im +Projekt-Dashboard (`planKommentarDaten()`, je Sitzung mit DEREN Gewerken) +zeigen nur Gewerke des Projekts; `ensureCurrentWeekRollover()` übernimmt nur +Kommentare dieser Gewerke. Gelöscht wird nichts — die verirrte Kopie fällt +beim nächsten Sitzungswechsel aus der aktuellen Sitzung heraus. + +**Darstellung mehrerer Gewerke „B – Überschrift mit Linie“** (Eriks Wahl +aus dem Artifact „Kommentare je Gewerk“): `kommentarAnzeigeHtml(p, null, +mitMeta)` zeichnet je Gewerk eine Kopfzeile (Gewerk in seiner Farbe +`--gewerk-elt/-hls/-arch`, feine Linie, mit `mitMeta` Verfasser und Zeit) +und darunter den Text; bei nur einem Gewerk wie bisher nur den Text. +Genutzt von der Kachel „Kommentar der Sitzung“ im Projekt-Dashboard (dort +entfällt dann die gemeinsame Verfasserzeile, `kommentarGewerkeMitText()`), +der Kommentaransicht der Projektliste/Wandtafel (`renderKommentarAnsicht()`) +und den Verlauf-Kacheln (ohne Verfasser). Stile `.kommentar-gewerke`, +`.kommentar-kopf` in `styles.css`. + ## Behobene Fehler (Testlauf 10/2026, fünf Test-Agents) - **Gesperrte Sitzungen sind für Nicht-Admins auch inhaltlich unveränderlich**: diff --git a/public/App.html b/public/App.html index 870a9e3..70a3949 100644 --- a/public/App.html +++ b/public/App.html @@ -38,7 +38,7 @@ - + - + diff --git a/public/js/board/board.js b/public/js/board/board.js index 624f86f..928cb29 100644 --- a/public/js/board/board.js +++ b/public/js/board/board.js @@ -748,7 +748,7 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){ // renderKommentarAnsicht: der Kommentar der laufenden Sitzung, nur zu lesen. // Parameter: p (Projekt), locked. Rückgabe: HTML-String. function renderKommentarAnsicht(p, locked){ - const text = sanitizeStatusHtml(kommentarVon(p, kommentarGewerk()).status || ''); + const text = kommentarAnzeigeHtml(p, kommentarGewerk(), true); return `
${text || 'Für diese Sitzung ist noch kein Kommentar erfasst.'}
`; } diff --git a/public/js/plan/plan-info.js b/public/js/plan/plan-info.js index baf3c7a..a83dc6c 100644 --- a/public/js/plan/plan-info.js +++ b/public/js/plan/plan-info.js @@ -93,7 +93,7 @@ for(let i = versionen.length - 1; i >= 0; i--){ const k = kommentarVon(versionen[i].project, null); if(statusPlainText(k.status)){ - letzter = { week: versionen[i].week, k, aktuell: i === versionen.length - 1 }; + letzter = { week: versionen[i].week, k, project: versionen[i].project, aktuell: i === versionen.length - 1 }; break; } } @@ -103,7 +103,8 @@ gewerke.forEach(g => { let vorher = ''; versionen.forEach(v => { - const k = kommentarVon(v.project, g); + // Nur, wenn das Projekt in JENER Sitzung das Gewerk hatte. + const k = (g in kommentareDesProjekts(v.project)) ? kommentarVon(v.project, g) : { status: '' }; const text = statusPlainText(k.status); if(text && text !== vorher && !(letzter && v.week === letzter.week)){ verlauf.push({ week: v.week, gewerk: g, html: sanitizeStatusHtml(k.status), autor: k.statusAuthor || '', at: k.statusAt || '' }); @@ -184,8 +185,9 @@ if(editEl){ editEl.className = 'status-edit is-ansicht'; editEl.contentEditable = 'false'; - // Alle Gewerke zusammen, je mit Gewerk davor (kommentarVon(), js/utils.js). - const text = kom.letzter ? sanitizeStatusHtml(kom.letzter.k.status) : ''; + // Alle Gewerke zusammen; bei mehreren je mit Überschrift, Linie und + // Verfasser (kommentarAnzeigeHtml(), js/utils.js, Eriks Wahl „B“). + const text = kom.letzter ? kommentarAnzeigeHtml(kom.letzter.project, null, true) : ''; editEl.innerHTML = text || 'Zu diesem Projekt ist noch kein Kommentar erfasst.'; } const hinweisEl = document.getElementById('planStatusHinweis'); @@ -200,7 +202,9 @@ const autorEl = document.getElementById('planStatusAutor'); if(autorEl){ const k = kom.letzter ? kom.letzter.k : {}; - autorEl.innerHTML = k.status && k.statusAuthor + // Bei mehreren Gewerken steht der Verfasser schon je Gewerk im Text. + const mehrere = kom.letzter && kommentarGewerkeMitText(kom.letzter.project) > 1; + autorEl.innerHTML = !mehrere && k.status && k.statusAuthor ? `${esc(initialsForName(k.statusAuthor))}${esc(k.statusAuthor)}` + (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '') : ''; diff --git a/public/js/utils.js b/public/js/utils.js index 09ad2e6..1f62aa4 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -385,8 +385,21 @@ function kommentarGewerk(){ // Verfasser/Zeit des jüngsten, besprochen nur, wenn ALLE Gewerke des // Projekts es abgehakt haben. Parameter: p (Projekt), g (Gewerk oder // null/undefined). Rückgabe: { status, statusAuthor, statusAt, besprochen }. -function kommentarVon(p, g){ +// kommentareDesProjekts: die Kommentare nur der Gewerke, die das Projekt hat. +// Ein Kommentar eines Gewerks, das dem Projekt nicht (mehr) gehört — etwa aus +// einer Zeit, als es noch HLS hatte —, gehört nicht in die Gesamtanzeige +// (Eriks Meldung zu 21-4030). Ohne gepflegte Gewerke: alle. Rückgabe: Objekt. +function kommentareDesProjekts(p){ const alle = (p && p.kommentare) || {}; + const gewerke = Array.isArray(p && p.gewerke) ? p.gewerke : []; + if(!gewerke.length) return alle; + const aus = {}; + Object.keys(alle).forEach(k => { if(gewerke.includes(k)) aus[k] = alle[k]; }); + return aus; +} + +function kommentarVon(p, g){ + const alle = g ? ((p && p.kommentare) || {}) : kommentareDesProjekts(p); if(g){ const e = alle[g] || {}; return { status: e.status || '', statusAuthor: e.statusAuthor || '', statusAt: e.statusAt || '', besprochen: !!e.besprochen }; @@ -408,19 +421,37 @@ function kommentarVon(p, g){ } // kommentarAnzeigeHtml: der Kommentar zur ANZEIGE, bereinigt. Mit Gewerk g -// nur dessen Text; ohne Gewerk die Texte aller Gewerke, bei mehreren je mit -// dem Gewerk als eigener, dezenter Beschriftung (.kommentar-gewerk) — -// Systemtext, nicht Teil des Kommentars (anders als die fette "ELT:"-Zeile, -// die kommentarVon() für Klartext-Vorschauen einfügt). Rückgabe: HTML ('' ohne Text). -function kommentarAnzeigeHtml(p, g){ +// nur dessen Text; ohne Gewerk die Texte aller Gewerke des Projekts. Bei +// mehreren Gewerken Darstellung „B – Überschrift mit Linie“ (Eriks Wahl): +// je Gewerk eine Kopfzeile mit dem Gewerk in seiner Farbe, einer feinen Linie +// und (mitMeta) Verfasser und Zeit, darunter der Text — Systemtext, nicht +// Teil des Kommentars (anders als die fette "ELT:"-Zeile, die kommentarVon() +// für Klartext-Vorschauen einfügt). Parameter: p, g, mitMeta. +// Rückgabe: HTML ('' ohne Text). +function kommentarAnzeigeHtml(p, g, mitMeta){ if(g) return sanitizeStatusHtml(kommentarVon(p, g).status || ''); - const alle = (p && p.kommentare) || {}; + const alle = kommentareDesProjekts(p); const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH']; Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); }); const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== ''); if(mitText.length === 0) return ''; if(mitText.length === 1) return sanitizeStatusHtml(alle[mitText[0]].status); - return mitText.map(k => `
${esc(k)}
${sanitizeStatusHtml(alle[k].status)}
`).join(''); + return '
' + mitText.map(k => { + const e = alle[k]; + const zeit = e.statusAt && typeof formatCommentStamp === 'function' ? formatCommentStamp(e.statusAt) : ''; + const meta = mitMeta ? [e.statusAuthor, zeit].filter(Boolean).join(' · ') : ''; + return '
' + + '
' + esc(k) + '' + + '' + (meta ? '' + esc(meta) + '' : '') + '
' + + '
' + sanitizeStatusHtml(e.status) + '
'; + }).join('') + '
'; +} + +// kommentarGewerkeMitText: wie viele Gewerke des Projekts einen Kommentar +// haben (für die Frage, ob eine gemeinsame Verfasserzeile Sinn ergibt). +function kommentarGewerkeMitText(p){ + const alle = kommentareDesProjekts(p); + return Object.keys(alle).filter(k => alle[k] && statusPlainText(alle[k].status) !== '').length; } // kommentarSetzen: ändert den Kommentar-Eintrag eines Gewerks (nur die in diff --git a/public/styles.css b/public/styles.css index d8be91d..7b23298 100644 --- a/public/styles.css +++ b/public/styles.css @@ -4188,12 +4188,19 @@ tr.is-menu-open { background: var(--surface-alt); } min-height: 3em; } .timeline-text p { margin: 0; } -/* Gewerk über dem Kommentar: Systemtext, nicht Teil des Kommentars. */ -.kommentar-gewerk { - font-size: 11px; font-weight: 600; letter-spacing: 0; - color: var(--text-subtle); margin-top: 4px; -} -.kommentar-gewerk:first-child { margin-top: 0; } +/* Kommentare mehrerer Gewerke, Darstellung „B – Überschrift mit Linie“ + (Eriks Wahl): Gewerk in seiner Farbe, feine Linie, Verfasser und Zeit, + darunter der Text. Systemtext, nicht Teil des Kommentars. */ +.kommentar-gewerke { display: flex; flex-direction: column; gap: 16px; } +.kommentar-block { display: flex; flex-direction: column; gap: 4px; } +.kommentar-kopf { display: flex; align-items: center; gap: 12px; } +.kommentar-gewerk { flex: none; font-size: 12px; font-weight: 500; letter-spacing: 0; color: var(--text-muted); } +.kommentar-gewerk.gewerk-elt { color: var(--gewerk-elt); } +.kommentar-gewerk.gewerk-hls { color: var(--gewerk-hls); } +.kommentar-gewerk.gewerk-arch { color: var(--gewerk-arch); } +.kommentar-linie { flex: 1; height: 1px; background: var(--line-soft); min-width: 16px; } +.kommentar-meta { flex: none; font-size: 12px; font-weight: 400; color: var(--text-subtle); white-space: nowrap; } +:root { --gewerk-elt: var(--accent); --gewerk-hls: var(--green); --gewerk-arch: var(--amber); } .timeline-leer { color: var(--text-muted); font-style: normal; } .timeline-lph-list { display: flex; flex-direction: column; gap: 1px; } diff --git a/server.js b/server.js index c4c0d51..41449bc 100644 --- a/server.js +++ b/server.js @@ -2956,8 +2956,12 @@ function ensureCurrentWeekRollover(){ // zur Sitzung und beginnt leer. const vorher = p.kommentare || {}; p.kommentare = {}; + // Nur Gewerke, die das Projekt hat — sonst lebte ein Kommentar eines + // längst abgewählten Gewerks Woche für Woche weiter (21-4030). + const gewerke = Array.isArray(p.gewerke) ? p.gewerke : []; Object.keys(vorher).forEach(g => { const k = vorher[g] || {}; + if(gewerke.length && !gewerke.includes(g)) return; if(!String(k.status || '').trim()) return; p.kommentare[g] = { status: k.status, statusAuthor: k.statusAuthor || '', statusAt: k.statusAt || '', besprochen: false };