feat(tools): mirror-docs — Liste geteilter Dokumente per Timer nach git spiegeln (D88-Nachtrag 3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c87cf6212d
commit
2e408d956c
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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