Compare commits

...

10 Commits

Author SHA1 Message Date
Christoph K.
de6e387c7b Update gitignore files for all components
Root: add OS/IDE/secrets patterns
webapp: add .vite/, .tsbuildinfo, .env.local
backend: add /createuser binary, bin/, uploads/ (submodule bump)
app: remove redundant entries, add *.hprof, *.class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:57:31 +02:00
Christoph K.
23506bab7d Replace hardcoded DB credentials with env vars in docker-compose
Adds .env.example as a template and .gitignore to exclude the actual
.env file, preventing accidental credential commits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:54:18 +02:00
Christoph K.
a49416854e Remove nginx/webapp container; single Go server serves SPA + API
- Add root Dockerfile: node build → copy dist into Go embed path → distroless binary
- Update docker-compose: one service (api on :9050), DB renamed ralph→pamietnik
- Remove references to RALPH/reisejournal across all docs and configs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:32:04 +02:00
Christoph K.
d1436abca8 Add Mermaid diagrams to architecture doc (implementation status, risk quadrant, production readiness)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:24:34 +02:00
Christoph K.
9775a22473 Add arc42 architecture documentation in doc/architecture.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:20:01 +02:00
Christoph K.
5abfa29e91 Remove migrate service from docker-compose; update README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:05:22 +02:00
Christoph K.
bf89ef01c7 Add hashtag requirements (REQ-TAG-*) and backlog tasks (T073-T087)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:59:23 +02:00
Christoph K.
bc2fa7b966 Apply typewriter/editorial UI style using system fonts only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:57:03 +02:00
Christoph K.
48ff7104da Fix migrate service: use entrypoint instead of command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:54:54 +02:00
Christoph K.
909e9b6813 Fix unused import in days-page.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:36:51 +02:00
35 changed files with 993 additions and 82 deletions

View File

@@ -3,7 +3,7 @@ name: code-reviewer
description: Prüft Codequalität, Lesbarkeit und Konsistenz. Vor Commits einsetzen. description: Prüft Codequalität, Lesbarkeit und Konsistenz. Vor Commits einsetzen.
--- ---
Du bist Code-Reviewer für das Projekt Pamietnik (RALPH). Du bist Code-Reviewer für das Projekt Pamietnik.
## Checkliste Go (Backend) ## Checkliste Go (Backend)

View File

@@ -3,7 +3,7 @@ name: dokumentar
description: Pflegt Markdown-Dokumentation und Mermaid-Diagramme. Bei neuen Features, Architekturänderungen oder wenn Doku und Code auseinanderlaufen. description: Pflegt Markdown-Dokumentation und Mermaid-Diagramme. Bei neuen Features, Architekturänderungen oder wenn Doku und Code auseinanderlaufen.
--- ---
Du bist Dokumentar für das Projekt Pamietnik (RALPH). Du bist Dokumentar für das Projekt Pamietnik.
## Zu pflegende Dokumente ## Zu pflegende Dokumente

View File

@@ -3,7 +3,7 @@ name: programmierer
description: Schreibt und ändert Code für Features und Bug-Fixes. Für Go-Backend und Android/Kotlin. Einsetzen bei konkreten Implementierungsaufgaben. description: Schreibt und ändert Code für Features und Bug-Fixes. Für Go-Backend und Android/Kotlin. Einsetzen bei konkreten Implementierungsaufgaben.
--- ---
Du bist Programmierer für das Projekt Pamietnik (RALPH). Du bist Programmierer für das Projekt Pamietnik.
## Stack ## Stack

View File

@@ -3,7 +3,7 @@ name: security-reviewer
description: Prüft OWASP Top 10, Dependency-Schwachstellen und Secrets. Vor Releases und bei Änderungen an externen APIs oder Auth-Code einsetzen. description: Prüft OWASP Top 10, Dependency-Schwachstellen und Secrets. Vor Releases und bei Änderungen an externen APIs oder Auth-Code einsetzen.
--- ---
Du bist Security-Reviewer für das Projekt Pamietnik (RALPH). Du bist Security-Reviewer für das Projekt Pamietnik.
## Prüf-Befehle ## Prüf-Befehle

View File

@@ -3,7 +3,7 @@ name: software-architekt
description: Analysiert Struktur, Abhängigkeiten und Architekturentscheidungen. Einsetzen vor größeren Änderungen, neuen Modulen oder wenn Komponenten-Grenzen unklar sind. description: Analysiert Struktur, Abhängigkeiten und Architekturentscheidungen. Einsetzen vor größeren Änderungen, neuen Modulen oder wenn Komponenten-Grenzen unklar sind.
--- ---
Du bist Software-Architekt für das Projekt Pamietnik (RALPH): Go-Backend + Android-App (Kotlin/Compose). Du bist Software-Architekt für das Projekt Pamietnik: Go-Backend + Android-App (Kotlin/Compose).
## Deine Aufgaben ## Deine Aufgaben

View File

@@ -3,7 +3,7 @@ name: tester
description: Schreibt und führt Unit- und Integrationstests aus. Nach jeder Code-Änderung einsetzen. description: Schreibt und führt Unit- und Integrationstests aus. Nach jeder Code-Änderung einsetzen.
--- ---
Du bist Tester für das Projekt Pamietnik (RALPH). Du bist Tester für das Projekt Pamietnik.
## Test-Befehle ## Test-Befehle

4
.env.example Normal file
View File

