Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-04-05 20:15:47 +02:00
commit 2985b3c76e
28 changed files with 1598 additions and 0 deletions

632
README.md Normal file
View File

@@ -0,0 +1,632 @@
Pamietnik ist eine Lebens/Reisejournal (Android +Webapp + Go Server) — Single Source of Truth
1. Zielbild
Eine Android-App oder Webui erstellt ein Reisejournal, loggt Standortdaten im Background, cached offline, und lädt bevorzugt per HTTP zu einem Go-Server hoch; zusätzlich ist ein Datei-Export aus der App möglich.
Der Server stellt zusätzlich eine Website bereit (Login erforderlich), auf der pro Tag die gespeicherten Punkte angezeigt werden.
Das Backend soll basierend auf Logdaten Vorschläge für bestimmte Standorte machen (z. B. wenn man sich länger an einem Ort aufhält). Die Standortinformationen (Reverse-Geocoding/Place-Info) sollen über eine kostenlose API bezogen werden.
In der App soll es möglich sein, manuell Punkte hinzuzufügen (ohne GPS).
2. Festlegungen (Decisions)
DEC-CLIENT-01: Android (Kotlin), UI mit Jetpack Compose.
DEC-CLIENT-LANG-01: Android wird in Kotlin implementiert (Android „Kotlin-first").
DEC-LOC-01: Standortlogging im Background ist erforderlich.
DEC-API-01: Server-Schnittstelle ist HTTP/REST (MQTT verworfen).
DEC-DOC-01: Alle wesentlichen Inhalte werden in Markdown gepflegt; Diagramme in Mermaid.
DEC-DB-01: Android lokal/offline: SQLite (über Room); Backend: PostgreSQL.
DEC-WEB-01: Server bietet Website mit Login; Anzeige der Trackpoints pro Tag.
DEC-AUTH-01: Für die Website wird Session-basierte Auth (Session Cookie) bevorzugt; JWT ist optional für spätere Erweiterungen (z. B. API-first/SSO).
DEC-MAP-01: Für Kartenanzeige wird OpenStreetMap-Datenbasis genutzt, mit MapLibre als SDK/Renderer (Android) bzw. Web-Kartenbibliothek nach Wahl; Tile-Quelle wird konfigurierbar gehalten.
DEC-GEO-01: Für Reverse-Geocoding (kostenlos) wird initial Nominatim (OSM) eingeplant, aber nur mit striktem Caching/Rate-Limiting und „Provider austauschbar"-Design.
DEC-OPENAPI-01: Die Backend-HTTP-Schnittstelle wird als OpenAPI-Dokument gepflegt (OpenAPI 3.1, YAML oder JSON).
3. Produktanforderungen (PRD Requirements)
3.1 Background-Logging & Permissions
REQ-LOC-01: Die App MUSS Standort im Vordergrund und im Hintergrund erfassen können.
REQ-LOC-02: Manifest & Runtime: COARSE (und optional FINE) MUSS deklariert und zur Laufzeit angefragt werden; die App MUSS mit „approximate" funktionieren.
REQ-LOC-03: Background-Location: Für Hintergrundzugriff MUSS Background-Location berücksichtigt und korrekt angefordert werden (wenn Background-Logging aktiviert ist).
REQ-LOC-04: Falls Tracking als Foreground Service umgesetzt wird, MUSS beim Service der Location-Typ gesetzt werden (foregroundServiceType="location").
3.2 Offline-First, Upload bevorzugt
REQ-SYNC-01: Trackpoints MÜSSEN zuerst lokal persistiert werden (keine rein RAM-basierte Queue).
REQ-SYNC-02: Upload ist bevorzugter Weg; bei Server-Nichterreichbarkeit MUSS gecached werden und später automatisch nachgesendet werden (Retry).
REQ-SYNC-03: Hintergrund-Upload SOLL über WorkManager erfolgen, mit Constraints „Network connected" und Retry/Backoff.
REQ-SYNC-04: Retries dürfen keine Duplikate erzeugen; es MUSS eine Idempotenz-Strategie geben (event_id).
3.3 Manuelle Punkte
REQ-MAN-01: Die App MUSS es erlauben, manuell einen Punkt hinzuzufügen (mindestens: lat, lon, timestamp; optional: Name/Notiz).
REQ-MAN-02: Manuelle Punkte MÜSSEN lokal in SQLite/Room gespeichert werden, genauso wie GPS-Punkte.
REQ-MAN-03: Manuelle Punkte MÜSSEN in den Upload-Flow (Queue) integriert sein und zum Server hochgeladen werden (inkl. event_id).
REQ-MAN-04: Manuelle Punkte MÜSSEN validiert werden (z. B. Pflichtfelder, Wertebereich) und bei Fehlern im UI eine verständliche Fehlermeldung anzeigen; Validierung kann „as the user types" erfolgen.
REQ-MAN-05: Der Nutzer SOLL optional „aktuellen Zeitpunkt" und/oder „aktuelle Position" als Vorschlag übernehmen können, aber die Eingabe bleibt manuell editierbar.
3.4 Export (sekundär)
REQ-EXP-01: Nutzer MUSS Logs/Trips in eine Datei exportieren können.
REQ-EXP-02: Export MUSS über Storage Access Framework erfolgen (z. B. ACTION_CREATE_DOCUMENT; Nutzer wählt Speicherort, App schreibt in URI).
3.5 Server Website
REQ-WEB-01: Der Server MUSS eine Website bereitstellen, die nur nach Login zugänglich ist (Auth erforderlich).
REQ-WEB-02: Nach Login MUSS die Website pro Tag die gespeicherten Punkte anzeigen können (z. B. Tagesliste + Detailansicht eines Tages).
REQ-WEB-03: Die Website MUSS die Daten nutzerspezifisch anzeigen (ein Nutzer sieht nur seine eigenen Daten / Devices/Trips innerhalb seiner Berechtigung).
REQ-WEB-04: Die Website SOLL eine Kartenansicht bieten (zusätzlich zur Liste), um Punkte/Stops pro Tag visuell darzustellen.
3.6 Security / Auth
REQ-AUTH-01: Passwörter DÜRFEN nicht im Klartext gespeichert werden; Password Hashing MUSS sicher erfolgen (Argon2id bevorzugt).
REQ-AUTH-02: Session-basierte Auth (Cookie) ist erlaubt; Sessions SOLLEN serverseitig verwaltet werden, damit Sessions invalidierbar sind (Logout/Expire).
REQ-AUTH-03: Auth muss auch für Web-Query-Endpoints gelten (Tagesliste/Tagesdetail sind geschützt).
3.7 Vorschläge/Stops
REQ-SUG-01: Das Backend MUSS aus Trackpoints „Stops" erkennen können (Aufenthalte an ungefähr gleicher Position über eine Mindestdauer).
REQ-SUG-02: Das Backend MUSS aus Stops „Vorschläge" ableiten können.
REQ-SUG-03: Stops/Vorschläge MÜSSEN pro Nutzer/Device/Trip zuordenbar sein und in der Website pro Tag sichtbar sein (Liste + Karte).
3.8 Kostenlose Standortinfo-API
REQ-GEO-01: Reverse-Geocoding/Place-Info MUSS über einen kostenlosen Dienst erfolgen (initial Nominatim), mit Caching und klaren Limits/Retry-Strategie.
REQ-GEO-02: Geocoding-Provider MUSS austauschbar sein (z. B. via Server-Config/Proxy), ohne App-Update.
REQ-GEO-03: Bulk/periodische Reverse-Geocoding Requests SOLLEN vermieden werden; Geocoding wird bevorzugt ereignisbasiert (z. B. pro erkanntem Stop) und gecached.
3.9 Karten-Dienst
REQ-MAP-01: Kartenanzeige SOLL auf OpenStreetMap-Daten basieren; Rendering/SDK via MapLibre (Android) oder äquivalente Open-Source Web-Kartenkomponente.
REQ-MAP-02: Tile-Quelle MUSS konfigurierbar sein (später Self-hosting oder Provider-Wechsel möglich).
3.10 OpenAPI
REQ-OPENAPI-01: Die Backend-HTTP-Schnittstelle MUSS im OpenAPI-Format dokumentiert werden (OAS 3.1).
REQ-OPENAPI-02: Das OpenAPI-Dokument MUSS als YAML oder JSON im Repository versioniert werden (Entry-Point: openapi.yaml oder openapi.json).
REQ-OPENAPI-03: Das OpenAPI-Dokument MUSS alle implementierten Endpoints, Schemas (Request/Response), Fehlerformate und Security-Schemes enthalten.
REQ-OPENAPI-04: Cookie-basierte Auth für Web-Endpoints SOLL im OpenAPI über type: apiKey, in: cookie beschrieben werden (CookieAuth).
3.11 Google Play / Transparenz
REQ-PRIV-01: Bei Standortzugriff im Hintergrund MUSS eine klare In-App-Erklärung/Disclosure vorhanden sein; Background-Location ist zu begründen und sauber zu deklarieren.
3.12 Dokumentationsstil
REQ-DOC-STYLE-01: Alle wesentlichen Anforderungen, Architekturentscheidungen, API-Contracts, Testanforderungen und Tasks MÜSSEN in diesem Markdown gepflegt werden.
REQ-DOC-STYLE-02: Im Chat wird nur minimal kommentiert; Details stehen im Markdown.
REQ-DOC-STYLE-03: Änderungen erfolgen als fortlaufende Updates dieses Dokuments.
4. Architektur-Dokumentation (arc42-orientiert)
Die Architekturdokumentation MUSS Kontextsicht, Bausteinsicht, Laufzeitsicht und Verteilungssicht enthalten.
Alle Diagramme MÜSSEN Mermaid sein.
Architektur wird zuerst erstellt und abgestimmt, bevor Implementierung beginnt.
4.1 Kontextsicht
Fachlicher Kontext (Kommunikationspartner)
Benutzer mobil (startet/stopt Trips, sieht Status, exportiert, fügt manuell Punkte hinzu).
Benutzer web (loggt sich ein, sieht pro Tag Punkte/Stops/Vorschläge).
Android OS / Location Services (liefert Standortupdates).
Webserver (nimmt Trackpoints entgegen, berechnet Stops, stellt Website bereit).
Geocoding-Dienst (liefert Adress-/Place-Infos).
Dateisystem via SAF (Export-Ziel).
Technischer Kontext (Schnittstellen)
Android → Server: HTTPS/REST (JSON oder Protobuf, Entscheidung offen).
Browser → Server: HTTPS (Web UI, Login, Views).
Server → Geocoding: HTTPS (Reverse-Geocoding; Provider austauschbar, gecached).
Android → SAF: ACTION_CREATE_DOCUMENT und Schreiben in content:// URI.
text
flowchart LR
UserM[User Mobile] -->|Start/Stop Trip, Export, Manual point| App[Android App]
OS[Android Location Services] -->|Location updates| App
App -->|HTTPS REST: trackpoints| API[Go REST API]
API --> DB[(PostgreSQL)]
API -->|HTTPS reverse-geocode cached| GEO[Geocoding Provider]
UserW[User Web] -->|HTTPS| Web[Go Web UI]
Web --> API
App -->|SAF create document| SAF[Android Storage Access Framework]
SAF --> File[(Exported file)]
4.2 Bausteinsicht
Bausteine (initial):
Android App: UI (Compose), Domain/Use-Cases, Background Location Logging, Manuelle Punkt-Eingabe, Lokale DB + Upload-Queue, Upload Worker, Export, Map View (optional).
Go Server: REST Router/Handler, Validation, Idempotenz/Dedupe, Persistenz, Observability.
Go Web UI: Login, Tagesübersicht, Tagesdetail (Punktliste + Karte), Vorschläge.
Suggestion Engine: Stop Detection + Reverse-Geocoding + Vorschlagslogik.
Geocoding Adapter: Provider/Proxy + Cache/RateLimit.
text
flowchart TB
subgraph Mobile[Android App]
UI[Compose UI]
Domain[Domain / Use-Cases]
Loc[Background Location Logging]
Manual[Manual Point Entry + Validation]
Store[(SQLite/Room: Local DB + Upload Queue)]
Worker[Upload Worker]
Export[Export SAF]
UI --> Domain
Domain --> Loc
Domain --> Manual
Loc --> Store
Manual --> Store
Store --> Worker
Domain --> Export
end
subgraph Server[Go Server]
API[HTTP REST API]
Val[Validation]
Idem[Idempotency/Dedupe]
Auth[Auth users sessions]
Web[Web UI login day view map]
Suggest[Stop Detection + Suggestions]
Geo[Geocoding Adapter + Cache]
SDB[(PostgreSQL)]
API --> Val --> Idem --> SDB
API --> Suggest --> SDB
Suggest --> Geo
Geo -->|HTTPS| Provider[Geocoding Provider]
Web --> Auth
Web --> API
Auth --> SDB
end
Worker -->|HTTPS Batch Upload| API
4.3 Laufzeitsicht (Szenarien)
Szenarien:
R1: Trip starten → Background-Logging → Trackpoints in lokale DB.
R2: Offline → Queue wächst → Online → Batch Upload → Ack → Queue markiert als sent.
R3: Retry/Duplicate → gleiche event_id erneut → Server erkennt Duplicate → kein doppelter Insert.
R4: Export → Nutzer wählt Datei → App schreibt Export.
R5: Web Login → Tagesübersicht → Tagesdetail (Punktliste + Karte).
R6: Stop erkannt → Reverse-Geocoding (cached) → Vorschlag wird gespeichert → Web zeigt Vorschlag.
R7: Nutzer fügt Punkt manuell hinzu → Validierung → SQLite/Room persistiert → Queue pending → später Upload.
text
sequenceDiagram
participant U as User
participant A as Android App
participant L as Location Services
participant D as Local DB Queue
U->>A: Start Trip
A->>L: Request location updates
L-->>A: Location update trackpoint
A->>D: Persist trackpoint pending
text
sequenceDiagram
participant A as Android App
participant W as WorkManager Worker
participant D as Local DB Queue
participant S as Go REST API
W->>D: Fetch pending batch
W->>S: POST /v1/trackpoints:batch
alt Server unreachable timeout
S--xW: Network error
W->>D: Keep pending; schedule retry
else Success
S-->>W: Ack accepted_ids rejected[]
W->>D: Mark accepted as sent; keep rejected with reason
end
text
sequenceDiagram
participant B as Browser
participant W as Go Web UI
participant A as Auth sessions
participant API as Go REST API
participant DB as PostgreSQL
B->>W: GET /login
B->>W: POST /login username + password
W->>A: Verify credentials + create session
A->>DB: Load user + verify hash
A-->>W: Session created
W-->>B: Set-Cookie session + redirect
B->>W: GET /days
W->>API: GET /v1/days auth
API->>DB: Query days group filter by date
API-->>W: days[]
W-->>B: Render days view
B->>W: GET /days/yyyy-mm-dd
W->>API: GET /v1/trackpoints?date=YYYY-MM-DD
API->>DB: Query trackpoints for date
API-->>W: trackpoints[]
W-->>B: Render day detail
text
sequenceDiagram
participant W as WorkManager Worker
participant S as Go REST API
participant DB as PostgreSQL
participant SE as Suggestion Engine
participant G as Geocoding Adapter Cache
participant P as Geocoding Provider
W->>S: POST /v1/trackpoints:batch
S->>DB: Persist trackpoints
S->>SE: Trigger stop detection async
SE->>DB: Read recent points
SE->>G: Reverse-geocode stop if not cached
alt Cache hit
G-->>SE: Place info cached
else Cache miss
G->>P: HTTPS reverse-geocode
P-->>G: Place info
G-->>SE: Place info
end
SE->>DB: Persist stop + suggestion
text
sequenceDiagram
participant U as User
participant A as Android App
participant V as Validation ViewModel
participant D as SQLite Room Queue
participant W as Upload Worker
participant S as Go REST API
U->>A: Add manual point lat/lon/time/note
A->>V: Update fields as user types
V-->>A: Show validation errors or OK
A->>D: Persist trackpoint source=manual pending
W->>D: Fetch pending batch
W->>S: POST /v1/trackpoints:batch
S-->>W: Ack
W->>D: Mark sent
4.4 Verteilungssicht
text
flowchart LR
Phone[Android Device] -->|Internet HTTPS| Edge[Reverse Proxy LB optional]
Browser[Web Browser] -->|Internet HTTPS| Edge
Edge --> API[Go API Service REST + Web UI]
API --> DB[(PostgreSQL)]
API --> GEO[Geocoding Adapter Proxy + Cache]
GEO -->|HTTPS| Provider[Geocoding Provider]
5. HTTP API (REST) — Vertrag (Draft)
5.1 Ziele
Single-Point ingest und Batch ingest.
Idempotenz über event_id (mind. unique pro device).
Klare Acks für Queue-Weiterdrehen.
Web UI benötigt Tagesaggregation und Tagesdetail.
Vorschläge/Stops müssen abfragbar sein.
5.2 Endpoints (Vorschlag)
Ingest:
POST /v1/trackpoints (single)
POST /v1/trackpoints:batch (batch)
GET /healthz
GET /readyz
Web/Query:
GET /v1/days?from=YYYY-MM-DD&to=YYYY-MM-DD
GET /v1/trackpoints?date=YYYY-MM-DD
GET /v1/stops?date=YYYY-MM-DD
GET /v1/suggestions?date=YYYY-MM-DD
Web UI Routes (serverseitig gerendert):
GET /login
POST /login
POST /logout
GET /days
GET /days/{yyyy-mm-dd}
5.3 Datenmodell (Draft)
Trackpoint:
event_id (string, client-generated; UUID empfohlen)
device_id (string)
trip_id (string)
timestamp (RFC3339 oder epochMillis, offen)
lat, lon (float)
source: "gps" | "manual" (optional; Default: gps)
note (string, optional; nur bei manuellen Punkten)
optional: accuracy_m, speed_mps, bearing_deg, altitude_m
Stop:
stop_id
device_id, trip_id
start_ts, end_ts
center_lat, center_lon
duration_s
place_label (optional, aus reverse-geocode)
place_details (optional JSON)
Suggestion:
suggestion_id
stop_id
type (z. B. "highlight", "name_place", "add_note")
title/text
created_at
dismissed_at (optional)
Batch response:
server_time
accepted_ids (string[])
rejected (array of {event_id, code, message})
Day summary:
date (YYYY-MM-DD)
count (int)
first_ts, last_ts (optional)
5.4 Idempotenz-Regel
Unique Key: (device_id, event_id)
Duplicate: Server antwortet erfolgreich (z. B. 200) und behandelt es als „already processed".
5.5 Auth (Draft)
Website-Zugriff MUSS Login erfordern (Session Cookie).
API-Calls für Web UI müssen Auth prüfen (Session).
Android Upload Auth ist separat zu entscheiden (API-Key vs Bearer/JWT).
6. Auth: Session Cookie vs JWT
Session Cookie (session-basiert, stateful)
Vorteile:
Browser sendet Cookies automatisch mit; serverseitige Sessions sind gut kontrollierbar.
Sofortiger Widerruf möglich (Logout = Session löschen).
Nachteile:
Server muss Session-State speichern und über Instanzen verfügbar machen (z. B. Postgres/Redis).
JWT (token-basiert, stateless)
Vorteile:
Stateless pro Request; skaliert oft leichter für reine APIs/mehrere Services.
Gut für API-first/SSO/Verteilung.
Nachteile:
Widerruf/Revocation, Rotation und Lebensdauer-Management erhöhen Komplexität.
Empfehlung für RALPH
Website: Session Cookie.
JWT optional später, falls API-first/SSO nötig wird.
7. Testing (Requirements)
7.1 Use-Case Coverage
Alle wichtigen Use Cases (R1R7, plus Duplicate/Retry und Validierungsfehler) MÜSSEN automatisiert getestet werden.
7.2 Android Unit Tests
REQ-TEST-A01: Lokale JVM-Unit-Tests für Domain/Queue/Serializer/Retry-Entscheidungen.
REQ-TEST-A02: Unit Tests für Validierungslogik manueller Punkte.
7.3 Android Integration/Instrumentation (WorkManager)
REQ-TEST-A03: Integrationstests für Worker/Constraints/Delays mit WorkManager-Testunterstützung.
7.4 Go Unit & HTTP Handler Tests
REQ-TEST-S01: Go Unit Tests via testing (go test ./...).
REQ-TEST-S02: REST Handler Tests (inkl. /v1/days, /v1/trackpoints?date=…, /v1/stops, /v1/suggestions) mit net/http/httptest.
7.5 System-/Integrationstests (App ↔ Server)
REQ-TEST-I01: Integrationstest: Pending Queue → Batch Upload → Ack → Mark sent.
REQ-TEST-I02: Offline→Online-Wechsel: zuerst Fehler, später Erfolg.
REQ-TEST-I03: Duplicate event_id: gleicher Batch zweimal → keine Duplikate.
REQ-TEST-I04: Manual point → Queue → Upload → Ack (End-to-End).
7.6 Web UI Tests
REQ-TEST-W01: Login-Flow muss getestet werden (gültig/ungültig, Session created, Zugriffsschutz).
REQ-TEST-W02: Tagesansicht muss getestet werden (korrekte Tage, korrekte Punkte/Stops/Vorschläge pro Tag, AuthZ).
7.7 Geocoding/RateLimit Tests
REQ-TEST-GEO-01: Tests müssen sicherstellen, dass Reverse-Geocoding gecached wird und keine Bulk/periodischen Calls entstehen.
REQ-TEST-GEO-02: Provider-Wechsel muss testbar sein (Config umstellen → anderer Endpoint, ohne App-Update).
8. Backlog (Tasks)
8.1 Reihenfolge (wichtig)
T000: Architektur in diesem Dokument finalisieren (Kontext, Bausteine, Laufzeit, Verteilung) und abnehmen, bevor Implementierung startet.
T000e: Architektur-Review/Abnahme-Checkpoint (Issue/PR), erst danach Implementierungs-Tasks starten.
8.2 Android (Client)
T001: Compose-Projekt anlegen (Kotlin, minSdk festlegen).
T001a: Kotlin-Standards festlegen (Coroutines-Usage, KTX/Extensions, Code Style).
T004: Permissions im Manifest + Runtime-Flow (COARSE/FINE + Background).
T006: Background-Tracking Architektur (Provider/Manager/Service falls nötig).
T033: Android Persistenz: Room-Setup (Entities/DAOs/Migrations) auf SQLite-Basis.
T008: Lokale DB + Upload-Queue (pending/sent/failed, retryCount).
T012: Upload-Worker (Constraints Network connected, Retry/Backoff).
T018: Export via SAF ACTION_CREATE_DOCUMENT + Schreiben in URI.
T060: App-Kartenansicht (Track/Stops lokal anzeigen), SDK nach DEC-MAP-01 (optional).
T080: UI Screen/Flow „Manuellen Punkt hinzufügen" (lat/lon, timestamp, note optional).
T081: Eingabevalidierung in Compose (as the user types) + Fehlermeldungen.
T082: Persistenz: Manual-Point als Trackpoint in Room speichern, inkl. source=manual.
T083: Upload: Manual-Points in Batch/Single Upload einbeziehen (idempotent über event_id).
T084: Tests: Unit Tests für Validierung + Mapping; Integrationstest: Manual point → Queue → Upload → Ack.
8.3 Server (Go, HTTP + Web UI)
API / Persistenz:
T024: REST API finalisieren (Single + Batch, Fehlerformat, Limits).
T027: Persistenz: Postgres Schema + Migrationen + Indizes + Retention.
T028: Server-Idempotenz (unique event_id pro device).
T029: Observability (Logs/Metrics), Health/Ready.
T030: E2E lokal: docker compose (API + Postgres) + Minimal-Client zum Senden.
Auth / Website:
T050: User-/Auth-Konzept festlegen (User, Devices, Ownership, Session-Lifetime).
T051: Passwort-Hashing implementieren (Argon2id bevorzugt).
T052: Session-Management implementieren (Session-Store in Postgres, Cookie setzen, Logout/Invalidate).
T053: Web UI: Login-Seite (GET/POST), Zugriffsschutz (Middleware).
T054: Web UI: Tagesübersicht (GET /days) + Tagesdetail (GET /days/{date}) inkl. Karte.
T055: API: GET /v1/days und GET /v1/trackpoints?date=YYYY-MM-DD (Auth required).
T061: Stop Detection implementieren (Parameter: minDuration, radiusMeters; konfigurierbar).
T062: Suggestions implementieren (aus Stops ableiten; Storage + API).
T063: Geocoding Adapter + Cache + RateLimit + Provider-Wechsel implementieren.
T064: Web UI: Stops/Vorschläge pro Tag anzeigen (Liste + Karte).
OpenAPI:
T070: OpenAPI-Dokument anlegen (openapi.yaml als OAS 3.1) und im Repo versionieren.
T071: OpenAPI vollständig halten: alle Endpoints/Schemas/Responses/Errors/Security abbilden, inkl. CookieAuth für Web-API.
T072: OpenAPI in CI validieren (Spec-Validation) und Änderungen nur via PR.
8.4 Tests
T040: Use-Case-Matrix (Use Case → Testart).
T041: Android Unit Tests (Queue/Serializer/Retry/Validation).
T042: Android WorkManager Integrationstests.
T043: Go Unit Tests (testing).
T044: Go HTTP Handler Tests (httptest).
T045: Integrationstests (E2E ingest + Ack + Dedupe).
T057: Web Login Tests (happy path + invalid creds + session required).
T058: Web Tagesansicht Tests (korrekte Tage, korrekte Punkte pro Tag, AuthZ).
T065: Stop/Suggestion Tests (synthetische Trackpoints → erwartete Stops/Vorschläge).
T066: Geocoding Cache/RateLimit Tests (keine Bulk Calls; Provider switch).
9. Offene Entscheidungen (Checklist)
timestamp Format: epochMillis vs RFC3339.
Android Upload Auth: X-API-Key vs Bearer/JWT (Web bleibt Session Cookie).
Payload: JSON vs Protobuf über HTTP.
Batch limits (max items, max bytes).
Server Retention-Policy (wie lange Trackpoints gespeichert werden).
Stop-Detection Parameter (min Dauer, Radius).
Karten-Tile-Provider (konkreter Anbieter/Hosting; muss austauschbar bleiben).
Geocoding Provider (konkret: Nominatim public vs self-hosted Nominatim vs alternative freie Provider); Nutzung muss Policy-konform sein.

16
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
*.iml
.gradle/
.idea/
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build/
/captures/
.externalNativeBuild/
.cxx/
*.keystore
!debug.keystore
local.properties
app/build/
app/.cxx/

65
app/CLAUDE.md Normal file
View File

@@ -0,0 +1,65 @@
# CLAUDE.md — RALPH Android Frontend
## Stack
Language: Kotlin
UI: Jetpack Compose
DB: Room (SQLite)
Worker: WorkManager
Maps: MapLibre + OpenStreetMap
HTTP: Retrofit oder Ktor Client (TBD)
Auth: TBD (API-Key vs JWT für Upload)
---
## Kern-Features (Android)
1. Background GPS-Tracking (COARSE/FINE + Background permission)
2. Offline Queue (Room) + WorkManager Upload (Idempotenz via event_id)
3. Manuelle Punkte hinzufügen (lat/lon/timestamp/notiz + Validierung)
4. Kartenansicht (MapLibre, Tile-Quelle konfigurierbar)
5. Datei-Export (SAF ACTION_CREATE_DOCUMENT)
---
## Architektur-Prinzipien
- Offline-First: Jeder Punkt wird zuerst lokal in Room gespeichert
- Upload via WorkManager mit Constraint: Network connected + Retry/Backoff
- Idempotenz: Jeder Punkt erhält eine client-seitige event_id (UUID)
- source-Feld: "gps" | "manual" unterscheidet Punkt-Typ
- Validierung manueller Punkte: as the user types im ViewModel
---
## Offene Entscheidungen (TBD)
- HTTP Client: Retrofit vs Ktor
- timestamp Format: epochMillis vs RFC3339
- Android Upload Auth: X-API-Key vs JWT
- minSdk festlegen
---
## Nächste Tasks (Reihenfolge)
- [ ] T001 Compose-Projekt anlegen (Kotlin, minSdk festlegen)
- [ ] T001a Kotlin-Standards festlegen (Coroutines, KTX, Code Style)
- [ ] T004 Permissions Manifest + Runtime-Flow (COARSE/FINE + Background)
- [ ] T006 Background-Tracking Architektur (Foreground Service)
- [ ] T033 Room-Setup (Entities/DAOs/Migrations)
- [ ] T008 Upload-Queue (pending/sent/failed, retryCount)
- [ ] T012 Upload-Worker (WorkManager, Retry/Backoff)
- [ ] T080 UI: Manuellen Punkt hinzufügen
- [ ] T081 Eingabevalidierung Compose (as the user types)
- [ ] T018 Export via SAF
---
## Antwort-Stil
- Kotlin-Code immer mit Coroutines/Flow
- Compose-Code: State Hoisting, ViewModel pattern
- Kein direkter DB/Network-Zugriff in Composables
- Tests: JVM Unit Tests für Domain/Queue; Instrumentation für Room/Worker

103
app/app/build.gradle.kts Normal file
View File

@@ -0,0 +1,103 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "de.jacek.reisejournal"
compileSdk = 35
defaultConfig {
applicationId = "de.jacek.reisejournal"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}
dependencies {
// Compose
val composeBom = platform(libs.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3)
implementation(libs.compose.activity)
implementation(libs.navigation.compose)
implementation(libs.lifecycle.viewmodel.compose)
debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.ui.test.manifest)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose)
// Room
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
// WorkManager
implementation(libs.workmanager)
implementation(libs.hilt.work)
ksp(libs.hilt.work.compiler)
// Retrofit + Moshi
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.moshi.kotlin)
ksp(libs.moshi.codegen)
implementation(libs.okhttp.logging)
// Location
implementation(libs.play.services.location)
// DataStore
implementation(libs.datastore.preferences)
// MapLibre
implementation(libs.maplibre)
// Coroutines
implementation(libs.coroutines.core)
implementation(libs.coroutines.android)
// Tests
testImplementation(libs.junit)
androidTestImplementation(libs.junit.ext)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.compose.ui.test.junit4)
}

