Compare commits
6
Commits
3cafc92d49
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db7b8004ee | ||
|
|
28ff6844b0 | ||
|
|
2e408d956c | ||
|
|
c87cf6212d | ||
|
|
5ec5118cee | ||
|
|
063d2c947b |
@@ -134,6 +134,13 @@ Datei committet — Datum, Titel und Version in der Nachricht, kein Commit ohne
|
|||||||
Änderung. Per Cron aufgerufen archiviert sich der Plan von selbst; `--open`
|
Änderung. Per Cron aufgerufen archiviert sich der Plan von selbst; `--open`
|
||||||
öffnet die Datei danach in IntelliJ IDEA. Siehe `docs/DECISIONS.md` D88.
|
öffnet die Datei danach in IntelliJ IDEA. Siehe `docs/DECISIONS.md` D88.
|
||||||
|
|
||||||
|
**Mehrere Dokumente, ohne Zutun:** `tools/mirror-docs [--push] <worktree>
|
||||||
|
<liste>` schickt jede Zeile der Liste (`<uuid-oder-url> <pfad>`) durch
|
||||||
|
`pull-doc --git-commit` und pusht einmal am Ende; eine UUID braucht die
|
||||||
|
Backend-Basis in `--api` oder `WERKBAUM_API`. Als systemd-Timer auf dem
|
||||||
|
Backend-Host spiegelt es die geteilten Pläne alle 15 Minuten in einen Branch —
|
||||||
|
siehe D88, Nachtrag 3.
|
||||||
|
|
||||||
Das Backend einrichten: siehe [backend/README.md](backend/README.md) und
|
Das Backend einrichten: siehe [backend/README.md](backend/README.md) und
|
||||||
`docs/DECISIONS.md` D76.
|
`docs/DECISIONS.md` D76.
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ title and version in the message, and no commit when nothing changed. Run it
|
|||||||
from cron and the plan archives itself; `--open` opens the file in IntelliJ
|
from cron and the plan archives itself; `--open` opens the file in IntelliJ
|
||||||
IDEA afterwards. See `docs/DECISIONS.md` D88.
|
IDEA afterwards. See `docs/DECISIONS.md` D88.
|
||||||
|
|
||||||
|
**Several documents, unattended:** `tools/mirror-docs [--push] <worktree>
|
||||||
|
<list>` runs every line of the list (`<uuid-or-url> <path>`) through
|
||||||
|
`pull-doc --git-commit` and pushes once at the end; a UUID needs the backend
|
||||||
|
base in `--api` or `WERKBAUM_API`. As a systemd timer on the backend host it
|
||||||
|
mirrors the shared plans into a branch every 15 minutes — see D88, addendum 3.
|
||||||
|
|
||||||
Setting up the backend: see [backend/README.md](backend/README.md) and
|
Setting up the backend: see [backend/README.md](backend/README.md) and
|
||||||
`docs/DECISIONS.md` D76.
|
`docs/DECISIONS.md` D76.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ the git history of `docs/examples/werkbaum.werkbaum`. A day can therefore carry
|
|||||||
a link without having a note (someone forgot to write one) — but never the
|
a link without having a note (someone forgot to write one) — but never the
|
||||||
reverse.
|
reverse.
|
||||||
|
|
||||||
|
## 2026-09-04
|
||||||
|
|
||||||
|
- Fix: a shared document's reconnect no longer pushes whatever text the window holds — a phone waking up with yesterday's state overwrote the current plan twice; now the server state is taken when nothing was edited here, and the conflict bar asks when it was
|
||||||
|
- A `mirror-docs` script runs a list of shared documents through `pull-doc --git-commit` and pushes once — as a 15-minute timer on the backend host it gives a shared plan a git history without anyone remembering to run it
|
||||||
|
|
||||||
## 2026-09-03
|
## 2026-09-03
|
||||||
|
|
||||||
- The project moved to its own Gitea at git.javagil.de — the footer's repository and version links point there now, and GitHub stays a clone carrying the very same commits
|
- The project moved to its own Gitea at git.javagil.de — the footer's repository and version links point there now, and GitHub stays a clone carrying the very same commits
|
||||||
|
|||||||
@@ -7638,6 +7638,63 @@ Haupttext gelten fort; neu geprüft außerdem: flaglos außerhalb jedes
|
|||||||
Worktrees schreibt anstandslos, `--git-commit` dort lehnt mit klarer
|
Worktrees schreibt anstandslos, `--git-commit` dort lehnt mit klarer
|
||||||
Meldung ab.
|
Meldung ab.
|
||||||
|
|
||||||
|
**Nachtrag 3 — `mirror-docs`: ein Timer auf dem Backend-Host spiegelt eine
|
||||||
|
Liste von Dokumenten in einen eigenen Branch (2026-09-04).** Anlass war der
|
||||||
|
zweite Datenverlust an demselben geteilten Dokument: Am 3.9. um 19:03 schrieb
|
||||||
|
ein Fenster mit veralteter Schattenkopie den Stand vom Vorabend als Volltext
|
||||||
|
zurück — die Server-Historie hatte den ganzen Tag (Version 659 war der letzte
|
||||||
|
gute Stand, byte-genau wiederhergestellt), der Git-Spiegel aber nicht: Er lief
|
||||||
|
von Hand, und der letzte Lauf lag vor der verlorenen Arbeit. Ein Spiegel, der
|
||||||
|
nur läuft, wenn jemand daran denkt, ist an dem Tag nicht da, an dem man ihn
|
||||||
|
braucht.
|
||||||
|
|
||||||
|
**Der Spiegel läuft auf dem Backend-Host, nicht auf dem Laptop.** Der Server
|
||||||
|
ist rund um die Uhr an, `Linger=yes` ist gesetzt (D77), und er holt das
|
||||||
|
Dokument von `127.0.0.1:9080` ohne den Proxy. Ein Timer auf dem Laptop hätte
|
||||||
|
nur die Stunden abgedeckt, in denen der zu ist — mit `Persistent=true` zwar
|
||||||
|
nachgeholt, aber nach der Lücke, nicht davor. Gebaut als systemd-User-Timer
|
||||||
|
(`werkbaum-mirror.timer`, alle 15 Minuten, `Persistent=true`) neben der
|
||||||
|
Backend-Unit; der Service ruft `mirror-docs --push`.
|
||||||
|
|
||||||
|
**`tools/mirror-docs` ist die Schleife um `pull-doc`, nicht sein Ersatz.** Es
|
||||||
|
liest eine Liste (`documents.list`: je Zeile UUID oder URL und Pfad im Repo),
|
||||||
|
ruft je Zeile `pull-doc --git-commit` und pusht einmal am Ende — nur, wenn ein
|
||||||
|
Commit entstanden ist. Jede Zeile läuft für sich: Ein gescheiterter Abruf
|
||||||
|
bricht die übrigen nicht ab, steht aber auf stderr, und der Lauf endet mit 1,
|
||||||
|
damit das Journal des Timers den Fehler zeigt, statt still zu bleiben.
|
||||||
|
Nachgemessen lokal: eine Fehlerzeile in der Liste kostet genau ihr Dokument,
|
||||||
|
die beiden anderen werden committet; der zweite Lauf ohne Änderung committet
|
||||||
|
nichts und endet mit 0.
|
||||||
|
|
||||||
|
**Eine Liste statt „alle Dokumente des Servers“.** Die Dokumentenliste
|
||||||
|
verlangt das Master-Passwort (D76-Nachtrag 6), und auf dem Server liegt davon
|
||||||
|
nur der Hash. Für den Cron müsste das Klartext-Passwort dorthin, nur damit er
|
||||||
|
eine Liste bekommt, die er um Dateinamen ohnehin ergänzen müsste. Eine Zeile
|
||||||
|
je Dokument ist der ehrlichere Preis: kein Geheimnis mehr auf dem Host, und
|
||||||
|
Probe-Dokumente bleiben von selbst draußen.
|
||||||
|
|
||||||
|
**Ein Branch, viele Dateien.** Der Spiegel committet blind, also auf einen
|
||||||
|
eigenen Branch `werkbaum-mirror`, den man bei Bedarf nach `main` mergt — nicht
|
||||||
|
auf `main` mit den Hand-Commits dazwischen, sonst kollidiert der Server-Push
|
||||||
|
mit dem lokalen Stand und der Timer bleibt hängen. Beim ersten Lauf von Hand
|
||||||
|
war der Stand auf dem gerade ausgecheckten Feature-Branch gelandet; genau
|
||||||
|
das soll ein Timer nicht tun. Ein Branch je Werkbaum wäre eine dritte Spalte
|
||||||
|
in der Liste, wenn ihn je jemand braucht.
|
||||||
|
|
||||||
|
**Ein eigener Deploy-Key, ein Sparse-Checkout.** Auf dem Server liegt ein
|
||||||
|
Schlüssel des Backend-Logins; der bekommt nicht nebenbei Schreibrecht auf ein
|
||||||
|
privates Notizen-Repo. Der Spiegel hat seinen eigenen ed25519-Schlüssel
|
||||||
|
(`~/.ssh/werkbaum-mirror`, Host-Alias `notes-mirror` mit `IdentitiesOnly`),
|
||||||
|
als Deploy-Key mit Schreibrecht nur für dieses eine Repo. Geklont wird mit
|
||||||
|
`--filter=blob:none` und Sparse-Checkout auf das Zielverzeichnis — auf dem
|
||||||
|
geteilten Host liegen so nur die gespiegelten Dateien, nicht das ganze Repo
|
||||||
|
(5,7 MB getrackt, 666 MB Arbeitsverzeichnis lokal).
|
||||||
|
|
||||||
|
**Das Notes-Repo ist dabei nach Gitea gezogen** (`mi/notes` auf
|
||||||
|
`git.javagil.de`, alle vier Branches; `origin` ist Gitea, GitHub bleibt als
|
||||||
|
`github` — dieselbe Aufteilung wie D95). Der Deploy-Key gilt je Repo, also
|
||||||
|
zuerst der Umzug, dann der Schlüssel, statt beides zweimal.
|
||||||
|
|
||||||
## D89 — Zwei Stunden verlorene Arbeit: vier Netze gegen den stillen Live-Verlust
|
## D89 — Zwei Stunden verlorene Arbeit: vier Netze gegen den stillen Live-Verlust
|
||||||
Der Vorfall (2026-08-27 vormittags, gemeldet vom Nutzer): Zwei Stunden Arbeit
|
Der Vorfall (2026-08-27 vormittags, gemeldet vom Nutzer): Zwei Stunden Arbeit
|
||||||
an einem geteilten Dokument (`?live=`, PWA) waren weg — die Server-Historie
|
an einem geteilten Dokument (`?live=`, PWA) waren weg — die Server-Historie
|
||||||
@@ -7758,6 +7815,58 @@ Entscheidungen: **D94** und `docs/rfc/002-mehrfenster.md`. Der Präsenz-Kanal
|
|||||||
(Herzschlag, Timeout, Notluke) und der modale Dialog sind ersatzlos
|
(Herzschlag, Timeout, Notluke) und der modale Dialog sind ersatzlos
|
||||||
ausgebaut; die drei anderen Netze (Sicherungen, Rettung, Wachhund) bleiben.
|
ausgebaut; die drei anderen Netze (Sicherungen, Rettung, Wachhund) bleiben.
|
||||||
|
|
||||||
|
**Nachtrag 2 — die Wiederverbindung sendet nie von selbst (2026-09-04).**
|
||||||
|
Der zweite Datenverlust am selben Dokument, diesmal nachgemessen bis zur
|
||||||
|
Ursache: Version 679 (11:49 Uhr) kam von einem anderen Client (der PWA auf
|
||||||
|
dem Telefon) und war byte-genau Version 667 — der letzte Stand, den das
|
||||||
|
Telefon am Vortag gesehen hatte — plus drei Faltmarken. Die 16 Zeilen aus
|
||||||
|
668 bis 671 waren weg; der Nutzer hat 671 per Rollback (v680) zurückgeholt.
|
||||||
|
Dasselbe Muster wie v660 am Abend davor (byte-gleich mit v339).
|
||||||
|
|
||||||
|
**Der Mechanismus stand im D90-Nachtrag, als Rettung gemeint.** Die
|
||||||
|
Wiederverbindung nach einer toten Sitzung holte den Server-Stand als
|
||||||
|
Schattenkopie und schickte dann jeden abweichenden lokalen Text als Diff —
|
||||||
|
„ungesendeter Text bleibt stehen, die keepMine-Semantik". Sie konnte nicht
|
||||||
|
unterscheiden, ob der lokale Text **neuer** ist als der Server (ungesendete
|
||||||
|
Arbeit) oder **älter** (ein Fenster, das lange geschlafen hat): In beiden
|
||||||
|
Fällen ist er „anders", und die alte Fassung ging als Diff gegen den
|
||||||
|
aktuellen Server hinaus, was einer Vollersetzung gleichkommt. Die Kette auf
|
||||||
|
dem Telefon: Die PWA wacht auf, der erste Abruf scheitert (Funk noch aus),
|
||||||
|
die Sitzung ist tot, der Editor zeigt den lokal gespeicherten Text vom
|
||||||
|
Vortag. Ein Tipp auf ein Falt-Zeichen ändert ihn (Falten schreibt in den
|
||||||
|
Text, D38-Nachtrag 2). Der Wachhund verbindet neu, sieht „lokal ≠ Server"
|
||||||
|
und schickt.
|
||||||
|
|
||||||
|
**Entscheidend ist nicht der Vergleich der Texte, sondern ob hier gearbeitet
|
||||||
|
wurde.** Zwei Zähler in app.js: einer zählt jede Textänderung an einem
|
||||||
|
Server-Dokument — mit oder ohne Sitzung, denn das Falten auf einem toten
|
||||||
|
Telefon ist genau der Fall —, der andere hält den Stand, den der Server
|
||||||
|
zuletzt bestätigt hat oder den wir von ihm übernommen haben (gelungenes
|
||||||
|
Senden, Laden, Volltext aus dem Feed, *Fremde übernehmen*). Ein Senden,
|
||||||
|
während weiter getippt wird, bestätigt nur, was es getragen hat. Die Regel
|
||||||
|
selbst steht headless in `live.js` (`reconnectAction`, D54-Nachtrag 3):
|
||||||
|
Texte gleich → nichts zu entscheiden; nicht gearbeitet → der Server gilt,
|
||||||
|
der lokale Text wandert in die lokalen Sicherungen; gearbeitet → das
|
||||||
|
vorhandene Konflikt-Band fragt, mit eigenem Text („Dieses Fenster war
|
||||||
|
getrennt und hat eigene Änderungen — der Stand auf dem Server weicht ab").
|
||||||
|
Nichts geschieht stumm, und beide Stände existieren, bis ein Mensch
|
||||||
|
entscheidet.
|
||||||
|
|
||||||
|
**Nachgemessen** im Browser gegen ein lokales Backend, das Telefon-Szenario
|
||||||
|
nachgestellt: Server auf v2 gesetzt, Backend gestoppt, Seite geladen — Chip
|
||||||
|
„getrennt", Editor zeigt den alten Spiegel (v1). Backend wieder da, nichts
|
||||||
|
angefasst: nach ≤ 20 s zeigt der Editor v2, der Server steht **unverändert
|
||||||
|
auf v2** (vorher hätte das Fenster hier v3 mit dem alten Text geschrieben).
|
||||||
|
Zweiter Durchlauf mit einer getippten Zeile im getrennten Zustand: Das Band
|
||||||
|
erscheint mit dem neuen Text, der Server bleibt auf v2, der lokale Text
|
||||||
|
steht; *Eigene durchsetzen* schreibt v3 mit genau dieser Zeile. 669 Tests,
|
||||||
|
davon 3 neue.
|
||||||
|
|
||||||
|
**Was bleibt:** Ein Fenster, das mit **ungesendeter** Arbeit ganz beendet
|
||||||
|
wird (Android räumt die PWA ab), startet mit dem Server-Stand; die Arbeit
|
||||||
|
liegt dann in den lokalen Sicherungen (Netz 3 aus dem Haupttext), nicht im
|
||||||
|
Editor — unverändert seit D89.
|
||||||
|
|
||||||
## D90 — Die Dokumentart steht grau hinter dem Namens-Chip
|
## D90 — Die Dokumentart steht grau hinter dem Namens-Chip
|
||||||
Nutzerwunsch, unmittelbar aus dem D89-Vorfall: Hinter der Brotkrume
|
Nutzerwunsch, unmittelbar aus dem D89-Vorfall: Hinter der Brotkrume
|
||||||
(„Werkbaum › Name", D81) soll erkennbar sein, was für ein Dokument vorn ist —
|
(„Werkbaum › Name", D81) soll erkennbar sein, was für ein Dokument vorn ist —
|
||||||
|
|||||||
@@ -951,6 +951,13 @@ anderen, ohne neu zu laden.
|
|||||||
wandert er in die **lokalen Sicherungen**: Für Server-Dokumente sammelt der
|
wandert er in die **lokalen Sicherungen**: Für Server-Dokumente sammelt der
|
||||||
10-Minuten-Takt sie wieder, und das Uhr-Menü zeigt sie als eigenen Abschnitt
|
10-Minuten-Takt sie wieder, und das Uhr-Menü zeigt sie als eigenen Abschnitt
|
||||||
unter den Server-Meilensteinen. Siehe D89.
|
unter den Server-Meilensteinen. Siehe D89.
|
||||||
|
- **Eine tote Sitzung verbindet sich von selbst neu — und sendet dabei nie
|
||||||
|
von selbst.** Wurde in diesem Fenster seit dem letzten gelungenen Abgleich
|
||||||
|
nicht gearbeitet, gilt der Stand des Servers; der lokale Text ist dann nur
|
||||||
|
ein alter Spiegel und wandert in die lokalen Sicherungen. Wurde gearbeitet,
|
||||||
|
fragt das Konflikt-Band, wessen Fassung gelten soll. Ein Fenster, das mit
|
||||||
|
dem Stand vom Vortag aufwacht, kann den Plan so nicht mehr überschreiben.
|
||||||
|
Siehe D89-Nachtrag 2.
|
||||||
|
|
||||||
**Ein zweites Werkbaum-Fenster desselben Browsers** — Tab, Fenster oder PWA
|
**Ein zweites Werkbaum-Fenster desselben Browsers** — Tab, Fenster oder PWA
|
||||||
— kollidiert nur noch am **selben nicht geteilten Dokument**: Text, Name und
|
— kollidiert nur noch am **selben nicht geteilten Dokument**: Text, Name und
|
||||||
|
|||||||
@@ -124,8 +124,8 @@
|
|||||||
- [^] #bld.backend.proxy: Apache passes /api/ on, in .htaccess (XS) %% measured: the P flag is allowed
|
- [^] #bld.backend.proxy: Apache passes /api/ on, in .htaccess (XS) %% measured: the P flag is allowed
|
||||||
- [^] #bld.backend.remote: One command for everything on the server (S) %% remote <target> <action>
|
- [^] #bld.backend.remote: One command for everything on the server (S) %% remote <target> <action>
|
||||||
- [^] #bld.backend.backup: Save the database, and prove it can be read back (XS)
|
- [^] #bld.backend.backup: Save the database, and prove it can be read back (XS)
|
||||||
- [x] #bld.werkator: Also built by Werkator, the project's own CI (S) %% .werkator.yml, backend + frontend
|
- [^] #bld.werkator: Also built by Werkator, the project's own CI (S) %% .werkator.yml, backend + frontend
|
||||||
- [x] #bld.gitea: The repository lives on Gitea, GitHub stays a clone (S)
|
- [^] #bld.gitea: The repository lives on Gitea, GitHub stays a clone (S)
|
||||||
- [-] #bld.ghpages: Deploy through a gh-pages branch (S) %% GitHub Actions instead
|
- [-] #bld.ghpages: Deploy through a gh-pages branch (S) %% GitHub Actions instead
|
||||||
- [-] #bld.gfonts: Fonts from Google (XS) %% self-hosted, no third-party request
|
- [-] #bld.gfonts: Fonts from Google (XS) %% self-hosted, no third-party request
|
||||||
- [ ] #be: Backend (XL)
|
- [ ] #be: Backend (XL)
|
||||||
@@ -151,6 +151,7 @@
|
|||||||
- [~] #col.git: Git as the shared store (L)
|
- [~] #col.git: Git as the shared store (L)
|
||||||
| [ ] #col.git.pr: A file in a repository, changed by pull request (S) %% works today, no code
|
| [ ] #col.git.pr: A file in a repository, changed by pull request (S) %% works today, no code
|
||||||
| [^] #col.git.pull: A script pulls the server document and commits it (S) %% tools/pull-doc --git-commit, dated commits — see D88
|
| [^] #col.git.pull: A script pulls the server document and commits it (S) %% tools/pull-doc --git-commit, dated commits — see D88
|
||||||
|
- [^] #col.git.timer: A timer on the backend host mirrors a list of documents into a branch (S) %% tools/mirror-docs, systemd timer — see D88-Nachtrag 3
|
||||||
| [?] #col.git.auto: The backend commits every change (L) :#be.scaffold
|
| [?] #col.git.auto: The backend commits every change (L) :#be.scaffold
|
||||||
- [?] #col.git.hist: History and restore (M)
|
- [?] #col.git.hist: History and restore (M)
|
||||||
- [?] #col.git.diff: Diff between two versions (S)
|
- [?] #col.git.diff: Diff between two versions (S)
|
||||||
@@ -970,6 +971,13 @@
|
|||||||
worktree — date, title and version in the message, no commit when nothing
|
worktree — date, title and version in the message, no commit when nothing
|
||||||
changed. Run from cron, a shared plan archives itself.
|
changed. Run from cron, a shared plan archives itself.
|
||||||
|
|
||||||
|
#col.git.timer
|
||||||
|
tools/mirror-docs reads a list of documents (uuid and path), runs pull-doc
|
||||||
|
--git-commit for each and pushes once. As a systemd timer on the backend host
|
||||||
|
it mirrors the shared plans every 15 minutes into a werkbaum-mirror branch —
|
||||||
|
the net outside the browser and outside the backend, running whether or not
|
||||||
|
anyone remembers.
|
||||||
|
|
||||||
#col.git.auto
|
#col.git.auto
|
||||||
The server turns every change into a commit. History, restore and branches
|
The server turns every change into a commit. History, restore and branches
|
||||||
follow from that, with commit granularity as the open question.
|
follow from that, with commit granularity as the open question.
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,7 @@ Die Datei selbst war also einwandfrei erreichbar; es fehlte genau ein Header.
|
|||||||
- Die Konfiguration liegt beim Unix-Benutzer `mih09-git` auf mih09 und in **keinem** Repository; sie ist im Wortlaut in D96 festgehalten.
|
- Die Konfiguration liegt beim Unix-Benutzer `mih09-git` auf mih09 und in **keinem** Repository; sie ist im Wortlaut in D96 festgehalten.
|
||||||
- Giteas eigener `[cors]`-Abschnitt und die `.htaccess` der Domain teilen sich die Arbeit: die API-Route deckt Gitea ab, die Web-Route der Apache davor.
|
- Giteas eigener `[cors]`-Abschnitt und die `.htaccess` der Domain teilen sich die Arbeit: die API-Route deckt Gitea ab, die Web-Route der Apache davor.
|
||||||
|
|
||||||
#### Szenario#1.01: Ein Beispiel-Link öffnet den Plan statt eines CORS-Fehlers
|
#### Szenario#3.01: Ein Beispiel-Link öffnet den Plan statt eines CORS-Fehlers
|
||||||
|
|
||||||
Damit die Links auf `origin` zeigen können und nicht auf einen Klon.
|
Damit die Links auf `origin` zeigen können und nicht auf einen Klon.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Damit die Links auf `origin` zeigen können und nicht auf einen Klon.
|
|||||||
|
|
||||||
- Messung 2026-09-03 auf allen fünf Beispieldateien: je HTTP 200 mit genau einem `Access-Control-Allow-Origin: *`.
|
- Messung 2026-09-03 auf allen fünf Beispieldateien: je HTTP 200 mit genau einem `Access-Control-Allow-Origin: *`.
|
||||||
|
|
||||||
#### Szenario#1.02: Die API-Route trägt den Header genau einmal
|
#### Szenario#3.02: Die API-Route trägt den Header genau einmal
|
||||||
|
|
||||||
Damit die Antwort gültig bleibt — zwei `Access-Control-Allow-Origin`-Header verwirft der Browser als ungültig.
|
Damit die Antwort gültig bleibt — zwei `Access-Control-Allow-Origin`-Header verwirft der Browser als ungültig.
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ Damit die Antwort gültig bleibt — zwei `Access-Control-Allow-Origin`-Header v
|
|||||||
|
|
||||||
- Messung 2026-09-03: `/api/v1/repos/mi/werkdock/raw/README.md` → ein Header (zuvor, mit der ersten Fassung der `.htaccess`, waren es zwei).
|
- Messung 2026-09-03: `/api/v1/repos/mi/werkdock/raw/README.md` → ein Header (zuvor, mit der ersten Fassung der `.htaccess`, waren es zwei).
|
||||||
|
|
||||||
#### Szenario#1.03: Gewöhnliche Repo-Seiten bleiben unberührt
|
#### Szenario#3.03: Gewöhnliche Repo-Seiten bleiben unberührt
|
||||||
|
|
||||||
Damit der Header nur dort steht, wo er gebraucht wird, und nicht site-weit.
|
Damit der Header nur dort steht, wo er gebraucht wird, und nicht site-weit.
|
||||||
|
|
||||||
@@ -7,6 +7,9 @@ Jeder Pull Request bekommt hier **eine** Datei.
|
|||||||
`JJJJ-MM-TT-PR#<nummer>-kurze-beschreibung.md`
|
`JJJJ-MM-TT-PR#<nummer>-kurze-beschreibung.md`
|
||||||
|
|
||||||
Die Nummer ist die des Gitea-PR auf <https://git.javagil.de/mi/werkbaum>.
|
Die Nummer ist die des Gitea-PR auf <https://git.javagil.de/mi/werkbaum>.
|
||||||
|
Gitea begann nach dem Umzug (D95) wieder bei 1, während die Historie schon
|
||||||
|
GitHub-PR #1 und #2 kennt; die Nummern 1 und 2 sind deshalb abgebrannt und die
|
||||||
|
Zählung setzt bei **#3** auf.
|
||||||
Steht sie noch nicht fest, gilt `PR#000` als Platzhalter — im Dateinamen wie in
|
Steht sie noch nicht fest, gilt `PR#000` als Platzhalter — im Dateinamen wie in
|
||||||
den Szenario-IDs — und wird nachgezogen, sobald der PR offen ist.
|
den Szenario-IDs — und wird nachgezogen, sobald der PR offen ist.
|
||||||
|
|
||||||
|
|||||||
+39
-7
@@ -3484,6 +3484,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"Server-Dokument nicht geladen: {url} ({error}). Läuft das Backend, und ist die Adresse eine Dokument-Adresse (…/documents/<uuid>)?",
|
liveLoadWarn:"Server-Dokument nicht geladen: {url} ({error}). Läuft das Backend, und ist die Adresse eine Dokument-Adresse (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Deine Änderung war nicht mehr anwendbar ({error}) — der Stand wurde einmal frisch geholt.",
|
liveStaleWarn:"Deine Änderung war nicht mehr anwendbar ({error}) — der Stand wurde einmal frisch geholt.",
|
||||||
liveConflictText:"Jemand hat dieselben Zeilen geändert. Wessen Fassung soll gelten?",
|
liveConflictText:"Jemand hat dieselben Zeilen geändert. Wessen Fassung soll gelten?",
|
||||||
|
liveReconnectText:"Dieses Fenster war getrennt und hat eigene Änderungen — der Stand auf dem Server weicht ab. Wessen Fassung soll gelten?",
|
||||||
liveConflictTheirs:"Fremde übernehmen",
|
liveConflictTheirs:"Fremde übernehmen",
|
||||||
liveConflictMine:"Eigene durchsetzen",
|
liveConflictMine:"Eigene durchsetzen",
|
||||||
riskTooltip:"High Risk – Aufwand noch unklar.",
|
riskTooltip:"High Risk – Aufwand noch unklar.",
|
||||||
@@ -3627,6 +3628,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"Server document not loaded: {url} ({error}). Is the backend running, and is the address a document address (…/documents/<uuid>)?",
|
liveLoadWarn:"Server document not loaded: {url} ({error}). Is the backend running, and is the address a document address (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Your change no longer applied ({error}) — the document was fetched afresh once.",
|
liveStaleWarn:"Your change no longer applied ({error}) — the document was fetched afresh once.",
|
||||||
liveConflictText:"Someone changed the same lines. Whose version should win?",
|
liveConflictText:"Someone changed the same lines. Whose version should win?",
|
||||||
|
liveReconnectText:"This window was disconnected and holds changes of its own — the server state differs. Whose version should win?",
|
||||||
liveConflictTheirs:"Take theirs",
|
liveConflictTheirs:"Take theirs",
|
||||||
liveConflictMine:"Keep mine",
|
liveConflictMine:"Keep mine",
|
||||||
riskTooltip:"High risk – effort still unclear.",
|
riskTooltip:"High risk – effort still unclear.",
|
||||||
@@ -3769,6 +3771,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"Documento del servidor no cargado: {url} ({error}). ¿Está el backend en marcha y es la dirección la de un documento (…/documents/<uuid>)?",
|
liveLoadWarn:"Documento del servidor no cargado: {url} ({error}). ¿Está el backend en marcha y es la dirección la de un documento (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Tu cambio ya no era aplicable ({error}): se volvió a cargar el estado una vez.",
|
liveStaleWarn:"Tu cambio ya no era aplicable ({error}): se volvió a cargar el estado una vez.",
|
||||||
liveConflictText:"Alguien cambió las mismas líneas. ¿Qué versión debe prevalecer?",
|
liveConflictText:"Alguien cambió las mismas líneas. ¿Qué versión debe prevalecer?",
|
||||||
|
liveReconnectText:"Esta ventana estuvo desconectada y tiene cambios propios — el estado en el servidor difiere. ¿Qué versión debe prevalecer?",
|
||||||
liveConflictTheirs:"Tomar la ajena",
|
liveConflictTheirs:"Tomar la ajena",
|
||||||
liveConflictMine:"Mantener la mía",
|
liveConflictMine:"Mantener la mía",
|
||||||
riskTooltip:"Alto riesgo – esfuerzo aún incierto.",
|
riskTooltip:"Alto riesgo – esfuerzo aún incierto.",
|
||||||
@@ -3911,6 +3914,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"Document du serveur non chargé : {url} ({error}). Le backend tourne-t-il, et l'adresse est-elle celle d'un document (…/documents/<uuid>) ?",
|
liveLoadWarn:"Document du serveur non chargé : {url} ({error}). Le backend tourne-t-il, et l'adresse est-elle celle d'un document (…/documents/<uuid>) ?",
|
||||||
liveStaleWarn:"Ta modification n'était plus applicable ({error}) — l'état a été rechargé une fois.",
|
liveStaleWarn:"Ta modification n'était plus applicable ({error}) — l'état a été rechargé une fois.",
|
||||||
liveConflictText:"Quelqu'un a modifié les mêmes lignes. Quelle version doit l'emporter ?",
|
liveConflictText:"Quelqu'un a modifié les mêmes lignes. Quelle version doit l'emporter ?",
|
||||||
|
liveReconnectText:"Cette fenêtre a été déconnectée et contient ses propres modifications — l'état sur le serveur diffère. Quelle version doit l'emporter ?",
|
||||||
liveConflictTheirs:"Prendre la sienne",
|
liveConflictTheirs:"Prendre la sienne",
|
||||||
liveConflictMine:"Garder la mienne",
|
liveConflictMine:"Garder la mienne",
|
||||||
riskTooltip:"Risque élevé – effort encore incertain.",
|
riskTooltip:"Risque élevé – effort encore incertain.",
|
||||||
@@ -4053,6 +4057,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"Nie wczytano dokumentu z serwera: {url} ({error}). Czy backend działa i czy adres wskazuje dokument (…/documents/<uuid>)?",
|
liveLoadWarn:"Nie wczytano dokumentu z serwera: {url} ({error}). Czy backend działa i czy adres wskazuje dokument (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Twoja zmiana nie dała się już zastosować ({error}) — stan pobrano raz od nowa.",
|
liveStaleWarn:"Twoja zmiana nie dała się już zastosować ({error}) — stan pobrano raz od nowa.",
|
||||||
liveConflictText:"Ktoś zmienił te same wiersze. Która wersja ma obowiązywać?",
|
liveConflictText:"Ktoś zmienił te same wiersze. Która wersja ma obowiązywać?",
|
||||||
|
liveReconnectText:"To okno było rozłączone i ma własne zmiany — stan na serwerze się różni. Która wersja ma obowiązywać?",
|
||||||
liveConflictTheirs:"Przyjmij cudzą",
|
liveConflictTheirs:"Przyjmij cudzą",
|
||||||
liveConflictMine:"Zachowaj własną",
|
liveConflictMine:"Zachowaj własną",
|
||||||
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
||||||
@@ -4195,6 +4200,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"Документ с сервера не загружен: {url} ({error}). Запущен ли бэкенд и является ли адрес адресом документа (…/documents/<uuid>)?",
|
liveLoadWarn:"Документ с сервера не загружен: {url} ({error}). Запущен ли бэкенд и является ли адрес адресом документа (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Ваше изменение больше не применялось ({error}) — состояние загружено заново.",
|
liveStaleWarn:"Ваше изменение больше не применялось ({error}) — состояние загружено заново.",
|
||||||
liveConflictText:"Кто-то изменил те же строки. Чья версия должна остаться?",
|
liveConflictText:"Кто-то изменил те же строки. Чья версия должна остаться?",
|
||||||
|
liveReconnectText:"Это окно было отключено и содержит собственные изменения — состояние на сервере отличается. Чья версия должна остаться?",
|
||||||
liveConflictTheirs:"Принять чужую",
|
liveConflictTheirs:"Принять чужую",
|
||||||
liveConflictMine:"Оставить свою",
|
liveConflictMine:"Оставить свою",
|
||||||
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
||||||
@@ -4337,6 +4343,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"सर्वर दस्तावेज़ लोड नहीं हुआ: {url} ({error})। क्या बैकएंड चल रहा है और क्या पता दस्तावेज़ का पता है (…/documents/<uuid>)?",
|
liveLoadWarn:"सर्वर दस्तावेज़ लोड नहीं हुआ: {url} ({error})। क्या बैकएंड चल रहा है और क्या पता दस्तावेज़ का पता है (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"आपका बदलाव अब लागू नहीं हो सका ({error}) — स्थिति एक बार नए सिरे से ली गई।",
|
liveStaleWarn:"आपका बदलाव अब लागू नहीं हो सका ({error}) — स्थिति एक बार नए सिरे से ली गई।",
|
||||||
liveConflictText:"किसी और ने वही पंक्तियाँ बदली हैं। किसका संस्करण रहे?",
|
liveConflictText:"किसी और ने वही पंक्तियाँ बदली हैं। किसका संस्करण रहे?",
|
||||||
|
liveReconnectText:"यह विंडो डिस्कनेक्ट थी और इसमें अपने बदलाव हैं — सर्वर की स्थिति अलग है। किसका संस्करण रहे?",
|
||||||
liveConflictTheirs:"दूसरे का लें",
|
liveConflictTheirs:"दूसरे का लें",
|
||||||
liveConflictMine:"अपना रखें",
|
liveConflictMine:"अपना रखें",
|
||||||
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
||||||
@@ -4490,6 +4497,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"未能加载服务器文档:{url}({error})。后端在运行吗?该地址是文档地址(…/documents/<uuid>)吗?",
|
liveLoadWarn:"未能加载服务器文档:{url}({error})。后端在运行吗?该地址是文档地址(…/documents/<uuid>)吗?",
|
||||||
liveStaleWarn:"你的更改已无法应用({error})——已重新获取一次当前状态。",
|
liveStaleWarn:"你的更改已无法应用({error})——已重新获取一次当前状态。",
|
||||||
liveConflictText:"有人改动了同样的行。以谁的版本为准?",
|
liveConflictText:"有人改动了同样的行。以谁的版本为准?",
|
||||||
|
liveReconnectText:"此窗口曾断开连接并有自己的改动——服务器上的状态与此不同。以谁的版本为准?",
|
||||||
liveConflictTheirs:"采用对方的",
|
liveConflictTheirs:"采用对方的",
|
||||||
liveConflictMine:"保留我的",
|
liveConflictMine:"保留我的",
|
||||||
riskTooltip:"高风险 – 工作量尚不明确。",
|
riskTooltip:"高风险 – 工作量尚不明确。",
|
||||||
@@ -4632,6 +4640,7 @@ const I18N = {
|
|||||||
liveLoadWarn:"サーバー文書を読み込めませんでした: {url}({error})。バックエンドは動いていますか。アドレスは文書のアドレス(…/documents/<uuid>)ですか。",
|
liveLoadWarn:"サーバー文書を読み込めませんでした: {url}({error})。バックエンドは動いていますか。アドレスは文書のアドレス(…/documents/<uuid>)ですか。",
|
||||||
liveStaleWarn:"あなたの変更はもう適用できませんでした({error})。状態を一度取り直しました。",
|
liveStaleWarn:"あなたの変更はもう適用できませんでした({error})。状態を一度取り直しました。",
|
||||||
liveConflictText:"同じ行が他の人にも変更されました。どちらの版を採りますか。",
|
liveConflictText:"同じ行が他の人にも変更されました。どちらの版を採りますか。",
|
||||||
|
liveReconnectText:"このウィンドウは切断されていて独自の変更があります — サーバー側の状態は異なります。どちらの版を採りますか。",
|
||||||
liveConflictTheirs:"相手の版",
|
liveConflictTheirs:"相手の版",
|
||||||
liveConflictMine:"自分の版",
|
liveConflictMine:"自分の版",
|
||||||
riskTooltip:"高リスク – 規模はまだ不明。",
|
riskTooltip:"高リスク – 規模はまだ不明。",
|
||||||
@@ -5787,6 +5796,7 @@ document.addEventListener('pointerdown', e => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
||||||
|
liveSynced(); /* der Text im Feld ist jetzt der geladene Stand — nichts davon ist Arbeit dieses Fensters */
|
||||||
clearLens(); /* die Personen-Linse (D87) gilt je Dokument-Sitzung */
|
clearLens(); /* die Personen-Linse (D87) gilt je Dokument-Sitzung */
|
||||||
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
||||||
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
||||||
@@ -6396,6 +6406,17 @@ function nextSeq(){
|
|||||||
|
|
||||||
let liveState = null; /* {urls, id, version, shadow, seq, pushTimer, feedAbort, busy} */
|
let liveState = null; /* {urls, id, version, shadow, seq, pushTimer, feedAbort, busy} */
|
||||||
let liveConflict = null; /* offener Konflikt: bis der Mensch entscheidet, ruht alles */
|
let liveConflict = null; /* offener Konflikt: bis der Mensch entscheidet, ruht alles */
|
||||||
|
/* Wurde in diesem Fenster seit dem letzten gelungenen Abgleich gearbeitet?
|
||||||
|
Zwei Zähler statt eines Flags: `liveEditSeq` zählt jede Textänderung an
|
||||||
|
einem Server-Dokument (auch ohne Sitzung — das Falten auf einem toten
|
||||||
|
Telefon ist genau der Fall), `liveSyncSeq` hält den Stand, den der Server
|
||||||
|
zuletzt bestätigt hat oder den wir von ihm übernommen haben. Ein Senden,
|
||||||
|
während weiter getippt wird, gilt so nur für das, was es getragen hat.
|
||||||
|
Die Wiederverbindung fragt danach, statt den Text zu vergleichen
|
||||||
|
(D89-Nachtrag 2). */
|
||||||
|
let liveEditSeq = 0, liveSyncSeq = 0;
|
||||||
|
function liveEdited(){ return liveEditSeq !== liveSyncSeq; }
|
||||||
|
function liveSynced(){ liveSyncSeq = liveEditSeq; }
|
||||||
|
|
||||||
function liveActive(){ return !!liveState && activeId === liveState.id; }
|
function liveActive(){ return !!liveState && activeId === liveState.id; }
|
||||||
|
|
||||||
@@ -6488,6 +6509,8 @@ function adoptLive(doc){
|
|||||||
weckt den wartenden Feed 39 ms nach dem PATCH (D79). Sie bleibt ein
|
weckt den wartenden Feed 39 ms nach dem PATCH (D79). Sie bleibt ein
|
||||||
Debounce, kein Takt: Wer durchtippt, erzeugt weiterhin keine Version. */
|
Debounce, kein Takt: Wer durchtippt, erzeugt weiterhin keine Version. */
|
||||||
function scheduleLivePush(){
|
function scheduleLivePush(){
|
||||||
|
const d = activeDoc();
|
||||||
|
if(d && String(d.id).startsWith('live:')) liveEditSeq++; /* gearbeitet — mit oder ohne Sitzung */
|
||||||
if(!liveActive() || liveConflict) return;
|
if(!liveActive() || liveConflict) return;
|
||||||
if(liveState.pushTimer) clearTimeout(liveState.pushTimer);
|
if(liveState.pushTimer) clearTimeout(liveState.pushTimer);
|
||||||
liveState.pushTimer = setTimeout(() => { liveState.pushTimer = null; pushLive(); },
|
liveState.pushTimer = setTimeout(() => { liveState.pushTimer = null; pushLive(); },
|
||||||
@@ -6520,9 +6543,12 @@ function rescueSnapshot(docId, text){
|
|||||||
/* Eine tote Sitzung heilt sich SELBST (Nutzer-Einwand zu D89: „Ohne Reload
|
/* Eine tote Sitzung heilt sich SELBST (Nutzer-Einwand zu D89: „Ohne Reload
|
||||||
ist wieder alles tot?" — nein): Der Wachhund versucht die Wiederverbindung,
|
ist wieder alles tot?" — nein): Der Wachhund versucht die Wiederverbindung,
|
||||||
alle 10 s, solange das aktive Dokument ein Server-Dokument ohne Sitzung
|
alle 10 s, solange das aktive Dokument ein Server-Dokument ohne Sitzung
|
||||||
ist. Ungesendeter Text bleibt dabei STEHEN und geht als Diff an den Server
|
ist. Sie sendet NIE von selbst (D89-Nachtrag 2): Wurde hier seit dem
|
||||||
(die keepMine-Semantik aus dem Konflikt-Band) — die Wiederverbindung darf
|
letzten Abgleich nicht gearbeitet, gilt der Server-Stand — der lokale
|
||||||
nicht kosten, was sie retten soll. */
|
Text ist dann nur ein alter Spiegel; wurde gearbeitet, fragt das
|
||||||
|
Konflikt-Band, wessen Fassung gelten soll. Die frühere Fassung schickte
|
||||||
|
jeden abweichenden lokalen Text als Diff und ließ damit ein aufgewachtes
|
||||||
|
Telefon den Plan mit dem Stand vom Vortag überschreiben. */
|
||||||
let reconnectBusy = false, reconnectNext = 0;
|
let reconnectBusy = false, reconnectNext = 0;
|
||||||
async function reconnectLive(d){
|
async function reconnectLive(d){
|
||||||
if(reconnectBusy || Date.now() < reconnectNext) return;
|
if(reconnectBusy || Date.now() < reconnectNext) return;
|
||||||
@@ -6535,7 +6561,9 @@ async function reconnectLive(d){
|
|||||||
const serverText = live.normalize(doc.content || '');
|
const serverText = live.normalize(doc.content || '');
|
||||||
liveState = {urls, id: d.id, version: doc.version, shadow: live.lines(serverText),
|
liveState = {urls, id: d.id, version: doc.version, shadow: live.lines(serverText),
|
||||||
pushTimer: null, feedAbort: null, busy: false};
|
pushTimer: null, feedAbort: null, busy: false};
|
||||||
if(live.normalize(src.value) !== serverText) pushLive();
|
const what = live.reconnectAction(liveEdited(), src.value, serverText);
|
||||||
|
if(what === 'adopt') adoptLive(doc); /* sichert den lokalen Text vorher (D89) */
|
||||||
|
else if(what === 'ask') openConflict(doc.version, live.lines(serverText), 'liveReconnectText');
|
||||||
runFeed();
|
runFeed();
|
||||||
if(sourceWarning && (sourceWarning.type === 'liveLoad' || sourceWarning.type === 'liveStale')){
|
if(sourceWarning && (sourceWarning.type === 'liveLoad' || sourceWarning.type === 'liveStale')){
|
||||||
sourceWarning = null;
|
sourceWarning = null;
|
||||||
@@ -6594,6 +6622,7 @@ async function pushLive(meilenstein){
|
|||||||
const sitzung = liveState;
|
const sitzung = liveState;
|
||||||
sitzung.busy = true;
|
sitzung.busy = true;
|
||||||
const seq = nextSeq();
|
const seq = nextSeq();
|
||||||
|
const editStand = liveEditSeq; /* was danach getippt wird, trägt dieses Senden nicht */
|
||||||
/* Die Basis, gegen die `ops` gerechnet sind — VOR dem Warten festgehalten.
|
/* Die Basis, gegen die `ops` gerechnet sind — VOR dem Warten festgehalten.
|
||||||
Sie hinterher aus `liveState` zu lesen hieße anzunehmen, dass sich
|
Sie hinterher aus `liveState` zu lesen hieße anzunehmen, dass sich
|
||||||
dazwischen nichts ändert; genau diese Annahme ist gebrochen, sobald der
|
dazwischen nichts ändert; genau diese Annahme ist gebrochen, sobald der
|
||||||
@@ -6628,6 +6657,7 @@ async function pushLive(meilenstein){
|
|||||||
sitzung.shadow = live.applyOps(
|
sitzung.shadow = live.applyOps(
|
||||||
foreign.length ? live.applyOps(basis, foreign) : basis, meine);
|
foreign.length ? live.applyOps(basis, foreign) : basis, meine);
|
||||||
sitzung.version = result.version;
|
sitzung.version = result.version;
|
||||||
|
liveSyncSeq = editStand;
|
||||||
/* Ein gelungener Abgleich räumt liegengebliebene Live-Warnungen SOFORT
|
/* Ein gelungener Abgleich räumt liegengebliebene Live-Warnungen SOFORT
|
||||||
weg — „nicht geladen" oder „ungesendet" neben funktionierendem Senden
|
weg — „nicht geladen" oder „ungesendet" neben funktionierendem Senden
|
||||||
wäre eine Lüge, und der Wachhund-Tick kann gedrosselt sein (1/min in
|
wäre eine Lüge, und der Wachhund-Tick kann gedrosselt sein (1/min in
|
||||||
@@ -6737,6 +6767,7 @@ function applyFeed(feed){
|
|||||||
liveState.version = feed.currentVersion;
|
liveState.version = feed.currentVersion;
|
||||||
liveState.shadow = live.lines(neu);
|
liveState.shadow = live.lines(neu);
|
||||||
setLiveText(live.text(liveState.shadow), null);
|
setLiveText(live.text(liveState.shadow), null);
|
||||||
|
liveSynced();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const alt = liveState.shadow;
|
const alt = liveState.shadow;
|
||||||
@@ -6808,9 +6839,9 @@ function setLiveText(text, caret){
|
|||||||
/* Echte Überschneidung: Der Server hat abgelehnt, und jetzt entscheidet der
|
/* Echte Überschneidung: Der Server hat abgelehnt, und jetzt entscheidet der
|
||||||
Mensch. Bis dahin ruhen Senden und Feed — sonst zöge der Stand unter der
|
Mensch. Bis dahin ruhen Senden und Feed — sonst zöge der Stand unter der
|
||||||
Frage weg, die gerade gestellt ist. */
|
Frage weg, die gerade gestellt ist. */
|
||||||
function openConflict(version, serverLines){
|
function openConflict(version, serverLines, textKey){
|
||||||
if(liveConflict) return; /* eine Frage zur Zeit */
|
if(liveConflict) return; /* eine Frage zur Zeit */
|
||||||
liveConflict = {version, serverLines};
|
liveConflict = {version, serverLines, textKey: textKey || 'liveConflictText'};
|
||||||
showConflictBanner();
|
showConflictBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6829,6 +6860,7 @@ function takeTheirs(){
|
|||||||
liveState.version = c.version;
|
liveState.version = c.version;
|
||||||
liveState.shadow = c.serverLines;
|
liveState.shadow = c.serverLines;
|
||||||
setLiveText(live.text(c.serverLines), null);
|
setLiveText(live.text(c.serverLines), null);
|
||||||
|
liveSynced();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Eigene durchsetzen: Die Schattenkopie zieht auf den Server-Stand nach, der
|
/* Eigene durchsetzen: Die Schattenkopie zieht auf den Server-Stand nach, der
|
||||||
@@ -6853,7 +6885,7 @@ function showConflictBanner(){
|
|||||||
'<div class="live-conflict-actions">' +
|
'<div class="live-conflict-actions">' +
|
||||||
'<button type="button" class="theirs"></button>' +
|
'<button type="button" class="theirs"></button>' +
|
||||||
'<button type="button" class="mine"></button></div>';
|
'<button type="button" class="mine"></button></div>';
|
||||||
bar.querySelector('span').textContent = t('liveConflictText');
|
bar.querySelector('span').textContent = t(liveConflict.textKey);
|
||||||
const theirs = bar.querySelector('.theirs');
|
const theirs = bar.querySelector('.theirs');
|
||||||
const mine = bar.querySelector('.mine');
|
const mine = bar.querySelector('.mine');
|
||||||
theirs.textContent = t('liveConflictTheirs');
|
theirs.textContent = t('liveConflictTheirs');
|
||||||
|
|||||||
@@ -302,3 +302,20 @@ export function feedAction(feed, shadowVersion, pushInFlight){
|
|||||||
if(Array.isArray(feed.ops) && feed.fromVersion === shadowVersion) return 'apply';
|
if(Array.isArray(feed.ops) && feed.fromVersion === shadowVersion) return 'apply';
|
||||||
return 'skip';
|
return 'skip';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Was die Wiederverbindung nach einer toten Sitzung mit dem lokalen Text
|
||||||
|
tut (D89-Nachtrag 2). Die erste Fassung schickte ihn, sobald er vom Server
|
||||||
|
abwich — und konnte nicht unterscheiden, ob er NEUER ist als der Server
|
||||||
|
(ungesendete Arbeit) oder ÄLTER (ein Fenster, das lange geschlafen hat):
|
||||||
|
Ein Telefon, das mit dem Stand vom Vortag aufwachte, schrieb ihn als
|
||||||
|
Vollersetzung über den aktuellen Plan. Entscheidend ist deshalb nicht der
|
||||||
|
Vergleich der Texte, sondern ob in diesem Fenster seit dem letzten
|
||||||
|
gelungenen Abgleich GEARBEITET wurde.
|
||||||
|
|
||||||
|
'none' – Texte gleich: nur Version und Schattenkopie übernehmen
|
||||||
|
'adopt' – nicht gearbeitet: der Server-Stand gilt, der lokale weicht
|
||||||
|
'ask' – gearbeitet: beide Stände existieren, ein Mensch entscheidet */
|
||||||
|
export function reconnectAction(edited, localText, serverText){
|
||||||
|
if(normalize(localText) === normalize(serverText)) return 'none';
|
||||||
|
return edited ? 'ask' : 'adopt';
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {describe, it, expect} from 'vitest';
|
|||||||
import {
|
import {
|
||||||
liveUrls, normalize, lines, text, computeOps, applyOps,
|
liveUrls, normalize, lines, text, computeOps, applyOps,
|
||||||
mapLine, caretToLineCol, lineColToCaret, feedAction, serverBase, documentsUrl, infoUrl,
|
mapLine, caretToLineCol, lineColToCaret, feedAction, serverBase, documentsUrl, infoUrl,
|
||||||
|
reconnectAction,
|
||||||
} from '../src/live.js';
|
} from '../src/live.js';
|
||||||
|
|
||||||
describe('Adressen', () => {
|
describe('Adressen', () => {
|
||||||
@@ -243,3 +244,20 @@ describe('Feed-Antwort anwenden oder nicht', () => {
|
|||||||
expect(feedAction({fromVersion: null, currentVersion: 87, content: 'x'}, 4, true)).toBe('skip');
|
expect(feedAction({fromVersion: null, currentVersion: 87, content: 'x'}, 4, true)).toBe('skip');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Wiederverbindung (D89-Nachtrag 2)', () => {
|
||||||
|
// Der Vorfall: Ein Telefon wacht mit dem Stand vom Vortag auf, die Sitzung
|
||||||
|
// ist tot, ein Tipp auf ein Falt-Zeichen — und die alte Fassung ging als
|
||||||
|
// Diff über den aktuellen Plan. Entscheidend ist nicht, ob die Texte
|
||||||
|
// abweichen, sondern ob hier gearbeitet wurde.
|
||||||
|
it('gleicher Text: nichts zu entscheiden', () => {
|
||||||
|
expect(reconnectAction(false, 'a\nb', 'a\nb')).toBe('none');
|
||||||
|
expect(reconnectAction(true, 'a\r\nb', 'a\nb')).toBe('none'); // Zeilenenden normalisiert
|
||||||
|
});
|
||||||
|
it('nicht gearbeitet: der Server gilt, auch wenn der lokale Text abweicht', () => {
|
||||||
|
expect(reconnectAction(false, 'alter Stand', 'neuer Stand')).toBe('adopt');
|
||||||
|
});
|
||||||
|
it('gearbeitet: fragen statt senden — beide Stände existieren', () => {
|
||||||
|
expect(reconnectAction(true, 'alter Stand plus Faltmarke', 'neuer Stand')).toBe('ask');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
# dorthin: `main`. Feature-Branches und Tags bleiben in Gitea — was auf GitHub
|
# dorthin: `main`. Feature-Branches und Tags bleiben in Gitea — was auf GitHub
|
||||||
# steht, ist der veröffentlichte Stand.
|
# steht, ist der veröffentlichte Stand.
|
||||||
#
|
#
|
||||||
# Der Klon ist kein Selbstzweck. An ihm hängen zwei Dinge, die nicht umziehen
|
# Der Klon ist kein Selbstzweck. An ihm hängt der GitHub-Pages-Workflow
|
||||||
# können:
|
# (`.github/workflows/pages.yml`), also die „latest build"-Instanz
|
||||||
# * der GitHub-Pages-Workflow (`.github/workflows/pages.yml`), also die
|
# mhoennig.github.io/werkbaum — die kann nicht umziehen.
|
||||||
# „latest build"-Instanz mhoennig.github.io/werkbaum;
|
#
|
||||||
# * die Beispiel-Links der READMEs, die per `?sourceUrl=` von
|
# Die Beispiel-Links der READMEs waren ein zweiter Grund und sind es nicht mehr:
|
||||||
# raw.githubusercontent.com laden — Gitea sendet auf `raw`-Dateien KEIN
|
# `git.javagil.de` sendet auf `raw`-Dateien inzwischen
|
||||||
# `Access-Control-Allow-Origin`, der Browser blockte sie also (D23, D95).
|
# `Access-Control-Allow-Origin`, sie laden deshalb von Gitea (D96, revidiert D95).
|
||||||
#
|
#
|
||||||
# Verwendung:
|
# Verwendung:
|
||||||
# scripts/push-github.sh [-n] [-y]
|
# scripts/push-github.sh [-n] [-y]
|
||||||
|
|||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Werkbaum — mirror-docs: mehrere Server-Dokumente in ein git-Worktree
|
||||||
|
# spiegeln. Liest eine Liste, ruft je Zeile `pull-doc --git-commit` und pusht
|
||||||
|
# auf Wunsch einmal am Ende. Gedacht für einen Timer auf dem Backend-Host
|
||||||
|
# (D88-Nachtrag 3): Der geteilte Plan bekommt so ohne Zutun eine Git-Historie.
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# mirror-docs [--push] [--api <basis>] <worktree> <liste>
|
||||||
|
#
|
||||||
|
# --push Nach dem Durchlauf `git push`, wenn etwas committet wurde.
|
||||||
|
# Der Branch braucht dafür einen Upstream.
|
||||||
|
# --api <basis> Basis-Adresse des Backends (…/api/v1) für Zeilen, die nur
|
||||||
|
# eine UUID nennen; sonst aus WERKBAUM_API.
|
||||||
|
# <worktree> Wurzel des git-Worktrees, in das gespiegelt wird.
|
||||||
|
# <liste> Textdatei, je Zeile `<uuid-oder-url> <pfad>`; der Pfad ist
|
||||||
|
# relativ zum Worktree. Leerzeilen und `#`-Zeilen sind
|
||||||
|
# Kommentar. Ein neues Dokument ist eine Zeile mehr.
|
||||||
|
#
|
||||||
|
# Jede Zeile läuft für sich: Ein gescheiterter Abruf bricht die übrigen nicht
|
||||||
|
# ab, steht aber auf stderr, und der Lauf endet dann mit 1 — ein Timer sieht
|
||||||
|
# so im Journal, was fehlt, statt still zu bleiben. Unverändertes committet
|
||||||
|
# nichts (das regelt pull-doc); ohne neuen Commit wird nicht gepusht.
|
||||||
|
#
|
||||||
|
# Siehe docs/DECISIONS.md D88.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage(){ sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||||
|
|
||||||
|
do_push=0; api="${WERKBAUM_API:-}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage; exit 0;;
|
||||||
|
--push) do_push=1; shift;;
|
||||||
|
--api) [ $# -ge 2 ] || { echo "Fehler: --api braucht eine Adresse." >&2; exit 2; }; api="$2"; shift 2;;
|
||||||
|
--*) echo "Fehler: unbekannter Schalter $1" >&2; echo >&2; usage >&2; exit 2;;
|
||||||
|
*) break;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ $# -eq 2 ] || { echo "Fehler: erwartet [--push] [--api <basis>] Worktree und Liste." >&2; echo >&2; usage >&2; exit 2; }
|
||||||
|
tree="$1"; list="$2"
|
||||||
|
pulldoc="$(dirname -- "$0")/pull-doc"
|
||||||
|
|
||||||
|
[ -x "$pulldoc" ] || { echo "Fehler: $pulldoc fehlt oder ist nicht ausführbar." >&2; exit 2; }
|
||||||
|
[ -f "$list" ] || { echo "Fehler: Liste $list gibt es nicht." >&2; exit 2; }
|
||||||
|
[ "$(git -C "$tree" rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ] \
|
||||||
|
|| { echo "Fehler: $tree ist kein git-Worktree." >&2; exit 2; }
|
||||||
|
|
||||||
|
before="$(git -C "$tree" rev-parse HEAD)"
|
||||||
|
fail=0; n=0
|
||||||
|
while read -r src path _; do
|
||||||
|
case "$src" in ''|'#'*) continue;; esac
|
||||||
|
n=$((n+1))
|
||||||
|
[ -n "$path" ] || { echo "Fehler: Zeile ohne Pfad: $src" >&2; fail=1; continue; }
|
||||||
|
case "$src" in
|
||||||
|
http://*|https://*) url="$src";;
|
||||||
|
*) [ -n "$api" ] || { echo "Fehler: $src ist keine URL, und --api/WERKBAUM_API fehlt." >&2; fail=1; continue; }
|
||||||
|
url="${api%/}/documents/$src";;
|
||||||
|
esac
|
||||||
|
case "$path" in /*|*..*) echo "Fehler: Pfad muss relativ und ohne .. sein: $path" >&2; fail=1; continue;; esac
|
||||||
|
mkdir -p -- "$tree/$(dirname -- "$path")"
|
||||||
|
"$pulldoc" --git-commit "$url" "$tree/$path" || { echo "Fehler bei $path (siehe oben)." >&2; fail=1; }
|
||||||
|
done < "$list"
|
||||||
|
[ "$n" -gt 0 ] || echo "Hinweis: Liste $list nennt kein Dokument." >&2
|
||||||
|
|
||||||
|
after="$(git -C "$tree" rev-parse HEAD)"
|
||||||
|
if [ "$do_push" = 1 ] && [ "$before" != "$after" ]; then
|
||||||
|
git -C "$tree" push -q || { echo "Fehler: push gescheitert — Commits liegen lokal in $tree." >&2; fail=1; }
|
||||||
|
[ "$fail" = 0 ] && echo "Gepusht: $(git -C "$tree" rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
exit "$fail"
|
||||||
Reference in New Issue
Block a user