90 lines
4.2 KiB
Swift
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?
|
|
}
|