implausible/CLAUDE.md
Ole-Morten Duesund aa66172d58 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>
2026-03-18 16:46:08 +01:00

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 (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.