Files
aris-old/IrisCompanion/iris/Views/OrchestratorView.swift
2026-01-08 19:16:32 +00:00

177 lines
7.4 KiB
Swift

//
// OrchestratorView.swift
// iris
//
// Created by Codex.
//
import CoreLocation
import Foundation
import MusicKit
import SwiftUI
@available(iOS 16.0, *)
struct OrchestratorView: View {
@EnvironmentObject private var orchestrator: ContextOrchestrator
var body: some View {
NavigationStack {
List {
Section("Location") {
LabeledContent("Auth") { Text(authText(orchestrator.authorization)) }
if let loc = orchestrator.lastLocation {
LabeledContent("Lat/Lon") {
Text("\(format(loc.coordinate.latitude, 5)), \(format(loc.coordinate.longitude, 5))")
.textSelection(.enabled)
}
LabeledContent("Accuracy") { Text("\(Int(loc.horizontalAccuracy)) m") }
LabeledContent("Speed") { Text(speedText(loc.speed)) }
} else {
Text("No location yet")
.foregroundStyle(.secondary)
}
}
Section("Recompute") {
LabeledContent("Last reason") { Text(orchestrator.lastRecomputeReason ?? "") }
LabeledContent("Last time") { Text(orchestrator.lastRecomputeAt.map(timeOnly) ?? "") }
LabeledContent("Elapsed") { Text(orchestrator.lastPipelineElapsedMs.map { "\($0) ms" } ?? "") }
LabeledContent("Fetch failed") { Text(orchestrator.lastFetchFailed ? "Yes" : "No") }
if let err = orchestrator.lastError {
Text(err)
.font(.footnote)
.foregroundStyle(.secondary)
}
Button("Recompute Now") { orchestrator.recomputeNow() }
}
Section("Winner") {
if let env = orchestrator.lastWinner {
Text(env.winner.title)
.font(.headline)
Text(env.winner.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
Text("type \(env.winner.type.rawValue) • prio \(String(format: "%.2f", env.winner.priority)) • ttl \(env.winner.ttlSec)s")
.font(.caption)
.foregroundStyle(.secondary)
} else {
Text("No winner yet")
.foregroundStyle(.secondary)
}
}
Section("Now Playing") {
LabeledContent("Music auth") { Text(musicAuthText(orchestrator.musicAuthorization)) }
if let snapshot = orchestrator.nowPlaying {
Text(snapshot.title)
.font(.headline)
.lineLimit(1)
Text(nowPlayingSubtitle(snapshot))
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
Text(String(describing: snapshot.playbackStatus))
.font(.caption)
.foregroundStyle(.secondary)
} else {
Text(orchestrator.musicAuthorization == .authorized ? "Nothing playing" : "Not authorized")
.foregroundStyle(.secondary)
}
}
Section("Candidates (\(orchestrator.lastCandidates.count))") {
if orchestrator.lastCandidates.isEmpty {
Text("No candidates")
.foregroundStyle(.secondary)
} else {
ForEach(orchestrator.lastCandidates, id: \.id) { c in
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(c.title)
.font(.headline)
.lineLimit(1)
Spacer()
Text(c.type.rawValue)
.font(.caption)
.foregroundStyle(.secondary)
}
Text(c.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
Text("conf \(String(format: "%.2f", c.confidence)) • ttl \(c.ttlSec)s")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}
}
if !orchestrator.lastWeatherDiagnostics.isEmpty {
Section("Weather Diagnostics") {
ForEach(orchestrator.lastWeatherDiagnostics.keys.sorted(), id: \.self) { key in
LabeledContent(key) {
Text(orchestrator.lastWeatherDiagnostics[key] ?? "")
.font(.caption)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
}
}
Section("Test") {
Button("Send Fixture Feed Now") { orchestrator.sendFixtureFeedNow() }
}
}
.navigationTitle("Orchestrator")
}
}
private func authText(_ s: CLAuthorizationStatus) -> String {
switch s {
case .notDetermined: return "Not Determined"
case .restricted: return "Restricted"
case .denied: return "Denied"
case .authorizedAlways: return "Always"
case .authorizedWhenInUse: return "When In Use"
@unknown default: return "Other"
}
}
private func timeOnly(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .medium
return formatter.string(from: date)
}
private func speedText(_ speed: CLLocationSpeed) -> String {
guard speed >= 0 else { return "" }
return "\(String(format: "%.1f", speed)) m/s"
}
private func musicAuthText(_ status: MusicAuthorization.Status) -> String {
switch status {
case .notDetermined: return "Not Determined"
case .denied: return "Denied"
case .restricted: return "Restricted"
case .authorized: return "Authorized"
@unknown default: return "Other"
}
}
private func nowPlayingSubtitle(_ snapshot: NowPlayingSnapshot) -> String {
let parts = [snapshot.artist, snapshot.album]
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
return parts.isEmpty ? "Apple Music" : parts.joined(separator: "")
}
private func format(_ value: Double, _ precision: Int) -> String {
String(format: "%.\(precision)f", value)
}
}