@@ -0,0 +1,4 @@
# Copy to .env and set a strong password before starting
DB_USER=pamietnik
DB_NAME=pamietnik
DB_PASSWORD=change-me-before-production

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Environment / Secrets
.env
*.env
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
**Pamietnik** (Codename: RALPH) is a life/travel journal consisting of three components: **Pamietnik** is a life/travel journal consisting of three components:
- `app/` — Android app (Kotlin + Jetpack Compose) - `app/` — Android app (Kotlin + Jetpack Compose)
- `backend/` — Go REST API + server-side rendered Web UI - `backend/` — Go REST API + server-side rendered Web UI
- `README.md` — single source of truth for architecture, requirements, and backlog - `README.md` — single source of truth for architecture, requirements, and backlog
@@ -28,7 +28,7 @@ go run ./cmd/server # starts API on :8080 (default)
``` ```
**Env vars** (with defaults): **Env vars** (with defaults):
- `DATABASE_URL``postgres://ralph:ralph@localhost:5432/ralph?sslmode=disable` - `DATABASE_URL``postgres://pamietnik:pamietnik@localhost:5432/pamietnik?sslmode=disable`
- `LISTEN_ADDR``:8080` - `LISTEN_ADDR``:8080`
- `UPLOAD_DIR``./uploads` - `UPLOAD_DIR``./uploads`
@@ -93,7 +93,7 @@ cd app
### Android Architecture ### Android Architecture
``` ```
de.jacek.reisejournal/ de.jacek.pamietnik/
domain/ Trackpoint domain model domain/ Trackpoint domain model
data/ Room entities, DAOs, local DB data/ Room entities, DAOs, local DB
service/ Background location foreground service service/ Background location foreground service

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Stage 1: Build Vite SPA
FROM node:22-alpine AS webapp-builder
WORKDIR /webapp
COPY webapp/package.json webapp/package-lock.json ./
RUN npm ci
COPY webapp/ ./
RUN npm run build
# Stage 2: Build Go server
FROM golang:1.25-alpine AS go-builder
WORKDIR /app
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
# Inject built SPA into embed path
COPY --from=webapp-builder /webapp/dist ./internal/api/webapp/
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server
RUN CGO_ENABLED=0 GOOS=linux go build -o /createuser ./cmd/createuser
# Stage 3: Minimal runtime image
FROM gcr.io/distroless/static-debian12
COPY --from=go-builder /server /server
COPY --from=go-builder /createuser /createuser
ENTRYPOINT ["/server"]

View File

@@ -1,13 +1,17 @@
Pamietnik ist eine Lebens/Reisejournal (Android +Webapp + Go Server) — Single Source of Truth Pamietnik ist eine Journal App (Android + Webapp + Go Server)
1. Zielbild 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. Die Applikation besteht aus folgenden Komponenten
Der Server stellt zusätzlich eine Website bereit (Login erforderlich), auf der pro Tag die gespeicherten Punkte angezeigt werden. - Backend in go programmiert mit Postgres DB und REST API
- Webapp, eine simple im schlichten design umgesetzte Web App
- Android app loggt Standortdaten im Background, cached offline, und lädt per HTTP backend
Fuer Nutzung ist ein Account und Login notwendig.
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. 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). Es möglich sein, manuell Punkte hinzuzufügen (ohne GPS).
2. Festlegungen (Decisions) 2. Festlegungen (Decisions)
DEC-CLIENT-01: Android (Kotlin), UI mit Jetpack Compose. DEC-CLIENT-01: Android (Kotlin), UI mit Jetpack Compose.
@@ -114,6 +118,19 @@ REQ-OPENAPI-04: Cookie-basierte Auth für Web-Endpoints SOLL im OpenAPI über ty
3.11 Google Play / Transparenz 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. 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.13 Hashtags
REQ-TAG-01: Trackpoints und manuelle Punkte MÜSSEN mit einem oder mehreren Hashtags versehen werden können (z. B. `#restaurant`, `#aussicht`, `#hotel`).
REQ-TAG-02: Hashtags MÜSSEN lokal in SQLite/Room gespeichert und beim Upload zum Server übertragen werden (als Teil des Trackpoint-Schemas, Feld `tags: string[]`).
REQ-TAG-03: Der Server MUSS Hashtags persistieren und pro Trackpoint/Stop abfragbar machen.
REQ-TAG-04: Die Webapp MUSS eine Filteransicht bieten, die alle Trackpoints eines Tages oder Trips nach einem oder mehreren Hashtags filtert.
REQ-TAG-05: Hashtags SOLLEN bei der Eingabe in der App als Vorschläge angezeigt werden (aus bereits verwendeten Tags des Nutzers).
REQ-TAG-06: Die Webapp SOLL eine Tag-Übersicht bieten, die alle verwendeten Hashtags des Nutzers mit Häufigkeit auflistet.
3.12 Dokumentationsstil 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-01: Alle wesentlichen Anforderungen, Architekturentscheidungen, API-Contracts, Testanforderungen und Tasks MÜSSEN in diesem Markdown gepflegt werden.
@@ -362,6 +379,10 @@ GET /v1/stops?date=YYYY-MM-DD
GET /v1/suggestions?date=YYYY-MM-DD GET /v1/suggestions?date=YYYY-MM-DD
GET /v1/tags (alle Tags des Nutzers mit Häufigkeit)
GET /v1/trackpoints?tag=restaurant (Filter nach Hashtag)
Web UI Routes (serverseitig gerendert): Web UI Routes (serverseitig gerendert):
GET /login GET /login
@@ -391,6 +412,8 @@ source: "gps" | "manual" (optional; Default: gps)
note (string, optional; nur bei manuellen Punkten) note (string, optional; nur bei manuellen Punkten)
tags (string[], optional; Hashtags ohne #-Präfix, z. B. ["restaurant", "aussicht"])
optional: accuracy_m, speed_mps, bearing_deg, altitude_m optional: accuracy_m, speed_mps, bearing_deg, altitude_m
Stop: Stop:
@@ -474,7 +497,7 @@ Nachteile:
Widerruf/Revocation, Rotation und Lebensdauer-Management erhöhen Komplexität. Widerruf/Revocation, Rotation und Lebensdauer-Management erhöhen Komplexität.
Empfehlung für RALPH Empfehlung für Pamietnik
Website: Session Cookie. Website: Session Cookie.
JWT optional später, falls API-first/SSO nötig wird. JWT optional später, falls API-first/SSO nötig wird.
@@ -550,6 +573,14 @@ REQ-TEST-GEO-02: Provider-Wechsel muss testbar sein (Config umstellen → andere
T084: Tests: Unit Tests für Validierung + Mapping; Integrationstest: Manual point → Queue → Upload → Ack. T084: Tests: Unit Tests für Validierung + Mapping; Integrationstest: Manual point → Queue → Upload → Ack.
Hashtags (Android):
T085: Room-Schema erweitern: tags-Feld (String-Array) in Trackpoint-Entity + Migration.
T086: UI: Hashtag-Eingabe in Compose (manueller Punkt + GPS-Punkt nachträglich taggen), Vorschläge aus vorhandenen Tags.
T087: Upload: tags-Feld in Batch-Payload einbeziehen.
8.3 Server (Go, HTTP + Web UI) 8.3 Server (Go, HTTP + Web UI)
API / Persistenz: API / Persistenz:
@@ -593,6 +624,16 @@ OpenAPI:
T072: OpenAPI in CI validieren (Spec-Validation) und Änderungen nur via PR. T072: OpenAPI in CI validieren (Spec-Validation) und Änderungen nur via PR.
Hashtags (Server + Webapp):
T073: DB-Schema: tags-Spalte (text[]) in trackpoints-Tabelle + Migration + Index (GIN für Array-Suche).
T074: API: tags in Ingest-Endpoints annehmen und speichern; GET /v1/tags (Häufigkeit) und GET /v1/trackpoints?tag=... implementieren.
T075: Webapp: Tag-Filter in Tagesdetail; Tag-Übersicht (/tags); Tags bei Trackpoints anzeigen.
T076: OpenAPI: tags-Feld in Trackpoint-Schema + neue Endpoints dokumentieren.
8.4 Tests 8.4 Tests
T040: Use-Case-Matrix (Use Case → Testart). T040: Use-Case-Matrix (Use Case → Testart).

9
app/.gitignore vendored
View File

@@ -1,9 +1,7 @@
*.iml *.iml
.gradle/ .gradle/
.idea/ .idea/
/local.properties local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store .DS_Store
/build/ /build/
/captures/ /captures/
@@ -11,6 +9,5 @@
.cxx/ .cxx/
*.keystore *.keystore
!debug.keystore !debug.keystore
local.properties *.hprof
app/build/ *.class
app/.cxx/

View File

@@ -7,11 +7,11 @@ plugins {
} }
android { android {
namespace = "de.jacek.reisejournal" namespace = "de.jacek.pamietnik"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "de.jacek.reisejournal" applicationId = "de.jacek.pamietnik"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal package de.jacek.pamietnik
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -11,6 +11,6 @@ class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("de.jacek.reisejournal", appContext.packageName) assertEquals("de.jacek.pamietnik", appContext.packageName)
} }
} }

