// // AdminLogsView.swift // FTStore Admin // // The server's activity log — the same paginated, filterable feed the web // dashboard shows, which is usually the fastest way to find out why a build // never made it into the feed. // import SwiftUI struct AdminLogsView: View { @EnvironmentObject private var session: AdminSession @StateObject private var _logs = AdminLoader() @State private var _search = "" @State private var _type: String? @State private var _status: String? @State private var _page = 1 private static let _types = ["scan", "download", "process", "generate", "cleanup", "system"] private static let _statuses = ["success", "info", "warning", "error"] var body: some View { List { _statsSection Section { if _logs.isLoading { AdminLoadingRow() } else if let error = _logs.errorMessage, !_logs.hasLoaded { AdminErrorRow(message: error) { Task { await _reload() } } } else if let entries = _logs.value?.data, entries.isEmpty { AdminEmptyRow( icon: "scroll", title: "Nothing logged", detail: "No entry matches the current filter." ) } else if let entries = _logs.value?.data { ForEach(entries) { entry in AdminActivityRow(entry: entry) } } } header: { if let total = _logs.value?.total { Text( total == 1 ? String.localized("1 entry") : String.localized("%@ entries", AdminFormat.count(total)) ) } } _pagingSection } .navigationTitle("Logs") .navigationBarTitleDisplayMode(.inline) .searchable(text: $_search, prompt: "Message contains") .refreshable { await _reload() } .task { await _reload() } .onChange(of: _search) { _ in _resetAndReload() } .onChange(of: _type) { _ in _resetAndReload() } .onChange(of: _status) { _ in _resetAndReload() } .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { Picker("Type", selection: $_type) { Text("All types").tag(String?.none) ForEach(Self._types, id: \.self) { type in Text(type.capitalized).tag(String?.some(type)) } } Divider() Picker("Status", selection: $_status) { Text("All statuses").tag(String?.none) ForEach(Self._statuses, id: \.self) { status in Text(status.capitalized).tag(String?.some(status)) } } } label: { Image(systemName: _type == nil && _status == nil ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") } } } } // MARK: Sections @ViewBuilder private var _statsSection: some View { if let stats = _logs.value?.stats, !stats.isEmpty { Section { HStack(spacing: 8) { ForEach(stats.keys.sorted(), id: \.self) { key in VStack(spacing: 2) { Text(AdminFormat.count(stats[key] ?? 0)) .font(.subheadline.monospacedDigit()) .foregroundStyle(AdminStatusPill.tint(for: key)) Text(key.capitalized) .font(.caption2) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) } } .padding(.vertical, 4) } } } @ViewBuilder private var _pagingSection: some View { if let response = _logs.value, response.totalPages > 1 { Section { HStack { Button("Previous") { _page = max(1, _page - 1) Task { await _reload() } } .disabled(_page <= 1) Spacer() Text("Page \(response.page) of \(response.totalPages)") .font(.caption) .foregroundStyle(.secondary) Spacer() Button("Next") { _page = min(response.totalPages, _page + 1) Task { await _reload() } } .disabled(_page >= response.totalPages) } .font(.subheadline) } } } // MARK: Loading private func _resetAndReload() { _page = 1 Task { await _reload() } } private func _reload() async { var query = ["page": String(_page), "pageSize": "50"] if let _type { query["type"] = _type } if let _status { query["status"] = _status } let needle = _search.trimmingCharacters(in: .whitespaces) if !needle.isEmpty { query["search"] = needle } await _logs.load { try await session.get("/api/dashboard/activity", query: query, as: AdminActivityResponse.self) } } }