import AppKit // Application-window specific status view: vertical, one line per item, no marquee. final class AppStatusView: NSView { private let lightSvc = TrafficLight() private let lightK3s = TrafficLight() private let lightLocal = TrafficLight() private let labelSvc = NSTextField(labelWithString: "") private let labelK3s = NSTextField(labelWithString: "") private let labelLocal = NSTextField(labelWithString: "") // Top-right timestamp private let timestampLabel: NSTextField = { let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 Z") tf.font = NSFont.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular) tf.textColor = .secondaryLabelColor tf.alignment = .right tf.translatesAutoresizingMaskIntoConstraints = false return tf }() private var checker: ServiceChecker? private let timeFormatter: DateFormatter = { let df = DateFormatter() df.locale = .current df.timeZone = .current df.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZZZ" return df }() override init(frame frameRect: NSRect) { super.init(frame: frameRect) translatesAutoresizingMaskIntoConstraints = false // Configure labels [labelSvc, labelK3s, labelLocal].forEach { l in l.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)) l.textColor = .labelColor l.lineBreakMode = .byTruncatingTail l.translatesAutoresizingMaskIntoConstraints = false } // Rows: [light][label] func row(light: NSView, label: NSTextField) -> NSStackView { let s = NSStackView(views: [light, label]) s.orientation = .horizontal s.alignment = .centerY s.spacing = 8 s.translatesAutoresizingMaskIntoConstraints = false return s } let row1 = row(light: lightSvc, label: labelSvc) let row2 = row(light: lightK3s, label: labelK3s) let row3 = row(light: lightLocal, label: labelLocal) let vstack = NSStackView(views: [row1, row2, row3]) vstack.orientation = .vertical vstack.alignment = .leading vstack.spacing = 10 vstack.translatesAutoresizingMaskIntoConstraints = false addSubview(vstack) addSubview(timestampLabel) NSLayoutConstraint.activate([ vstack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), vstack.topAnchor.constraint(equalTo: topAnchor, constant: 24), timestampLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), timestampLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), timestampLabel.leadingAnchor.constraint(greaterThanOrEqualTo: vstack.trailingAnchor, constant: 12) ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func bindTo(serviceChecker: ServiceChecker) { self.checker = serviceChecker NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: ServiceChecker.statusDidChangeNotification, object: serviceChecker) updateUI() } @objc private func updateUI() { guard let c = checker else { return } let cfg = Config.shared // Timestamp let now = Date() timestampLabel.stringValue = timeFormatter.string(from: now) // SVC lightSvc.state = c.svcReachable ? .green : .red labelSvc.stringValue = "svc: \(cfg.svcHost):\(cfg.svcPort) \(c.svcReachable ? "✓" : "✗") \(c.svcLatency >= 0 ? "(\(c.svcLatency) ms)" : "")" labelSvc.toolTip = c.tooltipForSvc() // K3s aggregate: retropie + pi let aggCount = (c.raspberryReachable ? 1 : 0) + (c.piReachable ? 1 : 0) lightK3s.state = aggCount == 2 ? .green : (aggCount == 1 ? .yellow : .red) labelK3s.stringValue = "k3s: \(cfg.retropieHost) \(c.raspberryReachable ? "✓" : "✗"), \(cfg.piHost) \(c.piReachable ? "✓" : "✗")" labelK3s.toolTip = c.tooltipForAggregateK3s() // Local lightLocal.state = c.localReachable ? .green : .red labelLocal.stringValue = "local: \(cfg.localHost):\(cfg.localPort) \(c.localReachable ? "✓" : "✗") \(c.localLatency >= 0 ? "(\(c.localLatency) ms)" : "")" labelLocal.toolTip = c.tooltipForLocal() needsLayout = true } }