Query event:goal dimension with visitors, events, and conversion_rate
metrics via the existing POST /api/v2/query endpoint. Shows goal name,
unique conversions, and conversion rate percentage in a dedicated card.
- Add GoalConversion model with name, visitors, events, conversionRate
- Add 9th concurrent query in StatsRepository (try/catch for graceful
degradation when no goals are configured)
- Add GoalConversionsSection composable; hidden when no goals exist
- Add goalConversions to DashboardData with default emptyList() for
backward cache compatibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Load site favicons (svg → png → ico fallback) via Coil 3 with
SubcomposeAsyncImage; globe icon as final fallback
- Register SvgDecoder in ImplausiblyApp for SVG favicon support
- Add drag-to-reorder via sh.calvin.reorderable library with a
drag handle per site row; order persisted to sort_order column
- Add sort_order column to stored_sites with schema migration (1.sqm)
- New SiteRepository methods: reorderSites(), deleteSitesForInstance(),
getAllSites() now includes sort_order
- Dependencies: coil-compose, coil-network-okhttp, coil-svg,
reorderable (all Apache 2.0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the two-step instance/site switching flow with a single Site
Picker screen that shows all sites grouped by instance. One tap to
switch to any site regardless of which instance it belongs to.
- Add SitePickerScreen with grouped LazyColumn (instance headers +
site rows), inline add/edit/delete, current selection highlight
- Validate new site IDs against the Plausible API before adding
- Support deleting instances with cascading site removal
- Simplify Dashboard to single switch button
- Simplify Setup to add-only (no instance picker), with back arrow
- Remove old SiteListScreen/SiteListViewModel (replaced by SitePicker)
- Add selectAll query to StoredSite.sq, getAllSites/deleteSitesForInstance
to SiteRepository
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Opens {baseUrl}/{siteId} in the default browser so users can quickly
access the full Plausible web interface for their site.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove dead-end back arrow from dashboard; sites and instances
are accessible via globe and swap icons in the top bar
- Replace gear icon with swap icon (SwapHoriz) for switching instances
- Require successful connection test before enabling Save
- Add API key visibility toggle on setup form
- Add delete confirmation dialog on site list
- Fix chart double-modifier bug (height applied twice)
- Add empty states for chart ("No visitor data") and dimension
sections ("No data for this period") instead of silently hiding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous approach had two separate composable entries for setup and
setup-with-clone-params, which Jetpack Navigation couldn't distinguish.
Now uses one route with nullable query param arguments and defaultValue
null, so navigating to "setup" opens fresh and "setup?cloneInstanceId=
...&cloneSiteId=..." opens pre-filled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clone button on site list now navigates to setup screen pre-filled
with the source instance's URL, API key, and site ID. User can edit
all fields and save as a new instance. API key is resolved from
encrypted storage via the ViewModel, never passed through nav args.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copies the site ID into the add field so users can duplicate an
existing site and just change the ID. Useful when autodiscovery is
unavailable and multiple sites share the same instance credentials.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use flow.first() instead of collectAsState(initial = null) to
determine start destination. The previous approach fired immediately
with the initial null value before DataStore loaded from disk, always
routing to Setup. Now the loading screen suspends until preferences
are actually read.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 4 new dimension queries (concurrent with existing 4, total 8) to
the dashboard: countries, devices, browsers, and operating systems.
All reuse the existing DimensionSection component with proportional
progress bars.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix Plausible API v2 compatibility: move `limit` into `pagination`
object, add `expectSuccess = true` to Ktor client so API errors are
surfaced instead of "Illegal input" serialization errors, configure
`explicitNulls = false` to avoid sending null fields
- Add smart start destination: app checks DataStore on launch and
navigates directly to dashboard if an instance/site was previously
selected, skipping the setup screen
- Add settings and sites icons to dashboard top bar for navigating
back to instance management and site list
- Add site editing (inline rename) to site list screen
- Fix DateRangeSelector crash caused by sealed class data object
initialization issue in Compose lambda captures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>