Watch: status, environment and notifications screens
A gear on the feed opens a status overview that leads with what is wrong: offline hosts, hosts on a fallback route, tokens expiring within 7 days, and push problems, with healthy hosts folded into one row. Each host opens its route ladder with Reconnect. The notifications page shows registration, the last push, Send test and Unpair. Demo mode gains a Laptop on its VPN fallback, and snapshot tests render the three screens.
This commit is contained in:
1 parent
9707daf1fe
commit
7ca0362745
17 files changed
+1063
-14
No files matched your search
@@ -139,6 +139,33 @@ private let feedItemJSON = """
|
||||
#expect(try decode(ErrorResponse.self, #"{ "error": "That host is offline" }"#).error == "That host is offline")
|
||||
}
|
||||
|
||||
@Test func statusFromDocs() throws {
|
||||
let status = try decode(StatusResponse.self, """
|
||||
{
|
||||
"hosts": [{
|
||||
"id": "laptop", "label": "Laptop", "status": "online", "error": null,
|
||||
"activeUrl": "http://laptopvpn.gib:3773",
|
||||
"routes": [
|
||||
{ "url": "http://laptop.gib:3773", "kind": "direct", "result": "failed", "error": "no answer in 5 s", "checkedAt": "2026-11-01T09:12:00.000Z" },
|
||||
{ "url": "http://laptopvpn.gib:3773", "kind": "vpn", "result": "ok", "error": null, "checkedAt": "2026-11-01T09:12:00.000Z" },
|
||||
{ "url": "https://t3laptop.gbrown.org", "kind": "public", "result": "untried", "error": null, "checkedAt": null }
|
||||
],
|
||||
"onlineSince": "2026-11-01T09:12:00.000Z", "lastOnlineAt": null, "nextRetryAt": null,
|
||||
"tokenExpiresAt": "2026-12-01T00:00:00.000Z", "serverVersion": "0.1.5"
|
||||
}],
|
||||
"gateway": { "startedAt": "2026-11-01T08:00:00.000Z", "pushConfigured": true,
|
||||
"lastPush": { "ok": false, "at": "2026-11-01T09:40:00.000Z", "status": 400, "reason": "BadDeviceToken" } },
|
||||
"device": { "name": "Apple Watch", "pairedAt": "2026-10-30T08:00:00.000Z", "pushRegistered": true, "apnsEnvironment": null }
|
||||
}
|
||||
""")
|
||||
let host = try #require(status.hosts.first)
|
||||
#expect(host.onFallback)
|
||||
#expect(host.activeRoute?.kind == .vpn)
|
||||
#expect(host.routes.map(\.hostPort) == ["laptop.gib:3773", "laptopvpn.gib:3773", "t3laptop.gbrown.org"])
|
||||
#expect(status.device.apnsEnvironment == nil)
|
||||
#expect(status.gateway.lastPush?.hint?.contains("sandbox/production") == true)
|
||||
}
|
||||
|
||||
@Test func badDateFails() {
|
||||
#expect(throws: DecodingError.self) {
|
||||
try decode(Message.self, #"{ "id": "m", "role": "user", "text": "x", "createdAt": "yesterday" }"#)
|
||||
@@ -236,6 +263,9 @@ private let feedItemJSON = """
|
||||
#expect(thread.url.absoluteString == "wristcode://thread/mac%20mini/t%2F1")
|
||||
#expect(Route(url: thread.url) == thread)
|
||||
#expect(Route(url: Route.newThread.url) == .newThread)
|
||||
for route in [Route.status, .host(id: "mac mini"), .notifications] {
|
||||
#expect(Route(url: route.url) == route)
|
||||
}
|
||||
#expect(Route(url: URL(string: "wristcode://thread/only-host")!) == nil)
|
||||
#expect(Route(url: URL(string: "https://thread/h/t")!) == nil)
|
||||
}
|
||||
@@ -322,6 +352,41 @@ private let feedItemJSON = """
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct StatusReportTests {
|
||||
private let now = Date(timeIntervalSince1970: 1_800_000_000)
|
||||
|
||||
@Test func demoListsIssuesWorstFirst() {
|
||||
let report = StatusReport(DemoFixtures(now: now).status, now: now)
|
||||
#expect(report.issues.map(\.title) == [
|
||||
"LittleMan offline", "Laptop is on its VPN route", "server-gib token expires in 5 days",
|
||||
])
|
||||
#expect(report.issues.map(\.detail) == ["Both routes failed · 4 min", "laptop.gib:3773 did not answer", "Run renew-hosts.sh"])
|
||||
#expect(!report.allConnected)
|
||||
#expect(report.worst == .problem)
|
||||
#expect(report.healthySummary == "2 more, all direct")
|
||||
#expect(report.subline == "4 environments · pushes OK")
|
||||
}
|
||||
|
||||
@Test func fallbackAloneIsStillAllConnected() {
|
||||
var status = DemoFixtures(now: now).status
|
||||
status.hosts.removeAll { $0.id == "littleman" }
|
||||
for i in status.hosts.indices { status.hosts[i].tokenExpiresAt = now.addingTimeInterval(30 * 86_400) }
|
||||
let report = StatusReport(status, now: now)
|
||||
#expect(report.allConnected)
|
||||
#expect(report.issues.map(\.kind) == [.fallback])
|
||||
}
|
||||
|
||||
@Test func pushProblems() {
|
||||
var status = DemoFixtures(now: now).status
|
||||
status.device.pushRegistered = false
|
||||
#expect(StatusReport(status, now: now).pushSummary == "watch not registered")
|
||||
status.gateway.pushConfigured = false
|
||||
let report = StatusReport(status, now: now)
|
||||
#expect(report.pushSummary == "pushes off")
|
||||
#expect(report.issues.first { $0.kind == .push }?.route == .notifications)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct DemoGatewayTests {
|
||||
@Test func allowMovesThreadToWorking() async throws {
|
||||
let gateway = DemoGateway()
|
||||
|
||||
Reference in new issue
Block a user