16 KiB
Sub2API Monitor Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Build a runnable native Android prototype with a Compose configuration screen and a Glance home-screen widget for monitoring Sub2API using mock data.
Architecture: Create a single-module Android app. Keep business state in small Kotlin domain models and repository interfaces, persist configuration and cached widget state through DataStore, render the app with Compose, and render the home-screen widget with Glance. The first data source is mock-only, but the repository boundary must allow a future real Sub2API admin client to replace it.
Tech Stack: Kotlin, Gradle Android plugin, Jetpack Compose, Jetpack Glance, DataStore, kotlinx.serialization, WorkManager boundary, JUnit.
Task 1: Scaffold Android Project
Files:
- Create:
settings.gradle.kts - Create:
build.gradle.kts - Create:
gradle.properties - Create:
app/build.gradle.kts - Create:
app/src/main/AndroidManifest.xml - Create:
app/src/main/java/com/sub2api/monitor/MainActivity.kt - Create:
app/src/test/java/com/sub2api/monitor/SmokeTest.kt
Step 1: Write the failing test
Create app/src/test/java/com/sub2api/monitor/SmokeTest.kt:
package com.sub2api.monitor
import org.junit.Assert.assertEquals
import org.junit.Test
class SmokeTest {
@Test
fun projectRunsUnitTests() {
assertEquals(4, 2 + 2)
}
}
Step 2: Run test to verify project wiring
Run: ./gradlew :app:testDebugUnitTest
Expected before scaffolding is complete: command fails because no Android project or Gradle wrapper exists.
Step 3: Write minimal implementation
Create the Gradle Android project, Android manifest, and a minimal MainActivity that renders a Compose Text("Sub2API Monitor").
Use package name:
com.sub2api.monitor
Use minimum SDK 26 and target/compile SDK available in the local Android toolchain.
Step 4: Run test to verify it passes
Run: ./gradlew :app:testDebugUnitTest
Expected: BUILD SUCCESSFUL.
Step 5: Commit
git add settings.gradle.kts build.gradle.kts gradle.properties app
git commit -m "chore: scaffold Android app"
Task 2: Add Domain Models and Formatting
Files:
- Create:
app/src/main/java/com/sub2api/monitor/domain/Sub2ApiModels.kt - Create:
app/src/main/java/com/sub2api/monitor/domain/MetricFormatters.kt - Create:
app/src/test/java/com/sub2api/monitor/domain/MetricFormattersTest.kt
Step 1: Write the failing tests
Create tests for formatting behavior:
package com.sub2api.monitor.domain
import org.junit.Assert.assertEquals
import org.junit.Test
class MetricFormattersTest {
@Test
fun formatsLargeTokenCounts() {
assertEquals("12.3K", formatTokens(12_345))
assertEquals("9.9M", formatTokens(9_876_543))
}
@Test
fun formatsCurrencyWithDollarPrefix() {
assertEquals("$3.21", formatCurrency(3.214))
}
@Test
fun formatsLatencyAndRates() {
assertEquals("248 ms", formatLatency(248))
assertEquals("42 RPM", formatRpm(42))
assertEquals("18.5K TPM", formatTpm(18_500))
}
}
Step 2: Run tests to verify they fail
Run: ./gradlew :app:testDebugUnitTest --tests "*MetricFormattersTest"
Expected: FAIL because formatter functions do not exist.
Step 3: Write minimal implementation
Create immutable domain data classes:
data class Sub2ApiSnapshot(...)
data class RecentCall(...)
data class ModelUsage(...)
enum class ServiceStatus { Healthy, Degraded, Down }
Add formatter functions for tokens, currency, latency, RPM, TPM, and display time.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*MetricFormattersTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/domain app/src/test/java/com/sub2api/monitor/domain
git commit -m "feat: add monitoring domain models"
Task 3: Add Configuration Storage
Files:
- Create:
app/src/main/java/com/sub2api/monitor/data/AppConfig.kt - Create:
app/src/main/java/com/sub2api/monitor/data/ConfigRepository.kt - Create:
app/src/main/java/com/sub2api/monitor/data/DataStoreConfigRepository.kt - Create:
app/src/test/java/com/sub2api/monitor/data/ConfigRepositoryTest.kt
Step 1: Write the failing tests
Test pure configuration validation first:
package com.sub2api.monitor.data
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ConfigRepositoryTest {
@Test
fun detectsMissingConfiguration() {
assertFalse(AppConfig().isConfigured)
}
@Test
fun detectsSavedConfiguration() {
val config = AppConfig(
baseUrl = "https://sub2api.example.com",
adminKey = "secret",
refreshIntervalMinutes = 30,
)
assertTrue(config.isConfigured)
}
}
Step 2: Run tests to verify they fail
Run: ./gradlew :app:testDebugUnitTest --tests "*ConfigRepositoryTest"
Expected: FAIL because AppConfig does not exist.
Step 3: Write minimal implementation
Create AppConfig with defaults and isConfigured.
Create repository interface:
interface ConfigRepository {
val config: Flow<AppConfig>
suspend fun save(config: AppConfig)
}
Implement DataStoreConfigRepository with preferences DataStore. Keep admin key local and never expose it outside config/repository boundaries.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*ConfigRepositoryTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/data app/src/test/java/com/sub2api/monitor/data
git commit -m "feat: add local configuration storage"
Task 4: Add Mock Monitoring Repository
Files:
- Create:
app/src/main/java/com/sub2api/monitor/data/Sub2ApiRepository.kt - Create:
app/src/main/java/com/sub2api/monitor/data/MockSub2ApiRepository.kt - Create:
app/src/test/java/com/sub2api/monitor/data/MockSub2ApiRepositoryTest.kt
Step 1: Write the failing tests
package com.sub2api.monitor.data
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class MockSub2ApiRepositoryTest {
@Test
fun returnsCompleteSnapshot() = runTest {
val snapshot = MockSub2ApiRepository().fetchSnapshot()
assertTrue(snapshot.todayTokens > 0)
assertTrue(snapshot.todayCost > 0.0)
assertEquals(5, snapshot.recentCalls.size)
assertEquals(4, snapshot.modelTop.size)
}
}
Step 2: Run tests to verify they fail
Run: ./gradlew :app:testDebugUnitTest --tests "*MockSub2ApiRepositoryTest"
Expected: FAIL because repository does not exist.
Step 3: Write minimal implementation
Create:
interface Sub2ApiRepository {
suspend fun fetchSnapshot(): Sub2ApiSnapshot
suspend fun testConnection(config: AppConfig): Result<Unit>
}
Implement MockSub2ApiRepository returning realistic but deterministic data for the widget and app.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*MockSub2ApiRepositoryTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/data app/src/test/java/com/sub2api/monitor/data
git commit -m "feat: add mock Sub2API repository"
Task 5: Add Widget State and Refresh Behavior
Files:
- Create:
app/src/main/java/com/sub2api/monitor/widget/WidgetState.kt - Create:
app/src/main/java/com/sub2api/monitor/widget/WidgetStateRepository.kt - Create:
app/src/main/java/com/sub2api/monitor/widget/Sub2ApiWidgetViewModel.kt - Create:
app/src/test/java/com/sub2api/monitor/widget/Sub2ApiWidgetViewModelTest.kt
Step 1: Write the failing tests
package com.sub2api.monitor.widget
import com.sub2api.monitor.data.AppConfig
import com.sub2api.monitor.data.MockSub2ApiRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
class Sub2ApiWidgetViewModelTest {
@Test
fun missingConfigShowsSetupPrompt() = runTest {
val state = buildWidgetState(AppConfig(), null, null)
assertFalse(state.isConfigured)
}
@Test
fun failedRefreshPreservesLastSuccessfulSnapshot() = runTest {
val previous = MockSub2ApiRepository().fetchSnapshot()
val state = buildWidgetState(
config = AppConfig("https://example.com", "secret", 30),
lastSuccessfulSnapshot = previous,
errorMessage = "Network error",
)
assertTrue(state.isConfigured)
assertNotNull(state.snapshot)
assertTrue(state.hasError)
}
}
Step 2: Run tests to verify they fail
Run: ./gradlew :app:testDebugUnitTest --tests "*Sub2ApiWidgetViewModelTest"
Expected: FAIL because widget state functions do not exist.
Step 3: Write minimal implementation
Create widget state models:
data class Sub2ApiWidgetState(
val isConfigured: Boolean,
val snapshot: Sub2ApiSnapshot?,
val errorMessage: String?,
)
Add buildWidgetState to map config, last successful snapshot, and error into renderable state. Add repository methods for reading/writing last successful snapshot and last error.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*Sub2ApiWidgetViewModelTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/widget app/src/test/java/com/sub2api/monitor/widget
git commit -m "feat: add widget state handling"
Task 6: Build Compose Configuration Screen
Files:
- Create:
app/src/main/java/com/sub2api/monitor/ui/ConfigScreen.kt - Create:
app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt - Modify:
app/src/main/java/com/sub2api/monitor/MainActivity.kt - Create:
app/src/test/java/com/sub2api/monitor/ui/ConfigViewModelTest.kt
Step 1: Write the failing tests
Test ViewModel-level behavior:
package com.sub2api.monitor.ui
import com.sub2api.monitor.data.AppConfig
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class ConfigViewModelTest {
@Test
fun updatesFormFields() = runTest {
val state = ConfigUiState()
.withBaseUrl("https://sub2api.example.com")
.withAdminKey("secret")
.withRefreshInterval("30")
assertEquals("https://sub2api.example.com", state.baseUrl)
assertEquals("secret", state.adminKey)
assertEquals("30", state.refreshIntervalMinutes)
}
}
Step 2: Run tests to verify they fail
Run: ./gradlew :app:testDebugUnitTest --tests "*ConfigViewModelTest"
Expected: FAIL because UI state does not exist.
Step 3: Write minimal implementation
Create a Compose screen with:
- Title
Sub2API Monitor - URL text field
- Password admin key field
- Refresh interval input
- Test connection button
- Save configuration button
- Status message area
Use white/soft dashboard styling consistent with the widget. Wire MainActivity to display ConfigScreen.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*ConfigViewModelTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/ui app/src/main/java/com/sub2api/monitor/MainActivity.kt app/src/test/java/com/sub2api/monitor/ui
git commit -m "feat: add configuration screen"
Task 7: Build Glance Widget UI
Files:
- Create:
app/src/main/java/com/sub2api/monitor/widget/Sub2ApiMonitorWidget.kt - Create:
app/src/main/java/com/sub2api/monitor/widget/Sub2ApiMonitorWidgetReceiver.kt - Create:
app/src/main/res/xml/sub2api_monitor_widget.xml - Modify:
app/src/main/AndroidManifest.xml
Step 1: Write the failing test
Add a JVM test for the widget content source:
package com.sub2api.monitor.widget
import com.sub2api.monitor.data.AppConfig
import org.junit.Assert.assertEquals
import org.junit.Test
class WidgetStateTextTest {
@Test
fun missingConfigUsesSetupPrompt() {
val state = buildWidgetState(AppConfig(), null, null)
assertEquals("请先配置 Sub2API", state.primaryMessage)
}
}
Step 2: Run test to verify it fails
Run: ./gradlew :app:testDebugUnitTest --tests "*WidgetStateTextTest"
Expected: FAIL because primaryMessage does not exist.
Step 3: Write minimal implementation
Add primaryMessage to widget state and build the Glance widget:
- White translucent rounded card.
- Header with title, last update, refresh action.
- Metric blocks for today's usage, cost, requests, status.
- Secondary metrics.
- Recent calls list.
- Model TOP4 list.
- Lifetime totals.
- Error banner when last refresh failed.
Register widget receiver and widget provider XML in the manifest.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*WidgetStateTextTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/widget app/src/main/res/xml/sub2api_monitor_widget.xml app/src/main/AndroidManifest.xml
git commit -m "feat: add Sub2API home screen widget"
Task 8: Add Refresh Actions and Scheduler Boundary
Files:
- Create:
app/src/main/java/com/sub2api/monitor/widget/RefreshWidgetAction.kt - Create:
app/src/main/java/com/sub2api/monitor/sync/RefreshScheduler.kt - Create:
app/src/main/java/com/sub2api/monitor/sync/WidgetRefreshWorker.kt - Modify:
app/src/main/java/com/sub2api/monitor/widget/Sub2ApiMonitorWidget.kt - Modify:
app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt - Create:
app/src/test/java/com/sub2api/monitor/sync/RefreshSchedulerTest.kt
Step 1: Write the failing tests
package com.sub2api.monitor.sync
import org.junit.Assert.assertEquals
import org.junit.Test
class RefreshSchedulerTest {
@Test
fun clampsRefreshIntervalToWorkManagerMinimum() {
assertEquals(15, sanitizeRefreshIntervalMinutes(5))
assertEquals(30, sanitizeRefreshIntervalMinutes(30))
}
}
Step 2: Run tests to verify they fail
Run: ./gradlew :app:testDebugUnitTest --tests "*RefreshSchedulerTest"
Expected: FAIL because scheduler helpers do not exist.
Step 3: Write minimal implementation
Add manual refresh action for the widget. Add a scheduler boundary with WorkManager-compatible interval clamping. When configuration is saved, enqueue or update the periodic refresh request.
For the prototype, the worker can call the mock repository and write cached widget state.
Step 4: Run tests to verify they pass
Run: ./gradlew :app:testDebugUnitTest --tests "*RefreshSchedulerTest"
Expected: PASS.
Step 5: Commit
git add app/src/main/java/com/sub2api/monitor/sync app/src/main/java/com/sub2api/monitor/widget app/src/main/java/com/sub2api/monitor/ui app/src/test/java/com/sub2api/monitor/sync
git commit -m "feat: add widget refresh actions"
Task 9: Final Verification
Files:
- Modify only if verification reveals issues.
Step 1: Run unit tests
Run: ./gradlew :app:testDebugUnitTest
Expected: BUILD SUCCESSFUL.
Step 2: Run Android build
Run: ./gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL and debug APK generated.
Step 3: Inspect repository status
Run: git status --short
Expected: no unintended files. Commit any intentional final fixes.
Step 4: Report
Summarize implemented features, verification commands, and any limitations such as simulator/device testing not being run.