Files
Felitendo a75d295897 feat: fill in the missing artwork without being asked
Taking an app's icon out of its build fixed every app arriving from then on and
nothing already in the catalog, unless somebody found the button for it. That is
the wrong way round: the placeholders people are looking at are on the builds
that are already there, and a fix nobody presses is a fix that has not happened.

So the server now does it on its own. A pass runs every half minute while there
are builds it has not looked inside and once an hour when there are none, which
means a deployment works through a catalog full of grey squares in minutes and
then costs one counted query an hour. The button stays, for wanting it now
rather than within the hour, and both go through the same pass.

Every build examined is recorded as examined, whether or not artwork came out of
it. Without that the pass keeps re-opening the same archives that have nothing
in them and never reaches the ones behind them, which is a backfill that runs
forever and finishes nothing. A build recorded with no icon has been looked
inside and had none; re-examining those is a switch on the request, for the day
the extractor learns something new.
2026-08-31 13:54:48 +02:00

188 lines
5.1 KiB
Swift

//
// 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.")
}
}
}