import './style.css'; import { parse, setFoldMark, setStatusBox, expandShortIds, shortIdClosed } from './parser.js'; import { computeCheapPlan, overloadedAssignee, assigneeLoads, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, personFoldSet, allTags, lineTargets, taigaSlugs } from './model.js'; import { ticketRefOf, taskCandidates, appendToken, refToken, slugToken, ticketUrl, ticketRefAt, ticketApiPath, mapTaigaStatus, taigaStatusName, pickStatus, statusApiPath, statusListPath, storyAncestor } from './taiga.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { formatWarning, warningText } from './warnings.js'; import * as live from './live.js'; import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js'; import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel } from './snapshots.js'; import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.js'; import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch, docKind } from './docurl.js'; import { readDocs, storeDocs, storeDocText, isDocKey, LS_DOCS, LS_ACTIVE, LS_SRC } from './docstore.js'; /* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in vite.config.js). Zur Laufzeit gibt es kein git — und keinen Server, der nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */ import NEWS from 'virtual:werkbaum-news'; /* Werkbaum, mit Werkbaum geplant — als mitgeliefertes Dokument „Werkbank" (D27). Dieselbe Datei, die auch per ?sourceUrl= geladen werden kann; `?raw` bettet sie beim Build in die eine Ausgabedatei ein (D19), es wird nichts nachgeladen (D20). Quelle bleibt docs/examples/ — keine Kopie, die auseinanderläuft. */ import WERKBAUM_DOC from '../../docs/examples/werkbaum.werkbaum?raw'; const INITIAL = `%% Project structure – Sprint 14 [~] Website relaunch (XL) https://wiki.example.com/relaunch " Folded chapters are done — click the ▸ to peek inside. - [x] > Concept (M) - [x] Audience analysis (S) - [x] Sitemap (XS) - [~] Implementation (XL) - [~] Frontend (M) https://git.example.com/frontend @anna | [ ] PWA (S) | [ ] Web+Native - [/] Web (S) - [ ] Android (M) - [ ] iOS (M) - [!] Backend (L) @ben @carla - [ ] #cms: CMS integration (M) | [ ] WordPress | [?] Headless CMS | [-] Custom build %% too much effort - [x] Landing page (S) :#cms %% done, but effectively waiting for the CMS + [?] Dark mode (S) %% nice to have, never on the cheapest path - [?] Hosting (M) %% exactly one of these, hence = = [ ] Cooperative Community Cloud https://hostsharing.net = [?] On-premise --- #cms The articles live in the CMS, so everything that shows content depends on it — the dotted arrows point at what is needed.`; const src = document.getElementById('src'); const out = document.getElementById('out'); const warnBox = document.getElementById('warn'); const peopleBar = document.getElementById('peopleBar'); /* Zeilennummern-Streifen (D33) — hier oben geholt, weil render() ihn füllt. */ const srcWrap = document.getElementById('srcWrap'); const lineNoBox = document.getElementById('lineNos'); const lineNoInner = lineNoBox.firstElementChild; /* Baum-/Kostenlogik (gateOf, needsBreakdown, visibleChildren, günstigster 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; let depLinksOn = true; /* Querverbindungen der Abhängigkeiten anzeigen (D75) */ let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */ /* Falt-Durchschalter (SPEC §9/D75): vier Voreinstellungen, reihum. `Next` ist der Schritt, den der nächste Druck ausführt (Icon + Tooltip zeigen IHN), `Applied` der zuletzt hergestellte — render() prüft ihn gegen den Baum und setzt die Position zurück, sobald er nicht mehr stimmt. Nicht persistiert: Der Faltzustand steht im Text (D38), die Position ist nur der Zeiger darin. */ const FOLD_CYCLE = ['small', 'path', 'closed', 'open']; let foldCycleNext = 0, foldCycleApplied = -1; /* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag): dort ist immer genau einer zu sehen. Hier oben deklariert, weil saveUI() ihn liest und schon aus applySplit() heraus laufen kann — weiter unten stünde er dann noch in der temporalen Todeszone. */ let mobilePane = 'diagram'; /* Cursor-Zeile (D25) — bleibt `null`, bis der Cursor das erste Mal bewegt wurde, sonst wäre nach dem Laden ungefragt die Wurzel markiert. Aus demselben Grund hier oben: der Zeilennummern-Streifen (D33) liest sie, und der hängt an render(). */ let caretLine = null, currentNodeEl = null; /* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */ let sourceWarning = null; /* Scheitert das Speichern im Browser (Quota voll), wird das GEMELDET statt geschluckt (D82) — vorher lief der Editor scheinbar normal weiter, und der Verlust fiel erst beim Neuladen auf. Persistent, bis ein Schreiben wieder gelingt; zeilenlos und zuoberst. */ let storeWarning = null; /* Ein ANDERER Tab schreibt in dieselbe Dokument-Ablage (D84): Das storage-Ereignis feuert nur in fremden Tabs desselben Ursprungs — genau das richtige Signal. Kein Sync (der letzte Flush gewinnt weiterhin, wie immer), aber kein STILLER Verlust mehr: Die Warnung bleibt stehen, solange dieser Tab lebt — die Lage ändert sich ja nicht dadurch, dass der andere Tab gerade nichts schreibt. */ let tabWarning = null; window.addEventListener('storage', e => { if(tabWarning || !e.key || !isDocKey(e.key)) return; tabWarning = {type: 'tabConflict'}; render(); }); /* Zwei lebende Fenster sind mehr als eine Fußnote (D89): Die zeilenlose Warnung oben meldet nur, dass ein anderer Tab GESCHRIEBEN hat — ein zweites offenes Fenster bekommt zusätzlich einen MODALEN Dialog, in beiden Fenstern, bis das Problem behoben ist. Herzschlag über BroadcastChannel: jedes Fenster meldet sich sekündlich; verstummt das andere (Abmeldung beim Schließen, sonst 3 s Stille), schließt der Dialog sich von selbst — die App läuft ohne weiteren Klick weiter. „Trotzdem fortfahren" ist die Notluke und gilt nur, solange DIESES zweite Fenster lebt. */ /* Wachhund-Warnung (D89): Ungesendete Live-Änderungen bzw. tote Sitzung. Gesetzt vom 5-s-Takt im Live-Abschnitt, gelesen von render(). */ let liveWarning = null; const WIN_ID = Math.random().toString(36).slice(2); /* 75 s statt weniger Sekunden: Chrome drosselt die Timer verborgener Fenster nach fünf Minuten auf einen Tick je MINUTE (dieselbe Umgebungsgrenze wie in D79 gemessen) — ein kürzerer Timeout ließe den Dialog im sichtbaren Fenster flackern, weil das verborgene nur noch minütlich schlägt. Das saubere Schließen meldet sich ohnehin sofort ab (bye bei pagehide); der Timeout fängt nur hart gestorbene Fenster. */ const FOREIGN_DEAD_MS = 75000; let presenceChannel = null, tabModalEl = null; const foreignBeats = new Map(); /* fremde Fenster-Id -> letzter Herzschlag */ const tabModalDismissed = new Set(); /* Notluke gilt je fremder Id, nicht je Sitzung: ein NEUES zweites Fenster bekommt wieder den Dialog */ try{ presenceChannel = new BroadcastChannel('werkbaum-presence'); }catch(_){} if(presenceChannel){ presenceChannel.onmessage = e => { const m = e.data || {}; if(!m.id || m.id === WIN_ID) return; if(m.type === 'hello') presenceChannel.postMessage({type: 'beat', id: WIN_ID}); if(m.type === 'hello' || m.type === 'beat') foreignBeats.set(m.id, Date.now()); if(m.type === 'bye'){ foreignBeats.delete(m.id); tabModalDismissed.delete(m.id); } updateTabModal(); }; presenceChannel.postMessage({type: 'hello', id: WIN_ID}); setInterval(() => { presenceChannel.postMessage({type: 'beat', id: WIN_ID}); for(const [id, wann] of foreignBeats) if(Date.now() - wann > FOREIGN_DEAD_MS){ foreignBeats.delete(id); tabModalDismissed.delete(id); } updateTabModal(); }, 1000); addEventListener('pagehide', () => { try{ presenceChannel.postMessage({type: 'bye', id: WIN_ID}); }catch(_){} }); } function updateTabModal(){ const offen = [...foreignBeats.keys()].some(id => !tabModalDismissed.has(id)); if(offen && !tabModalEl){ const ov = document.createElement('div'); ov.className = 'tabmodal-overlay'; ov.innerHTML = '