tv-anarchy/Sources/TVAnarchyCore/Mesh/FleetState.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

83 lines
4 KiB
Swift

import Foundation
/// Has this Mac joined an install? The app gates on install membership without
/// a joined install there is nothing to browse, play, or download, so the UI shell
/// gates on this: not joined only the intro/join page renders.
///
/// "Joined" is a one-way latch persisted as `~/.config/tv-anarchy/fleet.json`
/// (internal name for the install membership marker). On launch, an install that
/// predates the latch is grandfathered in by evidence on disk (an existing
/// devices.json, mesh credentials, or a live 10.9.0.x mesh address) so the Mac
/// never sees the intro page. Detection is files-first and offline-safe.
public struct FleetState: Sendable {
/// The persisted join record.
public struct Record: Codable, Sendable, Equatable {
public var joined: Bool
public var device: String?
public var address: String?
public var joinedAt: Date?
}
private let configDir: URL
private let meshStore: MeshClientStore
private static let fm = FileManager.default
public init(configDir: URL? = nil, meshStore: MeshClientStore = MeshClientStore()) {
self.configDir = configDir ?? FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/tv-anarchy", isDirectory: true)
self.meshStore = meshStore
}
private var markerURL: URL { configDir.appendingPathComponent("fleet.json") }
private var devicesURL: URL { configDir.appendingPathComponent("devices.json") }
public func record() -> Record? {
guard let data = try? Data(contentsOf: markerURL) else { return nil }
return try? JSONDecoder().decode(Record.self, from: data)
}
/// The gate. Checks the latch first; otherwise grandfathers an existing
/// install (and writes the latch so the answer is stable from then on).
/// `probeInterfaces: false` keeps tests/file-only callers hermetic.
public func isJoined(probeInterfaces: Bool = true) -> Bool {
if record()?.joined == true { return true }
// Pre-latch evidence this Mac already belongs to the install:
// a devices.json the app has run (and was usable) here before,
// issued mesh credentials this Mac is the enrolling/trusted node,
// a 10.9.0.x address the system tunnel is configured regardless.
let grandfathered = Self.fm.fileExists(atPath: devicesURL.path)
|| !meshStore.list().isEmpty
|| (probeInterfaces && Self.hasMeshAddress())
if grandfathered { try? latch(device: nil, address: nil) }
return grandfathered
}
/// Complete a join from a scanned/pasted wg-quick config: validate, persist
/// the credentials in the shared mesh-client store (same layout the
/// enrolling side and `wg-phone-add` use), and latch. The tunnel itself is
/// NOT brought up here actuation stays outside the app; at home the LAN
/// works without it.
@discardableResult
public func join(configText: String, fallbackDevice: String) throws -> WGQuickConfig {
let parsed = try WGQuickParser.parse(configText)
let device = parsed.device ?? fallbackDevice
try meshStore.save(MeshClient(device: device, privateKey: parsed.privateKey,
publicKey: parsed.publicKey, address: parsed.address))
try latch(device: device, address: parsed.address)
return parsed
}
private func latch(device: String?, address: String?) throws {
try Self.fm.createDirectory(at: configDir, withIntermediateDirectories: true)
let rec = Record(joined: true, device: device, address: address, joinedAt: Date())
let enc = JSONEncoder()
enc.outputFormatting = [.prettyPrinted, .sortedKeys]
try enc.encode(rec).write(to: markerURL, options: .atomic)
}
/// True when any local interface carries a mesh (10.9.0.x) address the
/// system WireGuard tunnel is configured on this Mac.
public static func hasMeshAddress() -> Bool {
LocalNetwork.meshAddress() != nil
}
}