Commit Graph
11 Commits
Author SHA1 Message Date
mhoennig ac6df1bb6b feat(tools): remote <ziel> <aktion> als Vordertuer zum Server (D77-Nachtrag)
Deployen, Log ansehen, Dienst schalten und fragen was laeuft waren vier
verschiedene Beschwoerungen, drei davon von Hand als ssh + systemctl. Jetzt:

  remote backend deploy|upload|setup|install-jdk|reset-password
  remote backend start|stop|restart|status|enable|disable
  remote backend log|info|documents|backup
  remote frontend deploy|preview|info
  remote ssh

Die Skripte unter scripts/ bleiben die Implementierung und einzeln aufrufbar;
tools/remote bringt nur mit, wofuer es bisher nichts gab. Wo ein Schalter
noetig war, kam er ins Skript statt ins Werkzeug: --unit-only in
deploy-backend.sh (sonst kennte eine zweite Stelle die Unit-Platzhalter) und
--dry-run in deploy-prod.sh, das die Befoerderung ausdruecklich mit abschaltet.

Neu ist die Sicherung: H2 haelt die Datei offen, also anhalten, holen, wieder
starten (gemessen ~8 s Auszeit) - und das Archiv lesen, bevor der Befehl es
behaelt. Gegenprobe von Hand: lokal ausgepackt, Backend mit --werkbaum.data-dir
dagegen gestartet, es liefert genau die Dokumente des Servers.

.envrc legt tools/ auf den PATH (direnv), 217 Plan-Knoten, 0 Warnungen.
2026-08-26 19:59:24 +02:00
mhoennigandClaude Opus 5 dea904814b fix(backend): Neustart gegen die eigene Datenbank, /info als Lebendprobe
Der Dienst lief einmal und stuerzte danach in einer Schleife: MODE=PostgreSQL
laesst H2 unquotierte Bezeichner klein anlegen, Liquibase sucht seine
Verwaltungstabellen gross, findet nichts, legt sie an — "Table
databasechangelog already exists". Der erste Start ging, jeder weitere nicht.
Gemessen mit dem echten Jar: ohne den Modus laufen beide. Liquibase auf
Kleinschreibung zu konfigurieren half nicht, es korrigiert den Namen selbst
zurueck — deshalb weicht der Modus ganz.

Die Testsuite konnte das nicht finden (jeder Test bekommt eine frische
In-Memory-DB), und der Regressionstest dafuer hat zweimal gelogen: erst reichte
er die URL als Default-Property herein, die die application.yaml ueberstimmt;
dann als Argument, aber damit pruefte er eine URL, die er sich selbst
ausgedacht hatte. Jetzt hat die URL einen Regler (werkbaum.data-dir), der Test
ueberschreibt nur den, und die Gegenprobe faellt.

Dieselbe Sorte Fehler eine Ebene hoeher: Die Testkonfiguration hiess
application.yaml und verdeckte damit die Hauptkonfiguration vollstaendig. Sie
ist jetzt eine Profil-Ueberlagerung.

Dazu GET /api/v1/info mit Name, Version und Bauzeitpunkt. Die Lebendprobe
erwartete bisher eine 404 von einem Dokument, das es nicht gibt — ein
erwarteter Fehler ist eine schlechte Zusicherung, dieselbe 404 liefert auch ein
falsch konfigurierter Proxy.

138 Backend-Tests. Gegenprobe: MODE=PostgreSQL zurueck -> genau der
Neustart-Test faellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:02:51 +02:00
mhoennigandClaude Opus 5 78901793f0 fix(docs): Master-Passwort interaktiv hashen, nicht auf der Kommandozeile
Meine Anleitung schrieb `htpasswd -bnBC 12 "" PASSWORT` — und lieferte 401,
obwohl Hash und Konfiguration nachweislich stimmten. Die Ursache liegt vor dem
Hashen: Das Passwort steht dort ungeschuetzt in einer Kommandozeile, und die
Shell fasst es an. `ge$heim` wird zu `ge`, `ge heim` zu `geheim`; gehasht wird
etwas anderes als das, was man spaeter eintippt.

Richtig ist `htpasswd -nBC 12 ''` ohne -b: Es fragt zweimal nach, das Passwort
geht nie durch eine Shell und landet nicht in der History. Dazu eine direkte
Probe (htpasswd -v gegen den gespeicherten Hash), weil der Fehler wie ein
Konfigurationsfehler aussieht — alles Pruefbare stimmt, nur der Vergleich
schlaegt fehl.

Vier Stellen: deploy-backend.sh, beide READMEs, MasterPasswordProperties.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:37:46 +02:00
mhoennigandClaude Opus 5 4aff995a0e feat(deploy): Backend auf die stabile Instanz bringen (D77)
Drei Teile, die das Frontend-Deploy nicht braucht: ein eigenes JDK 21 im Home
des Servers (dort ist nur 17 installiert, und die Toolchain zu senken hiesse,
Entwicklung und Produktion auseinanderlaufen zu lassen), ein systemd-User-Unit
statt nohup, und die Proxy-Regel in der .htaccess — gemessen ist, dass das
P-Flag auf diesem Hoster erlaubt ist und eine Verbindung 30 s durchhaelt.

