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, refParts, ticketUrl, ticketRefAt, ticketApiPath, mapTaigaStatus, taigaStatusName, pickStatus, statusApiPath, statusListPath, storyAncestor, collectTicketRefs, bulkPath, ticketDiverges, refVisibleInLabel, parseTicketInput, foreignTaigaUrl } 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 { SNAP_EVERY, addSnapshot, persistSnaps, snapLabel, readAllSnaps, readSnapList } 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, writeDoc, writeIndexHint, removeDoc, expireTombstones, migrateV3, storeDocText, readMeta, hasTombstone, isDocKey, LS_ACTIVE, LS_SRC, DOC_TEXT_PREFIX, DOC_META_PREFIX, DOC_GONE_PREFIX } from './docstore.js'; import { applyStorageEvent, lockDecision, lockName, createDirtySet } from './docsync.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 ANDERES Fenster schreibt in die Dokument-Ablage (D84, nach RFC 002 §6.3/§6.6): Das storage-Ereignis feuert nur in fremden Fenstern desselben Ursprungs — genau das richtige Signal. Die Warnung `tabConflict` erscheint aber NUR noch im Restfall (dasselbe nicht-`live:`-Dokument in beiden Fenstern — `foreignWrite` aus docsync.js); verschiedene Dokumente kollidieren nicht mehr und bleiben still. Sie bleibt stehen, solange das Dokument aktiv ist, und nennt es; die Lage ändert sich nicht dadurch, dass der andere gerade nichts tippt. */ let tabWarning = null; /* Ereignisse, die während des Starts ankommen (das andere Fenster tippt, während dieses hochläuft): sammeln und nach dem Boot nachziehen — der Handler ist damit registriert, bevor der erste Flush schreibt (§6.10). */ const storageBacklog = []; window.addEventListener('storage', e => { if(!e.key || !isDocKey(e.key)) return; const ev = {key: e.key, oldValue: e.oldValue, newValue: e.newValue}; if(bootDone) applyForeignStorageEvent(ev); else storageBacklog.push(ev); }); /* Wachhund-Warnung (D89): Ungesendete Live-Änderungen bzw. tote Sitzung. Gesetzt vom 5-s-Takt im Live-Abschnitt, gelesen von render(). */ let liveWarning = null; /* ---------- Sperre je Dokument (RFC 002 §6.4/§6.5, D94) ---------- Der einzige Restfall — dasselbe nicht-`live:`-Dokument in beiden Fenstern vorn — wird über die Web Locks API erkannt: atomar, sie fällt von selbst, wenn das Fenster schließt oder abstürzt, und ein wartender Request weckt das zweite Fenster, sobald der Halter loslässt. Der Präsenz-Kanal aus D89 (Herzschlag, Timeout, Notluke, modaler Dialog) ist damit ersatzlos weg. Ohne Locks-API (file://, alte Browser) bleibt der storage-Rückfall: Ein `foreignWrite` am eigenen aktiven Dokument ist der Beweis, dass ein zweites Fenster darin schreibt — dann Warnung, ohne Dialog. */ const hasLocks = typeof navigator !== 'undefined' && !!navigator.locks; let lockHeldId = null; /* id, für die die Sperre gehalten wird */ let lockTargetId = null; /* id der letzten Anfrage (syncDocLock vergleicht) */ let lockRelease = null; /* löst die gehaltene Sperre (Callback-Promise) */ let lockGen = 0; /* Generation: ungültigt ausstehende Anfragen */ let viewOnly = false; /* Ausweg 2: „hier nur ansehen" (§6.5) */ /* „Nur ansehen" gilt dem DOKUMENT, nicht nur dem Textfeld: Auch das Falten im Diagramm schreibt in den Text (D38-Nachtrag 2), und ein `readonly` am Textfeld hält das nicht auf — `replaceTextUndoable` fällt auf `value =` zurück, wenn `execCommand` scheitert, und das schreibt auch in ein schreibgeschütztes Feld (nachgemessen: die Faltmarke landete im Speicher). Die Klasse macht den Zustand für das CSS sichtbar; die Sperre selbst sitzt an den Schreibstellen (D94-Nachtrag 3). */ function setViewOnly(on){ viewOnly = on; document.body.classList.toggle('viewonly', on); } let docGoneElsewhere = false; /* aktives Dokument anderswo gelöscht (§6.5) */ let lockDialogEl = null; /* Dialog über dem Editor (§6.5) */ function clearTabConflict(){ if(!tabWarning) return; tabWarning = null; render(); } function releaseDocLock(){ if(lockRelease){ const r = lockRelease; lockRelease = null; lockHeldId = null; r(); } } /* Die Sperre folgt dem aktiven Dokument: Alte fallen, neue werden angefordert (ifAvailable). Sie wird gehalten, solange das Dokument aktiv ist — die Callback-Promise bleibt offen; der nächste Wechsel löst sie. */ function syncDocLock(){ const d = activeDoc(); const id = d ? d.id : null; if(id === lockTargetId) return; /* schon gehalten oder angefragt */ acquireDocLock(id); } function acquireDocLock(id){ releaseDocLock(); lockTargetId = id; docGoneElsewhere = false; setViewOnly(false); src.readOnly = false; /* im Zweifel beschreibbar; der Dialog setzt es zurück */ closeLockDialog(); clearTabConflict(); /* live: nie, ohne Locks-API nie — dort warnt der storage-Rückfall (§6.4). */ if(!id || !lockDecision(id) || !hasLocks) return; const gen = ++lockGen; navigator.locks.request(lockName(id), {ifAvailable: true}, lock => { if(gen !== lockGen) return; /* inzwischen gewechselt */ if(!lock){ enterLockDialog(id); return; } /* ein anderes Fenster hält es */ lockHeldId = id; return new Promise(res => { lockRelease = res; lockArrived(id); }); }).catch(() => {}); } /* Die Sperre ist da — vom ersten Anfordern oder weil das andere Fenster losgelassen hat: beschreibbar, Label normal, Warnung räumt sich (§6.6). */ function lockArrived(id){ if(id !== activeId) return; /* inzwischen weggewechselt */ lockHeldId = id; setViewOnly(false); src.readOnly = false; closeLockDialog(); clearTabConflict(); updateDocName(); } function waitInBackground(id){ if(!hasLocks || id == null) return; const gen = lockGen; /* ungültig, sobald das Dokument wechselt */ navigator.locks.request(lockName(id), lock => { if(gen !== lockGen || !lock) return; lockHeldId = id; return new Promise(res => { lockRelease = res; lockArrived(id); }); }).catch(() => {}); } /* Die Sperre wurde NICHT bekommen — der Restfall. Dialog über dem Editor, drei Auswege (§6.5); das Textfeld ist bis zur Wahl schreibgeschützt, Diagramm und Menü bleiben bedienbar. */ function enterLockDialog(id){ if(id !== activeId) return; src.readOnly = true; closeLockDialog(); const d = docs.find(x => x.id === id); if(!d) return; /* inzwischen weg — nichts tun */ const ov = document.createElement('div'); ov.className = 'lockdlg-overlay'; ov.innerHTML = '