diff --git a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt index f236365..5fc14a6 100644 --- a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -1632,11 +1632,30 @@ class DesktopState(private val appDir: File = defaultAppDir()) { return@launch } } - // Poll the target job to a terminal state. + // Poll the target job to a terminal state. A job this long + // (10+ min = hundreds of polls) WILL hit a transient fetch + // failure; treating one as terminal (the old `orEmpty()` → + // st == null → break) froze the screen on stale content until + // the user left and returned — exactly the reported symptom. + // So: only a SUCCESSFUL fetch reporting a terminal state + // breaks; failures keep polling up to ~30s of contact loss. + var miss = 0 + var gone = 0 while (true) { delay(2000) - val jobs = runCatching { parseJobs(provider.fetchJobs(remoteId)) }.getOrNull() - .orEmpty() + val fetched = runCatching { parseJobs(provider.fetchJobs(remoteId)) } + if (fetched.isFailure) { + if (++miss >= 15) { + d0.error = "Lost contact with the engine while " + + "processing — reopen this recording to check." + d0.busy = null + _detail.value = d0.copy() + return@launch + } + continue + } + miss = 0 + val jobs = fetched.getOrDefault(emptyList()) d0.jobs = jobs jobLabel(jobs)?.let { d0.busy = it } // Mirror live progress onto the library row too — otherwise @@ -1646,7 +1665,10 @@ class DesktopState(private val appDir: File = defaultAppDir()) { rescanStatuses() _detail.value = d0.copy() val st = jobs.firstOrNull { it.jobType == job }?.status - if (st == null || st in TERMINAL) break + if (st in TERMINAL) break + // Job row missing from a healthy fetch: give it a few + // cycles (the engine rewrites rows mid-requeue) then stop. + if (st == null && ++gone >= 3) break } setLive(d0.file.name, null) rescanStatuses()