import Combine import SwiftUI struct SentinelView: View { @State private var store = AppStore.shared private let refreshTimer = Timer.publish(every: 20, on: .main, in: .common).autoconnect() var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { header if let error = store.errorMessage { errorBanner(error) } availabilityCard postureCards timelineCard } .padding(24) } .background(VelocityTheme.background) .scrollContentBackground(.hidden) .task { await store.refresh() } .refreshable { await store.refresh() } .onReceive(refreshTimer) { _ in Task { await store.refresh(silent: true) } } } private var header: some View { VStack(alignment: .leading, spacing: 4) { Text(SentinelScope.productFamilyName.uppercased()) .font(.system(size: 10, weight: .semibold)) .tracking(1.2) .foregroundStyle(VelocityTheme.mutedFg) Text(SentinelScope.navigationTitle) .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("Truthful live posture for alerts and comms load. \(SentinelScope.productFamilyName) analytics stay disabled on iPad until a real production stream is exposed.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } } private var availabilityCard: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("Production Scope") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() statusBadge( label: SentinelScope.availabilityBadge, color: VelocityTheme.warning ) } Text("This iPad build does not synthesize \(SentinelScope.disabledAnalyticsSummary). A dedicated production Sentinel route is still required before those analytics can be shown safely.") .font(.system(size: 14)) .foregroundStyle(VelocityTheme.foreground) Text("Current surface instead reports real \(SentinelScope.liveBackedSummary) from the live mobile-edge backend.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(20) .glassCard(cornerRadius: 18) } private var postureCards: some View { HStack(spacing: 14) { SentinelCard( title: "Pending insights", value: "\(store.metrics.pendingInsights)", subtitle: "Recommendations waiting on operator review", color: VelocityTheme.danger ) SentinelCard( title: "Transcript queue", value: "\(store.metrics.pendingTranscriptions)", subtitle: "Imported recordings still processing", color: VelocityTheme.warning ) SentinelCard( title: "Upcoming 24h", value: "\(store.alertSnapshot?.upcomingCalendarEvents24h ?? 0)", subtitle: "Calendar events due soon", color: VelocityTheme.success ) } } private var timelineCard: some View { VStack(alignment: .leading, spacing: 14) { HStack { Text("Recent Operator Timeline") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() if let lastRefresh = store.lastRefreshAt { Text("Updated \(lastRefresh.relativeShort)") .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } } if store.timelineEvents.isEmpty { Text("No live communication events have been loaded for the current high-priority leads yet.") .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) } else { ForEach(store.timelineEvents.prefix(6)) { item in VStack(alignment: .leading, spacing: 6) { HStack { Text(item.leadName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() Text(item.event.channel.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(VelocityTheme.accent) } Text(item.event.summary ?? "No summary available for this event.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(item.event.timestampDate?.relativeShort ?? item.event.timestamp) .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.surface) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.borderSubtle, lineWidth: 1) ) ) } } } .padding(20) .glassCard(cornerRadius: 18) } private func statusBadge(label: String, color: Color) -> some View { Text(label) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(color) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() .fill(color.opacity(0.12)) .overlay(Capsule().stroke(color.opacity(0.22), lineWidth: 1)) ) } private func errorBanner(_ message: String) -> some View { Text(message) .font(.system(size: 13, weight: .medium)) .foregroundStyle(VelocityTheme.danger) .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.danger.opacity(0.10)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.danger.opacity(0.22), lineWidth: 1) ) ) } } private struct SentinelCard: View { let title: String let value: String let subtitle: String let color: Color var body: some View { VStack(alignment: .leading, spacing: 10) { Text(title.uppercased()) .font(.system(size: 10, weight: .semibold)) .tracking(1) .foregroundStyle(VelocityTheme.mutedFg) Text(value) .font(.system(size: 26, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text(subtitle) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) RoundedRectangle(cornerRadius: 4) .fill(color) .frame(width: 48, height: 4) } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) } } #Preview { SentinelView() }