Der Port steht an genau einer Stelle: deploy-prod.sh setzt ihn in die
Proxy-Regel, deploy-backend.sh in die Unit. Zwei Zahlen, die zueinander passen
muessen, sind eine zu viel.

Die JVM-Flags sind gemessen, nicht geschaetzt. Mein erster Entwurf setzte
-Xmx384m; nachgemessen kam heraus, dass die Obergrenze der kleine Hebel ist:
Ohne Freiraum-Verhaeltnisse behaelt der Kollektor den gewachsenen Heap, obwohl
nach einem GC nur ~45 MB leben. Mit ihnen 174 MB RSS statt 291 MB ohne jede
Angabe — auf einem Host mit rund 300 MB frei ist das der Unterschied zwischen
"passt" und "draengt die Datenbank weiter in den Swap".

Zwei Fallen sind eingebaut, weil beide nur am Ziel auffielen: systemd
expandiert kein $HOME (deshalb %h), und `systemctl --user` findet ohne
XDG_RUNTIME_DIR seinen Manager nicht.

Geprueft bis an die SSH-Grenze: Das Jar startet mit genau den Flags der Unit
in einer Sekunde, antwortet auf die Lebendprobe mit 404 und ist von aussen
nicht erreichbar; die erzeugte Unit besteht systemd-analyze verify; der ganze
Ablauf lief mit gestelltem ssh/rsync durch; deploy-prod.sh liefert die
Proxy-Regel mit eingesetztem Port aus. Der Deploy selbst laeuft erst, wenn
jemand ihn startet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:11:56 +02:00
mhoennigandClaude Opus 5 bc8850a518 feat(backend): Master-Passwort fuer die Dokumentenliste (Schritt 5)
GET /documents listet alle Dokumente und machte damit jede UUID auffindbar —
das Zugriffsmodell "unerratbarer Link" waere hinfaellig. Der Endpunkt
verlangt jetzt HTTP Basic; alles andere bleibt bewusst offen.

Ohne konfigurierten Hash ist die Liste ausdruecklich gesperrt (denyAll),
nicht offen: Ein vergessener Umgebungswert gaebe sonst jede UUID preis, und
niemandem fiele es auf, weil alles funktioniert. Bewusst als Regel und nicht
bloss als zufaelliges Passwort, das niemand kennt — der Unterschied ist
pruefbar: Mit dem Zufallspasswort blieb die Gegenprobe stumm, mit denyAll
faellt genau die danach benannte Zusicherung.

Die Sperre nach Fehlversuchen ist global statt je Adresse: Es gibt genau ein
Passwort, und hinter dem Reverse Proxy der Zielumgebung saehe der Server
ohnehin fuer alle dieselbe Adresse. Preis benannt (D76-Nachtrag 6).

135 Tests. Gegenproben: Schutz entfernt, Sperre entfernt, Voreinstellung
geoeffnet -> es faellt jeweils genau die danach benannte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:30:10 +02:00
mhoennigandClaude Opus 5 741b41ef11 feat(backend): Aenderungsfeed per Long Polling (Schritt 4)
GET /documents/{id}/changes haelt die Anfrage offen und antwortet, sobald
sich etwas tut — kumuliertes Diff seit der bekannten Version, dazu die
Ereignisse mit ihrem Absender. Ist die Basis verdichtet oder hat der Client
noch gar nichts, kommt der Volltext statt der Operationen: ein Roundtrip und
ein Sonderzustand weniger als ein eigener Fehlerpfad.

Der Feed arbeitet auf der Historie, nicht am Dokument — ein geloeschtes
Dokument muss sein DELETED noch zustellen koennen.

Blockierend auf virtuellen Threads statt DeferredResult (D76-Nachtrag 5):
So behaelt der Endpunkt die aus der Spezifikation generierte Signatur, und
API-First bleibt fuer ihn unangetastet; ein Wartender kostet trotzdem fast
nichts. Geweckt wird nach dem Commit, nie davor, und ueber einen Stempel, den
der Aufrufer VOR dem Nachsehen liest — sonst ginge ein Signal aus der Luecke
dazwischen verloren.

122 Tests. Das Szenario "ein Wartender wird geweckt" misst die Dauer: Ohne
das bestuende es auch dann, wenn der Wartende bloss in den Timeout liefe und
danach die Aenderung vorfaende. Gegenprobe: Benachrichtigung entfernt ->
genau dieses Szenario faellt; Volltext-Rueckfall entfernt -> genau jenes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:16:13 +02:00
mhoennigandClaude Opus 5 ae85503b79 feat(backend): PATCH /content — Diffs einreichen, rebasen, wiederholen (Schritt 3)
Der Server rebased selbst: Ist die Basis veraltet, ueberschneiden sich die
Operationen aber nicht mit den zwischenzeitlichen, verschiebt er sie und
akzeptiert. Reines Ablehnen fuehrte zu Starvation — ein Client mit hoher
Latenz kaeme bei fleissigen Mitschreibern womoeglich nie durch. 409 gibt es
nur bei echter Ueberschneidung, mit allem, was der Client zum Weiterarbeiten
braucht, ohne neu zu laden.

