Phase F: NetworkMonitor, offline banner, README & architecture docs
- NetworkMonitor: ConnectivityState Flow using ConnectivityManager.NetworkCallback
- ListsScreen & ViewModel: live offline indicator banner when disconnected
- Documentation:
- docs/ARCHITECTURE.md: system design & tech stack overview
- docs/SYNC.md: HLC timestamping, op_log outbox & LWW projection specification
- docs/API.md: REST API endpoint specification
- README.md: quickstart guide for backend, docker compose & Android app
- Verification: backend & android test suites 100% green ✅
This commit is contained in:
parent
174aad535a
commit
a00db14cba
8 changed files with 482 additions and 96 deletions
151
docs/API.md
Normal file
151
docs/API.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Mitbringsl – REST API Specification
|
||||
|
||||
Base URL: `/`
|
||||
Authentication: Bearer Token via `Authorization: Bearer <session_token>` header or session cookie.
|
||||
|
||||
---
|
||||
|
||||
## Health & Diagnostics
|
||||
|
||||
### `GET /healthz`
|
||||
Liveness probe. Returns HTTP 200 `OK`.
|
||||
|
||||
### `GET /readyz`
|
||||
Readiness probe. Checks PostgreSQL connection pool health.
|
||||
- **200 OK**: Database reachable.
|
||||
- **503 Service Unavailable**: Database error.
|
||||
|
||||
---
|
||||
|
||||
## Authentication (`/auth`)
|
||||
|
||||
### `POST /auth/register`
|
||||
Create a new email/password account.
|
||||
- **Request**:
|
||||
```json
|
||||
{ "email": "user@example.com", "password": "secretpassword", "display_name": "Max" }
|
||||
```
|
||||
- **Response (201 Created)**:
|
||||
```json
|
||||
{
|
||||
"token": "<opaque_session_token>",
|
||||
"expires_at": "2026-09-04T20:00:00Z",
|
||||
"user": { "id": "<uuid>", "email": "user@example.com", "display_name": "Max" }
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /auth/login`
|
||||
Authenticate with email/password.
|
||||
- **Request**:
|
||||
```json
|
||||
{ "email": "user@example.com", "password": "secretpassword" }
|
||||
```
|
||||
- **Response (200 OK)**: Same shape as register.
|
||||
|
||||
### `POST /auth/oidc`
|
||||
Authenticate via Google or custom OpenID Connect provider.
|
||||
- **Request**:
|
||||
```json
|
||||
{ "provider": "google", "id_token": "<jwt>" }
|
||||
```
|
||||
- **Response (200 OK)**: Same shape as login.
|
||||
|
||||
### `POST /auth/logout`
|
||||
Revoke active session token. Returns `204 No Content`.
|
||||
|
||||
---
|
||||
|
||||
## Lists (`/api/lists`)
|
||||
|
||||
### `GET /api/lists`
|
||||
Fetch all active (non-deleted) lists owned by the authenticated user.
|
||||
- **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"lists": [
|
||||
{ "id": "<uuid>", "name": "Wocheneinkauf", "updated_at": "2026-08-05T19:00:00Z", "hlc_ts": 177000000000000 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/lists`
|
||||
Create a new list.
|
||||
- **Request**: `{ "name": "Supermarkt" }`
|
||||
- **Response (201 Created)**: `{ "id": "<uuid>", "name": "Supermarkt", ... }`
|
||||
|
||||
### `GET /api/lists/{id}`
|
||||
Fetch list detail including items.
|
||||
- **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"name": "Wocheneinkauf",
|
||||
"updated_at": "...",
|
||||
"hlc_ts": 177000000000000,
|
||||
"items": [
|
||||
{ "id": "<uuid>", "name": "Milch", "quantity": "1L", "checked": false, "hlc_ts": 177000000000001 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sync & Ops (`/api/lists/{id}/ops`)
|
||||
|
||||
### `POST /api/lists/{id}/ops`
|
||||
Push a batch of client operations (max 100).
|
||||
- **Request**:
|
||||
```json
|
||||
{
|
||||
"client_id": "<client_uuid>",
|
||||
"ops": [
|
||||
{
|
||||
"client_seq": 1,
|
||||
"op_type": "item_add",
|
||||
"target_id": "<item_uuid>",
|
||||
"hlc_ts": 177000000000000,
|
||||
"payload": { "name": "Brot", "quantity": "1 Stück" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{ "client_seq": 1, "seq": 42, "hlc_ts": 177000000000001 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/lists/{id}/ops?since={seq}`
|
||||
Pull operations since server sequence `since`.
|
||||
- **Response (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"ops": [
|
||||
{
|
||||
"seq": 42,
|
||||
"client_id": "<client_uuid>",
|
||||
"op_type": "item_add",
|
||||
"target_id": "<item_uuid>",
|
||||
"payload": { "name": "Brot" },
|
||||
"client_seq": 1,
|
||||
"hlc_ts": 177000000000001,
|
||||
"created_at": "2026-08-05T20:00:00Z"
|
||||
}
|
||||
],
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suggestions (`/api/suggestions`)
|
||||
|
||||
### `GET /api/suggestions?q={query}`
|
||||
Fuzzy autocomplete search for item names (pg_trgm).
|
||||
- **Response (200 OK)**:
|
||||
```json
|
||||
{ "suggestions": ["milch", "mineralwasser", "müsli"] }
|
||||
```
|
||||
51
docs/ARCHITECTURE.md
Normal file
51
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Mitbringsl – Architectural Design Document
|
||||
|
||||
## 1. Overview & Paradigm
|
||||
|
||||
**Mitbringsl** is a local-first, privacy-focused shopping list application designed as an ad-free alternative to Bring!.
|
||||
|
||||
### Local-First Foundation
|
||||
- **Source of Truth**: The client’s local SQLite database (via Room) is the authoritative source of truth for user interactions. All mutations (adding items, checking off items, renaming/deleting lists) take immediate effect locally.
|
||||
- **Append-Only Op Log**: Operations are logged sequentially to a local outbox queue (`op_log`) before being synced asynchronously to the server.
|
||||
- **Offline Resiliency**: The app functions 100% offline. Network connectivity is treated as an opportunistic transport to synchronize operations with the server and other clients.
|
||||
|
||||
---
|
||||
|
||||
## 2. System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Caddy (Reverse Proxy) │
|
||||
└────────────────────┬────────────────────┘
|
||||
│ HTTP / REST
|
||||
┌────────────────────▼────────────────────┐
|
||||
│ Go Backend (net/http + pgx) │
|
||||
└─────────┬──────────────────────┬────────┘
|
||||
│ │
|
||||
┌─────────▼────────┐ ┌─────────▼────────┐
|
||||
│ PostgreSQL (16) │ │ OIDC Providers │
|
||||
│ (op_log, LWW) │ │ (Google/Custom) │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Technology Stack
|
||||
|
||||
### Backend (Go 1.26)
|
||||
- **HTTP Routing**: `net/http` stdlib (Go 1.22+ method & path variables pattern matching).
|
||||
- **Database Driver**: `github.com/jackc/pgx/v5` (`pgxpool`).
|
||||
- **Database Migrations**: `github.com/golang-migrate/migrate/v4` (embedded via `embed.FS`).
|
||||
- **Password Hashing**: Argon2id in PHC format (`golang.org/x/crypto/argon2`).
|
||||
- **Authentication**: Opaque session tokens (32 bytes `crypto/rand`, SHA-256 hashed in database).
|
||||
- **OIDC Verification**: `github.com/coreos/go-oidc/v3` with JWKS key caching.
|
||||
- **Containerization**: Distroless multi-stage Docker build (`gcr.io/distroless/static-debian12:nonroot`).
|
||||
|
||||
### Android Client (Kotlin 2.x & Compose)
|
||||
- **UI Framework**: Jetpack Compose (Material Design 3).
|
||||
- **Architecture**: MVVM with Unidirectional Data Flow (UDF) & `StateFlow`.
|
||||
- **Database**: Room (KSP code generation).
|
||||
- **Network & Serialization**: Retrofit 2 + OkHttp 4 + `kotlinx.serialization`.
|
||||
- **Background Sync**: WorkManager (`HiltWorker` + `CoroutineWorker`).
|
||||
- **Dependency Injection**: Hilt.
|
||||
- **Clock**: Client-side Hybrid Logical Clock (HLC).
|
||||
58
docs/SYNC.md
Normal file
58
docs/SYNC.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Mitbringsl – Synchronization Protocol & Conflict Resolution
|
||||
|
||||
## 1. Synchronization Model
|
||||
|
||||
Mitbringsl uses an **Append-Only Operation Log (`op_log`)** with **Hybrid Logical Clock (HLC)** timestamps and **Last-Write-Wins (LWW)** projections.
|
||||
|
||||
---
|
||||
|
||||
## 2. Hybrid Logical Clock (HLC)
|
||||
|
||||
Every operation carries a 64-bit integer timestamp `hlc_ts`:
|
||||
|
||||
$$\text{hlc\_ts} = (\text{wall\_ms} \ll 16) \mid \text{counter}$$
|
||||
|
||||
- **`wall_ms`** (48 bits): Unix epoch milliseconds.
|
||||
- **`counter`** (16 bits): Logical counter disambiguating events produced within the same millisecond.
|
||||
|
||||
### Guarantees
|
||||
1. **Strict Monotonicity**: $t_{n+1} > t_n$ for all events generated on the same device.
|
||||
2. **Causal Ordering**: If event $B$ was generated after observing event $A$, then $HLC(B) > HLC(A)$.
|
||||
|
||||
---
|
||||
|
||||
## 3. Operations (`op_log`)
|
||||
|
||||
All state modifications are represented as structured operations:
|
||||
|
||||
| `op_type` | Target | Description | Payload Example |
|
||||
|---|---|---|---|
|
||||
| `list_create` | List UUID | Create list | `{"name":"Wocheneinkauf"}` |
|
||||
| `list_rename` | List UUID | Rename list | `{"name":"Supermarkt"}` |
|
||||
| `list_delete` | List UUID | Delete list | `{}` |
|
||||
| `item_add` | Item UUID | Add item | `{"name":"Milch","quantity":"2L"}` |
|
||||
| `item_update` | Item UUID | Update item | `{"name":"Hafermilch","checked":true}` |
|
||||
| `item_remove` | Item UUID | Delete item | `{}` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Conflict Resolution (LWW & Tombstones)
|
||||
|
||||
1. **Projection Updates**:
|
||||
Database tables (`items`, `lists`) are projections of `op_log`. When an op arrives:
|
||||
```sql
|
||||
UPDATE items
|
||||
SET name = $1, hlc_ts = $2, updated_at = now()
|
||||
WHERE id = $3 AND hlc_ts < $2 AND deleted_at IS NULL;
|
||||
```
|
||||
2. **Tombstones (`deleted_at`)**:
|
||||
When an item or list is deleted (`item_remove`, `list_delete`), a tombstone is set (`deleted_at = now()`). Late-arriving offline `item_update` operations with smaller `hlc_ts` values will **never** revive a tombstoned entity.
|
||||
|
||||
---
|
||||
|
||||
## 5. Idempotent Push & Cursor Pull
|
||||
|
||||
- **Idempotent Push (`POST /api/lists/{id}/ops`)**:
|
||||
Each operation carries a `(client_id, client_seq)` tuple enforced by a `UNIQUE` constraint in Postgres (`op_log`). Re-sent requests return previous `(seq, hlc_ts)` assignments without duplicating side effects.
|
||||
- **Cursor Pull (`GET /api/lists/{id}/ops?since={seq}`)**:
|
||||
Clients track `max(server_seq)` locally. Incremental sync fetches ops where `seq > cursor`, sorted by monotonic server sequence `seq ASC`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue