diff --git a/docs/SPEC.md b/docs/SPEC.md index e17322e..944d825 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -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]`) diff --git a/frontend/src/app.js b/frontend/src/app.js index 70a3cc9..c62e2f4 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -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(``); 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(`${esc(label)}`); + const riskEl = node.querySelector('.risk'); + if(riskEl){ + const rb = R(riskEl); + parts.push(``); + parts.push(`⚠︎`); + } 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; diff --git a/frontend/src/render.js b/frontend/src/render.js index d551f54..54889ef 100644 --- a/frontend/src/render.js +++ b/frontend/src/render.js @@ -47,8 +47,14 @@ function nodeHtml(n, extra, opts){ const sizeBadge = n.size ? `` : (cheapPath ? `` : ''); + /* 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' + ? `` + : ''; const inner = esc(n.label) + (n.url ? '' : '') + + riskMark + sizeBadge + tagsHtml; const aria = ` aria-label="${attr(nodeAria(n, opts))}"`; diff --git a/frontend/src/style.css b/frontend/src/style.css index f870a9e..5868b10 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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;