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>
4.6 KiB
4.6 KiB
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
# 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(singlePOST /api/v2/queryendpoint), DTOs, auth interceptordata/local/— SQLDelight.sqschemas (CachedStats,Instancetables)data/repository/—StatsRepository,SiteRepository,InstanceRepository,CacheManagerdata/—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 / pickerui/dashboard/— Main stats dashboard, components (StatCard,VisitorChart), sectionsui/filter/— Filter bottom sheetui/theme/— Material 3 theme, typography, colorsui/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 cachedtoday→ 5 min7d/30d→ 30 min6mo/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:dependenciesbefore 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-onlyin 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.