25
app/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# Moshi
-keepclassmembers class * {
@com.squareup.moshi.FromJson *;
@com.squareup.moshi.ToJson *;
}
# Retrofit
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn javax.annotation.**
-dontwarn kotlin.Unit
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**

View File

@@ -0,0 +1,16 @@
package de.jacek.reisejournal
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("de.jacek.reisejournal", appContext.packageName)
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Background location: requested at runtime separately (T004) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Foreground service (GPS tracking) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Network -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- WorkManager auto-start after reboot -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".RalphApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Reisejournal">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Reisejournal">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,22 @@
package de.jacek.reisejournal
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint
import de.jacek.reisejournal.ui.navigation.NavGraph
import de.jacek.reisejournal.ui.theme.RalphTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
RalphTheme {
NavGraph()
}
}
}
}

View File

@@ -0,0 +1,19 @@
package de.jacek.reisejournal
import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class RalphApp : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

View File

@@ -0,0 +1,16 @@
package de.jacek.reisejournal.domain
data class Trackpoint(
val eventId: String, // UUID, client-generated
val deviceId: String,
val tripId: String?,
val timestamp: String, // RFC3339, e.g. "2024-01-15T10:30:00Z"
val lat: Double,
val lon: Double,
val source: String, // "gps" | "manual"
val note: String?,
val accuracy: Float?,
val speed: Float?,
val bearing: Float?,
val altitude: Double?,
)

