diff --git a/README.md b/README.md index 39221e2..bc3df2a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Access Preferences from the menu bar icon. Here you'll be able to set the follo - Whether the alert window is displayed at every polling interval or only when the status changes.
- How the menubar icon is displayed. Minimizing will place a thin transparent icon in the menubar.
- Use of a LaunchAgent, to automatically start the app when logging in.
-- Information for your specific Jamf Cloud instance. The account used only needs to be able to authenticate, no need to assign permissions. If your cloud server does not utilize the HTTPS port 443 be sure to include the port you use in the URL. +- Information for your specific Jamf Cloud instance. Use either a local user account or API client. +- Most notification can be viewed using an account with no permissions set in Jamf Pro. Using an account with ready-only on all objects ensure you'll see all notifications. If your cloud server does not utilize the standard HTTPS port (443) be sure to include the port you use in the URL. notifications
@@ -119,7 +120,9 @@ Thu Sep 17 20:27:30 Jamf Cloud: All systems go. ## Change log -2022-10-02: v2.3.6 - Update logging to prevent potential looping. +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. diff --git a/jamfStatus.xcodeproj/project.pbxproj b/jamfStatus.xcodeproj/project.pbxproj index e297f8e..d1b7481 100644 --- a/jamfStatus.xcodeproj/project.pbxproj +++ b/jamfStatus.xcodeproj/project.pbxproj @@ -14,13 +14,13 @@ B51596A11E963D2D00ED3CA3 /* StatusMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51596A01E963D2D00ED3CA3 /* StatusMenuController.swift */; }; B542A1F124BA902C00EE33C8 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = B542A1F024BA902C00EE33C8 /* Globals.swift */; }; B542A1F324BA90AA00EE33C8 /* WriteToLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B542A1F224BA90A900EE33C8 /* WriteToLog.swift */; }; - B59970D327FCBB4C005175E2 /* JamfPro.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59970D227FCBB4C005175E2 /* JamfPro.swift */; }; B5A470B02271CE7200C2A88D /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A470AF2271CE7200C2A88D /* WebKit.framework */; }; B5AC20321F36479A00C16EAA /* com.jamf.cloudmonitor.plist in Resources */ = {isa = PBXBuildFile; fileRef = B5AC20311F36479A00C16EAA /* com.jamf.cloudmonitor.plist */; }; B5AC20351F37858800C16EAA /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = B5AC20341F37858800C16EAA /* index.html */; }; B5AC20371F37859F00C16EAA /* images in Resources */ = {isa = PBXBuildFile; fileRef = B5AC20361F37859F00C16EAA /* images */; }; - B5BD6AD723297762001D244A /* Credentials2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD6AD623297762001D244A /* Credentials2.swift */; }; + B5BD6AD723297762001D244A /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD6AD623297762001D244A /* Credentials.swift */; }; B5BD6AD9232F108A001D244A /* VersionCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD6AD8232F108A001D244A /* VersionCheck.swift */; }; + B5BE0F9E2AFEF434000CBEBE /* TokenDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BE0F9D2AFEF434000CBEBE /* TokenDelegate.swift */; }; B5E62F32231CAB640012FF5A /* UapiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E62F31231CAB640012FF5A /* UapiCall.swift */; }; B5E62F34231DA09F0012FF5A /* NotificationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E62F33231DA09F0012FF5A /* NotificationAlert.swift */; }; B5FD9DAC2742AAC60044C321 /* Misc in Resources */ = {isa = PBXBuildFile; fileRef = B5FD9DAB2742AAC60044C321 /* Misc */; }; @@ -36,14 +36,14 @@ B51596A01E963D2D00ED3CA3 /* StatusMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusMenuController.swift; sourceTree = ""; }; B542A1F024BA902C00EE33C8 /* Globals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; B542A1F224BA90A900EE33C8 /* WriteToLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WriteToLog.swift; sourceTree = ""; }; - B59970D227FCBB4C005175E2 /* JamfPro.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JamfPro.swift; sourceTree = ""; }; B5A46CCE226FB1E300C2A88D /* jamfStatus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = jamfStatus.entitlements; sourceTree = ""; }; B5A470AF2271CE7200C2A88D /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; B5AC20311F36479A00C16EAA /* com.jamf.cloudmonitor.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.jamf.cloudmonitor.plist; sourceTree = ""; }; B5AC20341F37858800C16EAA /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = ""; }; B5AC20361F37859F00C16EAA /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = ""; }; - B5BD6AD623297762001D244A /* Credentials2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials2.swift; sourceTree = ""; }; + B5BD6AD623297762001D244A /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; B5BD6AD8232F108A001D244A /* VersionCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionCheck.swift; sourceTree = ""; }; + B5BE0F9D2AFEF434000CBEBE /* TokenDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenDelegate.swift; sourceTree = ""; }; B5E62F31231CAB640012FF5A /* UapiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UapiCall.swift; sourceTree = ""; }; B5E62F33231DA09F0012FF5A /* NotificationAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAlert.swift; sourceTree = ""; }; B5FD9DAB2742AAC60044C321 /* Misc */ = {isa = PBXFileReference; lastKnownFileType = text; path = Misc; sourceTree = ""; }; @@ -85,11 +85,11 @@ B5A46CCE226FB1E300C2A88D /* jamfStatus.entitlements */, B5AC20331F37855800C16EAA /* about */, B5013BC61E95817C0099300A /* AppDelegate.swift */, - B5BD6AD623297762001D244A /* Credentials2.swift */, + B5BD6AD623297762001D244A /* Credentials.swift */, B542A1F024BA902C00EE33C8 /* Globals.swift */, - B59970D227FCBB4C005175E2 /* JamfPro.swift */, B5E62F33231DA09F0012FF5A /* NotificationAlert.swift */, B51596A01E963D2D00ED3CA3 /* StatusMenuController.swift */, + B5BE0F9D2AFEF434000CBEBE /* TokenDelegate.swift */, B5E62F31231CAB640012FF5A /* UapiCall.swift */, B5BD6AD8232F108A001D244A /* VersionCheck.swift */, B542A1F224BA90A900EE33C8 /* WriteToLog.swift */, @@ -160,7 +160,7 @@ TargetAttributes = { B5013BC21E95817C0099300A = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = PS2F6S478M; + DevelopmentTeam = T82LNZG37Z; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -218,10 +218,10 @@ B542A1F324BA90AA00EE33C8 /* WriteToLog.swift in Sources */, B5E62F32231CAB640012FF5A /* UapiCall.swift in Sources */, B51596A11E963D2D00ED3CA3 /* StatusMenuController.swift in Sources */, - B5BD6AD723297762001D244A /* Credentials2.swift in Sources */, + B5BD6AD723297762001D244A /* Credentials.swift in Sources */, B542A1F124BA902C00EE33C8 /* Globals.swift in Sources */, + B5BE0F9E2AFEF434000CBEBE /* TokenDelegate.swift in Sources */, B5013BC71E95817C0099300A /* AppDelegate.swift in Sources */, - B59970D327FCBB4C005175E2 /* JamfPro.swift in Sources */, B5BD6AD9232F108A001D244A /* VersionCheck.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -358,11 +358,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = jamfStatus/jamfStatus.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = T82LNZG37Z; ENABLE_HARDENED_RUNTIME = YES; EXCLUDED_SOURCE_FILE_NAMES = Notes; INFOPLIST_FILE = jamfStatus/Info.plist; @@ -376,7 +377,7 @@ "$(SDKROOT)/usr/lib/system/introspection", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.3.6; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.jamf.pse.jamfStatus; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -390,11 +391,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = jamfStatus/jamfStatus.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = T82LNZG37Z; ENABLE_HARDENED_RUNTIME = YES; EXCLUDED_SOURCE_FILE_NAMES = Notes; INFOPLIST_FILE = jamfStatus/Info.plist; @@ -408,7 +410,7 @@ "$(SDKROOT)/usr/lib/system/introspection", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.3.6; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.jamf.pse.jamfStatus; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/jamfStatus.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/UserInterfaceState.xcuserstate b/jamfStatus.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/UserInterfaceState.xcuserstate index 49d4918..e3e77dc 100644 Binary files a/jamfStatus.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/UserInterfaceState.xcuserstate and b/jamfStatus.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/jamfStatus.xcscheme b/jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/jamfStatus.xcscheme index 7705c3d..a41978f 100644 --- a/jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/jamfStatus.xcscheme +++ b/jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/jamfStatus.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -82,6 +82,24 @@ + revealArchiveInOrganizer = "NO"> + + + + + + + + + + diff --git a/jamfStatus/AppDelegate.swift b/jamfStatus/AppDelegate.swift index e638813..aecb7d2 100644 --- a/jamfStatus/AppDelegate.swift +++ b/jamfStatus/AppDelegate.swift @@ -13,7 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { @IBOutlet weak var cloudStatusWindow: NSWindow! @IBOutlet var page_WebView: WKWebView! - @IBOutlet weak var prefs_Panel: NSPanel! + @IBOutlet weak var prefs_Window: NSWindow! @IBOutlet weak var aboutVersion_TextField: NSTextField! @IBOutlet weak var aboutHomeUrl_Button: NSButton! @@ -25,8 +25,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { // site specific settings @IBOutlet weak var jamfServerUrl_TextField: NSTextField! + @IBOutlet weak var username_Label: NSTextField! + @IBOutlet weak var password_Label: NSTextField! @IBOutlet weak var username_TextField: NSTextField! @IBOutlet weak var password_TextField: NSSecureTextField! + @IBOutlet weak var useApiClient_button: NSButton! + @IBAction func useApiClient_action(_ sender: NSButton) { + setLabels() + useApiClient = useApiClient_button.state.rawValue + defaults.set(useApiClient_button.state.rawValue, forKey: "useApiClient") + fetchPassword() + } @IBOutlet weak var siteConnectionStatus_ImageView: NSImageView! let statusImage:[NSImage] = [NSImage(named: "red-dot")!, @@ -45,9 +54,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { var hideIcon: Bool = false let launchAgentPath = NSHomeDirectory()+"/Library/LaunchAgents/com.jamf.cloudmonitor.plist" - // let popover = NSPopover() - - @objc func notificationsAction(_ sender: NSMenuItem) { // print("\(sender.identifier!.rawValue)") // WriteToLog().message(stringOfText: ["\(sender.identifier!.rawValue)"]) @@ -66,7 +72,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { @IBAction func showAbout_MenuItem(_ sender: NSMenuItem) { - let appInfo = Bundle.main.infoDictionary! let version = appInfo["CFBundleShortVersionString"] as! String @@ -85,7 +90,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { aboutHomeUrl_Button.attributedTitle = attributedTitle aboutHomeUrl_Button.toolTip = "https://github.com/jamf/jamfStatus" - showOnActiveScreen(windowName: about_NSWindow, panelName: prefs_Panel, type: "window") + showOnActiveScreen(windowName: about_NSWindow) } @IBAction func aboutHomeUrl_Button(_ sender: NSButton) { @@ -93,7 +98,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { } @IBAction func checkForUpdates(_ sender: AnyObject) { -// let verCheck = VersionCheck() let appInfo = Bundle.main.infoDictionary! let version = appInfo["CFBundleShortVersionString"] as! String @@ -119,9 +123,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { } self.cloudStatusWindow.titleVisibility = .hidden - self.showOnActiveScreen(windowName: self.cloudStatusWindow, panelName: self.prefs_Panel, type: "window") -// NSApplication.shared.activate(ignoringOtherApps: true) -// self.cloudStatusWindow.setIsVisible(true) + self.showOnActiveScreen(windowName: self.cloudStatusWindow) } } @@ -140,7 +142,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { pollingInterval_TextField.stringValue = "300" } defaults.set(prefs.pollingInterval, forKey: "pollingInterval") - defaults.synchronize() +// defaults.synchronize() prefs.pollingInterval = defaults.object(forKey: "pollingInterval") as? Int } @IBAction func prefWindowAlerts_Action(_ sender: NSButton) { @@ -150,18 +152,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { // } prefs.hideUntilStatusChange = (prefWindowAlerts_Button.state.rawValue == 0 ? false:true) defaults.set(prefs.hideUntilStatusChange, forKey: "hideUntilStatusChange") - defaults.synchronize() +// defaults.synchronize() } @IBAction func hideMenubarIcon_Action(_ sender: NSButton) { prefs.hideMenubarIcon = (prefWindowIcon_Button.state.rawValue == 0 ? false:true) defaults.set(prefs.hideMenubarIcon, forKey: "hideMenubarIcon") - defaults.synchronize() +// defaults.synchronize() } @IBAction func launchAgent_Action(_ sender: NSButton) { var isDir: ObjCBool = true prefs.launchAgent = (launchAgent_Button.state.rawValue == 0 ? false:true) defaults.set(prefs.launchAgent, forKey: "launchAgent") - defaults.synchronize() +// defaults.synchronize() if launchAgent_Button.state.rawValue == 0 { if fm.fileExists(atPath: launchAgentPath) { do { @@ -189,28 +191,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { } @IBAction func credentials_Action(_ sender: Any) { + JamfProServer.url = jamfServerUrl_TextField.stringValue - if let _ = sender as? NSTextField { -// print("textField: \(textField.identifier!.rawValue)") - getJamfProVersion(jpURL: jamfServerUrl_TextField.stringValue) { - (result: [Int]) in -// print("jamfProVersion: \(result[0]).\(result[1]).\(result[2])") - } - } - - prefs.jamfServerUrl = jamfServerUrl_TextField.stringValue - defaults.set(prefs.jamfServerUrl, forKey: "jamfServerUrl") - defaults.synchronize() + let urlRegex = try! NSRegularExpression(pattern: "/?failover(.*?)", options:.caseInsensitive) + JamfProServer.url = urlRegex.stringByReplacingMatches(in: JamfProServer.url, options: [], range: NSRange(0.. Void) { - WriteToLog().message(stringOfText: ["getting Jamf Pro Version for \(jpURL)"]) - var versionString = "" - var versionArray = [Int]() - let semaphore = DispatchSemaphore(value: 0) - - OperationQueue().addOperation { - let encodedURL = NSURL(string: "\(jpURL)/JSSCheckConnection") - let request = NSMutableURLRequest(url: encodedURL! as URL) - request.httpMethod = "GET" - let configuration = URLSessionConfiguration.default - let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) - let task = session.dataTask(with: request as URLRequest, completionHandler: { - (data, response, error) -> Void in - if let httpResponse = response as? HTTPURLResponse { -// print("httpResponse: \(httpResponse)") -// print("raw versionString: \(versionString)") - versionString = String(data: data!, encoding: .utf8) ?? "" - - if versionString != "" { - let tmpArray = versionString.components(separatedBy: ".") - if tmpArray.count > 2 { - for i in 0...2 { - switch i { - case 2: - let tmp = tmpArray[i].components(separatedBy: "-") - versionArray.append(Int(tmp[0]) ?? 0) - default: - versionArray.append(Int(tmpArray[i]) ?? 0) - } - } - } - } - } else { - versionArray = [] - var theError = error?.localizedDescription - if theError == nil { theError = "unknown" } - WriteToLog().message(stringOfText: ["error: \(theError!)"]) - } - completion(versionArray) - }) // let task = session - end - task.resume() - semaphore.wait() - } - } - + func saveCreds(server: String, username: String, password: String) { if ( server != "" && username != "" && password != "" ) { @@ -298,14 +269,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { JamfProServer.base64Creds = ("\(username):\(password)".data(using: .utf8)?.base64EncodedString())! token.isValid = false // update the connection indicator for the site server - JamfPro().getToken(serverUrl: server, whichServer: "source", base64creds: JamfProServer.base64Creds) { - (returnedToken: String) in - if returnedToken != "failed" { + TokenDelegate().getToken(serverUrl: server, base64creds: JamfProServer.base64Creds) { + (authResult: (Int,String)) in + + if authResult.1 == "success" { // print("authentication verified") DispatchQueue.main.async { self.siteConnectionStatus_ImageView.image = self.statusImage[1] } - Credentials2().save(service: "jamfStatus: \(serverFqdn)", account: username, data: password) + Credentials().save(service: server.fqdnFromUrl, account: username, data: password) } else { print("authentication failed") DispatchQueue.main.async { @@ -322,6 +294,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { pollingInterval_TextField.stringValue = "\(String(describing: defaults.object(forKey:"pollingInterval")!))" prefWindowAlerts_Button.state = NSControl.StateValue.on + + useApiClient_button.state = NSControl.StateValue(rawValue: useApiClient) if (defaults.bool(forKey: "hideMenubarIcon")) { prefWindowIcon_Button.state = NSControl.StateValue.on @@ -344,69 +318,44 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { let serverUrl = defaults.string(forKey:"jamfServerUrl") ?? "" if serverUrl != "" { jamfServerUrl_TextField.stringValue = serverUrl - let urlRegex = try! NSRegularExpression(pattern: "http(.*?)://", options:.caseInsensitive) - let serverFqdn = urlRegex.stringByReplacingMatches(in: serverUrl, options: [], range: NSRange(0.. - + - - + + @@ -26,12 +26,15 @@ + - + + + @@ -56,12 +59,20 @@ + + + + + + + + @@ -74,6 +85,7 @@ + @@ -84,11 +96,25 @@ - + + - - - + + + + + + + + + + + + + + + + @@ -168,7 +194,7 @@ - + @@ -182,7 +208,7 @@ - + @@ -249,7 +275,7 @@ - + @@ -263,7 +289,7 @@ - + @@ -288,17 +314,17 @@ - + - + - - + + @@ -308,17 +334,18 @@ - - + + - - + + + @@ -331,7 +358,7 @@ - - + + - - - - + + + + - + @@ -384,23 +411,23 @@ - - - + + + - + - + - + @@ -411,7 +438,7 @@ - + @@ -422,7 +449,7 @@ - + @@ -447,8 +474,8 @@ - - + + @@ -456,7 +483,7 @@ - + @@ -475,33 +502,42 @@ + - + - - - + + - - + + + - - - + - + @@ -513,13 +549,14 @@ - + - + + @@ -527,7 +564,7 @@ - + @@ -549,7 +586,7 @@ - + @@ -563,14 +600,14 @@