feat(tools): mirror-docs — Liste geteilter Dokumente per Timer nach git spiegeln (D88-Nachtrag 3)
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-04 09:52:02 +02:00
co-authored by Claude Fable 5.1
parent c87cf6212d
commit 2e408d956c
6 changed files with 154 additions and 0 deletions
+7
View File
@@ -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] <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
`docs/DECISIONS.md` D76.
+6
View File
@@ -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] <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
`docs/DECISIONS.md` D76.
+4
View File
@@ -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
+57
View File
@@ -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
+8
View File
@@ -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.
+72
View File
@@ -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"