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:
commit
aa66172d58
69 changed files with 4778 additions and 0 deletions
105
CLAUDE.md
Normal file
105
CLAUDE.md
Normal 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue