feat(apple): surface host online status on the home grid
ci / web (push) Failing after 36s
ci / docs-site (push) Failing after 39s
ci / rust (push) Successful in 1m19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
apple / swift (push) Successful in 1m24s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m52s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 5m25s
ci / web (push) Failing after 36s
ci / docs-site (push) Failing after 39s
ci / rust (push) Successful in 1m19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
apple / swift (push) Successful in 1m24s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m52s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 5m25s
Saved host cards now show a presence dot — green when the host is advertising on
the LAN right now, grey when not seen. Cross-references each StoredHost against the
live mDNS discovery set (HostDiscovery). No host changes: the host already
advertises _punktfunk._udp with a stable id + cert fingerprint, which the client
already browses.
- StoredHost.matches(DiscoveredHost): fingerprint-first (survives a DHCP address
change), address:port fallback. The discovered-section dedup now uses the same
match, so a saved host whose IP changed no longer also shows up as a stranger.
- HostCardView gains an isOnline presence dot (accessibility-labelled).
- HomeView.isOnline recomputes on every @Published discovery change, so the dot
tracks hosts joining/leaving the network live.
Online detection is LAN-scoped by design: a remote/cross-subnet host that doesn't
advertise here shows grey ("not seen"), not a false "offline". Swift-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,7 @@ struct HomeView: View {
|
||||
private func hostCard(_ host: StoredHost) -> some View {
|
||||
HostCardView(
|
||||
host: host,
|
||||
isOnline: isOnline(host),
|
||||
isConnecting: model.phase == .connecting && model.activeHost?.id == host.id,
|
||||
isMostRecent: host.id == mostRecentHostID,
|
||||
isBusy: model.isBusy,
|
||||
@@ -176,12 +177,18 @@ struct HomeView: View {
|
||||
.padding(.top, store.hosts.isEmpty ? 0 : 8)
|
||||
}
|
||||
|
||||
/// Discovered hosts not already saved (matched by address+port) — the saved grid shows the
|
||||
/// rest, so this section only surfaces genuinely-new hosts on the network.
|
||||
private var discoveredUnsaved: [DiscoveredHost] {
|
||||
discovery.hosts.filter { d in
|
||||
!store.hosts.contains { $0.address == d.host && $0.port == d.port }
|
||||
/// A saved host is "online" iff a live mDNS advert currently matches it (see
|
||||
/// `StoredHost.matches`). Recomputed on every discovery change (the @Published set), so the
|
||||
/// dot tracks hosts appearing/leaving the network live.
|
||||
private func isOnline(_ host: StoredHost) -> Bool {
|
||||
discovery.hosts.contains { host.matches($0) }
|
||||
}
|
||||
|
||||
/// Discovered hosts not already saved — the saved grid shows the rest, so this section only
|
||||
/// surfaces genuinely-new hosts on the network. Same match as the online dot, so a saved host
|
||||
/// whose IP changed (still fingerprint-matched) doesn't also appear here as a stranger.
|
||||
private var discoveredUnsaved: [DiscoveredHost] {
|
||||
discovery.hosts.filter { d in !store.hosts.contains { $0.matches(d) } }
|
||||
}
|
||||
|
||||
/// The host of the most recent session — its card carries the accent ring.
|
||||
|
||||
@@ -24,6 +24,9 @@ private struct CardMetrics {
|
||||
/// pairs / speed-tests / forgets / removes. Disabled while a session is busy.
|
||||
struct HostCardView: View {
|
||||
let host: StoredHost
|
||||
/// Currently advertising on the LAN (matched against live mDNS discovery). False means
|
||||
/// "not seen on this network" — off, or a remote/cross-subnet host we can't observe.
|
||||
let isOnline: Bool
|
||||
let isConnecting: Bool
|
||||
let isMostRecent: Bool
|
||||
let isBusy: Bool
|
||||
@@ -48,9 +51,16 @@ struct HostCardView: View {
|
||||
}
|
||||
.frame(height: m.iconBox)
|
||||
VStack(spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
// Presence dot: green = advertising on the LAN now; grey = not seen.
|
||||
Circle()
|
||||
.fill(isOnline ? Color.green : Color.secondary.opacity(0.35))
|
||||
.frame(width: 7, height: 7)
|
||||
.accessibilityLabel(isOnline ? "Online" : "Offline")
|
||||
Text(host.displayName)
|
||||
.font(m.nameFont)
|
||||
.lineLimit(1)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
if host.pinnedSHA256 != nil {
|
||||
Image(systemName: "lock.fill")
|
||||
|
||||
@@ -25,6 +25,26 @@ struct StoredHost: Identifiable, Codable, Hashable {
|
||||
var displayName: String { name.isEmpty ? address : name }
|
||||
}
|
||||
|
||||
extension StoredHost {
|
||||
/// True when a live mDNS advert (`DiscoveredHost`) describes THIS saved host — drives the
|
||||
/// "online" indicator and de-dupes the discovered section. Matched by certificate
|
||||
/// fingerprint when both sides carry it (so it survives a DHCP address change), otherwise
|
||||
/// by address:port. Online detection is LAN-scoped: a host not advertising on this network
|
||||
/// (off, or a remote/cross-subnet address) simply won't match — "not seen", not proven off.
|
||||
func matches(_ discovered: DiscoveredHost) -> Bool {
|
||||
if let pin = pinnedSHA256, let fp = discovered.fingerprintHex,
|
||||
pin.hexLower == fp.lowercased() {
|
||||
return true
|
||||
}
|
||||
return address == discovered.host && port == discovered.port
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
/// Lowercase hex, no separators — to compare a pinned fingerprint against the mDNS `fp`.
|
||||
var hexLower: String { map { String(format: "%02x", $0) }.joined() }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class HostStore: ObservableObject {
|
||||
private static let key = DefaultsKey.hosts
|
||||
|
||||
Reference in New Issue
Block a user