// // AdminActionsView.swift // FTStore Admin // // The Settings → Scraper tab of the web dashboard: everything you can tell the // server to do right now. The destructive ones keep their confirmation, and // the two that cannot be undone are fenced off in their own section. // import SwiftUI struct AdminActionsView: View { @EnvironmentObject private var session: AdminSession @State private var _findQuery = "" var body: some View { List { _findSection _scrapingSection _maintenanceSection _dangerSection } .navigationTitle("Scraper Actions") .navigationBarTitleDisplayMode(.inline) } // MARK: Find private var _findSection: some View { Section { TextField("App name", text: $_findQuery) .textInputAutocapitalization(.never) .autocorrectionDisabled() AdminActionRow( title: "Find IPA", icon: "magnifyingglass", detail: "Backfill a specific app from the configured channels" ) { let query = _findQuery.trimmingCharacters(in: .whitespaces) guard !query.isEmpty else { return .localized("Type an app name first.") } let ack = try await session.act( .post, "/api/actions/find-ipa", body: AdminRequest.Search(query: query) ) return ack.summary } } header: { Text("Find IPA") } footer: { Text("Queues a targeted search on the worker. It runs as soon as the worker picks the job up.") } } // MARK: Scraping private var _scrapingSection: some View { Section { AdminActionRow( title: "Scan Now", icon: "arrow.clockwise.circle", detail: "Telegram channels and GitHub sources" ) { try await session.act(.post, "/api/actions/scan").summary } AdminActionRow( title: "Scan Previous", icon: "clock.arrow.circlepath", detail: "Walk further back through channel history" ) { try await session.act(.post, "/api/actions/scan-previous").summary } AdminActionRow( title: "Process Queue", icon: "play.circle", detail: "Download and ingest everything pending" ) { try await session.act(.post, "/api/actions/process").summary } AdminActionRow( title: "Publish Feed", icon: "arrow.up.doc", detail: "Re-render ftstore.json from the catalog" ) { try await session.act(.post, "/api/actions/generate-json").summary } } header: { Text("Scraping") } } // MARK: Maintenance private var _maintenanceSection: some View { Section { AdminActionRow( title: "Run Cleanup", icon: "sparkles", detail: "Retention sweep, old logs, expired caches and sessions" ) { try await session.act(.post, "/api/actions/cleanup").summary } AdminActionRow( title: "Clear Expired Cache", icon: "clock.badge.xmark", detail: "Drop stale App Store lookups" ) { try await session.act(.post, "/api/actions/cache").summary } AdminActionRow( title: "Clear All Cache", icon: "trash.slash", detail: "Every App Store lookup is fetched again", confirmation: "Clear the entire App Store cache?" ) { try await session.act(.delete, "/api/actions/cache").summary } // The counterpart one field over: a build the App Store has never // heard of gets no icon either, and the store draws a grey // placeholder for it. The artwork was inside the IPA all along. AdminActionRow( title: "Extract Missing Icons", icon: "photo.on.rectangle", detail: "The server does this by itself. Press for the placeholders to go now. Never replaces App Store artwork" ) { try await session.act(.post, "/api/actions/extract-icons").summary } // Its own row because it answers a specific complaint: a game // sitting in the Apps tab is almost always a cached lookup with // no genre number behind it, and nothing else here fills one in. AdminActionRow( title: "Fill In Missing Genres", icon: "gamecontroller", detail: "Decides which apps belong under Games. Only touches rows that have no genre yet" ) { try await session.act(.post, "/api/actions/cache?mode=genres").summary } } header: { Text("Maintenance") } } // MARK: Danger private var _dangerSection: some View { Section { AdminActionRow( title: "Rebuild Local Storage", icon: "externaldrive.badge.minus", detail: "Deletes every stored IPA and rewinds all scan cursors", destructive: true, confirmation: "Delete every stored IPA and re-download everything? Channels, sources and settings are kept." ) { let ack = try await session.act( .post, "/api/actions/reset-storage", body: AdminRequest.ResetStorage(clearTweakIcons: false) ) return ack.summary } AdminActionRow( title: "Nuke Everything", icon: "exclamationmark.octagon", detail: "Catalog, queue, renames, caches and stored files", destructive: true, confirmation: "This deletes the catalog, the queue, every stored file, all feed renames and the caches. It cannot be undone." ) { try await session.act(.post, "/api/actions/nuke").summary } } header: { Text("Danger Zone") .foregroundStyle(.red) } footer: { Text("Both actions run on the server and cannot be undone from here.") } } }