feat(paket c): Frühere Stände eines geteilten Dokuments kommen vom Server (D86)

Der Verlaufs-Knopf zeigt bei Geteilten die Meilenstein-Historie des
Servers (GET /history, jetzt samt clientId/displayName); Laden ist ein
Server-Rollback (POST /restore, ROLLED_BACK — neue Version für alle,
nichts geht verloren, mit Rückfrage). Die Kamera legt einen
Server-Meilenstein an (pushLive(true), leeres Diff erlaubt); lokale
Momentaufnahmen sammelt snapshotNow() für Server-Dokumente nicht mehr —
sie enthielten fremde Arbeit und überschrieben sie beim Laden als
eigenes Diff. Dazu der Anzeigename: einmal gefragt, im Browser gemerkt,
füllt das 'geändert von' der Historie (Behauptung, kein Nachweis).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-27 08:21:17 +02:00
co-authored by Claude Fable 5
parent 1fb11ceba4
commit 53b48bcd42
7 changed files with 201 additions and 5 deletions
@@ -193,5 +193,7 @@ class DocumentsController(
content = content, content = content,
changeType = ApiHistoryEntry.ChangeType.valueOf(changeType.name), changeType = ApiHistoryEntry.ChangeType.valueOf(changeType.name),
timestamp = timestamp, timestamp = timestamp,
clientId = author?.clientId,
displayName = author?.displayName,
) )
} }
@@ -481,6 +481,15 @@ components:
timestamp: timestamp:
type: string type: string
format: date-time format: date-time
clientId:
type: string
description: Fehlt bei Aenderungen ohne Absender (etwa ueber PUT).
displayName:
type: string
description: >
Selbstgewaehlt und ohne Anmeldung eine Behauptung - in der
Oberflaeche nicht wie ein Nachweis darstellen. Traegt das
"geaendert von" der Historie (D86).
TitlePatchRequest: TitlePatchRequest:
type: object type: object
+3
View File
@@ -19,6 +19,9 @@ reverse.
## 2026-08-27 ## 2026-08-27
- On shared documents the history button shows the server's milestones — loading one is a rollback for everyone, as a new version, nothing lost
- The camera button turns the current state of a shared document into a server milestone instead of a local snapshot
- Joining or sharing a document asks once for a display name — it fills the history's "changed by" and stays a claim, not a proof
- Renaming a shared document now renames it for everyone: the pencil patches the server title, and the change feed hands the new name to every open editor - Renaming a shared document now renames it for everyone: the pencil patches the server title, and the change feed hands the new name to every open editor
- URL documents lost their pencil — their name is the URL, and a local name would not survive the next load - URL documents lost their pencil — their name is the URL, and a local name would not survive the next load
- 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 - 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
+42
View File
@@ -7331,3 +7331,45 @@ den Inhalt stehen; Feed samt Titel; veraltete Version ⇒ 409; leerer Titel ⇒
BUILD SUCCESSFUL samt Coverage. Werkzeuggrenze wie in D76-Nachtrag 7: Der BUILD SUCCESSFUL samt Coverage. Werkzeuggrenze wie in D76-Nachtrag 7: Der
Automations-Tab meldet sich dauerhaft als verborgen — der Feed lief nur mit Automations-Tab meldet sich dauerhaft als verborgen — der Feed lief nur mit
gestellter Sichtbarkeit. gestellter Sichtbarkeit.
## D86 — Frühere Stände eines geteilten Dokuments kommen vom Server
Paket C der Ungereimtheiten-Liste (D84), der schwerste Punkt: Der
Verlaufs-Knopf zeigte bei geteilten Dokumenten die **lokalen**
Momentaufnahmen — und die enthalten fremde Änderungen; ein alter Stand hätte
beim Laden die Arbeit aller als eigenes Riesen-Diff überschrieben. D76 hatte
die Server-Meilensteine für genau diesen Knopf vorgesehen; jetzt sind sie da.
**Der Verlaufs-Knopf zeigt bei Geteilten die Meilenstein-Historie des
Servers** (`GET /history`, jetzt samt `clientId`/`displayName` — das
„geändert von"). Gelistet werden die Stände **vor** dem aktuellen („Frühere
Stände" eben); rechts steht der Anzeigename des Urhebers. **Laden ist ein
Server-Rollback** (`POST /restore {version}`, `ROLLED_BACK`, D76): Der
Rücksprung geschieht für alle nachvollziehbar auf dem Server — als neue
Version, nichts geht verloren — statt als eigenes Diff dieses Clients.
Deshalb mit Rückfrage: Er trifft alle Mitschreiber. **Die Kamera** legt bei
Geteilten einen **Server-Meilenstein** an (`pushLive(true)`: ein wartendes
Debounce-Diff geht mit, und auch das leere Diff ist erlaubt — es bumpt die
Version als Meilenstein, genau das ist „Stand jetzt sichern"). Lokale
Momentaufnahmen sammelt `snapshotNow()` für Server-Dokumente **gar nicht
mehr** — die Historie führt der Server, und im Menü wären sie ohnehin
unsichtbar. Für lokale Dokumente ändert sich nichts.
**Der Anzeigename** (Punkt 9 der Liste, mit hinein gebaut): Beim ersten
Teilen oder Beitreten fragt ein Dialog einmal nach dem Namen —
`ensureDisplayName()`, gemerkt im Browser, geht mit jedem Patch mit und
füllt das „geändert von" der Historie. Auch die **leere** Antwort wird
gemerkt (anonym bleiben ist eine Antwort; Nachfragen bei jedem Öffnen wäre
Gängelung), nur Abbruch fragt beim nächsten Mal erneut. Ohne Anmeldung
bleibt der Name eine Behauptung, kein Nachweis (D76) — die API-Beschreibung
sagt das ausdrücklich.
**Nachgemessen** Ende-zu-Ende gegen das lokal laufende Backend: Leere
Historie zeigt den eigenen Leertext; Tippen + Kamera erzeugt den
Server-Meilenstein mit `displayName: Anna`; nach einer zweiten Änderung
listet das Menü zwei frühere Stände (der aktuelle fehlt, rechts der Name);
der Rollback auf den ältesten stellt den Text im Client zurück und steht auf
dem Server als `ROLLED_BACK`-Version 4 mit dem Inhalt von Version 1. Ein
lokales Dokument zeigt unverändert seine lokalen Stände. Der Namens-Dialog
beim Beitreten blockiert nichts (die Automation wies ihn ab — Dokument lädt,
nichts gemerkt, nächstes Mal wird wieder gefragt). 531 Frontend-Tests und
der Backend-`check` grün.
+13
View File
@@ -155,6 +155,8 @@
- [?] #col.live.cursors: Cursors and who else is here (S) - [?] #col.live.cursors: Cursors and who else is here (S)
- [ ] #col.live.owner: An owner password guards managing a shared plan (M) %% returned once on creation, admin can reclaim - [ ] #col.live.owner: An owner password guards managing a shared plan (M) %% returned once on creation, admin can reclaim
- [x] #col.live.rename: Renaming reaches everyone (S) %% PATCH /title + RENAMED in the feed, see D85 - [x] #col.live.rename: Renaming reaches everyone (S) %% PATCH /title + RENAMED in the feed, see D85
- [x] #col.live.history: Earlier states come from the server (M) %% milestones in the clock menu, rollback for everyone, see D86
- [x] #col.live.name: A self-chosen display name (XS) %% fills "changed by", a claim, not a proof
- [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough - [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough
- [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent - [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent
- [?] #mmd: Mermaid plugin (XL) - [?] #mmd: Mermaid plugin (XL)
@@ -924,6 +926,17 @@
and the change feed hands the new title to every client in plain text. URL and the change feed hands the new title to every client in plain text. URL
documents lost their pencil instead - their name is the URL. documents lost their pencil instead - their name is the URL.
#col.live.history
On a shared document the clock button lists the server's milestone history
instead of local snapshots - those would contain other people's work and
overwrite everyone when loaded. The camera makes a server milestone, and
loading an earlier state is a server-side rollback: a new version for
everyone, nothing lost.
#col.live.name
Asked once when joining or sharing, kept in the browser, sent with every
patch. Without accounts it is a claim, not a proof.
#col.live.cursors #col.live.cursors
Seeing where the others are. A by-product of the merge layer rather than a Seeing where the others are. A by-product of the merge layer rather than a
feature in its own right. feature in its own right.
+9 -1
View File
@@ -129,7 +129,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Änderung nach einem Reload für eine Wiederholung und tut nichts), und der Änderung nach einem Reload für eine Wiederholung und tut nichts), und der
**Konflikt entsteht beim Tippen**, nicht erst beim Senden — der Server kennt **Konflikt entsteht beim Tippen**, nicht erst beim Senden — der Server kennt
den ungesendeten Text nicht. Fremde Änderungen werden bewusst **nicht** den ungesendeten Text nicht. Fremde Änderungen werden bewusst **nicht**
undo-fähig eingespielt. undo-fähig eingespielt. **Frühere Stände sind bei Server-Dokumenten die
SERVER-Meilensteine** (D86): `renderSnapMenu()` verzweigt bei `liveActive()`
auf `renderServerHistory()`, die Kamera schickt `pushLive(true)` (leeres
Diff erlaubt, `milestone:true`), Laden ist ein Server-Rollback
(`POST /restore`, ROLLED_BACK für alle) — und `snapshotNow()` sammelt für
`live:`-Dokumente KEINE lokalen Stände (sie enthielten fremde Arbeit und
überschrieben sie beim Laden als eigenes Diff). Der Anzeigename
(`ensureDisplayName()`, einmal gefragt, leer = anonym gemerkt) geht mit
jedem Patch mit.
**Der Feed liefert die EIGENE Änderung zurück** (D76-Nachtrag 9): Er **Der Feed liefert die EIGENE Änderung zurück** (D76-Nachtrag 9): Er
beantwortet „was ist seit Version N geschehen", und wer mitgeschrieben hat, beantwortet „was ist seit Version N geschehen", und wer mitgeschrieben hat,
steht nicht in der Frage. Wacht er im Moment des eigenen Sendens auf, hielte steht nicht in der Frage. Wacht er im Moment des eigenen Sendens auf, hielte
+123 -4
View File
@@ -2268,6 +2268,9 @@ const I18N = {
snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände", snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände",
snapNone:"Noch keine früheren Stände gesichert wird auf Knopfdruck und alle 10 Minuten, sobald sich etwas geändert hat.", snapNone:"Noch keine früheren Stände gesichert wird auf Knopfdruck und alle 10 Minuten, sobald sich etwas geändert hat.",
snapAddTooltip:"Aktuellen Stand jetzt sichern", snapAddTooltip:"Aktuellen Stand jetzt sichern",
liveNameAsk:"Dein Name für die anderen (erscheint in der Historie des geteilten Dokuments; leer = anonym):",
snapNoneLive:"Noch keine früheren Stände auf dem Server — Meilensteine entstehen nach Schreibpausen und mit dem Kamera-Knopf.",
snapRollbackConfirm:"Auf den Stand von {when} zurücksetzen? Das gilt für alle, die dieses Dokument bearbeiten — als neue Version, nichts geht verloren.",
idsTooltip:"Knoten-IDs vor dem Titel einblenden", idsTooltip:"Knoten-IDs vor dem Titel einblenden",
snapLines:"{n} Zeilen", snapLines:"{n} Zeilen",
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen", docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
@@ -2386,6 +2389,9 @@ const I18N = {
snapTooltip:"Earlier states of this document", snapMenuAria:"Earlier states", snapTooltip:"Earlier states of this document", snapMenuAria:"Earlier states",
snapNone:"No earlier states yet one is kept on demand and every 10 minutes, once something has changed.", snapNone:"No earlier states yet one is kept on demand and every 10 minutes, once something has changed.",
snapAddTooltip:"Save the current state now", snapAddTooltip:"Save the current state now",
liveNameAsk:"Your name for the others (shown in the shared document's history; empty = anonymous):",
snapNoneLive:"No earlier states on the server yet — milestones appear after writing pauses and with the camera button.",
snapRollbackConfirm:"Roll back to the state of {when}? This applies to everyone editing this document — as a new version, nothing is lost.",
idsTooltip:"Show node ids before the title", idsTooltip:"Show node ids before the title",
snapLines:"{n} lines", snapLines:"{n} lines",
docNew:"New document", docRename:"Rename", docDelete:"Delete", docNew:"New document", docRename:"Rename", docDelete:"Delete",
@@ -2504,6 +2510,9 @@ const I18N = {
snapTooltip:"Estados anteriores de este documento", snapMenuAria:"Estados anteriores", snapTooltip:"Estados anteriores de este documento", snapMenuAria:"Estados anteriores",
snapNone:"Aún no hay estados anteriores: se guarda uno al pulsar el botón y cada 10 minutos, cuando algo ha cambiado.", snapNone:"Aún no hay estados anteriores: se guarda uno al pulsar el botón y cada 10 minutos, cuando algo ha cambiado.",
snapAddTooltip:"Guardar ahora el estado actual", snapAddTooltip:"Guardar ahora el estado actual",
liveNameAsk:"Tu nombre para los demás (aparece en el historial del documento compartido; vacío = anónimo):",
snapNoneLive:"Aún no hay estados anteriores en el servidor: los hitos surgen tras pausas de escritura y con el botón de cámara.",
snapRollbackConfirm:"¿Volver al estado de {when}? Vale para todos los que editan este documento: como versión nueva, nada se pierde.",
idsTooltip:"Mostrar los id de nodo antes del título", idsTooltip:"Mostrar los id de nodo antes del título",
snapLines:"{n} líneas", snapLines:"{n} líneas",
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar", docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
@@ -2622,6 +2631,9 @@ const I18N = {
snapTooltip:"États antérieurs de ce document", snapMenuAria:"États antérieurs", snapTooltip:"États antérieurs de ce document", snapMenuAria:"États antérieurs",
snapNone:"Pas encore d’état antérieur : un état est conservé sur demande et toutes les 10 minutes, dès que quelque chose a changé.", snapNone:"Pas encore d’état antérieur : un état est conservé sur demande et toutes les 10 minutes, dès que quelque chose a changé.",
snapAddTooltip:"Enregistrer l’état actuel maintenant", snapAddTooltip:"Enregistrer l’état actuel maintenant",
liveNameAsk:"Votre nom pour les autres (visible dans l'historique du document partagé ; vide = anonyme) :",
snapNoneLive:"Pas encore d'états antérieurs sur le serveur — les jalons naissent après une pause d'écriture et avec le bouton appareil photo.",
snapRollbackConfirm:"Revenir à l'état de {when} ? Cela vaut pour tous ceux qui éditent ce document — en nouvelle version, rien n'est perdu.",
idsTooltip:"Afficher les identifiants de nœud avant le titre", idsTooltip:"Afficher les identifiants de nœud avant le titre",
snapLines:"{n} lignes", snapLines:"{n} lignes",
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer", docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
@@ -2740,6 +2752,9 @@ const I18N = {
snapTooltip:"Wcześniejsze stany tego dokumentu", snapMenuAria:"Wcześniejsze stany", snapTooltip:"Wcześniejsze stany tego dokumentu", snapMenuAria:"Wcześniejsze stany",
snapNone:"Brak wcześniejszych stanów zapisywany jest na żądanie i co 10 minut, gdy coś się zmieni.", snapNone:"Brak wcześniejszych stanów zapisywany jest na żądanie i co 10 minut, gdy coś się zmieni.",
snapAddTooltip:"Zapisz bieżący stan teraz", snapAddTooltip:"Zapisz bieżący stan teraz",
liveNameAsk:"Twoje imię dla innych (widoczne w historii udostępnionego dokumentu; puste = anonimowo):",
snapNoneLive:"Na serwerze nie ma jeszcze wcześniejszych stanów — kamienie milowe powstają po przerwach w pisaniu i przyciskiem aparatu.",
snapRollbackConfirm:"Wrócić do stanu z {when}? Dotyczy wszystkich edytujących ten dokument — jako nowa wersja, nic nie przepada.",
idsTooltip:"Pokaż identyfikatory węzłów przed tytułem", idsTooltip:"Pokaż identyfikatory węzłów przed tytułem",
snapLines:"wierszy: {n}", snapLines:"wierszy: {n}",
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń", docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
@@ -2858,6 +2873,9 @@ const I18N = {
snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния", snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния",
snapNone:"Прежних состояний пока нет — они сохраняются по нажатию кнопки и каждые 10 минут, если что-то изменилось.", snapNone:"Прежних состояний пока нет — они сохраняются по нажатию кнопки и каждые 10 минут, если что-то изменилось.",
snapAddTooltip:"Сохранить текущее состояние сейчас", snapAddTooltip:"Сохранить текущее состояние сейчас",
liveNameAsk:"Ваше имя для остальных (видно в истории общего документа; пусто = анонимно):",
snapNoneLive:"На сервере пока нет прежних состояний — вехи возникают после пауз в наборе и по кнопке камеры.",
snapRollbackConfirm:"Вернуться к состоянию от {when}? Это касается всех, кто редактирует документ — новой версией, ничего не теряется.",
idsTooltip:"Показывать идентификаторы узлов перед заголовком", idsTooltip:"Показывать идентификаторы узлов перед заголовком",
snapLines:"строк: {n}", snapLines:"строк: {n}",
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить", docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
@@ -2976,6 +2994,9 @@ const I18N = {
snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ", snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ",
snapNone:"अभी कोई पिछली स्थिति नहीं — बटन दबाने पर और कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।", snapNone:"अभी कोई पिछली स्थिति नहीं — बटन दबाने पर और कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।",
snapAddTooltip:"मौजूदा स्थिति अभी सहेजें", snapAddTooltip:"मौजूदा स्थिति अभी सहेजें",
liveNameAsk:"दूसरों के लिए आपका नाम (साझा दस्तावेज़ के इतिहास में दिखता है; खाली = अनाम):",
snapNoneLive:"सर्वर पर अभी कोई पिछली स्थिति नहीं — पड़ाव लिखने के विराम के बाद और कैमरा बटन से बनते हैं।",
snapRollbackConfirm:"{when} की स्थिति पर लौटें? यह इस दस्तावेज़ को संपादित करने वाले सभी पर लागू होगा — नई वर्शन के रूप में, कुछ नहीं खोता।",
idsTooltip:"शीर्षक से पहले नोड आईडी दिखाएँ", idsTooltip:"शीर्षक से पहले नोड आईडी दिखाएँ",
snapLines:"{n} पंक्तियाँ", snapLines:"{n} पंक्तियाँ",
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ", docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
@@ -3094,6 +3115,9 @@ const I18N = {
snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态", snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态",
snapNone:"暂无早期状态——可随时手动保存;内容有变动时,也会每 10 分钟保存一次。", snapNone:"暂无早期状态——可随时手动保存;内容有变动时,也会每 10 分钟保存一次。",
snapAddTooltip:"立即保存当前状态", snapAddTooltip:"立即保存当前状态",
liveNameAsk:"你展示给其他人的名字(显示在共享文档的历史中;留空 = 匿名):",
snapNoneLive:"服务器上还没有以前的状态——里程碑在写作停顿后以及通过相机按钮产生。",
snapRollbackConfirm:"回退到 {when} 的状态?这对所有编辑此文档的人生效——作为新版本,不会丢失任何内容。",
idsTooltip:"在标题前显示节点 ID", idsTooltip:"在标题前显示节点 ID",
snapLines:"{n} 行", snapLines:"{n} 行",
docNew:"新建文档", docRename:"重命名", docDelete:"删除", docNew:"新建文档", docRename:"重命名", docDelete:"删除",
@@ -3212,6 +3236,9 @@ const I18N = {
snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態", snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態",
snapNone:"以前の状態はまだありません — ボタンを押したとき、および変更があれば 10 分ごとに保存されます。", snapNone:"以前の状態はまだありません — ボタンを押したとき、および変更があれば 10 分ごとに保存されます。",
snapAddTooltip:"現在の状態を今すぐ保存", snapAddTooltip:"現在の状態を今すぐ保存",
liveNameAsk:"他の人に表示される名前(共有ドキュメントの履歴に表示。空欄 = 匿名):",
snapNoneLive:"サーバーにはまだ以前の状態がありません — マイルストーンは書き込みの合間とカメラボタンで生まれます。",
snapRollbackConfirm:"{when} の状態に戻しますか?このドキュメントを編集する全員に適用されます — 新しいバージョンとして、何も失われません。",
idsTooltip:"タイトルの前にノード ID を表示", idsTooltip:"タイトルの前にノード ID を表示",
snapLines:"{n} 行", snapLines:"{n} 行",
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除", docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
@@ -3887,6 +3914,10 @@ function loadSnaps(){ snaps = parseSnaps(localStorage.getItem(LS_SNAPS)); }
function snapshotNow(manuell){ function snapshotNow(manuell){
const d = activeDoc(); const d = activeDoc();
if(!d) return false; if(!d) return false;
/* Server-Dokumente: Die Historie führt der Server (D86) lokale Stände
wären irreführend (sie enthalten fremde Änderungen, D76) und im Menü
ohnehin unsichtbar. */
if(String(d.id).startsWith('live:')) return false;
const text = src.value; const text = src.value;
if(!addSnapshot(snaps, d.id, text, Date.now(), {base: snapBase, manual: manuell})) return false; if(!addSnapshot(snaps, d.id, text, Date.now(), {base: snapBase, manual: manuell})) return false;
snapBase = text; snapBase = text;
@@ -3904,6 +3935,7 @@ const snapAddBtn = document.getElementById('snapAddBtn');
const snapMenu = document.getElementById('snapMenu'); const snapMenu = document.getElementById('snapMenu');
function renderSnapMenu(){ function renderSnapMenu(){
if(liveActive()){ renderServerHistory(); return; }
const d = activeDoc(); const d = activeDoc();
const list = d ? (snaps[d.id] || []) : []; const list = d ? (snaps[d.id] || []) : [];
snapMenu.innerHTML = ''; snapMenu.innerHTML = '';
@@ -3945,6 +3977,65 @@ function closeSnapMenu(){
snapMenu.hidden = true; snapMenu.hidden = true;
snapBtn.setAttribute('aria-expanded', 'false'); snapBtn.setAttribute('aria-expanded', 'false');
} }
/* Frühere Stände eines GETEILTEN Dokuments: die Meilenstein-Historie des
Servers (D76 sah das vor, D86 baut es). Gezeigt werden die Stände VOR dem
aktuellen Frühere Stände" eben; das „geändert von" ist der
selbstgewählte Anzeigename und damit eine Behauptung, kein Nachweis. */
async function renderServerHistory(){
const sitzung = liveState;
snapMenu.innerHTML = '';
const lade = document.createElement('div');
lade.className = 'snapempty';
lade.textContent = '…';
snapMenu.appendChild(lade);
let eintraege;
try{ eintraege = await fetchJson(sitzung.urls.doc + '/history'); }
catch(err){ lade.textContent = (err && err.message) || String(err); return; }
if(liveState !== sitzung || snapMenu.hidden) return; /* inzwischen zu oder umgeschaltet */
snapMenu.innerHTML = '';
const max = Math.max(0, ...eintraege.map(x => x.version));
const fruehere = eintraege.filter(x => x.version !== max).reverse(); /* neueste zuoberst */
if(!fruehere.length){
const p = document.createElement('div');
p.className = 'snapempty';
p.textContent = t('snapNoneLive');
snapMenu.appendChild(p);
return;
}
fruehere.forEach(x => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'snapitem';
b.setAttribute('role', 'menuitem');
b.innerHTML = '<span></span><span class="snapsize"></span>';
b.firstChild.textContent = snapLabel(Date.parse(x.timestamp), lang, Date.now());
b.lastChild.textContent = x.displayName || '';
b.addEventListener('click', e => { e.stopPropagation(); rollbackToVersion(x); });
snapMenu.appendChild(b);
});
}
/* Rücksprung als SERVER-Rollback (ROLLED_BACK, D76/D86): Er geschieht für
alle nachvollziehbar auf dem Server als neue Version, nichts geht
verloren statt als riesiges eigenes Diff dieses Clients. Deshalb mit
Rückfrage: Er trifft alle Mitschreiber. */
async function rollbackToVersion(eintrag){
const sitzung = liveState;
closeSnapMenu();
const wann = snapLabel(Date.parse(eintrag.timestamp), lang, Date.now());
if(!window.confirm(t('snapRollbackConfirm', {when: wann}))) return;
try{
const doc = await fetchJson(sitzung.urls.doc + '/restore', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({version: eintrag.version}),
});
if(liveState === sitzung) adoptLive(doc);
}catch(err){
sourceWarning = {type: 'liveLoad', url: sitzung.urls.doc + '/restore',
error: (err && err.message) || String(err)};
render();
}
}
snapBtn.addEventListener('click', e => { snapBtn.addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
snapMenu.hidden ? openSnapMenu() : closeSnapMenu(); snapMenu.hidden ? openSnapMenu() : closeSnapMenu();
@@ -3956,9 +4047,17 @@ snapBtn.addEventListener('click', e => {
der Liste. Einen doppelten Eintrag zu erzwingen kostete nur einen der 20 der Liste. Einen doppelten Eintrag zu erzwingen kostete nur einen der 20
Plätze (D54-Nachtrag). Ein offenes Menü bleibt offen und zeichnet neu, dort Plätze (D54-Nachtrag). Ein offenes Menü bleibt offen und zeichnet neu, dort
sieht man den neuen Eintrag entstehen. */ sieht man den neuen Eintrag entstehen. */
snapAddBtn.addEventListener('click', e => { snapAddBtn.addEventListener('click', async e => {
e.stopPropagation(); e.stopPropagation();
snapshotNow(true); if(liveActive()){
/* Geteilt: als SERVER-Meilenstein (D86) eine lokale Momentaufnahme
wäre dort irreführend, sie enthielte fremde Änderungen (D76). Ein
wartendes Debounce-Diff geht dabei gleich mit. */
if(liveState.pushTimer){ clearTimeout(liveState.pushTimer); liveState.pushTimer = null; }
await pushLive(true);
} else {
snapshotNow(true);
}
flashBtn(snapAddBtn); flashBtn(snapAddBtn);
}); });
document.addEventListener('click', e => { document.addEventListener('click', e => {
@@ -4726,6 +4825,7 @@ function loadLive(){ startLive(urlParam(LIVE_PARAM)); }
ein Server-Dokument im Wähler (D80). */ ein Server-Dokument im Wähler (D80). */
async function startLive(raw){ async function startLive(raw){
if(!raw) return; if(!raw) return;
ensureDisplayName(); /* wer mitschreibt, darf sagen, wer er ist (D86) */
const urls = live.liveUrls(raw, location.href); const urls = live.liveUrls(raw, location.href);
if(!urls){ if(!urls){
sourceWarning = {type:'liveLoad', url: raw, error: 'not a Werkbaum document URL'}; sourceWarning = {type:'liveLoad', url: raw, error: 'not a Werkbaum document URL'};
@@ -4794,11 +4894,15 @@ function scheduleLivePush(){
LIVE_DEBOUNCE_MS); LIVE_DEBOUNCE_MS);
} }
async function pushLive(){ /* [meilenstein]: Der Kamera-Knopf hält den Stand als SERVER-Meilenstein fest
(D86) dann darf das Diff auch leer sein: Die leere Änderung bumpt die
Version und markiert sie als Meilenstein, genau das ist Stand jetzt
sichern" auf einem geteilten Dokument. */
async function pushLive(meilenstein){
if(!liveActive() || liveConflict || liveState.busy) return; if(!liveActive() || liveConflict || liveState.busy) return;
const now = live.lines(live.normalize(src.value)); const now = live.lines(live.normalize(src.value));
const ops = live.computeOps(liveState.shadow, now); const ops = live.computeOps(liveState.shadow, now);
if(!ops.length) return; if(!ops.length && !meilenstein) return;
/* Die Sitzung selbst festhalten, nicht nur ihre Felder: Wer während des /* Die Sitzung selbst festhalten, nicht nur ihre Felder: Wer während des
Sendens auf ein anderes Dokument umschaltet, beendet sie (D80) die Sendens auf ein anderes Dokument umschaltet, beendet sie (D80) die
@@ -4822,6 +4926,7 @@ async function pushLive(){
displayName: displayName(), displayName: displayName(),
seq, seq,
ops, ops,
milestone: meilenstein || undefined,
}; };
const result = await fetchJson(sitzung.urls.content, { const result = await fetchJson(sitzung.urls.content, {
method: 'PATCH', method: 'PATCH',
@@ -5061,6 +5166,19 @@ const LS_NAME = 'werkbaum-name';
function displayName(){ function displayName(){
try{ return localStorage.getItem(LS_NAME) || undefined; }catch(_){ return undefined; } try{ return localStorage.getItem(LS_NAME) || undefined; }catch(_){ return undefined; }
} }
/* Der Anzeigename (D86): selbstgewählt, im Browser gemerkt, geht mit jedem
Patch mit und füllt das geändert von" der Server-Historie. Einmal gefragt
auch die leere Antwort wird gemerkt (anonym bleiben ist eine Antwort,
Nachfragen bei jedem Öffnen wäre Gängelung); Abbruch fragt beim nächsten
Mal erneut. Ohne Anmeldung bleibt der Name eine Behauptung (D76). */
function ensureDisplayName(){
try{
if(localStorage.getItem(LS_NAME) !== null) return;
const eingabe = window.prompt(t('liveNameAsk'), '');
if(eingabe === null) return;
localStorage.setItem(LS_NAME, eingabe.trim());
}catch(_){}
}
/* ---------- Ein Dokument auf den Server legen (D76) ---------- /* ---------- Ein Dokument auf den Server legen (D76) ----------
Der Weg vom lokalen Dokument zum gemeinsam bearbeitbaren: anlegen, dorthin Der Weg vom lokalen Dokument zum gemeinsam bearbeitbaren: anlegen, dorthin
@@ -5113,6 +5231,7 @@ async function serverBaseOrAsk(){
async function putOnServer(){ async function putOnServer(){
const d = activeDoc(); const d = activeDoc();
if(!d) return; if(!d) return;
ensureDisplayName(); /* D86 */
const basis = await serverBaseOrAsk(); const basis = await serverBaseOrAsk();
if(!basis) return; if(!basis) return;