diff --git a/README.md b/README.md new file mode 100644 index 0000000..5035cb3 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# Virtueller Gesundheitsassistent + +A Java 17 desktop application that ingests binary health predictions over MQTT, stores them in SQLite, computes rolling risk levels, and visualizes the result in a dashboard with optional avatar/Unreal integration. + +## Contents + +- [Features](#features) +- [Architecture Overview](#architecture-overview) +- [Tech Stack](#tech-stack) +- [Getting Started](#getting-started) +- [Configuration](#configuration) +- [Testing](#testing) +- [Project Structure](#project-structure) +- [Documentation](#documentation) +- [Known Limitations](#known-limitations) + +## Features + +- Swing desktop UI with: + - `Avatar Streaming` tab (embedded JCEF browser) + - `Dashboard` tab (risk time-series chart + level bar) +- MQTT subscription to prediction events (`PREDICTION` by default) +- SQLite persistence (`data/health.db`) +- Threshold-based risk classification from the latest 20 values: + - `NONE` for ratio `< 0.5` + - `WARNING` for ratio `>= 0.5` + - `HIGH` for ratio `>= 0.8` + - `DISASTER` for ratio `>= 0.9` +- Animation state JSON export (`animation.output.path`) +- Optional startup/shutdown management for: + - Python MQTT simulator + - Unreal/Pixel Streaming startup script +- Asynchronous logger with optional file output and rotation + +## Architecture Overview + +Runtime flow: + +1. The app initializes UI and services. +2. MQTT messages are received on a configured topic. +3. Incoming JSON payloads are validated. +4. Accepted predictions are persisted in SQLite. +5. Risk ratio and level are re-evaluated. +6. App state notifies UI controllers. +7. Dashboard widgets and chart update. +8. Animation JSON is written for external consumers. + +## Tech Stack + +- Java 17 +- Maven +- Swing + FlatLaf + JFreeChart +- Eclipse Paho MQTT client +- SQLite JDBC +- JCEF (`jcefmaven`) +- JUnit 5 + Mockito + +## Getting Started + +### Prerequisites + +- JDK 17 +- Maven 3.9+ +- MQTT broker running at `localhost:1883` (or adjust config) +- Windows for bundled PowerShell/BAT integration scripts +- Optional: Python for simulator support +- Optional: Unreal/Pixel Streaming environment + +### 1) Configure a minimal local run + +In `src/main/resources/config/application.properties`, disable optional external integrations if they are not available on your machine: + +```properties +mqtt_sim.enabled=false +unreal.enabled=false +``` + +### 2) Build and run + +```powershell +mvn clean compile +mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=vassistent.App +``` + +You can also run `vassistent.App` directly from an IDE. + +### 3) Send test data + +Publish to topic `PREDICTION` with payload format: + +```json +{ + "valid": true, + "_id": 1, + "prediction": 0 +} +``` + +Validation rules: + +- `valid` must be `true` +- `_id` must exist +- `prediction` must be `0` or `1` + +## Configuration + +Main config: `src/main/resources/config/application.properties` +Logger config: `src/main/resources/config/logger.properties` + +High-impact keys: + +- `mqtt.broker.url`, `mqtt.client.id`, `mqtt.topic` +- `streaming.url` +- `animation.output.path` +- `mqtt_sim.enabled`, `python.path`, `mqtt_sim.script` +- `unreal.enabled`, `unreal.executable`, `unreal.signalling_server.script` + +A full key-by-key reference is available in [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md). + +## Testing + +Run tests with: + +```powershell +mvn test +``` + +The suite includes unit and integration tests for state changes, payload validation, threshold evaluation, persistence behavior, process management, MQTT behavior, and animation output. + +## Project Structure + +```text +src/main/java/vassistent + App.java + bootstrap/ # startup wiring + shutdown sequencing + controller/ # UI orchestration + model/ # app state + value objects + service/ # MQTT, DB, stats, evaluation, processes + ui/ # Swing views/components + util/ # config loading/validation + logging + +src/main/resources + config/application.properties + config/logger.properties + scripts/mqtt_simulator.py + scripts/start_avatar.ps1 +``` + +## Documentation + +- [Documentation Index](docs/INDEX.md) +- [Project Review](docs/PROJECT_REVIEW.md) +- [Architecture](docs/ARCHITECTURE.md) +- [Setup and Run](docs/SETUP_AND_RUN.md) +- [Configuration](docs/CONFIGURATION.md) +- [Testing](docs/TESTING.md) +- [Source Map](docs/SOURCE_MAP.md) +- [Known Issues](docs/KNOWN_ISSUES.md) + +## Known Limitations + +- The runtime database (`data/health.db`) is currently deleted on application shutdown. +- Default Unreal-related paths in `application.properties` are machine-specific and require adaptation on other systems. +- Enabling optional external integrations without valid local paths causes startup validation to fail. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5858d62..6dc75c7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,62 +1,95 @@ # Architecture -## High-Level Overview +## System Overview -The application is a Java Swing desktop client that: +The application is a state-driven desktop client with an event-processing pipeline: -1. subscribes to MQTT prediction events, -2. stores accepted events in SQLite, -3. computes risk ratios, -4. updates UI state and visualization, -5. exports animation state for Unreal integration. +1. ingest prediction events from MQTT, +2. persist accepted values in SQLite, +3. evaluate risk based on recent history, +4. update UI state and rendering, +5. export animation state for external consumers. -## Layers +## Module Structure -- `bootstrap` -- builds and wires all services in `ApplicationInitializer` -- handles orderly shutdown in `ApplicationShutdownManager` -- `service` -- integration and business logic (MQTT, DB, stats, evaluation, external processes) -- `model` -- simple state and value objects (`AppState`, `ProblemLevel`, `DatabaseEntry`, `RatioPoint`) -- `controller` -- UI orchestration between state/services and Swing views -- `ui` -- window and visual components (tabs, chart, browser, status widgets) -- `util` -- shared helpers for config and logging +- `vassistent.bootstrap` + Application startup/shutdown orchestration (`ApplicationInitializer`, `ApplicationShutdownManager`) +- `vassistent.service` + Runtime services and integrations (MQTT, persistence, statistics, evaluation, process management) +- `vassistent.model` + Core state/data objects (`AppState`, `ProblemLevel`, `DatabaseEntry`, `RatioPoint`) +- `vassistent.controller` + Connects app state/services with Swing views +- `vassistent.ui` + Window and visual components +- `vassistent.util` + Configuration loading/validation and logging -## Runtime Data Flow +## Runtime Flow -1. `App.main` initializes look-and-feel and context. -2. `MqttClientService` connects to `tcp://localhost:1883`. -3. Subscription on configured topic routes payloads into `BinaryEventService`. -4. `BinaryEventService` validates JSON and stores `prediction` values. -5. `EvaluationService` gets ratio from `StatisticsService`. -6. `AppState` is updated with the new `ProblemLevel`. -7. Controllers listen to `AppState` and refresh UI (`DashboardView`, status bar). -8. `AnimationFileService` writes the animation JSON consumed by Unreal side logic. +### Startup sequence -## Main Components +1. `App.main` initializes look-and-feel. +2. `ApplicationInitializer.initialize()` validates config and wires all services. +3. MQTT subscription is registered on configured topic. +4. Optional external processes are started via `ProcessManagerService`. +5. UI window/controller is created on the Swing event thread. +6. JVM shutdown hook is registered through `ApplicationShutdownManager`. -- `vassistent.App` -- process entry point and shutdown hook registration -- `ApplicationContext` -- in-memory service container -- `DataPersistenceService` -- SQLite schema init and data access -- `MqttClientService` -- MQTT connect/subscribe/publish callbacks -- `StatisticsService` -- ratio and rolling-average computations -- `EvaluationService` -- ratio-to-level mapping -- `ProcessManagerService` -- optional startup/shutdown of simulator/Unreal helper processes +### Event processing sequence -## UI Structure +1. MQTT message arrives (`MqttClientService.messageArrived`). +2. Payload is routed to `BinaryEventService.handlePayload`. +3. Payload validation checks: + - `valid == true` + - `_id` is present + - `prediction` is `0` or `1` + - duplicate `_id` vs last processed message is rejected +4. Accepted value is stored by `DataPersistenceService`. +5. `EvaluationService` requests ratio from `StatisticsService.getRatio(20)`. +6. Ratio is mapped to a `ProblemLevel`. +7. `AppState` updates notify registered listeners. +8. `AnimationFileService` writes the corresponding animation JSON file. -- Main window: `AppWindow` -- Tab 1: `PixelStreamingView` (JCEF browser) -- Tab 2: `DashboardView` (JFreeChart + `ProblemLevelBar`) -- Footer status bar: MQTT status + current level +## Core Domain Rules + +`EvaluationService` maps ratios to levels: + +- `< 0.5` -> `NONE` +- `>= 0.5` -> `WARNING` +- `>= 0.8` -> `HIGH` +- `>= 0.9` -> `DISASTER` + +Persistence schema (`binary_event`): + +- `id` (autoincrement primary key) +- `value` (`0`/`1`) +- `timestamp` (ISO local date-time string) + +## Integration Boundaries + +- **MQTT**: Eclipse Paho client, broker URL and topic from config. +- **Database**: local SQLite file (`data/health.db` by default). +- **UI streaming tab**: JCEF browser for a configured URL (`streaming.url`). +- **External processes**: optional Python simulator and Unreal launcher script. +- **Animation output**: JSON file written to `animation.output.path`. + +## Shutdown Behavior + +`ApplicationShutdownManager` performs: + +1. MQTT disconnect, +2. managed process shutdown, +3. runtime database file deletion (`data/health.db`), +4. logger shutdown. + +## Architectural Tradeoffs + +- **Pros** + - Clear service boundaries and explicit startup wiring. + - Good testability for service layer and integration logic. + - Configuration validation catches many misconfigurations early. +- **Current tradeoffs** + - Local database lifecycle is ephemeral by default (deleted on shutdown). + - Startup is tightly coupled to external-process configuration when enabled. + - UI logic is mostly verified indirectly through state and service tests. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e9e5f02..28efb1a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,82 +1,125 @@ # Configuration -Application settings are loaded from classpath properties files in `src/main/resources/config`. +Configuration is loaded from classpath properties files in `src/main/resources/config`: + +- `application.properties` for runtime behavior +- `logger.properties` for logging behavior + +`AppConfigValidator` performs startup validation and fails fast on invalid settings. ## `application.properties` -| Key | Purpose | Current Default | +### Core runtime keys + +| Key | Required | Description | |---|---|---| -| `app.mode` | Generic mode flag | `test` | -| `streaming.url` | Initial URL for pixel streaming view | `http://localhost` | -| `python.path` | Python interpreter for simulator process | `C:\\Program Files\\PyManager\\python.exe` | -| `mqtt.broker.url` | MQTT broker connection URL | `tcp://localhost:1883` | -| `mqtt.client.id` | MQTT client identifier | `JavaClientPublisherSubscriber` | -| `mqtt.topic` | MQTT topic to subscribe to | `PREDICTION` | -| `mqtt_sim.enabled` | Enables simulator startup | `false` | -| `mqtt_sim.script` | Simulator script path | `src/main/resources/scripts/mqtt_simulator.py` | -| `mqtt_sim.broker` | Simulator broker override (env passthrough) | `localhost` | -| `mqtt_sim.port` | Simulator broker port override | `1883` | -| `mqtt_sim.topic` | Simulator publish topic override | `PREDICTION` | -| `mqtt_sim.qos` | Simulator QoS override | `0` | -| `mqtt_sim.interval_seconds` | Simulator publish interval override | `5` | -| `mqtt_sim.username` | Optional simulator auth username | empty | -| `mqtt_sim.password` | Optional simulator auth password | empty | -| `mqtt_sim.start_id` | Optional simulator initial payload id | `1` | -| `mqtt_sim.client_id` | Optional simulator MQTT client id | `mqtt-simulator` | -| `mqtt_sim.log_file` | Optional simulator log file path | `logs/mqtt_simulator.log` | -| `animation.output.path` | Generated animation state file path | `data/animation.json` | -| `unreal.enabled` | Enables Unreal startup flow | `true` | -| `unreal.executable` | PowerShell script to start Unreal process | absolute path | -| `unreal.signalling_server.script` | Signalling server BAT file path | absolute path | -| `unreal.pid.file` | Unreal PID file for shutdown cleanup | absolute path | -| `unreal.signalling.pid.file` | Signalling PID file for shutdown cleanup | absolute path | -| `unreal.pid.dir` | Script-side PID directory override | absolute path | -| `unreal.target.executable` | Script-side Unreal executable override | absolute path | -| `unreal.target.args` | Script-side Unreal argument string override | command string | -| `unreal.target.working_dir` | Script-side Unreal working dir override | absolute path | -| `unreal.startup.delay.seconds` | Script-side signalling warmup delay override | `5` | +| `mqtt.broker.url` | yes | MQTT broker URI (`tcp://...`, `ssl://...`, `ws://...`, `wss://...`) | +| `mqtt.client.id` | yes | MQTT client ID | +| `mqtt.topic` | yes | MQTT topic to subscribe to | +| `streaming.url` | yes | URL for embedded streaming view (`http`/`https`) | +| `animation.output.path` | yes | Target path for animation JSON output | +| `app.mode` | no | Generic runtime mode flag | -Notes: +### Optional simulator keys (`mqtt_sim.*`) -- Paths can be provided with or without wrapping quotes; startup sanitizes surrounding quotes. -- Unreal-related defaults are environment-specific and should be replaced per machine. -- `mqtt_sim.*` overrides are forwarded as environment variables to `mqtt_simulator.py`. -- `unreal.target.*` and `unreal.pid.dir` are forwarded as environment variables to `start_avatar.ps1`. +These become required/validated when `mqtt_sim.enabled=true`. + +| Key | Description | +|---|---| +| `mqtt_sim.enabled` | Enable Python simulator startup | +| `python.path` | Python executable path | +| `mqtt_sim.script` | Simulator script path | +| `mqtt_sim.broker` | Broker host passed to simulator | +| `mqtt_sim.port` | Broker port passed to simulator | +| `mqtt_sim.topic` | Simulator publish topic | +| `mqtt_sim.qos` | QoS (`0-2`) | +| `mqtt_sim.interval_seconds` | Publish interval in seconds | +| `mqtt_sim.username` / `mqtt_sim.password` | Optional broker auth | +| `mqtt_sim.start_id` | Optional starting `_id` | +| `mqtt_sim.client_id` | Optional simulator client id | +| `mqtt_sim.log_file` | Optional simulator log path | + +### Optional Unreal keys (`unreal.*`) + +These become required/validated when `unreal.enabled=true`. + +| Key | Description | +|---|---| +| `unreal.enabled` | Enable Unreal startup flow | +| `unreal.executable` | PowerShell launcher script path | +| `unreal.signalling_server.script` | Signalling server BAT path | +| `unreal.target.executable` | Unreal executable path passed to script | +| `unreal.target.working_dir` | Unreal working directory passed to script | +| `unreal.target.args` | Unreal launch arguments | +| `unreal.pid.dir` | Optional PID output directory for script | +| `unreal.pid.file` | PID file used for shutdown cleanup | +| `unreal.signalling.pid.file` | Signalling PID file used for cleanup | +| `unreal.startup.delay.seconds` | Delay before Unreal launch in script | + +### Process startup health checks + +| Key | Description | Default | +|---|---|---| +| `process.startup.healthcheck.enabled` | Enable startup health checks for managed processes | `true` | +| `process.startup.healthcheck.timeout.millis` | Health-check timeout | `10000` | +| `process.startup.healthcheck.poll.millis` | Poll interval | `250` | ## `logger.properties` -| Key | Purpose | Current Default | +| Key | Description | Typical value | |---|---|---| | `logger.level` | Minimum log level | `DEBUG` | -| `logger.file.enabled` | Enables file logging | `true` | -| `logger.file` | Log output file | `logs/application.log` | -| `logger.max.size.mb` | Rotation threshold | `10` | +| `logger.file.enabled` | Toggle file logging | `true` | +| `logger.file` | Log file path | `logs/application.log` | +| `logger.max.size.mb` | Rotation threshold in MB | `10` | -## Related Script Files +## Validation Behavior -- `src/main/resources/scripts/mqtt_simulator.py` -- `src/main/resources/scripts/start_avatar.ps1` +The validator checks: -## Script Runtime Inputs +- URI format and scheme for MQTT broker URL +- non-empty required keys +- numeric ranges (`port`, QoS, timeout/poll values, delays) +- file/directory existence for enabled integrations +- optional quote handling and invalid path syntax -`mqtt_simulator.py` supports CLI options and equivalent env vars: +Any validation errors are grouped and thrown as a single startup exception. -- `--broker` / `MQTT_SIM_BROKER` -- `--port` / `MQTT_SIM_PORT` -- `--topic` / `MQTT_SIM_TOPIC` -- `--username` / `MQTT_SIM_USERNAME` -- `--password` / `MQTT_SIM_PASSWORD` -- `--qos` / `MQTT_SIM_QOS` -- `--interval-seconds` / `MQTT_SIM_INTERVAL_SECONDS` -- `--start-id` / `MQTT_SIM_START_ID` -- `--client-id` / `MQTT_SIM_CLIENT_ID` -- `--log-file` / `MQTT_SIM_LOG_FILE` +## Script Environment Mapping -`start_avatar.ps1` supports parameters and equivalent env vars: +### `mqtt_simulator.py` -- `-PidDir` / `VGA_PID_DIR` -- `-UeExe` / `VGA_UE_EXE` -- `-UeArgs` / `VGA_UE_ARGS` -- `-SignallingBat` / `VGA_SIGNALLING_BAT` -- `-UeWorkingDir` / `VGA_UE_WORKDIR` -- `-SignallingStartupDelaySeconds` / `VGA_SIGNALLING_DELAY` +App config keys are forwarded as environment variables: + +- `mqtt_sim.broker` -> `MQTT_SIM_BROKER` +- `mqtt_sim.port` -> `MQTT_SIM_PORT` +- `mqtt_sim.topic` -> `MQTT_SIM_TOPIC` +- `mqtt_sim.username` -> `MQTT_SIM_USERNAME` +- `mqtt_sim.password` -> `MQTT_SIM_PASSWORD` +- `mqtt_sim.qos` -> `MQTT_SIM_QOS` +- `mqtt_sim.interval_seconds` -> `MQTT_SIM_INTERVAL_SECONDS` +- `mqtt_sim.start_id` -> `MQTT_SIM_START_ID` +- `mqtt_sim.client_id` -> `MQTT_SIM_CLIENT_ID` +- `mqtt_sim.log_file` -> `MQTT_SIM_LOG_FILE` + +### `start_avatar.ps1` + +App config keys are forwarded as environment variables: + +- `unreal.target.executable` -> `VGA_UE_EXE` +- `unreal.target.args` -> `VGA_UE_ARGS` +- `unreal.target.working_dir` -> `VGA_UE_WORKDIR` +- `unreal.signalling_server.script` -> `VGA_SIGNALLING_BAT` +- `unreal.startup.delay.seconds` -> `VGA_SIGNALLING_DELAY` +- `unreal.pid.dir` (or PID parent fallback) -> `VGA_PID_DIR` + +## Practical Recommendation + +For first-time local runs, keep: + +```properties +mqtt_sim.enabled=false +unreal.enabled=false +``` + +Then enable integrations incrementally after validating local paths and dependencies. diff --git a/docs/INDEX.md b/docs/INDEX.md index 5f827d7..b7effeb 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,20 +1,30 @@ # Documentation Index -This folder contains project-level documentation for the virtual health assistant. +This folder contains detailed technical documentation for the project. -## Contents +## Suggested Reading Order -- [Architecture](ARCHITECTURE.md) -- [Setup and Run](SETUP_AND_RUN.md) -- [Configuration](CONFIGURATION.md) -- [Source Map](SOURCE_MAP.md) -- [Testing](TESTING.md) -- [Known Issues](KNOWN_ISSUES.md) +1. [Project Review](PROJECT_REVIEW.md) +2. [Architecture](ARCHITECTURE.md) +3. [Setup and Run](SETUP_AND_RUN.md) +4. [Configuration](CONFIGURATION.md) +5. [Testing](TESTING.md) +6. [Known Issues](KNOWN_ISSUES.md) +7. [Source Map](SOURCE_MAP.md) -## Recommended Reading Order +## Document Guide -1. [Architecture](ARCHITECTURE.md) -2. [Setup and Run](SETUP_AND_RUN.md) -3. [Configuration](CONFIGURATION.md) -4. [Testing](TESTING.md) -5. [Known Issues](KNOWN_ISSUES.md) +- [Project Review](PROJECT_REVIEW.md) + End-to-end implementation summary, design decisions, quality checks, and roadmap. +- [Architecture](ARCHITECTURE.md) + Component responsibilities, runtime data flow, and integration boundaries. +- [Setup and Run](SETUP_AND_RUN.md) + Reproducible setup for minimal and full integration modes. +- [Configuration](CONFIGURATION.md) + Complete key reference, validation behavior, and script environment mapping. +- [Testing](TESTING.md) + Test strategy, inventory, execution, and remaining gaps. +- [Known Issues](KNOWN_ISSUES.md) + Active limitations and impact. +- [Source Map](SOURCE_MAP.md) + Package-level map for fast code navigation. diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index e2f2a5c..7fe9e00 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -2,35 +2,32 @@ ## 1. Environment-Specific Defaults -Configuration is now path-driven, but several default values in `application.properties` are still machine-specific: - -- Unreal startup script paths -- Unreal/signalling PID file paths +`application.properties` contains machine-specific Unreal paths and currently enables optional integrations by default. Impact: -- configuration updates are required before first run on a different machine +- first run on a different machine can fail at startup validation unless values are adapted. -## 2. Database Deleted on Shutdown +## 2. Database Is Deleted on Shutdown -`App.deleteDatabase()` deletes `data/health.db` on app exit. +`ApplicationShutdownManager` deletes `data/health.db` during shutdown. Impact: -- historical data is lost every run unless behavior is changed +- historical event data is not retained across sessions. -## 3. Signalling Process Startup Incomplete +## 3. Duplicate Filtering Is Limited to Last Seen `_id` -In `ProcessManagerService.startSignallingServer()`, process creation is currently prepared but not started (`pb.start()` is commented). +`BinaryEventService` only rejects a duplicate when it equals the immediate previous `_id`. Impact: -- log output may indicate startup intent, but process is not actually launched via that path +- non-consecutive duplicate IDs can still be persisted. -## 4. Encoding Artifacts in Logs/Strings +## 4. Startup Coupling to Optional Integrations -Some source/log text contains mojibake characters in comments/messages. +When `mqtt_sim.enabled=true` or `unreal.enabled=true`, startup is blocked if related files/paths are invalid. Impact: -- readability and consistency issues in logs and source text +- optional integrations can prevent the core app from launching. diff --git a/docs/PROJECT_REVIEW.md b/docs/PROJECT_REVIEW.md new file mode 100644 index 0000000..83b0548 --- /dev/null +++ b/docs/PROJECT_REVIEW.md @@ -0,0 +1,74 @@ +# Project Review + +## Purpose + +The virtual health assistant is built as an event-driven desktop system that converts binary health predictions into a continuously updated risk visualization. + +Primary goals: + +1. receive predictions in near real time via MQTT, +2. persist events locally, +3. compute rolling risk levels, +4. provide immediate visual feedback in the UI, +5. export machine-readable state for external avatar integration. + +## End-to-End Pipeline + +1. `MqttClientService` subscribes to the configured topic. +2. Payloads are forwarded to `BinaryEventService`. +3. Payloads are validated (`valid`, `_id`, `prediction`). +4. Accepted predictions are stored by `DataPersistenceService`. +5. `StatisticsService` computes the current ratio from the latest 20 entries. +6. `EvaluationService` maps ratio to `ProblemLevel`. +7. `AppState` notifies UI controllers and views. +8. `AnimationFileService` writes `animation.json`. + +## Functional Scope + +- Desktop monitoring UI with two views (stream + dashboard) +- MQTT-driven event ingestion +- SQLite event persistence +- Rolling ratio analytics +- Level classification (`NONE`, `WARNING`, `HIGH`, `DISASTER`) +- Optional external process orchestration (simulator + Unreal launcher) +- Startup configuration validation and structured logging + +## Design Highlights + +- **Separation of concerns** + Startup/shutdown orchestration lives in `bootstrap`; business logic in `service`; view orchestration in `controller`. +- **Config-first runtime behavior** + MQTT, simulator, Unreal, and animation output are controlled via properties. +- **Fail-fast startup validation** + `AppConfigValidator` blocks startup when enabled integrations are misconfigured. +- **Testability by construction** + Multiple services support dependency injection in tests (for example process launchers and MQTT client instances). + +## Quality and Verification + +Automated tests cover: + +- App state transitions and listener notifications +- Payload validation and duplicate-id handling +- Ratio and threshold evaluation behavior +- SQLite persistence integration behavior +- MQTT subscribe/publish/callback flow +- Process startup/shutdown command sequencing and startup report behavior +- Animation file mapping and overwrite behavior + +See [Testing](TESTING.md) for details and command examples. + +## Current Limitations + +- Runtime database is deleted on shutdown (`ApplicationShutdownManager`), so historical data is not retained. +- Default Unreal-related paths are machine-specific and must be adapted for new environments. +- UI behavior is not covered by end-to-end GUI tests. +- Duplicate payload protection is based on the last seen `_id` only (consecutive duplicates). + +## Improvement Roadmap + +1. Make database cleanup optional through configuration. +2. Add profile-based configuration (local/dev/demo) to reduce machine-specific defaults. +3. Add UI-level automated checks (or contract tests around controller/view boundaries). +4. Improve duplicate handling (e.g., bounded cache of recent IDs). +5. Add metrics/telemetry for startup health and message throughput. diff --git a/docs/SETUP_AND_RUN.md b/docs/SETUP_AND_RUN.md index 3001a97..b774f7a 100644 --- a/docs/SETUP_AND_RUN.md +++ b/docs/SETUP_AND_RUN.md @@ -4,13 +4,30 @@ - Java 17 (JDK) - Maven 3.9+ -- MQTT broker reachable at `localhost:1883` -- Windows environment for bundled PowerShell/BAT process scripts +- MQTT broker reachable at `localhost:1883` (or custom broker in config) +- Windows for bundled PowerShell/BAT process scripts Optional: -- Python (for MQTT simulator startup) -- Unreal + Pixel Streaming environment (if enabled in config) +- Python (for automatic MQTT simulator startup) +- Unreal + Pixel Streaming setup (for avatar integration) + +## Recommended Startup Modes + +### Mode A: Minimal local run (recommended first) + +Use this mode to run only the Java application and MQTT pipeline. + +In `src/main/resources/config/application.properties`: + +```properties +mqtt_sim.enabled=false +unreal.enabled=false +``` + +### Mode B: Full integration run + +Enable simulator and/or Unreal startup only after all referenced paths are valid on your machine. ## Build @@ -18,21 +35,21 @@ Optional: mvn clean compile ``` -## Run (CLI) +## Run from CLI ```powershell mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=vassistent.App ``` -## Run (IDE) +## Run from IDE -Run the class: +Start class: - `vassistent.App` -## MQTT Payload Format +## Send Test Messages -Publish payloads to configured topic (default `PREDICTION`) in this format: +Publish payloads to configured topic (default: `PREDICTION`) in this format: ```json { @@ -42,17 +59,25 @@ Publish payloads to configured topic (default `PREDICTION`) in this format: } ``` -Rules: +Validation rules: - `valid` must be `true` - `_id` must exist - `prediction` must be `0` or `1` +## Startup Validation Notes + +The app validates configuration at startup (`AppConfigValidator`): + +- if simulator is enabled, Python executable/script and simulator settings are validated; +- if Unreal integration is enabled, script/executable paths and related directories are validated; +- invalid configuration throws an `IllegalStateException` before UI startup. + ## Shutdown Behavior On shutdown, the app: -- disconnects MQTT -- stops managed external processes -- flushes logger queue -- deletes `data/health.db` (current implementation) +1. disconnects MQTT, +2. stops managed external processes, +3. deletes `data/health.db` (current behavior), +4. flushes and closes the logger. diff --git a/docs/SOURCE_MAP.md b/docs/SOURCE_MAP.md index e3aede6..5b7e0bf 100644 --- a/docs/SOURCE_MAP.md +++ b/docs/SOURCE_MAP.md @@ -1,53 +1,86 @@ # Source Map -Package-level map of all Java source files in `src/main/java` and tests in `src/test/java`. +Package-level source overview for `src/main/java` and `src/test/java`. ## Main Sources ### `vassistent` -- `App.java`: application entry point and shutdown hook logic +- `App.java` + Application entry point. ### `vassistent.bootstrap` -- `ApplicationContext.java`: container for shared services/state -- `ApplicationInitializer.java`: wiring and startup sequence -- `ApplicationShutdownManager.java`: managed shutdown sequence +- `ApplicationContext.java` + In-memory container for shared services/state. +- `ApplicationInitializer.java` + Service wiring, startup orchestration, process startup reporting. +- `ApplicationShutdownManager.java` + Graceful shutdown: MQTT disconnect, process stop, DB cleanup, logger shutdown. ### `vassistent.controller` -- `AppWindowController.java`: main window lifecycle and state-driven UI updates -- `DashboardController.java`: chart and level updates on state changes -- `StreamingController.java`: stream view actions (reload/focus) +- `AppWindowController.java` + Main window lifecycle and cross-view coordination. +- `DashboardController.java` + Updates chart/level view from app state. +- `StreamingController.java` + Controls stream view actions. ### `vassistent.model` -- `AppState.java`: observable app state -- `DatabaseEntry.java`: persisted event value + timestamp -- `ProblemLevel.java`: risk level enum -- `RatioPoint.java`: chart point model +- `AppState.java` + Observable application state. +- `DatabaseEntry.java` + Persisted event value + timestamp. +- `ProblemLevel.java` + Risk-level enum. +- `RatioPoint.java` + Time-series point for chart data. ### `vassistent.service` -- `AnimationFileService.java`: writes animation state JSON file -- `BinaryEventService.java`: payload validation + dispatch -- `DataPersistenceService.java`: SQLite init and CRUD-like methods -- `EvaluationService.java`: ratio-to-problem-level mapping -- `MqttClientService.java`: MQTT connectivity and callbacks -- `ProcessManagerService.java`: optional process startup/shutdown -- `StatisticsService.java`: ratio and rolling-avg computations +- `AnimationFileService.java` + Writes current animation state JSON. +- `BinaryEventService.java` + Validates payloads and triggers persistence/evaluation. +- `DataPersistenceService.java` + SQLite schema setup and event read/write methods. +- `EvaluationService.java` + Ratio-to-level mapping and state update. +- `MqttClientService.java` + MQTT connect/subscribe/publish and callback routing. +- `ProcessManagerService.java` + Optional simulator/Unreal process startup, health checks, shutdown. +- `StatisticsService.java` + Ratio and rolling-average calculations from persisted events. ### `vassistent.ui` -- `AppWindow.java`: root frame with tabbed layout -- `DashboardView.java`: chart + threshold markers + level display -- `PixelStreamingView.java`: JCEF browser panel for stream -- `ProblemLevelBar.java`: custom level visualization widget +- `AppWindow.java` + Root frame with tabbed layout. +- `DashboardView.java` + Chart and current-level visualization. +- `PixelStreamingView.java` + JCEF browser panel for stream URL. +- `ProblemLevelBar.java` + Custom UI component for level display. ### `vassistent.util` -- `ConfigLoader.java`: classpath properties loader -- `Logger.java`: async logger with file rotation +- `AppConfigValidator.java` + Startup configuration validation. +- `ConfigLoader.java` + Classpath properties loading. +- `Logger.java` + Asynchronous logger with file rotation support. + +## Resource Files + +- `src/main/resources/config/application.properties` +- `src/main/resources/config/logger.properties` +- `src/main/resources/scripts/mqtt_simulator.py` +- `src/main/resources/scripts/start_avatar.ps1` ## Test Sources diff --git a/docs/TESTING.md b/docs/TESTING.md index 14ac7dc..5615843 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,43 +1,57 @@ # Testing -## Run Tests +## Run the Test Suite ```powershell mvn test ``` -## Current Test Suite +## Test Strategy -Located in: +The project uses a mix of unit and integration tests: -- `src/test/java/vassistent/model/AppStateTest` -- `src/test/java/vassistent/service/AnimationFileServiceTest` -- `src/test/java/vassistent/service/BinaryEventServiceTest` -- `src/test/java/vassistent/service/DataPersistenceServiceTest` -- `src/test/java/vassistent/service/EvaluationServiceTest` -- `src/test/java/vassistent/service/MqttClientServiceTest` -- `src/test/java/vassistent/service/ProcessManagerServiceTest` -- `src/test/java/vassistent/service/StatisticsServiceTest` +- **Unit tests** for isolated business logic and service behavior +- **Integration tests** for filesystem/SQLite behavior and output artifacts +- **Mock-based tests** for MQTT and process-launch behavior -Coverage focus: +Frameworks: -- state change notifications -- payload validation and processing flow -- level evaluation thresholds -- MQTT lifecycle and topic routing -- process launch/shutdown command sequencing -- animation file output mapping -- SQLite persistence behavior -- rolling averages and ratio calculations +- JUnit Jupiter +- Mockito JUnit Jupiter -## Current Status (Java 17) +## Test Inventory -- full suite passes with `mvn test` -- test stack: - - `org.junit.jupiter:junit-jupiter` - - `org.mockito:mockito-junit-jupiter` +| Test Class | Type | Main Focus | +|---|---|---| +| `AppStateTest` | Unit | state defaults and listener notifications | +| `BinaryEventServiceTest` | Unit | payload validation and duplicate handling | +| `EvaluationServiceTest` | Unit | ratio-threshold mapping and animation side effect | +| `StatisticsServiceTest` | Unit + Integration | rolling averages and ratio calculation | +| `DataPersistenceServiceTest` | Integration | SQLite persistence and retrieval ordering | +| `MqttClientServiceTest` | Unit (mocked client) | MQTT connect/subscribe/publish callbacks | +| `ProcessManagerServiceTest` | Unit (injected launcher) | process startup/shutdown and healthcheck reporting | +| `AnimationFileServiceTest` | Integration | JSON output mapping and overwrite behavior | -## Notes +## What Is Covered Well -- some tests are integration-style and use temporary SQLite databases -- test output may include logger lines because services log during execution +- Core event pipeline behavior +- Threshold logic and level transitions +- Persistence query correctness +- Managed process command generation and reporting +- Animation output mapping to domain levels + +## Known Testing Gaps + +- No full end-to-end GUI automation (Swing/JCEF interaction) +- No performance/load tests for high MQTT throughput +- No integration tests for real external Unreal processes in CI +- No property-based or fuzz testing for malformed payload variants + +## Manual Verification Checklist + +After a successful run, verify: + +1. MQTT status in the UI changes to connected. +2. New payloads affect chart and level bar. +3. `animation.output.path` file is updated when level changes. +4. Shutdown stops optional processes and closes the application cleanly. diff --git a/readme.md b/readme.md deleted file mode 100644 index 471e077..0000000 --- a/readme.md +++ /dev/null @@ -1,168 +0,0 @@ -# Virtueller Gesundheitsassistent - -Java desktop application that receives binary health predictions via MQTT, stores them in SQLite, computes rolling risk ratios, and visualizes the result in a dashboard with an avatar streaming view. - -## Documentation - -- [Docs Index](docs/INDEX.md) -- [Architecture](docs/ARCHITECTURE.md) -- [Setup and Run](docs/SETUP_AND_RUN.md) -- [Configuration](docs/CONFIGURATION.md) -- [Source Map](docs/SOURCE_MAP.md) -- [Testing](docs/TESTING.md) -- [Known Issues](docs/KNOWN_ISSUES.md) - -## Features - -- Swing desktop UI with two tabs: -- `Avatar Streaming` (embedded JCEF browser, default `http://localhost`) -- `Dashboard` (time series chart + problem level bar) -- MQTT client subscriber (`tcp://localhost:1883`) for prediction events -- SQLite persistence (`data/health.db`) -- Rolling ratio evaluation and automatic risk level classification: -- `NONE` for `< 0.5` -- `WARNING` for `>= 0.5` -- `HIGH` for `>= 0.8` -- `DISASTER` for `>= 0.9` -- Writes animation state JSON for Unreal integration -- Optional external process startup: -- Python MQTT simulator -- Unreal/Pixel Streaming scripts -- Custom async logger with file output + rotation - -## Tech Stack - -- Java 17 -- Maven -- Swing + FlatLaf -- JFreeChart -- Eclipse Paho MQTT -- SQLite JDBC -- JCEF (`jcefmaven`) -- JUnit 5 + Mockito (tests) - -## Runtime Flow - -1. App initializes look and feel and services. -2. MQTT subscriber listens on topic from config (`mqtt.topic`). -3. Incoming JSON payloads are validated in `BinaryEventService`. -4. `prediction` (`0`/`1`) is stored in SQLite. -5. `EvaluationService` computes ratio over last 20 values. -6. `AppState` updates notify UI controllers. -7. Dashboard chart and status bar refresh. -8. Animation state file is written for Unreal consumption. - -## Payload Format - -Incoming MQTT payload must be JSON like: - -```json -{ - "valid": true, - "_id": 123, - "prediction": 0 -} -``` - -## Prerequisites - -- JDK 17 -- Maven 3.9+ -- Local MQTT broker on `localhost:1883` (or adjust code/config) -- Windows environment if using bundled process scripts/paths -- Optional: Python (if MQTT simulator should be auto-started) -- Optional: Unreal + Pixel Streaming setup (path-based integration) - -## Configuration - -Main config: `src/main/resources/config/application.properties` - -- `app.mode`: current mode flag (`test`) -- `streaming.url`: initial URL for embedded stream browser -- `python.path`: Python executable for simulator startup -- `mqtt.broker.url`: MQTT broker URL -- `mqtt.client.id`: MQTT client id -- `mqtt.topic`: subscribed topic (default `PREDICTION`) -- `mqtt_sim.enabled`: start simulator process on app startup -- `mqtt_sim.script`: simulator script path -- `mqtt_sim.broker` / `mqtt_sim.port` / `mqtt_sim.topic`: simulator runtime overrides -- `animation.output.path`: path for generated animation JSON file -- `unreal.enabled`: start Unreal-related processes -- `unreal.executable`: PowerShell script path for Unreal start -- `unreal.signalling_server.script`: signalling server batch path -- `unreal.pid.file`: PID file used for Unreal shutdown cleanup -- `unreal.signalling.pid.file`: PID file used for signalling shutdown cleanup -- `unreal.target.executable` / `unreal.target.args` / `unreal.target.working_dir`: optional overrides passed into `start_avatar.ps1` - -Logger config: `src/main/resources/config/logger.properties` - -- `logger.level` -- `logger.file.enabled` -- `logger.file` -- `logger.max.size.mb` - -## Run - -### 1) Start MQTT Broker - -Start a broker on `localhost:1883` (for example Mosquitto). - -### 2) Start the App - -```powershell -mvn clean compile -mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=vassistent.App -``` - -Or run `vassistent.App` directly from IntelliJ. - -### 3) Send Test Data - -Option A: enable simulator in `application.properties`: - -- set `mqtt_sim.enabled=true` -- verify `python.path` and `mqtt_sim.script` - -Option B: publish messages manually to topic `PREDICTION`. - -## Tests - -Run: - -```powershell -mvn test -``` - -Current state in this repository on Java 17: - -- unit and integration tests are available for state, event processing, evaluation, persistence, and statistics -- test stack uses JUnit 5 + Mockito JUnit Jupiter -- full suite runs successfully with `mvn test` - -## Project Structure - -```text -src/main/java/vassistent - App.java - bootstrap/ # wiring, context, shutdown sequencing - controller/ # Swing controllers - model/ # state + data types - service/ # MQTT, DB, stats, evaluation, processes - ui/ # App window, dashboard, streaming, widgets - util/ # config + logger - -src/main/resources - config/application.properties - config/logger.properties - scripts/mqtt_simulator.py - scripts/start_avatar.ps1 -``` - -## Important Notes - -- Animation output path is now config-driven (`animation.output.path`). -- MQTT broker URL/client id are config-driven (`mqtt.broker.url`, `mqtt.client.id`). -- Unreal PID cleanup paths are config-driven (`unreal.pid.file`, `unreal.signalling.pid.file`). -- Script runtime values can be passed from config into `mqtt_simulator.py` and `start_avatar.ps1`. -- On app shutdown, `data/health.db` is deleted by `App.deleteDatabase()`. -- The signalling server process startup in `ProcessManagerService` is prepared but currently not launched (`pb.start()` commented). diff --git a/src/main/java/vassistent/controller/AppWindowController.java b/src/main/java/vassistent/controller/AppWindowController.java index 137daa6..b9a0f5b 100644 --- a/src/main/java/vassistent/controller/AppWindowController.java +++ b/src/main/java/vassistent/controller/AppWindowController.java @@ -92,14 +92,14 @@ public class AppWindowController { private void subscribeAppState() { Logger.info("Controller", - "Subscribe AppState Observer gestartet"); + "Starting AppState observer subscription"); AppState state = context.getAppState(); state.addListener(this::onStateChanged); Logger.info("Controller", - "AppState Observer registriert"); + "AppState observer registered"); } /** @@ -111,7 +111,7 @@ public class AppWindowController { if (window == null) { Logger.warn("Controller", - "Window ist null, UI Update übersprungen"); + "Window is null, UI update skipped"); return; } @@ -126,7 +126,7 @@ public class AppWindowController { ); Logger.debug("Controller", - "ProblemLevel UI aktualisiert → " + "Problem level UI updated -> " + appState.getProblemLevel()); }); } diff --git a/src/main/java/vassistent/service/AnimationFileService.java b/src/main/java/vassistent/service/AnimationFileService.java index e16ddf9..e3ca736 100644 --- a/src/main/java/vassistent/service/AnimationFileService.java +++ b/src/main/java/vassistent/service/AnimationFileService.java @@ -58,10 +58,10 @@ public class AnimationFileService { writer.write(json); } - Logger.info("ANIMATION FILE", "Animation json geschrieben"); + Logger.info("ANIMATION FILE", "Animation JSON written"); } catch (IOException e) { - Logger.error("ANIMATION FILE", "Fehler beim Schreiben der Animation Datei", e); + Logger.error("ANIMATION FILE", "Failed to write animation file", e); } } diff --git a/src/main/java/vassistent/service/BinaryEventService.java b/src/main/java/vassistent/service/BinaryEventService.java index aafde68..e5e84a7 100644 --- a/src/main/java/vassistent/service/BinaryEventService.java +++ b/src/main/java/vassistent/service/BinaryEventService.java @@ -39,19 +39,19 @@ public class BinaryEventService { JsonObject json = JsonParser.parseString(payload).getAsJsonObject(); if (!json.has("valid") || !json.get("valid").getAsBoolean()) { - Logger.warn("EVENT", "Payload ist nicht valid"); + Logger.warn("EVENT", "Payload is not valid"); return; } if (!json.has("_id") || !json.has("prediction")) { - Logger.warn("EVENT", "Payload unvollständig"); + Logger.warn("EVENT", "Payload is incomplete"); return; } int id = json.get("_id").getAsInt(); if (lastId != null && lastId == id) { - Logger.warn("EVENT", "Payload ID bereits verarbeitet: " + id); + Logger.warn("EVENT", "Payload ID already processed: " + id); return; } @@ -60,17 +60,17 @@ public class BinaryEventService { int prediction = json.get("prediction").getAsInt(); if (prediction != 0 && prediction != 1) { - Logger.warn("EVENT", "Ungültige Prediction: " + prediction); + Logger.warn("EVENT", "Invalid prediction: " + prediction); return; } persistenceService.store(prediction); evaluationService.evaluate(); - Logger.debug("EVENT", "Prediction verarbeitet: " + prediction); + Logger.debug("EVENT", "Prediction processed: " + prediction); } catch (Exception e) { - Logger.error("EVENT", "Payload Verarbeitung fehlgeschlagen: " + payload, e); + Logger.error("EVENT", "Payload processing failed: " + payload, e); } } } diff --git a/src/main/java/vassistent/service/DataPersistenceService.java b/src/main/java/vassistent/service/DataPersistenceService.java index aa3fc1f..6e884e2 100644 --- a/src/main/java/vassistent/service/DataPersistenceService.java +++ b/src/main/java/vassistent/service/DataPersistenceService.java @@ -27,7 +27,7 @@ public class DataPersistenceService { init(); } - // Für Tests + // For tests /** * Creates a persistence service using a custom JDBC URL (primarily for tests). * @@ -47,9 +47,9 @@ public class DataPersistenceService { if (!folder.exists()) { boolean created = folder.mkdirs(); if (created) { - Logger.info("DB", "Datenbank-Ordner erstellt"); + Logger.info("DB", "Database folder created"); } else { - Logger.warn("DB", "Konnte DB-Ordner nicht erstellen"); + Logger.warn("DB", "Could not create database folder"); } } } @@ -69,10 +69,10 @@ public class DataPersistenceService { ) """); - Logger.info("DB", "Datenbank initialisiert"); + Logger.info("DB", "Database initialized"); } catch (SQLException e) { - Logger.error("DB", "Datenbank initialisierung fehlgeschlagen", e); + Logger.error("DB", "Database initialization failed", e); } } @@ -89,9 +89,9 @@ public class DataPersistenceService { ps.setInt(1, value); ps.setString(2, LocalDateTime.now().toString()); ps.executeUpdate(); - //Logger.debug("DB", "Database Insert erfolgreich: " + ps.toString()); + //Logger.debug("DB", "Database insert successful: " + ps.toString()); } catch (SQLException e) { - Logger.error("DB", "Database Insert fehlgeschlagen", e); + Logger.error("DB", "Database insert failed", e); } } @@ -130,7 +130,7 @@ public class DataPersistenceService { } catch (SQLException e) { Logger.error("DB", - "Fehler beim Laden der letzten Einträge", e); + "Failed to load latest entries", e); } Collections.reverse(result); diff --git a/src/main/java/vassistent/service/MqttClientService.java b/src/main/java/vassistent/service/MqttClientService.java index 06215af..08feaea 100644 --- a/src/main/java/vassistent/service/MqttClientService.java +++ b/src/main/java/vassistent/service/MqttClientService.java @@ -111,7 +111,7 @@ public class MqttClientService implements MqttCallback { } } catch (MqttException e) { - Logger.error("MQTT", "Fehler beim Verbinden", e); + Logger.error("MQTT", "Failed to connect", e); appState.setMqttConnected(false); } } @@ -126,10 +126,10 @@ public class MqttClientService implements MqttCallback { options.setCleanSession(true); options.setAutomaticReconnect(true); - Logger.info("MQTT", "Verbinde mit Broker " + brokerUrl); + Logger.info("MQTT", "Connecting to broker " + brokerUrl); client.connect(options); appState.setMqttConnected(true); - Logger.info("MQTT", "Verbindung hergestellt"); + Logger.info("MQTT", "Connection established"); } /** @@ -139,10 +139,10 @@ public class MqttClientService implements MqttCallback { try { if (client != null && client.isConnected()) { client.disconnect(); - Logger.info("MQTT", "Verbindung getrennt"); + Logger.info("MQTT", "Connection closed"); } } catch (MqttException e) { - Logger.error("MQTT", "Fehler beim Trennen", e); + Logger.error("MQTT", "Failed to disconnect", e); } } @@ -156,9 +156,9 @@ public class MqttClientService implements MqttCallback { topicListeners.put(topic, listener); try { client.subscribe(topic); - Logger.info("MQTT", "Topic abonniert: " + topic); + Logger.info("MQTT", "Subscribed to topic: " + topic); } catch (MqttException e) { - Logger.error("MQTT", "Subscribe fehlgeschlagen: " + topic, e); + Logger.error("MQTT", "Subscribe failed: " + topic, e); } } @@ -182,7 +182,7 @@ public class MqttClientService implements MqttCallback { "Publish -> Topic=" + topic + ", QoS=" + qos + ", Payload=" + message ); } catch (MqttException e) { - Logger.error("MQTT", "Publish fehlgeschlagen", e); + Logger.error("MQTT", "Publish failed", e); } } @@ -195,7 +195,7 @@ public class MqttClientService implements MqttCallback { public void connectionLost(Throwable cause) { Logger.warn( "MQTT", - "Verbindung verloren: " + cause.getMessage() + "Connection lost: " + cause.getMessage() ); appState.setMqttConnected(false); @@ -215,7 +215,7 @@ public class MqttClientService implements MqttCallback { //Logger.debug( // "MQTT", - // "Nachricht empfangen -> Topic=" + topic + ", Payload=" + payload + // "Message received -> Topic=" + topic + ", Payload=" + payload //); Consumer listener = topicListeners.get(topic); @@ -225,7 +225,7 @@ public class MqttClientService implements MqttCallback { } else { Logger.warn( "MQTT", - "Keine Listener für Topic: " + topic + "No listener for topic: " + topic ); } } diff --git a/src/main/java/vassistent/service/ProcessManagerService.java b/src/main/java/vassistent/service/ProcessManagerService.java index 6f3d5b5..51d84d2 100644 --- a/src/main/java/vassistent/service/ProcessManagerService.java +++ b/src/main/java/vassistent/service/ProcessManagerService.java @@ -336,13 +336,13 @@ public class ProcessManagerService { pythonProcess = processLauncher.launch(pb); verifyPythonStartupHealth(); - Logger.info("PROCESS", "Mqtt Simulator gestartet"); + Logger.info("PROCESS", "MQTT simulator started"); return ProcessStartStatus.started("mqtt_sim"); } catch (Exception e) { if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } - Logger.error("PROCESS", "Mqtt Simulator Start fehlgeschlagen", e); + Logger.error("PROCESS", "MQTT simulator startup failed", e); return ProcessStartStatus.failed( "mqtt_sim", exceptionMessage(e) @@ -372,7 +372,7 @@ public class ProcessManagerService { if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } - Logger.error("PROCESS", "Unreal Start fehlgeschlagen", e); + Logger.error("PROCESS", "Unreal startup failed", e); return ProcessStartStatus.failed( "unreal", exceptionMessage(e) @@ -404,7 +404,7 @@ public class ProcessManagerService { //pb.directory(new File(exe).getParentFile()); Logger.info("PROCESS", - "Unreal Engine gestartet" + pb.command()); + "Unreal Engine started " + pb.command()); } /** @@ -450,7 +450,7 @@ public class ProcessManagerService { */ public void shutdown() { - Logger.info("PROCESS", "Shutdown externe Prozesse gestartet"); + Logger.info("PROCESS", "External process shutdown started"); terminateProcess(pythonProcess); terminateProcess(unrealProcess); @@ -458,7 +458,7 @@ public class ProcessManagerService { killProcessFromPidFile(unrealPidFile); killProcessFromPidFile(signallingPidFile); - Logger.info("PROCESS", "Externe Prozesse beendet"); + Logger.info("PROCESS", "External processes stopped"); } /** @@ -486,11 +486,11 @@ public class ProcessManagerService { processLauncher.launch(pb).waitFor(); Logger.info("PROCESS", - "Process Tree beendet → PID " + pid); + "Process tree terminated -> PID " + pid); } catch (Exception e) { Logger.error("PROCESS", - "Fehler beim Prozess Kill", e); + "Failed to terminate process", e); } } @@ -518,7 +518,7 @@ public class ProcessManagerService { processLauncher.launch(pb).waitFor(); } catch (Exception e) { - Logger.error("PROCESS", "PID Kill fehlgeschlagen", e); + Logger.error("PROCESS", "PID kill failed", e); } } diff --git a/src/main/java/vassistent/ui/AppWindow.java b/src/main/java/vassistent/ui/AppWindow.java index ba034bb..bb8f5bf 100644 --- a/src/main/java/vassistent/ui/AppWindow.java +++ b/src/main/java/vassistent/ui/AppWindow.java @@ -1,12 +1,10 @@ package vassistent.ui; -import vassistent.bootstrap.ApplicationContext; -import vassistent.controller.DashboardController; import vassistent.util.ConfigLoader; import javax.swing.*; -import javax.swing.border.Border; import java.awt.*; +import java.util.Locale; import java.util.Properties; /** @@ -14,6 +12,9 @@ import java.util.Properties; */ public class AppWindow extends JFrame { + private static final int EXPANDED_DASHBOARD_WIDTH = 460; + private static final int EXPANDED_DIVIDER_SIZE = 8; + private static final Properties config = ConfigLoader.loadProperties( "config/application.properties" @@ -22,6 +23,9 @@ public class AppWindow extends JFrame { private PixelStreamingView streamingView; private DashboardView dashboardView; private JTabbedPane tabs; + private JSplitPane driveSplitPane; + private JPanel dashboardCard; + private JToggleButton dashboardToggleButton; private JLabel mqttStatusLabel; private JLabel problemLevelLabel; @@ -33,10 +37,12 @@ public class AppWindow extends JFrame { setTitle("Virtueller Gesundheitsassistent"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setSize(1400, 850); + setMinimumSize(new Dimension(1280, 720)); + setSize(1700, 900); setLocationRelativeTo(null); + getContentPane().setBackground(new Color(15, 19, 26)); - setLayout(new BorderLayout(10,10)); + setLayout(new BorderLayout(12, 12)); String streamUrl = normalizeConfigValue( @@ -51,11 +57,13 @@ public class AppWindow extends JFrame { dashboardView = new DashboardView(); + add(createHeaderPanel(), BorderLayout.NORTH); tabs = createTabPane(); add(tabs, BorderLayout.CENTER); add(createStatusBar(), BorderLayout.SOUTH); + SwingUtilities.invokeLater(() -> setDashboardVisible(false)); - Font uiFont = new Font("Segoe UI", Font.PLAIN, 14); + Font uiFont = new Font("Segoe UI", Font.PLAIN, 16); UIManager.put("Label.font", uiFont); UIManager.put("TabbedPane.font", uiFont); } @@ -68,17 +76,96 @@ public class AppWindow extends JFrame { private JTabbedPane createTabPane() { JTabbedPane tabs = new JTabbedPane(); - tabs.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); - - JPanel streamingPanel = new JPanel(new BorderLayout(10,10)); - streamingPanel.add(streamingView, BorderLayout.CENTER); - - tabs.addTab("Avatar Streaming", streamingView); - tabs.addTab("Dashboard", dashboardView); + tabs.setBorder(BorderFactory.createEmptyBorder(0, 10, 6, 10)); + tabs.setFocusable(false); + tabs.putClientProperty("JTabbedPane.tabHeight", 38); + tabs.putClientProperty("JTabbedPane.hideTabAreaWithOneTab", true); + tabs.addTab("Drive View", createDriveViewPanel()); return tabs; } + /** + * Creates the center-display optimized drive panel. + * + * @return drive panel with large assistant stream and compact dashboard + */ + private JPanel createDriveViewPanel() { + JPanel drivePanel = new JPanel(new BorderLayout()); + drivePanel.setOpaque(false); + drivePanel.setBorder(BorderFactory.createEmptyBorder(6, 0, 0, 0)); + + JPanel streamCard = createCardPanel(); + streamCard.setLayout(new BorderLayout(0, 12)); + streamCard.add( + createSectionHeader( + "Virtual Health Assistant", + null + ), + BorderLayout.NORTH + ); + streamCard.add(streamingView, BorderLayout.CENTER); + + dashboardCard = createCardPanel(); + dashboardCard.setLayout(new BorderLayout(0, 12)); + dashboardCard.add( + createSectionHeader( + "Health Overview", + "Live trend and current risk level" + ), + BorderLayout.NORTH + ); + dashboardCard.add(dashboardView, BorderLayout.CENTER); + dashboardCard.setPreferredSize(new Dimension(EXPANDED_DASHBOARD_WIDTH, 0)); + dashboardCard.setMinimumSize(new Dimension(380, 0)); + + driveSplitPane = + new JSplitPane( + JSplitPane.HORIZONTAL_SPLIT, + streamCard, + dashboardCard + ); + + driveSplitPane.setBorder(null); + driveSplitPane.setOpaque(false); + driveSplitPane.setContinuousLayout(true); + driveSplitPane.setOneTouchExpandable(false); + driveSplitPane.setDividerSize(EXPANDED_DIVIDER_SIZE); + driveSplitPane.setResizeWeight(0.72); + driveSplitPane.setDividerLocation(0.72); + + drivePanel.add(driveSplitPane, BorderLayout.CENTER); + + return drivePanel; + } + + /** + * Creates a top banner with project title and context. + * + * @return header panel + */ + private JPanel createHeaderPanel() { + JPanel header = new JPanel(new BorderLayout(12, 0)); + header.setBorder(BorderFactory.createEmptyBorder(12, 16, 0, 16)); + header.setOpaque(false); + + JLabel title = new JLabel("Virtueller Gesundheitsassistent"); + title.setFont(new Font("Segoe UI Semibold", Font.PLAIN, 30)); + title.setForeground(new Color(238, 244, 255)); + + JPanel textStack = new JPanel(); + textStack.setLayout(new BoxLayout(textStack, BoxLayout.Y_AXIS)); + textStack.setOpaque(false); + title.setAlignmentX(Component.LEFT_ALIGNMENT); + textStack.add(title); + textStack.add(Box.createVerticalStrut(4)); + + header.add(textStack, BorderLayout.WEST); + header.add(createDashboardToggleButton(), BorderLayout.EAST); + + return header; + } + /** * Creates the status bar with MQTT and problem-level indicators. * @@ -86,18 +173,18 @@ public class AppWindow extends JFrame { */ private JPanel createStatusBar() { - JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT,15,5)); + JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 12, 8)); + statusBar.setBackground(new Color(13, 16, 23)); statusBar.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1,0,0,0, Color.LIGHT_GRAY), - BorderFactory.createEmptyBorder(5,10,5,10) + BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(54, 64, 78)), + BorderFactory.createEmptyBorder(6, 12, 8, 12) )); - mqttStatusLabel = new JLabel("MQTT: Disconnected"); - problemLevelLabel = new JLabel("Problem: NONE"); + mqttStatusLabel = createStatusChip("MQTT: Disconnected"); + problemLevelLabel = createStatusChip("Problem: NONE"); statusBar.add(mqttStatusLabel); - statusBar.add(new JLabel(" | ")); statusBar.add(problemLevelLabel); return statusBar; @@ -109,8 +196,11 @@ public class AppWindow extends JFrame { * @param connected whether MQTT is currently connected */ public void updateMqttStatus(boolean connected) { - mqttStatusLabel.setText("MQTT: " + - (connected ? "Connected" : "Disconnected")); + mqttStatusLabel.setText("MQTT: " + + (connected ? "Connected" : "Disconnected")); + mqttStatusLabel.setBackground( + connected ? new Color(30, 101, 70) : new Color(108, 47, 47) + ); } /** @@ -120,6 +210,7 @@ public class AppWindow extends JFrame { */ public void updateProblemLevel(String level) { problemLevelLabel.setText("Problem: " + level); + problemLevelLabel.setBackground(getProblemChipColor(level)); } /** @@ -149,6 +240,160 @@ public class AppWindow extends JFrame { return tabs; } + /** + * Creates a panel with a subtle border and dark background. + * + * @return styled card panel + */ + private JPanel createCardPanel() { + JPanel card = new JPanel(); + card.setOpaque(true); + card.setBackground(new Color(24, 31, 41)); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(61, 74, 92)), + BorderFactory.createEmptyBorder(12, 12, 12, 12) + )); + return card; + } + + /** + * Creates a section heading with title and subtitle text. + * + * @param title section title + * @param subtitle section subtitle + * @return heading panel + */ + private JPanel createSectionHeader(String title, String subtitle) { + JPanel panel = new JPanel(); + panel.setOpaque(false); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel titleLabel = new JLabel(title); + titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + titleLabel.setFont(new Font("Segoe UI Semibold", Font.PLAIN, 20)); + titleLabel.setForeground(new Color(233, 241, 255)); + + JLabel subtitleLabel = new JLabel(subtitle); + subtitleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + subtitleLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + subtitleLabel.setForeground(new Color(143, 159, 182)); + + panel.add(titleLabel); + panel.add(Box.createVerticalStrut(2)); + panel.add(subtitleLabel); + + return panel; + } + + /** + * Creates one status label used in the bottom status bar. + * + * @param text initial status text + * @return styled status label + */ + private JLabel createStatusChip(String text) { + JLabel label = new JLabel(text); + label.setOpaque(true); + label.setFont(new Font("Segoe UI Semibold", Font.PLAIN, 14)); + label.setForeground(new Color(237, 244, 255)); + label.setBackground(new Color(56, 66, 83)); + label.setBorder(BorderFactory.createEmptyBorder(5, 12, 5, 12)); + return label; + } + + /** + * Returns a status chip background color for the current problem level. + * + * @param level problem level name + * @return matching background color + */ + private Color getProblemChipColor(String level) { + if (level == null) { + return new Color(56, 66, 83); + } + + String normalized = level.trim().toUpperCase(Locale.ROOT); + + switch (normalized) { + case "WARNING": + return new Color(134, 104, 29); + case "HIGH": + return new Color(168, 92, 31); + case "DISASTER": + return new Color(140, 49, 49); + default: + return new Color(46, 99, 61); + } + } + + /** + * Creates the button used to toggle dashboard visibility. + * + * @return configured toggle button + */ + private JToggleButton createDashboardToggleButton() { + dashboardToggleButton = new JToggleButton("Dashboard einblenden"); + dashboardToggleButton.setFocusable(false); + dashboardToggleButton.setFont(new Font("Segoe UI Semibold", Font.PLAIN, 14)); + dashboardToggleButton.setForeground(new Color(233, 241, 255)); + dashboardToggleButton.setBackground(new Color(39, 77, 57)); + dashboardToggleButton.setBorder(BorderFactory.createEmptyBorder(8, 14, 8, 14)); + dashboardToggleButton.addActionListener( + event -> setDashboardVisible(dashboardToggleButton.isSelected()) + ); + return dashboardToggleButton; + } + + /** + * Shows or hides the dashboard side panel in drive view. + * + * @param visible whether dashboard should be visible + */ + private void setDashboardVisible(boolean visible) { + if (driveSplitPane == null || dashboardCard == null || dashboardToggleButton == null) { + return; + } + + dashboardCard.setVisible(visible); + driveSplitPane.setDividerSize(visible ? EXPANDED_DIVIDER_SIZE : 0); + + if (visible) { + dashboardToggleButton.setSelected(true); + dashboardToggleButton.setText("Dashboard ausblenden"); + dashboardToggleButton.setBackground(new Color(69, 86, 109)); + + SwingUtilities.invokeLater(() -> { + int width = driveSplitPane.getWidth(); + if (width <= 0) { + driveSplitPane.setDividerLocation(0.72); + return; + } + + int dividerLocation = Math.max( + (int) (width * 0.55), + width - EXPANDED_DASHBOARD_WIDTH + ); + driveSplitPane.setDividerLocation(dividerLocation); + }); + } else { + dashboardToggleButton.setSelected(false); + dashboardToggleButton.setText("Dashboard einblenden"); + dashboardToggleButton.setBackground(new Color(39, 77, 57)); + + SwingUtilities.invokeLater(() -> { + int width = driveSplitPane.getWidth(); + if (width > 0) { + driveSplitPane.setDividerLocation(width); + } else { + driveSplitPane.setDividerLocation(1.0d); + } + }); + } + + driveSplitPane.revalidate(); + driveSplitPane.repaint(); + } + /** * Trims a config value and strips optional wrapping quotes. * diff --git a/src/main/java/vassistent/ui/DashboardView.java b/src/main/java/vassistent/ui/DashboardView.java index 4ab9c42..78da4d7 100644 --- a/src/main/java/vassistent/ui/DashboardView.java +++ b/src/main/java/vassistent/ui/DashboardView.java @@ -12,18 +12,15 @@ import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.ui.Layer; import org.jfree.chart.ui.RectangleAnchor; import org.jfree.chart.ui.TextAnchor; -import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.data.time.Millisecond; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; -import vassistent.model.ProblemLevel; import vassistent.model.RatioPoint; import javax.swing.*; import java.awt.*; -import java.sql.Timestamp; +import java.text.SimpleDateFormat; import java.time.ZoneId; -import java.util.Arrays; import java.util.Date; import java.util.List; @@ -42,28 +39,30 @@ public class DashboardView extends JPanel { * Creates the dashboard panel with problem-level bar and time-series chart. */ public DashboardView() { - setLayout(new BorderLayout()); - setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); + setLayout(new BorderLayout(0, 12)); + setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); setOpaque(false); - JPanel card = new JPanel(new BorderLayout(10,10)); - card.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); - - card.setBackground(UIManager.getColor("Panel.background")); + JPanel card = new JPanel(new BorderLayout(0, 14)); + card.setBackground(new Color(22, 28, 37)); card.setOpaque(true); - card.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( - UIManager.getColor("Component.borderColor") + new Color(58, 72, 90) ), - BorderFactory.createEmptyBorder(12,12,12,12) + BorderFactory.createEmptyBorder(14, 14, 14, 14) )); // ---------- TOP: Problem Level ---------- - JPanel topWrapper = new JPanel(new BorderLayout()); - topWrapper.setBorder(BorderFactory.createEmptyBorder(0,0,10,0)); + JPanel topWrapper = new JPanel(new BorderLayout(0, 8)); + topWrapper.setBorder(BorderFactory.createEmptyBorder(0, 0, 8, 0)); topWrapper.setOpaque(false); + JLabel topLabel = new JLabel("Current Risk Level"); + topLabel.setFont(new Font("Segoe UI Semibold", Font.PLAIN, 24)); + topLabel.setForeground(new Color(230, 237, 249)); + topWrapper.add(topLabel, BorderLayout.NORTH); + levelBar = new ProblemLevelBar(); topWrapper.add(levelBar, BorderLayout.CENTER); @@ -83,21 +82,28 @@ public class DashboardView extends JPanel { ); XYPlot plot = chart.getXYPlot(); + chart.setBackgroundPaint(new Color(22, 28, 37)); + chart.getTitle().setPaint(new Color(222, 231, 245)); + chart.getTitle().setFont(new Font("Segoe UI Semibold", Font.PLAIN, 16)); long tenMinutes = 10 * 60 * 1000L; DateAxis domainAxis = (DateAxis) plot.getDomainAxis(); - domainAxis.setFixedAutoRange(tenMinutes); domainAxis.setAutoRange(true); + domainAxis.setDateFormatOverride(new SimpleDateFormat("HH:mm:ss")); + domainAxis.setLabelPaint(new Color(170, 185, 208)); + domainAxis.setTickLabelPaint(new Color(170, 185, 208)); + domainAxis.setTickMarkPaint(new Color(89, 105, 128)); XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) plot.getRenderer(); renderer.setDefaultShapesVisible(false); + renderer.setSeriesPaint(0, new Color(67, 175, 255)); renderer.setSeriesStroke(0, new BasicStroke( - 2.5f, + 3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND )); @@ -110,14 +116,25 @@ public class DashboardView extends JPanel { chart.getXYPlot().getRangeAxis().setRange(0.0, 1.02); NumberAxis rangeAxis = (NumberAxis) chart.getXYPlot().getRangeAxis(); rangeAxis.setTickUnit(new NumberTickUnit(0.1)); + rangeAxis.setLabelPaint(new Color(170, 185, 208)); + rangeAxis.setTickLabelPaint(new Color(170, 185, 208)); + rangeAxis.setTickMarkPaint(new Color(89, 105, 128)); + + plot.setBackgroundPaint(new Color(16, 20, 27)); + plot.setOutlineVisible(false); + plot.setRangeGridlinePaint(new Color(89, 105, 128, 120)); + plot.setDomainGridlinePaint(new Color(89, 105, 128, 120)); chart.setAntiAlias(true); chart.setTextAntiAlias(true); - chart.getPlot().setBackgroundPaint(new Color(245,245,245)); - chart.getPlot().setOutlineVisible(false); ChartPanel chartPanel = new ChartPanel(chart); - chartPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); + chartPanel.setOpaque(false); + chartPanel.setBackground(new Color(16, 20, 27)); + chartPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(58, 72, 90)), + BorderFactory.createEmptyBorder(8, 8, 8, 8) + )); card.add(chartPanel, BorderLayout.CENTER); @@ -188,7 +205,7 @@ public class DashboardView extends JPanel { ValueMarker marker = new ValueMarker(value); - marker.setPaint(new Color(150,150,150,150)); + marker.setPaint(getThresholdColor(label)); float[] dash = {6.0f, 6.0f}; marker.setStroke(new BasicStroke( @@ -201,12 +218,31 @@ public class DashboardView extends JPanel { )); marker.setLabel(label); - marker.setLabelFont(new Font("Segoe UI", Font.PLAIN, 11)); - marker.setLabelPaint(new Color(160,160,160)); + marker.setLabelFont(new Font("Segoe UI Semibold", Font.PLAIN, 12)); + marker.setLabelPaint(new Color(188, 203, 223)); marker.setLabelAnchor(RectangleAnchor.RIGHT); marker.setLabelTextAnchor(TextAnchor.TOP_RIGHT); plot.addRangeMarker(marker, Layer.BACKGROUND); } + + /** + * Returns a distinct marker color for each threshold level. + * + * @param label threshold label + * @return translucent marker color + */ + private Color getThresholdColor(String label) { + switch (label) { + case "Warning": + return new Color(255, 210, 69, 165); + case "High": + return new Color(255, 153, 65, 170); + case "Disaster": + return new Color(255, 92, 92, 175); + default: + return new Color(168, 178, 194, 140); + } + } } diff --git a/src/main/java/vassistent/ui/PixelStreamingView.java b/src/main/java/vassistent/ui/PixelStreamingView.java index 5b884fb..a8d9065 100644 --- a/src/main/java/vassistent/ui/PixelStreamingView.java +++ b/src/main/java/vassistent/ui/PixelStreamingView.java @@ -8,8 +8,6 @@ import org.cef.handler.CefAppHandlerAdapter; import javax.swing.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; /** * Embedded JCEF browser panel used to display the avatar pixel stream. @@ -31,7 +29,9 @@ public class PixelStreamingView extends JPanel { */ public PixelStreamingView(String startURL, boolean useOSR, boolean isTransparent) { super(new BorderLayout()); - + setOpaque(true); + setBackground(new Color(8, 11, 16)); + CefApp.addAppHandler(new CefAppHandlerAdapter(null) { /** * Handles JCEF application state transitions. @@ -55,7 +55,16 @@ public class PixelStreamingView extends JPanel { browser = client.createBrowser(startURL, useOSR, isTransparent); browserUI_ = browser.getUIComponent(); - add(browserUI_, BorderLayout.CENTER); + JPanel browserContainer = new JPanel(new BorderLayout()); + browserContainer.setOpaque(true); + browserContainer.setBackground(new Color(6, 9, 14)); + browserContainer.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(56, 68, 86)), + BorderFactory.createEmptyBorder(6, 6, 6, 6) + )); + browserContainer.add(browserUI_, BorderLayout.CENTER); + + add(browserContainer, BorderLayout.CENTER); } /** diff --git a/src/main/java/vassistent/ui/ProblemLevelBar.java b/src/main/java/vassistent/ui/ProblemLevelBar.java index ac23172..f23c753 100644 --- a/src/main/java/vassistent/ui/ProblemLevelBar.java +++ b/src/main/java/vassistent/ui/ProblemLevelBar.java @@ -17,9 +17,10 @@ public class ProblemLevelBar extends JPanel { * Creates the level bar component with preferred sizing and transparent background. */ public ProblemLevelBar() { - setPreferredSize(new Dimension(100, 50)); + setPreferredSize(new Dimension(420, 88)); + setMinimumSize(new Dimension(320, 72)); setOpaque(false); - setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); + setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); } /** @@ -66,47 +67,45 @@ public class ProblemLevelBar extends JPanel { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - // --- Hintergrund --- - GradientPaint bg = new GradientPaint( - 0, 0, new Color(235,235,235), - 0, height, new Color(210,210,210) + int arc = Math.max(22, height / 2); + + GradientPaint trackGradient = new GradientPaint( + 0, 0, new Color(51, 60, 76), + 0, height, new Color(29, 36, 48) ); + g2.setPaint(trackGradient); + g2.fillRoundRect(0, 0, width, height, arc, arc); - g2.setPaint(bg); - g2.fillRoundRect(0, 0, width, height, 20, 20); - - // --- Gefüllter Bereich --- int filledWidth = (int) (width * ratio); + if (filledWidth > 0) { + filledWidth = Math.max(filledWidth, arc / 2); + } Color baseColor = getColorForLevel(level); - Color darkerColor = baseColor.darker(); - - GradientPaint gradient = new GradientPaint( - 0, 0, darkerColor, - filledWidth, 0, baseColor + GradientPaint fillGradient = new GradientPaint( + 0, 0, baseColor.darker(), + Math.max(1, filledWidth), 0, baseColor ); - - g2.setPaint(gradient); - g2.fillRoundRect(0, 0, filledWidth, height, 20, 20); + g2.setPaint(fillGradient); + g2.fillRoundRect(0, 0, filledWidth, height, arc, arc); g2.setStroke(new BasicStroke(2f)); - g2.setColor(baseColor.brighter()); - g2.drawRoundRect(1,1,width-2,height-2,20,20); + g2.setColor(new Color(175, 190, 212)); + g2.drawRoundRect(1, 1, width - 2, height - 2, arc, arc); - // --- Text --- - String text = level.name() + " (" + (int)(ratio * 100) + "%)"; - - g2.setColor(Color.BLACK); + String text = "Problem " + level.name() + " " + (int) (ratio * 100) + "%"; + g2.setFont(new Font("Segoe UI Semibold", + Font.PLAIN, + Math.max(19, (int) (height * 0.33)))); + g2.setColor(new Color(244, 248, 255)); FontMetrics fm = g2.getFontMetrics(); - int textWidth = fm.stringWidth(text); - - g2.drawString( - text, - (width - textWidth) / 2, - (height + fm.getAscent()) / 2 - 3 - ); + int textX = (width - fm.stringWidth(text)) / 2; + int textY = (height - fm.getHeight()) / 2 + fm.getAscent(); + g2.drawString(text, textX, textY); } /** @@ -118,13 +117,13 @@ public class ProblemLevelBar extends JPanel { private Color getColorForLevel(ProblemLevel level) { switch (level) { case DISASTER: - return new Color(200, 0, 0); + return new Color(214, 70, 70); case HIGH: - return new Color(255, 140, 0); + return new Color(234, 134, 45); case WARNING: - return new Color(255, 215, 0); + return new Color(219, 180, 52); default: - return new Color(0, 170, 0); + return new Color(53, 178, 107); } } } diff --git a/src/main/java/vassistent/util/ConfigLoader.java b/src/main/java/vassistent/util/ConfigLoader.java index 4c8369c..e0e61db 100644 --- a/src/main/java/vassistent/util/ConfigLoader.java +++ b/src/main/java/vassistent/util/ConfigLoader.java @@ -32,7 +32,7 @@ public class ConfigLoader { } catch (Exception e) { Logger.error("CONFIG", - "Properties Laden fehlgeschlagen", e); + "Failed to load properties", e); } return props; diff --git a/src/main/java/vassistent/util/Logger.java b/src/main/java/vassistent/util/Logger.java index 41e7af2..d9737ba 100644 --- a/src/main/java/vassistent/util/Logger.java +++ b/src/main/java/vassistent/util/Logger.java @@ -69,7 +69,7 @@ public class Logger { props.getProperty("logger.max.size.mb", "10")); } catch (Exception e) { - System.err.println("Logger Config Fehler: " + e.getMessage()); + System.err.println("Logger config error: " + e.getMessage()); } } @@ -181,7 +181,7 @@ public class Logger { System.out.println(logEntry); } catch (Exception e) { - System.err.println("Logger Writer Fehler: " + e.getMessage()); + System.err.println("Logger writer error: " + e.getMessage()); } } @@ -221,7 +221,7 @@ public class Logger { } } catch (Exception e) { - System.err.println("Logger Rotation Fehler: " + e.getMessage()); + System.err.println("Logger rotation error: " + e.getMessage()); } } @@ -230,10 +230,10 @@ public class Logger { */ public static void shutdown() { - System.out.println("Logger Shutdown gestartet"); + System.out.println("Logger shutdown started"); try { - Thread.sleep(500); // Warte auf Queue Flush + Thread.sleep(500); // Wait for queue flush while (!logQueue.isEmpty()) { String log = logQueue.poll(); @@ -243,7 +243,7 @@ public class Logger { } } catch (Exception e) { - System.err.println("Logger Shutdown Fehler: " + e.getMessage()); + System.err.println("Logger shutdown error: " + e.getMessage()); } } }