-
Notifications
You must be signed in to change notification settings - Fork 0
Fix toggle race, IP retry, permission banner, heartbeat schema #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4640491
31ea558
17cee5a
b166b6c
96fe122
35e821a
71f41db
77b122a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import com.cashpilot.android.model.WorkerHeartbeat | |
| import com.cashpilot.android.util.SettingsStore | ||
| import io.ktor.client.HttpClient | ||
| import io.ktor.client.engine.okhttp.OkHttp | ||
| import io.ktor.client.plugins.HttpTimeout | ||
| import io.ktor.client.plugins.contentnegotiation.ContentNegotiation | ||
| import io.ktor.client.request.bearerAuth | ||
| import io.ktor.client.request.post | ||
|
|
@@ -50,7 +51,13 @@ class HeartbeatService : Service() { | |
| install(ContentNegotiation) { | ||
| json(Json { ignoreUnknownKeys = true }) | ||
| } | ||
| install(HttpTimeout) { | ||
| requestTimeoutMillis = 15_000 | ||
| connectTimeoutMillis = 10_000 | ||
| socketTimeoutMillis = 10_000 | ||
| } | ||
| } | ||
| private var consecutiveFailures = 0 | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
|
|
@@ -67,7 +74,15 @@ class HeartbeatService : Service() { | |
| if (settings.serverUrl.isNotBlank() && settings.apiKey.isNotBlank()) { | ||
| sendHeartbeat(settings) | ||
| } | ||
| delay(settings.heartbeatIntervalSeconds * 1000L) | ||
| // Exponential backoff on consecutive failures (30s → 60s → 120s, max 5min) | ||
| val baseDelay = (settings.heartbeatIntervalSeconds * 1000L).coerceAtLeast(5_000L) | ||
| val backoff = if (consecutiveFailures > 0) { | ||
| (baseDelay * (1L shl consecutiveFailures.coerceAtMost(3))) | ||
| .coerceAtMost(300_000L) | ||
| } else { | ||
| baseDelay | ||
| } | ||
| delay(backoff) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| return START_STICKY | ||
|
|
@@ -77,10 +92,13 @@ class HeartbeatService : Service() { | |
| try { | ||
| val apps = detector.detectAll(settings.enabledSlugs) | ||
|
|
||
| // Map app statuses to the server's container-like format | ||
| // Send apps in both formats for backward compatibility: | ||
| // - `apps` (new): rich app data for servers that understand Android workers | ||
| // - `containers` (legacy): simplified format so older servers still show the worker | ||
| val containers = apps.map { app -> | ||
| AppContainer( | ||
| name = app.slug, | ||
| slug = app.slug, | ||
| name = "cashpilot-${app.slug}", | ||
| status = if (app.running) "running" else "stopped", | ||
| labels = mapOf( | ||
| "cashpilot.managed" to "true", | ||
|
|
@@ -92,12 +110,12 @@ class HeartbeatService : Service() { | |
| val heartbeat = WorkerHeartbeat( | ||
| name = "${Build.MANUFACTURER} ${Build.MODEL} (${deviceId()})", | ||
| containers = containers, | ||
| apps = apps, | ||
| systemInfo = SystemInfo( | ||
| os = "Android", | ||
| arch = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown", | ||
| osVersion = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})", | ||
| deviceType = "android", | ||
| apps = apps, | ||
| ), | ||
|
Comment on lines
110
to
119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heartbeat payload is emitted with empty At send time, app statuses are placed at top-level and not in Suggested fix val heartbeat = WorkerHeartbeat(
name = "${Build.MANUFACTURER} ${Build.MODEL} (${deviceId()})",
- apps = apps,
systemInfo = SystemInfo(
os = "Android",
arch = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown",
osVersion = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})",
deviceType = "android",
+ apps = apps,
),
)Based on learnings: "Applies to **/{Heartbeat,HeartbeatService}.kt : Heartbeat payload in CashPilot-android must match the server's WorkerHeartbeat schema with name, url, containers, and system_info fields, with Android-specific app status packed into system_info.apps". 🤖 Prompt for AI Agents |
||
| ) | ||
|
|
||
|
|
@@ -109,16 +127,19 @@ class HeartbeatService : Service() { | |
| } | ||
|
|
||
| if (response.status.isSuccess()) { | ||
| consecutiveFailures = 0 | ||
| _lastHeartbeat.value = System.currentTimeMillis() | ||
| _lastHeartbeatFailed.value = false | ||
| val runningCount = apps.count { it.running } | ||
| updateNotification("$runningCount/${apps.size} apps running") | ||
| } else { | ||
| consecutiveFailures++ | ||
| _lastHeartbeatFailed.value = true | ||
| Log.w(TAG, "Heartbeat rejected: HTTP ${response.status.value}") | ||
| updateNotification("Server rejected heartbeat (${response.status.value})") | ||
| } | ||
| } catch (e: Exception) { | ||
| consecutiveFailures++ | ||
| _lastHeartbeatFailed.value = true | ||
| Log.w(TAG, "Heartbeat failed: ${e.message}") | ||
| updateNotification("Heartbeat failed — retrying...") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Schema contract drift:
appsshould not be top-level inWorkerHeartbeat.This change introduces a payload shape that conflicts with the documented WorkerHeartbeat contract used by this repo. Keep Android app status under
system_info.apps(optionally dual-write only during migration windows).Suggested fix
data class WorkerHeartbeat( val name: String, val url: String = "", /** Docker containers (empty for Android workers). */ val containers: List<AppContainer> = emptyList(), - /** Android app statuses (empty for Docker workers). */ - val apps: List<AppStatus> = emptyList(), `@SerialName`("system_info") val systemInfo: SystemInfo = SystemInfo(), )As per coding guidelines: "Heartbeat payload in CashPilot-android must match the server's WorkerHeartbeat schema with name, url, containers, and system_info fields, with Android-specific app status packed into system_info.apps".
🤖 Prompt for AI Agents