Die Datasource-URL enthielt kein DB_CLOSE_DELAY. H2 nimmt dann 0 und schließt die Datei-Datenbank, sobald die letzte Verbindung geht — bei unbenutzter Anwendung zieht der Hikari-Pool genau das zurück. Der laufende Prozess schloss und öffnete seine Datenbank also wiederholt unter sich selbst. Das war der Anlass, und die Klärung kam vor der Behebung. Ergebnis: ein leerer Pool bringt H2 nachweislich dazu, die Datenbank zu schließen — der nächste Schreibvorgang öffnet sie aber einfach wieder und gelingt. Das gilt auch bei 8000 Öffnen-und-Schließen-Durchläufen auf vier Threads, die das Rennen absichtlich suchen. Die führende Erklärung trägt in dieser Form nicht. Zwei Annahmen des Auftrags sind dabei als falsch belegt: H2 2.4.240 vergibt für 90xxx-Fehler den Fehlercode selbst als SQLState, nicht eine 08er-Klasse, und von den genannten Kandidaten erzeugen OBJECT_CLOSED (90007) und IO_EXCEPTION (90028/90031) gar keine JdbcSQLNonTransientConnectionException. Weiter eingegrenzt hat die Ursachenkette: UiErrorConfiguration protokolliert die unterste Ursache, und protokolliert wurde die H2-Klasse selbst. Ein Speichermangel hätte dort java.lang.OutOfMemoryError hinterlassen, ein Schreibfehler der Ablage einen MVStoreException oder IOException. Übrig bleibt ein ursachenloses DATABASE_IS_CLOSED. Das Symptombild zeigt auf eine geschlossene Datenbank, der Weg dorthin bleibt offen — und dass Lesen durchgehend funktionierte, ist damit ebenfalls nicht erklärt. - **`;DB_CLOSE_DELAY=-1` an der Standard-URL, nicht `connection-init-sql`.** `SET DB_CLOSE_DELAY` ist in H2 eine dauerhafte Einstellung der Datenbankdatei: nach einem Lauf meldet auch eine Verbindung ohne die Anweisung -1. Die Einstellung wanderte damit unsichtbar in die Daten und gälte auch für Werkzeuge, die die Datei kurz öffnen. Eine Konfigurationszeile, die sich zurücknehmen lässt, ist dem vorzuziehen. Der Lesezugriff auf die geschützte Umgebungsdatei in Produktion zeigt, dass dort keine Überschreibung der Datasource-URL gesetzt ist; die Standard-URL ist also maßgeblich. - **Die Preflight prüft den Dateipfad statt der Zeichenkette.** Ihre Zusicherung bleibt: der Pfad aus der URL wird — relativ wie absolut — normalisiert und muss genau auf die verwaltete Datei fallen, weil das Deployment das geschlossene Datenverzeichnis sichert. Umwege über `..`, andere Dateinamen, `mem:` und `tcp:` werden weiterhin abgelehnt. Zusätzlich muss eine Überschreibung `DB_CLOSE_DELAY=-1` mitführen, damit die Härtung nicht stillschweigend verlorengeht. - **DB_CLOSE_ON_EXIT bleibt auf der Vorgabe TRUE.** Der Shutdown-Hook schließt die Datenbank beim Prozessende weiterhin, und genau darauf beruht die konsistente Sicherung des gestoppten Dienstes; `-1` verhindert nur das Schließen im Betrieb. Jeder weitere URL-Parameter wird abgelehnt, was insbesondere `DB_CLOSE_ON_EXIT=FALSE` ausschließt. Keine Migration, kein Schemaeingriff. Die fest eingetragene URL in AdminRecovery bleibt unverändert: dieser Offline-Befehl läuft kurzlebig ohne Pool. - `H2ConnectionLifecycleTest`: liest die Parameter der konfigurierten Standard-URL aus application.properties, baut einen Pool in der Form der Produktion und zieht dessen Verbindungen zurück. Ohne Härtung zählt ein DATABASE_EVENT_LISTENER mindestens eine Schließung, mit der konfigurierten URL keine. - `tests/operations/test_update.py`: neuer `ManagedDatabaseUrlTest` für die Pfadprüfung und die abgelehnten Parameter, dazu die gehärtete Überschreibung durch die vollständige Preflight. - `JAVA_HOME=~/opt/jdk25 ./tools/ci-jvm --without-firefox` grün: BUILD SUCCESSFUL in 8m 15s, 72 Betriebstests. Details: `docs/implementation/h2-connection-lifecycle.md`. Die Ursache selbst. Der fertige Commit auf Branch `failure-diagnostics` ergänzt die Logzeile um die Kette der Exception-Klassen samt sqlState und errorCode und wird getrennt ausgeliefert; er ist das Mittel, die Ursache beim nächsten Auftreten zu belegen statt zu raten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Werkjournal
Private Arbeitszeit- und Habit-Erfassung mit Kotlin, Vaadin Flow und Spring Boot.
Stand: E-Mail-Anmeldung aktiv; Ersteinrichtung erfolgreich bestätigt. Private Konten und der getrennte gemeinsame Gastzugang sind verfügbar. Vorhandene Daten müssen bei allen weiteren Migrationen erhalten bleiben.
Planung und Arbeitsweise
werkjournal.werkbaum: Entscheidungen und aktueller Umsetzungsstand.gherkin/: englische Abnahmeszenarien; noch nicht vollständig implementiert.docs/implementation/: geprüfte Infrastruktur, Toolchain und Testzuordnung.docs/plans/: Spezifikationen geplanter Arbeitspakete mit HTML-Entwürfen unterdocs/plans/mockups/; Umsetzung je Paket auf eigenem Branch mit Ergebnisdokument unterdocs/implementation/.
Ein Umsetzungsknoten wird vor Beginn mit [~] committet und gepusht. Nach erfolgreicher Prüfung folgt [x] mit Abschlusscommit und Push. Abgeschlossene Entscheidungen sind ausdrücklich als solche beschriftet und nicht mit ausgelieferter Funktion gleichzusetzen.
Gestaltung
Farben, Schrift und Abstände stammen aus einem gemeinsamen Token-Satz (--wj-*), der in src/main/resources/META-INF/resources/styles.css und src/main/resources/static/login.css identisch definiert ist; alle Lumo-Variablen werden daraus abgeleitet. Hell und dunkel folgen der Darstellungswahl in den Einstellungen. Die Oberfläche ist für das Smartphone entworfen: beschriftete Bottom-Navigation, kompakte Eintragszeilen mit Inline-Stepper und Formulare als Bottom-Sheet unter 600 px. Neue Flächen erben Karten- und Buttonstil aus diesen Tokens, ohne eigene Farbwerte. Maßgeblich sind docs/plans/ui-refresh.md und der Nachweis docs/implementation/ui-refresh.md.
Anwendung bauen
./gradlew test bootJar -Pproduction=true
Der gepinnte Gradle-Wrapper prüft seine Distribution per SHA-256. Build und Tests verwenden Temurin JDK 25; Download und Prüfsumme sind in packaging/jdk.properties fixiert. Das Produktionsartefakt ist build/libs/werkjournal.jar. tools/ci-jvm baut, testet und prüft das JAR mit 256 MiB Heap und 192 MiB Metaspace in einer isolierten Testumgebung.
cp .env.example .env
remote backend install-jdk
# Nach Änderung des gepinnten JDK-Stands:
remote backend install-jdk --force
Die Installation benötigt keine Root-Rechte, verwendet eine SSH-Verbindung und aktiviert erst das vollständig heruntergeladene, geprüfte JDK. Ziel ist standardmäßig ~/opt/jdk25 beim Werkjournal-User. Bestehende JDK-Releases bleiben erhalten, damit laufende JVMs ihre Bibliotheken weiter erreichen. Geheimnisse gehören nicht in die JDK-Konfiguration.
GraalVM ist optional zurückgestellt. Nur -Pnative=true aktiviert dessen Gradle-Plugin; tools/ci-native bleibt ein experimenteller Einstieg ohne CI-Trigger oder Freigabeanspruch. Die bisherigen Ergebnisse stehen in docs/implementation/native-spike.md.
Die Anwendung bindet standardmäßig an 127.0.0.1:18090. /api/v1/info liefert die Buildversion, / das Heute-Dashboard. Dort führen Vortag, Heute und Folgetag auf einen anderen Tag, höchstens bis heute, und daneben öffnet ein Kalendersymbol die Datumsauswahl; /?date=JJJJ-MM-TT steigt direkt dort ein. Details: docs/implementation/home-day-navigation.md.
Ein separates Bookworm-Rootfs für Werkdock lässt sich lokal über tools/build-rootfs vorbereiten. Dazu wird ein lokaler Docker-Dienst verwendet; auf Hostsharing laufen Builds anschließend in Werkdock. Die erzeugten Dateien liegen unter dem git-ignorierten work/.
Mit direnv und direnv allow nimmt .envrc das Verzeichnis tools/ in den PATH auf. Produktionsgeheimnisse bleiben außerhalb des Repositorys.
Konten und Gastzugang
Auf dem Server ist WERKJOURNAL_GUEST_MODE=false gesetzt. Die Loginseite führt vier getrennte Ansichten: „Anmelden“ mit nur einem Adressfeld, „Du bist eingeladen“ beim Öffnen eines Einladungslinks, den Code-Schritt und unter /login?setup die Einrichtung bzw. Wiederherstellung mit dem Einrichtungscode vom Server. Der ausdrückliche Gastzugang steht weiter auf der Standardansicht. Einladungen entstehen unter /admin: Adresse und Sprache eingeben, „Einladung senden“ — die E-Mail geht sofort raus, der Link ist nur auf Wunsch oder nach einem fehlgeschlagenen Versand sichtbar. Die Einladungsliste zeigt Empfänger, Sprache, Zustand und lokal formatierte Zeitpunkte; „Neu einladen“ widerruft die offene Einladung und legt eine neue an. Die Empfängeradresse dient nur der Anzeige und wird beim Verbrauch der Einladung gelöscht; wer den unbenutzten Link besitzt, kann registrieren. Details: docs/implementation/auth-flow-refresh.md. Der bisherige gemeinsame Gastbereich bleibt getrennt von privaten Konten. Die Datenbank liegt unter data/werkjournal.mv.db. Die frühere Erlaubnis, Gastdaten zu verwerfen, gilt nicht mehr. Migrationen benötigen erhaltende Changesets, einen Test mit befülltem Vorgängerstand und eine konsistente Sicherung vor der Migration; siehe docs/implementation/deployment.md.
Das Datenfundament ist unter docs/implementation/foundation.md beschrieben. Die Kunden-/Projektpflege ist unter /catalog verfügbar. Der Quellstand enthält unter /habits frei konfigurierbare Gewohnheiten mit Tageszählern und optionalen Tages-/Wochenminima und -maxima. Grenzen sind Hinweise und blockieren das Zählen nicht. Einmalige, veränderbare Startvorgaben sind Kunde/Projekt „Allgemein“ sowie „Lesen“ und „Spaziergänge“ (Tagesminimum 1, Wochenminimum 5). Details und Testnachweise: docs/implementation/habits.md. Unter /work lassen sich Arbeitszeiten erfassen und ändern; Konflikte erhalten den Entwurf und erfordern eine ausdrückliche Auflösung. Entwürfe bleiben pro Tab erhalten und lassen sich auf demselben Gerät wiederaufnehmen. Details: docs/implementation/work.md. Die Arbeitserinnerung kommt Montag bis Freitag zur Erinnerungszeit des Kontos, einstellbar in den Einstellungen in Viertelstundenschritten zwischen 06:00 und 23:00; Vorgabe ist 20:00. Ein Ausfall wird noch zwei Stunden lang nachgeholt, höchstens bis Mitternacht, damit eine Erinnerung ihr Datum behält. Der Ein- und Ausschalter für Push bleibt pro Gerät. Details: docs/implementation/reminder-time.md. Dieser Funktionsstand ist öffentlich verfügbar.
Erstdeployment
remote backend deploy-prototype --artifact /pfad/zum/geprueften-ci-release
Das Verzeichnis enthält werkjournal.jar und das dazugehörige manifest.json aus einem erfolgreichen Werkator-Build. Der Befehl prüft den Hash lokal und auf dem Zielserver und baut nicht neu. Er richtet den systemd-User-Service sowie HTTP-Weiterleitung und HTTPS-Proxy auf dem Werkjournal-Host ein. Bereits vorhandene Releases, Dienste oder H2-Dateien werden abgelehnt: für reguläre Upgrades mit Datensicherung und Rollback dient remote backend deploy.
Betriebsstand, Verzeichnisse und Prüfergebnisse: docs/implementation/first-deploy.md.
Der laufende Dienst lässt sich mit remote backend status prüfen und mit
remote backend start|stop|restart steuern. remote backend enable|disable
ändert nur den Autostart. remote backend log --lines 30 zeigt Journalzeilen,
remote backend log --follow folgt neuen Meldungen. Mutierende Befehle verwenden
die Deployment-Sperre; vorhandene Wartung bleibt bestehen. Details und Fehlerfälle:
docs/implementation/service-control.md.
remote backend setup schreibt die versionierte Unit und lädt systemd neu,
ohne den Dienst neu zu starten. remote backend info prüft Version/Commit
sowie interne und öffentliche Bereitschaft. remote ssh [COMMAND ARG...]
öffnet eine Shell oder führt einen Befehl mit getrennten Argumenten aus.
Details und vollständiger Befehlsabgleich: docs/implementation/remote-utilities.md.
Die angebundenen Fach- und Chromium-Gherkin-Szenarien laufen im normalen Testlauf mit. Die vollständige Pipeline ergänzt die Firefox-Abnahme und den Produktions-JAR-Smoke. Lokal zuerst die gepinnten Browser installieren:
JAVA_HOME=/pfad/zum/jdk25 ./gradlew --no-daemon installBrowsers
Der Werkdock-Build nutzt die Browser aus seinem vorbereiteten Image. Fehlende Browser werden im Testlauf nicht automatisch heruntergeladen oder übersprungen.
| Bereich | Suite | Details |
|---|---|---|
| Arbeitszeit | WorkCucumberTest |
docs/implementation/work-gherkin.md |
| Habits | HabitCucumberTest |
docs/implementation/habit-gherkin.md |
| Push | PushCucumberTest |
docs/implementation/push-gherkin.md |
| Konten | IdentityCucumberTest |
docs/implementation/identity-gherkin.md |
| Chromium | BrowserCucumberTest |
docs/implementation/browser-gherkin.md |
| Firefox | firefoxAcceptance |
docs/implementation/firefox-acceptance.md |
| Produktions-JAR | productionAcceptance |
docs/implementation/production-smoke-gherkin.md |
Aufruf beispielsweise: ./gradlew test --tests '*BrowserCucumberTest' -Pproduction=true mit gesetztem JAVA_HOME. Berichte stehen unter build/reports/cucumber; noch offene Bindungen in docs/implementation/acceptance.md.
Erste Gastvorschau
remote backend deploy-guest-preview --artifact DIRECTORY ermöglicht ausschließlich den einmaligen Wechsel vom bekannten datenlosen Prototyp. Der Übergang wurde ausgeführt. Bei vorhandener Datenbank verweigert der Befehl weitere Deployments; allgemeine sichere Upgrades erfolgen über remote backend deploy. Details: docs/implementation/guest-preview.md.
Die zentrale Betriebsanleitung beschreibt Konfiguration, Releases, Wiederherstellung, Admin-Recovery und den geplanten Trixie-Wechsel.
Updates und Wiederherstellung
remote backend deploy --artifact /pfad/zum/geprueften-ci-release
remote backend recover
Mit remote backend upload --artifact DIRECTORY lässt sich ein geprüftes Release
vorab bereitstellen. Es bleibt inaktiv, bis remote backend deploy --artifact DIRECTORY
es mit Sicherung und Bereitschaftsprüfung aktiviert. Ein gewöhnlicher Neustart
aktiviert den Upload nicht. Details: docs/implementation/candidate-upload.md.
remote backend backup [FILE] erstellt eine konsistente private Sicherung von
H2-Daten und passendem Programmstand. Ohne Dateinamen landet sie in backups/.
Der Dienst wird dafür kurz unter Wartung gestoppt und anschließend nur dann
wieder gestartet, wenn er vorher lief. Vorhandene Dateien werden nicht
überschrieben. Ablauf und Fehlerbehandlung: docs/implementation/operator-backup.md.
remote backend restore --backup FILE spielt diese Sicherung erst nach expliziter
Bestätigung ein und sichert zuvor den bisherigen Stand. remote backend restore --recover
behandelt einen unterbrochenen Versuch. Vorab lässt sich die Sicherung mit
JAVA_HOME=/path/to/jdk25 tools/probe-restore --backup FILE isoliert prüfen.
Details: docs/implementation/operator-restore.md.
Updates verwenden Wartungsseite, Dienststopp, konsistente H2-Dateisicherung und einen dauerhaften Wiederherstellungszustand. Überholte Kandidaten werden abgewiesen. Ein fehlgeschlagener Versuch bleibt in der Pipeline fehlgeschlagen, auch wenn die bisherige Version erfolgreich wiederhergestellt wurde. Einzelheiten: docs/implementation/deployment.md. Werkator führt dieses Deployment nach erfolgreichem Build im selben Pipeline-Skript aus.
Push-Schlüssel vorbereiten
Mit remote backend setup-push werden die VAPID-Schlüssel direkt auf dem Server in der geschützten systemd-Umgebungsdatei erzeugt. Wiederholter Aufruf erhält die vorhandenen Schlüssel. Der Befehl startet keinen Dienst neu und aktiviert keinen Versand. Details: docs/implementation/web-push-transport.md.