Files
enricobuehler 9291568ce0
ci / web (push) Failing after 35s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / docs-site (push) Failing after 40s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m20s
refactor(apple): decompose ContentView (735 -> 272 lines)
Split the monolithic ContentView into focused view files — a pure structural refactor
with no behavior change (verified: builds macOS/iOS/tvOS, the test suite is green, and a
fidelity review against the original found no discrepancies):

- ContentView (272): the coordinator — owns the session model / host store / discovery,
  switches home<->session, holds the connect logic (it reads @AppStorage) + the dev
  hooks, and the stream builder (whose stable identity across awaiting-trust->streaming
  must NOT move — it stays here).
- HomeView (251): the hosts grid + navigation + toolbar + sheets + "On this network"
  discovery section + empty state.
- HostCards (158): HostCardView + DiscoveredCardView, sharing a CardMetrics struct
  (dedupes the platform-tuned sizing the two cards had copy-pasted).
- TrustCardView (80): the TOFU prompt + fingerprint formatting.
- StreamHUDView (67): the streaming overlay HUD.

State flows idiomatically: @StateObject (ContentView) -> @ObservedObject in subviews,
@State -> @Binding; the connect logic is passed as closures. Sheet placement is
preserved — the pairing/speed-test sheets stay on the outer body so they survive the
trust->home transition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:30:34 +02:00

81 lines
3.2 KiB
Swift

// Trust-on-first-use prompt: shown over the live-but-blurred stream when connecting to an
// unpinned host. The user compares the fingerprint with the one the host logged at startup,
// or drops this and runs the PIN pairing ceremony instead.
import Foundation
import SwiftUI
struct TrustCardView: View {
let fingerprint: Data
let hostName: String
let onCancel: () -> Void
let onTrust: () -> Void
let onPairInstead: () -> Void
var body: some View {
VStack(spacing: 14) {
Image(systemName: "lock.shield")
.font(.system(size: 36, weight: .light))
.foregroundStyle(.tint)
Text("Verify \(hostName)")
.font(.title3.weight(.semibold))
Text("First connection. Compare this fingerprint with the one "
+ "punktfunk-host logged at startup (\u{201C}clients pin this "
+ "fingerprint\u{201D}):")
.font(.callout)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Text(Self.format(fingerprint: fingerprint))
.font(.system(.callout, design: .monospaced))
#if !os(tvOS)
.textSelection(.enabled)
#endif
.padding(10)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
HStack(spacing: 12) {
Button("Cancel", role: .cancel, action: onCancel)
#if !os(tvOS)
.keyboardShortcut(.cancelAction)
#endif
Button("Trust & Connect", action: onTrust)
.buttonStyle(.borderedProminent)
#if !os(tvOS)
.keyboardShortcut(.defaultAction)
#endif
}
#if os(iOS)
.controlSize(.large)
#endif
// The verified alternative to eyeballing hex: drop this session (the host
// serves one connection at a time) and run the SPAKE2 PIN ceremony instead.
Button("Pair with PIN instead…", action: onPairInstead)
#if os(macOS)
.buttonStyle(.link)
#else
.buttonStyle(.borderless)
#endif
.font(.callout)
}
.padding(28)
.frame(maxWidth: 440)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18))
}
/// 64 hex chars four groups per line, two lines easy to eyeball against the log.
private static func format(fingerprint: Data) -> String {
let hex = fingerprint.map { String(format: "%02x", $0) }.joined()
let groups = stride(from: 0, to: hex.count, by: 8).map { i -> String in
let start = hex.index(hex.startIndex, offsetBy: i)
let end = hex.index(start, offsetBy: min(8, hex.count - i))
return String(hex[start..<end])
}
return groups.chunks(of: 4).map { $0.joined(separator: " ") }.joined(separator: "\n")
}
}
private extension Array {
func chunks(of size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map { Array(self[$0..<Swift.min($0 + size, count)]) }
}
}