frontend: High-Risk-Warndreieck + Prod-Gating der Test-Hilfen

- [!]-Knoten trägt jetzt ein Warndreieck (⚠) an der oberen linken Ecke,
  zusätzlich zur orangen Färbung — im Diagramm, im SVG/PNG-Export und
  barrierefrei (aria-hidden, Status steckt im aria-label). Tooltip in allen
  9 Sprachen.
- SPEC §4: [!]/highrisk-Status nachgetragen (war beim ersten Hinzufügen
  übersprungen), inkl. Warndreieck-Kennzeichnung.
- Beispiel-PSP: Backend als High-Risk ([!]) markiert.
- Debug-Panel + Reset-Button nur außerhalb des Prod-Builds (isProdBuild via
  VITE_BUILD_BADGE); im build:prod eliminiert esbuild den Zweig als toten
  Code. Update-Check + Banner bleiben in allen Builds aktiv.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-23 10:00:51 +02:00
co-authored by Claude Opus 4.8
parent 90b5deb45b
commit 095dbfb63e
4 changed files with 50 additions and 5 deletions
+7
View File
@@ -59,9 +59,16 @@ Codiert als Checkbox nach dem Zeichen (Erweiterung der Markdown-Task-Syntax):
| `[x]` | fertig | fertig | abgeschlossen | `#DCF1DE` (pastellgrün) | `#86C293` |
| `[^]` | prod | in Produktion | deployed/live | `#DBEAF8` (pastellblau) | `#85ACD7` |
| `[-]` | verworfen | verworfen | bewusst nicht weiterverfolgt | `#F1F2F4`, gestrichelter Rahmen, Text durchgestrichen | `#B3BAC2` |
| `[!]` | highrisk | High Risk | Aufwand noch unklar, hohes Risiko | `#FFE5CC` (orange) | `#F97316` |
- Ohne Statusbox: neutraler Knoten (weiß).
- `x` auch als `X` zulässig.
- **High-Risk-Kennzeichnung:** Der `[!]`-Knoten trägt zusätzlich zur orangen
Färbung ein **Warndreieck** (⚠, in `#F97316`) als kleines Badge an der oberen
linken Ecke — Tooltip „High Risk Aufwand noch unklar." Das Badge erscheint
auch im Grafikexport. (Mnemonik: `!` = Achtung. Ob `[!]` später zusätzlich zum
Fertigstellungsstatus stehen darf, ist offen; vorerst ist es ein eigener
Status wie die übrigen.)
- Verworfene Knoten (inkl. Teilbaum) sind per Default **ausgeblendet**;
Toggle „verworfene einblenden“ zeigt sie.
- **Fehlertoleranz:** Ein unbekanntes Zeichen in der Statusbox (z. B. `[z]`)
+28 -5
View File
@@ -16,7 +16,7 @@ const INITIAL = `%% Project structure Sprint 14
- [/] Web (S)
- [ ] Android (M)
- [ ] iOS (M)
- [ ] Backend (L) @ben @carla
- [!] Backend (L) @ben @carla
- [ ] CMS integration (M)
| [ ] WordPress
| [?] Headless CMS
@@ -195,10 +195,16 @@ function diagramToSvg(){
const dashed = cs.borderTopStyle === 'dashed';
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="${cs.backgroundColor}" stroke="${cs.borderTopColor}" stroke-width="${parseFloat(cs.borderTopWidth)||1.5}"${dashed?' stroke-dasharray="4 3"':''}/>`);
const clone = node.cloneNode(true);
clone.querySelectorAll('.size,.tags,.ext').forEach(e => e.remove());
clone.querySelectorAll('.size,.tags,.ext,.risk').forEach(e => e.remove());
const label = clone.textContent.replace(/\s+/g,' ').trim();
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
const riskEl = node.querySelector('.risk');
if(riskEl){
const rb = R(riskEl);
parts.push(`<circle cx="${rb.cx.toFixed(1)}" cy="${rb.cy.toFixed(1)}" r="${(Math.min(rb.w,rb.h)/2).toFixed(1)}" fill="#ffffff" stroke="#F97316" stroke-width="1.5"/>`);
parts.push(`<text x="${rb.cx.toFixed(1)}" y="${(rb.cy+3.5).toFixed(1)}" text-anchor="middle" fill="#F97316" font-size="10">⚠︎</text>`);
}
const sizeEl = node.querySelector('.size');
if(sizeEl) drawBadge(sizeEl, '#0F766E', '#ffffff');
node.querySelectorAll('.tag').forEach(tg => {
@@ -559,6 +565,7 @@ const I18N = {
privacy:"Datenschutz",
legendTooltip:"Legende ein-/ausblenden",
ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.",
riskTooltip:"High Risk Aufwand noch unklar.",
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
cheapTooltip:"Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück",
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung als M angenommen",
@@ -599,6 +606,7 @@ const I18N = {
privacy:"Privacy",
legendTooltip:"Show/hide legend",
ghostTooltip:"From size M upward, an item should be broken down further.",
riskTooltip:"High risk effort still unclear.",
discardedTooltip:"Show/hide discarded nodes and their subtree",
cheapTooltip:"Highlight the cheapest path unneeded alternatives recede",
implicitSizeTooltip:"No size given assumed as M for the cost estimate",
@@ -639,6 +647,7 @@ const I18N = {
privacy:"Privacidad",
legendTooltip:"Mostrar u ocultar la leyenda",
ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.",
riskTooltip:"Alto riesgo esfuerzo aún incierto.",
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
implicitSizeTooltip:"Sin tamaño indicado: se asume M para el cálculo de costes",
@@ -679,6 +688,7 @@ const I18N = {
privacy:"Confidentialité",
legendTooltip:"Afficher/masquer la légende",
ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.",
riskTooltip:"Risque élevé effort encore incertain.",
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
cheapTooltip:"Mettre en évidence le chemin le moins coûteux les alternatives inutiles s'estompent",
implicitSizeTooltip:"Aucune taille indiquée considérée comme M pour l'estimation des coûts",
@@ -719,6 +729,7 @@ const I18N = {
privacy:"Prywatność",
legendTooltip:"Pokaż/ukryj legendę",
ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.",
riskTooltip:"Wysokie ryzyko nakład jeszcze niejasny.",
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
cheapTooltip:"Wyróżnij najtańszą ścieżkę niepotrzebne alternatywy są przygaszone",
implicitSizeTooltip:"Nie podano rozmiaru przyjęto M do szacowania kosztów",
@@ -759,6 +770,7 @@ const I18N = {
privacy:"Конфиденциальность",
legendTooltip:"Показать/скрыть легенду",
ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.",
riskTooltip:"Высокий риск – оценка ещё не ясна.",
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
@@ -799,6 +811,7 @@ const I18N = {
privacy:"गोपनीयता",
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।",
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
@@ -844,6 +857,7 @@ const I18N = {
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
ghostTooltip:"从 M 号起,元素应进一步细分。",
riskTooltip:"高风险 – 工作量尚不明确。",
editorTitle:"结构(文本)", diagramTitle:"图表",
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
@@ -884,6 +898,7 @@ const I18N = {
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
riskTooltip:"高リスク – 規模はまだ不明。",
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
@@ -1194,6 +1209,13 @@ mountBuildBadge();
})();
/* ---------- Update-Detection (Client-seitig, einfach & zuverlässig) ---------- */
/* Test-Hilfen (Debug-Panel + Reset-Button) nur AUSSERHALB des Prod-Builds:
im Prod-Build (`build:prod` -> VITE_BUILD_BADGE==='none') eliminiert esbuild
den Zweig als toten Code — analog zum Build-Badge (D16). Dev-Server (🔧) und
Default-/Pages-Build (🚧) behalten sie. Der Update-Check + Banner bleibt in
ALLEN Builds aktiv. */
const isProdBuild = import.meta.env.VITE_BUILD_BADGE === 'none';
async function checkForUpdates(){
try {
/* Fetch HTML mit Cache-Busting via Timestamp */
@@ -1255,8 +1277,9 @@ function logUpdate(msg){
localStorage.setItem('werkbaum-update-log', log.join('\n'));
}
/* Debug-Panel anzeigen (nur wenn localStorage Update-Log da ist) */
/* Debug-Panel anzeigen (Test-Hilfe; im Prod-Build unterdrückt) */
function showUpdateDebug(){
if(isProdBuild) return;
let panel = document.getElementById('updateDebugPanel');
if(!panel){
panel = document.createElement('div');
@@ -1286,8 +1309,8 @@ function showUpdateDebug(){
panel.textContent = log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge';
}
/* Reset-Button im Header neben Fullscreen */
document.addEventListener('DOMContentLoaded', () => {
/* Reset-Button im Header neben Fullscreen (Test-Hilfe; im Prod-Build weggelassen) */
if(!isProdBuild) document.addEventListener('DOMContentLoaded', () => {
const fsToggle = document.getElementById('fsToggle');
if(!fsToggle) return;
+6
View File
@@ -47,8 +47,14 @@ function nodeHtml(n, extra, opts){
const sizeBadge = n.size
? `<span class="size" aria-hidden="true">${n.size}</span>`
: (cheapPath ? `<span class="size implicit" aria-hidden="true" title="${implicitTip}">M</span>` : '');
/* High-Risk: Warndreieck (⚠, Textpräsentation via VS15) an der oberen linken
Ecke. aria-hidden — die Information steckt bereits im Status des aria-label. */
const riskMark = n.status && n.status.key === 'highrisk'
? `<span class="risk" aria-hidden="true" title="${attr(t('riskTooltip'))}">⚠︎</span>`
: '';
const inner = esc(n.label) +
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
riskMark +
sizeBadge +
tagsHtml;
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
+9
View File
@@ -393,6 +393,15 @@
border:1.5px solid var(--card);
border-radius:999px;padding:1px 6px;line-height:1.4;
}
.risk{
position:absolute;top:-10px;left:-10px;
width:18px;height:18px;
display:flex;align-items:center;justify-content:center;
font-size:.62rem;line-height:1;
background:var(--card);color:#F97316;
border:1.5px solid #F97316;border-radius:999px;
box-shadow:0 0 0 1.5px var(--card);
}
.tags{
position:absolute;right:-9px;bottom:-9px;
display:flex;gap:3px;