From 1557a7ab0def35bb163f73fd962fe3d36bb58660 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sat, 25 Jul 2026 11:23:40 +0200 Subject: [PATCH] frontend: Notationstext per ?sourceUrl= von einer URL laden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Editor kann den Text aus einer externen Textdatei beziehen: …/index.html?sourceUrl=https://example.org/plan.txt - Das Geladene wird als eigenes Dokument (D22) geführt, dessen Name die URL ist; die id leitet sich aus der URL ab, derselbe Link aktualisiert also dieses Dokument, statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente bleiben unberührt. Die volle URL steht zusätzlich im Tooltip (der Name wird in der Titelzeile elidiert). - Ist der Parameter gesetzt, wird bei jedem Laden neu geholt — die URL ist die Quelle der Wahrheit. - Nur http(s) (relative Angaben werden aufgelöst), credentials:'omit'; file:/data:/javascript: werden abgewiesen. Fremder Text ist ungefährlich: Labels werden escaped, der Parser erkennt als Link nur https?:// (SPEC §1). - Fehler (CORS, 404, Netz) landen als zeilenlose Warnung `sourceLoad` im Warnbereich und nennen CORS ausdrücklich als wahrscheinliche Ursache; der bisherige Stand bleibt stehen. i18n in allen 9 Sprachen. - Nebenbei: checkForUpdates() baut die Cache-Buster-URL nun über URL/searchParams — mit vorhandenem Query-String hätte '?t=' ein zweites '?' erzeugt. Verifiziert headless (jsdom gegen den gebauten Bundle: Erfolg, CORS-Fehler, 404, ohne Parameter, verbotenes Schema) und im Browser gegen raw.githubusercontent.com. Dokumentiert als D23 + SPEC §9 + frontend/CLAUDE.md. Co-Authored-By: Claude Opus 4.8 --- docs/DECISIONS.md | 42 ++++++++++++++++++++ docs/SPEC.md | 12 ++++++ frontend/CLAUDE.md | 11 +++++ frontend/src/app.js | 86 ++++++++++++++++++++++++++++++++++++++-- frontend/src/warnings.js | 6 ++- 5 files changed, 152 insertions(+), 5 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 33668f9..1d9e25e 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -386,3 +386,45 @@ Dokumente; das war zu grob, sobald man mehrere Dokumente pflegt. Die GUI-Ansichts-Einstellungen (Modus, Zoom, Aufteilung; `werkbaum-ui`) bleiben bewusst **global** über alle Dokumente — pro-Dokument-Ansichtszustand wäre eine spätere Erweiterung. + +## D23 — Notationstext per `?sourceUrl=` von einer URL laden +Der Editor kann den Notationstext aus einer **externen Textdatei** beziehen: +`…/index.html?sourceUrl=https://example.org/plan.txt`. Damit lässt sich ein +Diagramm teilen/verlinken, ohne den Text in die URL zu packen, und die Quelle +kann anderswo (Git, Wiki, Server) gepflegt werden. + +**Die URL ist der Titel.** Das geladene Dokument wird als eigenes Dokument im +Sinne von D22 geführt; sein **Name ist die URL**. Die **id leitet sich aus der +URL ab** (`url:`), damit derselbe Link dieses eine Dokument *aktualisiert*, +statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente des Nutzers bleiben +unberührt. Da der Name in der Titelzeile mit Ellipse abgeschnitten wird, steht +die vollständige URL zusätzlich im Tooltip. + +**Die URL ist die Quelle der Wahrheit:** Ist der Parameter gesetzt, wird bei +**jedem** Laden neu geholt und der Dokumenttext überschrieben. Lokale Änderungen +an einem URL-Dokument überleben ein Neuladen also nicht — bewusst, weil +`sourceUrl` eine *Ansicht auf eine entfernte Datei* ist, nicht deren Kopie. (Eine +konfliktbewusste Variante — lokale Änderungen erkennen und behalten — wäre eine +mögliche Erweiterung.) + +**CORS ist die eigentliche Einschränkung.** Der Browser lädt fremde Hosts nur, +wenn die Zielseite `Access-Control-Allow-Origin` sendet. Das tun u. a. +`raw.githubusercontent.com` und GitLab-Raw-Links; ein beliebiger Webserver +oft **nicht**. Scheitert das Laden (CORS, 404, Netz), bleibt der bisherige Stand +stehen und es erscheint eine **Warnung** im Warnbereich (Typ `sourceLoad`, +zeilenlos ⇒ zuoberst), die CORS ausdrücklich als wahrscheinliche Ursache nennt. +Bewusst kein Proxy-Dienst als Ausweg: das würde fremde Inhalte über einen +Dritt-Host leiten und dem Datenschutz-Anspruch aus D20 widersprechen. + +**Verhältnis zu D20 („keine externen Requests"):** Der Grundsatz bleibt — die App +lädt von sich aus **nichts** nach (Schriften inline, kein CDN). Der Request +entsteht **nur**, wenn der Nutzer selbst eine URL angibt, und geht **nur** an +genau diesen Host; `credentials:'omit'` verhindert das Mitsenden von Cookies. +Erlaubt sind nur `http:`/`https:` (relative Angaben werden gegen die Seite +aufgelöst); andere Schemata (`file:`, `data:`, `javascript:`) werden abgewiesen. +Fremder Text ist ungefährlich: Labels werden escaped, und der Parser erkennt als +Knoten-Link ohnehin nur `https?://…` (SPEC §1/§6) — kein `javascript:`-Vektor. + +Nebenbefund: `checkForUpdates()` hängte den Cache-Buster als `location.href + +'?t=…'` an, was mit vorhandenem Query-String ein zweites `?` erzeugt hätte; es +baut die URL nun über `URL`/`searchParams`. diff --git a/docs/SPEC.md b/docs/SPEC.md index 944d825..8a939f4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -213,6 +213,18 @@ Punkt, sodass eindeutig bleibt, welche Knoten auf dem Pfad **enden**. Linie, abgetönte Kopie und Punkte erscheinen bei aktivem Umschalter auch im Grafikexport. Siehe D18. +### Diagramm aus einer URL laden (`?sourceUrl=`) +Der Editor kann den Notationstext aus einer externen Textdatei beziehen: +`…?sourceUrl=https://example.org/plan.txt` (relative Angaben werden gegen die +Seite aufgelöst; zugelassen sind nur `http`/`https`). Der Text wird als +**eigenes Dokument** geführt, dessen **Name die URL** ist (vollständige URL im +Tooltip); derselbe Link aktualisiert dieses Dokument, statt ein neues anzulegen. +Ist der Parameter gesetzt, wird bei **jedem** Laden neu geholt — die URL ist die +Quelle der Wahrheit, lokale Änderungen daran überleben ein Neuladen nicht. +Scheitert das Laden (häufigster Fall: das Ziel sendet keinen +`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der +bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23. + ### Grafikexport des Diagramms Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen: diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index d10c675..045fd7d 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -110,6 +110,17 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der unterdrückt): `renameDoc()` setzt `renamingId`, `renderDocMenu()` rendert dann ein `` (Enter = `commitRename`, Esc = `cancelRename`, Blur = commit). Doc-Namen sind Nutzerdaten und werden **nicht** übersetzt. +- `?sourceUrl=` (D23): `loadFromSourceUrl()` (Aufruf am Ende des Starts, async) + holt eine externe Textdatei und führt sie als Dokument mit `id: 'url:'`, + `name`/`source` = URL. Nur `http(s)`, `credentials:'omit'`; bei jedem Laden wird + neu geholt (URL = Quelle der Wahrheit, lokale Edits daran gehen verloren). + Fehler landen als zeilenlose Warnung `{type:'sourceLoad', url, error}` in + `sourceWarning` — ein **persistenter** Kanal, den `render()` jedem Warnungs-Satz + voranstellt (überlebt Neu-Renderings, bis das Laden gelingt). Hauptstolperfalle + ist **CORS**: fremde Hosts brauchen `Access-Control-Allow-Origin` (raw.github… + ja, beliebiger Webserver oft nicht) — der Warntext nennt das ausdrücklich. + `updateDocName()` setzt für solche Dokumente den vollen URL-Tooltip und muss + deshalb **nach** dem `data-i18n-title`-Durchlauf in `applyLang()` laufen. - Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den diff --git a/frontend/src/app.js b/frontend/src/app.js index b744f02..11b2ace 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -33,6 +33,8 @@ const warnBox = document.getElementById('warn'); Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */ let cheapPathOn = true; +/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */ +let sourceWarning = null; /* ---------- Renderer (Anbindung an den DOM) ---------- parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren -> @@ -46,8 +48,10 @@ function render(){ } /* Warnungen aus Parser (unbekannte Statuszeichen) + Renderer (gemischte - Gates) zusammenführen, nach Zeile sortiert anzeigen. */ - let warnings = parsed.warnings; + Gates) zusammenführen, nach Zeile sortiert anzeigen. `sourceWarning` + (?sourceUrl nicht ladbar, D23) gehört keiner Zeile und bleibt über + Neu-Renderings bestehen, bis das Laden gelingt. */ + let warnings = sourceWarning ? [sourceWarning].concat(parsed.warnings) : parsed.warnings; if(!roots.length){ out.innerHTML = `
${esc(t('empty'))}
`; @@ -593,6 +597,7 @@ const I18N = { st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich", st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen", unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.", + sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).", a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yLink:"verlinkt", hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.", hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen", @@ -638,6 +643,7 @@ const I18N = { st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton", st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded", unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.", + sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).", a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yLink:"has link", hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.", hint_all:"sub-task, all required", hint_any:"alternative, choose one", @@ -683,6 +689,7 @@ const I18N = { st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional", st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado", unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.", + sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).", a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yLink:"con enlace", hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.", hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una", @@ -728,6 +735,7 @@ const I18N = { st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel", st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné", unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.", + sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).", a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yLink:"avec lien", hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.", hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une", @@ -773,6 +781,7 @@ const I18N = { st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet", st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone", unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.", + sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).", a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yLink:"z linkiem", hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.", hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną", @@ -818,6 +827,7 @@ const I18N = { st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип", st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено", unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.", + sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).", a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yLink:"со ссылкой", hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.", hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну", @@ -863,6 +873,7 @@ const I18N = { st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा", st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत", unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।", + sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।", a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yLink:"लिंक सहित", hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।", hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें", @@ -908,6 +919,7 @@ const I18N = { st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架", st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃", unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。", + sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。", a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yLink:"含链接", hint_indent:"缩进(2 个空格或制表符)定义层级。", hint_all:"子任务,全部必需", hint_any:"备选项,择其一", @@ -953,6 +965,7 @@ const I18N = { st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン", st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄", unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。", + sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。", a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yLink:"リンクあり", hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。", hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ", @@ -1014,6 +1027,7 @@ function applyLang(l){ langMore.setAttribute('aria-expanded', 'true'); } render(); + updateDocName(); /* Titelzeilen-Tooltip nach dem data-i18n-title-Durchlauf setzen */ try{ localStorage.setItem('werkbaum-lang', l); }catch(_){} } const langsel = document.querySelector('.langsel'); @@ -1184,6 +1198,14 @@ const docList = document.getElementById('docList'); function updateDocName(){ const d = activeDoc(); if(docNameEl) docNameEl.textContent = d ? d.name : ''; + /* Aus einer URL geladene Dokumente (D23): die vollständige Quelle in den + Tooltip, da der Name in der Titelzeile mit Ellipse abgeschnitten wird. + Muss nach applyLang erneut laufen — das setzt data-i18n-title zurück. */ + if(docTrigger && d){ + docTrigger.title = d.source + ? d.source + '\n' + t('docSwitchTooltip') + : t('docSwitchTooltip'); + } } let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */ function renderDocMenu(){ @@ -1274,6 +1296,57 @@ function initDocs(){ updateDocName(); render(); } + +/* ---------- ?sourceUrl= — Notationstext von einer URL laden (D23) ---------- + Die Datei wird geholt und als eigenes Dokument geführt, dessen Name die URL + ist. Die id leitet sich aus der URL ab: derselbe Link aktualisiert dieses + Dokument, statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente des + Nutzers bleiben unberührt. Scheitert das Laden (häufigster Fall: das Ziel + sendet keinen CORS-Header), bleibt der bisherige Stand stehen und es + erscheint eine Warnung. */ +const SOURCE_PARAM = 'sourceUrl'; +function sourceUrlParam(){ + try{ return new URLSearchParams(location.search).get(SOURCE_PARAM); }catch(_){ return null; } +} +async function loadFromSourceUrl(){ + const raw = sourceUrlParam(); + if(!raw) return; + let url; + /* Relative Angaben gegen die Seite auflösen; nur http(s) zulassen (kein + file:/data:/javascript: — die Notation selbst erlaubt ohnehin nur http(s)). */ + try{ url = new URL(raw, location.href); }catch(_){ + /* Fehlerdetail bewusst technisch/englisch wie die Browser-Meldungen + („Failed to fetch", „HTTP 404") — der Rahmentext ist lokalisiert. */ + sourceWarning = {type:'sourceLoad', url: raw, error: 'invalid URL'}; + render(); + return; + } + if(url.protocol !== 'http:' && url.protocol !== 'https:'){ + sourceWarning = {type:'sourceLoad', url: url.href, error: url.protocol}; + render(); + return; + } + try{ + const resp = await fetch(url.href, {cache:'no-store', credentials:'omit'}); + if(!resp.ok) throw new Error('HTTP ' + resp.status); + const text = await resp.text(); + const id = 'url:' + url.href; + flushActive(); /* laufende Bearbeitung nicht verlieren */ + let d = docs.find(x => x.id === id); + if(d){ d.text = text; d.name = url.href; } + else { d = {id, name: url.href, text, source: url.href}; docs.push(d); } + d.source = url.href; + activeId = id; + sourceWarning = null; + loadActiveIntoEditor(); + persistDocs(); + }catch(err){ + /* CORS-Fehler melden sich als „TypeError: Failed to fetch" ohne Details — + der Warntext nennt CORS daher ausdrücklich als wahrscheinliche Ursache. */ + sourceWarning = {type:'sourceLoad', url: url.href, error: (err && err.message) || String(err)}; + render(); + } +} docTrigger.addEventListener('click', e => { /* Desktop: ist der Editor minimiert, stellt ein Klick ihn wieder her (Bubbling zur Titelzeile) statt das Menü zu öffnen. */ @@ -1383,6 +1456,7 @@ try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_ applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */ initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */ applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */ +loadFromSourceUrl(); /* ?sourceUrl= nachladen (asynchron, D23) */ /* ---------- Build-Hinweis (Vorschau/Dev + „latest build") ---------- Kennzeichnet einen nicht-produktiven Build mit einem kleinen Symbol samt @@ -1469,8 +1543,12 @@ function hashContent(str){ async function checkForUpdates(){ try { - /* Fetch HTML mit Cache-Busting via Timestamp */ - const resp = await fetch(location.href + '?t=' + new Date().getTime(), { cache: 'no-store' }); + /* Fetch HTML mit Cache-Busting via Timestamp. Über URL/searchParams gebaut, + damit ein bereits vorhandener Query-String (z. B. ?sourceUrl=…, D23) nicht + durch ein angehängtes zweites „?" zerstört wird. */ + const bust = new URL(location.href); + bust.searchParams.set('t', String(new Date().getTime())); + const resp = await fetch(bust.href, { cache: 'no-store' }); if(!resp.ok) throw new Error(`HTTP ${resp.status}`); /* Voller Content-Hash ist die alleinige Wahrheit. ETag/Last-Modified werden diff --git a/frontend/src/warnings.js b/frontend/src/warnings.js index 2e60430..f179381 100644 --- a/frontend/src/warnings.js +++ b/frontend/src/warnings.js @@ -8,7 +8,9 @@ Bekannte Typen: - mixedGate { line, label } — Geschwister mit gemischtem Gate (SPEC §3) - - unknownStatus { line, code } — unbekanntes Statuszeichen (Phase 2) */ + - unknownStatus { line, code } — unbekanntes Statuszeichen (Phase 2) + - sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne + Zeilennummer, erscheint dadurch zuoberst */ import { esc } from './render.js'; @@ -20,6 +22,8 @@ export function formatWarning(w, t){ return t('mixedWarn', {line: w.line, label: esc(w.label)}); case 'unknownStatus': return t('unknownStatusWarn', {line: w.line, code: esc(w.code)}); + case 'sourceLoad': + return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)}); default: return `${esc(String(w.type))} (${w.line ?? '?'})`; }