From a91095935361585f720380bbba29227d20cc226f Mon Sep 17 00:00:00 2001 From: triggermeelmo Date: Mon, 12 Jan 2026 14:07:48 +0100 Subject: [PATCH] init --- .gitignore | 110 ++++++++ LICENSE | 21 ++ README.md | 176 ++++++++++++ app/build.gradle.kts | 109 ++++++++ app/proguard-rules.pro | 32 +++ app/src/main/AndroidManifest.xml | 33 +++ .../java/com/watcher/mobile/MainActivity.kt | 143 ++++++++++ .../com/watcher/mobile/WatcherApplication.kt | 46 +++ .../com/watcher/mobile/data/MonitoringData.kt | 131 +++++++++ .../com/watcher/mobile/network/ApiService.kt | 62 ++++ .../watcher/mobile/network/RetrofitClient.kt | 97 +++++++ .../mobile/repository/MonitoringRepository.kt | 137 +++++++++ .../com/watcher/mobile/ui/AgentsScreen.kt | 226 +++++++++++++++ .../com/watcher/mobile/ui/DashboardScreen.kt | 264 ++++++++++++++++++ .../java/com/watcher/mobile/ui/theme/Theme.kt | 76 +++++ .../java/com/watcher/mobile/ui/theme/Type.kt | 59 ++++ .../mobile/utils/PreferencesManager.kt | 91 ++++++ .../mobile/viewmodel/AgentsViewModel.kt | 52 ++++ .../mobile/viewmodel/DashboardViewModel.kt | 52 ++++ app/src/main/res/values/strings.xml | 45 +++ app/src/main/res/values/themes.xml | 4 + app/src/main/res/xml/backup_rules.xml | 5 + .../main/res/xml/data_extraction_rules.xml | 9 + build.gradle.kts | 10 + gradle.properties | 9 + gradlew | 7 + settings.gradle.kts | 18 ++ 27 files changed, 2024 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/watcher/mobile/MainActivity.kt create mode 100644 app/src/main/java/com/watcher/mobile/WatcherApplication.kt create mode 100644 app/src/main/java/com/watcher/mobile/data/MonitoringData.kt create mode 100644 app/src/main/java/com/watcher/mobile/network/ApiService.kt create mode 100644 app/src/main/java/com/watcher/mobile/network/RetrofitClient.kt create mode 100644 app/src/main/java/com/watcher/mobile/repository/MonitoringRepository.kt create mode 100644 app/src/main/java/com/watcher/mobile/ui/AgentsScreen.kt create mode 100644 app/src/main/java/com/watcher/mobile/ui/DashboardScreen.kt create mode 100644 app/src/main/java/com/watcher/mobile/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/watcher/mobile/ui/theme/Type.kt create mode 100644 app/src/main/java/com/watcher/mobile/utils/PreferencesManager.kt create mode 100644 app/src/main/java/com/watcher/mobile/viewmodel/AgentsViewModel.kt create mode 100644 app/src/main/java/com/watcher/mobile/viewmodel/DashboardViewModel.kt create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100755 gradlew create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d0d3a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/jarRepositories.xml +.idea/caches +.idea/modules.xml +.idea/.name +.idea/misc.xml +.idea/deploymentTargetDropDown.xml +.idea/vcs.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +*.jks +*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +lint-results.html +lint-results.xml +*.lint + +# Android Profiling +*.hprof + +# Mac OS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# VS Code +.vscode/ + +# Editor backup files +*~ +*.swp +*.swo +*.bak diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eb6d51b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Watcher Mobile + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e5104e --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# Watcher Mobile + +Android Mobile App für das Watcher Monitoring System + +## Übersicht + +Watcher Mobile ist eine native Android App, die mit dem Watcher Monitoring System kommuniziert. Die App ermöglicht es, Monitoring-Daten von Agents abzurufen, den System-Status zu überwachen und Alerts zu verwalten. + +## Features + +- **Dashboard**: Übersicht über den aktuellen System-Status + - Anzahl aktiver/inaktiver Agents + - Aktuelle Alerts und kritische Meldungen + - System-Health Status + +- **Agent-Verwaltung**: Übersicht aller Monitoring Agents + - Status-Anzeige (Online/Offline/Warning/Error) + - Grundlegende Metriken (CPU, RAM, Disk) + - Letzte Verbindungszeit + +- **Alerts**: Benachrichtigungen und Warnungen + - Nach Schweregrad gefiltert (Info, Warning, Critical) + - Zeitstempel und Details + +- **Settings**: Konfiguration der App + - API URL Einstellungen + - API Key Verwaltung + - Refresh-Intervall + - Benachrichtigungseinstellungen + +## Technologie-Stack + +- **Sprache**: Kotlin +- **UI Framework**: Jetpack Compose +- **Architecture**: MVVM (Model-View-ViewModel) +- **Networking**: Retrofit + OkHttp +- **JSON**: Gson +- **Async**: Kotlin Coroutines + Flow +- **Storage**: DataStore Preferences +- **Minimum SDK**: 24 (Android 7.0) +- **Target SDK**: 34 (Android 14) + +## Projektstruktur + +``` +app/src/main/java/com/watcher/mobile/ +├── data/ # Datenmodelle +│ └── MonitoringData.kt +├── network/ # API Service und Retrofit Client +│ ├── ApiService.kt +│ └── RetrofitClient.kt +├── repository/ # Repository Pattern für Datenzugriff +│ └── MonitoringRepository.kt +├── viewmodel/ # ViewModels für UI State Management +│ ├── DashboardViewModel.kt +│ └── AgentsViewModel.kt +├── ui/ # Compose UI Komponenten +│ ├── DashboardScreen.kt +│ ├── AgentsScreen.kt +│ └── theme/ +│ ├── Theme.kt +│ └── Type.kt +├── utils/ # Hilfsfunktionen +│ └── PreferencesManager.kt +├── MainActivity.kt # Haupt-Activity +└── WatcherApplication.kt # Application Class +``` + +## Setup & Installation + +### Voraussetzungen + +- Android Studio Hedgehog (2023.1.1) oder neuer +- JDK 17 +- Android SDK mit API Level 34 +- Gradle 8.2+ + +### Installation + +1. **Repository klonen** + ```bash + git clone + cd Watcher-Mobile + ``` + +2. **Projekt in Android Studio öffnen** + - Android Studio starten + - "Open an Existing Project" auswählen + - Zum Watcher-Mobile Ordner navigieren + +3. **API URL konfigurieren** + + Bearbeite `app/build.gradle.kts` und setze die Base URL deiner API: + ```kotlin + buildConfigField("String", "API_BASE_URL", "\"https://your-api.com/api/\"") + ``` + +4. **Build & Run** + - Sync Project with Gradle Files + - Build → Make Project + - Run → Run 'app' + +## API Integration + +Die App kommuniziert mit der WebApp über eine REST API. Folgende Endpunkte werden verwendet: + +### Dashboard +- `GET /api/dashboard` - Dashboard Summary + +### Agents +- `GET /api/agents` - Liste aller Agents (mit Pagination) +- `GET /api/agents/{id}` - Details zu einem Agent +- `GET /api/agents/{id}/metrics` - Metriken eines Agents + +### Alerts +- `GET /api/alerts` - Liste aller Alerts (mit Pagination) +- `GET /api/alerts/{id}` - Details zu einem Alert +- `POST /api/alerts/{id}/acknowledge` - Alert bestätigen + +### Health +- `GET /api/health` - Health Check + +### API Response Format + +Die API sollte folgendes Response-Format verwenden: + +```json +{ + "success": true, + "data": { ... }, + "error": null, + "timestamp": 1234567890 +} +``` + +### Authentication + +Die App unterstützt API Key Authentication über den `Authorization` Header: +``` +Authorization: Bearer YOUR_API_KEY +``` + +Die API URL und der API Key können in den App-Einstellungen konfiguriert werden. + +## Entwicklung + +### Code Style + +Das Projekt folgt den offiziellen Kotlin Coding Conventions. + +### Build Variants + +- **Debug**: Entwicklungsversion mit Logging +- **Release**: Produktionsversion mit ProGuard/R8 Optimierung + +### Logging + +Im Debug-Build sind HTTP Requests vollständig geloggt (via OkHttp Interceptor). + +## Zukünftige Features + +- [ ] Push-Benachrichtigungen für kritische Alerts +- [ ] Detaillierte Metriken-Grafiken +- [ ] Alert-Filter und -Suche +- [ ] Dark Mode Toggle +- [ ] Offline-Cache mit Room Database +- [ ] Widget für Quick-Status +- [ ] Biometrische Authentifizierung + +## Lizenz + +[Hier deine Lizenz einfügen] + +## Kontakt + +[Hier deine Kontaktinformationen einfügen] diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..57b964d --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,109 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.serialization") +} + +android { + namespace = "com.watcher.mobile" + compileSdk = 34 + + defaultConfig { + applicationId = "com.watcher.mobile" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + + // API Base URL - kann über Build Config überschrieben werden + buildConfigField("String", "API_BASE_URL", "\"http://localhost:8080/api/\"") + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + debug { + isDebuggable = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + buildConfig = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.4" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Core Android + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + + // Compose + implementation(platform("androidx.compose:compose-bom:2024.01.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.navigation:navigation-compose:2.7.6") + + // ViewModel + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") + + // Networking - Retrofit + OkHttp + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + // JSON Serialization + implementation("com.google.code.gson:gson:2.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + + // DataStore for preferences + implementation("androidx.datastore:datastore-preferences:1.0.0") + + // Testing + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2024.01.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..16766ea --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,32 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. + +# Retrofit +-keepattributes Signature, InnerClasses, EnclosingMethod +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn javax.annotation.** +-dontwarn kotlin.Unit +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# OkHttp +-dontwarn okhttp3.** +-dontwarn okio.** + +# Gson +-keepattributes Signature +-keepattributes *Annotation* +-dontwarn sun.misc.** +-keep class com.google.gson.** { *; } +-keep class * implements com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Keep data classes +-keep class com.watcher.mobile.data.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a22832e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/watcher/mobile/MainActivity.kt b/app/src/main/java/com/watcher/mobile/MainActivity.kt new file mode 100644 index 0000000..dc3565b --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/MainActivity.kt @@ -0,0 +1,143 @@ +package com.watcher.mobile + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.watcher.mobile.ui.AgentsScreen +import com.watcher.mobile.ui.DashboardScreen +import com.watcher.mobile.ui.theme.WatcherMobileTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + WatcherMobileTheme { + WatcherApp() + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WatcherApp() { + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.app_name)) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary + ) + ) + }, + bottomBar = { + NavigationBar { + NavigationBarItem( + icon = { Icon(Icons.Default.Dashboard, contentDescription = null) }, + label = { Text(stringResource(R.string.nav_dashboard)) }, + selected = currentDestination?.hierarchy?.any { it.route == "dashboard" } == true, + onClick = { + navController.navigate("dashboard") { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Computer, contentDescription = null) }, + label = { Text(stringResource(R.string.nav_agents)) }, + selected = currentDestination?.hierarchy?.any { it.route == "agents" } == true, + onClick = { + navController.navigate("agents") { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Notifications, contentDescription = null) }, + label = { Text(stringResource(R.string.nav_alerts)) }, + selected = currentDestination?.hierarchy?.any { it.route == "alerts" } == true, + onClick = { + navController.navigate("alerts") { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Settings, contentDescription = null) }, + label = { Text(stringResource(R.string.nav_settings)) }, + selected = currentDestination?.hierarchy?.any { it.route == "settings" } == true, + onClick = { + navController.navigate("settings") { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = "dashboard", + modifier = Modifier.padding(innerPadding) + ) { + composable("dashboard") { + DashboardScreen() + } + composable("agents") { + AgentsScreen() + } + composable("alerts") { + // TODO: Implement AlertsScreen + PlaceholderScreen(stringResource(R.string.nav_alerts)) + } + composable("settings") { + // TODO: Implement SettingsScreen + PlaceholderScreen(stringResource(R.string.nav_settings)) + } + } + } +} + +@Composable +fun PlaceholderScreen(title: String) { + Surface(modifier = Modifier.padding()) { + Text( + text = "$title - Coming Soon", + style = MaterialTheme.typography.headlineMedium + ) + } +} diff --git a/app/src/main/java/com/watcher/mobile/WatcherApplication.kt b/app/src/main/java/com/watcher/mobile/WatcherApplication.kt new file mode 100644 index 0000000..c7ba60f --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/WatcherApplication.kt @@ -0,0 +1,46 @@ +package com.watcher.mobile + +import android.app.Application +import com.watcher.mobile.network.RetrofitClient +import com.watcher.mobile.utils.PreferencesManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * Application Class für globale Initialisierung + */ +class WatcherApplication : Application() { + + lateinit var preferencesManager: PreferencesManager + private set + + override fun onCreate() { + super.onCreate() + + // Preferences Manager initialisieren + preferencesManager = PreferencesManager(this) + + // Retrofit mit gespeicherten Einstellungen initialisieren + initializeRetrofitClient() + } + + private fun initializeRetrofitClient() { + CoroutineScope(Dispatchers.IO).launch { + val apiUrl = preferencesManager.apiBaseUrl.first() + val apiKey = preferencesManager.apiKey.first() + + if (!apiUrl.isNullOrEmpty()) { + RetrofitClient.initialize(apiUrl, apiKey) + } else { + // Default URL aus BuildConfig verwenden + RetrofitClient.initialize(BuildConfig.API_BASE_URL, null) + } + } + } + + companion object { + const val TAG = "WatcherApp" + } +} diff --git a/app/src/main/java/com/watcher/mobile/data/MonitoringData.kt b/app/src/main/java/com/watcher/mobile/data/MonitoringData.kt new file mode 100644 index 0000000..a83a53b --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/data/MonitoringData.kt @@ -0,0 +1,131 @@ +package com.watcher.mobile.data + +import com.google.gson.annotations.SerializedName + +/** + * Datenmodelle für die Monitoring API + */ + +// Agent Status +data class Agent( + @SerializedName("id") + val id: String, + @SerializedName("name") + val name: String, + @SerializedName("status") + val status: AgentStatus, + @SerializedName("hostname") + val hostname: String?, + @SerializedName("ipAddress") + val ipAddress: String?, + @SerializedName("lastSeen") + val lastSeen: Long, + @SerializedName("version") + val version: String?, + @SerializedName("metrics") + val metrics: AgentMetrics? +) + +enum class AgentStatus { + @SerializedName("online") + ONLINE, + @SerializedName("offline") + OFFLINE, + @SerializedName("warning") + WARNING, + @SerializedName("error") + ERROR +} + +data class AgentMetrics( + @SerializedName("cpuUsage") + val cpuUsage: Float?, + @SerializedName("memoryUsage") + val memoryUsage: Float?, + @SerializedName("diskUsage") + val diskUsage: Float?, + @SerializedName("networkIn") + val networkIn: Long?, + @SerializedName("networkOut") + val networkOut: Long? +) + +// Alert +data class Alert( + @SerializedName("id") + val id: String, + @SerializedName("agentId") + val agentId: String?, + @SerializedName("title") + val title: String, + @SerializedName("message") + val message: String, + @SerializedName("severity") + val severity: AlertSeverity, + @SerializedName("timestamp") + val timestamp: Long, + @SerializedName("acknowledged") + val acknowledged: Boolean = false +) + +enum class AlertSeverity { + @SerializedName("info") + INFO, + @SerializedName("warning") + WARNING, + @SerializedName("critical") + CRITICAL +} + +// Dashboard Summary +data class DashboardSummary( + @SerializedName("totalAgents") + val totalAgents: Int, + @SerializedName("onlineAgents") + val onlineAgents: Int, + @SerializedName("offlineAgents") + val offlineAgents: Int, + @SerializedName("activeAlerts") + val activeAlerts: Int, + @SerializedName("criticalAlerts") + val criticalAlerts: Int, + @SerializedName("recentAlerts") + val recentAlerts: List, + @SerializedName("systemHealth") + val systemHealth: SystemHealth? +) + +data class SystemHealth( + @SerializedName("status") + val status: String, + @SerializedName("uptime") + val uptime: Long?, + @SerializedName("timestamp") + val timestamp: Long +) + +// API Response Wrapper +data class ApiResponse( + @SerializedName("success") + val success: Boolean, + @SerializedName("data") + val data: T?, + @SerializedName("error") + val error: String?, + @SerializedName("timestamp") + val timestamp: Long? +) + +// Pagination +data class PaginatedResponse( + @SerializedName("items") + val items: List, + @SerializedName("page") + val page: Int, + @SerializedName("pageSize") + val pageSize: Int, + @SerializedName("totalItems") + val totalItems: Int, + @SerializedName("totalPages") + val totalPages: Int +) diff --git a/app/src/main/java/com/watcher/mobile/network/ApiService.kt b/app/src/main/java/com/watcher/mobile/network/ApiService.kt new file mode 100644 index 0000000..3872a03 --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/network/ApiService.kt @@ -0,0 +1,62 @@ +package com.watcher.mobile.network + +import com.watcher.mobile.data.Agent +import com.watcher.mobile.data.Alert +import com.watcher.mobile.data.ApiResponse +import com.watcher.mobile.data.DashboardSummary +import com.watcher.mobile.data.PaginatedResponse +import retrofit2.Response +import retrofit2.http.* + +/** + * Retrofit API Service Interface + * Definiert alle API Endpunkte für das Monitoring System + */ +interface ApiService { + + // Dashboard + @GET("dashboard") + suspend fun getDashboard(): Response> + + // Agents + @GET("agents") + suspend fun getAgents( + @Query("page") page: Int = 1, + @Query("pageSize") pageSize: Int = 50 + ): Response>> + + @GET("agents/{id}") + suspend fun getAgent( + @Path("id") agentId: String + ): Response> + + @GET("agents/{id}/metrics") + suspend fun getAgentMetrics( + @Path("id") agentId: String, + @Query("from") from: Long? = null, + @Query("to") to: Long? = null + ): Response> // Kann später spezifiziert werden + + // Alerts + @GET("alerts") + suspend fun getAlerts( + @Query("page") page: Int = 1, + @Query("pageSize") pageSize: Int = 50, + @Query("severity") severity: String? = null, + @Query("acknowledged") acknowledged: Boolean? = null + ): Response>> + + @GET("alerts/{id}") + suspend fun getAlert( + @Path("id") alertId: String + ): Response> + + @POST("alerts/{id}/acknowledge") + suspend fun acknowledgeAlert( + @Path("id") alertId: String + ): Response> + + // Health Check + @GET("health") + suspend fun healthCheck(): Response> +} diff --git a/app/src/main/java/com/watcher/mobile/network/RetrofitClient.kt b/app/src/main/java/com/watcher/mobile/network/RetrofitClient.kt new file mode 100644 index 0000000..66d5edb --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/network/RetrofitClient.kt @@ -0,0 +1,97 @@ +package com.watcher.mobile.network + +import com.google.gson.GsonBuilder +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +/** + * Retrofit Client Setup + */ +object RetrofitClient { + + private var apiService: ApiService? = null + private var baseUrl: String = "" + private var apiKey: String? = null + + /** + * Initialisiert den Retrofit Client mit Base URL und API Key + */ + fun initialize(baseUrl: String, apiKey: String? = null) { + this.baseUrl = baseUrl.let { + if (it.endsWith("/")) it else "$it/" + } + this.apiKey = apiKey + apiService = null // Reset service to force recreation + } + + /** + * Gibt die API Service Instanz zurück + */ + fun getApiService(): ApiService { + if (apiService == null) { + if (baseUrl.isEmpty()) { + throw IllegalStateException("RetrofitClient not initialized. Call initialize() first.") + } + apiService = createApiService() + } + return apiService!! + } + + private fun createApiService(): ApiService { + val gson = GsonBuilder() + .setLenient() + .create() + + val retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(createOkHttpClient()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + + return retrofit.create(ApiService::class.java) + } + + private fun createOkHttpClient(): OkHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + val authInterceptor = Interceptor { chain -> + val originalRequest = chain.request() + val requestBuilder = originalRequest.newBuilder() + .header("Accept", "application/json") + .header("Content-Type", "application/json") + + // Füge API Key hinzu, falls vorhanden + apiKey?.let { + requestBuilder.header("Authorization", "Bearer $it") + // Oder als Custom Header, je nach API: + // requestBuilder.header("X-API-Key", it) + } + + chain.proceed(requestBuilder.build()) + } + + return OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .addInterceptor(loggingInterceptor) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } + + /** + * Gibt die aktuelle Base URL zurück + */ + fun getBaseUrl(): String = baseUrl + + /** + * Prüft ob der Client initialisiert ist + */ + fun isInitialized(): Boolean = baseUrl.isNotEmpty() +} diff --git a/app/src/main/java/com/watcher/mobile/repository/MonitoringRepository.kt b/app/src/main/java/com/watcher/mobile/repository/MonitoringRepository.kt new file mode 100644 index 0000000..04170e7 --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/repository/MonitoringRepository.kt @@ -0,0 +1,137 @@ +package com.watcher.mobile.repository + +import com.watcher.mobile.data.* +import com.watcher.mobile.network.RetrofitClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Repository für Monitoring Daten + * Bietet eine saubere API für den Zugriff auf Remote-Daten + */ +class MonitoringRepository { + + private val apiService by lazy { RetrofitClient.getApiService() } + + /** + * Result Wrapper für API Calls + */ + sealed class Result { + data class Success(val data: T) : Result() + data class Error(val message: String, val exception: Throwable? = null) : Result() + object Loading : Result() + } + + // Dashboard + suspend fun getDashboard(): Result = withContext(Dispatchers.IO) { + try { + val response = apiService.getDashboard() + if (response.isSuccessful) { + val body = response.body() + if (body?.success == true && body.data != null) { + Result.Success(body.data) + } else { + Result.Error(body?.error ?: "Unknown error") + } + } else { + Result.Error("HTTP ${response.code()}: ${response.message()}") + } + } catch (e: Exception) { + Result.Error("Network error: ${e.message}", e) + } + } + + // Agents + suspend fun getAgents(page: Int = 1, pageSize: Int = 50): Result> = + withContext(Dispatchers.IO) { + try { + val response = apiService.getAgents(page, pageSize) + if (response.isSuccessful) { + val body = response.body() + if (body?.success == true && body.data != null) { + Result.Success(body.data) + } else { + Result.Error(body?.error ?: "Unknown error") + } + } else { + Result.Error("HTTP ${response.code()}: ${response.message()}") + } + } catch (e: Exception) { + Result.Error("Network error: ${e.message}", e) + } + } + + suspend fun getAgent(agentId: String): Result = withContext(Dispatchers.IO) { + try { + val response = apiService.getAgent(agentId) + if (response.isSuccessful) { + val body = response.body() + if (body?.success == true && body.data != null) { + Result.Success(body.data) + } else { + Result.Error(body?.error ?: "Unknown error") + } + } else { + Result.Error("HTTP ${response.code()}: ${response.message()}") + } + } catch (e: Exception) { + Result.Error("Network error: ${e.message}", e) + } + } + + // Alerts + suspend fun getAlerts( + page: Int = 1, + pageSize: Int = 50, + severity: String? = null, + acknowledged: Boolean? = null + ): Result> = withContext(Dispatchers.IO) { + try { + val response = apiService.getAlerts(page, pageSize, severity, acknowledged) + if (response.isSuccessful) { + val body = response.body() + if (body?.success == true && body.data != null) { + Result.Success(body.data) + } else { + Result.Error(body?.error ?: "Unknown error") + } + } else { + Result.Error("HTTP ${response.code()}: ${response.message()}") + } + } catch (e: Exception) { + Result.Error("Network error: ${e.message}", e) + } + } + + suspend fun acknowledgeAlert(alertId: String): Result = withContext(Dispatchers.IO) { + try { + val response = apiService.acknowledgeAlert(alertId) + if (response.isSuccessful) { + val body = response.body() + if (body?.success == true && body.data != null) { + Result.Success(body.data) + } else { + Result.Error(body?.error ?: "Unknown error") + } + } else { + Result.Error("HTTP ${response.code()}: ${response.message()}") + } + } catch (e: Exception) { + Result.Error("Network error: ${e.message}", e) + } + } + + // Health Check + suspend fun healthCheck(): Result = withContext(Dispatchers.IO) { + try { + val response = apiService.healthCheck() + if (response.isSuccessful) { + Result.Success(true) + } else { + Result.Error("Health check failed") + } + } catch (e: Exception) { + Result.Error("Health check failed: ${e.message}", e) + } + } +} diff --git a/app/src/main/java/com/watcher/mobile/ui/AgentsScreen.kt b/app/src/main/java/com/watcher/mobile/ui/AgentsScreen.kt new file mode 100644 index 0000000..1591c0d --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/ui/AgentsScreen.kt @@ -0,0 +1,226 @@ +package com.watcher.mobile.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.watcher.mobile.R +import com.watcher.mobile.data.Agent +import com.watcher.mobile.data.AgentStatus +import com.watcher.mobile.viewmodel.AgentsUiState +import com.watcher.mobile.viewmodel.AgentsViewModel +import java.text.SimpleDateFormat +import java.util.* + +@Composable +fun AgentsScreen( + viewModel: AgentsViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + Column(modifier = Modifier.fillMaxSize()) { + when (val state = uiState) { + is AgentsUiState.Loading -> { + LoadingView() + } + is AgentsUiState.Success -> { + AgentsContent( + agents = state.agents, + onRefresh = { viewModel.refresh() } + ) + } + is AgentsUiState.Error -> { + ErrorView( + message = state.message, + onRetry = { viewModel.refresh() } + ) + } + } + } +} + +@Composable +fun AgentsContent( + agents: List, + onRefresh: () -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + Text( + text = stringResource(R.string.agents_title), + style = MaterialTheme.typography.headlineLarge + ) + } + + if (agents.isEmpty()) { + item { + EmptyAgentsView() + } + } else { + items(agents) { agent -> + AgentCard(agent = agent) + } + } + } +} + +@Composable +fun AgentCard(agent: Agent) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // Status Indicator + Box( + modifier = Modifier + .size(48.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = when (agent.status) { + AgentStatus.ONLINE -> Icons.Default.CheckCircle + AgentStatus.OFFLINE -> Icons.Default.Cancel + AgentStatus.WARNING -> Icons.Default.Warning + AgentStatus.ERROR -> Icons.Default.Error + }, + contentDescription = null, + tint = when (agent.status) { + AgentStatus.ONLINE -> Color(0xFF4CAF50) + AgentStatus.OFFLINE -> Color(0xFF9E9E9E) + AgentStatus.WARNING -> Color(0xFFFFC107) + AgentStatus.ERROR -> Color(0xFFD32F2F) + }, + modifier = Modifier.size(32.dp) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + // Agent Info + Column(modifier = Modifier.weight(1f)) { + Text( + text = agent.name, + style = MaterialTheme.typography.titleMedium + ) + agent.hostname?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + agent.ipAddress?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = stringResource( + R.string.agents_last_seen, + formatTimestamp(agent.lastSeen) + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + // Metrics if available + agent.metrics?.let { metrics -> + Spacer(modifier = Modifier.height(8.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + metrics.cpuUsage?.let { + MetricBadge("CPU", "${it.toInt()}%") + } + metrics.memoryUsage?.let { + MetricBadge("RAM", "${it.toInt()}%") + } + metrics.diskUsage?.let { + MetricBadge("Disk", "${it.toInt()}%") + } + } + } + } + } + } +} + +@Composable +fun MetricBadge(label: String, value: String) { + Surface( + color = MaterialTheme.colorScheme.primaryContainer, + shape = MaterialTheme.shapes.small + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + text = value, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } +} + +@Composable +fun EmptyAgentsView() { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Computer, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "No agents found", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +private fun formatTimestamp(timestamp: Long): String { + val sdf = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault()) + return sdf.format(Date(timestamp)) +} diff --git a/app/src/main/java/com/watcher/mobile/ui/DashboardScreen.kt b/app/src/main/java/com/watcher/mobile/ui/DashboardScreen.kt new file mode 100644 index 0000000..8571b4a --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/ui/DashboardScreen.kt @@ -0,0 +1,264 @@ +package com.watcher.mobile.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.watcher.mobile.R +import com.watcher.mobile.data.Alert +import com.watcher.mobile.data.AlertSeverity +import com.watcher.mobile.viewmodel.DashboardUiState +import com.watcher.mobile.viewmodel.DashboardViewModel +import java.text.SimpleDateFormat +import java.util.* + +@Composable +fun DashboardScreen( + viewModel: DashboardViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + Column(modifier = Modifier.fillMaxSize()) { + when (val state = uiState) { + is DashboardUiState.Loading -> { + LoadingView() + } + is DashboardUiState.Success -> { + DashboardContent( + state = state, + onRefresh = { viewModel.refresh() } + ) + } + is DashboardUiState.Error -> { + ErrorView( + message = state.message, + onRetry = { viewModel.refresh() } + ) + } + } + } +} + +@Composable +fun DashboardContent( + state: DashboardUiState.Success, + onRefresh: () -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Header + item { + Text( + text = stringResource(R.string.dashboard_title), + style = MaterialTheme.typography.headlineLarge + ) + } + + // Summary Cards + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SummaryCard( + title = "Online", + value = state.data.onlineAgents.toString(), + icon = Icons.Default.CheckCircle, + color = Color(0xFF4CAF50), + modifier = Modifier.weight(1f) + ) + SummaryCard( + title = "Offline", + value = state.data.offlineAgents.toString(), + icon = Icons.Default.Cancel, + color = Color(0xFFFF5252), + modifier = Modifier.weight(1f) + ) + } + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SummaryCard( + title = "Alerts", + value = state.data.activeAlerts.toString(), + icon = Icons.Default.Notifications, + color = Color(0xFFFFC107), + modifier = Modifier.weight(1f) + ) + SummaryCard( + title = "Critical", + value = state.data.criticalAlerts.toString(), + icon = Icons.Default.Warning, + color = Color(0xFFFF5252), + modifier = Modifier.weight(1f) + ) + } + } + + // Recent Alerts + if (state.data.recentAlerts.isNotEmpty()) { + item { + Text( + text = stringResource(R.string.dashboard_recent_alerts), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 8.dp) + ) + } + + items(state.data.recentAlerts) { alert -> + AlertItem(alert = alert) + } + } + } +} + +@Composable +fun SummaryCard( + title: String, + value: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + color: Color, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = color, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = value, + style = MaterialTheme.typography.headlineMedium, + color = color + ) + Text( + text = title, + style = MaterialTheme.typography.bodyMedium + ) + } + } +} + +@Composable +fun AlertItem(alert: Alert) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = when (alert.severity) { + AlertSeverity.CRITICAL -> Color(0xFFFFEBEE) + AlertSeverity.WARNING -> Color(0xFFFFF8E1) + AlertSeverity.INFO -> Color(0xFFE3F2FD) + } + ) + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = when (alert.severity) { + AlertSeverity.CRITICAL -> Icons.Default.Error + AlertSeverity.WARNING -> Icons.Default.Warning + AlertSeverity.INFO -> Icons.Default.Info + }, + contentDescription = null, + tint = when (alert.severity) { + AlertSeverity.CRITICAL -> Color(0xFFD32F2F) + AlertSeverity.WARNING -> Color(0xFFFFA000) + AlertSeverity.INFO -> Color(0xFF1976D2) + }, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = alert.title, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = alert.message, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatTimestamp(alert.timestamp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +fun LoadingView() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +fun ErrorView(message: String, onRetry: () -> Unit) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(48.dp) + ) + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error + ) + Button(onClick = onRetry) { + Text(stringResource(R.string.retry)) + } + } + } +} + +private fun formatTimestamp(timestamp: Long): String { + val sdf = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault()) + return sdf.format(Date(timestamp)) +} diff --git a/app/src/main/java/com/watcher/mobile/ui/theme/Theme.kt b/app/src/main/java/com/watcher/mobile/ui/theme/Theme.kt new file mode 100644 index 0000000..40321eb --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/ui/theme/Theme.kt @@ -0,0 +1,76 @@ +package com.watcher.mobile.ui.theme + +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +// Light Theme Colors +private val LightColorScheme = lightColorScheme( + primary = Color(0xFF1976D2), + onPrimary = Color.White, + primaryContainer = Color(0xFFBBDEFB), + onPrimaryContainer = Color(0xFF003C6E), + secondary = Color(0xFF4CAF50), + onSecondary = Color.White, + secondaryContainer = Color(0xFFC8E6C9), + onSecondaryContainer = Color(0xFF1B5E20), + error = Color(0xFFD32F2F), + onError = Color.White, + errorContainer = Color(0xFFFFCDD2), + onErrorContainer = Color(0xFF8B0000), + background = Color(0xFFFAFAFA), + onBackground = Color(0xFF1C1B1F), + surface = Color.White, + onSurface = Color(0xFF1C1B1F) +) + +// Dark Theme Colors +private val DarkColorScheme = darkColorScheme( + primary = Color(0xFF64B5F6), + onPrimary = Color(0xFF003C6E), + primaryContainer = Color(0xFF1976D2), + onPrimaryContainer = Color(0xFFBBDEFB), + secondary = Color(0xFF81C784), + onSecondary = Color(0xFF1B5E20), + secondaryContainer = Color(0xFF388E3C), + onSecondaryContainer = Color(0xFFC8E6C9), + error = Color(0xFFEF5350), + onError = Color(0xFF8B0000), + errorContainer = Color(0xFFD32F2F), + onErrorContainer = Color(0xFFFFCDD2), + background = Color(0xFF1C1B1F), + onBackground = Color(0xFFE6E1E5), + surface = Color(0xFF2C2C2E), + onSurface = Color(0xFFE6E1E5) +) + +@Composable +fun WatcherMobileTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/watcher/mobile/ui/theme/Type.kt b/app/src/main/java/com/watcher/mobile/ui/theme/Type.kt new file mode 100644 index 0000000..4d4d989 --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/ui/theme/Type.kt @@ -0,0 +1,59 @@ +package com.watcher.mobile.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + displayLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = 0.sp + ), + headlineLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + headlineMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + bodyMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ) +) diff --git a/app/src/main/java/com/watcher/mobile/utils/PreferencesManager.kt b/app/src/main/java/com/watcher/mobile/utils/PreferencesManager.kt new file mode 100644 index 0000000..4d1dcd3 --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/utils/PreferencesManager.kt @@ -0,0 +1,91 @@ +package com.watcher.mobile.utils + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * PreferencesManager für App-Einstellungen + * Verwendet DataStore für moderne, sichere Preferences + */ +class PreferencesManager(private val context: Context) { + + private val Context.dataStore: DataStore by preferencesDataStore(name = "watcher_preferences") + + companion object { + private val API_BASE_URL = stringPreferencesKey("api_base_url") + private val API_KEY = stringPreferencesKey("api_key") + private val REFRESH_INTERVAL = intPreferencesKey("refresh_interval") + private val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled") + private val FIRST_RUN = booleanPreferencesKey("first_run") + } + + // API Base URL + val apiBaseUrl: Flow = context.dataStore.data.map { preferences -> + preferences[API_BASE_URL] + } + + suspend fun setApiBaseUrl(url: String) { + context.dataStore.edit { preferences -> + preferences[API_BASE_URL] = url + } + } + + // API Key + val apiKey: Flow = context.dataStore.data.map { preferences -> + preferences[API_KEY] + } + + suspend fun setApiKey(key: String) { + context.dataStore.edit { preferences -> + preferences[API_KEY] = key + } + } + + // Refresh Interval (in Sekunden) + val refreshInterval: Flow = context.dataStore.data.map { preferences -> + preferences[REFRESH_INTERVAL] ?: 30 // Default: 30 Sekunden + } + + suspend fun setRefreshInterval(interval: Int) { + context.dataStore.edit { preferences -> + preferences[REFRESH_INTERVAL] = interval + } + } + + // Notifications + val notificationsEnabled: Flow = context.dataStore.data.map { preferences -> + preferences[NOTIFICATIONS_ENABLED] ?: true // Default: enabled + } + + suspend fun setNotificationsEnabled(enabled: Boolean) { + context.dataStore.edit { preferences -> + preferences[NOTIFICATIONS_ENABLED] = enabled + } + } + + // First Run + val isFirstRun: Flow = context.dataStore.data.map { preferences -> + preferences[FIRST_RUN] ?: true + } + + suspend fun setFirstRunComplete() { + context.dataStore.edit { preferences -> + preferences[FIRST_RUN] = false + } + } + + // Clear all preferences + suspend fun clearAll() { + context.dataStore.edit { preferences -> + preferences.clear() + } + } +} diff --git a/app/src/main/java/com/watcher/mobile/viewmodel/AgentsViewModel.kt b/app/src/main/java/com/watcher/mobile/viewmodel/AgentsViewModel.kt new file mode 100644 index 0000000..06a6307 --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/viewmodel/AgentsViewModel.kt @@ -0,0 +1,52 @@ +package com.watcher.mobile.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.watcher.mobile.data.Agent +import com.watcher.mobile.repository.MonitoringRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * ViewModel für Agents Screen + */ +class AgentsViewModel( + private val repository: MonitoringRepository = MonitoringRepository() +) : ViewModel() { + + private val _uiState = MutableStateFlow(AgentsUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadAgents() + } + + fun loadAgents(page: Int = 1) { + viewModelScope.launch { + _uiState.value = AgentsUiState.Loading + when (val result = repository.getAgents(page)) { + is MonitoringRepository.Result.Success -> { + _uiState.value = AgentsUiState.Success(result.data.items) + } + is MonitoringRepository.Result.Error -> { + _uiState.value = AgentsUiState.Error(result.message) + } + is MonitoringRepository.Result.Loading -> { + _uiState.value = AgentsUiState.Loading + } + } + } + } + + fun refresh() { + loadAgents() + } +} + +sealed class AgentsUiState { + object Loading : AgentsUiState() + data class Success(val agents: List) : AgentsUiState() + data class Error(val message: String) : AgentsUiState() +} diff --git a/app/src/main/java/com/watcher/mobile/viewmodel/DashboardViewModel.kt b/app/src/main/java/com/watcher/mobile/viewmodel/DashboardViewModel.kt new file mode 100644 index 0000000..eb7fed1 --- /dev/null +++ b/app/src/main/java/com/watcher/mobile/viewmodel/DashboardViewModel.kt @@ -0,0 +1,52 @@ +package com.watcher.mobile.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.watcher.mobile.data.DashboardSummary +import com.watcher.mobile.repository.MonitoringRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * ViewModel für Dashboard Screen + */ +class DashboardViewModel( + private val repository: MonitoringRepository = MonitoringRepository() +) : ViewModel() { + + private val _uiState = MutableStateFlow(DashboardUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadDashboard() + } + + fun loadDashboard() { + viewModelScope.launch { + _uiState.value = DashboardUiState.Loading + when (val result = repository.getDashboard()) { + is MonitoringRepository.Result.Success -> { + _uiState.value = DashboardUiState.Success(result.data) + } + is MonitoringRepository.Result.Error -> { + _uiState.value = DashboardUiState.Error(result.message) + } + is MonitoringRepository.Result.Loading -> { + _uiState.value = DashboardUiState.Loading + } + } + } + } + + fun refresh() { + loadDashboard() + } +} + +sealed class DashboardUiState { + object Loading : DashboardUiState() + data class Success(val data: DashboardSummary) : DashboardUiState() + data class Error(val message: String) : DashboardUiState() +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..9f6279c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,45 @@ + + + Watcher Mobile + + + Dashboard + Agents + Alerts + Settings + + + Monitoring Dashboard + System Status + Active Agents + Recent Alerts + + + Monitoring Agents + Online + Offline + Last seen: %1$s + + + System Alerts + Critical + Warning + Info + + + Settings + API URL + https://your-api.com/api/ + API Key + Enter your API key + Refresh Interval + Notifications + Save + + + Network error. Please check your connection. + Error loading data + Loading… + Refresh + Retry + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..7614a41 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +