import Foundation import SwiftUI // MARK: - User enum UserRole: String, Codable, CaseIterable { case admin case `operator` case viewer var label: String { switch self { case .admin: return "Admin" case .operator: return "Operatör" case .viewer: return "Visare" } } var color: Color { switch self { case .admin: return Color(.systemRed) case .operator: return Color(.systemOrange) case .viewer: return Color(.systemBlue) } } var icon: String { switch self { case .admin: return "shield.fill" case .operator: return "wrench.and.screwdriver.fill" case .viewer: return "eye.fill" } } } struct User: Codable, Identifiable { let id: String let email: String let name: String let role: UserRole let createdAt: Date let lastLogin: Date? enum CodingKeys: String, CodingKey { case id, email, name, role case createdAt = "created_at" case lastLogin = "last_login" } } // MARK: - Module enum ModuleStatus: String, Codable { case healthy case degraded case offline var label: String { switch self { case .healthy: return "Aktiv" case .degraded: return "Degraderad" case .offline: return "Offline" } } var color: Color { switch self { case .healthy: return Color(.systemGreen) case .degraded: return Color(.systemOrange) case .offline: return Color(.systemRed) } } var icon: String { switch self { case .healthy: return "checkmark.circle.fill" case .degraded: return "exclamationmark.triangle.fill" case .offline: return "xmark.circle.fill" } } } struct Module: Codable, Identifiable { let id: String let name: String let enabled: Bool let description: String let icon: String let status: ModuleStatus } // MARK: - AuditEvent struct AuditEvent: Codable, Identifiable { let id: String let userId: String let action: String let resource: String let ip: String let timestamp: Date let details: [String: String]? enum CodingKeys: String, CodingKey { case id, action, resource, ip, timestamp, details case userId = "user_id" } } // MARK: - SystemMetrics struct SystemMetrics: Codable { let cpu: Double let ram: Double let disk: Double let servicesHealthy: Int let servicesTotal: Int enum CodingKeys: String, CodingKey { case cpu, ram, disk case servicesHealthy = "services_healthy" case servicesTotal = "services_total" } var servicesRatio: Double { guard servicesTotal > 0 else { return 0 } return Double(servicesHealthy) / Double(servicesTotal) } func statusColor(for value: Double) -> Color { switch value { case ..<0.6: return Color(.systemGreen) case ..<0.8: return Color(.systemOrange) default: return Color(.systemRed) } } } // MARK: - DashboardData struct DashboardData: Codable { let usersCount: Int let modulesHealthy: Int let modulesTotal: Int let recentAudit: [AuditEvent] let metrics: SystemMetrics enum CodingKeys: String, CodingKey { case metrics case usersCount = "users_count" case modulesHealthy = "modules_healthy" case modulesTotal = "modules_total" case recentAudit = "recent_audit" } var modulesRatio: Double { guard modulesTotal > 0 else { return 0 } return Double(modulesHealthy) / Double(modulesTotal) } } // MARK: - JSON Decoder extension JSONDecoder { static let aamos: JSONDecoder = { let d = JSONDecoder() d.dateDecodingStrategy = .iso8601 return d }() } // MARK: - UserRowView struct UserRowView: View { let user: User var body: some View { HStack(spacing: 12) { ZStack { Circle() .fill(user.role.color.opacity(0.15)) .frame(width: 40, height: 40) Image(systemName: user.role.icon) .foregroundStyle(user.role.color) .font(.system(size: 18, weight: .medium)) } VStack(alignment: .leading, spacing: 2) { Text(user.name) .font(.system(.body, design: .default, weight: .semibold)) .foregroundStyle(Color(.label)) Text(user.email) .font(.caption) .foregroundStyle(Color(.secondaryLabel)) } Spacer() Text(user.role.label) .font(.caption2) .fontWeight(.semibold) .padding(.horizontal, 8) .padding(.vertical, 4) .background(user.role.color.opacity(0.15)) .foregroundStyle(user.role.color) .clipShape(Capsule()) } .padding(12) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - ModuleCardView struct ModuleCardView: View { let module: Module var body: some View { HStack(spacing: 12) { ZStack { RoundedRectangle(cornerRadius: 10) .fill(module.status.color.opacity(0.15)) .frame(width: 44, height: 44) Image(systemName: module.icon) .foregroundStyle(module.status.color) .font(.system(size: 20, weight: .medium)) } VStack(alignment: .leading, spacing: 4) { HStack { Text(module.name) .font(.system(.body, design: .default, weight: .semibold)) .foregroundStyle(Color(.label)) Spacer() HStack(spacing: 4) { Image(systemName: module.status.icon) Text(module.status.label) } .font(.caption) .fontWeight(.medium) .foregroundStyle(module.status.color) } Text(module.description) .font(.caption) .foregroundStyle(Color(.secondaryLabel)) .lineLimit(2) } } .padding(12) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) .opacity(module.enabled ? 1 : 0.5) } } // MARK: - AuditEventRowView struct AuditEventRowView: View { let event: AuditEvent private var relativeTime: String { let f = RelativeDateTimeFormatter() f.unitsStyle = .abbreviated return f.localizedString(for: event.timestamp, relativeTo: .now) } var body: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: "clock.arrow.circlepath") .foregroundStyle(Color(.systemBlue)) .font(.system(size: 16, weight: .medium)) .frame(width: 24) .padding(.top, 2) VStack(alignment: .leading, spacing: 4) { HStack { Text(event.action) .font(.system(.subheadline, design: .monospaced, weight: .semibold)) .foregroundStyle(Color(.label)) Spacer() Text(relativeTime) .font(.caption2) .foregroundStyle(Color(.tertiaryLabel)) } Text(event.resource) .font(.caption) .foregroundStyle(Color(.secondaryLabel)) HStack(spacing: 4) { Image(systemName: "network") .font(.caption2) Text(event.ip) .font(.caption2) } .foregroundStyle(Color(.tertiaryLabel)) } } .padding(12) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - MetricGaugeView struct MetricGaugeView: View { let label: String let value: Double let icon: String let metrics: SystemMetrics var body: some View { let color = metrics.statusColor(for: value) VStack(spacing: 8) { ZStack { Circle() .stroke(Color(.systemFill), lineWidth: 6) Circle() .trim(from: 0, to: value) .stroke(color, style: StrokeStyle(lineWidth: 6, lineCap: .round)) .rotationEffect(.degrees(-90)) .animation(.easeOut(duration: 0.6), value: value) VStack(spacing: 1) { Image(systemName: icon) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(color) Text("\(Int(value * 100))%") .font(.system(.caption, design: .rounded, weight: .bold)) .foregroundStyle(Color(.label)) } } .frame(width: 64, height: 64) Text(label) .font(.caption2) .foregroundStyle(Color(.secondaryLabel)) } .padding(12) .frame(maxWidth: .infinity) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - SystemMetricsView struct SystemMetricsView: View { let metrics: SystemMetrics var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Label("Systemstatus", systemImage: "gauge.with.dots.needle.67percent") .font(.headline) .foregroundStyle(Color(.label)) Spacer() HStack(spacing: 4) { Circle() .fill(metrics.servicesRatio >= 0.9 ? Color(.systemGreen) : Color(.systemOrange)) .frame(width: 8, height: 8) Text("\(metrics.servicesHealthy)/\(metrics.servicesTotal) tjänster") .font(.caption) .foregroundStyle(Color(.secondaryLabel)) } } HStack(spacing: 8) { MetricGaugeView(label: "CPU", value: metrics.cpu, icon: "cpu", metrics: metrics) MetricGaugeView(label: "RAM", value: metrics.ram, icon: "memorychip", metrics: metrics) MetricGaugeView(label: "Disk", value: metrics.disk, icon: "internaldrive", metrics: metrics) } } .padding(12) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - StatTileView struct StatTileView: View { let icon: String let value: String let label: String let color: Color var body: some View { VStack(alignment: .leading, spacing: 8) { Image(systemName: icon) .font(.system(size: 22, weight: .semibold)) .foregroundStyle(color) Spacer() Text(value) .font(.system(.title2, design: .rounded, weight: .bold)) .foregroundStyle(Color(.label)) Text(label) .font(.caption) .foregroundStyle(Color(.secondaryLabel)) } .padding(12) .frame(maxWidth: .infinity, minHeight: 96, alignment: .leading) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - DashboardSummaryView struct DashboardSummaryView: View { let data: DashboardData var body: some View { HStack(spacing: 8) { StatTileView( icon: "person.2.fill", value: "\(data.usersCount)", label: "Användare", color: Color(.systemBlue) ) StatTileView( icon: "puzzlepiece.fill", value: "\(data.modulesHealthy)/\(data.modulesTotal)", label: "Moduler", color: data.modulesRatio == 1 ? Color(.systemGreen) : Color(.systemOrange) ) } } } // MARK: - Preview Data #if DEBUG extension User { static let preview = User( id: "u1", email: "admin@aamos.io", name: "Bernt Lind", role: .admin, createdAt: Date().addingTimeInterval(-86400 * 90), lastLogin: Date().addingTimeInterval(-3600) ) static let previewList: [User] = [ preview, User(id: "u2", email: "ops@aamos.io", name: "Sara Nilsson", role: .operator, createdAt: Date().addingTimeInterval(-86400 * 30), lastLogin: Date().addingTimeInterval(-7200)), User(id: "u3", email: "view@aamos.io", name: "Erik Svensson", role: .viewer, createdAt: Date().addingTimeInterval(-86400 * 7), lastLogin: nil) ] } extension Module { static let preview = Module( id: "m1", name: "AAMOS Core", enabled: true, description: "Kärntjänst för agentkörning och orchestration", icon: "cpu.fill", status: .healthy ) static let previewList: [Module] = [ preview, Module(id: "m2", name: "Watchman", enabled: true, description: "Realtidsövervakning av byggen och deployments", icon: "eye.circle.fill", status: .healthy), Module(id: "m3", name: "Mail Relay", enabled: true, description: "SMTP-proxy med krypterad vidarebefordran", icon: "envelope.fill", status: .degraded), Module(id: "m4", name: "Vault", enabled: false, description: "Hemlig lagring och nyckelrotation", icon: "lock.shield.fill", status: .offline) ] } extension AuditEvent { static let previewList: [AuditEvent] = [ AuditEvent(id: "e1", userId: "u1", action: "USER_LOGIN", resource: "/auth/login", ip: "192.168.1.10", timestamp: Date().addingTimeInterval(-600), details: nil), AuditEvent(id: "e2", userId: "u1", action: "MODULE_TOGGLE", resource: "/modules/vault", ip: "192.168.1.10", timestamp: Date().addingTimeInterval(-1800), details: ["module": "vault", "enabled": "false"]), AuditEvent(id: "e3", userId: "u2", action: "CONFIG_UPDATE", resource: "/system/config", ip: "10.0.0.5", timestamp: Date().addingTimeInterval(-3600), details: nil) ] } extension SystemMetrics { static let preview = SystemMetrics( cpu: 0.42, ram: 0.67, disk: 0.31, servicesHealthy: 17, servicesTotal: 20 ) } extension DashboardData { static let preview = DashboardData( usersCount: 8, modulesHealthy: 17, modulesTotal: 20, recentAudit: AuditEvent.previewList, metrics: .preview ) } // MARK: - Previews #Preview("UserRowView") { VStack(spacing: 8) { ForEach(User.previewList) { user in UserRowView(user: user) } } .padding(16) .background(Color(.systemBackground)) } #Preview("ModuleCardView") { NavigationStack { ScrollView { LazyVStack(spacing: 8) { ForEach(Module.previewList) { module in ModuleCardView(module: module) } } .padding(16) } .background(Color(.systemBackground)) .navigationTitle("Moduler") .navigationBarTitleDisplayMode(.large) } } #Preview("AuditEventRowView") { VStack(spacing: 8) { ForEach(AuditEvent.previewList) { event in AuditEventRowView(event: event) } } .padding(16) .background(Color(.systemBackground)) } #Preview("SystemMetricsView") { SystemMetricsView(metrics: .preview) .padding(16) .background(Color(.systemBackground)) } #Preview("DashboardSummaryView") { DashboardSummaryView(data: .preview) .padding(16) .background(Color(.systemBackground)) } #endif