View File

@@ -1,12 +1,12 @@
package de.jacek.reisejournal package de.jacek.pamietnik
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import de.jacek.reisejournal.ui.navigation.NavGraph import de.jacek.pamietnik.ui.navigation.NavGraph
import de.jacek.reisejournal.ui.theme.RalphTheme import de.jacek.pamietnik.ui.theme.RalphTheme
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal package de.jacek.pamietnik
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
@@ -7,7 +7,7 @@ import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class RalphApp : Application(), Configuration.Provider { class PamietnikApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var workerFactory: HiltWorkerFactory lateinit var workerFactory: HiltWorkerFactory

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal.domain package de.jacek.pamietnik.domain
data class Trackpoint( data class Trackpoint(
val eventId: String, // UUID, client-generated val eventId: String, // UUID, client-generated

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal.ui.home package de.jacek.pamietnik.ui.home
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -16,7 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import de.jacek.reisejournal.R import de.jacek.pamietnik.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal.ui.home package de.jacek.pamietnik.ui.home
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel

View File

@@ -1,10 +1,10 @@
package de.jacek.reisejournal.ui.navigation package de.jacek.pamietnik.ui.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import de.jacek.reisejournal.ui.home.HomeScreen import de.jacek.pamietnik.ui.home.HomeScreen
const val HOME_ROUTE = "home" const val HOME_ROUTE = "home"

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal.ui.theme package de.jacek.pamietnik.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal.ui.theme package de.jacek.pamietnik.ui.theme
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal.ui.theme package de.jacek.pamietnik.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

View File

@@ -1,4 +1,4 @@
package de.jacek.reisejournal package de.jacek.pamietnik
import org.junit.Test import org.junit.Test
import org.junit.Assert.* import org.junit.Assert.*

View File

@@ -25,5 +25,5 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "reisejournal" rootProject.name = "pamietnik"
include(":app") include(":app")

Submodule backend updated: 55d2ffc203...dbcb0d4a09

632
doc/architecture.md Normal file
View File

@@ -0,0 +1,632 @@
# Pamietnik — Architekturdokumentation (arc42)
## 1. Einführung und Ziele
**Pamietnik** ist ein persönliches Journal bestehend aus drei Komponenten: einer Android-App, einer Web-App und einem Go-Backend-Server.
### Fachliches Zielbild
- Android-App loggt GPS-Standortdaten im Hintergrund, cached offline, lädt bevorzugt per HTTP zum Server hoch
- Manuelles Hinzufügen von Punkten (ohne GPS) mit optionaler Notiz und Hashtags
- Server erkennt aus Logdaten „Stops" (längere Aufenthalte) und leitet Vorschläge ab
- Standortinformationen (Reverse-Geocoding) über kostenlose API (Nominatim/OSM)
- Web-Ansicht nach Login: Tagesübersicht und Tagesdetail mit Karte, Stops, Vorschlägen
- Datei-Export aus der App (Android SAF)
- Nutzung erfordert Account und Login
### Qualitätsziele
| Priorität | Ziel | Maßnahme |
|-----------|------|----------|
| 1 | Datensicherheit | Argon2id Passwort-Hashing, Session-Auth, HTTPS |
| 2 | Offline-Fähigkeit | Room + WorkManager auf Android, kein RAM-only Queue |
| 3 | Idempotenz | `event_id` (UUID) als Unique Key pro Device |
| 4 | Erweiterbarkeit | Geocoding-Provider über Config austauschbar |
| 5 | Datenschutz | Background-Location Disclosure (Google Play) |
### Stakeholder
| Rolle | Erwartung |
|-------|-----------|
| Mobiler Nutzer | Automatisches Tracking, Offline-Betrieb, Export |
| Web-Nutzer | Login, Tagesübersicht mit Karte, Stops und Vorschläge |
---
## 2. Randbedingungen
| Art | Bedingung |
|-----|-----------|
| Technisch | Android: Kotlin + Jetpack Compose (DEC-CLIENT-01) |
| Technisch | Backend: Go, PostgreSQL (DEC-DB-01) |
| Technisch | Webapp: Vanilla TypeScript, keine Frameworks |
| Technisch | Server-Schnittstelle: HTTP/REST, kein MQTT (DEC-API-01) |
| Technisch | Karten: OpenStreetMap + MapLibre, konfigurierbare Tile-Quelle (DEC-MAP-01) |
| Technisch | API-Spec: OpenAPI 3.1 als `openapi.yaml` (DEC-OPENAPI-01) |
| Organisatorisch | Keine kommerziell lizenzierten Bibliotheken oder Schriften |
| Organisatorisch | Geocoding über kostenlose API, Policy-konform (DEC-GEO-01) |
| Organisatorisch | Alle Anforderungen in Markdown; Diagramme in Mermaid (DEC-DOC-01) |
---
## 3. Kontextsicht
### Fachlicher Kontext
| Kommunikationspartner | Interaktion |
|-----------------------|-------------|
| Benutzer mobil | Start/Stop Trip, Status, Export, manuelle Punkte, Hashtags |
| Benutzer web | Login, Tagesübersicht, Tagesdetail, Stops, Vorschläge, Tag-Filter |
| Android OS / Location Services | Liefert GPS-Standortupdates |
| Webserver | Nimmt Trackpoints entgegen, berechnet Stops, stellt Website bereit |
| Geocoding-Dienst (Nominatim) | Liefert Adress-/Place-Infos zu Koordinaten |
| Dateisystem via SAF | Export-Ziel für Logs/Trips |
### Technischer Kontext
```mermaid
flowchart LR
UserM[Nutzer mobil]
UserW[Nutzer Web]
App[Android App\nKotlin + Compose]
OS[Android Location Services]
Webapp[Webapp\nTS + nginx :9050]
API[Go Backend\nREST + Web UI]
DB[(PostgreSQL)]
GEO[Nominatim\nReverse-Geocoding]
SAF[Android SAF\nDatei-Export]
UserM -->|Start/Stop\nManuelle Punkte\nHashtags| App
OS -->|GPS Updates| App
App -->|HTTPS REST JSON\nPOST /v1/trackpoints:batch| API
UserW -->|HTTPS Browser| Webapp
Webapp -->|/v1/* proxy_pass| API
API --> DB
API -->|HTTPS cached| GEO
App -->|ACTION_CREATE_DOCUMENT| SAF
```
| Schnittstelle | Protokoll | Auth |
|---------------|-----------|------|
| Android → Backend | HTTPS REST JSON | Session Cookie (TBD: API-Key/JWT) |
| Browser → Backend | HTTPS | Session Cookie |
| Backend → Nominatim | HTTPS | — (Rate-Limit, gecached) |
| App → Dateisystem | Android SAF | OS-Permission |
---
## 4. Lösungsstrategie
| Entscheidung | ID | Begründung |
|---|---|---|
| Android: Kotlin + Jetpack Compose | DEC-CLIENT-01 | Android „Kotlin-first", moderne UI |
| Go Single Binary als Backend | — | Geringe Ressourcen, einfaches Deployment |
| Distroless Docker Image | — | Minimale Angriffsfläche |
| Schema via `go:embed` + `CREATE TABLE IF NOT EXISTS` | DEC-SCHEMA-01 | Keine Migrations-Infrastruktur, idempotenter Start |
| Room (SQLite) auf Android | DEC-DB-01 | Offline-First Persistenz |
| PostgreSQL im Backend | DEC-DB-01 | JSONB, UUID-Funktionen, zuverlässig |
| Session Cookie statt JWT (Web) | DEC-AUTH-01 | Serverseitige Invalidierung, einfacheres Threat Model |
| Argon2id für Passwort-Hashing | REQ-AUTH-01 | GPU-resistent, State-of-the-art |
| MapLibre + OpenStreetMap | DEC-MAP-01 | Open Source, keine Lizenzkosten, konfigurierbare Tiles |
| Nominatim als Geocoding-Provider | DEC-GEO-01 | Kostenlos, OSM-basiert, Provider austauschbar |
| Vanilla Web Components | — | Kein Framework-Overhead, langlebig, keine Lizenzkonflikte |
| OpenAPI 3.1 als API-Spec | DEC-OPENAPI-01 | Maschinenlesbar, Validierbar in CI |
---
## 5. Bausteinsicht
### Ebene 1 — Systeme
```mermaid
flowchart TB
subgraph Mobile[Android App — app/]
UI[Compose UI\nScreens + ViewModels]
Domain[Domain / Use-Cases]
LocalDB[(Room SQLite\nLocal DB + Queue)]
Worker[WorkManager\nUpload Worker]
Service[Foreground Service\nGPS Tracking]
Export[Export SAF]
end
subgraph Server[Go Backend — backend/]
Router[chi Router\n+ Middleware]
IngestAPI[Ingest API\nPOST /v1/trackpoints]
QueryAPI[Query API\nGET /v1/*]
WebUI[Web UI\nGo Templates SSR]
Auth[Auth Store\nArgon2id + Sessions]
Stores[DB Stores\nTrackpoints / Stops\nSuggestions / Journal]
StopEngine[Stop Detection\n+ Suggestions]
GeoAdapter[Geocoding Adapter\n+ Cache + RateLimit]
DB[(PostgreSQL)]
end
subgraph Web[Webapp — webapp/]
SPA[Vanilla TS SPA\nWeb Components]
Map[TrackMap\nMapLibre GL]
Nginx[nginx\nProxy + Static Files]
end
Mobile -->|HTTPS Batch| IngestAPI
SPA -->|HTTPS /v1/*| Nginx
Nginx -->|proxy_pass| Router
Router --> IngestAPI
Router --> QueryAPI
Router --> WebUI
Router --> Auth
IngestAPI --> Stores
QueryAPI --> Stores
WebUI --> Stores
Auth --> DB
Stores --> DB
StopEngine --> GeoAdapter
GeoAdapter -->|HTTPS| GEO[Nominatim]
```
### Ebene 2 — Backend-Pakete
```
backend/
├── cmd/
│ ├── server/ Einstiegspunkt: Pool → InitSchema → Router → HTTP Server
│ └── createuser/ CLI: User mit Argon2id-Hash anlegen
└── internal/
├── domain/ Shared Types: Trackpoint, Stop, Suggestion, Session, User
├── db/
│ ├── schema.sql Eingebettetes Schema (go:embed, IF NOT EXISTS)
│ ├── db.go NewPool + InitSchema
│ ├── trackpoints.go UpsertBatch, ListByDate, ListDays, EnsureDevice
│ ├── stops.go ListByDate
│ ├── suggestions.go ListByDate
│ └── journal.go CRUD Journal Entries + Images
├── auth/
│ └── auth.go HashPassword, VerifyPassword, Login, GetSession, Logout
└── api/
├── router.go chi Routing, Middleware-Gruppen
├── middleware.go RequireAuth (Session Cookie → Context)
├── ingest.go HandleSingleTrackpoint, HandleBatchTrackpoints
├── query.go HandleListDays, HandleListTrackpoints, Stops, Suggestions
├── webui.go Server-side rendered Web UI (Go Templates)
├── journal.go Journal Entry Endpoints
└── response.go writeJSON, writeError helpers
```
### Ebene 2 — Android-Pakete
```
app/app/src/main/kotlin/de/jacek/pamietnik/
├── domain/ Trackpoint Domain Model
├── data/ Room Entities, DAOs, lokale DB
├── service/ Background Location Foreground Service
├── worker/ WorkManager Upload Worker
└── ui/
├── home/ HomeScreen (Compose) + HomeViewModel
├── navigation/ NavGraph
└── theme/ Compose Theme
```
---
## 6. Laufzeitsicht
### R1 — Trip starten und Trackpoints senden
```mermaid
sequenceDiagram
participant App as Android App
participant Room as Room DB
participant Worker as WorkManager
participant API as Go Backend
participant DB as PostgreSQL
App->>Room: Trackpoint persistieren (source=gps, pending)
Worker->>Room: Pending Batch laden (NetworkConnected)
Worker->>API: POST /v1/trackpoints:batch + Cookie
API->>DB: EnsureDevice (INSERT ... ON CONFLICT DO NOTHING)
API->>DB: UpsertBatch (ON CONFLICT device+event_id DO NOTHING)
API-->>Worker: {accepted_ids, rejected: []}
Worker->>Room: Accepted als "sent" markieren
```
### R2 — Offline → Online Retry (REQ-SYNC-02, REQ-SYNC-04)
```mermaid
sequenceDiagram
participant Worker as WorkManager
participant Room as Room DB
participant API as Go Backend
Worker->>API: POST /v1/trackpoints:batch
API--xWorker: Netzwerkfehler / Timeout
Worker->>Room: Pending belassen, retryCount++
Note over Worker: Exponential Backoff\nConstraint: NetworkConnected
Worker->>API: POST /v1/trackpoints:batch (gleiche event_ids)
API-->>Worker: {accepted_ids} (Duplikate via ON CONFLICT ignoriert)
Worker->>Room: Accepted als "sent" markieren
```
### R3 — Web Login und Tagesansicht (REQ-WEB-01, REQ-WEB-02)
```mermaid
sequenceDiagram
participant B as Browser
participant Nginx as nginx :9050
participant API as Go Backend
participant DB as PostgreSQL
B->>Nginx: POST /login (username, password)
Nginx->>API: proxy_pass
API->>DB: User laden, Argon2id verify
API-->>B: Set-Cookie: session=..., 303 /days
B->>Nginx: GET /v1/days?from=&to= + Cookie
Nginx->>API: proxy_pass
API->>DB: trackpoints JOIN devices WHERE user_id = $1
API-->>B: [{date, count, first_ts, last_ts}]
B->>Nginx: GET /v1/trackpoints?date=YYYY-MM-DD + Cookie
API->>DB: trackpoints WHERE date = $2 AND user_id = $1
API-->>B: [Trackpoint...]
```
### R4 — Manuellen Punkt hinzufügen (REQ-MAN-01 bis REQ-MAN-05)
```mermaid
sequenceDiagram
participant U as Nutzer
participant VM as ViewModel
participant Room as Room DB
participant Worker as WorkManager
participant API as Go Backend
U->>VM: Eingabe lat/lon/timestamp/note/tags
VM-->>U: Validierung as-the-user-types
VM->>Room: Trackpoint (source=manual, tags=[...], pending)
Worker->>Room: Fetch pending
Worker->>API: POST /v1/trackpoints:batch
API-->>Worker: Ack
Worker->>Room: Mark sent
```
### R5 — Stop-Erkennung und Geocoding (REQ-SUG-01, REQ-GEO-01)
```mermaid
sequenceDiagram
participant API as Go Backend
participant SE as Stop Engine
participant GEO as Geocoding Adapter
participant NOM as Nominatim
participant DB as PostgreSQL
API->>SE: Trigger nach Batch-Ingest (async)
SE->>DB: Letzte Trackpoints lesen
SE->>SE: Stop erkennen (minDuration, radiusMeters)
SE->>GEO: Reverse-Geocode Stop-Koordinate
alt Cache Hit
GEO-->>SE: Place Info (gecached)
else Cache Miss
GEO->>NOM: HTTPS Reverse-Geocode
NOM-->>GEO: Place Info
GEO-->>SE: Place Info (jetzt gecached)
end
SE->>DB: Stop + Suggestion speichern
```
### R6 — Schema-Initialisierung beim Server-Start
```mermaid
sequenceDiagram
participant S as cmd/server
participant DB as PostgreSQL
S->>DB: pgxpool.New + Ping
S->>DB: schema.sql ausführen (go:embed)\nCREATE TABLE IF NOT EXISTS ...
Note over DB: Idempotent — bestehende\nTabellen bleiben erhalten
S->>S: Stores + Router aufbauen
S->>S: HTTP Server starten :8080
```
---
## 7. Verteilungssicht
```mermaid
flowchart LR
Phone[Android Device]
Browser[Web Browser]
subgraph Docker[Docker Compose]
Nginx[webapp\nnginx:alpine\n:9050]
API[api\ndistroless\n:8080 intern]
PG[postgres\npostgres:16-alpine\ninternal]
end
GEO[Nominatim\nexternal HTTPS]
Phone -->|HTTPS| Nginx
Browser -->|HTTPS| Nginx
Nginx -->|/v1/* /login /logout\nproxy_pass| API
Nginx -->|/ static SPA| Nginx
API --> PG
API -->|cached Rate-Limited| GEO
```
**Docker Compose Services:**
| Service | Image | Port extern | Funktion |
|---------|-------|-------------|----------|
| `postgres` | postgres:16-alpine | — (intern) | Datenhaltung |
| `api` | golang:1.25 → distroless | — (intern) | REST API + Web UI |
| `webapp` | node:22 → nginx:alpine | **9050** | SPA + API-Proxy |
**Start:**
```bash
docker-compose up --build
```
Schema wird beim API-Start automatisch initialisiert (keine separate Migration nötig).
---
## 8. Querschnittskonzepte
### Authentifizierung & Sessions (REQ-AUTH-01, REQ-AUTH-02, DEC-AUTH-01)
- Passwörter: Argon2id (`$argon2id$<saltHex>$<hashHex>`)
- Sessions in PostgreSQL (`sessions`-Tabelle): sofortige Invalidierung bei Logout
- Cookie: `HttpOnly`, `SameSite=Lax`, 24h Lebensdauer
- Android Upload-Auth: aktuell Session Cookie — **TBD: API-Key oder JWT** (DEC-AUTH-01)
### Idempotenz (REQ-SYNC-04)
- Jeder Trackpoint trägt eine client-generierte `event_id` (UUID empfohlen)
- Unique Constraint `(device_id, event_id)` in PostgreSQL
- `ON CONFLICT DO NOTHING` → doppelter Upload = 200 OK, kein Fehler
- Batch-Response enthält `accepted_ids` und `rejected[]` für Queue-Steuerung
### Offline-First (REQ-SYNC-01 bis REQ-SYNC-03)
- Jeder Punkt wird zuerst in Room (SQLite) persistiert
- Upload via WorkManager: `NetworkConnected`-Constraint, Exponential Backoff
- `source`-Feld unterscheidet `"gps"` und `"manual"`
### Device-Registrierung
- `EnsureDevice()` beim ersten Ingest: `INSERT ... ON CONFLICT DO NOTHING`
- Verknüpft `device_id` mit authentifiziertem `user_id`
- Query-Endpoints filtern über `devices.user_id`
### Hashtags (REQ-TAG-01 bis REQ-TAG-06)
- Feld `tags: string[]` im Trackpoint-Schema
- Eingabe-Vorschläge aus vorhandenen Tags des Nutzers
- Server: `GET /v1/tags` (Häufigkeit), `GET /v1/trackpoints?tag=...`
- Webapp: Tag-Filter in Tagesdetail, Tag-Übersicht
### Geocoding (REQ-GEO-01 bis REQ-GEO-03)
- Nur ereignisbasiert (pro Stop), niemals bulk/periodisch
- Cache vor jedem Provider-Call
- Provider über Config austauschbar (kein Hardcode)
- Nominatim: User-Agent korrekt setzen (Policy-Pflicht)
### Karten (DEC-MAP-01, REQ-MAP-01, REQ-MAP-02)
- Android: MapLibre SDK
- Webapp: MapLibre GL JS (open source, lizenzfrei)
- Tile-Quelle konfigurierbar (`tileUrl`-Property auf `<track-map>`)
---
## 9. Produktanforderungen (aus README)
### 3.1 Background-Logging
| ID | Anforderung |
|----|------------|
| REQ-LOC-01 | App MUSS Standort im Vordergrund und Hintergrund erfassen |
| REQ-LOC-02 | COARSE (optional FINE) im Manifest + Runtime; App funktioniert mit „approximate" |
| REQ-LOC-03 | Background-Location korrekt angefordert |
| REQ-LOC-04 | `foregroundServiceType="location"` wenn Foreground Service |
### 3.2 Offline-First
| ID | Anforderung |
|----|------------|
| REQ-SYNC-01 | Trackpoints zuerst lokal persistieren (kein RAM-Queue) |
| REQ-SYNC-02 | Upload bevorzugt; bei Fehler cachen und automatisch nachladen |
| REQ-SYNC-03 | WorkManager mit `NetworkConnected` + Retry/Backoff |
| REQ-SYNC-04 | Keine Duplikate bei Retries; Idempotenz via `event_id` |
### 3.3 Manuelle Punkte
| ID | Anforderung |
|----|------------|
| REQ-MAN-01 | Manueller Punkt: lat, lon, timestamp (Pflicht); Name/Notiz (optional) |
| REQ-MAN-02 | Lokal in Room speichern |
| REQ-MAN-03 | In Upload-Flow integriert (inkl. `event_id`) |
| REQ-MAN-04 | Validierung as-the-user-types; verständliche Fehlermeldungen |
| REQ-MAN-05 | Vorschlag „aktueller Zeitpunkt / aktuelle Position" (editierbar) |
### 3.4 Export
| ID | Anforderung |
|----|------------|
| REQ-EXP-01 | Export in Datei möglich |
| REQ-EXP-02 | Via SAF `ACTION_CREATE_DOCUMENT`; Nutzer wählt Speicherort |
### 3.5 Server Website
| ID | Anforderung |
|----|------------|
| REQ-WEB-01 | Website nur nach Login zugänglich |
| REQ-WEB-02 | Pro Tag Punkte anzeigen (Liste + Detailansicht) |
| REQ-WEB-03 | Daten nutzerspezifisch (ein Nutzer sieht nur eigene Daten) |
| REQ-WEB-04 | Kartenansicht (Punkte/Stops pro Tag) |
### 3.6 Security / Auth
| ID | Anforderung |
|----|------------|
| REQ-AUTH-01 | Passwörter NICHT im Klartext; Argon2id |
| REQ-AUTH-02 | Session Cookie; serverseitig verwaltbar (Logout/Expire) |
| REQ-AUTH-03 | Auth auch für Web-Query-Endpoints |
### 3.7 Stops / Vorschläge
| ID | Anforderung |
|----|------------|
| REQ-SUG-01 | Stop-Erkennung (Mindestdauer, Radius konfigurierbar) |
| REQ-SUG-02 | Vorschläge aus Stops ableiten |
| REQ-SUG-03 | Pro Nutzer/Device/Trip zuordenbar; in Website sichtbar |
### 3.8 Geocoding
| ID | Anforderung |
|----|------------|
| REQ-GEO-01 | Kostenloser Dienst (Nominatim); Caching + Rate-Limit |
| REQ-GEO-02 | Provider austauschbar via Config (ohne App-Update) |
| REQ-GEO-03 | Kein Bulk/periodisches Geocoding; ereignisbasiert (pro Stop) |
### 3.9 Karten
| ID | Anforderung |
|----|------------|
| REQ-MAP-01 | OpenStreetMap-Basis; MapLibre als Renderer |
| REQ-MAP-02 | Tile-Quelle konfigurierbar |
### 3.10 OpenAPI
| ID | Anforderung |
|----|------------|
| REQ-OPENAPI-01 | API als OpenAPI 3.1 dokumentiert |
| REQ-OPENAPI-02 | `openapi.yaml` im Repository versioniert |
| REQ-OPENAPI-03 | Alle Endpoints, Schemas, Fehler, Security-Schemes |
| REQ-OPENAPI-04 | CookieAuth via `type: apiKey, in: cookie` |
### 3.11 Google Play / Datenschutz
| ID | Anforderung |
|----|------------|
| REQ-PRIV-01 | Background-Location Disclosure im UI; korrekte Deklaration |
### 3.13 Hashtags
| ID | Anforderung |
|----|------------|
| REQ-TAG-01 | Trackpoints mit Hashtags versehen (`#restaurant`, ...) |
| REQ-TAG-02 | Tags lokal in Room + beim Upload übertragen (`tags: string[]`) |
| REQ-TAG-03 | Server persistiert Tags, abfragbar pro Trackpoint |
| REQ-TAG-04 | Webapp: Filter nach Hashtag |
| REQ-TAG-05 | Eingabe-Vorschläge aus vorhandenen Tags des Nutzers |
| REQ-TAG-06 | Webapp: Tag-Übersicht mit Häufigkeit |
---
## 10. Architekturentscheidungen
| ID | Entscheidung | Alternativen | Status |
|----|-------------|--------------|--------|
| DEC-CLIENT-01 | Android (Kotlin + Compose) | iOS, Flutter | Entschieden |
| DEC-LOC-01 | Background Location Logging | Nur Vordergrund | Entschieden |
| DEC-API-01 | HTTP/REST + JSON | MQTT, gRPC, Protobuf | Entschieden (Protobuf offen) |
| DEC-DB-01 | Room (Android) + PostgreSQL (Server) | SQLite Server, MySQL | Entschieden |
| DEC-WEB-01 | Website mit Login (SSR + SPA) | nur API | Entschieden |
| DEC-AUTH-01 | Session Cookie für Web; JWT optional | JWT only | Session entschieden |
| DEC-MAP-01 | MapLibre + OSM, konfigurierbare Tiles | Google Maps | Entschieden |
| DEC-GEO-01 | Nominatim, Provider austauschbar | Google Places API | Entschieden |
| DEC-OPENAPI-01 | OpenAPI 3.1 YAML | kein Spec | Entschieden |
| DEC-SCHEMA-01 | `go:embed` schema.sql, IF NOT EXISTS | golang-migrate | Entschieden |
---
## 11. Risiken und offene Punkte
### Implementierungsstatus
```mermaid
flowchart TD
subgraph Impl[Implementiert]
A[REST API Ingest\nPOST /v1/trackpoints]
B[Session Auth\nArgon2id + Cookie]
C[Tagesabfragen\nGET /v1/days etc.]
D[Web UI\nGo Templates SSR]
E[SPA Webapp\nMapLibre + WebComponents]
F[Schema Init\ngo:embed + IF NOT EXISTS]
G[Device-Registrierung\nEnsureDevice]
end
subgraph Backlog[Backlog / TBD]
H[Stop Detection\nT061]
I[Geocoding Adapter\nT063]
J[Suggestions Engine\nT062]
K[Android Auth\nAPI-Key / JWT T050]
L[OpenAPI Spec\nT070]
M[Hashtag-Support\nT073-T087]
end
subgraph Risiko[Sicherheit / Production]
N[CSRF-Schutz\nkritisch]
O[TLS / HTTPS\nhoch]
P[Rate-Limiting\nmittel]
Q[Secrets via .env\nkritisch]
R[Pagination\nhoch]
S[Security-Header\nnginx mittel]
end
style Impl fill:#d4edda,stroke:#28a745
style Backlog fill:#fff3cd,stroke:#ffc107
style Risiko fill:#f8d7da,stroke:#dc3545
```
### Security-Risiken (Architekt-Review)
```mermaid
quadrantChart
title Risiken — Schwere vs. Aufwand
x-axis Geringer Aufwand --> Hoher Aufwand
y-axis Geringes Risiko --> Hohes Risiko
quadrant-1 Sofort beheben
quadrant-2 Planen
quadrant-3 Beobachten
quadrant-4 Nice to have
CSRF-Schutz: [0.2, 0.9]
Secrets via .env: [0.1, 0.85]
TLS/HTTPS: [0.5, 0.8]
Rate-Limiting Login: [0.3, 0.75]
Pagination: [0.4, 0.6]
Android Auth: [0.7, 0.65]
Security-Header nginx: [0.15, 0.5]
Error-Disclosure: [0.2, 0.5]
EnsureDevice N+1: [0.35, 0.35]
DB Pool Config: [0.2, 0.4]
Stop Detection: [0.75, 0.5]
Geocoding: [0.8, 0.4]
OpenAPI Spec: [0.5, 0.2]
Hashtags: [0.65, 0.3]
```
### Produktionsreife
```mermaid
flowchart LR
DEV[Development\n✓ Lauffähig]
MVP[MVP\nFehlend: CSRF, TLS\nSecrets, Rate-Limit]
PROD[Production Ready\n+ Stop Detection\n+ Geocoding\n+ Monitoring\n+ Backup]
DEV -->|Kritische Security-Fixes| MVP
MVP -->|Business-Logik\n+ Observability| PROD
style DEV fill:#d4edda,stroke:#28a745
style MVP fill:#fff3cd,stroke:#ffc107
style PROD fill:#cce5ff,stroke:#004085
```
### Offene Entscheidungen
| Thema | Optionen | Auswirkung |
|-------|----------|-----------|
| `timestamp`-Format | epochMillis vs RFC3339 | Android + API Kompatibilität |
| Android Upload Auth | Session Cookie / API-Key / JWT | Security-Architektur |
| Payload | JSON vs Protobuf | Bandbreite auf Mobile |
| Batch-Limits | max Items / max Bytes | Denial-of-Service Schutz |
| Retention Policy | Löschen nach X Tagen | Storage-Kosten |
| Stop-Parameter | Mindestdauer, Radius | Qualität der Vorschläge |
| Geocoding Provider | Nominatim public / self-hosted | Datenschutz, Verfügbarkeit |

View File

@@ -2,45 +2,29 @@ services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_USER: ralph POSTGRES_USER: ${DB_USER:-pamietnik}
POSTGRES_PASSWORD: ralph POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
POSTGRES_DB: ralph POSTGRES_DB: ${DB_NAME:-pamietnik}
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ralph"] test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-pamietnik}"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
migrate: api:
build: ./backend build:
command: ["/migrate"] context: .
dockerfile: Dockerfile
ports:
- "9050:8080"
environment: environment:
DATABASE_URL: postgres://ralph:ralph@postgres:5432/ralph?sslmode=disable DATABASE_URL: postgres://${DB_USER:-pamietnik}:${DB_PASSWORD:?DB_PASSWORD is required}@postgres:5432/${DB_NAME:-pamietnik}?sslmode=disable
LISTEN_ADDR: :8080
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
restart: on-failure
api:
build: ./backend
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://ralph:ralph@postgres:5432/ralph?sslmode=disable
LISTEN_ADDR: :8080
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
webapp:
build: ./webapp
ports:
- "9050:80"
depends_on:
- api
restart: unless-stopped restart: unless-stopped
volumes: volumes:

7
webapp/.gitignore vendored
View File

@@ -1,2 +1,9 @@
node_modules/ node_modules/
dist/ dist/
.vite/
*.tsbuildinfo
# Environment
.env
.env.local
.env.*.local

View File

@@ -1,6 +1,6 @@
# Pamietnik Webapp # Pamietnik Webapp
Eigenständige Single-Page-Application für das Pamietnik-Reisejournal. Kommuniziert über REST mit dem Go-Backend. Eigenständige Single-Page-Application für das Pamietnik. Kommuniziert über REST mit dem Go-Backend.
## Technologie ## Technologie

View File

@@ -2,6 +2,16 @@ server {
listen 80; listen 80;
# API und Auth-Endpunkte zum Backend proxieren # API und Auth-Endpunkte zum Backend proxieren
location /healthz {
proxy_pass http://api:8080;
proxy_set_header Host $host;
}
location /readyz {
proxy_pass http://api:8080;
proxy_set_header Host $host;
}
location /v1/ { location /v1/ {
proxy_pass http://api:8080; proxy_pass http://api:8080;
proxy_set_header Host $host; proxy_set_header Host $host;

View File

@@ -50,9 +50,15 @@ async function get<T>(path: string): Promise<T> {
export const api = { export const api = {
getDays(from?: string, to?: string): Promise<DaySummary[]> { getDays(from?: string, to?: string): Promise<DaySummary[]> {
const params = new URLSearchParams() const now = new Date()
if (from) params.set('from', from) const defaultTo = now.toISOString().slice(0, 10)
if (to) params.set('to', to) const past = new Date(now)
past.setDate(past.getDate() - 90)
const defaultFrom = past.toISOString().slice(0, 10)
const params = new URLSearchParams({
from: from ?? defaultFrom,
to: to ?? defaultTo,
})
return get<DaySummary[]>(`/v1/days?${params}`) return get<DaySummary[]>(`/v1/days?${params}`)
}, },
getTrackpoints(date: string): Promise<Trackpoint[]> { getTrackpoints(date: string): Promise<Trackpoint[]> {

View File

@@ -7,7 +7,7 @@ export class AppShell extends HTMLElement {
connectedCallback(): void { connectedCallback(): void {
this.innerHTML = ` this.innerHTML = `
<nav> <nav>
<a href="/days" id="nav-days">Tage</a> <a href="/days" id="nav-days">Pamietnik</a>
<button id="nav-logout">Abmelden</button> <button id="nav-logout">Abmelden</button>
</nav> </nav>
<main id="outlet"></main> <main id="outlet"></main>

View File

@@ -1,4 +1,4 @@
import { api, DaySummary } from '../api' import { api } from '../api'
import { navigate } from '../router' import { navigate } from '../router'
export class DaysPage extends HTMLElement { export class DaysPage extends HTMLElement {

View File

@@ -1,17 +1,210 @@
/* System fonts only — no external dependencies, no license issues */
:root {
--font-mono: 'Courier New', Courier, monospace;
--font-serif: Georgia, 'Times New Roman', serif;
--color-ink: #1a1a1a;
--color-muted: #666;
--color-rule: #ddd;
--color-bg: #fafaf8;
--column: 680px;
}
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
html, body { html {
height: 100%; font-size: 18px;
font-family: system-ui, sans-serif; background: var(--color-bg);
font-size: 16px; color: var(--color-ink);
color: #1a1a1a; }
background: #f5f5f5;
body {
font-family: var(--font-serif);
line-height: 1.7;
min-height: 100vh;
} }
#app { #app {
height: 100%; min-height: 100vh;
}
/* --- Navigation --- */
nav {
border-bottom: 1px solid var(--color-rule);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
nav a {
color: var(--color-ink);
text-decoration: none;
}
nav a:hover {
text-decoration: underline;
}
nav button {
background: none;
border: none;
cursor: pointer;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted);
padding: 0;
}
nav button:hover {
color: var(--color-ink);
}
/* --- Main content column --- */
main {
max-width: var(--column);
margin: 0 auto;
padding: 3rem 1.5rem;
}
/* --- Typography --- */
h1 {
font-family: var(--font-mono);
font-size: 1rem;
font-weight: normal;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 2rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-rule);
}
h2 {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: normal;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted);
margin: 2.5rem 0 1rem;
}
p {
margin-bottom: 1rem;
color: var(--color-muted);
font-family: var(--font-mono);
font-size: 0.85rem;
}
/* --- Lists --- */
ul {
list-style: none;
}
li {
border-bottom: 1px solid var(--color-rule);
padding: 0.75rem 0;
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--color-ink);
}
li:first-child {
border-top: 1px solid var(--color-rule);
}
li a {
color: var(--color-ink);
text-decoration: none;
display: block;
}
li a:hover {
text-decoration: underline;
}
/* --- Login form --- */
form {
max-width: 360px;
}
form h1 {
margin-bottom: 2.5rem;
}
label {
display: block;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 1.5rem;
}
input[type="text"],
input[type="password"] {
display: block;
width: 100%;
margin-top: 0.4rem;
padding: 0.5rem 0;
border: none;
border-bottom: 1px solid var(--color-ink);
background: transparent;
font-family: var(--font-mono);
font-size: 0.9rem;
color: var(--color-ink);
outline: none;
}
input:focus {
border-bottom-color: var(--color-ink);
}
button[type="submit"] {
margin-top: 2rem;
background: var(--color-ink);
color: var(--color-bg);
border: none;
padding: 0.6rem 1.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
}
button[type="submit"]:hover {
opacity: 0.8;
}
#login-error {
color: #c00;
font-size: 0.8rem;
margin-top: -0.5rem;
margin-bottom: 1rem;
}
/* --- Map --- */
track-map {
display: block;
margin: 1.5rem 0;
border: 1px solid var(--color-rule);
} }