Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2.5.1

**Update the phone app.** The helpers don't change.

- **No more false "went offline".** A phone waking from Doze often can't reach a computer for a few seconds while Tailscale reconnects, and one missed background check was enough to say the computer went offline. Now it checks twice more, 20 seconds apart, and says so only if the computer stayed unreachable.

## 2.5.0

Assistants get the whole recovery layer over MCP, with a guide for connecting your own.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import net.odysseylabs.powerdeck.ui.QuickActionActivity
import net.odysseylabs.powerdeck.ui.RecoveryText
import net.odysseylabs.powerdeck.ui.recoveryText
import net.odysseylabs.powerdeck.ui.Shortcuts
import kotlinx.coroutines.delay
import java.time.Instant
import java.util.concurrent.TimeUnit

Expand Down Expand Up @@ -60,7 +61,7 @@ class EventsWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
// On the first run only remember where the log is; don't replay history.
if (seen != null) tell(ctx, d, all, quiet(fresh.filter { notable(it, d, helperNames) }).takeLast(3))
events.maxOfOrNull { it.at }?.let { deck.store.setSeenEvents(d.id, it.toString()) }
} else if (before == Power.On && st == DeviceState.Off) {
} else if (before == Power.On && st == DeviceState.Off && confirmedOff { deck.state(d) == DeviceState.Off }) {
notify(ctx, d, "${d.name} went offline.")
}
}
Expand Down Expand Up @@ -176,6 +177,19 @@ class EventsWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
* Drops what a recovery line already says: "looks frozen" when a reset is proposed for that
* machine, and "answers again" when it's back after one.
*/
/**
* Whether a device that just missed one check is really off: [probe] answers off [tries] more times,
* [pauseMs] apart. A phone waking from Doze often can't reach a peer for a few seconds while
* Tailscale reconnects, which on its own said "went offline" about a machine that never was.
*/
internal suspend fun confirmedOff(tries: Int = 2, pauseMs: Long = 20_000, probe: suspend () -> Boolean): Boolean {
repeat(tries) {
delay(pauseMs)
if (!probe()) return false
}
return true
}

internal fun quiet(events: List<Event>): List<Event> {
fun machines(result: String) = events.filter { it.result == result }.mapNotNull(::recoveryMachine).map(String::lowercase).toSet()
val proposed = machines(PROPOSED)
Expand Down
13 changes: 13 additions & 0 deletions android/app/src/test/java/net/odysseylabs/powerdeck/UiRulesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import net.odysseylabs.powerdeck.ui.hubNote
import net.odysseylabs.powerdeck.ui.recoveryText
import net.odysseylabs.powerdeck.ui.uninstallBlocker
import net.odysseylabs.powerdeck.work.EventsWorker
import kotlinx.coroutines.test.runTest
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
Expand Down Expand Up @@ -67,6 +68,18 @@ class UiRulesTest {
assertFalse(EventsWorker.hubWatch(Event(at, "Atlas", "Workstation looks frozen", "ok")))
}

/** One missed background check isn't "went offline": the machine must stay unreachable twice more. */
@Test fun offlineNeedsTwoMoreMisses() = runTest {
var calls = 0
assertFalse(EventsWorker.confirmedOff { calls++; false }) // answers on the first re-check
assertEquals(1, calls)
val answers = ArrayDeque(listOf(true, false))
assertFalse(EventsWorker.confirmedOff { answers.removeFirst() }) // answers on the second
calls = 0
assertTrue(EventsWorker.confirmedOff { calls++; true })
assertEquals(2, calls)
}

@Test fun parsesARecovery() {
val m = HubMachine.from(JSONObject("""{"name":"workstation","state":"hung","led":true,"kvm":{"name":"kvm-2"},
"recovery":{"id":"r1","machine":"workstation","action":"reset","state":"proposed","reason":"Atlas can't reach it",
Expand Down
Loading