Kein Fenster schreibt mehr den Schlüssel eines anderen: Text, Meta und frühere Stände liegen je Dokument unter eigenen Schlüsseln, der Index ist nur noch Reihenfolge-Hinweis und löscht nie, Löschungen hinterlassen einen Tombstone (7 Tage). Der Voll-Flush weicht dem Dirty-Flush. Damit sind Befund 2 und 3 des RFC weg — verschiedene Dokumente in zwei Fenstern kollidieren gar nicht mehr. docsync.js zieht Liste, Namen, Tombstones und den Stände-Cache aus dem storage-Ereignis nach; am aktiven Dokument die drei Fälle umbenannt / anderswo gelöscht (behalten, bis getippt wird) / fremd geschrieben. Der modale D89-Dialog samt Präsenz-Kanal ist ersatzlos raus; den einen Verlustfall — dasselbe nicht-`live:`-Dokument in beiden Fenstern vorn — findet eine Sperre per Web Locks, und das zweite Fenster bekommt drei Auswege statt „trotzdem fortfahren". Im Browser mit zwei echten Tabs nachgemessen (RFC §10): Fälle 1–8 grün, Fall 1 im Vor-v3-Build gegengeprüft und dort nachweislich rot. Fall 9 deckte auf, dass reviveGoneDoc() den Index-Hinweis nicht mitschrieb — ein durch Tippen wiederbelebtes Dokument hing bis zum nächsten Flush allein an seinen eigenen Schlüsseln und wäre bei einem Rückbau auf einen Build vor v3 still verlorengegangen; behoben, Gegenprobe per Mutation gezogen. Headless 666 Tests grün. Offen bleibt die Handarbeit: PWA neben Tab, Firefox, Safari, ein Browser ohne Locks-API. SPEC §9, D94, D89-Nachtrag, RFC (Status, §10, Revisionsgeschichte), CHANGELOG und Plan-Knoten #ed.docs.windows nachgezogen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
7.4 KiB
JavaScript
133 lines
7.4 KiB
JavaScript
/* Vereinheitlichtes Warnungs-Modell.
|
|
Producer (Renderer, künftig auch Parser) liefern strukturierte Objekte
|
|
{ type, line, ...data }
|
|
statt fertig formatierter Strings — so bleiben Typ und Zeilennummer
|
|
maschinenlesbar (sortierbar, filterbar, testbar). Die i18n-/HTML-Aufbereitung
|
|
passiert an genau einer Stelle: formatWarning(). Vgl. SPEC §3 (gemischte
|
|
Gates) und TASKS Phase 2 (unbekannte Statuszeichen).
|
|
|
|
Bekannte Typen:
|
|
- mixedGate { line, label } — Geschwister mit gemischtem Gate (SPEC §3)
|
|
- unknownStatus { line, code } — unbekanntes Statuszeichen (Phase 2)
|
|
- xorConflict { line, label } — weitere realisierte Alternative in einer
|
|
`=`-Gruppe (SPEC §3/D35); je Zeile eine
|
|
Warnung, damit sie dorthin zeigt
|
|
- duplicateId { line, id, firstLine } — Knoten-ID doppelt vergeben
|
|
(SPEC §1/D36); gemeldet an der späteren
|
|
Zeile, die erste wird genannt
|
|
- unknownDep { line, id } — Abhängigkeit auf eine ID ohne Knoten
|
|
(SPEC §1/D37); Zyklen sind dagegen
|
|
zulässig und warnen nie
|
|
- unknownDesc { line, id } — Beschreibungsblock für eine ID ohne
|
|
Knoten (SPEC §11/D40)
|
|
- descStray { line } — Beschreibungszeile ohne Bezug: `"`-Zeile
|
|
ohne Knoten davor bzw. Zeile im
|
|
Beschreibungsteil ohne #id-Block; meldet
|
|
auch die von einem versehentlichen
|
|
`---` verschluckten Knotenzeilen
|
|
- sizeConflict { line, size } — Teilpakete übersteigen zusammen die
|
|
angegebene Größe des Elternknotens, selbst
|
|
in der günstigsten Bereichs-Lesart
|
|
(SPEC §5/D62); Zeile = Elternknoten
|
|
- assigneeOverload { tag, share, stations, totalStations }
|
|
— eine Person trägt mehr als die Hälfte der
|
|
offenen Arbeit des günstigsten Pfads
|
|
(SPEC §9/D71); zeilenlos — der Engpass
|
|
hat keine einzelne Zeile
|
|
- cheapApprox { } — günstigster Pfad nur gierig geschätzt:
|
|
zu viele gekoppelte Gruppen für die
|
|
exakte Suche (D42); zeilenlos
|
|
- sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne
|
|
Zeilennummer, erscheint dadurch zuoberst
|
|
- padGone { } — ein alter ?etherpad=-Link: die Anbindung
|
|
ist ausgebaut (D78), der Text zeigt auf
|
|
?live=; zeilenlos
|
|
- liveLoad { url, error } — Server-Dokument (?live=, D76) nicht
|
|
erreichbar oder keine Dokument-Adresse
|
|
- liveStale { error } — eigene Änderung nicht anwendbar
|
|
(Prüfsumme, Index, Basis verdichtet);
|
|
der Client hat einmal neu geladen (D76)
|
|
- storeFailed { } — Speichern im Browser fehlgeschlagen
|
|
(localStorage-Quota, D82): Datenverlust
|
|
droht beim Neuladen; zeilenlos, steht
|
|
zuoberst und verschwindet, sobald ein
|
|
Schreiben wieder gelingt
|
|
- tabConflict { name, where } — dasselbe nicht geteilte Dokument wird
|
|
in einem anderen Fenster bearbeitet
|
|
(RFC 002/D94, der Restfall): der letzte
|
|
Tastendruck gewinnt; zeilenlos, bleibt
|
|
stehen, solange das Dokument aktiv ist.
|
|
`where` ist ein i18n-Schlüssel für
|
|
„in der App" / „im Browser" — aus der
|
|
Sicht des eigenen Fensters gefolgert
|
|
- liveUnsent { min } — Änderungen an einem Server-Dokument seit
|
|
{min} Minuten nicht angekommen (D89):
|
|
sie existieren nur in diesem Fenster;
|
|
zeilenlos, zuoberst, verschwindet mit
|
|
dem nächsten erfolgreichen Abgleich
|
|
- liveEnded { } — die Live-Sitzung ist beendet, ohne dass
|
|
der Nutzer sie beendet hat (D89):
|
|
Änderungen werden nicht mehr geteilt */
|
|
|
|
import { esc } from './render.js';
|
|
|
|
/* Strukturierte Warnung -> lokalisierter Anzeigetext (HTML-escaped Daten).
|
|
`t` ist die i18n-Funktion (key, vars) -> String. */
|
|
export function formatWarning(w, t){
|
|
return build(w, t, esc);
|
|
}
|
|
|
|
/* Dasselbe als **Klartext**, ohne HTML-Escaping — für Senken, die kein HTML
|
|
sind: der `title` der Zeilennummer (D33-Nachtrag). Dort stünde sonst
|
|
„Drag & Drop“ statt „Drag & Drop“; die Vorlagen selbst enthalten kein
|
|
Markup, escaped werden ohnehin nur die eingesetzten Daten. */
|
|
export function warningText(w, t){
|
|
return build(w, t, s => String(s));
|
|
}
|
|
|
|
/* Der Parameter verdeckt das importierte `esc` bewusst: So bleibt der Rumpf
|
|
unverändert und es gibt nur **eine** Stelle, die die Typen kennt. */
|
|
function build(w, t, esc){
|
|
switch(w.type){
|
|
case 'mixedGate':
|
|
return t('mixedWarn', {line: w.line, label: esc(w.label)});
|
|
case 'unknownStatus':
|
|
return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
|
|
case 'xorConflict':
|
|
return t('xorConflictWarn', {line: w.line, label: esc(w.label)});
|
|
case 'duplicateId':
|
|
return t('duplicateIdWarn', {line: w.line, id: esc(w.id), firstLine: w.firstLine});
|
|
case 'unknownDep':
|
|
return t('unknownDepWarn', {line: w.line, id: esc(w.id)});
|
|
case 'unknownDesc':
|
|
return t('unknownDescWarn', {line: w.line, id: esc(w.id)});
|
|
case 'descStray':
|
|
return t('descStrayWarn', {line: w.line});
|
|
case 'sizeConflict':
|
|
return t('sizeConflictWarn', {line: w.line, size: esc(w.size)});
|
|
case 'assigneeOverload':
|
|
return t('assigneeOverloadWarn', {tag: esc(w.tag), share: w.share,
|
|
stations: w.stations, total: w.totalStations});
|
|
case 'cheapApprox':
|
|
return t('cheapApproxWarn');
|
|
case 'sourceLoad':
|
|
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
|
case 'padGone':
|
|
return t('padGoneWarn');
|
|
case 'liveLoad':
|
|
return t('liveLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
|
case 'liveStale':
|
|
return t('liveStaleWarn', {error: esc(w.error)});
|
|
case 'storeFailed':
|
|
return t('storeFailedWarn');
|
|
case 'tabConflict':
|
|
return t('tabConflictWarn', {name: esc(w.name || ''), where: t(w.where || 'windowKindBrowser')});
|
|
case 'liveUnsent':
|
|
return t('liveUnsentWarn', {min: w.min});
|
|
case 'liveEnded':
|
|
return t('liveEndedWarn');
|
|
default:
|
|
return `${esc(String(w.type))} (${w.line ?? '?'})`;
|
|
}
|
|
}
|