View File

@@ -0,0 +1,52 @@
package de.jacek.reisejournal.ui.home
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import de.jacek.reisejournal.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) }
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineMedium,
)
Text(
text = "Punkte: ${uiState.trackpointCount}",
style = MaterialTheme.typography.bodyLarge,
)
}
}
}

View File

@@ -0,0 +1,20 @@
package de.jacek.reisejournal.ui.home
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
data class HomeUiState(
val isTracking: Boolean = false,
val trackpointCount: Int = 0,
)
@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
}

View File

@@ -0,0 +1,24 @@
package de.jacek.reisejournal.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import de.jacek.reisejournal.ui.home.HomeScreen
const val HOME_ROUTE = "home"
@Composable
fun NavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = HOME_ROUTE,
) {
composable(HOME_ROUTE) {
HomeScreen()
}
// Future routes: map, add_point, settings, export
}
}

View File

@@ -0,0 +1,20 @@
package de.jacek.reisejournal.ui.theme
import androidx.compose.ui.graphics.Color
val Primary = Color(0xFF2E7D32) // Forest green
val OnPrimary = Color(0xFFFFFFFF)
val PrimaryContainer = Color(0xFFA5D6A7)
val OnPrimaryContainer = Color(0xFF003909)
val Secondary = Color(0xFF558B2F)
val OnSecondary = Color(0xFFFFFFFF)
val SecondaryContainer = Color(0xFFCCFF90)
val OnSecondaryContainer = Color(0xFF1B5E20)
val Background = Color(0xFFF8FFF8)
val OnBackground = Color(0xFF1A1C1A)
val Surface = Color(0xFFF8FFF8)
val OnSurface = Color(0xFF1A1C1A)
val SurfaceVariant = Color(0xFFDCE5DC)
val OnSurfaceVariant = Color(0xFF404940)

