import SwiftUI // MARK: - MetricCard private struct MetricCard: View { let title: String let value: String let symbol: String let color: Color let subtitle: String? init(title: String, value: String, symbol: String, color: Color, subtitle: String? = nil) { self.title = title self.value = value self.symbol = symbol self.color = color self.subtitle = subtitle } var body: some View { VStack(alignment: .leading, spacing: 8) { Image(systemName: symbol) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(color) Text(value) .font(.system(size: 30, weight: .bold, design: .rounded)) .foregroundStyle(Color(.label)) .minimumScaleFactor(0.6) .lineLimit(1) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.system(size: 13, weight: .medium)) .foregroundStyle(Color(.secondaryLabel)) if let subtitle { Text(subtitle) .font(.system(size: 11)) .foregroundStyle(Color(.tertiaryLabel)) } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(16) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) } } // MARK: - CircularProgressView private struct CircularProgressView: View { let progress: Double // 0.0 – 1.0 let headline: String let subline: String @State private var animated: Double = 0 private var ringColor: Color { switch progress { case 0.9...: return Color(.systemGreen) case 0.6..<0.9: return Color(.systemOrange) default: return Color(.systemRed) } } var body: some View { HStack(spacing: 24) { ZStack { Circle() .stroke(Color(.systemFill), lineWidth: 14) Circle() .trim(from: 0, to: animated) .stroke( ringColor, style: StrokeStyle(lineWidth: 14, lineCap: .round) ) .rotationEffect(.degrees(-90)) .animation(.easeInOut(duration: 0.9), value: animated) VStack(spacing: 2) { Text("\(Int(progress * 100))%") .font(.system(size: 22, weight: .bold, design: .rounded)) .foregroundStyle(Color(.label)) Text("health") .font(.system(size: 11, weight: .medium)) .foregroundStyle(Color(.secondaryLabel)) } } .frame(width: 96, height: 96) VStack(alignment: .leading, spacing: 10) { Text("Services Health") .font(.system(size: 17, weight: .semibold)) .foregroundStyle(Color(.label)) Text(headline) .font(.system(size: 14)) .foregroundStyle(Color(.secondaryLabel)) Text(subline) .font(.system(size: 12)) .foregroundStyle(Color(.tertiaryLabel)) VStack(spacing: 6) { RingLegendRow(color: Color(.systemGreen), label: "Healthy", fraction: progress) RingLegendRow(color: Color(.systemRed), label: "Degraded", fraction: max(0, 1 - progress)) } } Spacer(minLength: 0) } .padding(16) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .onAppear { animated = progress } .onChange(of: progress) { _, newValue in withAnimation(.easeInOut(duration: 0.8)) { animated = newValue } } } } private struct RingLegendRow: View { let color: Color let label: String let fraction: Double var body: some View { HStack(spacing: 6) { Circle() .fill(color) .frame(width: 8, height: 8) Text(label) .font(.system(size: 12)) .foregroundStyle(Color(.secondaryLabel)) Spacer() Text("\(Int(fraction * 100))%") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(Color(.label)) } } } // MARK: - AuditEventRow private struct AuditEventRow: View { let entry: AuditEntry private static let relativeFormatter: RelativeDateTimeFormatter = { let f = RelativeDateTimeFormatter() f.unitsStyle = .abbreviated return f }() private var outcomeSymbol: String { switch entry.outcome { case .success: "checkmark.circle.fill" case .failure: "xmark.circle.fill" case .warning: "exclamationmark.triangle.fill" } } private var outcomeColor: Color { switch entry.outcome { case .success: Color(.systemGreen) case .failure: Color(.systemRed) case .warning: Color(.systemOrange) } } var body: some View { HStack(spacing: 12) { Image(systemName: outcomeSymbol) .font(.system(size: 20, weight: .medium)) .foregroundStyle(outcomeColor) .frame(width: 28, alignment: .center) VStack(alignment: .leading, spacing: 3) { HStack(alignment: .firstTextBaseline) { Text(entry.action) .font(.system(size: 15, weight: .medium)) .foregroundStyle(Color(.label)) .lineLimit(1) Spacer() Text(Self.relativeFormatter.localizedString(for: entry.timestamp, relativeTo: .now)) .font(.system(size: 12)) .foregroundStyle(Color(.tertiaryLabel)) } HStack(spacing: 4) { Text(entry.resource) .font(.system(size: 13)) .foregroundStyle(Color(.secondaryLabel)) .lineLimit(1) Text("·") .font(.system(size: 13)) .foregroundStyle(Color(.quaternaryLabel)) Text(entry.actor) .font(.system(size: 13)) .foregroundStyle(Color(.secondaryLabel)) .lineLimit(1) .truncationMode(.middle) } if let detail = entry.detail { Text(detail) .font(.system(size: 12)) .foregroundStyle(Color(.tertiaryLabel)) .lineLimit(1) } } } .padding(.vertical, 10) } } // MARK: - DashboardView struct DashboardView: View { @StateObject private var viewModel = DashboardViewModel() @State private var showError = false var body: some View { NavigationStack { ScrollView { LazyVStack(spacing: 8) { metricsGrid healthRing auditSection } .padding(.horizontal, 16) .padding(.top, 8) .padding(.bottom, 32) } .background(Color(.systemBackground)) .navigationTitle("Dashboard") .navigationBarTitleDisplayMode(.large) .toolbar { toolbarContent } .refreshable { await viewModel.loadDashboard() } .onAppear { viewModel.startPolling() } .onDisappear { viewModel.stopPolling() } .onChange(of: viewModel.error != nil) { _, hasError in if hasError { showError = true } } .alert("Load Failed", isPresented: $showError) { Button("Retry") { Task { await viewModel.loadDashboard() } } Button("Dismiss", role: .cancel) {} } message: { Text(viewModel.error?.localizedDescription ?? "Unknown error") } } } // MARK: - Sections @ViewBuilder private var metricsGrid: some View { LazyVGrid( columns: [GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8)], spacing: 8 ) { MetricCard( title: "Active Agents", value: "\(viewModel.metrics.activeAgents)", symbol: "person.2.fill", color: Color(.systemBlue) ) MetricCard( title: "Services", value: "\(Int(viewModel.metrics.uptimePercent))%", symbol: "server.rack", color: viewModel.metrics.uptimePercent >= 90 ? Color(.systemGreen) : Color(.systemOrange), subtitle: "uptime" ) MetricCard( title: "Modules", value: "\(viewModel.metrics.tasksCompleted)", symbol: "puzzlepiece.extension.fill", color: Color(.systemOrange), subtitle: "\(viewModel.metrics.tasksFailed) failed" ) MetricCard( title: "CPU / Latency", value: latencyLabel, symbol: "gauge.with.needle.fill", color: latencyColor, subtitle: "avg response" ) } } @ViewBuilder private var healthRing: some View { CircularProgressView( progress: viewModel.metrics.uptimePercent / 100, headline: "\(Int(viewModel.metrics.uptimePercent))% services operational", subline: "\(viewModel.metrics.alertCount) active alert\(viewModel.metrics.alertCount == 1 ? "" : "s")" ) } @ViewBuilder private var auditSection: some View { VStack(spacing: 0) { HStack { Label("Recent Activity", systemImage: "clock.arrow.circlepath") .font(.system(size: 17, weight: .semibold)) .foregroundStyle(Color(.label)) Spacer() if viewModel.isLoading { ProgressView() .scaleEffect(0.75) } } .padding(.horizontal, 16) .padding(.top, 16) .padding(.bottom, 4) let events = Array(viewModel.recentAudit.prefix(5)) if events.isEmpty && !viewModel.isLoading { Text("No recent events") .font(.system(size: 14)) .foregroundStyle(Color(.tertiaryLabel)) .frame(maxWidth: .infinity) .padding(.vertical, 24) } else { ForEach(Array(events.enumerated()), id: \.element.id) { index, entry in AuditEventRow(entry: entry) .padding(.horizontal, 16) if index < events.count - 1 { Divider() .padding(.leading, 56) } } .padding(.bottom, 8) } } .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) } @ToolbarContentBuilder private var toolbarContent: some ToolbarContent { ToolbarItem(placement: .topBarTrailing) { Button { Task { await viewModel.loadDashboard() } } label: { Image(systemName: "arrow.clockwise") .font(.system(size: 16, weight: .medium)) } .disabled(viewModel.isLoading) } } // MARK: - Computed helpers private var latencyLabel: String { let ms = viewModel.metrics.avgResponseTimeMs return ms < 1_000 ? "\(Int(ms))ms" : String(format: "%.1fs", ms / 1_000) } private var latencyColor: Color { let ms = viewModel.metrics.avgResponseTimeMs if ms < 200 { return Color(.systemGreen) } if ms < 500 { return Color(.systemOrange) } return Color(.systemRed) } } // MARK: - Preview #Preview { DashboardView() }