Compare commits
10 Commits
304ee6d0b5
...
de6e387c7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de6e387c7b | ||
|
|
23506bab7d | ||
|
|
a49416854e | ||
|
|
d1436abca8 | ||
|
|
9775a22473 | ||
|
|
5abfa29e91 | ||
|
|
bf89ef01c7 | ||
|
|
bc2fa7b966 | ||
|
|
48ff7104da | ||
|
|
909e9b6813 |
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
4
.env.example
Normal 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
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Environment / Secrets
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
@@ -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
24
Dockerfile
Normal 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"]
|
||||||
51
README.md
51
README.md
@@ -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
9
app/.gitignore
vendored
@@ -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/
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.*
|
||||||
|
|||||||
@@ -25,5 +25,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "reisejournal"
|
rootProject.name = "pamietnik"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
2
backend
2
backend
Submodule backend updated: 55d2ffc203...dbcb0d4a09
632
doc/architecture.md
Normal file
632
doc/architecture.md
Normal 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 |
|
||||||
@@ -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
7
webapp/.gitignore
vendored
@@ -1,2 +1,9 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
.vite/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[]> {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user