View File

@@ -0,0 +1,61 @@
package de.jacek.reisejournal.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColorScheme = lightColorScheme(
primary = Primary,
onPrimary = OnPrimary,
primaryContainer = PrimaryContainer,
onPrimaryContainer = OnPrimaryContainer,
secondary = Secondary,
onSecondary = OnSecondary,
secondaryContainer = SecondaryContainer,
onSecondaryContainer = OnSecondaryContainer,
background = Background,
onBackground = OnBackground,
surface = Surface,
onSurface = OnSurface,
surfaceVariant = SurfaceVariant,
onSurfaceVariant = OnSurfaceVariant,
)
private val DarkColorScheme = darkColorScheme(
primary = PrimaryContainer,
onPrimary = OnPrimaryContainer,
primaryContainer = Primary,
onPrimaryContainer = OnPrimary,
secondary = SecondaryContainer,
onSecondary = OnSecondaryContainer,
secondaryContainer = Secondary,
onSecondaryContainer = OnSecondary,
)
@Composable
fun RalphTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,31 @@
package de.jacek.reisejournal.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Reisejournal</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="Theme.Reisejournal" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,11 @@
package de.jacek.reisejournal
import org.junit.Test
import org.junit.Assert.*
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

8
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.hilt) apply false
}

6
app/gradle.properties Normal file
View File

