This commit is contained in:
2026-01-12 14:07:48 +01:00
commit a910959353
27 changed files with 2024 additions and 0 deletions

110
.gitignore vendored Normal file
View File

@@ -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

21
LICENSE Normal file
View File

@@ -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.

176
README.md Normal file
View File

@@ -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 <your-repository-url>
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]

109
app/build.gradle.kts Normal file
View File

@@ -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")
}

32
app/proguard-rules.pro vendored Normal file
View File

@@ -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.* <methods>;
}
-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.** { *; }

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Internet Permission für API Calls -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".WatcherApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WatcherMobile"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.WatcherMobile">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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
)
}
}

View File

@@ -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"
}
}

View File

@@ -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<Alert>,
@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<T>(
@SerializedName("success")
val success: Boolean,
@SerializedName("data")
val data: T?,
@SerializedName("error")
val error: String?,
@SerializedName("timestamp")
val timestamp: Long?
)
// Pagination
data class PaginatedResponse<T>(
@SerializedName("items")
val items: List<T>,
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("totalItems")
val totalItems: Int,
@SerializedName("totalPages")
val totalPages: Int
)

View File

@@ -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<ApiResponse<DashboardSummary>>
// Agents
@GET("agents")
suspend fun getAgents(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 50
): Response<ApiResponse<PaginatedResponse<Agent>>>
@GET("agents/{id}")
suspend fun getAgent(
@Path("id") agentId: String
): Response<ApiResponse<Agent>>
@GET("agents/{id}/metrics")
suspend fun getAgentMetrics(
@Path("id") agentId: String,
@Query("from") from: Long? = null,
@Query("to") to: Long? = null
): Response<ApiResponse<Any>> // 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<ApiResponse<PaginatedResponse<Alert>>>
@GET("alerts/{id}")
suspend fun getAlert(
@Path("id") alertId: String
): Response<ApiResponse<Alert>>
@POST("alerts/{id}/acknowledge")
suspend fun acknowledgeAlert(
@Path("id") alertId: String
): Response<ApiResponse<Alert>>
// Health Check
@GET("health")
suspend fun healthCheck(): Response<ApiResponse<Any>>
}

View File

@@ -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()
}

View File

@@ -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<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val exception: Throwable? = null) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// Dashboard
suspend fun getDashboard(): Result<DashboardSummary> = 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<PaginatedResponse<Agent>> =
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<Agent> = 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<PaginatedResponse<Alert>> = 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<Alert> = 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<Boolean> = 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)
}
}
}

View File

@@ -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<Agent>,
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))
}

View File

@@ -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))
}

View File

@@ -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
)
}

View File

@@ -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
)
)

View File

@@ -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<Preferences> 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<String?> = 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<String?> = 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<Int> = 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<Boolean> = 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<Boolean> = 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()
}
}
}

View File

@@ -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>(AgentsUiState.Loading)
val uiState: StateFlow<AgentsUiState> = _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<Agent>) : AgentsUiState()
data class Error(val message: String) : AgentsUiState()
}

View File

@@ -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>(DashboardUiState.Loading)
val uiState: StateFlow<DashboardUiState> = _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()
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Watcher Mobile</string>
<!-- Navigation -->
<string name="nav_dashboard">Dashboard</string>
<string name="nav_agents">Agents</string>
<string name="nav_alerts">Alerts</string>
<string name="nav_settings">Settings</string>
<!-- Dashboard -->
<string name="dashboard_title">Monitoring Dashboard</string>
<string name="dashboard_system_status">System Status</string>
<string name="dashboard_active_agents">Active Agents</string>
<string name="dashboard_recent_alerts">Recent Alerts</string>
<!-- Agents -->
<string name="agents_title">Monitoring Agents</string>
<string name="agents_status_online">Online</string>
<string name="agents_status_offline">Offline</string>
<string name="agents_last_seen">Last seen: %1$s</string>
<!-- Alerts -->
<string name="alerts_title">System Alerts</string>
<string name="alerts_severity_critical">Critical</string>
<string name="alerts_severity_warning">Warning</string>
<string name="alerts_severity_info">Info</string>
<!-- Settings -->
<string name="settings_title">Settings</string>
<string name="settings_api_url">API URL</string>
<string name="settings_api_url_hint">https://your-api.com/api/</string>
<string name="settings_api_key">API Key</string>
<string name="settings_api_key_hint">Enter your API key</string>
<string name="settings_refresh_interval">Refresh Interval</string>
<string name="settings_notifications">Notifications</string>
<string name="settings_save">Save</string>
<!-- Common -->
<string name="error_network">Network error. Please check your connection.</string>
<string name="error_loading">Error loading data</string>
<string name="loading">Loading…</string>
<string name="refresh">Refresh</string>
<string name="retry">Retry</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WatcherMobile" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Exclude DataStore preferences from backup -->
<exclude domain="sharedpref" path="datastore" />
</full-backup-content>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="datastore" />
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="datastore" />
</device-transfer>
</data-extraction-rules>

10
build.gradle.kts Normal file
View File

@@ -0,0 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20" apply false
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}

9
gradle.properties Normal file
View File

@@ -0,0 +1,9 @@
# Project-wide Gradle settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=false
kotlin.code.style=official
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true

7
gradlew vendored Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Gradle wrapper script placeholder
# Run './gradlew wrapper' to generate the actual wrapper files
echo "Please run 'gradle wrapper' from Android Studio or use the Gradle installation to generate wrapper files"
exit 1

18
settings.gradle.kts Normal file
View File

@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Watcher-Mobile"
include(":app")