// // AdminUsageView.swift // FTStore Admin // // Who is using this, and what they are taking. // // Two questions the dashboard next door cannot answer. Its counters are about // the catalog — what the server ingested, how the pipeline is doing — and none // of that says whether anybody fetched any of it. // // **What "installs" means here.** The board mints an anonymous row per install // on first contact and stamps a recency time at most once an hour. So these // count installs that have *opened the board*, not everyone running the app. // The screen says that out loud rather than putting it in a tooltip: a number // labelled "users" that quietly meant something narrower is worse than no // number, and an operator making decisions on it deserves to know which. // // Nothing here is per person. No path, no address, no history — a stamp on a // row that already existed, aggregated. Which install downloaded what is a log // this server deliberately does not keep, so this screen cannot show it. // import SwiftUI struct AdminUsageView: View { @EnvironmentObject private var session: AdminSession @StateObject private var _usage = AdminLoader() private var _data: AdminUsage.Payload? { _usage.value?.data } var body: some View { List { if _usage.isLoading && !_usage.hasLoaded { AdminLoadingRow() } else if let error = _usage.errorMessage, !_usage.hasLoaded { AdminErrorRow(message: error) { Task { await _reload() } } } else if let data = _data { _installsSection(data.installs) _boardsSection(data.boards) _downloadsSection(data.downloads) } } .navigationTitle("Usage") .navigationBarTitleDisplayMode(.inline) .refreshable { await _reload() } .task { await _reload() } } // MARK: People @ViewBuilder private func _installsSection(_ installs: AdminUsage.Installs) -> some View { Section { LazyVGrid( columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)], spacing: 10 ) { AdminKPITile( title: "Today", value: AdminFormat.count(installs.seenDay), detail: "installs seen", icon: "iphone.gen3" ) AdminKPITile( title: "This week", value: AdminFormat.count(installs.seenWeek), detail: "installs seen", icon: "calendar" ) AdminKPITile( title: "This month", value: AdminFormat.count(installs.seenMonth), detail: "installs seen", icon: "calendar.badge.clock" ) AdminKPITile( title: "Known", value: AdminFormat.count(installs.known), detail: "since the board opened", icon: "person.2.fill" ) } .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) .listRowBackground(Color.clear) HStack { Text("Registered names") Spacer() Text(verbatim: AdminFormat.count(installs.named)) .foregroundStyle(.secondary) .monospacedDigit() } HStack { Text("Muted") Spacer() Text(verbatim: AdminFormat.count(installs.muted)) .foregroundStyle(installs.muted > 0 ? .red : .secondary) .monospacedDigit() } } header: { Text("People") } footer: { // The caveat belongs on the screen, not behind it. if installs.measured { Text("Counts installs that have opened Feedback or Tweak Requests, not everyone running FTStore. Somebody who only browses the store is not in these. Recency is stamped at most once an hour, and nothing about which install did what is recorded.") } else { Text("Nothing measured yet. Recency is only stamped when an install opens Feedback or Tweak Requests, so these fill in as people do.") } } } // MARK: Boards @ViewBuilder private func _boardsSection(_ boards: AdminUsage.Boards) -> some View { Section { HStack { Label("Open feedback", systemImage: "bubble.left.and.bubble.right") Spacer() Text(verbatim: AdminFormat.count(boards.openFeedback)) .foregroundStyle(.secondary) .monospacedDigit() } HStack { Label("Pending requests", systemImage: "tray.and.arrow.down") Spacer() Text(verbatim: AdminFormat.count(boards.pendingRequests)) .foregroundStyle(.secondary) .monospacedDigit() } } header: { Text("Waiting on you") } footer: { Text(verbatim: String.localized( "%lld reports and %lld requests arrived in the last 7 days.", boards.feedbackThisWeek, boards.requestsThisWeek )) } } // MARK: Downloads @ViewBuilder private func _downloadsSection(_ downloads: AdminUsage.Downloads) -> some View { Section { if downloads.apps.isEmpty { AdminEmptyRow( icon: "arrow.down.circle", title: "Nothing downloaded yet", detail: "This counts what people fetched, not what the scraper ingested." ) } else { ForEach(downloads.apps) { app in HStack(alignment: .firstTextBaseline, spacing: 10) { VStack(alignment: .leading, spacing: 2) { Text(app.name) .font(.subheadline) .lineLimit(2) Text(app.bundleId) .font(.caption2.monospaced()) .foregroundStyle(.tertiary) .lineLimit(1) .truncationMode(.middle) } Spacer(minLength: 8) Text(verbatim: AdminFormat.count(app.downloads)) .font(.subheadline.weight(.semibold).monospacedDigit()) .foregroundStyle(AdminTheme.brand) } .padding(.vertical, 2) } } } header: { Text("Most downloaded") } footer: { Text(verbatim: _downloadsFooter(downloads)) } } /// Built outside the builder: a `var` and a `return` inside a `@ViewBuilder` /// closure is the kind of thing that compiles until it doesn't. private func _downloadsFooter(_ downloads: AdminUsage.Downloads) -> String { var line = String.localized( "%@ downloads all time. Versions of one app are counted together.", AdminFormat.count(downloads.total) ) if downloads.truncated { line += " " + .localized("The ranking covers the most-fetched builds only.") } return line } // MARK: Actions private func _reload() async { await _usage.load { try await session.get("/api/dashboard/usage", as: AdminUsage.self) } } }