disallow to disable guard during focus
docker / image (push) Failing after 24s
lint / ruff (push) Failing after 28s
tests / backend (push) Successful in 37s
tests / desktop (push) Successful in 22s
tests / android-core (push) Successful in 49s
tests / web (push) Successful in 1m20s
android / apk (push) Successful in 8m49s
docker / image (push) Failing after 24s
lint / ruff (push) Failing after 28s
tests / backend (push) Successful in 37s
tests / desktop (push) Successful in 22s
tests / android-core (push) Successful in 49s
tests / web (push) Successful in 1m20s
android / apk (push) Successful in 8m49s
This commit is contained in:
21 files changed
+481
-43
No files matched your search
@@ -62,6 +62,7 @@ import tech.kadence.core.DomainMatcher
|
||||
import tech.kadence.core.IntervalFacts
|
||||
import tech.kadence.core.evaluatePolicy
|
||||
import tech.kadence.core.expandDomains
|
||||
import tech.kadence.core.guardLocked
|
||||
import tech.kadence.core.invalidExpansions
|
||||
import tech.kadence.core.unexpandableMasks
|
||||
import tech.kadence.core.zoneOrDefault
|
||||
@@ -83,7 +84,10 @@ data class BlockerUi(
|
||||
val reason: String = "",
|
||||
val enabled: Boolean = false,
|
||||
val mode: String = MODE_DURING_WORK,
|
||||
val strict: Boolean = false,
|
||||
/** The guard is locked on — an interval is live, so it cannot be switched off. */
|
||||
val locked: Boolean = false,
|
||||
/** The server's sentence for why, shown under the toggle. */
|
||||
val lockReason: String = "",
|
||||
val killApps: Boolean = true,
|
||||
val notifyOnBlock: Boolean = true,
|
||||
/** Hostnames in the expanded set — `all_domains`, so it is non-zero even when idle. */
|
||||
@@ -481,7 +485,8 @@ class BlockerEngine(
|
||||
reason = dto.reason,
|
||||
enabled = dto.enabled,
|
||||
mode = dto.mode,
|
||||
strict = dto.strict,
|
||||
locked = dto.locked,
|
||||
lockReason = dto.lockReason,
|
||||
killApps = dto.killApps,
|
||||
notifyOnBlock = dto.notifyOnBlock,
|
||||
hostnames = dto.allDomains.size,
|
||||
@@ -509,7 +514,8 @@ class BlockerEngine(
|
||||
reason = verdict.reason,
|
||||
enabled = current?.enabled ?: false,
|
||||
mode = current?.mode ?: MODE_DURING_WORK,
|
||||
strict = current?.strict ?: false,
|
||||
locked = guardLocked(current ?: BlockerRules(), timerFacts()),
|
||||
lockReason = "",
|
||||
killApps = current?.killBlockedApps ?: true,
|
||||
notifyOnBlock = current?.notifyOnBlock ?: true,
|
||||
hostnames = current?.let { expandDomains(it).size } ?: 0,
|
||||
@@ -648,6 +654,5 @@ fun BlockerSettingsDto.toCore(): BlockerRules = BlockerRules(
|
||||
blockedApps = blockedApps,
|
||||
killBlockedApps = killBlockedApps,
|
||||
schedule = schedule.map { BlockWindow(it.label, it.start, it.end, it.days) },
|
||||
strict = strict,
|
||||
notifyOnBlock = notifyOnBlock,
|
||||
)
|
||||
@@ -140,7 +140,9 @@ data class BlockerPolicyDto(
|
||||
val reason: String = "",
|
||||
val mode: String = "during_work",
|
||||
val enabled: Boolean = false,
|
||||
val strict: Boolean = false,
|
||||
/** The guard is on and an interval is live, so it may not be switched off. */
|
||||
val locked: Boolean = false,
|
||||
@SerialName("lock_reason") val lockReason: String = "",
|
||||
@SerialName("redirect_ip") val redirectIp: String = "127.0.0.1",
|
||||
/** Empty whenever `active` is false — read `all_domains` instead. */
|
||||
val domains: List<String> = emptyList(),
|
||||
@@ -395,7 +397,6 @@ data class BlockerSettingsDto(
|
||||
@SerialName("blocked_apps") val blockedApps: List<String> = emptyList(),
|
||||
@SerialName("kill_blocked_apps") val killBlockedApps: Boolean = true,
|
||||
val schedule: List<BlockWindowDto> = emptyList(),
|
||||
val strict: Boolean = false,
|
||||
@SerialName("notify_on_block") val notifyOnBlock: Boolean = true,
|
||||
)
|
||||
|
||||
|
||||
@@ -237,18 +237,26 @@ fun BlockerScreen(
|
||||
Panel(padding = 0.dp) {
|
||||
ToggleRow(
|
||||
label = "Focus guard on",
|
||||
detail = if (ui.enabled) {
|
||||
"Everything below applies."
|
||||
} else {
|
||||
"Off everywhere — the phone, the desktop app and the web UI all " +
|
||||
detail = when {
|
||||
ui.locked -> ui.lockReason.ifBlank {
|
||||
"Finish or drop the interval to switch the guard off."
|
||||
}
|
||||
ui.enabled -> "Everything below applies."
|
||||
else -> "Off everywhere — the phone, the desktop app and the web UI all " +
|
||||
"stop enforcing."
|
||||
},
|
||||
checked = ui.enabled,
|
||||
// The rules are the server's document and there is no outbox for it:
|
||||
// a settings PUT is a deep merge of a shared object, and queueing one
|
||||
// would mean replaying a stale version over whatever the web UI did
|
||||
// meanwhile. So offline these say no, rather than appearing to work.
|
||||
enabled = ui.connection == Connection.Online,
|
||||
// Two reasons this says no, both said out loud rather than left as a
|
||||
// grey switch:
|
||||
// * offline — the rules are the server's document and there is no
|
||||
// outbox for it: a settings PUT is a deep merge of a shared object,
|
||||
// and queueing one would mean replaying a stale version over
|
||||
// whatever the web UI did meanwhile;
|
||||
// * locked — the server refuses to switch the guard off while an
|
||||
// interval is running or paused, which is the whole point of it.
|
||||
// Switching it *on* is never refused, which is why `locked` already has
|
||||
// "and the guard is on" folded into it server-side.
|
||||
enabled = ui.connection == Connection.Online && !ui.locked,
|
||||
onChange = { value -> patch(mapOf("enabled" to value)) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,6 +75,15 @@ class BlockerEngineTest {
|
||||
return engine
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the lock reaches the screen from the server's policy`() {
|
||||
// The screen never decides this itself — it renders what the server said,
|
||||
// and the server has already folded "and the guard is on" into `locked`.
|
||||
val ui = engine().state.value
|
||||
assertTrue(ui.locked)
|
||||
assertEquals("Finish or drop the interval to switch the focus guard off.", ui.lockReason)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
server = MockWebServer()
|
||||
|
||||
@@ -49,7 +49,6 @@ class BlockerSettingsTest {
|
||||
assertEquals(listOf("*discord*", "instagram"), blocker.blockedApps)
|
||||
assertFalse(blocker.killBlockedApps)
|
||||
assertFalse(blocker.notifyOnBlock)
|
||||
assertTrue(blocker.strict)
|
||||
assertEquals(1, blocker.schedule.size)
|
||||
assertEquals("Work mornings", blocker.schedule[0].label)
|
||||
assertEquals(listOf(0, 1, 2, 3, 4), blocker.schedule[0].days)
|
||||
@@ -106,7 +105,6 @@ class BlockerSettingsTest {
|
||||
assertTrue(blocker.blockedApps.isEmpty())
|
||||
assertTrue(blocker.killBlockedApps)
|
||||
assertTrue(blocker.notifyOnBlock)
|
||||
assertFalse(blocker.strict)
|
||||
assertEquals(BlockerRules.DEFAULT_SUBDOMAINS, blocker.subdomainExpansions)
|
||||
}
|
||||
|
||||
@@ -118,7 +116,7 @@ class BlockerSettingsTest {
|
||||
{"settings":{"blocker":{
|
||||
"enabled":null,"mode":null,"blocked_domains":null,
|
||||
"allowed_domains":null,"blocked_apps":null,"subdomain_expansions":null,
|
||||
"kill_blocked_apps":null,"notify_on_block":null,"strict":null
|
||||
"kill_blocked_apps":null,"notify_on_block":null
|
||||
}}}
|
||||
""".trimIndent(),
|
||||
)
|
||||
@@ -138,4 +136,24 @@ class BlockerSettingsTest {
|
||||
val inactive = KadenceJson.decodeFromString<BlockerPolicyDto>(fixture("blocker_policy_inactive.json"))
|
||||
assertTrue(inactive.invalidExpansions.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the policy carries whether the guard is locked on`() {
|
||||
val active = KadenceJson.decodeFromString<BlockerPolicyDto>(fixture("blocker_policy_active.json"))
|
||||
assertTrue(active.locked)
|
||||
assertEquals("Finish or drop the interval to switch the focus guard off.", active.lockReason)
|
||||
|
||||
val inactive = KadenceJson.decodeFromString<BlockerPolicyDto>(fixture("blocker_policy_inactive.json"))
|
||||
assertFalse(inactive.locked)
|
||||
assertEquals("", inactive.lockReason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a backend too old to send the lock reads as unlocked`() {
|
||||
// The switch then looks operable and the server still refuses the write with a
|
||||
// 409 — which is why the lock cannot live in the UI alone.
|
||||
val policy = KadenceJson.decodeFromString<BlockerPolicyDto>("""{"enabled":true}""")
|
||||
assertFalse(policy.locked)
|
||||
assertEquals("", policy.lockReason)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
"reason": "a work interval is running",
|
||||
"mode": "during_work",
|
||||
"enabled": true,
|
||||
"strict": false,
|
||||
"locked": true,
|
||||
"lock_reason": "Finish or drop the interval to switch the focus guard off.",
|
||||
"redirect_ip": "127.0.0.1",
|
||||
"domains": ["youtube.com", "www.youtube.com", "m.youtube.com", "reddit.com", "www.reddit.com", "m.reddit.com"],
|
||||
"apps": ["steam*", "*discord*"],
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"reason": "no work interval is running",
|
||||
"mode": "during_work",
|
||||
"enabled": true,
|
||||
"strict": false,
|
||||
"locked": false,
|
||||
"lock_reason": "",
|
||||
"redirect_ip": "127.0.0.1",
|
||||
"domains": [],
|
||||
"apps": [],
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"schedule": [
|
||||
{"label": "Work mornings", "start": "09:00:00", "end": "12:00:00", "days": [0, 1, 2, 3, 4]}
|
||||
],
|
||||
"strict": true,
|
||||
"notify_on_block": false,
|
||||
"redirect_ip": "0.0.0.0",
|
||||
"app_poll_seconds": 5
|
||||
|
||||
Reference in new issue
Block a user