feat: implement Phase 1 MVP of Implausibly

Working Android dashboard app for self-hosted Plausible Analytics CE.
Connects to Plausible API v2 (POST /api/v2/query), displays top stats,
visitor chart, top sources, and top pages with date range selection.

Architecture: Kotlin + Jetpack Compose + Material 3 + Hilt + Ktor +
SQLDelight + EncryptedSharedPreferences. Single :app module, four-layer
unidirectional data flow (UI → ViewModel → Repository → Data).

Includes: instance management, site list, caching with TTL per date
range, encrypted API key storage, custom Canvas visitor chart,
pull-to-refresh, and unit tests for API, cache, repository, and
domain model layers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-18 16:46:08 +01:00
commit aa66172d58
69 changed files with 4778 additions and 0 deletions

105
CLAUDE.md Normal file
View file

@ -0,0 +1,105 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**Implausibly** — an open-source Android dashboard app for self-hosted Plausible Analytics CE.
- **Package:** `no.naiv.implausibly`
- **Stack:** Kotlin, Jetpack Compose, Material 3
- **License:** GPL-3.0-only
- **Target:** API 26+ (Android 8.0)
- **Distribution:** F-Droid (primary), Forgejo Releases on kode.naiv.no — no Google Play
## Build & Development Commands
```bash
# Build (requires gradle wrapper JAR — run once from a machine with Gradle installed)
gradle wrapper --gradle-version 8.10.2
# Standard build/test commands
./gradlew assembleDebug
./gradlew test # Unit tests
./gradlew connectedAndroidTest # Instrumented tests
./gradlew lint # Android lint
./gradlew :app:dependencies # Audit dependency tree (must be free of Play Services)
```
## Architecture
Four-layer architecture with unidirectional data flow:
```
UI (Jetpack Compose + Material 3)
↓ observes StateFlow
ViewModel (per screen, sealed UiState classes)
↓ calls
Repository (single source of truth, coordinates API + cache)
↓ delegates to
Remote (Ktor) + Local Cache (SQLDelight)
```
### Key technology choices
| Component | Library | Rationale |
|-----------|---------|-----------|
| HTTP | **Ktor** | Pure Kotlin, no annotation processing; we only call one endpoint |
| Serialization | **kotlinx.serialization** | Compiler plugin, no reflection |
| Local DB | **SQLDelight** | SQL-first `.sq` files → typesafe Kotlin; KMP-ready for future iOS |
| DI | **Hilt** | Compile-time safe, Android standard |
| Preferences | **DataStore** | Non-sensitive prefs (theme, selected site) |
| Secrets | **EncryptedSharedPreferences** | API keys via MasterKey + AES-256-GCM (AndroidX Security Crypto / Tink) |
| Charts | **Vico 1.x** | Compose-native; uses `CartesianChartHost` + `CartesianChartModelProducer` API |
### Package structure (`no.naiv.implausibly/`)
- `data/remote/``PlausibleApi` (single `POST /api/v2/query` endpoint), DTOs, auth interceptor
- `data/local/` — SQLDelight `.sq` schemas (`CachedStats`, `Instance` tables)
- `data/repository/``StatsRepository`, `SiteRepository`, `InstanceRepository`, `CacheManager`
- `data/``ApiKeyStore` (encrypted), `AppPreferences` (DataStore)
- `domain/model/` — Domain models decoupled from API DTOs (`AccessMode`, `PlausibleInstance`, `Site`, `DateRange`, `DashboardData`)
- `di/` — Hilt modules (`DatabaseModule`, `NetworkModule`)
- `ui/setup/` — Onboarding + instance management (add/edit/delete/switch)
- `ui/sites/` — Site list / picker
- `ui/dashboard/` — Main stats dashboard, components (`StatCard`, `VisitorChart`), sections
- `ui/filter/` — Filter bottom sheet
- `ui/theme/` — Material 3 theme, typography, colors
- `ui/navigation/` — NavHost, routes
## API Integration
Everything goes through **one endpoint**: `POST /api/v2/query` on the configured Plausible instance.
- Dashboard loads fire ~8 concurrent queries via `coroutineScope { async }` — one failure cancels all
- Rate limit: 600 req/hr (generous for dashboard use)
- Realtime: same endpoint with `"date_range": "realtime"`, polled every 30s in foreground
- Three access modes: full API key, stats-only key, public shared link (no key)
- API docs: https://plausible.io/docs/stats-api/query
## Caching (SQLDelight)
Cache key = hash of `(instanceId, siteId, queryBody)`. TTL varies by date range:
- `realtime` → never cached
- `today` → 5 min
- `7d`/`30d` → 30 min
- `6mo`/`12mo` → 2 hours
- Custom past range → 24 hours
Show cached data immediately on app open, refresh in background.
## Critical Constraints
- **Zero Play Services dependencies** — audit `./gradlew :app:dependencies` before every release. AndroidX Security Crypto uses Tink (Apache 2.0, pure Java), not Play.
- **All dependencies must be GPL-3.0-compatible** (Apache 2.0, MIT, LGPL OK; no AGPL, no proprietary)
- **F-Droid reproducible builds** — pin all dependency versions in `gradle/libs.versions.toml`, use locked Gradle wrapper
- **No telemetry** — zero analytics, nothing phones home
- **SPDX header** `GPL-3.0-only` in every source file
- **HTTPS enforced** for all network calls; no default certificate pinning (self-hosted users may use custom CAs)
## Gradle Configuration
- Version catalog: `gradle/libs.versions.toml` (all versions pinned)
- Convention plugins in `build-logic/` (`android.convention`, `compose.convention`)
- Gradle 8.10.2, Kotlin 2.1.0, AGP 8.7.3
## Repository Hosting
This project is hosted on Forgejo at `kode.naiv.no`. Use `fj` (Forgejo CLI) for PRs, issues, releases — not `gh`.