@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.parallel=true
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

View File

@@ -0,0 +1,85 @@
[versions]
kotlin = "2.0.21"
ksp = "2.0.21-1.0.27"
agp = "8.7.3"
composeBom = "2024.12.01"
navigationCompose = "2.8.5"
hilt = "2.52"
hiltNavigationCompose = "1.2.0"
room = "2.6.1"
workManager = "2.10.0"
retrofit = "2.11.0"
moshi = "1.15.2"
okhttp = "4.12.0"
playServicesLocation = "21.3.0"
datastorePreferences = "1.1.2"
lifecycleViewmodelCompose = "2.8.7"
maplibre = "11.7.1"
coroutines = "1.9.0"
junit = "4.13.2"
junitExt = "1.2.1"
espresso = "3.6.1"
[libraries]
# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-activity = { group = "androidx.activity", name = "activity-compose", version = "1.9.3" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
# WorkManager
workmanager = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" }
hilt-work = { group = "androidx.hilt", name = "hilt-work", version = "1.2.0" }
hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version = "1.2.0" }
# Retrofit + Moshi
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" }
moshi-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
# Location
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
# DataStore
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
# MapLibre
maplibre = { group = "org.maplibre.gl", name = "android-sdk", version.ref = "maplibre" }
# Coroutines
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
# Tests
junit = { group = "junit", name = "junit", version.ref = "junit" }
junit-ext = { group = "androidx.test.ext", name = "junit", version.ref = "junitExt" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

BIN
app/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

187
app/gradlew vendored Executable file
View File

@@ -0,0 +1,187 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by "Gradle init".
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other POSIX-compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for distributing:
#
# (2) This script is not designed to run as an executable binary. It is
# designed to be run by the shell interpreter.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #( absolute
*) app_path=$APP_HOME$link ;; #( relative
esac
done
# This is reliable if the script itself doesn't have a relative symlink
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
;;
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution,
# so instead we have to hack around them using a temporary file.
#
# We also fail if there are not enough args to parse the input.
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
app/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /C_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

29
app/settings.gradle.kts Normal file
View File

@@ -0,0 +1,29 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("gradle/libs.versions.toml"))
}
}
}
rootProject.name = "reisejournal"
include(":app")

1
backend Submodule

Submodule backend added at 55d2ffc203