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:
co-authored by
Claude Fable 5
parent
1fb11ceba4
commit
53b48bcd42
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
+122
-3
@@ -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();
|
||||||
|
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);
|
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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user