fix(paket a): URL-Reload ohne Rückfrage, Letztes-Dokument-Hinweis, Fremd-Tab-Warnung (D84)

1. Neu laden fragt bei URL-Dokumenten nicht mehr (F5 verwirft ohnehin
   still, D23); bei Datei-Dokumenten bleibt die Rückfrage. Dabei gefunden
   und behoben: Der Knopf erschien auch bei Server-Dokumenten und hätte
   die JSON-Antwort der API als Text an alle gepusht.
2. Die Rückfrage beim letzten Dokument sagt, dass danach wieder das
   Beispiel dasteht (docDeleteLastConfirm, 9 Sprachen).
3. Ein zweiter Tab, der in dieselbe Ablage schreibt, wird per
   storage-Ereignis erkannt und gemeldet (tabConflict, isDocKey in
   docstore.js) — kein Sync, aber kein stiller Verlust mehr.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-27 08:02:15 +02:00
co-authored by Claude Fable 5
parent 83dc329b43
commit c438a88344
6 changed files with 128 additions and 7 deletions
+4
View File
@@ -19,6 +19,10 @@ reverse.
## 2026-08-27 ## 2026-08-27
- If Werkbaum is open in a second browser tab, a warning says so — both tabs write the same document list, and the last one to save wins
- Reloading a URL document no longer asks — a page reload discards local changes silently anyway, so the question promised a protection that did not exist
- Deleting your last document now says that a fresh example will take its place
- Fix: the reload button also showed for shared documents and would have loaded the API's JSON answer as text, pushing it to everyone
- An owner password for shared plans recorded as planned: creating a document returns it once, management will bind to it, an admin can reclaim - An owner password for shared plans recorded as planned: creating a document returns it once, management will bind to it, an admin can reclaim
- Shared documents are left, not deleted: their row shows a leave action with its own icon and wording — the document stays on the server, and its link lands on the clipboard - Shared documents are left, not deleted: their row shows a leave action with its own icon and wording — the document stays on the server, and its link lands on the clipboard
- The document menu's groups are named Included, Local and Shared now — "yours" and "sources" said less than where a document lives - The document menu's groups are named Included, Local and Shared now — "yours" and "sources" said less than where a document lives
+44
View File
@@ -7246,3 +7246,47 @@ Papierkorb. Werkzeug-Lehre am Rande: Ein noch offener, veralteter Test-Tab
flushte bei jedem Sichtbarkeitswechsel seinen alten Speicherstand über die flushte bei jedem Sichtbarkeitswechsel seinen alten Speicherstand über die
Inszenierung (Last-Writer-Wins zwischen Tabs — kein neues Verhalten, aber Inszenierung (Last-Writer-Wins zwischen Tabs — kein neues Verhalten, aber
eine Falle für Speicher-Tests: alte Tabs erst schließen). eine Falle für Speicher-Tests: alte Tabs erst schließen).
## D84 — Drei kleine Ungereimtheiten und ein Fund: Reload, letztes Dokument, fremder Tab
Aus der Nutzer-Frage „Gibt es noch weitere ähnliche Ungereimtheiten?" entstand
eine Liste von zehn Punkten; die Entscheidungen dazu fielen als
Multiple-Choice-Runde, umgesetzt in drei Paketen. Dies ist **Paket A** — die
drei kleinen, nur im Frontend:
**1. Der Neu-laden-Knopf fragt bei URL-Dokumenten nicht mehr nach.** Die
Rückfrage („lokale Änderungen gehen verloren") versprach einen Schutz, den es
nicht gibt: Jedes Neuladen der **Seite** verwirft lokale Änderungen an einem
URL-Dokument ohnehin still — die URL ist die Quelle der Wahrheit (D23). Bei
**Datei-Dokumenten** bleibt die Rückfrage: Dort gibt es den stillen
Konkurrenzweg nicht.
**Dabei ein echter Fehler gefunden: Der Neu-laden-Knopf erschien auch bei
Server-Dokumenten** — `adoptLive()` setzt `source`, und der Knopf prüfte nur
darauf. Ein Druck hätte die **JSON-Antwort der API** als Notationstext geladen
und per Live-Diff an alle Mitschreiber gepusht. Jetzt sind `live:`-Dokumente
an beiden Stellen ausgenommen (Knopf-Sichtbarkeit und `reloadDoc()` selbst) —
der Feed hält sie ohnehin aktuell.
**2. Die Rückfrage beim letzten Dokument sagt, was danach dasteht.** Der
Editor steht nie leer (D22): Wer sein letztes Dokument löscht, bekommt still
ein frisches Beispiel. Die Rückfrage trägt das jetzt („Es ist das letzte —
danach steht wieder das mitgelieferte Beispiel da.", `docDeleteLastConfirm`,
neun Sprachen); das Verhalten bleibt unverändert.
**3. Ein fremder Tab wird erkannt und gemeldet.** Zwei offene Tabs schreiben
in dieselbe Dokument-Ablage, der letzte Flush gewinnt — schon immer so, aber
nirgends benannt (bei den D83-Tests dreimal hineingelaufen). Das
`storage`-Ereignis feuert genau in den **anderen** Tabs desselben Ursprungs;
`isDocKey()` (docstore.js, getestet) filtert auf Dokument-Schlüssel, und die
zeilenlose Warnung `tabConflict` bleibt bis zum Neuladen stehen — die Lage
ändert sich nicht dadurch, dass der andere Tab gerade pausiert. Bewusst
**kein** Sync: Erkennen ist der Schritt, der stillen Verlust beendet; ein
echter Tab-Abgleich wäre ein eigener Umbau mit eigenen Konfliktfragen.
**Nachgemessen** im Browser: URL-Dokument lädt am Knopf ohne Rückfrage frisch
(0 confirm-Aufrufe, Text ersetzt); beim aktiven Server-Dokument ist der Knopf
verborgen; die normale und die Letztes-Dokument-Rückfrage tragen die
richtigen Texte, Abbruch lässt alles stehen; und die Tab-Warnung erschien
durch **echtes** Tippen in einem zweiten Tab (kein synthetisches Ereignis —
das storage-Ereignis kommt nur aus fremden Fenstern). 531 Tests, davon 2 neue
für `isDocKey`.
+47 -5
View File
@@ -9,7 +9,7 @@ import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel
from './snapshots.js'; from './snapshots.js';
import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.js'; import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.js';
import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch, docKind } from './docurl.js'; import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch, docKind } from './docurl.js';
import { readDocs, storeDocs, storeDocText, LS_DOCS, LS_ACTIVE, LS_SRC } from './docstore.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 /* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
vite.config.js). Zur Laufzeit gibt es kein git und keinen Server, der vite.config.js). Zur Laufzeit gibt es kein git und keinen Server, der
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */ nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */
@@ -87,6 +87,18 @@ let sourceWarning = null;
Verlust fiel erst beim Neuladen auf. Persistent, bis ein Schreiben wieder Verlust fiel erst beim Neuladen auf. Persistent, bis ein Schreiben wieder
gelingt; zeilenlos und zuoberst. */ gelingt; zeilenlos und zuoberst. */
let storeWarning = null; 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();
});
function noteStore(ok){ function noteStore(ok){
const neu = ok ? null : {type: 'storeFailed'}; const neu = ok ? null : {type: 'storeFailed'};
const wechsel = (!!storeWarning) !== (!!neu); const wechsel = (!!storeWarning) !== (!!neu);
@@ -148,6 +160,7 @@ function render(){
(?sourceUrl nicht ladbar, D23) gehört keiner Zeile und bleibt über (?sourceUrl nicht ladbar, D23) gehört keiner Zeile und bleibt über
Neu-Renderings bestehen, bis das Laden gelingt. */ Neu-Renderings bestehen, bis das Laden gelingt. */
let warnings = sourceWarning ? [sourceWarning].concat(parsed.warnings) : parsed.warnings; let warnings = sourceWarning ? [sourceWarning].concat(parsed.warnings) : parsed.warnings;
if(tabWarning) warnings = [tabWarning].concat(warnings); /* fremder Tab schreibt mit (D84) */
if(storeWarning) warnings = [storeWarning].concat(warnings); /* Datenverlust droht — zuoberst (D82) */ if(storeWarning) warnings = [storeWarning].concat(warnings); /* Datenverlust droht — zuoberst (D82) */
if(!roots.length){ if(!roots.length){
@@ -2260,6 +2273,7 @@ const I18N = {
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen", docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
docNewName:"Unbenannt", docNewName:"Unbenannt",
docDeleteConfirm:"Dokument „{name}“ löschen?", docDeleteConfirm:"Dokument „{name}“ löschen?",
docDeleteLastConfirm:"Dokument „{name}“ löschen? Es ist das letzte — danach steht wieder das mitgelieferte Beispiel da.",
docLeave:"Verlassen nur aus der eigenen Liste entfernen", docLeave:"Verlassen nur aus der eigenen Liste entfernen",
docLeaveConfirm:"„{name}“ verlassen? Es wird nur aus deiner Liste entfernt — auf dem Server bleibt es bestehen, und sein Link liegt danach in der Zwischenablage.", docLeaveConfirm:"„{name}“ verlassen? Es wird nur aus deiner Liste entfernt — auf dem Server bleibt es bestehen, und sein Link liegt danach in der Zwischenablage.",
docRestore:"Original wiederherstellen", docRestore:"Original wiederherstellen",
@@ -2315,6 +2329,7 @@ const I18N = {
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).", sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
padGoneWarn:"Dieser Link zeigt auf ein Etherpad-Pad. Die Etherpad-Anbindung gibt es nicht mehr — gemeinsames Arbeiten läuft jetzt über ein Werkbaum-Backend (?live=…). Der Text des Pads lässt sich dort einmal einfügen und dann zu zweit bearbeiten.", padGoneWarn:"Dieser Link zeigt auf ein Etherpad-Pad. Die Etherpad-Anbindung gibt es nicht mehr — gemeinsames Arbeiten läuft jetzt über ein Werkbaum-Backend (?live=…). Der Text des Pads lässt sich dort einmal einfügen und dann zu zweit bearbeiten.",
storeFailedWarn:"Speichern im Browser fehlgeschlagen — vermutlich ist der Speicher voll. Änderungen können beim Neuladen verloren gehen; Platz schaffen: nicht mehr gebrauchte Dokumente oder frühere Stände löschen.", storeFailedWarn:"Speichern im Browser fehlgeschlagen — vermutlich ist der Speicher voll. Änderungen können beim Neuladen verloren gehen; Platz schaffen: nicht mehr gebrauchte Dokumente oder frühere Stände löschen.",
tabConflictWarn:"Werkbaum ist in einem weiteren Browser-Tab geöffnet — beide schreiben in dieselbe Dokumentenliste, und der zuletzt speichernde gewinnt. Am besten nur in einem Tab arbeiten.",
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt", a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", 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",
@@ -2376,6 +2391,7 @@ const I18N = {
docNew:"New document", docRename:"Rename", docDelete:"Delete", docNew:"New document", docRename:"Rename", docDelete:"Delete",
docNewName:"Untitled", docNewName:"Untitled",
docDeleteConfirm:"Delete document “{name}”?", docDeleteConfirm:"Delete document “{name}”?",
docDeleteLastConfirm:"Delete document “{name}”? It is the last one — the shipped example will take its place.",
docLeave:"Leave removes it from your list only", docLeave:"Leave removes it from your list only",
docLeaveConfirm:"Leave “{name}”? It is only removed from your list — it stays on the server, and its link is placed on your clipboard.", docLeaveConfirm:"Leave “{name}”? It is only removed from your list — it stays on the server, and its link is placed on your clipboard.",
docRestore:"Restore original", docRestore:"Restore original",
@@ -2431,6 +2447,7 @@ const I18N = {
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
padGoneWarn:"This link points at an Etherpad pad. The Etherpad connection is gone — collaboration now runs through a Werkbaum backend (?live=…). Paste the pads text there once, then edit it together.", padGoneWarn:"This link points at an Etherpad pad. The Etherpad connection is gone — collaboration now runs through a Werkbaum backend (?live=…). Paste the pads text there once, then edit it together.",
storeFailedWarn:"Saving in the browser failed — its storage is probably full. Changes may be lost on reload; free space by deleting unused documents or earlier states.", storeFailedWarn:"Saving in the browser failed — its storage is probably full. Changes may be lost on reload; free space by deleting unused documents or earlier states.",
tabConflictWarn:"Werkbaum is open in another browser tab — both write to the same document list, and the last one to save wins. Best work in a single tab.",
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link", a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", 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",
@@ -2492,6 +2509,7 @@ const I18N = {
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar", docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
docNewName:"Sin título", docNewName:"Sin título",
docDeleteConfirm:"¿Eliminar el documento «{name}»?", docDeleteConfirm:"¿Eliminar el documento «{name}»?",
docDeleteLastConfirm:"¿Eliminar el documento «{name}»? Es el último: el ejemplo incluido volverá a ocupar su lugar.",
docLeave:"Salir solo se quita de tu lista", docLeave:"Salir solo se quita de tu lista",
docLeaveConfirm:"¿Salir de «{name}»? Solo se quita de tu lista: sigue en el servidor y su enlace queda en el portapapeles.", docLeaveConfirm:"¿Salir de «{name}»? Solo se quita de tu lista: sigue en el servidor y su enlace queda en el portapapeles.",
docRestore:"Restaurar original", docRestore:"Restaurar original",
@@ -2547,6 +2565,7 @@ const I18N = {
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
padGoneWarn:"Este enlace apunta a un pad de Etherpad. La conexión con Etherpad ya no existe: ahora se colabora a través de un backend de Werkbaum (?live=…). Pega allí el texto del pad una vez y editadlo juntos.", padGoneWarn:"Este enlace apunta a un pad de Etherpad. La conexión con Etherpad ya no existe: ahora se colabora a través de un backend de Werkbaum (?live=…). Pega allí el texto del pad una vez y editadlo juntos.",
storeFailedWarn:"No se pudo guardar en el navegador: su almacenamiento probablemente está lleno. Los cambios pueden perderse al recargar; libera espacio borrando documentos sin uso o estados anteriores.", storeFailedWarn:"No se pudo guardar en el navegador: su almacenamiento probablemente está lleno. Los cambios pueden perderse al recargar; libera espacio borrando documentos sin uso o estados anteriores.",
tabConflictWarn:"Werkbaum está abierto en otra pestaña — ambas escriben en la misma lista de documentos y gana la última en guardar. Mejor trabaja en una sola pestaña.",
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace", a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", 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",
@@ -2608,6 +2627,7 @@ const I18N = {
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer", docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
docNewName:"Sans titre", docNewName:"Sans titre",
docDeleteConfirm:"Supprimer le document « {name} » ?", docDeleteConfirm:"Supprimer le document « {name} » ?",
docDeleteLastConfirm:"Supprimer le document « {name} » ? C'est le dernier — l'exemple fourni reprendra sa place.",
docLeave:"Quitter ne le retire que de votre liste", docLeave:"Quitter ne le retire que de votre liste",
docLeaveConfirm:"Quitter « {name} » ? Il n'est retiré que de votre liste — il reste sur le serveur et son lien est copié dans le presse-papiers.", docLeaveConfirm:"Quitter « {name} » ? Il n'est retiré que de votre liste — il reste sur le serveur et son lien est copié dans le presse-papiers.",
docRestore:"Restaurer loriginal", docRestore:"Restaurer loriginal",
@@ -2663,6 +2683,7 @@ const I18N = {
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
padGoneWarn:"Ce lien pointe vers un pad Etherpad. La connexion Etherpad nexiste plus — la collaboration passe désormais par un backend Werkbaum (?live=…). Collez-y une fois le texte du pad, puis modifiez-le à plusieurs.", padGoneWarn:"Ce lien pointe vers un pad Etherpad. La connexion Etherpad nexiste plus — la collaboration passe désormais par un backend Werkbaum (?live=…). Collez-y une fois le texte du pad, puis modifiez-le à plusieurs.",
storeFailedWarn:"L'enregistrement dans le navigateur a échoué — son stockage est probablement plein. Les modifications peuvent être perdues au rechargement ; libérez de l'espace en supprimant des documents inutilisés ou des états antérieurs.", storeFailedWarn:"L'enregistrement dans le navigateur a échoué — son stockage est probablement plein. Les modifications peuvent être perdues au rechargement ; libérez de l'espace en supprimant des documents inutilisés ou des états antérieurs.",
tabConflictWarn:"Werkbaum est ouvert dans un autre onglet — les deux écrivent dans la même liste de documents et le dernier à enregistrer l'emporte. Mieux vaut travailler dans un seul onglet.",
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien", a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", 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",
@@ -2724,6 +2745,7 @@ const I18N = {
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń", docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
docNewName:"Bez nazwy", docNewName:"Bez nazwy",
docDeleteConfirm:"Usunąć dokument „{name}”?", docDeleteConfirm:"Usunąć dokument „{name}”?",
docDeleteLastConfirm:"Usunąć dokument „{name}”? To ostatni — jego miejsce zajmie dołączony przykład.",
docLeave:"Opuść usuwa tylko z twojej listy", docLeave:"Opuść usuwa tylko z twojej listy",
docLeaveConfirm:"Opuścić „{name}”? Zniknie tylko z twojej listy — na serwerze pozostaje, a jego link trafi do schowka.", docLeaveConfirm:"Opuścić „{name}”? Zniknie tylko z twojej listy — na serwerze pozostaje, a jego link trafi do schowka.",
docRestore:"Przywróć oryginał", docRestore:"Przywróć oryginał",
@@ -2779,6 +2801,7 @@ const I18N = {
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
padGoneWarn:"Ten link prowadzi do pada Etherpad. Połączenia z Etherpadem już nie ma — wspólna praca odbywa się teraz przez backend Werkbaum (?live=…). Wklej tam raz tekst pada i edytujcie go razem.", padGoneWarn:"Ten link prowadzi do pada Etherpad. Połączenia z Etherpadem już nie ma — wspólna praca odbywa się teraz przez backend Werkbaum (?live=…). Wklej tam raz tekst pada i edytujcie go razem.",
storeFailedWarn:"Zapis w przeglądarce nie powiódł się — jego pamięć jest zapewne pełna. Zmiany mogą przepaść przy przeładowaniu; zwolnij miejsce, usuwając nieużywane dokumenty lub wcześniejsze stany.", storeFailedWarn:"Zapis w przeglądarce nie powiódł się — jego pamięć jest zapewne pełna. Zmiany mogą przepaść przy przeładowaniu; zwolnij miejsce, usuwając nieużywane dokumenty lub wcześniejsze stany.",
tabConflictWarn:"Werkbaum jest otwarty w innej karcie — obie zapisują tę samą listę dokumentów i wygrywa ta, która zapisze ostatnia. Najlepiej pracować w jednej karcie.",
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem", a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", 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ą",
@@ -2840,6 +2863,7 @@ const I18N = {
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить", docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
docNewName:"Без названия", docNewName:"Без названия",
docDeleteConfirm:"Удалить документ «{name}»?", docDeleteConfirm:"Удалить документ «{name}»?",
docDeleteLastConfirm:"Удалить документ «{name}»? Он последний — его место снова займёт встроенный пример.",
docLeave:"Покинуть – удаляется только из вашего списка", docLeave:"Покинуть – удаляется только из вашего списка",
docLeaveConfirm:"Покинуть «{name}»? Он исчезнет только из вашего списка — на сервере он останется, а его ссылка будет в буфере обмена.", docLeaveConfirm:"Покинуть «{name}»? Он исчезнет только из вашего списка — на сервере он останется, а его ссылка будет в буфере обмена.",
docRestore:"Восстановить оригинал", docRestore:"Восстановить оригинал",
@@ -2895,6 +2919,7 @@ const I18N = {
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).", sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
padGoneWarn:"Эта ссылка ведёт на пад Etherpad. Подключения к Etherpad больше нет — совместная работа теперь идёт через бэкенд Werkbaum (?live=…). Вставьте туда текст пада один раз и редактируйте вместе.", padGoneWarn:"Эта ссылка ведёт на пад Etherpad. Подключения к Etherpad больше нет — совместная работа теперь идёт через бэкенд Werkbaum (?live=…). Вставьте туда текст пада один раз и редактируйте вместе.",
storeFailedWarn:"Не удалось сохранить в браузере — его хранилище, вероятно, заполнено. Изменения могут потеряться при перезагрузке; освободите место, удалив ненужные документы или прежние состояния.", storeFailedWarn:"Не удалось сохранить в браузере — его хранилище, вероятно, заполнено. Изменения могут потеряться при перезагрузке; освободите место, удалив ненужные документы или прежние состояния.",
tabConflictWarn:"Werkbaum открыт в другой вкладке — обе пишут в один список документов, и побеждает та, что сохранит последней. Лучше работать в одной вкладке.",
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой", a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.", hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну", hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
@@ -2956,6 +2981,7 @@ const I18N = {
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ", docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
docNewName:"बिना शीर्षक", docNewName:"बिना शीर्षक",
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?", docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
docDeleteLastConfirm:"दस्तावेज़ „{name}“ हटाएँ? यह आख़िरी है — इसकी जगह फिर से साथ आया उदाहरण आ जाएगा।",
docLeave:"छोड़ें – केवल आपकी सूची से हटता है", docLeave:"छोड़ें – केवल आपकी सूची से हटता है",
docLeaveConfirm:"„{name}“ छोड़ें? यह केवल आपकी सूची से हटेगा — सर्वर पर बना रहेगा, और इसका लिंक क्लिपबोर्ड में रख दिया जाएगा।", docLeaveConfirm:"„{name}“ छोड़ें? यह केवल आपकी सूची से हटेगा — सर्वर पर बना रहेगा, और इसका लिंक क्लिपबोर्ड में रख दिया जाएगा।",
docRestore:"मूल पुनर्स्थापित करें", docRestore:"मूल पुनर्स्थापित करें",
@@ -3011,6 +3037,7 @@ const I18N = {
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।", sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
padGoneWarn:"यह लिंक एक Etherpad पैड की ओर इशारा करता है। Etherpad कनेक्शन अब नहीं है — साझा काम अब Werkbaum बैकएंड (?live=…) से होता है। पैड का टेक्स्ट वहाँ एक बार चिपकाएँ और मिलकर संपादित करें।", padGoneWarn:"यह लिंक एक Etherpad पैड की ओर इशारा करता है। Etherpad कनेक्शन अब नहीं है — साझा काम अब Werkbaum बैकएंड (?live=…) से होता है। पैड का टेक्स्ट वहाँ एक बार चिपकाएँ और मिलकर संपादित करें।",
storeFailedWarn:"ब्राउज़र में सहेजना विफल रहा — संभवतः उसका संग्रहण भर गया है। पुनः लोड करने पर बदलाव खो सकते हैं; अनुपयोगी दस्तावेज़ या पिछली स्थितियाँ हटाकर जगह बनाएँ।", storeFailedWarn:"ब्राउज़र में सहेजना विफल रहा — संभवतः उसका संग्रहण भर गया है। पुनः लोड करने पर बदलाव खो सकते हैं; अनुपयोगी दस्तावेज़ या पिछली स्थितियाँ हटाकर जगह बनाएँ।",
tabConflictWarn:"Werkbaum एक और ब्राउज़र टैब में खुला है — दोनों एक ही दस्तावेज़ सूची में लिखते हैं, और आख़िर में सहेजने वाला जीतता है। बेहतर है कि एक ही टैब में काम करें।",
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित", a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।", hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें", hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
@@ -3072,6 +3099,7 @@ const I18N = {
docNew:"新建文档", docRename:"重命名", docDelete:"删除", docNew:"新建文档", docRename:"重命名", docDelete:"删除",
docNewName:"未命名", docNewName:"未命名",
docDeleteConfirm:"删除文档“{name}”?", docDeleteConfirm:"删除文档“{name}”?",
docDeleteLastConfirm:"删除文档“{name}”?这是最后一个——自带示例将重新取代它。",
docLeave:"离开——仅从你的列表中移除", docLeave:"离开——仅从你的列表中移除",
docLeaveConfirm:"离开“{name}”?它只会从你的列表中移除——仍保留在服务器上,其链接会放入剪贴板。", docLeaveConfirm:"离开“{name}”?它只会从你的列表中移除——仍保留在服务器上,其链接会放入剪贴板。",
docRestore:"恢复原始版本", docRestore:"恢复原始版本",
@@ -3127,6 +3155,7 @@ const I18N = {
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORSAccess-Control-Allow-Origin)。", sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORSAccess-Control-Allow-Origin)。",
padGoneWarn:"此链接指向一个 Etherpad pad。Etherpad 连接已移除——协作现在通过 Werkbaum 后端(?live=…)进行。把 pad 的文本粘贴过去一次,然后一起编辑。", padGoneWarn:"此链接指向一个 Etherpad pad。Etherpad 连接已移除——协作现在通过 Werkbaum 后端(?live=…)进行。把 pad 的文本粘贴过去一次,然后一起编辑。",
storeFailedWarn:"无法保存到浏览器——其存储空间可能已满。重新加载时更改可能丢失;请删除不再使用的文档或以前的状态以腾出空间。", storeFailedWarn:"无法保存到浏览器——其存储空间可能已满。重新加载时更改可能丢失;请删除不再使用的文档或以前的状态以腾出空间。",
tabConflictWarn:"Werkbaum 已在另一个浏览器标签页中打开——两者写入同一份文档列表,最后保存者生效。最好只在一个标签页中工作。",
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接", a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
hint_indent:"缩进(2 个空格或制表符)定义层级。", hint_indent:"缩进(2 个空格或制表符)定义层级。",
hint_all:"子任务,全部必需", hint_any:"备选项,择其一", hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
@@ -3188,6 +3217,7 @@ const I18N = {
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除", docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
docNewName:"無題", docNewName:"無題",
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?", docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
docDeleteLastConfirm:"ドキュメント「{name}」を削除しますか?これが最後です — 同梱のサンプルが再び置かれます。",
docLeave:"退出 – 自分のリストから外すだけ", docLeave:"退出 – 自分のリストから外すだけ",
docLeaveConfirm:"「{name}」から退出しますか?自分のリストから外れるだけで、サーバーには残ります。リンクはクリップボードに入ります。", docLeaveConfirm:"「{name}」から退出しますか?自分のリストから外れるだけで、サーバーには残ります。リンクはクリップボードに入ります。",
docRestore:"オリジナルを復元", docRestore:"オリジナルを復元",
@@ -3243,6 +3273,7 @@ const I18N = {
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORSAccess-Control-Allow-Origin)を許可する必要があります。", sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORSAccess-Control-Allow-Origin)を許可する必要があります。",
padGoneWarn:"このリンクは Etherpad のパッドを指しています。Etherpad 連携は廃止されました。共同編集は Werkbaum バックエンド(?live=…)で行います。パッドの本文を一度貼り付ければ、複数人で編集できます。", padGoneWarn:"このリンクは Etherpad のパッドを指しています。Etherpad 連携は廃止されました。共同編集は Werkbaum バックエンド(?live=…)で行います。パッドの本文を一度貼り付ければ、複数人で編集できます。",
storeFailedWarn:"ブラウザーへの保存に失敗しました — ストレージが満杯の可能性があります。再読み込みで変更が失われることがあります。不要なドキュメントや以前の状態を削除して空きを作ってください。", storeFailedWarn:"ブラウザーへの保存に失敗しました — ストレージが満杯の可能性があります。再読み込みで変更が失われることがあります。不要なドキュメントや以前の状態を削除して空きを作ってください。",
tabConflictWarn:"Werkbaum が別のタブでも開いています — 両方が同じドキュメント一覧に書き込み、最後に保存した方が勝ちます。1 つのタブでの作業をおすすめします。",
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり", a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。", hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ", hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
@@ -3753,7 +3784,11 @@ function updateDocButtons(){
neu.disabled = d.text === shipped.text && d.name === shipped.name; neu.disabled = d.text === shipped.text && d.name === shipped.name;
neu.title = t('docRestore'); neu.title = t('docRestore');
neu.setAttribute('aria-label', t('docRestore')); neu.setAttribute('aria-label', t('docRestore'));
} else if(d && (d.source || fileHandles.has(d.id))){ /* NICHT für Server-Dokumente: adoptLive() setzt zwar `source`, aber dort
hält der Feed den Stand aktuell und ein fetchRemote auf die
Dokument-Adresse lüde die JSON-Antwort der API als Text (D84). */
} else if(d && !String(d.id).startsWith('live:')
&& (d.source || fileHandles.has(d.id))){
neu.hidden = false; neu.disabled = false; neu.hidden = false; neu.disabled = false;
neu.title = t('docReload'); neu.title = t('docReload');
neu.setAttribute('aria-label', t('docReload')); neu.setAttribute('aria-label', t('docReload'));
@@ -3790,10 +3825,12 @@ function restoreDoc(id){
stehen der verweigerte Dialog IST die Antwort. */ stehen der verweigerte Dialog IST die Antwort. */
async function reloadDoc(){ async function reloadDoc(){
const d = activeDoc(); const d = activeDoc();
if(!d) return; if(!d || String(d.id).startsWith('live:')) return; /* Server-Dokumente hält der Feed aktuell (D84) */
if(shippedStateOf(d.id)){ restoreDoc(d.id); return; } if(shippedStateOf(d.id)){ restoreDoc(d.id); return; }
if(!window.confirm(t('docReloadConfirm', {name: d.name}))) return;
if(d.source){ if(d.source){
/* KEINE Rückfrage (D84): Die URL ist die Quelle der Wahrheit (D23)
jedes Neuladen der Seite verwirft lokale Änderungen ohnehin still.
Eine Rückfrage nur am Knopf versprach einen Schutz, den es nicht gibt. */
try{ try{
d.text = await fetchRemote(d.source); d.text = await fetchRemote(d.source);
sourceWarning = null; sourceWarning = null;
@@ -3807,6 +3844,7 @@ async function reloadDoc(){
} }
const h = fileHandles.get(d.id); const h = fileHandles.get(d.id);
if(!h) return; if(!h) return;
if(!window.confirm(t('docReloadConfirm', {name: d.name}))) return;
try{ try{
let perm = await h.queryPermission({mode: 'read'}); let perm = await h.queryPermission({mode: 'read'});
if(perm === 'prompt') perm = await h.requestPermission({mode: 'read'}); if(perm === 'prompt') perm = await h.requestPermission({mode: 'read'});
@@ -4215,7 +4253,11 @@ function removeDocLocally(d){
function deleteDoc(id){ function deleteDoc(id){
const d = docs.find(x => x.id === id); const d = docs.find(x => x.id === id);
if(!d) return; if(!d) return;
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return; /* Beim LETZTEN Dokument sagt die Rückfrage, was danach dasteht (D84):
Der Editor steht nie leer, es entsteht ein frisches Beispiel vorher
hieß es nur löschen?", und dann erschien still etwas Neues. */
const letztes = docs.length === 1;
if(!window.confirm(t(letztes ? 'docDeleteLastConfirm' : 'docDeleteConfirm', {name: d.name}))) return;
removeDocLocally(d); removeDocLocally(d);
} }
/* Verlassen (D81-Nachtrag 5): für Geteilte dem Dokument auf dem Server /* Verlassen (D81-Nachtrag 5): für Geteilte dem Dokument auf dem Server
+9
View File
@@ -76,6 +76,15 @@ export function storeDocs(storage, docs, allKeys){
} }
} }
/* Gehört ein localStorage-Schlüssel zur Dokument-Ablage? Für die
Fremd-Tab-Erkennung (D84): Das storage-Ereignis anderer Tabs nennt den
Schlüssel — nur Dokument-Schlüssel heißen „ein anderer Tab schreibt in
dieselbe Liste". */
export function isDocKey(k){
const s = String(k == null ? '' : k);
return s === LS_DOCS || s === LS_ACTIVE || s === LS_SRC || s.startsWith(DOC_TEXT_PREFIX);
}
/* Die Tastendruck-Hälfte: nur der Text EINES Dokuments plus der Spiegel. /* Die Tastendruck-Hälfte: nur der Text EINES Dokuments plus der Spiegel.
Anders als bis D82 schreibt der Tastendruck damit direkt in die echte Anders als bis D82 schreibt der Tastendruck damit direkt in die echte
Ablage — eine „Spiegel gewinnt"-Regel beim Laden braucht das neue Schema Ablage — eine „Spiegel gewinnt"-Regel beim Laden braucht das neue Schema
+7 -1
View File
@@ -51,7 +51,11 @@
(localStorage-Quota, D82): Datenverlust (localStorage-Quota, D82): Datenverlust
droht beim Neuladen; zeilenlos, steht droht beim Neuladen; zeilenlos, steht
zuoberst und verschwindet, sobald ein zuoberst und verschwindet, sobald ein
Schreiben wieder gelingt */ Schreiben wieder gelingt
- tabConflict { } — ein weiterer Tab schreibt in dieselbe
Dokument-Ablage (D84): der letzte Flush
gewinnt; zeilenlos, bleibt bis zum
Neuladen stehen */
import { esc } from './render.js'; import { esc } from './render.js';
@@ -104,6 +108,8 @@ function build(w, t, esc){
return t('liveStaleWarn', {error: esc(w.error)}); return t('liveStaleWarn', {error: esc(w.error)});
case 'storeFailed': case 'storeFailed':
return t('storeFailedWarn'); return t('storeFailedWarn');
case 'tabConflict':
return t('tabConflictWarn');
default: default:
return `${esc(String(w.type))} (${w.line ?? '?'})`; return `${esc(String(w.type))} (${w.line ?? '?'})`;
} }
+17 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { readDocs, storeDocs, storeDocText, docTextKey, import { readDocs, storeDocs, storeDocText, docTextKey, isDocKey,
LS_DOCS, LS_SRC, DOC_TEXT_PREFIX } from '../src/docstore.js'; LS_DOCS, LS_SRC, DOC_TEXT_PREFIX } from '../src/docstore.js';
/* Storage-Attrappe mit Zähler — die Vergleich-vor-Schreiben-Regel ist nur /* Storage-Attrappe mit Zähler — die Vergleich-vor-Schreiben-Regel ist nur
@@ -101,3 +101,19 @@ describe('docstore — Index + Text je Dokument (D83)', () => {
expect(() => storeDocs(voll, [A], [])).toThrow(); expect(() => storeDocs(voll, [A], [])).toThrow();
}); });
}); });
describe('isDocKey — Fremd-Tab-Erkennung (D84)', () => {
it('erkennt Index, aktives Dokument, Spiegel und Text-Schlüssel', () => {
expect(isDocKey(LS_DOCS)).toBe(true);
expect(isDocKey('werkbaum-active')).toBe(true);
expect(isDocKey(LS_SRC)).toBe(true);
expect(isDocKey(DOC_TEXT_PREFIX + 'live:https://x/api/v1/documents/a')).toBe(true);
});
it('fremde Schlüssel und Ansichts-Zustand sind keine Dokument-Schlüssel', () => {
expect(isDocKey('werkbaum-ui')).toBe(false);
expect(isDocKey('werkbaum-lang')).toBe(false);
expect(isDocKey('werkbaum-snaps')).toBe(false);
expect(isDocKey(null)).toBe(false);
});
});