Pruefsumme ist Pflicht (422 bei Abweichung): Die Versionsnummer bestaetigt
nur, dass die Basis dieselbe Version ist, nicht dass beide Seiten sie gleich
lesen. clientId + seq machen den Aufruf wiederholbar — im Mobilnetz ist die
verlorene Antwort der Normalfall.

Die Sperre je Dokument liegt ausserhalb der Transaktion: innen gaebe der
Proxy sie vor dem Commit frei, und der naechste Schreiber laese einen Stand,
der noch nicht steht. Deshalb ist LiveEditingService nicht transaktional und
schreibt ueber DocumentService.

Was das Konzept offenliess, ist jetzt entschieden und in D76-Nachtrag 4
begruendet: die Randfaelle der Einfuege-Ueberschneidung, die Trennung von
400 und 422, die gedeckelte Idempotenz im Speicher.

104 Tests, davon 8 Cucumber-Szenarien fuer das Live-Editing. Gegenprobe:
Pruefsumme nicht geprueft, Idempotenz entfernt, veraltete Basis abgelehnt
statt verschoben -> es fallen jeweils genau die danach benannten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:05:14 +02:00
mhoennigandClaude Opus 5 d5cdff6058 feat(backend): Historie in zwei Ebenen, gezielter Repository-Zugriff (Schritt 2)
Meilensteine sind die nutzersichtbare Historie und bleiben; Sync-Versionen
tragen die Diffs des Live-Editings und werden nach der Aufbewahrungsfrist
verdichtet (D76). Ohne die Trennung wuerde die Historie beim getakteten
Schreiben zum Transaktionslog.

Die Schreibpause braucht keinen Zeitgeber: Die naechste Aenderung stellt
fest, dass eine Pause war, und befoerdert die Version davor nachtraeglich.
Strukturelle Aenderungen sind immer Meilensteine.

Das Historie-Repository greift jetzt gezielt zu (eine Version, juengster,
aeltester, Meilensteine, maxVersion) statt stets alle Eintraege zu laden und
in Kotlin zu filtern — bei hunderten Volltext-Versionen je Dokument war das
untragbar. Restore liest den letzten Stand aus dem Tombstone: der ueberlebt
das Verdichten, die Version davor womoeglich nicht.

Dabei die D76-Unschaerfe aufgeloest: RESTORED heisst nur noch "ein
geloeschtes Dokument ist wieder da" (der Client hebt seine Sperre auf), der
Rueckfall eines lebenden Dokuments ist ROLLED_BACK.

81 Tests. Gegenprobe: Schreibpause ignoriert -> genau die danach benannte
Zusicherung faellt; Rueckfall wieder als RESTORED -> Unit- und
Cucumber-Test dazu; juengster Stand aus der Historie genommen -> genau einer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:51:46 +02:00
mhoennigandClaude Opus 5 f3afc8dfa8 refactor(backend): Paketwurzel de.werkbaum, Tests auf RestTestClient
Das Backend-Geruest kam unter der Platzhalter-Wurzel com.example.editor
herein und widersprach backend/CLAUDE.md. Umgezogen nach de.werkbaum: 17
Kotlin-Dateien, Gradle-group, apiPackage/modelPackage der OpenAPI-Generierung,
die jacoco-Ausschluesse und das Cucumber-glue-Paket. Jetzt war es billig, mit
jeder Woche Entwicklung waere es teurer geworden.

Die BDD-Tests nutzen jetzt RestTestClient statt TestRestTemplate, das in
Spring Boot 4 als Auslaufmodell gilt. Weil Cucumber Senden und Pruefen
trennt, wird die fluent API nicht fuer Zusicherungen genutzt, sondern ueber
returnResult das Ergebnis festgehalten.

Nachgemessen: 22 Tests gruen, check inklusive Coverage-Verifikation besteht,
91,7 % Zeilenabdeckung, generierter Code weiterhin ausgeschlossen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:21:34 +02:00
mhoennig 0446e3d81e initial backend: Dokumente, Historie, API und Persistenz (D76)
- Liquibase-Schema (`document`, `document_history`) + Rollback-scripts
- Spring Boot with JPA-repositories, entities and services
- REST-API (`/documents`, `/documents/{id}`, `/documents/{id}/history`)
- OpenAPI-specifikation for CRUD-Operationen and history
- config files (`application.yaml`, `Liquibase`, H2 im PostgreSQL-Modus)
- preps for future live-editing/delta-updates
- Exceptions for conflikt- and not-found cases (409/404)
- keeping document hostory even after `delete` for RESTORE functionality
2026-08-26 12:56:27 +02:00
Michael Hoennig ded894a4da initial, vibe-coded version, mostly frontend 2026-07-21 09:55:30 +02:00