diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 09142c4..4dc0bce 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -2,9 +2,9 @@ name: iOS CI for SPAR on: push: - branches: [ SPAR_iOS_App ] + branches: [ SPAR_iOS_App, main ] pull_request: - branches: [ SPAR_iOS_App ] + branches: [ SPAR_iOS_App, main ] jobs: build-and-test: diff --git a/README.md b/README.md index c7b5597..a18e77e 100644 --- a/README.md +++ b/README.md @@ -1 +1,158 @@ -# MSCS_Spring2025_Capping \ No newline at end of file +# πŸ–₯️ System Processes and Analysis Reporting (S.P.A.R) + +**S.P.A.R** is a cross-platform system monitoring application for Ubuntu Linux that provides real-time insights into CPU, memory, disk usage, and running processes. It supports both desktop and mobile platforms, enabling users to track system health anytime, anywhere. + +--- + +## πŸ“¦ Components + +### πŸ”Ή Desktop App (Electron + React) +- View real-time system metrics +- Responsive UI with Tailwind CSS +- Builds for `.exe`, `.deb`, `.tar.gz` + +### πŸ”Ή iOS App (SwiftUI) +- Monitors system metrics +- Supports accessibility: VoiceOver, Dynamic Type +- Face ID integration (Keychain) +- Interaction logging for future analytics + +### πŸ”Ή Backend Services +- RESTful API with **Spring Boot** +- Metrics collected using **Node.js** (`systeminformation`, `ps-list`) +- MySQL used for data storage +- API exposes CPU, memory, disk, battery, processes + +--- + +## 🧰 Tech Stack + +| Layer | Technology | +|--------------|---------------------------------| +| Desktop UI | Electron, React, Tailwind CSS | +| Mobile App | SwiftUI, Combine, Swift 5 | +| Backend API | Spring Boot (Java), Node.js | +| Data Storage | MySQL | +| Metrics Lib | `systeminformation`, `ps-list` | +| Tools | Xcode 15, IntelliJ, VS Code | + +--- + +## πŸš€ Getting Started + +### πŸ“ Clone Repo +```bash +git clone https://github.com/MSCS-Capping/MSCS_Spring2025_Capping.git +cd MSCS_Spring2025_Capping +``` + +### πŸ–₯️ Desktop App +```bash +cd desktop-app +npm install +npm start # Dev mode +npm run package # Build installer +``` + +### πŸ“± iOS App +1. cd SPAR_iOS +2. cd SPAR +1. Open `spar.xcodeproj` in Xcode 15 +2. Run on simulator or device +3. View process metrics and test accessibility + logging features + +### πŸ”§ Backend API +```bash +cd backend-api +./mvnw spring-boot:run +``` + +### 🟑 Node Metrics Collector +```bash +cd node-metrics +npm install +node collector.js +``` + +Ensure Spring Boot API is running before starting the Electron app. + +--- + +## πŸ”„ Actions & Deployment + +### βœ… GitHub Actions + +- iOS app uses GitHub Actions to ensure **all unit and UI tests pass before any merge** into `main` +- CI workflow runs on macOS runners with Xcode configured +- Prevents broken code from being merged into production + +```yaml +# .github/workflows/ios-tests.yml +name: iOS Tests +on: [pull_request] +jobs: + test: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Xcode + run: xcodebuild -project iOSApp.xcodeproj -scheme iOSApp -destination 'platform=iOS Simulator,name=iPhone 14' test +``` + +### πŸš€ Render Deployment + +- Spring Boot backend is deployed using **Render** +- MySQL database also hosted via Render's managed database services +- Auto-deploy enabled from `main` branch +- Environment variables (DB credentials, API keys) are stored securely in Render's dashboard + +--- + +## πŸ” Security & Testing + +- βœ… Unit & Integration Tests for backend and frontend +- β™Ώ Accessibility tested with VoiceOver and Dynamic Type +- πŸ”’ Keychain integration for login credentials (iOS) +- πŸ”„ MySQL data backups and error recovery mechanisms + +--- + +## 🌱 Future Enhancements + +- Export reports (CSV, PDF) +- Android version +- Push Notification +- Chatbot + +--- + +## πŸ‘₯ Contributors + +- **Abhijeet Cherungottil** + πŸ“§ [Abhijeet.Cherungottil1@marist.edu](mailto:Abhijeet.Cherungottil1@marist.edu) + +- **Sumanth Kumar Katapally** + πŸ“§ [SumanthKumar.Katapally1@marist.edu](mailto:SumanthKumar.Katapally1@marist.edu) + +- **Arjun Suresh (AJ)** + πŸ“§ [Arjun.Suresh1@marist.edu](mailto:Arjun.Suresh1@marist.edu) + +--- + +## πŸŽ₯ Demo Video + +πŸ“½οΈ Watch it here: Coming Soon +[]() + +--- + +## πŸ“š References + +- [Electron Documentation](https://www.electronjs.org/docs/latest) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [System Information (NPM)](https://www.npmjs.com/package/systeminformation) +- [SwiftUI Accessibility Guide](https://developer.apple.com/documentation/swiftui/accessibility) +- [Tailwind CSS](https://tailwindcss.com/) +- [Render Deployment Docs](https://render.com/docs/deploy-a-java-spring-boot-app) + +--- diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj index 526a3a8..e7dd99e 100644 --- a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 3D5995152DB4167A00E9215B /* MemoryUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5995062DB4112100E9215B /* MemoryUsage.swift */; }; 3D5995162DB41E0D00E9215B /* MockNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */; }; 3D5995172DB41E1500E9215B /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D59950D2DB413C100E9215B /* MockData.swift */; }; + 3D63FB0A2DC4F4F2006F53DE /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D63FB092DC4F4F2006F53DE /* LoadingView.swift */; }; 3D78BB2A2DB550E5000E5C2F /* LoginViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D78BB292DB550E5000E5C2F /* LoginViewUITests.swift */; }; 3D8423A72DB14F86009CF847 /* SPARTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8423A62DB14F86009CF847 /* SPARTests.swift */; }; 3D8423B02DB15044009CF847 /* LaunchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8423AF2DB15044009CF847 /* LaunchTest.swift */; }; @@ -130,6 +131,7 @@ 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkService.swift; sourceTree = ""; }; 3D59950D2DB413C100E9215B /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; 3D59950F2DB413E300E9215B /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = ""; }; + 3D63FB092DC4F4F2006F53DE /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 3D78BB292DB550E5000E5C2F /* LoginViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewUITests.swift; sourceTree = ""; }; 3D8423A42DB14F86009CF847 /* SPARTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPARTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3D8423A62DB14F86009CF847 /* SPARTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPARTests.swift; sourceTree = ""; }; @@ -171,10 +173,10 @@ 3D07B5312D851C1D0085B32C /* Extensions */ = { isa = PBXGroup; children = ( - 3DCE45902D68C7E700FACBB8 /* View+Extension.swift */, - 3DCE458C2D68C1A900FACBB8 /* Logger+Extension.swift */, 3D07B53D2D8887F30085B32C /* ContentSizeCategory+Extension.swift */, + 3DCE458C2D68C1A900FACBB8 /* Logger+Extension.swift */, 3D505B742DBACEBF00510486 /* String+Extension.swift */, + 3DCE45902D68C7E700FACBB8 /* View+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -182,8 +184,8 @@ 3D07B5322D851C490085B32C /* Onboarding */ = { isa = PBXGroup; children = ( - 3DCE458E2D68C1B200FACBB8 /* OnboardingView.swift */, 3DF9BA522D67767D00D0CC62 /* OnBoardingScreenView.swift */, + 3DCE458E2D68C1B200FACBB8 /* OnboardingView.swift */, ); path = Onboarding; sourceTree = ""; @@ -191,10 +193,12 @@ 3D07B5332D851C640085B32C /* Utilities */ = { isa = PBXGroup; children = ( - 3DCE45882D68C19200FACBB8 /* Constant.swift */, - 3D505B4E2DB9C6AD00510486 /* NavigationButton.swift */, 3D505B5C2DBA657B00510486 /* AppSettings.swift */, + 3DCE45882D68C19200FACBB8 /* Constant.swift */, 3D505B782DBB170700510486 /* InfoRow.swift */, + 3D63FB092DC4F4F2006F53DE /* LoadingView.swift */, + 3DA1AA462DBF034200678160 /* KeychainHelper.swift */, + 3D505B4E2DB9C6AD00510486 /* NavigationButton.swift */, ); path = Utilities; sourceTree = ""; @@ -210,16 +214,15 @@ 3D07B5352D85C3860085B32C /* ViewModel */ = { isa = PBXGroup; children = ( + 3D505B562DB9DF3F00510486 /* BatteryViewModel.swift */, 3D07B5392D85C3EC0085B32C /* ChartDataContainer.swift */, + 3D505B6A2DBAC5EC00510486 /* CpuUsageViewModel.swift */, + 3D505B702DBACDE500510486 /* DiskIOViewModel.swift */, + 3D505B642DBAAE4D00510486 /* DiskUsageViewModel.swift */, 3D5994F92DB307B400E9215B /* HomeViewModel.swift */, 3D5994FB2DB30B0100E9215B /* LoginViewModel.swift */, - 3D505B562DB9DF3F00510486 /* BatteryViewModel.swift */, 3D505B582DB9E14700510486 /* MemoryUsageViewModel.swift */, 3D505B5A2DB9E3F700510486 /* ProcessViewModel.swift */, - 3D505B642DBAAE4D00510486 /* DiskUsageViewModel.swift */, - 3D505B6A2DBAC5EC00510486 /* CpuUsageViewModel.swift */, - 3D505B702DBACDE500510486 /* DiskIOViewModel.swift */, - 3DA1AA462DBF034200678160 /* KeychainHelper.swift */, ); path = ViewModel; sourceTree = ""; @@ -227,17 +230,17 @@ 3D07B5362D85C3910085B32C /* Data */ = { isa = PBXGroup; children = ( - 3D59950D2DB413C100E9215B /* MockData.swift */, - 3D07B5372D85C3BB0085B32C /* ChartData.swift */, - 3D07B53F2D88881D0085B32C /* Onboarding.swift */, - 3D5995002DB410BD00E9215B /* DeviceSpecification.swift */, - 3D5995022DB410DB00E9215B /* ProcessStatus.swift */, 3D5995042DB4110700E9215B /* BatteryInfo.swift */, - 3D5995062DB4112100E9215B /* MemoryUsage.swift */, - 3D505B5E2DBA94B200510486 /* LoginModel.swift */, - 3D505B622DBAAE2C00510486 /* DiskUsage.swift */, + 3D07B5372D85C3BB0085B32C /* ChartData.swift */, 3D505B682DBAB31D00510486 /* CPUUsage.swift */, + 3D5995002DB410BD00E9215B /* DeviceSpecification.swift */, 3D505B6E2DBACDCA00510486 /* DiskIOModel.swift */, + 3D505B622DBAAE2C00510486 /* DiskUsage.swift */, + 3D505B5E2DBA94B200510486 /* LoginModel.swift */, + 3D5995062DB4112100E9215B /* MemoryUsage.swift */, + 3D59950D2DB413C100E9215B /* MockData.swift */, + 3D07B53F2D88881D0085B32C /* Onboarding.swift */, + 3D5995022DB410DB00E9215B /* ProcessStatus.swift */, ); path = Data; sourceTree = ""; @@ -245,13 +248,13 @@ 3D505B4B2DB9C19000510486 /* Device */ = { isa = PBXGroup; children = ( - 3D505B4C2DB9C1BE00510486 /* DeviceOptions.swift */, 3D505B502DB9D76200510486 /* BatteryDetailView.swift */, - 3D505B522DB9DAA500510486 /* MemoryUsageDetailView.swift */, - 3D505B542DB9DD5500510486 /* ProcessDetailPage.swift */, - 3D505B662DBAAFFE00510486 /* DiskUsageDetailView.swift */, 3D505B6C2DBAC6EA00510486 /* CpuUsageDetailView.swift */, + 3D505B4C2DB9C1BE00510486 /* DeviceOptions.swift */, 3D505B722DBACE3800510486 /* DiskIODetailView.swift */, + 3D505B662DBAAFFE00510486 /* DiskUsageDetailView.swift */, + 3D505B522DB9DAA500510486 /* MemoryUsageDetailView.swift */, + 3D505B542DB9DD5500510486 /* ProcessDetailPage.swift */, ); path = Device; sourceTree = ""; @@ -259,8 +262,8 @@ 3D5994F12DB2ACB300E9215B /* Login */ = { isa = PBXGroup; children = ( - 3D5994F22DB2B79400E9215B /* LoginView.swift */, 3D5994F42DB2BA1200E9215B /* BackgroundAnimationView.swift */, + 3D5994F22DB2B79400E9215B /* LoginView.swift */, ); path = Login; sourceTree = ""; @@ -269,8 +272,8 @@ isa = PBXGroup; children = ( 3D59950A2DB4137E00E9215B /* MockNetworkService.swift */, - 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */, 3D5995082DB4114200E9215B /* NetworkManager.swift */, + 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */, ); path = NetworkService; sourceTree = ""; @@ -278,12 +281,12 @@ 3D63FB062DC1B4FA006F53DE /* View */ = { isa = PBXGroup; children = ( - 3DF9BA502D67762C00D0CC62 /* HomeView.swift */, - 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */, - 3D07B5322D851C490085B32C /* Onboarding */, 3D07B5342D85C3740085B32C /* ChartUI */, - 3D5994F12DB2ACB300E9215B /* Login */, 3D505B4B2DB9C19000510486 /* Device */, + 3DF9BA502D67762C00D0CC62 /* HomeView.swift */, + 3D5994F12DB2ACB300E9215B /* Login */, + 3D07B5322D851C490085B32C /* Onboarding */, + 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */, ); path = View; sourceTree = ""; @@ -291,11 +294,11 @@ 3D8423A52DB14F86009CF847 /* SPARTests */ = { isa = PBXGroup; children = ( - 3D8423A62DB14F86009CF847 /* SPARTests.swift */, 3D8423AF2DB15044009CF847 /* LaunchTest.swift */, - 3D8423B22DB15082009CF847 /* OnboardingTests.swift */, - 3D59950F2DB413E300E9215B /* NetworkManagerTests.swift */, 3D78BB292DB550E5000E5C2F /* LoginViewUITests.swift */, + 3D59950F2DB413E300E9215B /* NetworkManagerTests.swift */, + 3D8423B22DB15082009CF847 /* OnboardingTests.swift */, + 3D8423A62DB14F86009CF847 /* SPARTests.swift */, ); path = SPARTests; sourceTree = ""; @@ -321,16 +324,16 @@ 3D98C7F02D4FE04500462EF4 /* SPAR */ = { isa = PBXGroup; children = ( - 3D63FB062DC1B4FA006F53DE /* View */, - 3D5994FD2DB3FF0700E9215B /* NetworkService */, + 3D98C7F52D4FE04700462EF4 /* Assets.xcassets */, + 3D98C7F32D4FE04500462EF4 /* ContentView.swift */, 3D07B5362D85C3910085B32C /* Data */, - 3D07B5352D85C3860085B32C /* ViewModel */, - 3D07B5332D851C640085B32C /* Utilities */, 3D07B5312D851C1D0085B32C /* Extensions */, - 3D98C7F12D4FE04500462EF4 /* SPARApp.swift */, - 3D98C7F32D4FE04500462EF4 /* ContentView.swift */, - 3D98C7F52D4FE04700462EF4 /* Assets.xcassets */, + 3D5994FD2DB3FF0700E9215B /* NetworkService */, 3D98C7F72D4FE04700462EF4 /* Preview Content */, + 3D98C7F12D4FE04500462EF4 /* SPARApp.swift */, + 3D07B5332D851C640085B32C /* Utilities */, + 3D63FB062DC1B4FA006F53DE /* View */, + 3D07B5352D85C3860085B32C /* ViewModel */, ); path = SPAR; sourceTree = ""; @@ -513,6 +516,7 @@ 3D07B53A2D85C3EC0085B32C /* ChartDataContainer.swift in Sources */, 3D07B5402D88881D0085B32C /* Onboarding.swift in Sources */, 3D5994F52DB2BA1200E9215B /* BackgroundAnimationView.swift in Sources */, + 3D63FB0A2DC4F4F2006F53DE /* LoadingView.swift in Sources */, 3D505B572DB9DF3F00510486 /* BatteryViewModel.swift in Sources */, 3DCE45892D68C19200FACBB8 /* Constant.swift in Sources */, 3D5994F32DB2B79400E9215B /* LoginView.swift in Sources */, diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 67ba8ea..4005bfc 100644 --- a/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/xcuserdata/abhijeetcherungottil.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -20,22 +20,6 @@ landmarkType = "3"> - - - - @@ -78,8 +62,8 @@ filePath = "SPAR/ViewModel/HomeViewModel.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "44" - endingLineNumber = "44" + startingLineNumber = "52" + endingLineNumber = "52" landmarkName = "getDeviceData()" landmarkType = "7"> diff --git a/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift b/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift index 49bcb29..920b38a 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/BatteryInfo.swift @@ -12,6 +12,7 @@ struct BatteryInfo: Codable, Identifiable { let userId: Int let hasBattery: Bool let batteryPercentage: Int + let deviceId:String let powerConsumption: Double let timestamp: String let charging: Bool diff --git a/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift b/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift index 9369982..cd6150e 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/DeviceSpecification.swift @@ -9,7 +9,7 @@ import Foundation struct DeviceSpecification: Codable, Identifiable { let id: Int - let userId: Int + let deviceId: String let deviceName: String let manufacturer: String let model: String @@ -20,12 +20,12 @@ struct DeviceSpecification: Codable, Identifiable { let graphics: String let operatingSystem: String let systemType: String - let timestamp: String + let registeredAt: String enum CodingKeys: String, CodingKey { - case id = "deviceId" - case userId, deviceName, manufacturer, model, processor + case id + case deviceId,deviceName, manufacturer, model, processor case cpuPhysicalCores, cpuLogicalCores, installedRam - case graphics, operatingSystem, systemType, timestamp + case graphics, operatingSystem, systemType, registeredAt } } diff --git a/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift b/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift index 5a302d1..1f8b4ba 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/MemoryUsage.swift @@ -10,6 +10,7 @@ import Foundation struct MemoryUsage: Codable, Identifiable { let id: Int let userId: Int + let deviceId:String let totalMemory: Double let usedMemory: Double let availableMemory: Double diff --git a/SPAR_iOS/SPAR/SPAR/Data/MockData.swift b/SPAR_iOS/SPAR/SPAR/Data/MockData.swift index bf91473..fe9de99 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/MockData.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/MockData.swift @@ -12,7 +12,7 @@ struct MockData { [ { "userId": 1, - "deviceId": 5, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", "deviceName": "MyComputer", "manufacturer": "Dell", "model": "Inspiron 15", @@ -23,11 +23,11 @@ struct MockData { "graphics": "NVIDIA GTX 1650", "operatingSystem": "Windows 10 x64", "systemType": "x64 operating system, x64-based processor", - "timestamp": "2025-03-28T16:03:30.041384" + "registeredAt": "2025-03-28T16:03:30.041384" }, { "userId": 1, - "deviceId": 13, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", "deviceName": "Home", "manufacturer": "Dell", "model": "Inspiron 15", @@ -38,7 +38,7 @@ struct MockData { "graphics": "NVIDIA GTX 1650", "operatingSystem": "Windows 10 x64", "systemType": "x64 operating system, x64-based processor", - "timestamp": "2025-04-13T15:28:39.97323" + "registeredAt": "2025-04-13T15:28:39.97323" } ] """.data(using: .utf8)! @@ -52,6 +52,7 @@ struct MockData { "name": "chrome.exe", "cpuUsage": 12.5, "memoryMB": 200.0, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", "timestamp": "2025-04-13T15:29:00.236114" }, { @@ -61,6 +62,7 @@ struct MockData { "name": "node.exe", "cpuUsage": 5.0, "memoryMB": 150.0, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", "timestamp": "2025-04-13T15:29:00.236114" } ] @@ -74,6 +76,7 @@ struct MockData { "batteryPercentage": 85, "powerConsumption": 5.0, "timestamp": "2025-04-13T15:29:10.549936", + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", "charging": false } """.data(using: .utf8)! @@ -85,6 +88,7 @@ struct MockData { "totalMemory": 16.0, "usedMemory": 8.5, "availableMemory": 7.5, + "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", "timestamp": "2025-04-13T15:28:49.261218" } """.data(using: .utf8)! diff --git a/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift b/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift index 3636d7b..596c827 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/ProcessStatus.swift @@ -9,10 +9,11 @@ import Foundation struct ProcessStatus: Codable, Identifiable { let id: Int - let userId: Int let pid: Int let name: String let cpuUsage: Double let memoryMB: Double + let userId: Int + let deviceId: String let timestamp: String } diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift index bb1a44a..e4828b2 100644 --- a/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift @@ -11,112 +11,50 @@ import Foundation // First, let's fix the MockNetworkService for CPU usage final class MockNetworkService: NetworkServicing { - private let sampleDataMapping: [String: Data] = [ - "device-specifications": MockData.sampleDeviceData, - "process-status": MockData.sampleProcessData, - "battery-info": MockData.sampleBatteryData, - "memory-usage": MockData.sampleMemoryUsageData, - "disk-usage": MockData.sampleDiskUsageData, - "disk-io": MockData.sampleDiskIOUsageData, - "cpu-usage": MockData.sampleCPUUsageData, - "auth/signin": MockData.sampleLoginData - ] - - func get(from url: URL, token: String?) async throws -> T { - guard let (key, data) = sampleDataMapping.first(where: { url.absoluteString.contains($0.key) }) else { - throw URLError(.badURL) - } - - var responseData = data - - if key == "cpu-usage" { - do { - // Parse the original JSON - let temp = try JSONDecoder().decode(TempCPUUsage.self, from: data) - - // Create a properly formatted JSON array for perCoreUsage - let coreUsages = try parseCoreUsages(from: temp.perCoreUsageJson) - - // Create a new CPU usage object with proper data structure - let fixedCpuUsage = CpuUsage( - id: temp.id, - totalCpuLoad: temp.totalCpuLoad, - perCoreUsage: coreUsages, - userId: temp.userId, - deviceId: temp.deviceId, - timestamp: temp.timestamp - ) - - // Encode the fixed object if T is CpuUsage - if T.self == CpuUsage.self { - responseData = try JSONEncoder().encode(fixedCpuUsage) - } - } catch { - print("Failed to prepare CPU mock data: \(error)") - } - } - - do { - return try JSONDecoder().decode(T.self, from: responseData) - } catch { - print("Failed to decode final response: \(error)") - throw error - } + enum Endpoint: String, CaseIterable { + case device = "device-specifications" + case process = "process-status" + case battery = "battery-info" + case memory = "ram-usage" + case disk = "disk-usage" + case diskIO = "disk-io" + case cpu = "cpu-usage" + case login = "auth/signin" } - // Helper method to parse core usages from the JSON string - private func parseCoreUsages(from jsonString: String) throws -> [CpuCoreUsage] { - // Clean up the JSON string - remove unneeded escapes - let cleanedString: String - - // Try to identify if it's already an array format - if jsonString.hasPrefix("[") && jsonString.hasSuffix("]") { - // It's already in array format, just remove unnecessary escapes - cleanedString = jsonString - .replacingOccurrences(of: "\\\"", with: "\"") - .replacingOccurrences(of: "\\\\", with: "\\") - } else { - // It's not in array format, add brackets - cleanedString = "[\(jsonString)]" - } - - // For debugging - print("Cleaned JSON string: \(cleanedString)") - - guard let jsonData = cleanedString.data(using: .utf8) else { - throw NSError(domain: "MockNetworkService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert string to data"]) - } - - do { - return try JSONDecoder().decode([CpuCoreUsage].self, from: jsonData) - } catch { - print("JSON parsing error: \(error)") - - // Last resort - manually parse the string if it's in the expected format - let manualCoreUsages = try manuallyParseCoreUsages(from: cleanedString) - return manualCoreUsages + func get(from url: URL, token: String?) async throws -> T { + guard let endpoint = Endpoint.allCases.first(where: { url.absoluteString.contains($0.rawValue) }) else { + throw URLError(.unsupportedURL) } - } - - // Manually parse core usages as a last resort - private func manuallyParseCoreUsages(from jsonString: String) throws -> [CpuCoreUsage] { - var coreUsages: [CpuCoreUsage] = [] - - // Create a default set of cores with zero usage - for i in 1...12 { - coreUsages.append(CpuCoreUsage(core: i, usage: 0.0)) + + let mockData: Data + switch endpoint { + case .device: mockData = SimpleMockData.device + case .process: mockData = SimpleMockData.process + case .battery: mockData = SimpleMockData.battery + case .memory: mockData = SimpleMockData.memory + case .disk: mockData = SimpleMockData.disk + case .diskIO: mockData = SimpleMockData.diskIO + case .cpu: + let decoded = try JSONDecoder().decode(TempCPUUsage.self, from: SimpleMockData.cpu) + let parsedCore = try JSONDecoder().decode([CpuCoreUsage].self, from: decoded.perCoreUsageJson.data(using: .utf8)!) + let final = CpuUsage(id: decoded.id, + totalCpuLoad: decoded.totalCpuLoad, + perCoreUsage: parsedCore, + userId: decoded.userId, + deviceId: decoded.deviceId, + timestamp: decoded.registeredAt) + return final as! T + case .login: mockData = SimpleMockData.login } - return coreUsages + return try JSONDecoder().decode(T.self, from: mockData) } - + func post(to url: URL, body: U) async throws -> T { - // Always returning login sample for mock POST - return try JSONDecoder().decode(T.self, from: MockData.sampleLoginData) + return try JSONDecoder().decode(T.self, from: SimpleMockData.login) } } - - // Helper extension to debug JSON data extension Data { var prettyPrintedJSONString: String? { @@ -141,5 +79,15 @@ struct TempCPUUsage: Codable { var perCoreUsageJson: String var userId: Int var deviceId: String - var timestamp: String + var registeredAt: String +} +struct SimpleMockData { + static let device = MockData.sampleDeviceData + static let process = MockData.sampleProcessData + static let battery = MockData.sampleBatteryData + static let memory = MockData.sampleMemoryUsageData + static let disk = MockData.sampleDiskUsageData + static let diskIO = MockData.sampleDiskIOUsageData + static let cpu = MockData.sampleCPUUsageData + static let login = MockData.sampleLoginData } diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift index ef54de2..0e4374c 100644 --- a/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkManager.swift @@ -9,58 +9,61 @@ import Foundation class NetworkManager { private let networkService: NetworkServicing - private let baseURL = "http://localhost:8080/api/metrics" + private let baseURL = "https://mscs-spring2025-capping.onrender.com" - init(networkService: NetworkServicing = MockNetworkService()) { + init(networkService: NetworkServicing = NetworkService()) { self.networkService = networkService } - private func makeURL(endpoint: String, userId: Int, deviceId: Int) -> URL? { - URL(string: "\(baseURL)/\(endpoint)/\(userId)/\(deviceId)") + private func makeURL(endpoint: String, userId: Int, deviceId: String) -> URL? { + URL(string: "\(baseURL)/api/metrics/\(endpoint)/\(userId)/\(deviceId)") } func fetchDeviceSpecifications(for userId: Int) async throws -> [DeviceSpecification] { - guard let url = URL(string: "http://localhost:8080/api/device-specifications/\(userId)"),let token = AppSettings.shared.authToken else { + + guard let url = URL(string: "https://mscs-spring2025-capping.onrender.com/api/users/\(userId)/getdevices"),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } + print("userId",userId) + print("token",token) return try await networkService.get(from: url,token: token) } - func fetchCPUUsageInfo(for userId: Int, deviceId: Int) async throws -> CpuUsage { + func fetchCPUUsageInfo(for userId: Int, deviceId: String) async throws -> CpuUsage { guard let url = makeURL(endpoint: "cpu-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } return try await networkService.get(from: url, token: token) } - func fetchProcessStatus(for userId: Int, deviceId: Int) async throws -> [ProcessStatus] { + func fetchProcessStatus(for userId: Int, deviceId: String) async throws -> [ProcessStatus] { guard let url = makeURL(endpoint: "process-status", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } return try await networkService.get(from: url, token: token) } - func fetchBatteryInfo(for userId: Int, deviceId: Int) async throws -> BatteryInfo { + func fetchBatteryInfo(for userId: Int, deviceId: String) async throws -> BatteryInfo { guard let url = makeURL(endpoint: "battery-info", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } return try await networkService.get(from: url, token: token) } - func fetchMemoryUsage(for userId: Int, deviceId: Int) async throws -> MemoryUsage { - guard let url = makeURL(endpoint: "memory-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { + func fetchMemoryUsage(for userId: Int, deviceId: String) async throws -> MemoryUsage { + guard let url = makeURL(endpoint: "ram-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } return try await networkService.get(from: url, token: token) } - func fetchDiskUsage(for userId: Int, deviceId: Int) async throws -> DiskUsage { + func fetchDiskUsage(for userId: Int, deviceId: String) async throws -> DiskUsage { guard let url = makeURL(endpoint: "disk-usage", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } return try await networkService.get(from: url, token: token) } - func fetchDiskIO(for userId: Int, deviceId: Int) async throws -> DiskIO { + func fetchDiskIO(for userId: Int, deviceId: String) async throws -> DiskIO { guard let url = makeURL(endpoint: "disk-io", userId: userId, deviceId: deviceId),let token = AppSettings.shared.authToken else { throw URLError(.badURL) } @@ -68,7 +71,7 @@ class NetworkManager { } func login(username: String, password: String) async throws -> LoginResponse { - guard let url = URL(string: "http://localhost:8080/api/auth/signin") else { + guard let url = URL(string: "https://mscs-spring2025-capping.onrender.com/api/auth/signin") else { throw URLError(.badURL) } let loginRequest = LoginRequest(username: username, password: password) diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift index d04fbc6..9268376 100644 --- a/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/NetworkService.swift @@ -25,7 +25,12 @@ class NetworkService: NetworkServicing { if let token = token { request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.addValue("application/json", forHTTPHeaderField: "Accept") + } + print("request",request) + print("Request Headers:", request.allHTTPHeaderFields ?? [:]) + print("Request URL:", request.url?.absoluteString ?? "nil") return try await perform(request: request) } diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift b/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift index 1964b73..0a3d3f6 100644 --- a/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift +++ b/SPAR_iOS/SPAR/SPAR/Utilities/AppSettings.swift @@ -16,6 +16,12 @@ final class AppSettings { private let tokenKey = "authToken" private let userIdKey = "userId" private let hasLoggedInOnceKey = "hasLoggedInOnce" + + func clearUserSession() { + self.authToken = nil + userId = nil + // clear any saved credentials or tokens + } var hasLoggedInOnce: Bool { get { UserDefaults.standard.bool(forKey: hasLoggedInOnceKey) } diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift b/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift index b6695c1..743db65 100644 --- a/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift +++ b/SPAR_iOS/SPAR/SPAR/Utilities/Constant.swift @@ -30,6 +30,8 @@ enum StringConstant { static let searchIcon = "It's icon for searching device." static let searchText = "Search devices..." static let sdearchText = "Search devices..." + static let emptyscreenmsg = "You need to have at least one Desktop APP." + static let downloadApp = "Download SPAR Desktop" // device detail static let deviceInfo = "Device Info" @@ -43,10 +45,10 @@ enum StringConstant { static let graphics = "Graphics" static let OS = "OS" static let systemType = "System Type" - static let timestamp = "Timestamp" + static let registeredAt = "Time Stamp" static let batteryInfo = "Battery Info" static let cpu = "CPU" - static let memoryUsage = "Memory Usage" + static let memoryUsage = "Ram Usage" static let diskUsage = "Disk Usage" static let diskIO = "Disk IO" static let processlist = "Running Processes" @@ -100,8 +102,13 @@ enum ImageConstant { static let magnifyingGlass = "magnifyingglass" static let xmarkCircleFill = "xmark.circle.fill" static let chevronRight = "chevron.right" - static let eye = "eye" - static let eyeSlash = "eye.slash" + static let eye = "eye" + static let eyeSlash = "eye.slash" + static let logout = "rectangle.portrait.and.arrow.right" + static let emptyScreenlogo = "desktopcomputer" + static let faceid = "faceid" + static let cpu = "cpu" + static let memorychip = "memorychip" } @@ -111,6 +118,14 @@ enum LoggerConstant { static let LoginSubmitTapped = "❕Submit button was tapped" } +enum AccessibilityConstant { + static let signOut = "Sign Out" + static let metricPicker = "Metric Picker" + static let processtip1 = "Double tap to view more process details" + static let top5 = "Top five CPU cores by usage" + static let barChart = "Bar chart showing the five CPU cores with highest utilization" +} + enum AccessibilityIdentifier: String { case dashboardTitle case onboardingWelcome diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift b/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift index 3cd19ae..e8f9364 100644 --- a/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift +++ b/SPAR_iOS/SPAR/SPAR/Utilities/InfoRow.swift @@ -21,6 +21,7 @@ struct InfoRow: View { .minimumScaleFactor(sizeCategory.customMinScaleFactor) Spacer() Text(value) + .multilineTextAlignment(.trailing) .foregroundColor(.black) .minimumScaleFactor(sizeCategory.customMinScaleFactor) } diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/KeychainHelper.swift b/SPAR_iOS/SPAR/SPAR/Utilities/KeychainHelper.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/ViewModel/KeychainHelper.swift rename to SPAR_iOS/SPAR/SPAR/Utilities/KeychainHelper.swift diff --git a/SPAR_iOS/SPAR/SPAR/Utilities/LoadingView.swift b/SPAR_iOS/SPAR/SPAR/Utilities/LoadingView.swift new file mode 100644 index 0000000..7d3d3cd --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/Utilities/LoadingView.swift @@ -0,0 +1,38 @@ +// +// LoadingView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 5/2/25. +// + +import SwiftUI + +struct LoadingView: View { + let isLoading: Bool + let content: () -> Content + + var body: some View { + ZStack { + content() + .disabled(isLoading) + .blur(radius: isLoading ? 2 : 0) + + if isLoading { + VStack(spacing: 12) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(1.5) + Text("Loading...") + .font(.caption) + .foregroundColor(.gray) + } + .padding(20) + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 10) + } + } + } +} + + diff --git a/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift b/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift index 4feeac5..1b500e2 100644 --- a/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift +++ b/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift @@ -10,67 +10,47 @@ import SwiftUI struct HalfDonutChart: View { @Binding var chartDataObj: ChartData @Environment(\.sizeCategory) var sizeCategory - + var body: some View { VStack { HStack { Text(chartDataObj.type) - .font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/) + .font(.title) .bold() .minimumScaleFactor(sizeCategory.customMinScaleFactor) Spacer() } + ZStack { - // Default background semi-circle ArcShape(startAngle: .degrees(180), endAngle: .degrees(360)) .stroke(chartDataObj.color.opacity(0.3), lineWidth: 45) - - // Foreground arc for current data + ArcShape(startAngle: .degrees(180), endAngle: .degrees(180 + (Double(chartDataObj.percent / 100) * 180))) .stroke(chartDataObj.color, lineWidth: 45) .animation(.easeInOut(duration: 1.0), value: chartDataObj.percent) - - // Percentage text + Text("\(Int(chartDataObj.percent))%") .font(.title) .bold() .foregroundColor(.black) .minimumScaleFactor(sizeCategory.customMinScaleFactor) - .offset(y: Double((250 / 3) * sizeCategory.customMinScaleFactor)) // Move text inside the arc + .offset(y: Double((250 / 3) * sizeCategory.customMinScaleFactor)) + .accessibilityHidden(true) // Hide visual label from VoiceOver } - Spacer() + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(chartDataObj.type) Usage") + .accessibilityValue("\(Int(chartDataObj.percent)) percent used") + .accessibilityHint("Semi-circular chart showing current \(chartDataObj.type.lowercased()) usage") - /* // Button to Change Data Manually - Button("Change Data") { - updateData() - } - .padding() - .background(Color.blue) - .foregroundColor(.white) - .clipShape(Capsule()) - }*/ + Spacer() } .frame(width: 300, height: 250) .padding() .background(Color(red: 1, green: 0.961, blue: 0.882)) - - - // Function to manually update the data - /* private func updateData() { - let newValues: [ChartData] = [ - ChartData(color: Color(#colorLiteral(red: 1, green: 0.493, blue: 0.474, alpha: 1)), percent: 25), - ChartData(color: Color(#colorLiteral(red: 1, green: 0.832, blue: 0.473, alpha: 1)), percent: 50), - ChartData(color: Color(#colorLiteral(red: 0.451, green: 0.988, blue: 0.838, alpha: 1)), percent: 75), - ChartData(color: Color(#colorLiteral(red: 0.477, green: 0.505, blue: 1, alpha: 1)), percent: 100) - ] - - withAnimation { - chartDataObj.currentData = newValues.randomElement()! - } - }*/ } } + struct ArcShape: Shape { var startAngle: Angle var endAngle: Angle diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift index 3db0b4a..8a3f2fa 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift @@ -19,23 +19,25 @@ struct BatteryDetailView: View { } var body: some View { - ZStack { - // MARK: Background Gradient - LinearGradient(colors: [.blue.opacity(0.15), .purple.opacity(0.2)], startPoint: .top, endPoint: .bottom) - .ignoresSafeArea() + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.blue.opacity(0.15), .purple.opacity(0.2)], startPoint: .top, endPoint: .bottom) + .ignoresSafeArea() - VStack { - Spacer() + VStack { + Spacer() - // MARK: Card Content - BatteryCardView(viewModel: viewModel, device: device) + // MARK: Card Content + BatteryCardView(viewModel: viewModel, device: device) - Spacer() + Spacer() + } + .padding() } - .padding() + .onAppear { + self.logPageVisit() } - .onAppear { - self.logPageVisit() } } } @@ -63,7 +65,7 @@ struct BatteryCardView: View { InfoRow(label: StringConstant.deviceName, value: device.deviceName) InfoRow(label: StringConstant.Charging, value: viewModel.batteryInfo.charging ? StringConstant.batYes : StringConstant.batNo) InfoRow(label: StringConstant.power, value: String(format: "%.2f W", viewModel.batteryInfo.powerConsumption)) - InfoRow(label: StringConstant.timestamp, value: viewModel.batteryInfo.timestamp.toFormattedDate()) + InfoRow(label: StringConstant.registeredAt, value: viewModel.batteryInfo.timestamp.toFormattedDate()) } } .padding() @@ -125,7 +127,7 @@ struct BatteryGaugeView: View { #Preview { BatteryDetailView(device: DeviceSpecification( id: 1, - userId: 1, + deviceId: "ff", deviceName: "MyComputer", manufacturer: "Dell", model: "Inspiron 15", @@ -136,6 +138,6 @@ struct BatteryGaugeView: View { graphics: "NVIDIA GTX 1650", operatingSystem: "Windows 10 x64", systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + registeredAt: "2025-03-28T16:03:30.041384" )) } diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift index e9af795..7005006 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift @@ -22,93 +22,102 @@ struct CpuUsageDetailView: View { // MARK: - Body var body: some View { - ZStack { - // MARK: Background Gradient - LinearGradient(colors: [.orange.opacity(0.2), .yellow.opacity(0.2)], - startPoint: .topLeading, - endPoint: .bottomTrailing) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 20) { - - // MARK: Header - Text(StringConstant.cpuUsage) - .font(.largeTitle) - .bold() - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - Spacer(minLength: 20) - - // MARK: Donut Chart - if let chartData = viewModel.chartData { - HalfDonutChart(chartDataObj: .constant(chartData)) - .frame(height: 180) - .transition(.scale) - } - - Spacer(minLength: 40) - - // MARK: CPU Usage Information - if let usage = viewModel.cpuUsage { - VStack(alignment: .leading, spacing: 16) { - - // MARK: General Info - InfoRow(label: StringConstant.deviceName, value: device.deviceName) - InfoRow(label: StringConstant.totalCPULoad, value: String(format: "%.1f%%", usage.totalCpuLoad)) - InfoRow(label: StringConstant.timestamp, value: usage.timestamp) + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.orange.opacity(0.2), .yellow.opacity(0.2)], + startPoint: .topLeading, + endPoint: .bottomTrailing) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + + // MARK: Header + Text(StringConstant.cpuUsage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Spacer(minLength: 20) + + // MARK: Donut Chart + if let chartData = viewModel.chartData { + HalfDonutChart(chartDataObj: .constant(chartData)) + .frame(height: 180) + .transition(.scale) + } - Divider().padding(.vertical, 8) + Spacer(minLength: 40) - // MARK: Per Core Usage Section (Consider splitting into its own view) - Text(StringConstant.allCore) - .font(.headline) + // MARK: CPU Usage Information + if let usage = viewModel.cpuUsage { + VStack(alignment: .leading, spacing: 16) { - ForEach(usage.perCoreUsage, id: \.core) { coreUsage in - InfoRow(label: "Core \(coreUsage.core)", value: String(format: "%.1f%%", coreUsage.usage)) - } + // MARK: General Info + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.totalCPULoad, value: String(format: "%.1f%%", usage.totalCpuLoad)) + InfoRow(label: StringConstant.registeredAt, value: usage.timestamp.toFormattedDate()) - Divider().padding(.vertical, 8) + Divider().padding(.vertical, 8) - // MARK: Top 5 Core Usage Graph (Consider moving to a ChartSection view) - Text(StringConstant.topFive) - .font(.headline) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) + // MARK: Per Core Usage Section (Consider splitting into its own view) + Text(StringConstant.allCore) + .font(.headline) - let topCores = usage.perCoreUsage - .sorted { $0.usage > $1.usage } - .prefix(5) + ForEach(usage.perCoreUsage, id: \.core) { coreUsage in + InfoRow(label: "Core \(coreUsage.core)", value: String(format: "%.1f%%", coreUsage.usage)) + } - Chart { - ForEach(topCores, id: \.core) { coreUsage in - BarMark( - x: .value("Core", "Core \(coreUsage.core)"), - y: .value("Usage", coreUsage.usage) - ) - .foregroundStyle(.blue.gradient) + Divider().padding(.vertical, 8) + + // MARK: Top 5 Core Usage Graph (Consider moving to a ChartSection view) + Text(StringConstant.topFive) + .font(.headline) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + let topCores = usage.perCoreUsage + .sorted { $0.usage > $1.usage } + .prefix(5) + + Chart { + ForEach(topCores, id: \.core) { coreUsage in + BarMark( + x: .value("Core", "Core \(coreUsage.core)"), + y: .value("Usage", coreUsage.usage) + ) + .foregroundStyle(.blue.gradient) + .accessibilityLabel("Core \(coreUsage.core)") + .accessibilityValue("\(String(format: "%.1f", coreUsage.usage)) percent usage") + .accessibilityHidden(false) + } } + .accessibilityElement(children: .contain) + .accessibilityLabel(AccessibilityConstant.top5) + .accessibilityHint(AccessibilityConstant.barChart) + .frame(height: 200) + .cornerRadius(10) + .padding(.top, 8) + } - .frame(height: 200) - .cornerRadius(10) - .padding(.top, 8) + .padding() + .frame(maxWidth: 360) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 5) } - .padding() - .frame(maxWidth: 360) - .background(Color.white) - .cornerRadius(16) - .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 5) - } - Spacer() + Spacer() + } + .padding() + .animation(.easeInOut, value: viewModel.cpuUsage?.totalCpuLoad) } - .padding() - .animation(.easeInOut, value: viewModel.cpuUsage?.totalCpuLoad) } + .onAppear { + // MARK: Log Page Visit + self.logPageVisit() } - .onAppear { - // MARK: Log Page Visit - self.logPageVisit() } } } @@ -117,7 +126,7 @@ struct CpuUsageDetailView: View { #Preview { CpuUsageDetailView(device: DeviceSpecification( id: 1, - userId: 1, + deviceId: "dd", deviceName: "MyComputer", manufacturer: "Dell", model: "Inspiron 15", @@ -128,6 +137,6 @@ struct CpuUsageDetailView: View { graphics: "NVIDIA GTX 1650", operatingSystem: "Windows 10 x64", systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + registeredAt: "2025-03-28T16:03:30.041384" )) } diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift index fcd9b47..e24bcad 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift @@ -63,7 +63,7 @@ struct DeviceInfoCard: View { DeviceInfoRow(label: StringConstant.graphics, value: device.graphics) DeviceInfoRow(label: StringConstant.OS, value: device.operatingSystem) DeviceInfoRow(label: StringConstant.systemType, value: device.systemType) - DeviceInfoRow(label: StringConstant.timestamp, value: device.timestamp.toFormattedDate()) + DeviceInfoRow(label: StringConstant.registeredAt, value: device.registeredAt.toFormattedDate()) } } .padding() @@ -128,6 +128,7 @@ struct DeviceInfoRow: View { .minimumScaleFactor(sizeCategory.customMinScaleFactor) Spacer() Text(value) + .multilineTextAlignment(.trailing) .foregroundColor(.cyan) .minimumScaleFactor(sizeCategory.customMinScaleFactor) } @@ -140,7 +141,7 @@ struct DeviceInfoRow: View { NavigationStack { DeviceOptions(currentView: .constant(.home), device: DeviceSpecification( id: 1, - userId: 1, + deviceId: "fff", deviceName: "MyComputer", manufacturer: "Dell", model: "Inspiron 15", @@ -151,7 +152,7 @@ struct DeviceInfoRow: View { graphics: "NVIDIA GTX 1650", operatingSystem: "Windows 10 x64", systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + registeredAt: "2025-03-28T16:03:30.041384" )) } } diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift index db847c2..c477a7b 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift @@ -22,37 +22,39 @@ struct DiskIODetailView: View { // MARK: - Body var body: some View { - ZStack { - // MARK: Background Gradient - LinearGradient(colors: [.blue.opacity(0.2), .purple.opacity(0.2)], - startPoint: .topLeading, - endPoint: .bottomTrailing) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 20) { - // MARK: Header - Text(StringConstant.diskIOUssage) - .font(.largeTitle) - .bold() - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - Spacer(minLength: 20) - - // MARK: Disk IO Information Section - if let diskIO = viewModel.diskIO { - DiskIOInfoSection(diskIO: diskIO) + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.blue.opacity(0.2), .purple.opacity(0.2)], + startPoint: .topLeading, + endPoint: .bottomTrailing) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + // MARK: Header + Text(StringConstant.diskIOUssage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Spacer(minLength: 20) + + // MARK: Disk IO Information Section + if let diskIO = viewModel.diskIO { + DiskIOInfoSection(diskIO: diskIO, device: device) + } + + Spacer() } - - Spacer() + .padding() } - .padding() } + .onAppear { + // MARK: Log Page Visit + self.logPageVisit() } - .onAppear { - // MARK: Log Page Visit - self.logPageVisit() } } } @@ -60,14 +62,15 @@ struct DiskIODetailView: View { // MARK: - DiskIOInfoSection struct DiskIOInfoSection: View { let diskIO: DiskIO + let device: DeviceSpecification var body: some View { VStack(alignment: .leading, spacing: 16) { // MARK: Basic Info - InfoRow(label: StringConstant.deviceName, value: "MyComputer") // Update with device value + InfoRow(label: StringConstant.deviceName, value: device.deviceName) // Update with device value InfoRow(label: StringConstant.RS, value: String(format: "%.1f MBps", diskIO.readSpeedMBps)) InfoRow(label: StringConstant.WS, value: String(format: "%.1f MBps", diskIO.writeSpeedMBps)) - InfoRow(label: StringConstant.timestamp, value: diskIO.timestamp) + InfoRow(label: StringConstant.registeredAt, value: diskIO.timestamp.toFormattedDate()) Divider().padding(.vertical, 8) @@ -116,7 +119,7 @@ struct DiskIOChart: View { #Preview { DiskIODetailView(device: DeviceSpecification( id: 1, - userId: 1, + deviceId: "ff", deviceName: "MyComputer", manufacturer: "Dell", model: "Inspiron 15", @@ -127,6 +130,6 @@ struct DiskIOChart: View { graphics: "NVIDIA GTX 1650", operatingSystem: "Windows 10 x64", systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + registeredAt: "2025-03-28T16:03:30.041384" )) } diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift index f1f74aa..80b3b8e 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift @@ -21,61 +21,64 @@ struct DiskUsageDetailView: View { // MARK: - Body var body: some View { - ZStack { - // MARK: Background Gradient - LinearGradient(colors: [.purple.opacity(0.15), .blue.opacity(0.15)], startPoint: .top, endPoint: .bottom) - .ignoresSafeArea() + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // Background Gradient + LinearGradient(colors: [.purple.opacity(0.15), .blue.opacity(0.15)], startPoint: .top, endPoint: .bottom) + .ignoresSafeArea() - VStack(spacing: 30) { - - // MARK: Header - Text(StringConstant.diskUsage) - .font(.largeTitle) - .bold() - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) + ScrollView { + VStack(spacing: 30) { + // Header + Text(StringConstant.diskUsage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) - // MARK: Disk Usage Donut Chart - HalfDonutChart(chartDataObj: $viewModel.chartData) + // Disk Usage Donut Chart + HalfDonutChart(chartDataObj: $viewModel.chartData) - // MARK: Disk Info Display - if let diskInfo = viewModel.diskUsage { - VStack(alignment: .leading, spacing: 15) { - InfoRow(label: StringConstant.deviceName, value: device.deviceName) - InfoRow(label: StringConstant.filesystem, value: diskInfo.filesystem) - InfoRow(label: StringConstant.size, value: String(format: "%.2f GB", diskInfo.sizeGB)) - InfoRow(label: StringConstant.usedSpace, value: String(format: "%.2f GB", diskInfo.usedGB)) - InfoRow(label: StringConstant.availableSpace, value: String(format: "%.2f GB", diskInfo.availableGB)) - InfoRow(label: StringConstant.timestamp, value: diskInfo.timestamp) + // Disk Info Display + if let diskInfo = viewModel.diskUsage { + VStack(alignment: .leading, spacing: 15) { + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.filesystem, value: diskInfo.filesystem) + InfoRow(label: StringConstant.size, value: String(format: "%.2f GB", diskInfo.sizeGB)) + InfoRow(label: StringConstant.usedSpace, value: String(format: "%.2f GB", diskInfo.usedGB)) + InfoRow(label: StringConstant.availableSpace, value: String(format: "%.2f GB", diskInfo.availableGB)) + InfoRow(label: StringConstant.registeredAt, value: diskInfo.timestamp.toFormattedDate()) + } + .padding() + .frame(maxWidth: 320) + .background(Color.white) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) + } else { + // Error Message + Text(viewModel.errorMessage) + .foregroundColor(.red) + .font(.body) + } + + Spacer(minLength: 20) } .padding() - .frame(maxWidth: 320) - .background(Color.white) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) - } else { - // MARK: Error Message - Text(viewModel.errorMessage) - .foregroundColor(.red) - .font(.body) } - - Spacer() } - .padding() - } - .onAppear { - // MARK: Log Page Visit - self.logPageVisit() + .onAppear { + self.logPageVisit() + } } } + } // MARK: - Preview #Preview { DiskUsageDetailView(device: DeviceSpecification( id: 1, - userId: 1, + deviceId: "ff", deviceName: "MyComputer", manufacturer: "Dell", model: "Inspiron 15", @@ -86,6 +89,6 @@ struct DiskUsageDetailView: View { graphics: "NVIDIA GTX 1650", operatingSystem: "Windows 10 x64", systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + registeredAt: "2025-03-28T16:03:30.041384" )) } diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift index 6c1a3f8..96570f8 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift @@ -19,33 +19,35 @@ struct MemoryUsageDetailView: View { } var body: some View { - ZStack { - // MARK: Background - LinearGradient(colors: [.mint.opacity(0.2), .cyan.opacity(0.2)], - startPoint: .top, - endPoint: .bottom) - .ignoresSafeArea() + LoadingView(isLoading: viewModel.isLoading) { + ZStack { + // MARK: Background + LinearGradient(colors: [.mint.opacity(0.2), .cyan.opacity(0.2)], + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() - VStack(spacing: 30) { - // MARK: Title - Text("Memory Usage") - .font(.largeTitle) - .bold() - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) + VStack(spacing: 30) { + // MARK: Title + Text(StringConstant.memoryUsage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) - // MARK: Memory Usage Chart - HalfDonutChart(chartDataObj: $viewModel.chartData) + // MARK: Memory Usage Chart + HalfDonutChart(chartDataObj: $viewModel.chartData) - // MARK: Info Section - MemoryInfoCard(device: device, viewModel: viewModel) + // MARK: Info Section + MemoryInfoCard(device: device, viewModel: viewModel) - Spacer() + Spacer() + } + .padding() } - .padding() + .onAppear { + self.logPageVisit() } - .onAppear { - self.logPageVisit() } } } @@ -64,7 +66,7 @@ struct MemoryInfoCard: View { value: String(format: "%.2f GB", viewModel.memoryInfo.usedMemory)) InfoRow(label: StringConstant.availableMemory, value: String(format: "%.2f GB", viewModel.memoryInfo.availableMemory)) - InfoRow(label: StringConstant.timestamp, + InfoRow(label: StringConstant.registeredAt, value: viewModel.memoryInfo.timestamp.toFormattedDate()) } .padding() @@ -79,17 +81,17 @@ struct MemoryInfoCard: View { #Preview { MemoryUsageDetailView(device: DeviceSpecification( id: 1, - userId: 1, - deviceName: "MyComputer", - manufacturer: "Dell", - model: "Inspiron 15", - processor: "Intel Core i7 2.8 GHz", + deviceId: "", + deviceName: "", + manufacturer: "", + model: "", + processor: "", cpuPhysicalCores: 4, cpuLogicalCores: 8, installedRam: 16.0, graphics: "NVIDIA GTX 1650", operatingSystem: "Windows 10 x64", systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + registeredAt: "2025-03-28T16:03:30.041384" )) } diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift b/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift index 0938442..c3528df 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift @@ -9,121 +9,121 @@ import SwiftUI import Charts struct ProcessDetailPage: View { - @StateObject private var viewModel: ProcessViewModel + @ObservedObject private var viewModel: ProcessViewModel let device: DeviceSpecification @Environment(\.sizeCategory) var sizeCategory init(device: DeviceSpecification) { - _viewModel = StateObject(wrappedValue: ProcessViewModel(device: device)) - self.device = device + let viewModel = ProcessViewModel(device: device) + self.viewModel = viewModel + self.device = device } @State private var showCPU = true // Toggle between CPU and Memory var body: some View { - ScrollView { - VStack(spacing: 20) { - Text(StringConstant.processMonitor) - .font(.largeTitle) - .bold() - .padding(.top) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - Picker(StringConstant.metric, selection: $showCPU) { - Text(StringConstant.cpuUsage).tag(true) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - Text(StringConstant.memoryUsage).tag(false) + LoadingView(isLoading: viewModel.isLoading) { + ScrollView { + VStack(spacing: 20) { + Text(StringConstant.processMonitor) + .font(.largeTitle) + .bold() + .padding(.top) .minimumScaleFactor(sizeCategory.customMinScaleFactor) - } - .pickerStyle(.segmented) - .padding(.horizontal) - Chart { - ForEach(viewModel.processList, id: \.id) { process in - BarMark( - x: .value(StringConstant.process, "\(process.name) (\(process.pid))"), - y: .value(showCPU ? StringConstant.cpu : StringConstant.memory, showCPU ? process.cpuUsage : process.memoryMB) - ) - .foregroundStyle(showCPU ? Color.blue : Color.green) - .annotation(position: .top) { - Text(String(format: "%.1f", showCPU ? process.cpuUsage : process.memoryMB)) - .font(.caption) - .foregroundColor(.gray) - } + Picker(StringConstant.metric, selection: $showCPU) { + Text(StringConstant.cpuUsage).tag(true) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Text(StringConstant.memoryUsage).tag(false) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) } - } - .frame(height: 300) - .padding(.horizontal) + .pickerStyle(.segmented) + .padding(.horizontal) + .accessibilityLabel(AccessibilityConstant.metricPicker) - ForEach(viewModel.processList, id: \.id) { process in - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("PID: \(process.pid)") - .font(.headline) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - Spacer() - Text(process.name) - .fontWeight(.semibold) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Chart { + ForEach(viewModel.processList) { process in + BarMark( + x: .value(StringConstant.process, "\(process.name) (\(process.pid))"), + y: .value(showCPU ? StringConstant.cpu : StringConstant.memory, showCPU ? process.cpuUsage : process.memoryMB) + ) + .foregroundStyle(showCPU ? Color.blue : Color.green) + .annotation(position: .top) { + Text(String(format: "%.1f", showCPU ? process.cpuUsage : process.memoryMB)) + .font(.caption) + .foregroundColor(.gray) + .accessibilityElement(children: .ignore) // Control VoiceOver reading + .accessibilityLabel("\(process.name), process id \(process.pid)") + .accessibilityValue(showCPU + ? String(format: "%.1f percent CPU usage", process.cpuUsage) + : String(format: "%.1f megabytes memory usage", process.memoryMB) + ) + .accessibilityHint(AccessibilityConstant.processtip1) + } } + } + .frame(height: 300) + .padding(.horizontal) - HStack { - Label(String(format: "%.1f%% CPU", process.cpuUsage), systemImage: "cpu") - Spacer() - Label(String(format: "%.1f MB", process.memoryMB), systemImage: "memorychip") - } - .font(.subheadline) - .foregroundColor(.gray) + ForEach(viewModel.processList) { process in + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("PID: \(process.pid)") + .font(.headline) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + Text(process.name) + .fontWeight(.semibold) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } - Text("Timestamp: \(formattedDate(process.timestamp))") - .font(.caption) - .foregroundColor(.secondary) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) + HStack { + Label(String(format: "%.1f%% CPU", process.cpuUsage), systemImage: ImageConstant.cpu) + Spacer() + Label(String(format: "%.1f MB", process.memoryMB), systemImage: ImageConstant.memorychip) + } + .font(.subheadline) + .foregroundColor(.gray) + + Text("Timestamp: \(process.timestamp.toFormattedDate())") + .font(.caption) + .foregroundColor(.secondary) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .padding() + .background(.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 4) + .padding(.horizontal) } - .padding() - .background(.white) - .cornerRadius(12) - .shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 4) - .padding(.horizontal) + + Spacer(minLength: 40) } - - Spacer(minLength: 40) } - } - .onAppear { - self.logPageVisit() - } + .onAppear { + self.logPageVisit() + } .background(Color(.systemGroupedBackground).ignoresSafeArea()) - } - - private func formattedDate(_ isoString: String) -> String { - let isoFormatter = ISO8601DateFormatter() - if let date = isoFormatter.date(from: isoString) { - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .short - return formatter.string(from: date) } - return isoString } -} - + +} #Preview { - ProcessDetailPage( device: DeviceSpecification( + ProcessDetailPage(device: DeviceSpecification( id: 1, - userId: 1, - deviceName: "MyComputer", - manufacturer: "Dell", - model: "Inspiron 15", - processor: "Intel Core i7 2.8 GHz", + deviceId: "preview", + deviceName: "Preview Device", + manufacturer: "Preview Inc.", + model: "Model X", + processor: "Apple M1", cpuPhysicalCores: 4, cpuLogicalCores: 8, installedRam: 16.0, - graphics: "NVIDIA GTX 1650", - operatingSystem: "Windows 10 x64", - systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" + graphics: "Integrated", + operatingSystem: "macOS", + systemType: "ARM64", + registeredAt: "2025-04-01T12:00:00Z" )) } diff --git a/SPAR_iOS/SPAR/SPAR/View/HomeView.swift b/SPAR_iOS/SPAR/SPAR/View/HomeView.swift index c01dd1e..3a3e878 100644 --- a/SPAR_iOS/SPAR/SPAR/View/HomeView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/HomeView.swift @@ -26,6 +26,16 @@ struct HomeView: View { .padding(.horizontal, 15) .minimumScaleFactor(sizeCategory.customMinScaleFactor) Spacer() + // πŸšͺ Sign Out Button + Button(action: { + viewModel.signOut(currentView: $currentView) + }) { + Image(systemName: ImageConstant.logout) + .font(.title2) + .foregroundColor(.red) + .padding(.trailing, 10) + .accessibilityLabel(AccessibilityConstant.signOut) + } } // Custom Navigation Bar @@ -91,6 +101,7 @@ struct HomeView: View { .navigationBarHidden(true) .onAppear { self.logPageVisit() + viewModel.getDeviceData() } // Show download popup when no devices are available @@ -100,14 +111,14 @@ struct HomeView: View { // Cool empty state VStack { - Image(systemName: "desktopcomputer") + Image(systemName: ImageConstant.emptyScreenlogo) .font(.system(size: 70)) .foregroundColor(.blue) .scaleEffect(1.2) .opacity(0.8) .animation(.easeInOut(duration: 1).repeatForever(autoreverses: true), value: viewModel.showDownloadPopup) - Text("You need to have at least one Desktop APP.") + Text(StringConstant.emptyscreenmsg) .font(.system(size: 28, weight: .bold)) .foregroundColor(.primary) .multilineTextAlignment(.center) @@ -116,7 +127,7 @@ struct HomeView: View { .scaleEffect(viewModel.showDownloadPopup ? 1 : 1.05) .animation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true), value: viewModel.showDownloadPopup) - Text("Download SPAR Desktop") + Text(StringConstant.downloadApp) .font(.system(size: 20, weight: .semibold)) .foregroundColor(.blue) .underline() diff --git a/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift b/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift index ee7a37f..b08679f 100644 --- a/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift @@ -194,7 +194,7 @@ struct FaceIDButton: View { Button(action: { viewModel.loginWithFaceID() }) { - Image(systemName: "faceid") + Image(systemName: ImageConstant.faceid) .resizable() .scaledToFit() .frame(width: 50, height: 50) diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift index 0e83d07..e47e0bf 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/BatteryViewModel.swift @@ -10,16 +10,18 @@ import Foundation class BatteryViewModel: ObservableObject { @Published var batteryInfo: BatteryInfo private let networkManager = NetworkManager() + @Published var isLoading = false + init(device: DeviceSpecification) { // 1. Set a placeholder batteryInfo first self.batteryInfo = BatteryInfo( id: 0, - userId: device.userId, + userId: 1, hasBattery: true, - batteryPercentage: 0, + batteryPercentage: 0, deviceId: "", powerConsumption: 0, - timestamp: "bb", + timestamp: "", charging: false ) @@ -29,18 +31,29 @@ class BatteryViewModel: ObservableObject { func fetchBatteryInfo(device: DeviceSpecification) { Task { + guard let userId = AppSettings.shared.userId else { return } + + // Update isLoading on the main thread + await MainActor.run { + self.isLoading = true + } + do { - guard let userId = AppSettings.shared.userId else { return } - let response = try await networkManager.fetchBatteryInfo(for: userId, deviceId: device.id) - - - DispatchQueue.main.async { - self.batteryInfo = response - } - + let response = try await networkManager.fetchBatteryInfo(for: userId, deviceId: device.deviceId) + + // Publish on the main thread + await MainActor.run { + self.batteryInfo = response + self.isLoading = false + } + } catch { print("Failed to fetch battery info: \(error)") + await MainActor.run { + self.isLoading = false + } } } } + } diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift index 1936bc6..fa0df3b 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/CpuUsageViewModel.swift @@ -13,7 +13,7 @@ class CpuUsageViewModel: ObservableObject { @Published var cpuUsage: CpuUsage? @Published var errorMessage: String = "" @Published var chartData: ChartData? - + @Published var isLoading: Bool = false private let logger = Logger.fileLocation private let networkManager = NetworkManager() @@ -23,11 +23,11 @@ class CpuUsageViewModel: ObservableObject { let sampleUsage = CpuUsage( id: 7, - totalCpuLoad: 32.1, + totalCpuLoad: 0, perCoreUsage: sampleCoreData, userId: 1, deviceId: "1", - timestamp: "2025-04-22T15:57:10.351457" + timestamp: "1" ) self.cpuUsage = sampleUsage @@ -43,23 +43,27 @@ class CpuUsageViewModel: ObservableObject { } func fetchCPUInfo(device: DeviceSpecification) { - // Print to ensure this function is being called print("Fetching CPU info for device: \(device.id)") - + Task { + await MainActor.run { + self.isLoading = true + } + do { guard let userId = AppSettings.shared.userId else { print("User ID is nil. Aborting fetch.") + await MainActor.run { + self.isLoading = false + } return } + print("User ID: \(userId)") - - let response = try await networkManager.fetchCPUUsageInfo(for: userId, deviceId: device.id) - - print("response",response) - - // Update on main thread - DispatchQueue.main.async { + let response = try await networkManager.fetchCPUUsageInfo(for: userId, deviceId: device.deviceId) + print("response", response) + + await MainActor.run { print("Received CPU usage data: \(response)") self.cpuUsage = response self.chartData = ChartData( @@ -67,14 +71,18 @@ class CpuUsageViewModel: ObservableObject { type: "CPU", percent: CGFloat(response.totalCpuLoad) ) + self.isLoading = false } - + } catch { print("Failed to fetch CPU info: \(error)") - DispatchQueue.main.async { + await MainActor.run { self.errorMessage = "Failed to fetch CPU info: \(error.localizedDescription)" + self.isLoading = false } } } } + + } diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift index e688053..8f23e73 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskIOViewModel.swift @@ -11,6 +11,7 @@ import OSLog class DiskIOViewModel: ObservableObject { @Published var diskIO: DiskIO? @Published var errorMessage: String = "" + @Published var isLoading: Bool = false // βœ… Add isLoading private let networkManager = NetworkManager() private let logger = Logger.fileLocation @@ -19,11 +20,11 @@ class DiskIOViewModel: ObservableObject { // Sample static data for testing purposes let sampleDiskIO = DiskIO( id: 5, - readSpeedMBps: 120.0, - writeSpeedMBps: 80.0, + readSpeedMBps: 0, + writeSpeedMBps: 0, userId: 1, deviceId: "1", - timestamp: "2025-04-22T15:57:10.377292".toFormattedDate() + timestamp: "".toFormattedDate() ) self.diskIO = sampleDiskIO fetchDiskIOInfo(device: device) @@ -32,16 +33,32 @@ class DiskIOViewModel: ObservableObject { // Fetch Disk I/O Data from the API func fetchDiskIOInfo(device: DeviceSpecification) { Task { + await MainActor.run { + self.isLoading = true // βœ… Start loading + } + do { - guard let userId = AppSettings.shared.userId else { return } - let response = try await networkManager.fetchDiskIO(for: userId, deviceId: device.id) - - DispatchQueue.main.async { - self.diskIO = response + guard let userId = AppSettings.shared.userId else { + await MainActor.run { + self.isLoading = false + self.errorMessage = "User ID not found" } + return + } + + let response = try await networkManager.fetchDiskIO(for: userId, deviceId: device.deviceId) + + await MainActor.run { + self.diskIO = response + self.isLoading = false // βœ… Stop loading + } } catch { - print("Failed to fetch Disk io info: \(error)") + print("Failed to fetch Disk IO info: \(error)") + await MainActor.run { + self.errorMessage = "Failed to load Disk IO info" + self.isLoading = false // βœ… Stop loading on error + } } } } diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift index 8d73932..a83df24 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/DiskUsageViewModel.swift @@ -13,6 +13,8 @@ class DiskUsageViewModel: ObservableObject { @Published var diskUsage: DiskUsage? @Published var errorMessage: String = "" @Published var chartData: ChartData = ChartData(color: .gray, type: "Disk", percent: 0) + @Published var isLoading: Bool = false + private let networkManager = NetworkManager() @@ -21,13 +23,13 @@ class DiskUsageViewModel: ObservableObject { init(device: DeviceSpecification) { let diskInfo = DiskUsage( id: 1, - filesystem: "/dev/sda1", - sizeGB: 512.0, - usedGB: 200.0, - availableGB: 312.0, + filesystem: "", + sizeGB: 1, + usedGB: 0, + availableGB: 0, userId: 1, deviceId: "1", - timestamp: "2025-04-22T15:57:10.390972".toFormattedDate() + timestamp: "".toFormattedDate() ) self.diskUsage = diskInfo @@ -44,25 +46,40 @@ class DiskUsageViewModel: ObservableObject { func fetchDiskUsageInfo(device: DeviceSpecification) { Task { + await MainActor.run { + self.isLoading = true + } + do { - guard let userId = AppSettings.shared.userId else { return } - let response = try await networkManager.fetchDiskUsage(for: userId, deviceId: device.id) - - DispatchQueue.main.async { - self.diskUsage = response - let usedPercent = (response.usedGB / response.availableGB) * 100 - self.chartData = ChartData( - color: .green, - type: "Disk", - percent: CGFloat(usedPercent) - ) + guard let userId = AppSettings.shared.userId else { + await MainActor.run { + self.isLoading = false } + return + } + let response = try await networkManager.fetchDiskUsage(for: userId, deviceId: device.deviceId) + + await MainActor.run { + self.diskUsage = response + let usedPercent = (response.usedGB / response.sizeGB) * 100 + self.chartData = ChartData( + color: .green, + type: "Disk", + percent: CGFloat(usedPercent) + ) + self.isLoading = false + } } catch { - print("Failed to fetch Disk Usage info: \(error)") + await MainActor.run { + self.errorMessage = "Failed to load disk usage" + self.isLoading = false + } + logger.error("Failed to fetch Disk Usage info: \(error.localizedDescription)") } } } + } diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift index 0e971ad..dbf0108 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/HomeViewModel.swift @@ -30,6 +30,14 @@ class HomeViewModel: ObservableObject { } } + func signOut(currentView: Binding) { + AppSettings.shared.clearUserSession() + DispatchQueue.main.async { + currentView.wrappedValue = .login // Redirect to login screen + } + } + + func getDeviceData() { Task { do { @@ -77,5 +85,5 @@ class HomeViewModel: ObservableObject { // graphics: "NVIDIA GTX 1650", // operatingSystem: "Windows 10 x64", // systemType: "x64-based processor", -// timestamp: "2025-03-28T16:03:30.041384" +// registeredAt: "2025-03-28T16:03:30.041384" //) diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift index a40d65c..ee28e5d 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/LoginViewModel.swift @@ -76,13 +76,6 @@ class LoginViewModel: ObservableObject { return } -// if username.lowercased() == "user" && password == "Password" { -// errorMessage = "" -// self.delegate?.didLoginSuccessfully() -// print("Login successful!") -// } else { -// errorMessage = StringConstant.incorrectCredentials -// } Task { do { diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift index 76350b1..49677e3 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/MemoryUsageViewModel.swift @@ -9,51 +9,62 @@ import SwiftUI class MemoryUsageViewModel: ObservableObject { @Published var memoryInfo: MemoryUsage - private let networkManager = NetworkManager() - @Published var chartData: ChartData + @Published var isLoading = false + + private let networkManager = NetworkManager() init(device: DeviceSpecification) { - // Initializing with some default data or fetching from an API or database. + // Default placeholder let memoryInfo = MemoryUsage( id: 1, userId: 1, - totalMemory: 16.0, - usedMemory: 8.5, - availableMemory: 7.5, - timestamp: "2025-04-19".toFormattedDate() + deviceId: "", + totalMemory: 1, + usedMemory: 0.0, + availableMemory: 0.0, + timestamp: "".toFormattedDate() ) self.memoryInfo = memoryInfo - + let usedPercent = (memoryInfo.usedMemory / memoryInfo.totalMemory) * 100 self.chartData = ChartData( color: Color.purple, type: "Memory", percent: CGFloat(usedPercent) ) - + fetchRamInfo(device: device) } + func fetchRamInfo(device: DeviceSpecification) { Task { + guard let userId = AppSettings.shared.userId else { return } + + await MainActor.run { + self.isLoading = true + } + do { - guard let userId = AppSettings.shared.userId else { return } - let response = try await networkManager.fetchMemoryUsage(for: userId, deviceId: device.id) - - DispatchQueue.main.async { - self.memoryInfo = response - let usedPercent = (response.usedMemory / response.totalMemory) * 100 - self.chartData = ChartData( - color: Color.purple, - type: "Memory", - percent: CGFloat(usedPercent) - ) - } - + let response = try await networkManager.fetchMemoryUsage(for: userId, deviceId: device.deviceId) + + await MainActor.run { + self.memoryInfo = response + let usedPercent = (response.usedMemory / response.totalMemory) * 100 + self.chartData = ChartData( + color: Color.purple, + type: "Memory", + percent: CGFloat(usedPercent) + ) + self.isLoading = false + } + } catch { print("Failed to fetch memory info: \(error)") + await MainActor.run { + self.isLoading = false + } } } } } - diff --git a/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift b/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift index e54c5f5..ccf109a 100644 --- a/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift +++ b/SPAR_iOS/SPAR/SPAR/ViewModel/ProcessViewModel.swift @@ -10,7 +10,7 @@ import Foundation class ProcessViewModel: ObservableObject { @Published var processList: [ProcessStatus] private let networkManager = NetworkManager() - + @Published var isLoading = false init(device: DeviceSpecification) { processList = [] @@ -20,11 +20,17 @@ class ProcessViewModel: ObservableObject { func fetchProcessInfo(device: DeviceSpecification) { Task { do { + DispatchQueue.main.async { + self.isLoading = true + + } + defer { isLoading = false } guard let userId = AppSettings.shared.userId else { return } - let response = try await networkManager.fetchProcessStatus(for: userId, deviceId: device.id) + let response = try await networkManager.fetchProcessStatus(for: userId, deviceId: device.deviceId) DispatchQueue.main.async { self.processList = response + self.isLoading = false } } catch { print("Failed to fetch Process info: \(error)") diff --git a/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift b/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift index 9d8e2d9..64f9fde 100644 --- a/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift +++ b/SPAR_iOS/SPAR/SPARTests/NetworkManagerTests.swift @@ -16,41 +16,33 @@ final class NetworkManagerTests: XCTestCase { super.tearDown() } - func testFetchDeviceSpecifications() async throws { - let devices = try await networkManager.fetchDeviceSpecifications(for: 1) - XCTAssertEqual(devices.count, 2) - XCTAssertEqual(devices.first?.deviceName, "MyComputer") - } - - func testFetchCPUUsageInfo() async throws { - // - } + func testFetchProcessStatus() async throws { - let processes = try await networkManager.fetchProcessStatus(for: 1, deviceId: 5) + let processes = try await networkManager.fetchProcessStatus(for: 1, deviceId: "hh") XCTAssertEqual(processes.count, 2) XCTAssertEqual(processes.first?.name, "chrome.exe") } func testFetchBatteryInfo() async throws { - let batteryInfo = try await networkManager.fetchBatteryInfo(for: 1, deviceId: 5) + let batteryInfo = try await networkManager.fetchBatteryInfo(for: 1, deviceId: "5") XCTAssertTrue(batteryInfo.hasBattery) XCTAssertEqual(batteryInfo.batteryPercentage, 85) } func testFetchMemoryUsage() async throws { - let memoryUsage = try await networkManager.fetchMemoryUsage(for: 1, deviceId: 5) + let memoryUsage = try await networkManager.fetchMemoryUsage(for: 1, deviceId: "5") XCTAssertEqual(memoryUsage.totalMemory, 16.0) XCTAssertEqual(memoryUsage.usedMemory, 8.5) } func testFetchDiskUsage() async throws { - let diskUsage = try await networkManager.fetchDiskUsage(for: 1, deviceId: 5) + let diskUsage = try await networkManager.fetchDiskUsage(for: 1, deviceId: "5") XCTAssertEqual(diskUsage.sizeGB, 512.0) } func testFetchDiskIO() async throws { - let diskIO = try await networkManager.fetchDiskIO(for: 1, deviceId: 5) + let diskIO = try await networkManager.fetchDiskIO(for: 1, deviceId: "5") XCTAssertEqual(diskIO.readSpeedMBps, 120.0) XCTAssertEqual(diskIO.writeSpeedMBps, 80.0) }