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:
gib committed 2026-11-01 15:16:18 -05:00
1 parent 9707daf1fe
commit 7ca0362745
17 files changed
+1063 -14

No files matched your search

+106 -1
View File
@@ -230,6 +230,111 @@ struct ProjectsResponse: Codable, Hashable, Sendable {
var hosts: [Host]
}
/// How a route reaches its host, guessed by the gateway from the URL.
struct RouteKind: RawRepresentable, Codable, Hashable, Sendable {
let rawValue: String
init(rawValue: String) { self.rawValue = rawValue }
static let direct = RouteKind(rawValue: "direct")
static let vpn = RouteKind(rawValue: "vpn")
static let `public` = RouteKind(rawValue: "public")
/// "direct", "VPN" or "public", for sentences like "Laptop is on its VPN route".
var name: String { self == .vpn ? "VPN" : rawValue }
var symbol: String {
switch self {
case .direct: "house.fill"
case .vpn: "lock.shield.fill"
default: "cloud.fill"
}
}
}
/// One way to reach a host. `Route` in the contract, renamed here because `Route` is the app's
/// navigation.
struct HostRoute: Codable, Hashable, Sendable {
struct Result: RawRepresentable, Codable, Hashable, Sendable {
let rawValue: String
init(rawValue: String) { self.rawValue = rawValue }
static let ok = Result(rawValue: "ok")
static let failed = Result(rawValue: "failed")
static let untried = Result(rawValue: "untried")
}
var url: String
var kind: RouteKind
/// From the latest connect. `untried` when an earlier route answered first.
var result: Result
var error: String?
var checkedAt: Date?
/// The URL without its scheme, such as `laptop.gib:3773`.
var hostPort: String {
guard let components = URLComponents(string: url), let host = components.host else { return url }
return components.port.map { "\(host):\($0)" } ?? host
}
}
/// One host as the status screens show it.
struct HostStatusDetail: Codable, Hashable, Sendable, Identifiable {
var id: String
var label: String
var status: HostStatus
var error: String?
/// The route in use. Null unless online.
var activeUrl: String?
/// Preferred first, the order the gateway tries them.
var routes: [HostRoute]
var onlineSince: Date?
var lastOnlineAt: Date?
/// When the gateway tries again, while it waits to retry.
var nextRetryAt: Date?
var tokenExpiresAt: Date?
var serverVersion: String?
var activeRoute: HostRoute? { routes.first { $0.url == activeUrl } }
/// Online, but not on the first route.
var onFallback: Bool {
status == .online && activeUrl != nil && activeUrl != routes.first?.url
}
}
/// What Apple answered for one push.
struct PushResult: Codable, Hashable, Sendable {
var ok: Bool
var at: Date
/// Apple's HTTP status, or 0 when the push never reached Apple.
var status: Int
/// Apple's reason, such as `BadDeviceToken`, or why the gateway could not send.
var reason: String?
}
/// `GET /v1/status`: everything the settings screens show.
struct StatusResponse: Codable, Hashable, Sendable {
struct Gateway: Codable, Hashable, Sendable {
var startedAt: Date
/// False when the gateway has no APNs key, so nothing can be pushed.
var pushConfigured: Bool
var lastPush: PushResult?
}
/// The watch asking.
struct Device: Codable, Hashable, Sendable {
var name: String
var pairedAt: Date
/// True once the watch has sent its APNs token.
var pushRegistered: Bool
var apnsEnvironment: PushRegistration.Environment?
}
var hosts: [HostStatusDetail]
var gateway: Gateway
var device: Device
}
struct PairRequest: Codable, Sendable {
var code: String
var deviceName: String
@@ -303,7 +408,7 @@ struct ErrorResponse: Codable, Sendable {
/// Notification categories the watch registers actions for.
enum PushCategory: String, CaseIterable, Sendable {
case approval, finished, failed, input, host
case approval, finished, failed, input, host, test
}
enum JSON {