import AppKit final class StatusView: NSView { private let light1 = TrafficLight() private let light2 = TrafficLight() private let light3 = TrafficLight() private let timestampLabel: NSTextField = { let tf = NSTextField(labelWithString: "--:--:--") tf.textColor = .secondaryLabelColor tf.alignment = .left tf.font = NSFont.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular) tf.setContentHuggingPriority(.required, for: .horizontal) tf.setContentCompressionResistancePriority(.required, for: .horizontal) return tf }() private var checker: ServiceChecker? private let timeFormatter: DateFormatter = { let df = DateFormatter() df.locale = .current df.timeZone = .current df.dateFormat = "HH:mm:ss" return df }() private let fullFormatter: DateFormatter = { let df = DateFormatter() df.locale = .current df.timeZone = .current df.dateStyle = .medium df.timeStyle = .medium return df }() override init(frame frameRect: NSRect) { super.init(frame: frameRect) wantsLayer = true translatesAutoresizingMaskIntoConstraints = false let stack = NSStackView(views: [light1, light2, light3, timestampLabel]) stack.orientation = .horizontal stack.alignment = .centerY stack.distribution = .equalSpacing stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false addSubview(stack) NSLayoutConstraint.activate([ stack.centerXAnchor.constraint(equalTo: centerXAnchor), stack.centerYAnchor.constraint(equalTo: centerYAnchor) ]) // Tracking for hover tooltips addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .inVisibleRect], owner: self, userInfo: nil)) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func hitTest(_ point: NSPoint) -> NSView? { // Pass all clicks through to apps beneath return nil } func bindTo(serviceChecker: ServiceChecker) { self.checker = serviceChecker NotificationCenter.default.addObserver(self, selector: #selector(updateLights), name: ServiceChecker.statusDidChangeNotification, object: serviceChecker) updateLights() } @objc private func updateLights() { guard let c = checker else { return } // Light 1: svc.prole.org:443 light1.state = c.svcReachable ? .green : .red light1.toolTip = c.tooltipForSvc() // Light 2: aggregate raspberry + pi let count = (c.raspberryReachable ? 1 : 0) + (c.piReachable ? 1 : 0) light2.state = count == 2 ? .green : (count == 1 ? .yellow : .red) light2.toolTip = c.tooltipForAggregateK3s() // Light 3: localhost k3d light3.state = c.localReachable ? .green : .red light3.toolTip = c.tooltipForLocal() // Timestamp label let now = Date() timestampLabel.stringValue = timeFormatter.string(from: now) timestampLabel.toolTip = "Last updated: " + fullFormatter.string(from: now) needsDisplay = true } func aggregateStatusSummary() -> String { guard let c = checker else { return "No status" } let svc = c.svcReachable ? "svc.prole.org:443 ✓" : "svc.prole.org:443 ✗" let agg = "k3s: raspberry \(c.raspberryReachable ? "✓" : "✗"), pi \(c.piReachable ? "✓" : "✗")" let loc = c.localReachable ? "k3d localhost:6443 ✓" : "k3d localhost:6443 ✗" return [svc, agg, loc].joined(separator: " • ") } } final class TrafficLight: NSView { enum State { case green, yellow, red } var state: State = .red { didSet { needsDisplay = true } } override var intrinsicContentSize: NSSize { NSSize(width: 14, height: 14) } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) let rect = bounds.insetBy(dx: 1, dy: 1) let path = NSBezierPath(ovalIn: rect) let color: NSColor switch state { case .green: color = NSColor.systemGreen case .yellow: color = NSColor.systemYellow case .red: color = NSColor.systemRed } color.setFill() path.fill() // subtle ring for better contrast against menu material NSColor.black.withAlphaComponent(0.12).setStroke() path.lineWidth = 1 path.stroke() } }