Files
boc/aamos-admin-upgrade/ios/AamosAdmin/Models/Models.swift
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

553 lines
16 KiB
Swift

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