Compare commits
6 Commits
1db49970d1
...
0ca10c7820
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ca10c7820 | |||
| 38875a1040 | |||
| f7743aa7d9 | |||
| 2c822f8007 | |||
| d1fce33716 | |||
| b6fda98c89 |
|
|
@ -21,12 +21,10 @@ BACKUP_RETENTION_DAYS=7
|
||||||
DOCKER_REGISTRY=git.mo-code.at/mocode-software/meldestelle
|
DOCKER_REGISTRY=git.mo-code.at/mocode-software/meldestelle
|
||||||
DOCKER_TAG=latest
|
DOCKER_TAG=latest
|
||||||
DOCKER_VERSION=1.0.0-SNAPSHOT
|
DOCKER_VERSION=1.0.0-SNAPSHOT
|
||||||
DOCKER_BUILD_DATE=2026-02-02T15:00:00Z
|
DOCKER_BUILD_DATE=2026-03-15T12:00:00Z
|
||||||
DOCKER_GRADLE_VERSION=9.3.1
|
DOCKER_GRADLE_VERSION=9.3.1
|
||||||
# Java 25 = Early Access; für LTS auf 21 setzen
|
|
||||||
DOCKER_JAVA_VERSION=25
|
DOCKER_JAVA_VERSION=25
|
||||||
DOCKER_NODE_VERSION=24.12.0
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
# Caddy Version für den Web-App Container
|
|
||||||
DOCKER_CADDY_VERSION=2.11-alpine
|
DOCKER_CADDY_VERSION=2.11-alpine
|
||||||
# ARM64 spezifische JVM-Optionen (leer lassen auf x86/amd64, z.B. auf Apple Silicon)
|
# ARM64 spezifische JVM-Optionen (leer lassen auf x86/amd64, z.B. auf Apple Silicon)
|
||||||
# Beispiel ARM64: JVM_OPTS_ARM64=-XX:UseSVE=0
|
# Beispiel ARM64: JVM_OPTS_ARM64=-XX:UseSVE=0
|
||||||
|
|
@ -43,7 +41,7 @@ POSTGRES_DB_URL=jdbc:postgresql://postgres:5432/pg-meldestelle-db
|
||||||
POSTGRES_SHARED_BUFFERS=256MB
|
POSTGRES_SHARED_BUFFERS=256MB
|
||||||
POSTGRES_EFFECTIVE_CACHE_SIZE=768MB
|
POSTGRES_EFFECTIVE_CACHE_SIZE=768MB
|
||||||
|
|
||||||
# --- VALKEY (formerly Redis) ---
|
# --- VALKEY ---
|
||||||
VALKEY_IMAGE=valkey/valkey:9-alpine
|
VALKEY_IMAGE=valkey/valkey:9-alpine
|
||||||
VALKEY_PASSWORD=<SICHERES_PASSWORT>
|
VALKEY_PASSWORD=<SICHERES_PASSWORT>
|
||||||
VALKEY_PORT=6379:6379
|
VALKEY_PORT=6379:6379
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Session Log: Architektonische Gesamtbewertung & Freigabe Phase 3
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🏗️ [Lead Architect] in Zusammenarbeit mit 🧹 [Curator]
|
||||||
|
**Typ:** Architektur-Entscheidung & Meilenstein-Review
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
Als Lead Architect habe ich die tiefgehenden Analysen aller Fachbereiche (DevOps, Backend, Frontend, UI/UX, QA, Curator)
|
||||||
|
evaluiert, um die Bereitschaft für die "Phase 3: Feature Development" der Master Roadmap zu bewerten.
|
||||||
|
|
||||||
|
**Das Ergebnis ist eindeutig:**
|
||||||
|
|
||||||
|
* 🟢 **Technologie & Infrastruktur:** 100% Ready. Das Fundament ist robust, modern und produktionsnah.
|
||||||
|
* 🔴 **Fachlichkeit & Domäne:** 0% Ready. Es existieren keine verabschiedeten Spezifikationen, Use-Cases oder UI-Flows.
|
||||||
|
|
||||||
|
## 2. Zusammenfassung der Fachberichte
|
||||||
|
|
||||||
|
1. **🐧 DevOps:** Infrastruktur, CI/CD und lokale Entwicklungsumgebung stehen stabil bereit. Die Dev/Prod-Parität (
|
||||||
|
Keycloak) ist gesichert.
|
||||||
|
2. **👷 Backend:** DDD-Core und Spring Boot Blueprints (KMP-kompatibel) sind lauffähig, aber fachliche Implementierung
|
||||||
|
würde ohne Domänenmodelle zu massiven Refactorings führen.
|
||||||
|
3. **🎨 Frontend:** KMP, Compose und Offline-First-Sync-Mechanismen sind exzellent aufgesetzt. Es fehlen jedoch
|
||||||
|
Datenmodelle und Wireframes.
|
||||||
|
4. **🖌️ UI/UX:** Das Basis-Design-System steht. Es fehlen jedoch zwingend Wireframes, Benutzer-Flows und Offline-First
|
||||||
|
UX-Konzepte, um das alte "SuDo"-System sinnvoll abzulösen.
|
||||||
|
5. **🧐 QA:** Test-Infrastruktur (Testcontainers, BDD-Bereitschaft) ist hervorragend. Es fehlen Akzeptanzkriterien,
|
||||||
|
Testdaten und "Given-When-Then"-Szenarien für TDD.
|
||||||
|
6. **🧹 Curator:** Docs-as-Code Struktur ist sauber. Jedoch sind alle Domänen-Dokumente im `docs/03_Domain/` als "Draft"
|
||||||
|
markiert, was die "Single Source of Truth"-Regel verletzt.
|
||||||
|
|
||||||
|
## 3. Architektonische Entscheidung (ADR)
|
||||||
|
|
||||||
|
Ich bestätige das einstimmige Veto des Teams gegen einen sofortigen Start der Programmierung. Das blinde Schreiben von
|
||||||
|
Feature-Code wird hiermit untersagt.
|
||||||
|
|
||||||
|
**Wir wenden strikt das "Shift-Left" Prinzip an.** Bevor Code geschrieben wird, muss die fachliche Architektur (Domäne)
|
||||||
|
geklärt sein.
|
||||||
|
|
||||||
|
## 4. Konkreter Fahrplan (Nächste Schritte)
|
||||||
|
|
||||||
|
Um die Blockade aufzulösen und Phase 3 aktiv zu schalten, ordne ich folgende Schritte an:
|
||||||
|
|
||||||
|
1. **Initiierung des Domain Workshops (Lead Architect & Fachexperte):**
|
||||||
|
* Fokus auf Kernprozesse (z.B. Nennung, Startliste, Ergebnisse).
|
||||||
|
* Überführung der Drafts (`Legacy_Spec_Analysis`, `Use_Cases_Draft.md`) in finale Spezifikationen.
|
||||||
|
2. **Parallele Ausarbeitung (QA & UI/UX):**
|
||||||
|
* **QA:** Definition der Akzeptanzkriterien und Gherkin-Szenarien aus den Workshop-Ergebnissen.
|
||||||
|
* **UI/UX:** Erstellung der Wireframes für die Kern-Flows (inkl. Offline-States) basierend auf den neuen
|
||||||
|
Domänenmodellen.
|
||||||
|
3. **Dokumenten-Freigabe (Curator):**
|
||||||
|
* Überführung der `docs/03_Domain/` Dokumente vom Status "DRAFT" auf "ACTIVE".
|
||||||
|
4. **Roadmap Update:**
|
||||||
|
* Sobald Schritte 1-3 abgeschlossen sind, wird Phase 3 in der `MASTER_ROADMAP.md` freigegeben und das
|
||||||
|
Entwicklungs-Team darf mit der Implementierung beginnen (Aktivierung des `entries-service`).
|
||||||
|
|
||||||
|
**Fazit:** Der technische Proof of Concept ist abgeschlossen. Wir wechseln nun in den Domänen- und Spezifikations-Modus.
|
||||||
48
docs/99_Journal/2026-03-16_Session_Log_Backend_Readiness.md
Normal file
48
docs/99_Journal/2026-03-16_Session_Log_Backend_Readiness.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Session Log: Analyse der Backend-Bereitschaft für die Feature-Entwicklung
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🧹 [Curator] in Zusammenarbeit mit 👷 [Backend Developer]
|
||||||
|
**Typ:** Code- & Projekt-Review
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
Nachdem die Infrastruktur freigegeben wurde, hat der 👷 **[Backend Developer]** die aktuelle Code-Basis (`:backend`,
|
||||||
|
`:core:core-domain`, `settings.gradle.kts`) auf ihre Bereitschaft für den Start der fachlichen Feature-Entwicklung
|
||||||
|
geprüft.
|
||||||
|
|
||||||
|
## 2. Analyse-Ergebnisse (Backend Developer)
|
||||||
|
|
||||||
|
Die Analyse gliedert sich in zwei Kernbereiche: die technische Basis und die fachliche Ausrichtung.
|
||||||
|
|
||||||
|
### 2.1 Technische Bereitschaft: 🟢 Exzellent
|
||||||
|
|
||||||
|
Das technische Fundament ist hervorragend aufgestellt und bereit für die Produktiventwicklung:
|
||||||
|
|
||||||
|
* **Domain-Driven Design (DDD) Core:** Das `:core:core-domain` Modul ist sehr sauber strukturiert. Die konsequente
|
||||||
|
Nutzung von Kotlin 2.3.0 `Value-Classes` (z.B. für `EntityId`, `ErrorCode` inkl. Regex-Validierung) garantiert
|
||||||
|
maximale Typsicherheit ohne Performance-Verlust zur Laufzeit.
|
||||||
|
* **KMP-Kompatibilität:** Die Basis-DTOs und Serializer (wie `KotlinxInstantSerializer`) sind im `commonMain` des
|
||||||
|
Core-Moduls platziert. Dies ermöglicht eine reibungslose Code-Wiederverwendung für das Kotlin/JS Frontend (KMP-Ready).
|
||||||
|
* **Service-Blaupause:** Der `ping-service` beweist, dass der aktuelle Technologie-Stack (Spring Boot 3.5.x, Security,
|
||||||
|
Resilience4j, Persistence, Monitoring via Zipkin/Micrometer) lauffähig verdrahtet ist. Auch die Testumgebung inkl.
|
||||||
|
`Testcontainers` steht bereit.
|
||||||
|
|
||||||
|
### 2.2 Fachliche Bereitschaft: 🔴 Blockiert (Warten auf Architect)
|
||||||
|
|
||||||
|
Trotz der technischen Einsatzbereitschaft darf die fachliche Implementierung noch nicht starten.
|
||||||
|
|
||||||
|
* **Roadmap-Status:** Die `MASTER_ROADMAP.md` definiert Phase 3 (Feature Development) aktuell ganz klar als **ON HOLD**.
|
||||||
|
* **Fehlende Domänen-Klarheit:** Es steht noch ein fundamentaler "Domain Analysis Workshop" mit dem Fachexperten aus, um
|
||||||
|
die Anforderungen, Aggregate und Bounded Contexts neu zu definieren.
|
||||||
|
* **Pausierte Services:** Der für die Fachlichkeit zentrale `:backend:services:entries:entries-service` ist in der
|
||||||
|
`settings.gradle.kts` bewusst auskommentiert und deaktiviert.
|
||||||
|
|
||||||
|
## 3. Fazit & Empfehlung
|
||||||
|
|
||||||
|
Ein unüberlegter Start der Programmierung würde unweigerlich zu Code führen, der nicht der finalen Domänen-Logik
|
||||||
|
entspricht und später aufwendig refaktorisiert werden müsste.
|
||||||
|
|
||||||
|
**Nächster logischer Schritt:**
|
||||||
|
Bevor Code geschrieben oder der `entries-service` reaktiviert wird, muss der **🏗️ [Lead Architect]** den Domain-Workshop
|
||||||
|
leiten. Erst wenn die fachliche "Feature Roadmap" und die Kern-Domänenmodelle spezifiziert sind, erfolgt die Freigabe
|
||||||
|
für das Backend-Team.
|
||||||
52
docs/99_Journal/2026-03-16_Session_Log_Curator_Readiness.md
Normal file
52
docs/99_Journal/2026-03-16_Session_Log_Curator_Readiness.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Session Log: Analyse der Dokumentations-Bereitschaft für die Feature-Entwicklung
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🧹 [Curator]
|
||||||
|
**Typ:** Projektdokumentations-Review
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
Als finaler Check vor dem möglichen Start der Phase 3 ("Feature Development") habe ich, der 🧹 **[Curator]**, das Projekt
|
||||||
|
aus Sicht der Dokumentation, Wissensverwaltung ("Docs-as-Code") und Nachvollziehbarkeit analysiert.
|
||||||
|
|
||||||
|
## 2. Analyse-Ergebnisse (Curator)
|
||||||
|
|
||||||
|
### 2.1 Technische Dokumentations-Infrastruktur: 🟢 Hervorragend
|
||||||
|
|
||||||
|
Die Basis für die Wissensverwaltung ist in einem exzellenten Zustand:
|
||||||
|
|
||||||
|
* **Docs-as-Code:** Die Verzeichnisstruktur unter `/docs` ist aufgeräumt (Phase 1 der Roadmap ist abgeschlossen). Alte
|
||||||
|
Dokumente sind ordnungsgemäß im `_archive` abgelegt.
|
||||||
|
* **Projekt-Journal:** Die Nachvollziehbarkeit von Architekturentscheidungen und Readiness-Checks funktioniert
|
||||||
|
fehlerfrei. Alle Agenten dokumentieren ihre Ergebnisse diszipliniert in `docs/99_Journal/`.
|
||||||
|
* **Playbooks:** Die Rollen und Vorgaben der KI-Agenten unter `docs/04_Agents/Playbooks/` sind präzise und lenken das
|
||||||
|
Team erfolgreich.
|
||||||
|
|
||||||
|
### 2.2 Fachliche Dokumentation (Single Source of Truth): 🔴 Blockiert
|
||||||
|
|
||||||
|
Trotz der guten Struktur fehlt der eigentliche Inhalt für die Umsetzung:
|
||||||
|
|
||||||
|
* **Domänen-Wissen als Draft:** Der Ordner `docs/03_Domain/` enthält wertvolle Vorarbeiten (z.B.
|
||||||
|
`Legacy_Spec_Analysis_2026-01.md`, `Use_Cases_Draft.md`), aber diese sind explizit als **"Draft"** markiert. Es gibt
|
||||||
|
keine final freigegebenen Spezifikationen.
|
||||||
|
* **Keine verbindlichen Verträge:** Für die Entwickler und die QA gibt es in der Dokumentation keine freigegebenen
|
||||||
|
API-Contracts, Bounded Contexts oder Aggregat-Definitionen.
|
||||||
|
* **Verletzung der Projekt-Philosophie:** Unsere Guideline lautet: *"Die Dokumentation ist die Single Source of Truth"*.
|
||||||
|
Solange diese "Truth" nicht formuliert ist, darf nicht programmiert werden.
|
||||||
|
|
||||||
|
## 3. Fazit & Empfehlung
|
||||||
|
|
||||||
|
Ich schließe mich aus dokumentarischer Sicht nahtlos allen anderen Fachbereichen an. Ein Start der Implementierung ohne
|
||||||
|
vorherige Festschreibung der Domäne würde das "Docs-as-Code"-Prinzip untergraben.
|
||||||
|
|
||||||
|
**Nächste Schritte & Fahrplan (Empfehlung des Curators):**
|
||||||
|
|
||||||
|
1. **Einberufung des Lead Architects:** Der **🏗️ [Lead Architect]** muss nun aktiv werden, um den "Domain Workshop"
|
||||||
|
durchzuführen.
|
||||||
|
2. **Spezifikationen finalisieren:** Die Ergebnisse des Workshops müssen von den aktuellen "Drafts" in verbindliche,
|
||||||
|
aktive Spezifikationen unter `docs/03_Domain/` überführt werden.
|
||||||
|
3. **Roadmap-Update:** Sobald die fachliche Dokumentation steht, passe ich als Curator die `MASTER_ROADMAP.md` an und
|
||||||
|
schalte Phase 3 offiziell auf "ACTIVE".
|
||||||
|
|
||||||
|
Alle Disziplinen haben nun gesprochen. Das Veto gegen einen verfrühten Code-Start ist einstimmig. Wir warten auf die
|
||||||
|
Architektur.
|
||||||
49
docs/99_Journal/2026-03-16_Session_Log_Frontend_Readiness.md
Normal file
49
docs/99_Journal/2026-03-16_Session_Log_Frontend_Readiness.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Session Log: Analyse der Frontend-Bereitschaft für die Feature-Entwicklung
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🧹 [Curator] in Zusammenarbeit mit 🎨 [Frontend Expert]
|
||||||
|
**Typ:** Code- & Projekt-Review
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
Nach dem Backend-Review hat nun auch der 🎨 **[Frontend Expert]** das Kotlin Multiplatform (KMP) Frontend (
|
||||||
|
`:frontend:core`, `:frontend:shells:meldestelle-portal`) auf seine Bereitschaft für die fachliche Implementierung hin
|
||||||
|
untersucht.
|
||||||
|
|
||||||
|
## 2. Analyse-Ergebnisse (Frontend Expert)
|
||||||
|
|
||||||
|
### 2.1 Technische Bereitschaft: 🟢 Sehr gut
|
||||||
|
|
||||||
|
Die Architektur ist exzellent auf die Anforderungen (insbesondere "Offline-First") vorbereitet:
|
||||||
|
|
||||||
|
* **Architektur:** Saubere Modul-Trennung (`:core:auth`, `:core:network`, `:core:local-db`, `:core:sync`) und
|
||||||
|
funktionierendes Dependency Injection Setup via Koin im Main-Entrypoint.
|
||||||
|
* **Offline-First Basis:** Die lokale Datenbank via SQLDelight (inkl. `WebWorkerDriver` für die Kotlin/JS Target) ist
|
||||||
|
voll funktionsfähig. Der abstrakte `SyncManager` im Modul `:core:sync` bietet genau die richtige Basis für den
|
||||||
|
Delta-Sync mit dem Backend.
|
||||||
|
* **UI Foundation:** Das `:core:design-system` etabliert eine professionelle und einheitliche Theming-Grundlage (Farben,
|
||||||
|
Typografie, Material 3), die sich nahtlos in Compose Multiplatform integriert. Ein funktionierendes Basis-Routing (
|
||||||
|
`StateNavigationPort`) und Auth-Flows existieren bereits.
|
||||||
|
|
||||||
|
### 2.2 Fachliche Bereitschaft: 🔴 Blockiert (Warten auf Architect & UX)
|
||||||
|
|
||||||
|
Trotz der technischen Startklarheit fehlt die Grundlage für die eigentliche Feature-Entwicklung:
|
||||||
|
|
||||||
|
* **Roadmap-Status:** Gemäß `MASTER_ROADMAP.md` ist die Phase 3 (Feature Development) weiterhin **ON HOLD**.
|
||||||
|
* **Fehlende Spezifikationen:** Es existieren noch keine definierten Use-Cases, Datenmodelle oder API-Contracts (außer
|
||||||
|
Ping) für die kommenden Features.
|
||||||
|
* **UX/UI Design:** Konkrete Wireframes, Screen-Layouts oder Benutzer-Flows für die fachlichen Screens (z.B.
|
||||||
|
Nennungs-Übersicht) vom 🖌️ **[UI/UX Designer]** fehlen vollständig.
|
||||||
|
|
||||||
|
## 3. Fazit & Empfehlung
|
||||||
|
|
||||||
|
Ein verfrühter Start im Frontend würde bedeuten, ohne klare UI-Spezifikationen oder abgestimmte Backend-APIs "ins Blaue"
|
||||||
|
zu programmieren. Dies muss vermieden werden.
|
||||||
|
|
||||||
|
**Nächster logischer Schritt:**
|
||||||
|
Der 🎨 **[Frontend Expert]** schließt sich der Empfehlung des Backends an:
|
||||||
|
|
||||||
|
1. Der **🏗️ [Lead Architect]** muss den ausstehenden Domain-Workshop durchführen.
|
||||||
|
2. Im Anschluss (oder parallel) muss der **🖌️ [UI/UX Designer]** erste Wireframes für die neuen Features entwerfen.
|
||||||
|
3. Erst wenn Fachlichkeit (Domain Models) und Benutzerführung (UX-Flows) definiert sind, kann die
|
||||||
|
Frontend-Implementierung starten.
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Session Log: Infrastruktur-Analyse & Freigabe
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🧹 [Curator] in Zusammenarbeit mit 🐧 [DevOps Engineer]
|
||||||
|
**Typ:** Infrastruktur-Review / Architektur-Entscheidung
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
Vor dem Start der fachlichen Implementierung wurde der 🐧 **[DevOps Engineer]** beauftragt, einen tiefgehenden Pre-Flight
|
||||||
|
Check der lokalen Docker-Infrastruktur (`docker-compose.yaml` und Teil-Dateien) durchzuführen. Ziel war es
|
||||||
|
sicherzustellen, dass das "Docs-as-Code" Setup robust, fehlerfrei und bereit für den Entwicklungsalltag ist.
|
||||||
|
|
||||||
|
## 2. Analyse-Ergebnisse
|
||||||
|
|
||||||
|
Das Setup (unterteilt in `dc-infra.yaml`, `dc-backend.yaml`, `dc-gui.yaml` und `dc-ops.yaml`) wurde als sehr robust und
|
||||||
|
durchdacht bewertet.
|
||||||
|
Folgende positive Aspekte wurden hervorgehoben:
|
||||||
|
|
||||||
|
* **Abhängigkeitsmanagement:** Exzellente Nutzung von `depends_on` mit `condition: service_healthy` (z.B. Backend wartet
|
||||||
|
auf Postgres und Keycloak).
|
||||||
|
* **Zentrale Konfiguration:** Erfolgreiche Einbindung von `config/app/base-application.yaml` via Volumes in die Spring
|
||||||
|
Boot Container.
|
||||||
|
* **Monitoring & Tools:** Das Ops-Profil bietet ein umfassendes Toolset (Mailpit, pgAdmin, Prometheus, Grafana).
|
||||||
|
|
||||||
|
### Identifizierter Klärungsbedarf (DevOps Perspektive)
|
||||||
|
|
||||||
|
Der **DevOps Engineer** wies auf eine Abweichung von den dokumentierten Leitlinien für die lokale Entwicklung hin:
|
||||||
|
|
||||||
|
* **Keycloak Setup:** Laut Playbook sollte für die lokale Entwicklung das offizielle Image (`quay.io/keycloak/keycloak`)
|
||||||
|
im schnellen `start-dev` Modus laufen.
|
||||||
|
* **Ist-Zustand:** Die `dc-infra.yaml` nutzt ein lokales Dockerfile (`config/docker/keycloak/Dockerfile`), führt einen
|
||||||
|
Build-Step (`kc.sh build`) aus und startet den Container im produktionsnahen `--optimized` Modus.
|
||||||
|
|
||||||
|
## 3. Architekturentscheidung (Owner)
|
||||||
|
|
||||||
|
Der **Owner** wurde über die Abweichung beim Keycloak-Setup informiert und hat folgende architektonische Entscheidung
|
||||||
|
getroffen:
|
||||||
|
|
||||||
|
* **Entscheidung:** Das aktuelle, produktionsnahe Keycloak-Setup (`build` + `start --optimized`) bleibt unverändert
|
||||||
|
bestehen. Es wird **nicht** auf den reinen `start-dev` Modus zurückgebaut.
|
||||||
|
* **Begründung:** Da die Entwicklung direkt auf dem `main`-Branch stattfindet, hat die **Dev/Prod-Parität** höchste
|
||||||
|
Priorität. Die lokale Umgebung soll so nah wie möglich an der späteren Produktionsumgebung (Zora) bleiben. Eine leicht
|
||||||
|
erhöhte initiale Build-Zeit des Containers wird zugunsten von maximaler Stabilität und Vorhersehbarkeit (Vermeidung
|
||||||
|
von "works on my machine" Problemen) in Kauf genommen.
|
||||||
|
|
||||||
|
## 4. Fazit & Nächste Schritte
|
||||||
|
|
||||||
|
* Die Infrastruktur-Analyse ist abgeschlossen.
|
||||||
|
* Das aktuelle Setup ist offiziell für die nächste Phase freigegeben.
|
||||||
|
* Das Team kann nun mit der fachlichen Implementierung (Backend/Frontend) beginnen.
|
||||||
51
docs/99_Journal/2026-03-16_Session_Log_QA_Readiness.md
Normal file
51
docs/99_Journal/2026-03-16_Session_Log_QA_Readiness.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Session Log: Analyse der QA-Bereitschaft für die Feature-Entwicklung
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🧹 [Curator] in Zusammenarbeit mit 🧐 [QA Specialist]
|
||||||
|
**Typ:** Code- & Projekt-Review
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
Nach den Analysen der Bereiche DevOps, Backend, Frontend und UI/UX hat der 🧐 **[QA Specialist]** das Projekt
|
||||||
|
hinsichtlich Testbarkeit, Test-Infrastruktur und "Shift-Left"-Qualitätssicherung bewertet.
|
||||||
|
|
||||||
|
## 2. Analyse-Ergebnisse (QA Specialist)
|
||||||
|
|
||||||
|
### 2.1 Technische Test-Infrastruktur: 🟢 Hervorragend
|
||||||
|
|
||||||
|
Das Projekt bietet eine exzellente Basis für eine moderne Test-Pyramide:
|
||||||
|
|
||||||
|
* **Zentrales Test-Modul:** Das Modul `:platform:platform-testing` bündelt alle wichtigen Test-Abhängigkeiten (JUnit 5,
|
||||||
|
AssertJ, MockK). Das sorgt für Konsistenz und vermeidet Versionskonflikte.
|
||||||
|
* **Integration Testing Setup:** Die Einbindung von `Testcontainers` (PostgreSQL, Keycloak, Kafka) ist vorhanden. Dies
|
||||||
|
ermöglicht deterministische Integrationstests gegen echte Infrastruktur-Komponenten, statt wackeliger Mocks.
|
||||||
|
* **Multiplatform Ready:** Die Test-SourceSets im `:core-domain` Modul sind für Kotlin Multiplatform (JVM, JS) korrekt
|
||||||
|
vorbereitet.
|
||||||
|
|
||||||
|
### 2.2 Fachliche Test-Spezifikation (Shift-Left): 🔴 Blockiert
|
||||||
|
|
||||||
|
Wie in den anderen Disziplinen fehlt die fachliche Grundlage:
|
||||||
|
|
||||||
|
* **Fehlende Akzeptanzkriterien:** Es existieren keine Use-Cases oder konkreten "Definition of Done" Kriterien für die
|
||||||
|
anstehenden Features.
|
||||||
|
* **Keine BDD-Szenarien:** Es gibt keine "Given-When-Then" (Gherkin) Spezifikationen, gegen die die Entwickler fachliche
|
||||||
|
Tests (TDD) schreiben könnten.
|
||||||
|
* **Testdaten-Mangel:** Da das finale Domain-Modell noch unklar ist, können noch keine realistischen Testdaten (
|
||||||
|
Mock-Objekte für Turniere, Nennungen etc.) definiert werden.
|
||||||
|
|
||||||
|
## 3. Fazit & Empfehlung
|
||||||
|
|
||||||
|
Ein sofortiger Implementierungsstart würde zu "technisch grünen", aber fachlich nutzlosen Tests führen. Die
|
||||||
|
Qualitätssicherung muss "Shift-Left" betrieben werden – also weit vor der ersten Zeile Feature-Code.
|
||||||
|
|
||||||
|
**Nächste Schritte & Fahrplan (Empfehlung des QA Specialists):**
|
||||||
|
|
||||||
|
1. **Teilnahme am Domain Workshop:** Der QA Specialist muss in den anstehenden Workshop des **🏗️ [Lead Architect]**
|
||||||
|
integriert werden.
|
||||||
|
2. **Szenario-Definition:** Parallel zur Definition der Use-Cases durch den Architekten müssen die Akzeptanzkriterien
|
||||||
|
als Gherkin-Szenarien (Given-When-Then) dokumentiert werden.
|
||||||
|
3. **Edge-Cases aufdecken:** Im Workshop sollen Randfälle (z.B. "Was passiert bei einer Nennung ohne gültige Lizenz?")
|
||||||
|
identifiziert und als Test-Szenarien festgehalten werden.
|
||||||
|
|
||||||
|
Erst wenn diese fachlichen Test-Szenarien als Verträge existieren, dürfen die Entwickler mit der Implementierung
|
||||||
|
beginnen, um diese Verträge technisch zu erfüllen (Test-Driven Development Ansatz).
|
||||||
54
docs/99_Journal/2026-03-16_Session_Log_UIUX_Readiness.md
Normal file
54
docs/99_Journal/2026-03-16_Session_Log_UIUX_Readiness.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Session Log: Analyse der UI/UX-Bereitschaft für die Feature-Entwicklung
|
||||||
|
|
||||||
|
**Datum:** 16. März 2026
|
||||||
|
**Verantwortlicher Agent:** 🧹 [Curator] in Zusammenarbeit mit 🖌️ [UI/UX Designer]
|
||||||
|
**Typ:** Code- & Projekt-Review
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
Nach den positiven technischen Reviews durch das Backend- und Frontend-Team hat der 🖌️ **[UI/UX Designer]** das Projekt
|
||||||
|
hinsichtlich der User Experience und Design-Bereitschaft analysiert. Der Fokus lag auf dem Zusammenspiel zwischen den
|
||||||
|
technischen Möglichkeiten, den alten System-Referenzen und den zukünftigen Anforderungen.
|
||||||
|
|
||||||
|
## 2. Analyse-Ergebnisse (UI/UX Designer)
|
||||||
|
|
||||||
|
### 2.1 Technische Design-Basis: 🟢 Gut
|
||||||
|
|
||||||
|
Die technische Umsetzung der grundlegenden Design-Sprache ist bereits im Frontend-Code verankert:
|
||||||
|
|
||||||
|
* **Design System (`AppTheme.kt`):** Ein starkes, "Enterprise-taugliches" Theme (Material 3) ist implementiert. Es
|
||||||
|
definiert Farbpaletten, Kontraste (inkl. Dark Mode) und Typografie-Hierarchien. Dies stellt sicher, dass neue
|
||||||
|
Komponenten konsistent und professionell aussehen werden.
|
||||||
|
* **Technologie (Compose):** Compose Multiplatform bietet die perfekte Basis, um hochgradig reaktive und responsive
|
||||||
|
Interfaces schnell zu entwickeln.
|
||||||
|
|
||||||
|
### 2.2 Fachliche UI/UX-Spezifikation: 🔴 Blockiert (Wireframes & Flows fehlen)
|
||||||
|
|
||||||
|
Die visuelle und konzeptionelle Ausarbeitung der eigentlichen Features fehlt komplett:
|
||||||
|
|
||||||
|
* **Fehlende UX-Flows:** Es gibt keine definierten Benutzerpfade. Es ist unklar, wie Nutzer durch die Kernprozesse (z.B.
|
||||||
|
Nennungen anlegen, Startlisten prüfen) navigieren sollen.
|
||||||
|
* **Umgang mit Legacy-Screenshots:** Die Bilder des alten "SuDo"-Systems (unter `docs/BilderSuDo/`) sind wertvolle
|
||||||
|
Referenzen für die Datenpunkte, **dürfen aber nicht 1:1 nachgebaut werden**. Das neue UI muss entschlackt, fokussiert
|
||||||
|
und intuitiver gestaltet werden ("Fokus auf den Sport").
|
||||||
|
* **Offline-First UX:** Ein entscheidender Aspekt fehlt: Das Interface-Design muss klar kommunizieren, ob der Nutzer
|
||||||
|
offline ist, ob Daten synchronisiert werden oder ob es Sync-Konflikte gibt. Diese speziellen UI-Muster müssen erst
|
||||||
|
gestaltet werden.
|
||||||
|
|
||||||
|
## 3. Fazit & Empfehlung
|
||||||
|
|
||||||
|
Ein Start der UI-Programmierung ohne vorherige Design-Spezifikationen würde unweigerlich zu einer unstrukturierten
|
||||||
|
Benutzeroberfläche ("Datenbank-Felder aufs UI klatschen") und damit zu einer schlechten User Experience führen.
|
||||||
|
|
||||||
|
**Nächste Schritte & Fahrplan (Empfehlung des UI/UX Designers):**
|
||||||
|
|
||||||
|
1. **Domain Workshop:** Teilnahme (passiv) am ausstehenden Workshop des **🏗️ [Lead Architect]**, um die Fachlichkeit und
|
||||||
|
die notwendigen Datenpunkte zu verstehen.
|
||||||
|
2. **Wireframing:** Erstellung von aufgeräumten, responsiven Wireframes (Mobile & Desktop) für die Kern-Flows (z.B.
|
||||||
|
Nennung, Startliste) basierend auf den neuen Modellen und unter zur Hilfenahme der SuDo-Screenshots als fachliche
|
||||||
|
Orientierung.
|
||||||
|
3. **Frontend-Abstimmung:** Review der Wireframes mit dem **🎨 [Frontend Expert]**, insbesondere hinsichtlich der
|
||||||
|
Offline-UX-Konzepte.
|
||||||
|
|
||||||
|
Erst nach der Freigabe der Wireframes für einen spezifischen Bounded Context sollte das Frontend-Team mit der
|
||||||
|
Implementierung der Screens beginnen.
|
||||||
|
|
@ -36,7 +36,7 @@ class AuthApiClient(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate user with username and password
|
* Authenticate a user with a username and password
|
||||||
*/
|
*/
|
||||||
suspend fun login(username: String, password: String): LoginResponse {
|
suspend fun login(username: String, password: String): LoginResponse {
|
||||||
val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token"
|
val tokenEndpoint = "$keycloakBaseUrl/realms/$realm/protocol/openid-connect/token"
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ expect suspend fun launchOidcFlow(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft beim App-Start, ob in der aktuellen URL ein OIDC-Callback steckt.
|
* Prüft beim App-Start, ob in der aktuellen URL ein OIDC-Callback steckt.
|
||||||
* Relevant für JS/Browser: nach Keycloak-Redirect enthält die URL code= und state=.
|
* Relevant für JS/Browser: Nach Keycloak-Redirect enthält die URL code= und state=.
|
||||||
* Gibt null zurück, wenn kein Callback vorhanden.
|
* Gibt null zurück, wenn kein Callback vorhanden ist.
|
||||||
*/
|
*/
|
||||||
expect fun consumePendingOidcCallback(): OidcCallbackResult?
|
expect fun consumePendingOidcCallback(): OidcCallbackResult?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD")
|
||||||
|
|
||||||
package at.mocode.frontend.core.auth.data
|
package at.mocode.frontend.core.auth.data
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,13 +62,13 @@ internal object Sha256 {
|
||||||
w[i] = w[i - 16] + s0 + w[i - 7] + s1
|
w[i] = w[i - 16] + s0 + w[i - 7] + s1
|
||||||
}
|
}
|
||||||
|
|
||||||
var a = h0;
|
var a = h0
|
||||||
var b = h1;
|
var b = h1
|
||||||
var c = h2;
|
var c = h2
|
||||||
var d = h3
|
var d = h3
|
||||||
var e = h4;
|
var e = h4
|
||||||
var f = h5;
|
var f = h5
|
||||||
var g = h6;
|
var g = h6
|
||||||
var h = h7
|
var h = h7
|
||||||
|
|
||||||
for (i in 0..63) {
|
for (i in 0..63) {
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ class LoginViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verarbeitet das Ergebnis des OIDC-Callbacks (intern + von JS-Startup). */
|
/** Verarbeitet das Ergebnis des OIDC-Callbacks (intern + von JS-Start-up). */
|
||||||
private suspend fun handleOidcCallbackResult(result: OidcCallbackResult) {
|
private suspend fun handleOidcCallbackResult(result: OidcCallbackResult) {
|
||||||
when (result) {
|
when (result) {
|
||||||
is OidcCallbackResult.Success -> {
|
is OidcCallbackResult.Success -> {
|
||||||
|
|
|
||||||
|
|
@ -1,122 +1,146 @@
|
||||||
// Minimal debug worker
|
'use strict';
|
||||||
console.log("Worker: sqlite.worker.js loaded. Starting initialization...");
|
|
||||||
|
|
||||||
try {
|
// --- State ---
|
||||||
// We do NOT import from node_modules anymore to avoid Webpack bundling issues.
|
|
||||||
// import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
|
||||||
|
|
||||||
// Message buffer for messages arriving before DB is ready
|
|
||||||
let messageQueue = [];
|
|
||||||
let db = null;
|
let db = null;
|
||||||
let isReady = false;
|
let isReady = false;
|
||||||
|
let initError = null;
|
||||||
|
const messageQueue = [];
|
||||||
|
|
||||||
// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`.
|
// --- Message Handler ---
|
||||||
self.onmessage = (event) => {
|
self.onmessage = (event) => {
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
console.log("Worker: Buffering message (DB not ready)", event.data);
|
if (initError) {
|
||||||
|
postMessage({id: event.data?.id, error: `Worker not initialized: ${initError}`});
|
||||||
|
} else {
|
||||||
messageQueue.push(event);
|
messageQueue.push(event);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
processMessage(event);
|
processMessage(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.onerror = (event) => {
|
||||||
|
console.error('[sqlite.worker] Uncaught error:', event.message, `${event.filename}:${event.lineno}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Message Processor ---
|
||||||
function processMessage(event) {
|
function processMessage(event) {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
let result;
|
||||||
try {
|
try {
|
||||||
switch (data && data.action) {
|
result = executeAction(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[sqlite.worker] Error processing message:', err);
|
||||||
|
postMessage({id: data.id, error: err?.message ?? String(err)});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postMessage({id: data.id, ...result});
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeAction(data) {
|
||||||
|
switch (data.action) {
|
||||||
case 'exec': {
|
case 'exec': {
|
||||||
if (!data.sql) throw new Error('exec: Missing query string');
|
if (!data.sql) {
|
||||||
|
postMessage({id: data.id, error: 'exec: Missing sql string'});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const rows = [];
|
const rows = [];
|
||||||
db.exec({
|
db.exec({
|
||||||
sql: data.sql,
|
sql: data.sql,
|
||||||
bind: data.params ?? [],
|
bind: data.params ?? [],
|
||||||
rowMode: 'array',
|
rowMode: 'array',
|
||||||
callback: (row) => rows.push(row)
|
callback: (row) => rows.push(row),
|
||||||
});
|
});
|
||||||
return postMessage({id: data.id, results: {values: rows}});
|
return {results: {values: rows}};
|
||||||
}
|
}
|
||||||
case 'begin_transaction':
|
case 'begin_transaction':
|
||||||
db.exec('BEGIN TRANSACTION;');
|
db.exec('BEGIN TRANSACTION;');
|
||||||
return postMessage({id: data.id, results: []});
|
return {results: []};
|
||||||
case 'end_transaction':
|
case 'end_transaction':
|
||||||
db.exec('END TRANSACTION;');
|
db.exec('END TRANSACTION;');
|
||||||
return postMessage({id: data.id, results: []});
|
return {results: []};
|
||||||
case 'rollback_transaction':
|
case 'rollback_transaction':
|
||||||
db.exec('ROLLBACK TRANSACTION;');
|
db.exec('ROLLBACK TRANSACTION;');
|
||||||
return postMessage({id: data.id, results: []});
|
return {results: []};
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported action: ${data && data.action}`);
|
postMessage({id: data.id, error: `Unsupported action: ${data.action}`});
|
||||||
}
|
return null;
|
||||||
} catch (err) {
|
|
||||||
console.error("Worker: Error processing message", err);
|
|
||||||
return postMessage({id: data && data.id, error: err?.message ?? String(err)});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onerror = function (event) {
|
// --- Initialization ---
|
||||||
console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno);
|
|
||||||
// Don't postMessage here as it might confuse the driver if it expects a response to a query
|
|
||||||
};
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
// Load sqlite3.js via importScripts (avoids Webpack bundling issues)
|
||||||
// 1. Load the sqlite3.js library manually via importScripts.
|
const loadResult = tryImportScripts();
|
||||||
console.log("Worker: Loading sqlite3.js via importScripts...");
|
if (loadResult !== null) {
|
||||||
try {
|
handleInitError(loadResult);
|
||||||
importScripts('sqlite3.js');
|
return;
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Failed to importScripts('sqlite3.js'). Check if file exists at root. Error: " + e.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof self.sqlite3InitModule !== 'function') {
|
if (typeof self.sqlite3InitModule !== 'function') {
|
||||||
throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly.");
|
handleInitError('sqlite3InitModule not found after importScripts – sqlite3.js may be corrupt or wrong version.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Worker: Fetching sqlite3.wasm manually...");
|
// Fetch WASM binary manually so we control the URL and can detect failures early
|
||||||
const response = await fetch('sqlite3.wasm');
|
let wasmBinary;
|
||||||
if (!response.ok) {
|
try {
|
||||||
throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`);
|
const wasmResponse = await fetch('sqlite3.wasm');
|
||||||
|
if (!wasmResponse.ok) {
|
||||||
|
handleInitError(`Failed to fetch sqlite3.wasm: ${wasmResponse.status} ${wasmResponse.statusText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wasmBinary = await wasmResponse.arrayBuffer();
|
||||||
|
} catch (e) {
|
||||||
|
handleInitError(`Failed to fetch sqlite3.wasm: ${e.message}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const wasmBinary = await response.arrayBuffer();
|
|
||||||
console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength);
|
|
||||||
|
|
||||||
console.log("Worker: Calling sqlite3InitModule with wasmBinary...");
|
try {
|
||||||
const sqlite3 = await self.sqlite3InitModule({
|
const sqlite3 = await self.sqlite3InitModule({
|
||||||
print: console.log,
|
print: console.log,
|
||||||
printErr: console.error,
|
printErr: console.error,
|
||||||
wasmBinary: wasmBinary
|
wasmBinary,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Worker: sqlite3InitModule resolved successfully");
|
|
||||||
const opfsAvailable = 'opfs' in sqlite3;
|
|
||||||
console.log("Worker: OPFS available:", opfsAvailable);
|
|
||||||
|
|
||||||
// Initialize DB
|
|
||||||
const dbName = 'app.db';
|
const dbName = 'app.db';
|
||||||
if (opfsAvailable) {
|
if ('opfs' in sqlite3) {
|
||||||
console.log("Initialisiere persistente OPFS Datenbank: " + dbName);
|
console.log(`[sqlite.worker] Using persistent OPFS database: ${dbName}`);
|
||||||
db = new sqlite3.oo1.OpfsDb(dbName);
|
db = new sqlite3.oo1.OpfsDb(dbName);
|
||||||
} else {
|
} else {
|
||||||
console.warn("OPFS nicht verfügbar, Fallback auf In-Memory");
|
console.warn('[sqlite.worker] OPFS not available – falling back to in-memory database');
|
||||||
db = new sqlite3.oo1.DB(dbName);
|
db = new sqlite3.oo1.DB(dbName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as ready and process queue
|
// Flush buffered messages
|
||||||
isReady = true;
|
isReady = true;
|
||||||
console.log("Worker: DB Ready. Processing " + messageQueue.length + " buffered messages.");
|
console.log(`[sqlite.worker] Ready. Flushing ${messageQueue.length} buffered message(s).`);
|
||||||
while (messageQueue.length > 0) {
|
while (messageQueue.length > 0) {
|
||||||
processMessage(messageQueue.shift());
|
processMessage(messageQueue.shift());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Database initialization error in worker:", e);
|
handleInitError(e?.message ?? String(e));
|
||||||
// We can't easily communicate this back to the driver during init,
|
}
|
||||||
// but console.error should show up.
|
}
|
||||||
|
|
||||||
|
function tryImportScripts() {
|
||||||
|
try {
|
||||||
|
importScripts('sqlite3.js');
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return `importScripts('sqlite3.js') failed – is the file served at root? ${e.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInitError(message) {
|
||||||
|
initError = message;
|
||||||
|
console.error('[sqlite.worker] Initialization failed:', message);
|
||||||
|
while (messageQueue.length > 0) {
|
||||||
|
const queued = messageQueue.shift();
|
||||||
|
postMessage({id: queued.data?.id, error: `Worker initialization failed: ${initError}`});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Critical Worker Error:", e);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package at.mocode.frontend.core.navigation
|
||||||
import at.mocode.frontend.core.domain.models.User
|
import at.mocode.frontend.core.domain.models.User
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction to obtain the current authenticated user (or null if guest).
|
* Abstraction to get the current authenticated user (or null if guest).
|
||||||
* Implementations live in shells/apps and provide access to the actual auth state.
|
* Implementations live in shells/apps and provide access to the actual auth state.
|
||||||
*/
|
*/
|
||||||
interface CurrentUserProvider {
|
interface CurrentUserProvider {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import at.mocode.frontend.core.domain.models.AppRoles
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep link handling with minimal auth-aware guard via CurrentUserProvider.
|
* Deep link handling with minimal auth-aware guard via CurrentUserProvider.
|
||||||
* This version is self-contained in core:navigation and has no dependency on shared app store.
|
* This version is self-contained in core:navigation and has no dependency on the shared app store.
|
||||||
*/
|
*/
|
||||||
class DeepLinkHandler(
|
class DeepLinkHandler(
|
||||||
private val navigation: NavigationPort,
|
private val navigation: NavigationPort,
|
||||||
|
|
@ -22,11 +22,11 @@ class DeepLinkHandler(
|
||||||
|
|
||||||
fun handleDeepLink(url: String): Boolean {
|
fun handleDeepLink(url: String): Boolean {
|
||||||
val parsed = parseDeepLink(url) ?: return false
|
val parsed = parseDeepLink(url) ?: return false
|
||||||
return processDeepLink(parsed)
|
processDeepLink(parsed)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement deep link processing logic
|
private fun processDeepLink(deepLink: DeepLink) {
|
||||||
private fun processDeepLink(deepLink: DeepLink): Boolean {
|
|
||||||
val route = cleanRoute(deepLink.route)
|
val route = cleanRoute(deepLink.route)
|
||||||
|
|
||||||
// If the route requires auth and the user is missing → redirect to log in
|
// If the route requires auth and the user is missing → redirect to log in
|
||||||
|
|
@ -34,20 +34,19 @@ class DeepLinkHandler(
|
||||||
val user = currentUserProvider.getCurrentUser()
|
val user = currentUserProvider.getCurrentUser()
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
navigation.navigateTo(config.loginRoute)
|
navigation.navigateTo(config.loginRoute)
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
// Admin section guard: requires ADMIN role
|
// Admin section guard: requires ADMIN role
|
||||||
if (requiresAdmin(route)) {
|
if (requiresAdmin(route)) {
|
||||||
val isAdmin = user.roles.contains(AppRoles.ADMIN)
|
val isAdmin = user.roles.contains(AppRoles.ADMIN)
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
navigation.navigateTo(Routes.HOME)
|
navigation.navigateTo(Routes.HOME)
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation.navigateTo(deepLink.route)
|
navigation.navigateTo(deepLink.route)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDeepLink(url: String): DeepLink? {
|
private fun parseDeepLink(url: String): DeepLink? {
|
||||||
|
|
@ -63,7 +62,7 @@ class DeepLinkHandler(
|
||||||
val parts = withoutScheme.split("/")
|
val parts = withoutScheme.split("/")
|
||||||
if (parts.isEmpty() || parts[0] != config.host) return null
|
if (parts.isEmpty() || parts[0] != config.host) return null
|
||||||
val path = "/" + parts.drop(1).joinToString("/")
|
val path = "/" + parts.drop(1).joinToString("/")
|
||||||
val route = if (path.isBlank()) Routes.HOME else path
|
val route = path.ifBlank { Routes.HOME }
|
||||||
return DeepLink(DeepLinkType.CUSTOM_SCHEME, route, url)
|
return DeepLink(DeepLinkType.CUSTOM_SCHEME, route, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +86,4 @@ class DeepLinkHandler(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requiresAdmin(route: String): Boolean = route.startsWith("${Routes.Admin.ROOT}/")
|
private fun requiresAdmin(route: String): Boolean = route.startsWith("${Routes.Admin.ROOT}/")
|
||||||
|
|
||||||
fun generateDeepLink(route: String, useCustomScheme: Boolean = true): String =
|
|
||||||
if (useCustomScheme) "${config.scheme}://${config.host}$route" else "https://${config.allowedDomains.first()}$route"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ import kotlinx.browser.window
|
||||||
|
|
||||||
@Suppress("UnsafeCastFromDynamic", "EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
@Suppress("UnsafeCastFromDynamic", "EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||||
actual object PlatformConfig {
|
actual object PlatformConfig {
|
||||||
|
|
||||||
|
private val globalScope: dynamic
|
||||||
|
get() = js("typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}))")
|
||||||
|
|
||||||
actual fun resolveApiBaseUrl(): String {
|
actual fun resolveApiBaseUrl(): String {
|
||||||
// 1) Prefer a global JS variable (can be injected by index.html or nginx)
|
// 1) Prefer a global JS variable (injected by main.kt via AppConfig)
|
||||||
val global =
|
|
||||||
js("typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}))")
|
|
||||||
val fromGlobal = try {
|
val fromGlobal = try {
|
||||||
(global.API_BASE_URL as? String)?.trim().orEmpty()
|
(globalScope.API_BASE_URL as? String)?.trim().orEmpty()
|
||||||
} catch (_: dynamic) {
|
} catch (_: dynamic) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -17,32 +19,27 @@ actual object PlatformConfig {
|
||||||
console.log("[PlatformConfig] Resolved API_BASE_URL from global: $fromGlobal")
|
console.log("[PlatformConfig] Resolved API_BASE_URL from global: $fromGlobal")
|
||||||
return fromGlobal.removeSuffix("/")
|
return fromGlobal.removeSuffix("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Try window location origin (same origin gateway/proxy setup)
|
// 2) Try window location origin (same origin gateway/proxy setup)
|
||||||
val origin = try {
|
val origin = try {
|
||||||
window.location.origin
|
window.location.origin
|
||||||
} catch (_: dynamic) {
|
} catch (_: dynamic) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!origin.isNullOrBlank()) {
|
if (!origin.isNullOrBlank()) {
|
||||||
val resolvedUrl = origin.removeSuffix("/") + "/api"
|
val resolvedUrl = origin.removeSuffix("/") + "/api"
|
||||||
console.log("[PlatformConfig] Resolved API_BASE_URL from window.location.origin: $resolvedUrl")
|
console.log("[PlatformConfig] Resolved API_BASE_URL from window.location.origin: $resolvedUrl")
|
||||||
return resolvedUrl
|
return resolvedUrl
|
||||||
}
|
}
|
||||||
|
// 3) Fallback to the local gateway directly (e.g., for tests without a window)
|
||||||
// 3) Fallback to the local gateway directly (e.g. for tests without window)
|
|
||||||
val fallbackUrl = "http://localhost:8081/api"
|
val fallbackUrl = "http://localhost:8081/api"
|
||||||
console.log("[PlatformConfig] Fallback API_BASE_URL: $fallbackUrl")
|
console.log("[PlatformConfig] Fallback API_BASE_URL: $fallbackUrl")
|
||||||
return fallbackUrl
|
return fallbackUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun resolveKeycloakUrl(): String {
|
actual fun resolveKeycloakUrl(): String {
|
||||||
// 1) Prefer a global JS variable injected by index.html at runtime
|
// 1) Prefer a global JS variable (injected by main.kt via AppConfig)
|
||||||
val global =
|
|
||||||
js("typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}))")
|
|
||||||
val fromGlobal = try {
|
val fromGlobal = try {
|
||||||
(global.KEYCLOAK_URL as? String)?.trim().orEmpty()
|
(globalScope.KEYCLOAK_URL as? String)?.trim().orEmpty()
|
||||||
} catch (_: dynamic) {
|
} catch (_: dynamic) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +47,6 @@ actual object PlatformConfig {
|
||||||
console.log("[PlatformConfig] Resolved KEYCLOAK_URL from global: $fromGlobal")
|
console.log("[PlatformConfig] Resolved KEYCLOAK_URL from global: $fromGlobal")
|
||||||
return fromGlobal.removeSuffix("/")
|
return fromGlobal.removeSuffix("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Derive from window.location.hostname with default Keycloak port
|
// 2) Derive from window.location.hostname with default Keycloak port
|
||||||
val hostname = try {
|
val hostname = try {
|
||||||
window.location.hostname
|
window.location.hostname
|
||||||
|
|
@ -62,7 +58,6 @@ actual object PlatformConfig {
|
||||||
console.log("[PlatformConfig] Resolved KEYCLOAK_URL from window.location.hostname: $resolvedUrl")
|
console.log("[PlatformConfig] Resolved KEYCLOAK_URL from window.location.hostname: $resolvedUrl")
|
||||||
return resolvedUrl
|
return resolvedUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Fallback for local development
|
// 3) Fallback for local development
|
||||||
val fallbackUrl = "http://localhost:8180"
|
val fallbackUrl = "http://localhost:8180"
|
||||||
console.log("[PlatformConfig] Fallback KEYCLOAK_URL: $fallbackUrl")
|
console.log("[PlatformConfig] Fallback KEYCLOAK_URL: $fallbackUrl")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import kotlinx.coroutines.await
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
private val AppJson = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AppConfig(
|
data class AppConfig(
|
||||||
val apiBaseUrl: String,
|
val apiBaseUrl: String,
|
||||||
|
|
@ -18,7 +20,7 @@ suspend fun loadAppConfig(): AppConfig {
|
||||||
return fallbackFromGlobal()
|
return fallbackFromGlobal()
|
||||||
}
|
}
|
||||||
val text = response.text().await()
|
val text = response.text().await()
|
||||||
Json.decodeFromString(AppConfig.serializer(), text)
|
AppJson.decodeFromString(AppConfig.serializer(), text)
|
||||||
} catch (e: dynamic) {
|
} catch (e: dynamic) {
|
||||||
console.error("[Config] Error loading config:", e)
|
console.error("[Config] Error loading config:", e)
|
||||||
// Fallback: Caddy-injizierte Werte aus index.html (globalThis.API_BASE_URL / KEYCLOAK_URL)
|
// Fallback: Caddy-injizierte Werte aus index.html (globalThis.API_BASE_URL / KEYCLOAK_URL)
|
||||||
|
|
@ -28,10 +30,10 @@ suspend fun loadAppConfig(): AppConfig {
|
||||||
|
|
||||||
private fun fallbackFromGlobal(): AppConfig {
|
private fun fallbackFromGlobal(): AppConfig {
|
||||||
val apiBase = (js("globalThis.API_BASE_URL") as? String)
|
val apiBase = (js("globalThis.API_BASE_URL") as? String)
|
||||||
?.takeIf { it.isNotBlank() && !it.startsWith("\${") }
|
?.takeIf { it.isNotBlank() && !it.startsWith($$"${") }
|
||||||
?: window.location.origin
|
?: window.location.origin
|
||||||
val kcUrl = (js("globalThis.KEYCLOAK_URL") as? String)
|
val kcUrl = (js("globalThis.KEYCLOAK_URL") as? String)
|
||||||
?.takeIf { it.isNotBlank() && !it.startsWith("\${") }
|
?.takeIf { it.isNotBlank() && !it.startsWith($$"${") }
|
||||||
console.log("[Config] Fallback: apiBaseUrl=$apiBase, keycloakUrl=$kcUrl")
|
console.log("[Config] Fallback: apiBaseUrl=$apiBase, keycloakUrl=$kcUrl")
|
||||||
return AppConfig(apiBaseUrl = apiBase, keycloakUrl = kcUrl)
|
return AppConfig(apiBaseUrl = apiBase, keycloakUrl = kcUrl)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ fun main() {
|
||||||
val config = loadAppConfig()
|
val config = loadAppConfig()
|
||||||
console.log("[WebApp] Configuration loaded: apiBaseUrl=${config.apiBaseUrl}, keycloakUrl=${config.keycloakUrl}")
|
console.log("[WebApp] Configuration loaded: apiBaseUrl=${config.apiBaseUrl}, keycloakUrl=${config.keycloakUrl}")
|
||||||
|
|
||||||
// Inject config into global JS scope for PlatformConfig to find
|
// Inject config into the global JS scope for PlatformConfig to find
|
||||||
val global = js("typeof globalThis !== 'undefined' ? globalThis : window")
|
val global = js("typeof globalThis !== 'undefined' ? globalThis : window")
|
||||||
global.API_BASE_URL = config.apiBaseUrl
|
global.API_BASE_URL = config.apiBaseUrl
|
||||||
config.keycloakUrl?.let { global.KEYCLOAK_URL = it }
|
config.keycloakUrl?.let { global.KEYCLOAK_URL = it }
|
||||||
|
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
// Minimal debug worker
|
|
||||||
console.log("Worker: sqlite.worker.js loaded. Starting initialization...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// We do NOT import from node_modules anymore to avoid Webpack bundling issues.
|
|
||||||
// import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
|
||||||
|
|
||||||
// Message buffer for messages arriving before DB is ready
|
|
||||||
let messageQueue = [];
|
|
||||||
let db = null;
|
|
||||||
let isReady = false;
|
|
||||||
|
|
||||||
// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`.
|
|
||||||
self.onmessage = (event) => {
|
|
||||||
if (!isReady) {
|
|
||||||
console.log("Worker: Buffering message (DB not ready)", event.data);
|
|
||||||
messageQueue.push(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
processMessage(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
function processMessage(event) {
|
|
||||||
const data = event.data;
|
|
||||||
try {
|
|
||||||
switch (data && data.action) {
|
|
||||||
case 'exec': {
|
|
||||||
if (!data.sql) throw new Error('exec: Missing query string');
|
|
||||||
const rows = [];
|
|
||||||
db.exec({
|
|
||||||
sql: data.sql,
|
|
||||||
bind: data.params ?? [],
|
|
||||||
rowMode: 'array',
|
|
||||||
callback: (row) => rows.push(row)
|
|
||||||
});
|
|
||||||
return postMessage({ id: data.id, results: { values: rows } });
|
|
||||||
}
|
|
||||||
case 'begin_transaction':
|
|
||||||
db.exec('BEGIN TRANSACTION;');
|
|
||||||
return postMessage({ id: data.id, results: [] });
|
|
||||||
case 'end_transaction':
|
|
||||||
db.exec('END TRANSACTION;');
|
|
||||||
return postMessage({ id: data.id, results: [] });
|
|
||||||
case 'rollback_transaction':
|
|
||||||
db.exec('ROLLBACK TRANSACTION;');
|
|
||||||
return postMessage({ id: data.id, results: [] });
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported action: ${data && data.action}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Worker: Error processing message", err);
|
|
||||||
return postMessage({ id: data && data.id, error: err?.message ?? String(err) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.onerror = function(event) {
|
|
||||||
console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno);
|
|
||||||
// Don't postMessage here as it might confuse the driver if it expects a response to a query
|
|
||||||
};
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
try {
|
|
||||||
// 1. Load the sqlite3.js library manually via importScripts.
|
|
||||||
console.log("Worker: Loading sqlite3.js via importScripts...");
|
|
||||||
try {
|
|
||||||
importScripts('sqlite3.js');
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Failed to importScripts('sqlite3.js'). Check if file exists at root. Error: " + e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof self.sqlite3InitModule !== 'function') {
|
|
||||||
throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly.");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Worker: Fetching sqlite3.wasm manually...");
|
|
||||||
const response = await fetch('sqlite3.wasm');
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const wasmBinary = await response.arrayBuffer();
|
|
||||||
console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength);
|
|
||||||
|
|
||||||
console.log("Worker: Calling sqlite3InitModule with wasmBinary...");
|
|
||||||
const sqlite3 = await self.sqlite3InitModule({
|
|
||||||
print: console.log,
|
|
||||||
printErr: console.error,
|
|
||||||
wasmBinary: wasmBinary
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Worker: sqlite3InitModule resolved successfully");
|
|
||||||
const opfsAvailable = 'opfs' in sqlite3;
|
|
||||||
console.log("Worker: OPFS available:", opfsAvailable);
|
|
||||||
|
|
||||||
// Initialize DB
|
|
||||||
const dbName = 'app.db';
|
|
||||||
if (opfsAvailable) {
|
|
||||||
console.log("Initialisiere persistente OPFS Datenbank: " + dbName);
|
|
||||||
db = new sqlite3.oo1.OpfsDb(dbName);
|
|
||||||
} else {
|
|
||||||
console.warn("OPFS nicht verfügbar, Fallback auf In-Memory");
|
|
||||||
db = new sqlite3.oo1.DB(dbName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as ready and process queue
|
|
||||||
isReady = true;
|
|
||||||
console.log("Worker: DB Ready. Processing " + messageQueue.length + " buffered messages.");
|
|
||||||
while (messageQueue.length > 0) {
|
|
||||||
processMessage(messageQueue.shift());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Database initialization error in worker:", e);
|
|
||||||
// We can't easily communicate this back to the driver during init,
|
|
||||||
// but console.error should show up.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Critical Worker Error:", e);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const IS_DEV = self.location.hostname === 'localhost' || self.location.hostname === '127.0.0.1' || self.location.hostname === '::1';
|
const IS_DEV = self.location.hostname === 'localhost' || self.location.hostname === '127.0.0.1' || self.location.hostname === '::1';
|
||||||
|
|
||||||
const CACHE_NAME = 'meldestelle-cache-v2';
|
const CACHE_NAME = 'meldestelle-cache-v3';
|
||||||
const PRECACHE_URLS = [
|
const PRECACHE_URLS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|
@ -10,7 +10,8 @@ const PRECACHE_URLS = [
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
// In dev, don't precache. Just activate the SW immediately.
|
// In dev, don't precache. Just activate the SW immediately.
|
||||||
self.skipWaiting();
|
self.skipWaiting().then(_ => {
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
|
|
@ -74,6 +75,12 @@ self.addEventListener('fetch', (event) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App-Bundle immer vom Netzwerk – niemals aus dem Cache (verhindert veraltete JS-Versionen)
|
||||||
|
if (url.pathname.endsWith('web-app.js') || url.pathname.endsWith('web-app.js.map')) {
|
||||||
|
event.respondWith(fetch(req));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid noisy errors for favicon during dev/prod when missing
|
// Avoid noisy errors for favicon during dev/prod when missing
|
||||||
if (url.pathname === '/favicon.ico') {
|
if (url.pathname === '/favicon.ico') {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// Dummy module to satisfy WASI imports in Webpack
|
|
||||||
// Used for skiko.wasm and potentially others
|
|
||||||
|
|
||||||
export function abort() {
|
|
||||||
console.error("WASI abort called");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some WASM modules might look for these
|
|
||||||
export function emscripten_notify_memory_growth() {
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
abort,
|
|
||||||
emscripten_notify_memory_growth
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue
Block a user