tv-anarchy/Sources/TVAnarchyCore/HostStats.swift
Natalie 0a4cde36d1 feat(devices): add dependency issue warnings
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:57:08 -07:00

90 lines
4.2 KiB
Swift

import Foundation
/// Host load for a target load averages plus mpv's instantaneous decode %CPU
/// (100 = one core), the figure that actually moves when you change quality.
public struct HostStats: Decodable, Sendable, Equatable {
public let load1: Double
public let load5: Double
public let load15: Double
public let cores: Int
public let mpv_cpu: Double? // null when nothing is playing
/// sha256 of the helper script that served this report the Devices tab
/// compares it against the repo's vendored copy to flag stale deploys.
/// Absent from helpers that predate self-reporting (itself a stale sign).
public let helper_sha: String?
/// Dependent-service facts (absent from pre-deps helpers).
public let deps: HostDeps?
}
/// Facts about the services a device's duties depend on, as the helper reports
/// them raw states only. What's *interesting* (worth surfacing in the UI) is
/// judged app-side by `issues`, so the thresholds live in one testable place.
public struct HostDeps: Decodable, Sendable, Equatable {
public let transmission: String? // systemd state: active/inactive/failed/unknown
public let mpv_unit: String? // the player unit; inactive just means idle
public let mpv_socket: Bool?
public let media_root: Bool? // does the media root directory exist
public let display: String? // DRM connector: connected/disconnected/unknown
public let disk_free_gb: Double? // free space under the media root
public let disk_used_pct: Int?
public init(transmission: String? = nil, mpv_unit: String? = nil,
mpv_socket: Bool? = nil, media_root: Bool? = nil, display: String? = nil,
disk_free_gb: Double? = nil, disk_used_pct: Int? = nil) {
self.transmission = transmission; self.mpv_unit = mpv_unit
self.mpv_socket = mpv_socket; self.media_root = media_root; self.display = display
self.disk_free_gb = disk_free_gb; self.disk_used_pct = disk_used_pct
}
}
/// One dependent-service fact, rendered and judged. `error` means a duty is
/// broken right now (transmission down, media root gone, disk effectively
/// full); `warning` means degraded / heading for trouble (disk filling, TV
/// unplugged, stale socket); `ok` is background detail for the summary.
public struct DepFact: Equatable, Sendable {
public enum Severity: Equatable, Sendable, Comparable { case ok, warning, error }
public let text: String
public let severity: Severity
public init(_ text: String, _ severity: Severity = .ok) {
self.text = text; self.severity = severity
}
}
public extension HostDeps {
/// Every reported fact, judged the single place the thresholds live. The
/// summary shows them all; the row surfaces only the non-ok subset.
var facts: [DepFact] {
var out: [DepFact] = []
if let t = transmission {
out.append(DepFact("transmission \(t)", t == "active" ? .ok : .error))
}
if media_root == false {
out.append(DepFact("media root missing", .error))
}
if let pct = disk_used_pct {
let free = disk_free_gb.map {
$0 >= 1024 ? String(format: ", %.1f TB free", $0 / 1024)
: String(format: ", %.0f GB free", $0)
} ?? ""
out.append(DepFact("disk \(pct)% full\(free)",
pct >= 97 ? .error : pct >= 90 ? .warning : .ok))
}
if let d = display {
out.append(DepFact("TV \(d)", d == "connected" ? .ok : .warning))
}
if mpv_socket == true, let u = mpv_unit, u != "active" {
out.append(DepFact("stale mpv socket (unit \(u))", .warning))
}
return out
}
/// The "interesting" subset empty when everything is healthy, which is
/// exactly when the row should stay quiet.
var issues: [DepFact] { facts.filter { $0.severity != .ok } }
}
/// A target that can report its host's load (only black it's a real machine
/// we have a shell on; VLC-on-plum is local and not interesting to chart).
public protocol HostStatsProvider: AnyObject {
func stats() async -> HostStats?
}