import Foundation import SwiftUI // MARK: - Build Record struct BuildRecord: Identifiable, Codable { let id: UUID let projectName: String let scheme: String let date: Date let duration: TimeInterval let action: String let configuration: String let destination: String let outcome: Outcome let errorCount: Int let warningCount: Int let logSnippet: String enum Outcome: String, Codable { case success, failure, cancelled var icon: String { switch self { case .success: return "checkmark.circle.fill" case .failure: return "xmark.circle.fill" case .cancelled: return "stop.circle.fill" } } var color: Color { switch self { case .success: return .green case .failure: return .red case .cancelled: return .secondary } } } var formattedDuration: String { if duration < 60 { return String(format: "%.1fs", duration) } let m = Int(duration / 60); let s = Int(duration) % 60 return "\(m)m \(s)s" } var formattedDate: String { let f = RelativeDateTimeFormatter() f.unitsStyle = .abbreviated return f.localizedString(for: date, relativeTo: Date()) } } // MARK: - Store @MainActor final class BuildHistoryStore: ObservableObject { @Published var records: [BuildRecord] = [] private let maxRecords = 50 private let key = "padxcode.buildHistory" init() { load() } func append(_ record: BuildRecord) { records.insert(record, at: 0) if records.count > maxRecords { records = Array(records.prefix(maxRecords)) } persist() } func clear() { records = []; persist() } func delete(id: UUID) { records.removeAll { $0.id == id }; persist() } private func persist() { if let d = try? JSONEncoder().encode(records) { UserDefaults.standard.set(d, forKey: key) } } private func load() { guard let d = UserDefaults.standard.data(forKey: key), let v = try? JSONDecoder().decode([BuildRecord].self, from: d) else { return } records = v } } // MARK: - Views (defined exactly once) struct BuildHistoryView: View { @ObservedObject var historyStore: BuildHistoryStore @State private var showClearAlert = false @State private var selectedRecord: BuildRecord? var body: some View { NavigationStack { Group { if historyStore.records.isEmpty { ContentUnavailableView( "No Build History", systemImage: "clock.badge.xmark", description: Text("Successful and failed builds appear here.") ) } else { List { ForEach(historyStore.records) { record in BuildRecordRow(record: record) .contentShape(Rectangle()) .onTapGesture { selectedRecord = record } .swipeActions { Button(role: .destructive) { historyStore.delete(id: record.id) } label: { Label("Delete", systemImage: "trash") } } } } .listStyle(.plain) } } .navigationTitle("Build History") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(role: .destructive) { showClearAlert = true } label: { Image(systemName: "trash") } .disabled(historyStore.records.isEmpty) } } .alert("Clear Build History?", isPresented: $showClearAlert) { Button("Clear All", role: .destructive) { historyStore.clear() } Button("Cancel", role: .cancel) {} } .sheet(item: $selectedRecord) { record in BuildRecordDetailView(record: record) } } } } struct BuildRecordRow: View { let record: BuildRecord var body: some View { HStack(spacing: 12) { Image(systemName: record.outcome.icon) .foregroundStyle(record.outcome.color) .font(.title3) .frame(width: 28) VStack(alignment: .leading, spacing: 3) { HStack { Text(record.scheme).font(.subheadline.bold()) Text("·").foregroundStyle(.tertiary) Text(record.configuration).font(.subheadline).foregroundStyle(.secondary) } HStack(spacing: 6) { Text(record.destination).font(.caption).foregroundStyle(.secondary) Text("·").foregroundStyle(.tertiary) Text(record.formattedDuration).font(.caption).foregroundStyle(.secondary) if record.errorCount > 0 { Text("·").foregroundStyle(.tertiary) Text("\(record.errorCount) error(s)").font(.caption).foregroundStyle(.red) } } } Spacer() Text(record.formattedDate).font(.caption2).foregroundStyle(.tertiary) } .padding(.vertical, 4) } } struct BuildRecordDetailView: View { let record: BuildRecord @Environment(\.dismiss) var dismiss var body: some View { NavigationStack { List { Section("Result") { LabeledContent("Outcome") { Label(record.outcome.rawValue.capitalized, systemImage: record.outcome.icon) .foregroundStyle(record.outcome.color) } LabeledContent("Duration", value: record.formattedDuration) LabeledContent("Date", value: record.date.formatted()) } Section("Project") { LabeledContent("Project", value: record.projectName) LabeledContent("Scheme", value: record.scheme) LabeledContent("Configuration", value: record.configuration) LabeledContent("Destination", value: record.destination) } if record.errorCount > 0 || record.warningCount > 0 { Section("Diagnostics") { LabeledContent("Errors", value: "\(record.errorCount)") LabeledContent("Warnings", value: "\(record.warningCount)") } } Section("Log Tail") { ScrollView { Text(record.logSnippet) .font(.system(.caption, design: .monospaced)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxHeight: 300) } } .navigationTitle("Build Detail") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } } .presentationDetents([.large]) } }