Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Change log

2025-01-19: v2.5.4 - Better handling of the health status window.

2025-01-15: v2.5.3 - Actively update health status window if it is presented.

2025-12-24: v2.5.1 - Log health status rates below 1.0.

2025-12-21: v2.5.0 - Add basic hardware, OS, and jamfStatus app usage collection. Data is sent anonymously to [TelemetryDeck](https://telemetrydeck.com) to aid in the development of the app. View 'About...' to opt out of sending the data. Add ability to view health-status for Jamf Cloud hosted instances. Fix an issue with token renewal.

2023-12-06: v2.4.1 - Minor updates to the alerts display.

2023-11-11: v2.4.0 - Fix issue with notifications not being displayed. Add ability to use API client.

2023-04-07: v2.3.6 - Update logging to prevent potential looping.

2022-10-02: v2.3.2 - Rework authentication/token refresh.

2022-06-12: v2.3.1 - Clean up notificatations not displaying properly.

2021-10-15: v2.3.0 - Updated notifications display.
22 changes: 4 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# jamfStatus
![GitHub release (latest by date)](https://img.shields.io/github/v/release/jamf/jamfStatus?display_name=tag) ![GitHub all releases](https://img.shields.io/github/downloads/jamf/jamfStatus/total) ![GitHub latest release](https://img.shields.io/github/downloads/jamf/jamfStatus/latest/total)
![GitHub issues](https://img.shields.io/github/issues-raw/jamf/jamfStatus) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/jamf/jamfStatus) ![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/jamf/jamfStatus) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/jamf/jamfStatus)

Download: [jamfStatus](https://github.com/jamf/jamfStatus/releases/latest/download/jamfStatus.zip)

The application submits basic hardware, OS, and jamfStatus application usage to [TelemetryDeck](https://telemetrydeck.com) by default. The data is sent anonymously and can be disabled by clicking 'Opt out of analytics' from the About window.
The application submits basic hardware, OS, and jamfStatus application usage to [TelemetryDeck](https://telemetrydeck.com) by default. The data is sent anonymously and can be disabled by clicking 'Opt out of analytics' from the 'About...' window.

Keep an eye on the status of Jamf Cloud with jamfStatus. The app will place an icon in the menu bar to reflect the current cloud status.

Expand Down Expand Up @@ -123,20 +126,3 @@ Thu Sep 17 20:27:30 Jamf Cloud: All systems go.
* Major Update for Jamf Connect Now Available (Jamf Connect <latestVersion>)
* Device Compliance Connection Interrupted
* Conditional Access Connection Interrupted


## Change log

2025-12-21: v2.5.0 - Add basic hardware, OS, and jamfStatus app usage collection. Data is sent anonymously to [TelemetryDeck](https:telemetrydeck.com) to aid in the development of the app. View 'About...' to opt out of sending the data. Add ability to view health-status for Jamf Cloud hosted instances. Fix an issue with token renewal.

2023-12-06: v2.4.1 - Minor updates to the alerts display.

2023-11-11: v2.4.0 - Fix issue with notifications not being displayed. Add ability to use API client.

2023-04-07: v2.3.6 - Update logging to prevent potential looping.

2022-10-02: v2.3.2 - Rework authentication/token refresh.

2022-06-12: v2.3.1 - Clean up notificatations not displaying properly.

2021-10-15: v2.3.0 - Updated notifications display.
4 changes: 2 additions & 2 deletions jamfStatus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@
"$(SDKROOT)/usr/lib/system/introspection",
);
MACOSX_DEPLOYMENT_TARGET = 12.4;
MARKETING_VERSION = 2.5.0;
MARKETING_VERSION = 2.5.4;
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.pse.jamfStatus;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -441,7 +441,7 @@
"$(SDKROOT)/usr/lib/system/introspection",
);
MACOSX_DEPLOYMENT_TARGET = 12.4;
MARKETING_VERSION = 2.5.0;
MARKETING_VERSION = 2.5.4;
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.pse.jamfStatus;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
161 changes: 96 additions & 65 deletions jamfStatus/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import WebKit
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {

func applicationDidFinishLaunching(_ notification: Notification) {
NotificationCenter.default.addObserver(self, selector: #selector(updateHealthStatusView(_:)), name: .updateHealthStatusView, object: nil)
}

@IBOutlet weak var cloudStatus_Toolbar: NSToolbar!
@IBOutlet weak var cloudStatusWindow: NSWindow!
@IBOutlet weak var healthStatus_Window: NSWindow!

@IBOutlet weak var showHealthStatus_MenuItem: NSMenuItem!

@IBOutlet var page_WebView: WKWebView!
@IBOutlet weak var prefs_Window: NSWindow!
Expand Down Expand Up @@ -50,10 +57,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {
TelemetryDeckConfig.OptOut = (sender.state == .on)
}



@IBOutlet weak var healthStatus_Window: NSWindow!

let prefs = Preferences.self

let fm = FileManager()
Expand Down Expand Up @@ -170,25 +173,39 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {
}
}

@IBAction func credentials_Action(_ sender: Any) {
JamfProServer.url = jamfServerUrl_TextField.stringValue

let urlRegex = try! NSRegularExpression(pattern: "/?failover(.*?)", options:.caseInsensitive)
JamfProServer.url = urlRegex.stringByReplacingMatches(in: JamfProServer.url, options: [], range: NSRange(0..<JamfProServer.url.utf16.count), withTemplate: "")

defaults.set(JamfProServer.url, forKey: "jamfServerUrl")
// defaults.synchronize()
@IBAction func credentials_Action(_ sender: NSTextField) {

JamfProServer.username = username_TextField.stringValue
JamfProServer.password = password_TextField.stringValue
switch sender.identifier?.rawValue {
case "jamfProURL":
JamfProServer.url = jamfServerUrl_TextField.stringValue
if jamfServerUrl_TextField.stringValue.baseUrl.isEmpty {
DispatchQueue.main.async { [self] in
jamfServerUrl_TextField.becomeFirstResponder()
alert_dialog(header: "", message: "Invlid URL\nMust be in the format: https://server.example.com", updateAvail: false)
if siteConnectionStatus_ImageView.image != statusImage[0] {
siteConnectionStatus_ImageView.image = statusImage[0]
}
return
}
} else {
JamfProServer.url = jamfServerUrl_TextField.stringValue.baseUrl
jamfServerUrl_TextField.stringValue = JamfProServer.url
}
case "account":
JamfProServer.username = username_TextField.stringValue
case "credential":
JamfProServer.password = password_TextField.stringValue
default:
break
}

saveCreds(server: JamfProServer.url, username: JamfProServer.username, password: JamfProServer.password)
}

// actions for preferences window - start

func fetchPassword() {
let credentialsArray = Credentials().itemLookup(service: jamfServerUrl_TextField.stringValue.fqdnFromUrl)
let credentialsArray = Credentials().itemLookup(service: jamfServerUrl_TextField.stringValue.fqdn)
if credentialsArray.count == 2 {
username_TextField.stringValue = credentialsArray[0]
password_TextField.stringValue = credentialsArray[1]
Expand Down Expand Up @@ -239,24 +256,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {
} // func alert_dialog - end

func saveCreds(server: String, username: String, password: String) {
if ( server != "" && username != "" && password != "" ) {

let urlRegex = try! NSRegularExpression(pattern: "http(.*?)://", options:.caseInsensitive)
let serverFqdn = urlRegex.stringByReplacingMatches(in: server, options: [], range: NSRange(0..<server.utf16.count), withTemplate: "")

if !( server.isEmpty || username.isEmpty || password.isEmpty ) {

JamfProServer.base64Creds = ("\(username):\(password)".data(using: .utf8)?.base64EncodedString())!
token.isValid = false
JamfProServer.validToken = false

// update the connection indicator for the site server
Task {
if await TokenManager.shared.tokenInfo?.renewToken ?? true {
await TokenManager.shared.setToken(serverUrl: JamfProServer.url, username: JamfProServer.username.lowercased(), password: JamfProServer.password)
}

if await TokenManager.shared.tokenInfo?.authMessage ?? "" == "success" {
defaults.set(JamfProServer.url, forKey: "jamfServerUrl")
DispatchQueue.main.async {
self.siteConnectionStatus_ImageView.image = self.statusImage[1]
}
Credentials().save(service: server.fqdnFromUrl, account: username, data: password)
Credentials().save(service: server.fqdn, account: username, data: password)
} else {
print("authentication failed")
DispatchQueue.main.async {
Expand Down Expand Up @@ -296,11 +312,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {
iconStyle_Button.selectItem(at: 1)
}

let serverUrl = defaults.string(forKey:"jamfServerUrl") ?? ""
if serverUrl != "" {
var serverUrl = defaults.string(forKey:"jamfServerUrl") ?? ""
serverUrl = serverUrl.baseUrl
if !serverUrl.isEmpty {
jamfServerUrl_TextField.stringValue = serverUrl

let credentialsArray = Credentials().itemLookup(service: serverUrl.fqdnFromUrl)
let credentialsArray = Credentials().itemLookup(service: serverUrl.fqdn)
if credentialsArray.count == 2 {
JamfProServer.username = credentialsArray[0]
JamfProServer.password = credentialsArray[1]
Expand Down Expand Up @@ -348,45 +365,56 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {
@IBOutlet weak var default_15m_TextField: NSTextField!
@IBOutlet weak var default_30m_TextField: NSTextField!

@IBAction func showHealthStatus_MenuItem(_ sender: NSMenuItem) {
if let api = HealthStatusStore.shared.healthStatus?.api,
let ui = HealthStatusStore.shared.healthStatus?.ui,
let enrollment = HealthStatusStore.shared.healthStatus?.enrollment,
let device = HealthStatusStore.shared.healthStatus?.device,
let defaultStatus = HealthStatusStore.shared.healthStatus?.healthStatusDefault {

api_30s_TextField.stringValue = "\(Int(api.thirtySeconds * 100))%"
api_1m_TextField.stringValue = "\(Int(api.oneMinute * 100))%"
api_5m_TextField.stringValue = "\(Int(api.fiveMinutes * 100))%"
api_15m_TextField.stringValue = "\(Int(api.fifteenMinutes * 100))%"
api_30m_TextField.stringValue = "\(Int(api.thirtyMinutes * 100))%"

ui_30s_TextField.stringValue = "\(Int(ui.thirtySeconds * 100))%"
ui_1m_TextField.stringValue = "\(Int(ui.oneMinute * 100))%"
ui_5m_TextField.stringValue = "\(Int(ui.fiveMinutes * 100))%"
ui_15m_TextField.stringValue = "\(Int(ui.fifteenMinutes * 100))%"
ui_30m_TextField.stringValue = "\(Int(ui.thirtyMinutes * 100))%"

enrollment_30s_TextField.stringValue = "\(Int(enrollment.thirtySeconds * 100))%"
enrollment_1m_TextField.stringValue = "\(Int(enrollment.oneMinute * 100))%"
enrollment_5m_TextField.stringValue = "\(Int(enrollment.fiveMinutes * 100))%"
enrollment_15m_TextField.stringValue = "\(Int(enrollment.fifteenMinutes * 100))%"
enrollment_30m_TextField.stringValue = "\(Int(enrollment.thirtyMinutes * 100))%"

device_30s_TextField.stringValue = "\(Int(device.thirtySeconds * 100))%"
device_1m_TextField.stringValue = "\(Int(device.oneMinute * 100))%"
device_5m_TextField.stringValue = "\(Int(device.fiveMinutes * 100))%"
device_15m_TextField.stringValue = "\(Int(device.fifteenMinutes * 100))%"
device_30m_TextField.stringValue = "\(Int(device.thirtyMinutes * 100))%"

default_30s_TextField.stringValue = "\(Int(defaultStatus.thirtySeconds * 100))%"
default_1m_TextField.stringValue = "\(Int(defaultStatus.oneMinute * 100))%"
default_5m_TextField.stringValue = "\(Int(defaultStatus.fiveMinutes * 100))%"
default_15m_TextField.stringValue = "\(Int(defaultStatus.fifteenMinutes * 100))%"
default_30m_TextField.stringValue = "\(Int(defaultStatus.thirtyMinutes * 100))%"

showOnActiveScreen(windowName: healthStatus_Window)
}
@MainActor @objc func updateHealthStatusView(_ notification: Notification) {
healthStatus_BringToFront(false)
}

@IBAction func showHealthStatus_Action(_ sender: NSMenuItem) {
healthStatus_BringToFront(true)
}

@MainActor
func healthStatus_BringToFront(_ bringToFront: Bool) {
if let api = HealthStatusStore.shared.healthStatus?.api,
let ui = HealthStatusStore.shared.healthStatus?.ui,
let enrollment = HealthStatusStore.shared.healthStatus?.enrollment,
let device = HealthStatusStore.shared.healthStatus?.device,
let defaultStatus = HealthStatusStore.shared.healthStatus?.healthStatusDefault {

api_30s_TextField.stringValue = "\(Int(api.thirtySeconds * 100))%"
api_1m_TextField.stringValue = "\(Int(api.oneMinute * 100))%"
api_5m_TextField.stringValue = "\(Int(api.fiveMinutes * 100))%"
api_15m_TextField.stringValue = "\(Int(api.fifteenMinutes * 100))%"
api_30m_TextField.stringValue = "\(Int(api.thirtyMinutes * 100))%"

ui_30s_TextField.stringValue = "\(Int(ui.thirtySeconds * 100))%"
ui_1m_TextField.stringValue = "\(Int(ui.oneMinute * 100))%"
ui_5m_TextField.stringValue = "\(Int(ui.fiveMinutes * 100))%"
ui_15m_TextField.stringValue = "\(Int(ui.fifteenMinutes * 100))%"
ui_30m_TextField.stringValue = "\(Int(ui.thirtyMinutes * 100))%"

enrollment_30s_TextField.stringValue = "\(Int(enrollment.thirtySeconds * 100))%"
enrollment_1m_TextField.stringValue = "\(Int(enrollment.oneMinute * 100))%"
enrollment_5m_TextField.stringValue = "\(Int(enrollment.fiveMinutes * 100))%"
enrollment_15m_TextField.stringValue = "\(Int(enrollment.fifteenMinutes * 100))%"
enrollment_30m_TextField.stringValue = "\(Int(enrollment.thirtyMinutes * 100))%"

device_30s_TextField.stringValue = "\(Int(device.thirtySeconds * 100))%"
device_1m_TextField.stringValue = "\(Int(device.oneMinute * 100))%"
device_5m_TextField.stringValue = "\(Int(device.fiveMinutes * 100))%"
device_15m_TextField.stringValue = "\(Int(device.fifteenMinutes * 100))%"
device_30m_TextField.stringValue = "\(Int(device.thirtyMinutes * 100))%"

default_30s_TextField.stringValue = "\(Int(defaultStatus.thirtySeconds * 100))%"
default_1m_TextField.stringValue = "\(Int(defaultStatus.oneMinute * 100))%"
default_5m_TextField.stringValue = "\(Int(defaultStatus.fiveMinutes * 100))%"
default_15m_TextField.stringValue = "\(Int(defaultStatus.fifteenMinutes * 100))%"
default_30m_TextField.stringValue = "\(Int(defaultStatus.thirtyMinutes * 100))%"

if !healthStatusIsVisible() || bringToFront {
showOnActiveScreen(windowName: healthStatus_Window)
}
}
}

func showOnActiveScreen(windowName: NSWindow) {
Expand All @@ -402,7 +430,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {
xPos = currentFrameWidth - windowWidth + Double(screen.frame.origin.x) - 20.0
yPos = currentFrameHeight - windowHeight + Double(screen.frame.origin.y) - 40.0
}
// windowName.collectionBehavior = NSWindow.CollectionBehavior.moveToActiveSpace

windowName.setFrameOrigin(NSPoint(x: xPos, y: yPos))
windowName.setIsVisible(true)

Expand All @@ -428,3 +456,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate {

}

extension Notification.Name {
public static let updateHealthStatusView = Notification.Name("updateHealthStatusView")
}
Loading