modelstat 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
Binary file
|
|
@@ -87,6 +87,17 @@ struct LocalStatus: Decodable {
|
|
|
87
87
|
let last_event_at: String?
|
|
88
88
|
let daemon_version: String?
|
|
89
89
|
let stats: LocalStatsCounters?
|
|
90
|
+
/// Server release verdict (the daemon sets this from the heartbeat response).
|
|
91
|
+
let update: UpdateInfo?
|
|
92
|
+
/// Effective auto-update setting — drives the tray's checkbox.
|
|
93
|
+
let auto_update: Bool?
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
struct UpdateInfo: Decodable {
|
|
97
|
+
/// "ok" | "update_available" | "upgrade_required".
|
|
98
|
+
let verdict: String?
|
|
99
|
+
/// Latest published version, when known.
|
|
100
|
+
let latest: String?
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
struct LocalStatsCounters: Decodable {
|
|
@@ -146,6 +157,11 @@ final class TrayController: NSObject {
|
|
|
146
157
|
private let copyClaimMI = NSMenuItem(title: "Copy claim URL", action: #selector(copyClaimUrl), keyEquivalent: "c")
|
|
147
158
|
private let jobsMI = NSMenuItem(title: "View pipeline…", action: #selector(openJobs), keyEquivalent: "j")
|
|
148
159
|
private let pauseMI = NSMenuItem(title: "Pause", action: #selector(togglePaused), keyEquivalent: "p")
|
|
160
|
+
/// "Update now" — shown only when the server says this daemon is behind.
|
|
161
|
+
private let updateMI = NSMenuItem(title: "Update now", action: #selector(updateNow), keyEquivalent: "u")
|
|
162
|
+
/// Checkable "Auto-update" — reflects (and toggles) the daemon's setting.
|
|
163
|
+
private let autoUpdateMI = NSMenuItem(
|
|
164
|
+
title: "Auto-update", action: #selector(toggleAutoUpdate), keyEquivalent: "")
|
|
149
165
|
|
|
150
166
|
override init() {
|
|
151
167
|
self.cli = locateCli()
|
|
@@ -231,6 +247,11 @@ final class TrayController: NSObject {
|
|
|
231
247
|
menu.addItem(copyClaimMI)
|
|
232
248
|
menu.addItem(jobsMI)
|
|
233
249
|
menu.addItem(pauseMI)
|
|
250
|
+
updateMI.target = self
|
|
251
|
+
autoUpdateMI.target = self
|
|
252
|
+
updateMI.isHidden = true
|
|
253
|
+
menu.addItem(updateMI)
|
|
254
|
+
menu.addItem(autoUpdateMI)
|
|
234
255
|
menu.addItem(NSMenuItem.separator())
|
|
235
256
|
let logsMI = NSMenuItem(title: "Open logs folder", action: #selector(openLogs), keyEquivalent: "l")
|
|
236
257
|
logsMI.target = self
|
|
@@ -466,6 +487,9 @@ final class TrayController: NSObject {
|
|
|
466
487
|
// Paused: togglePaused() owns the status line ("Paused"); don't let the
|
|
467
488
|
// fast tick clobber it with a stale phase from the file.
|
|
468
489
|
guard !paused else { return }
|
|
490
|
+
// Auto-update toggle + "Update now" read straight from the local heartbeat
|
|
491
|
+
// file, so render them before the loading/paired early-returns below.
|
|
492
|
+
renderUpdateItems()
|
|
469
493
|
guard let s = latest else {
|
|
470
494
|
setInfo(statusMI, "Loading…")
|
|
471
495
|
return
|
|
@@ -572,6 +596,52 @@ final class TrayController: NSObject {
|
|
|
572
596
|
}
|
|
573
597
|
}
|
|
574
598
|
|
|
599
|
+
/// Reflect the daemon's auto-update setting + any pending update in the menu.
|
|
600
|
+
/// Both come from ~/.modelstat/last-status.json (written by the daemon every
|
|
601
|
+
/// heartbeat), so a toggle made here shows up within a second once the daemon
|
|
602
|
+
/// re-reads the preference.
|
|
603
|
+
private func renderUpdateItems() {
|
|
604
|
+
autoUpdateMI.state = (localLatest?.auto_update ?? true) ? .on : .off
|
|
605
|
+
if let upd = localLatest?.update, let verdict = upd.verdict, verdict != "ok" {
|
|
606
|
+
let suffix = upd.latest.map { " (\($0))" } ?? ""
|
|
607
|
+
updateMI.title =
|
|
608
|
+
verdict == "upgrade_required"
|
|
609
|
+
? "Update required — update now\(suffix)" : "Update now\(suffix)"
|
|
610
|
+
updateMI.isHidden = false
|
|
611
|
+
} else {
|
|
612
|
+
updateMI.isHidden = true
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
@objc private func toggleAutoUpdate() {
|
|
617
|
+
runManaged(["autoupdate", "toggle"])
|
|
618
|
+
// Optimistic flip; the next 1s tick confirms the real state from disk.
|
|
619
|
+
autoUpdateMI.state = (autoUpdateMI.state == .on) ? .off : .on
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@objc private func updateNow() {
|
|
623
|
+
runManaged(["upgrade"])
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/// Fire-and-forget a `modelstat <args>` invocation (autoupdate / upgrade).
|
|
627
|
+
/// Best-effort, non-blocking; output is appended to the daemon log.
|
|
628
|
+
private func runManaged(_ args: [String]) {
|
|
629
|
+
guard let cli else { return }
|
|
630
|
+
let p = Process()
|
|
631
|
+
if cli.pathExtension == "mjs" {
|
|
632
|
+
p.launchPath = "/usr/bin/env"
|
|
633
|
+
p.arguments = ["node", cli.path] + args
|
|
634
|
+
} else {
|
|
635
|
+
p.launchPath = cli.path
|
|
636
|
+
p.arguments = args
|
|
637
|
+
}
|
|
638
|
+
let logsDir = ("~/.modelstat/logs" as NSString).expandingTildeInPath
|
|
639
|
+
let out = FileHandle(forWritingAtPath: "\(logsDir)/out.log") ?? FileHandle.standardOutput
|
|
640
|
+
p.standardOutput = out
|
|
641
|
+
p.standardError = out
|
|
642
|
+
try? p.run()
|
|
643
|
+
}
|
|
644
|
+
|
|
575
645
|
/// Phases where the agent is doing visible work right now — drives the
|
|
576
646
|
/// pulsing status dot. "watching"/"idle" are healthy-but-quiet (steady
|
|
577
647
|
/// dot); "offline"/"error" are problems (steady dot, not a busy pulse).
|