From 2e408d956ca6deaa00822e7bc9de2e0e9a5196ff Mon Sep 17 00:00:00 2001 From: mhoennig Date: Fri, 4 Sep 2026 09:52:02 +0200 Subject: [PATCH] =?UTF-8?q?feat(tools):=20mirror-docs=20=E2=80=94=20Liste?= =?UTF-8?q?=20geteilter=20Dokumente=20per=20Timer=20nach=20git=20spiegeln?= =?UTF-8?q?=20(D88-Nachtrag=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- README.de.md | 7 ++++ README.md | 6 +++ docs/CHANGELOG.md | 4 ++ docs/DECISIONS.md | 57 ++++++++++++++++++++++++++ docs/examples/werkbaum.werkbaum | 8 ++++ tools/mirror-docs | 72 +++++++++++++++++++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100755 tools/mirror-docs diff --git a/README.de.md b/README.de.md index 5bd69f4..833ec05 100644 --- a/README.de.md +++ b/README.de.md @@ -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` öffnet die Datei danach in IntelliJ IDEA. Siehe `docs/DECISIONS.md` D88. +**Mehrere Dokumente, ohne Zutun:** `tools/mirror-docs [--push] +` schickt jede Zeile der Liste (` `) 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 `docs/DECISIONS.md` D76. diff --git a/README.md b/README.md index b892bef..b6a9c8b 100644 --- a/README.md +++ b/README.md @@ -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 IDEA afterwards. See `docs/DECISIONS.md` D88. +**Several documents, unattended:** `tools/mirror-docs [--push] +` runs every line of the list (` `) 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 `docs/DECISIONS.md` D76. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bc2e0fd..4a215e1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -17,6 +17,10 @@ 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 reverse. +## 2026-09-04 + +- 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 - 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 diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 3021bd5..3b93d2f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -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 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 Der Vorfall (2026-08-27 vormittags, gemeldet vom Nutzer): Zwei Stunden Arbeit an einem geteilten Dokument (`?live=`, PWA) waren weg — die Server-Historie diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index 2b2e661..6e9ce42 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -151,6 +151,7 @@ - [~] #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.pull: A script pulls the server document and commits it (S) %% tools/pull-doc --git-commit, dated commits — see D88 + - [x] #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.hist: History and restore (M) - [?] #col.git.diff: Diff between two versions (S) @@ -970,6 +971,13 @@ worktree — date, title and version in the message, no commit when nothing 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 The server turns every change into a commit. History, restore and branches follow from that, with commit granularity as the open question. diff --git a/tools/mirror-docs b/tools/mirror-docs new file mode 100755 index 0000000..d7d9b8c --- /dev/null +++ b/tools/mirror-docs @@ -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 ] +# +# --push Nach dem Durchlauf `git push`, wenn etwas committet wurde. +# Der Branch braucht dafür einen Upstream. +# --api Basis-Adresse des Backends (…/api/v1) für Zeilen, die nur +# eine UUID nennen; sonst aus WERKBAUM_API. +# Wurzel des git-Worktrees, in das gespiegelt wird. +# Textdatei, je Zeile ` `; 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 ] 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"