frontend: Notationstext per ?sourceUrl= von einer URL laden
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
64e4e60b53
commit
1557a7ab0d
@@ -386,3 +386,45 @@ Dokumente; das war zu grob, sobald man mehrere Dokumente pflegt.
|
|||||||
Die GUI-Ansichts-Einstellungen (Modus, Zoom, Aufteilung; `werkbaum-ui`) bleiben
|
Die GUI-Ansichts-Einstellungen (Modus, Zoom, Aufteilung; `werkbaum-ui`) bleiben
|
||||||
bewusst **global** über alle Dokumente — pro-Dokument-Ansichtszustand wäre eine
|
bewusst **global** über alle Dokumente — pro-Dokument-Ansichtszustand wäre eine
|
||||||
spätere Erweiterung.
|
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:<href>`), 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`.
|
||||||
|
|||||||
@@ -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
|
abgetönte Kopie und Punkte erscheinen bei aktivem Umschalter auch im
|
||||||
Grafikexport. Siehe D18.
|
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
|
### Grafikexport des Diagramms
|
||||||
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
||||||
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
||||||
|
|||||||
@@ -110,6 +110,17 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
unterdrückt): `renameDoc()` setzt `renamingId`, `renderDocMenu()` rendert dann
|
unterdrückt): `renameDoc()` setzt `renamingId`, `renderDocMenu()` rendert dann
|
||||||
ein `<input class="docrename">` (Enter = `commitRename`, Esc = `cancelRename`,
|
ein `<input class="docrename">` (Enter = `commitRename`, Esc = `cancelRename`,
|
||||||
Blur = commit). Doc-Namen sind Nutzerdaten und werden **nicht** übersetzt.
|
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:<href>'`,
|
||||||
|
`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
|
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
||||||
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
||||||
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
||||||
|
|||||||
+82
-4
@@ -33,6 +33,8 @@ const warnBox = document.getElementById('warn');
|
|||||||
Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt
|
Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt
|
||||||
nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */
|
nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */
|
||||||
let cheapPathOn = true;
|
let cheapPathOn = true;
|
||||||
|
/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */
|
||||||
|
let sourceWarning = null;
|
||||||
|
|
||||||
/* ---------- Renderer (Anbindung an den DOM) ----------
|
/* ---------- Renderer (Anbindung an den DOM) ----------
|
||||||
parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren ->
|
parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren ->
|
||||||
@@ -46,8 +48,10 @@ function render(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Warnungen aus Parser (unbekannte Statuszeichen) + Renderer (gemischte
|
/* Warnungen aus Parser (unbekannte Statuszeichen) + Renderer (gemischte
|
||||||
Gates) zusammenführen, nach Zeile sortiert anzeigen. */
|
Gates) zusammenführen, nach Zeile sortiert anzeigen. `sourceWarning`
|
||||||
let warnings = parsed.warnings;
|
(?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){
|
if(!roots.length){
|
||||||
out.innerHTML = `<div class="empty">${esc(t('empty'))}</div>`;
|
out.innerHTML = `<div class="empty">${esc(t('empty'))}</div>`;
|
||||||
@@ -593,6 +597,7 @@ const I18N = {
|
|||||||
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
|
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",
|
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
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",
|
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_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||||||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
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_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",
|
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
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",
|
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_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||||||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
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_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",
|
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.",
|
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",
|
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_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
||||||
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
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_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é",
|
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.",
|
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",
|
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_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",
|
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_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",
|
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
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",
|
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_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||||||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
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_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||||
|
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
||||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yLink:"со ссылкой",
|
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yLink:"со ссылкой",
|
||||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||||
@@ -863,6 +873,7 @@ const I18N = {
|
|||||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||||
|
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
||||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yLink:"लिंक सहित",
|
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yLink:"लिंक सहित",
|
||||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||||
@@ -908,6 +919,7 @@ const I18N = {
|
|||||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||||
|
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
||||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yLink:"含链接",
|
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yLink:"含链接",
|
||||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||||
@@ -953,6 +965,7 @@ const I18N = {
|
|||||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||||
|
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
||||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yLink:"リンクあり",
|
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yLink:"リンクあり",
|
||||||
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
||||||
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||||
@@ -1014,6 +1027,7 @@ function applyLang(l){
|
|||||||
langMore.setAttribute('aria-expanded', 'true');
|
langMore.setAttribute('aria-expanded', 'true');
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
updateDocName(); /* Titelzeilen-Tooltip nach dem data-i18n-title-Durchlauf setzen */
|
||||||
try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
|
try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
|
||||||
}
|
}
|
||||||
const langsel = document.querySelector('.langsel');
|
const langsel = document.querySelector('.langsel');
|
||||||
@@ -1184,6 +1198,14 @@ const docList = document.getElementById('docList');
|
|||||||
function updateDocName(){
|
function updateDocName(){
|
||||||
const d = activeDoc();
|
const d = activeDoc();
|
||||||
if(docNameEl) docNameEl.textContent = d ? d.name : '';
|
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) */
|
let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */
|
||||||
function renderDocMenu(){
|
function renderDocMenu(){
|
||||||
@@ -1274,6 +1296,57 @@ function initDocs(){
|
|||||||
updateDocName();
|
updateDocName();
|
||||||
render();
|
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 => {
|
docTrigger.addEventListener('click', e => {
|
||||||
/* Desktop: ist der Editor minimiert, stellt ein Klick ihn wieder her
|
/* Desktop: ist der Editor minimiert, stellt ein Klick ihn wieder her
|
||||||
(Bubbling zur Titelzeile) statt das Menü zu öffnen. */
|
(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 */
|
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
|
||||||
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
|
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
|
||||||
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
|
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
|
||||||
|
loadFromSourceUrl(); /* ?sourceUrl= nachladen (asynchron, D23) */
|
||||||
|
|
||||||
/* ---------- Build-Hinweis (Vorschau/Dev + „latest build") ----------
|
/* ---------- Build-Hinweis (Vorschau/Dev + „latest build") ----------
|
||||||
Kennzeichnet einen nicht-produktiven Build mit einem kleinen Symbol samt
|
Kennzeichnet einen nicht-produktiven Build mit einem kleinen Symbol samt
|
||||||
@@ -1469,8 +1543,12 @@ function hashContent(str){
|
|||||||
|
|
||||||
async function checkForUpdates(){
|
async function checkForUpdates(){
|
||||||
try {
|
try {
|
||||||
/* Fetch HTML mit Cache-Busting via Timestamp */
|
/* Fetch HTML mit Cache-Busting via Timestamp. Über URL/searchParams gebaut,
|
||||||
const resp = await fetch(location.href + '?t=' + new Date().getTime(), { cache: 'no-store' });
|
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}`);
|
if(!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
|
||||||
/* Voller Content-Hash ist die alleinige Wahrheit. ETag/Last-Modified werden
|
/* Voller Content-Hash ist die alleinige Wahrheit. ETag/Last-Modified werden
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
Bekannte Typen:
|
Bekannte Typen:
|
||||||
- mixedGate { line, label } — Geschwister mit gemischtem Gate (SPEC §3)
|
- 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';
|
import { esc } from './render.js';
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ export function formatWarning(w, t){
|
|||||||
return t('mixedWarn', {line: w.line, label: esc(w.label)});
|
return t('mixedWarn', {line: w.line, label: esc(w.label)});
|
||||||
case 'unknownStatus':
|
case 'unknownStatus':
|
||||||
return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
|
return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
|
||||||
|
case 'sourceLoad':
|
||||||
|
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
||||||
default:
|
default:
|
||||||
return `${esc(String(w.type))} (${w.line ?? '?'})`;
|
return `${esc(String(w.type))} (${w.line ?? '?'})`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user