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:
co-authored by
Claude Opus 4.8
parent
90b5deb45b
commit
095dbfb63e
@@ -59,9 +59,16 @@ Codiert als Checkbox nach dem Zeichen (Erweiterung der Markdown-Task-Syntax):
|
|||||||
| `[x]` | fertig | fertig | abgeschlossen | `#DCF1DE` (pastellgrün) | `#86C293` |
|
| `[x]` | fertig | fertig | abgeschlossen | `#DCF1DE` (pastellgrün) | `#86C293` |
|
||||||
| `[^]` | prod | in Produktion | deployed/live | `#DBEAF8` (pastellblau) | `#85ACD7` |
|
| `[^]` | prod | in Produktion | deployed/live | `#DBEAF8` (pastellblau) | `#85ACD7` |
|
||||||
| `[-]` | verworfen | verworfen | bewusst nicht weiterverfolgt | `#F1F2F4`, gestrichelter Rahmen, Text durchgestrichen | `#B3BAC2` |
|
| `[-]` | 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ß).
|
- Ohne Statusbox: neutraler Knoten (weiß).
|
||||||
- `x` auch als `X` zulässig.
|
- `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**;
|
- Verworfene Knoten (inkl. Teilbaum) sind per Default **ausgeblendet**;
|
||||||
Toggle „verworfene einblenden“ zeigt sie.
|
Toggle „verworfene einblenden“ zeigt sie.
|
||||||
- **Fehlertoleranz:** Ein unbekanntes Zeichen in der Statusbox (z. B. `[z]`)
|
- **Fehlertoleranz:** Ein unbekanntes Zeichen in der Statusbox (z. B. `[z]`)
|
||||||
|
|||||||
+28
-5
@@ -16,7 +16,7 @@ const INITIAL = `%% Project structure – Sprint 14
|
|||||||
- [/] Web (S)
|
- [/] Web (S)
|
||||||
- [ ] Android (M)
|
- [ ] Android (M)
|
||||||
- [ ] iOS (M)
|
- [ ] iOS (M)
|
||||||
- [ ] Backend (L) @ben @carla
|
- [!] Backend (L) @ben @carla
|
||||||
- [ ] CMS integration (M)
|
- [ ] CMS integration (M)
|
||||||
| [ ] WordPress
|
| [ ] WordPress
|
||||||
| [?] Headless CMS
|
| [?] Headless CMS
|
||||||
@@ -195,10 +195,16 @@ function diagramToSvg(){
|
|||||||
const dashed = cs.borderTopStyle === 'dashed';
|
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"':''}/>`);
|
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);
|
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 label = clone.textContent.replace(/\s+/g,' ').trim();
|
||||||
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
|
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>`);
|
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');
|
const sizeEl = node.querySelector('.size');
|
||||||
if(sizeEl) drawBadge(sizeEl, '#0F766E', '#ffffff');
|
if(sizeEl) drawBadge(sizeEl, '#0F766E', '#ffffff');
|
||||||
node.querySelectorAll('.tag').forEach(tg => {
|
node.querySelectorAll('.tag').forEach(tg => {
|
||||||
@@ -559,6 +565,7 @@ const I18N = {
|
|||||||
privacy:"Datenschutz",
|
privacy:"Datenschutz",
|
||||||
legendTooltip:"Legende ein-/ausblenden",
|
legendTooltip:"Legende ein-/ausblenden",
|
||||||
ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.",
|
ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.",
|
||||||
|
riskTooltip:"High Risk – Aufwand noch unklar.",
|
||||||
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
||||||
cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück",
|
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",
|
implicitSizeTooltip:"Keine Größe angegeben – für die Kostenschätzung als M angenommen",
|
||||||
@@ -599,6 +606,7 @@ const I18N = {
|
|||||||
privacy:"Privacy",
|
privacy:"Privacy",
|
||||||
legendTooltip:"Show/hide legend",
|
legendTooltip:"Show/hide legend",
|
||||||
ghostTooltip:"From size M upward, an item should be broken down further.",
|
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",
|
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
||||||
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
||||||
implicitSizeTooltip:"No size given – assumed as M for the cost estimate",
|
implicitSizeTooltip:"No size given – assumed as M for the cost estimate",
|
||||||
@@ -639,6 +647,7 @@ const I18N = {
|
|||||||
privacy:"Privacidad",
|
privacy:"Privacidad",
|
||||||
legendTooltip:"Mostrar u ocultar la leyenda",
|
legendTooltip:"Mostrar u ocultar la leyenda",
|
||||||
ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.",
|
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",
|
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",
|
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",
|
implicitSizeTooltip:"Sin tamaño indicado: se asume M para el cálculo de costes",
|
||||||
@@ -679,6 +688,7 @@ const I18N = {
|
|||||||
privacy:"Confidentialité",
|
privacy:"Confidentialité",
|
||||||
legendTooltip:"Afficher/masquer la légende",
|
legendTooltip:"Afficher/masquer la légende",
|
||||||
ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.",
|
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",
|
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",
|
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",
|
implicitSizeTooltip:"Aucune taille indiquée – considérée comme M pour l'estimation des coûts",
|
||||||
@@ -719,6 +729,7 @@ const I18N = {
|
|||||||
privacy:"Prywatność",
|
privacy:"Prywatność",
|
||||||
legendTooltip:"Pokaż/ukryj legendę",
|
legendTooltip:"Pokaż/ukryj legendę",
|
||||||
ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.",
|
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",
|
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
||||||
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
||||||
implicitSizeTooltip:"Nie podano rozmiaru – przyjęto M do szacowania kosztów",
|
implicitSizeTooltip:"Nie podano rozmiaru – przyjęto M do szacowania kosztów",
|
||||||
@@ -759,6 +770,7 @@ const I18N = {
|
|||||||
privacy:"Конфиденциальность",
|
privacy:"Конфиденциальность",
|
||||||
legendTooltip:"Показать/скрыть легенду",
|
legendTooltip:"Показать/скрыть легенду",
|
||||||
ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.",
|
ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.",
|
||||||
|
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
||||||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||||||
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
||||||
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
|
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
|
||||||
@@ -799,6 +811,7 @@ const I18N = {
|
|||||||
privacy:"गोपनीयता",
|
privacy:"गोपनीयता",
|
||||||
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
|
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
|
||||||
ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।",
|
ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।",
|
||||||
|
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
||||||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||||||
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
||||||
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
|
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
|
||||||
@@ -844,6 +857,7 @@ const I18N = {
|
|||||||
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
||||||
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
|
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
|
||||||
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
||||||
|
riskTooltip:"高风险 – 工作量尚不明确。",
|
||||||
editorTitle:"结构(文本)", diagramTitle:"图表",
|
editorTitle:"结构(文本)", diagramTitle:"图表",
|
||||||
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
|
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
|
||||||
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
|
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
|
||||||
@@ -884,6 +898,7 @@ const I18N = {
|
|||||||
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
||||||
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
|
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
|
||||||
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
||||||
|
riskTooltip:"高リスク – 規模はまだ不明。",
|
||||||
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
||||||
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
|
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
|
||||||
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
|
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
|
||||||
@@ -1194,6 +1209,13 @@ mountBuildBadge();
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
/* ---------- Update-Detection (Client-seitig, einfach & zuverlässig) ---------- */
|
/* ---------- 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(){
|
async function checkForUpdates(){
|
||||||
try {
|
try {
|
||||||
/* Fetch HTML mit Cache-Busting via Timestamp */
|
/* Fetch HTML mit Cache-Busting via Timestamp */
|
||||||
@@ -1255,8 +1277,9 @@ function logUpdate(msg){
|
|||||||
localStorage.setItem('werkbaum-update-log', log.join('\n'));
|
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(){
|
function showUpdateDebug(){
|
||||||
|
if(isProdBuild) return;
|
||||||
let panel = document.getElementById('updateDebugPanel');
|
let panel = document.getElementById('updateDebugPanel');
|
||||||
if(!panel){
|
if(!panel){
|
||||||
panel = document.createElement('div');
|
panel = document.createElement('div');
|
||||||
@@ -1286,8 +1309,8 @@ function showUpdateDebug(){
|
|||||||
panel.textContent = log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge';
|
panel.textContent = log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reset-Button im Header neben Fullscreen */
|
/* Reset-Button im Header neben Fullscreen (Test-Hilfe; im Prod-Build weggelassen) */
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if(!isProdBuild) document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fsToggle = document.getElementById('fsToggle');
|
const fsToggle = document.getElementById('fsToggle');
|
||||||
if(!fsToggle) return;
|
if(!fsToggle) return;
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,14 @@ function nodeHtml(n, extra, opts){
|
|||||||
const sizeBadge = n.size
|
const sizeBadge = n.size
|
||||||
? `<span class="size" aria-hidden="true">${n.size}</span>`
|
? `<span class="size" aria-hidden="true">${n.size}</span>`
|
||||||
: (cheapPath ? `<span class="size implicit" aria-hidden="true" title="${implicitTip}">M</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) +
|
const inner = esc(n.label) +
|
||||||
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
|
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
|
||||||
|
riskMark +
|
||||||
sizeBadge +
|
sizeBadge +
|
||||||
tagsHtml;
|
tagsHtml;
|
||||||
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
|
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
|
||||||
|
|||||||
@@ -393,6 +393,15 @@
|
|||||||
border:1.5px solid var(--card);
|
border:1.5px solid var(--card);
|
||||||
border-radius:999px;padding:1px 6px;line-height:1.4;
|
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{
|
.tags{
|
||||||
position:absolute;right:-9px;bottom:-9px;
|
position:absolute;right:-9px;bottom:-9px;
|
||||||
display:flex;gap:3px;
|
display:flex;gap:3px;
|
||||||
|
|||||||
Reference in New Issue
Block a user