Compare commits

..

No commits in common. "11b4c98ef3a5699ce29d85035cc22a7ced83ff15" and "696c6df55fdcb732020d23eb203e3597162f1056" have entirely different histories.

22 changed files with 505 additions and 1019 deletions

164
README.md
View File

@ -1,164 +0,0 @@
# 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.

View File

@ -1,95 +1,62 @@
# Architecture # Architecture
## System Overview ## High-Level Overview
The application is a state-driven desktop client with an event-processing pipeline: The application is a Java Swing desktop client that:
1. ingest prediction events from MQTT, 1. subscribes to MQTT prediction events,
2. persist accepted values in SQLite, 2. stores accepted events in SQLite,
3. evaluate risk based on recent history, 3. computes risk ratios,
4. update UI state and rendering, 4. updates UI state and visualization,
5. export animation state for external consumers. 5. exports animation state for Unreal integration.
## Module Structure ## Layers
- `vassistent.bootstrap` - `bootstrap`
Application startup/shutdown orchestration (`ApplicationInitializer`, `ApplicationShutdownManager`) - builds and wires all services in `ApplicationInitializer`
- `vassistent.service` - handles orderly shutdown in `ApplicationShutdownManager`
Runtime services and integrations (MQTT, persistence, statistics, evaluation, process management) - `service`
- `vassistent.model` - integration and business logic (MQTT, DB, stats, evaluation, external processes)
Core state/data objects (`AppState`, `ProblemLevel`, `DatabaseEntry`, `RatioPoint`) - `model`
- `vassistent.controller` - simple state and value objects (`AppState`, `ProblemLevel`, `DatabaseEntry`, `RatioPoint`)
Connects app state/services with Swing views - `controller`
- `vassistent.ui` - UI orchestration between state/services and Swing views
Window and visual components - `ui`
- `vassistent.util` - window and visual components (tabs, chart, browser, status widgets)
Configuration loading/validation and logging - `util`
- shared helpers for config and logging
## Runtime Flow ## Runtime Data Flow
### Startup sequence 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.
1. `App.main` initializes look-and-feel. ## Main Components
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`.
### Event processing sequence - `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
1. MQTT message arrives (`MqttClientService.messageArrived`). ## UI Structure
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.
## Core Domain Rules - Main window: `AppWindow`
- Tab 1: `PixelStreamingView` (JCEF browser)
`EvaluationService` maps ratios to levels: - Tab 2: `DashboardView` (JFreeChart + `ProblemLevelBar`)
- Footer status bar: MQTT status + current level
- `< 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.

View File

@ -1,125 +1,82 @@
# Configuration # Configuration
Configuration is loaded from classpath properties files in `src/main/resources/config`: Application settings are 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` ## `application.properties`
### Core runtime keys | Key | Purpose | Current Default |
| Key | Required | Description |
|---|---|---| |---|---|---|
| `mqtt.broker.url` | yes | MQTT broker URI (`tcp://...`, `ssl://...`, `ws://...`, `wss://...`) | | `app.mode` | Generic mode flag | `test` |
| `mqtt.client.id` | yes | MQTT client ID | | `streaming.url` | Initial URL for pixel streaming view | `http://localhost` |
| `mqtt.topic` | yes | MQTT topic to subscribe to | | `python.path` | Python interpreter for simulator process | `C:\\Program Files\\PyManager\\python.exe` |
| `streaming.url` | yes | URL for embedded streaming view (`http`/`https`) | | `mqtt.broker.url` | MQTT broker connection URL | `tcp://localhost:1883` |
| `animation.output.path` | yes | Target path for animation JSON output | | `mqtt.client.id` | MQTT client identifier | `JavaClientPublisherSubscriber` |
| `app.mode` | no | Generic runtime mode flag | | `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` |
### Optional simulator keys (`mqtt_sim.*`) Notes:
These become required/validated when `mqtt_sim.enabled=true`. - 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.
| Key | Description | - `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`.
| `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` ## `logger.properties`
| Key | Description | Typical value | | Key | Purpose | Current Default |
|---|---|---| |---|---|---|
| `logger.level` | Minimum log level | `DEBUG` | | `logger.level` | Minimum log level | `DEBUG` |
| `logger.file.enabled` | Toggle file logging | `true` | | `logger.file.enabled` | Enables file logging | `true` |
| `logger.file` | Log file path | `logs/application.log` | | `logger.file` | Log output file | `logs/application.log` |
| `logger.max.size.mb` | Rotation threshold in MB | `10` | | `logger.max.size.mb` | Rotation threshold | `10` |
## Validation Behavior ## Related Script Files
The validator checks: - `src/main/resources/scripts/mqtt_simulator.py`
- `src/main/resources/scripts/start_avatar.ps1`
- URI format and scheme for MQTT broker URL ## Script Runtime Inputs
- 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
Any validation errors are grouped and thrown as a single startup exception. `mqtt_simulator.py` supports CLI options and equivalent env vars:
## Script Environment Mapping - `--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`
### `mqtt_simulator.py` `start_avatar.ps1` supports parameters and equivalent env vars:
App config keys are forwarded as environment variables: - `-PidDir` / `VGA_PID_DIR`
- `-UeExe` / `VGA_UE_EXE`
- `mqtt_sim.broker` -> `MQTT_SIM_BROKER` - `-UeArgs` / `VGA_UE_ARGS`
- `mqtt_sim.port` -> `MQTT_SIM_PORT` - `-SignallingBat` / `VGA_SIGNALLING_BAT`
- `mqtt_sim.topic` -> `MQTT_SIM_TOPIC` - `-UeWorkingDir` / `VGA_UE_WORKDIR`
- `mqtt_sim.username` -> `MQTT_SIM_USERNAME` - `-SignallingStartupDelaySeconds` / `VGA_SIGNALLING_DELAY`
- `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.

View File

@ -1,30 +1,20 @@
# Documentation Index # Documentation Index
This folder contains detailed technical documentation for the project. This folder contains project-level documentation for the virtual health assistant.
## Suggested Reading Order ## Contents
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)
## Document Guide
- [Project Review](PROJECT_REVIEW.md)
End-to-end implementation summary, design decisions, quality checks, and roadmap.
- [Architecture](ARCHITECTURE.md) - [Architecture](ARCHITECTURE.md)
Component responsibilities, runtime data flow, and integration boundaries.
- [Setup and Run](SETUP_AND_RUN.md) - [Setup and Run](SETUP_AND_RUN.md)
Reproducible setup for minimal and full integration modes.
- [Configuration](CONFIGURATION.md) - [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) - [Source Map](SOURCE_MAP.md)
Package-level map for fast code navigation. - [Testing](TESTING.md)
- [Known Issues](KNOWN_ISSUES.md)
## Recommended Reading Order
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)

View File

@ -2,32 +2,35 @@
## 1. Environment-Specific Defaults ## 1. Environment-Specific Defaults
`application.properties` contains machine-specific Unreal paths and currently enables optional integrations by default. 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
Impact: Impact:
- first run on a different machine can fail at startup validation unless values are adapted. - configuration updates are required before first run on a different machine
## 2. Database Is Deleted on Shutdown ## 2. Database Deleted on Shutdown
`ApplicationShutdownManager` deletes `data/health.db` during shutdown. `App.deleteDatabase()` deletes `data/health.db` on app exit.
Impact: Impact:
- historical event data is not retained across sessions. - historical data is lost every run unless behavior is changed
## 3. Duplicate Filtering Is Limited to Last Seen `_id` ## 3. Signalling Process Startup Incomplete
`BinaryEventService` only rejects a duplicate when it equals the immediate previous `_id`. In `ProcessManagerService.startSignallingServer()`, process creation is currently prepared but not started (`pb.start()` is commented).
Impact: Impact:
- non-consecutive duplicate IDs can still be persisted. - log output may indicate startup intent, but process is not actually launched via that path
## 4. Startup Coupling to Optional Integrations ## 4. Encoding Artifacts in Logs/Strings
When `mqtt_sim.enabled=true` or `unreal.enabled=true`, startup is blocked if related files/paths are invalid. Some source/log text contains mojibake characters in comments/messages.
Impact: Impact:
- optional integrations can prevent the core app from launching. - readability and consistency issues in logs and source text

View File

@ -1,74 +0,0 @@
# 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.

View File

@ -4,30 +4,13 @@
- Java 17 (JDK) - Java 17 (JDK)
- Maven 3.9+ - Maven 3.9+
- MQTT broker reachable at `localhost:1883` (or custom broker in config) - MQTT broker reachable at `localhost:1883`
- Windows for bundled PowerShell/BAT process scripts - Windows environment for bundled PowerShell/BAT process scripts
Optional: Optional:
- Python (for automatic MQTT simulator startup) - Python (for MQTT simulator startup)
- Unreal + Pixel Streaming setup (for avatar integration) - Unreal + Pixel Streaming environment (if enabled in config)
## 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 ## Build
@ -35,21 +18,21 @@ Enable simulator and/or Unreal startup only after all referenced paths are valid
mvn clean compile mvn clean compile
``` ```
## Run from CLI ## Run (CLI)
```powershell ```powershell
mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=vassistent.App mvn org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass=vassistent.App
``` ```
## Run from IDE ## Run (IDE)
Start class: Run the class:
- `vassistent.App` - `vassistent.App`
## Send Test Messages ## MQTT Payload Format
Publish payloads to configured topic (default: `PREDICTION`) in this format: Publish payloads to configured topic (default `PREDICTION`) in this format:
```json ```json
{ {
@ -59,25 +42,17 @@ Publish payloads to configured topic (default: `PREDICTION`) in this format:
} }
``` ```
Validation rules: Rules:
- `valid` must be `true` - `valid` must be `true`
- `_id` must exist - `_id` must exist
- `prediction` must be `0` or `1` - `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 ## Shutdown Behavior
On shutdown, the app: On shutdown, the app:
1. disconnects MQTT, - disconnects MQTT
2. stops managed external processes, - stops managed external processes
3. deletes `data/health.db` (current behavior), - flushes logger queue
4. flushes and closes the logger. - deletes `data/health.db` (current implementation)

View File

@ -1,86 +1,53 @@
# Source Map # Source Map
Package-level source overview for `src/main/java` and `src/test/java`. Package-level map of all Java source files in `src/main/java` and tests in `src/test/java`.
## Main Sources ## Main Sources
### `vassistent` ### `vassistent`
- `App.java` - `App.java`: application entry point and shutdown hook logic
Application entry point.
### `vassistent.bootstrap` ### `vassistent.bootstrap`
- `ApplicationContext.java` - `ApplicationContext.java`: container for shared services/state
In-memory container for shared services/state. - `ApplicationInitializer.java`: wiring and startup sequence
- `ApplicationInitializer.java` - `ApplicationShutdownManager.java`: managed shutdown sequence
Service wiring, startup orchestration, process startup reporting.
- `ApplicationShutdownManager.java`
Graceful shutdown: MQTT disconnect, process stop, DB cleanup, logger shutdown.
### `vassistent.controller` ### `vassistent.controller`
- `AppWindowController.java` - `AppWindowController.java`: main window lifecycle and state-driven UI updates
Main window lifecycle and cross-view coordination. - `DashboardController.java`: chart and level updates on state changes
- `DashboardController.java` - `StreamingController.java`: stream view actions (reload/focus)
Updates chart/level view from app state.
- `StreamingController.java`
Controls stream view actions.
### `vassistent.model` ### `vassistent.model`
- `AppState.java` - `AppState.java`: observable app state
Observable application state. - `DatabaseEntry.java`: persisted event value + timestamp
- `DatabaseEntry.java` - `ProblemLevel.java`: risk level enum
Persisted event value + timestamp. - `RatioPoint.java`: chart point model
- `ProblemLevel.java`
Risk-level enum.
- `RatioPoint.java`
Time-series point for chart data.
### `vassistent.service` ### `vassistent.service`
- `AnimationFileService.java` - `AnimationFileService.java`: writes animation state JSON file
Writes current animation state JSON. - `BinaryEventService.java`: payload validation + dispatch
- `BinaryEventService.java` - `DataPersistenceService.java`: SQLite init and CRUD-like methods
Validates payloads and triggers persistence/evaluation. - `EvaluationService.java`: ratio-to-problem-level mapping
- `DataPersistenceService.java` - `MqttClientService.java`: MQTT connectivity and callbacks
SQLite schema setup and event read/write methods. - `ProcessManagerService.java`: optional process startup/shutdown
- `EvaluationService.java` - `StatisticsService.java`: ratio and rolling-avg computations
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` ### `vassistent.ui`
- `AppWindow.java` - `AppWindow.java`: root frame with tabbed layout
Root frame with tabbed layout. - `DashboardView.java`: chart + threshold markers + level display
- `DashboardView.java` - `PixelStreamingView.java`: JCEF browser panel for stream
Chart and current-level visualization. - `ProblemLevelBar.java`: custom level visualization widget
- `PixelStreamingView.java`
JCEF browser panel for stream URL.
- `ProblemLevelBar.java`
Custom UI component for level display.
### `vassistent.util` ### `vassistent.util`
- `AppConfigValidator.java` - `ConfigLoader.java`: classpath properties loader
Startup configuration validation. - `Logger.java`: async logger with file rotation
- `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 ## Test Sources

View File

@ -1,57 +1,43 @@
# Testing # Testing
## Run the Test Suite ## Run Tests
```powershell ```powershell
mvn test mvn test
``` ```
## Test Strategy ## Current Test Suite
The project uses a mix of unit and integration tests: Located in:
- **Unit tests** for isolated business logic and service behavior - `src/test/java/vassistent/model/AppStateTest`
- **Integration tests** for filesystem/SQLite behavior and output artifacts - `src/test/java/vassistent/service/AnimationFileServiceTest`
- **Mock-based tests** for MQTT and process-launch behavior - `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`
Frameworks: Coverage focus:
- JUnit Jupiter - state change notifications
- Mockito JUnit Jupiter - 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
## Test Inventory ## Current Status (Java 17)
| Test Class | Type | Main Focus | - full suite passes with `mvn test`
|---|---|---| - test stack:
| `AppStateTest` | Unit | state defaults and listener notifications | - `org.junit.jupiter:junit-jupiter`
| `BinaryEventServiceTest` | Unit | payload validation and duplicate handling | - `org.mockito:mockito-junit-jupiter`
| `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 |
## What Is Covered Well ## Notes
- Core event pipeline behavior - some tests are integration-style and use temporary SQLite databases
- Threshold logic and level transitions - test output may include logger lines because services log during execution
- 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.

168
readme.md Normal file
View File

@ -0,0 +1,168 @@
# 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).

View File

@ -92,14 +92,14 @@ public class AppWindowController {
private void subscribeAppState() { private void subscribeAppState() {
Logger.info("Controller", Logger.info("Controller",
"Starting AppState observer subscription"); "Subscribe AppState Observer gestartet");
AppState state = context.getAppState(); AppState state = context.getAppState();
state.addListener(this::onStateChanged); state.addListener(this::onStateChanged);
Logger.info("Controller", Logger.info("Controller",
"AppState observer registered"); "AppState Observer registriert");
} }
/** /**
@ -111,7 +111,7 @@ public class AppWindowController {
if (window == null) { if (window == null) {
Logger.warn("Controller", Logger.warn("Controller",
"Window is null, UI update skipped"); "Window ist null, UI Update übersprungen");
return; return;
} }
@ -126,7 +126,7 @@ public class AppWindowController {
); );
Logger.debug("Controller", Logger.debug("Controller",
"Problem level UI updated -> " "ProblemLevel UI aktualisiert → "
+ appState.getProblemLevel()); + appState.getProblemLevel());
}); });
} }

View File

@ -58,10 +58,10 @@ public class AnimationFileService {
writer.write(json); writer.write(json);
} }
Logger.info("ANIMATION FILE", "Animation JSON written"); Logger.info("ANIMATION FILE", "Animation json geschrieben");
} catch (IOException e) { } catch (IOException e) {
Logger.error("ANIMATION FILE", "Failed to write animation file", e); Logger.error("ANIMATION FILE", "Fehler beim Schreiben der Animation Datei", e);
} }
} }

View File

@ -39,19 +39,19 @@ public class BinaryEventService {
JsonObject json = JsonParser.parseString(payload).getAsJsonObject(); JsonObject json = JsonParser.parseString(payload).getAsJsonObject();
if (!json.has("valid") || !json.get("valid").getAsBoolean()) { if (!json.has("valid") || !json.get("valid").getAsBoolean()) {
Logger.warn("EVENT", "Payload is not valid"); Logger.warn("EVENT", "Payload ist nicht valid");
return; return;
} }
if (!json.has("_id") || !json.has("prediction")) { if (!json.has("_id") || !json.has("prediction")) {
Logger.warn("EVENT", "Payload is incomplete"); Logger.warn("EVENT", "Payload unvollständig");
return; return;
} }
int id = json.get("_id").getAsInt(); int id = json.get("_id").getAsInt();
if (lastId != null && lastId == id) { if (lastId != null && lastId == id) {
Logger.warn("EVENT", "Payload ID already processed: " + id); Logger.warn("EVENT", "Payload ID bereits verarbeitet: " + id);
return; return;
} }
@ -60,17 +60,17 @@ public class BinaryEventService {
int prediction = json.get("prediction").getAsInt(); int prediction = json.get("prediction").getAsInt();
if (prediction != 0 && prediction != 1) { if (prediction != 0 && prediction != 1) {
Logger.warn("EVENT", "Invalid prediction: " + prediction); Logger.warn("EVENT", "Ungültige Prediction: " + prediction);
return; return;
} }
persistenceService.store(prediction); persistenceService.store(prediction);
evaluationService.evaluate(); evaluationService.evaluate();
Logger.debug("EVENT", "Prediction processed: " + prediction); Logger.debug("EVENT", "Prediction verarbeitet: " + prediction);
} catch (Exception e) { } catch (Exception e) {
Logger.error("EVENT", "Payload processing failed: " + payload, e); Logger.error("EVENT", "Payload Verarbeitung fehlgeschlagen: " + payload, e);
} }
} }
} }

View File

@ -27,7 +27,7 @@ public class DataPersistenceService {
init(); init();
} }
// For tests // Für Tests
/** /**
* Creates a persistence service using a custom JDBC URL (primarily for tests). * Creates a persistence service using a custom JDBC URL (primarily for tests).
* *
@ -47,9 +47,9 @@ public class DataPersistenceService {
if (!folder.exists()) { if (!folder.exists()) {
boolean created = folder.mkdirs(); boolean created = folder.mkdirs();
if (created) { if (created) {
Logger.info("DB", "Database folder created"); Logger.info("DB", "Datenbank-Ordner erstellt");
} else { } else {
Logger.warn("DB", "Could not create database folder"); Logger.warn("DB", "Konnte DB-Ordner nicht erstellen");
} }
} }
} }
@ -69,10 +69,10 @@ public class DataPersistenceService {
) )
"""); """);
Logger.info("DB", "Database initialized"); Logger.info("DB", "Datenbank initialisiert");
} catch (SQLException e) { } catch (SQLException e) {
Logger.error("DB", "Database initialization failed", e); Logger.error("DB", "Datenbank initialisierung fehlgeschlagen", e);
} }
} }
@ -89,9 +89,9 @@ public class DataPersistenceService {
ps.setInt(1, value); ps.setInt(1, value);
ps.setString(2, LocalDateTime.now().toString()); ps.setString(2, LocalDateTime.now().toString());
ps.executeUpdate(); ps.executeUpdate();
//Logger.debug("DB", "Database insert successful: " + ps.toString()); //Logger.debug("DB", "Database Insert erfolgreich: " + ps.toString());
} catch (SQLException e) { } catch (SQLException e) {
Logger.error("DB", "Database insert failed", e); Logger.error("DB", "Database Insert fehlgeschlagen", e);
} }
} }
@ -130,7 +130,7 @@ public class DataPersistenceService {
} catch (SQLException e) { } catch (SQLException e) {
Logger.error("DB", Logger.error("DB",
"Failed to load latest entries", e); "Fehler beim Laden der letzten Einträge", e);
} }
Collections.reverse(result); Collections.reverse(result);

View File

@ -111,7 +111,7 @@ public class MqttClientService implements MqttCallback {
} }
} catch (MqttException e) { } catch (MqttException e) {
Logger.error("MQTT", "Failed to connect", e); Logger.error("MQTT", "Fehler beim Verbinden", e);
appState.setMqttConnected(false); appState.setMqttConnected(false);
} }
} }
@ -126,10 +126,10 @@ public class MqttClientService implements MqttCallback {
options.setCleanSession(true); options.setCleanSession(true);
options.setAutomaticReconnect(true); options.setAutomaticReconnect(true);
Logger.info("MQTT", "Connecting to broker " + brokerUrl); Logger.info("MQTT", "Verbinde mit Broker " + brokerUrl);
client.connect(options); client.connect(options);
appState.setMqttConnected(true); appState.setMqttConnected(true);
Logger.info("MQTT", "Connection established"); Logger.info("MQTT", "Verbindung hergestellt");
} }
/** /**
@ -139,10 +139,10 @@ public class MqttClientService implements MqttCallback {
try { try {
if (client != null && client.isConnected()) { if (client != null && client.isConnected()) {
client.disconnect(); client.disconnect();
Logger.info("MQTT", "Connection closed"); Logger.info("MQTT", "Verbindung getrennt");
} }
} catch (MqttException e) { } catch (MqttException e) {
Logger.error("MQTT", "Failed to disconnect", e); Logger.error("MQTT", "Fehler beim Trennen", e);
} }
} }
@ -156,9 +156,9 @@ public class MqttClientService implements MqttCallback {
topicListeners.put(topic, listener); topicListeners.put(topic, listener);
try { try {
client.subscribe(topic); client.subscribe(topic);
Logger.info("MQTT", "Subscribed to topic: " + topic); Logger.info("MQTT", "Topic abonniert: " + topic);
} catch (MqttException e) { } catch (MqttException e) {
Logger.error("MQTT", "Subscribe failed: " + topic, e); Logger.error("MQTT", "Subscribe fehlgeschlagen: " + topic, e);
} }
} }
@ -182,7 +182,7 @@ public class MqttClientService implements MqttCallback {
"Publish -> Topic=" + topic + ", QoS=" + qos + ", Payload=" + message "Publish -> Topic=" + topic + ", QoS=" + qos + ", Payload=" + message
); );
} catch (MqttException e) { } catch (MqttException e) {
Logger.error("MQTT", "Publish failed", e); Logger.error("MQTT", "Publish fehlgeschlagen", e);
} }
} }
@ -195,7 +195,7 @@ public class MqttClientService implements MqttCallback {
public void connectionLost(Throwable cause) { public void connectionLost(Throwable cause) {
Logger.warn( Logger.warn(
"MQTT", "MQTT",
"Connection lost: " + cause.getMessage() "Verbindung verloren: " + cause.getMessage()
); );
appState.setMqttConnected(false); appState.setMqttConnected(false);
@ -215,7 +215,7 @@ public class MqttClientService implements MqttCallback {
//Logger.debug( //Logger.debug(
// "MQTT", // "MQTT",
// "Message received -> Topic=" + topic + ", Payload=" + payload // "Nachricht empfangen -> Topic=" + topic + ", Payload=" + payload
//); //);
Consumer<String> listener = topicListeners.get(topic); Consumer<String> listener = topicListeners.get(topic);
@ -225,7 +225,7 @@ public class MqttClientService implements MqttCallback {
} else { } else {
Logger.warn( Logger.warn(
"MQTT", "MQTT",
"No listener for topic: " + topic "Keine Listener für Topic: " + topic
); );
} }
} }

View File

@ -336,13 +336,13 @@ public class ProcessManagerService {
pythonProcess = processLauncher.launch(pb); pythonProcess = processLauncher.launch(pb);
verifyPythonStartupHealth(); verifyPythonStartupHealth();
Logger.info("PROCESS", "MQTT simulator started"); Logger.info("PROCESS", "Mqtt Simulator gestartet");
return ProcessStartStatus.started("mqtt_sim"); return ProcessStartStatus.started("mqtt_sim");
} catch (Exception e) { } catch (Exception e) {
if (e instanceof InterruptedException) { if (e instanceof InterruptedException) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
Logger.error("PROCESS", "MQTT simulator startup failed", e); Logger.error("PROCESS", "Mqtt Simulator Start fehlgeschlagen", e);
return ProcessStartStatus.failed( return ProcessStartStatus.failed(
"mqtt_sim", "mqtt_sim",
exceptionMessage(e) exceptionMessage(e)
@ -372,7 +372,7 @@ public class ProcessManagerService {
if (e instanceof InterruptedException) { if (e instanceof InterruptedException) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
Logger.error("PROCESS", "Unreal startup failed", e); Logger.error("PROCESS", "Unreal Start fehlgeschlagen", e);
return ProcessStartStatus.failed( return ProcessStartStatus.failed(
"unreal", "unreal",
exceptionMessage(e) exceptionMessage(e)
@ -404,7 +404,7 @@ public class ProcessManagerService {
//pb.directory(new File(exe).getParentFile()); //pb.directory(new File(exe).getParentFile());
Logger.info("PROCESS", Logger.info("PROCESS",
"Unreal Engine started " + pb.command()); "Unreal Engine gestartet" + pb.command());
} }
/** /**
@ -450,7 +450,7 @@ public class ProcessManagerService {
*/ */
public void shutdown() { public void shutdown() {
Logger.info("PROCESS", "External process shutdown started"); Logger.info("PROCESS", "Shutdown externe Prozesse gestartet");
terminateProcess(pythonProcess); terminateProcess(pythonProcess);
terminateProcess(unrealProcess); terminateProcess(unrealProcess);
@ -458,7 +458,7 @@ public class ProcessManagerService {
killProcessFromPidFile(unrealPidFile); killProcessFromPidFile(unrealPidFile);
killProcessFromPidFile(signallingPidFile); killProcessFromPidFile(signallingPidFile);
Logger.info("PROCESS", "External processes stopped"); Logger.info("PROCESS", "Externe Prozesse beendet");
} }
/** /**
@ -486,11 +486,11 @@ public class ProcessManagerService {
processLauncher.launch(pb).waitFor(); processLauncher.launch(pb).waitFor();
Logger.info("PROCESS", Logger.info("PROCESS",
"Process tree terminated -> PID " + pid); "Process Tree beendet → PID " + pid);
} catch (Exception e) { } catch (Exception e) {
Logger.error("PROCESS", Logger.error("PROCESS",
"Failed to terminate process", e); "Fehler beim Prozess Kill", e);
} }
} }
@ -518,7 +518,7 @@ public class ProcessManagerService {
processLauncher.launch(pb).waitFor(); processLauncher.launch(pb).waitFor();
} catch (Exception e) { } catch (Exception e) {
Logger.error("PROCESS", "PID kill failed", e); Logger.error("PROCESS", "PID Kill fehlgeschlagen", e);
} }
} }

View File

@ -1,10 +1,12 @@
package vassistent.ui; package vassistent.ui;
import vassistent.bootstrap.ApplicationContext;
import vassistent.controller.DashboardController;
import vassistent.util.ConfigLoader; import vassistent.util.ConfigLoader;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*; import java.awt.*;
import java.util.Locale;
import java.util.Properties; import java.util.Properties;
/** /**
@ -12,9 +14,6 @@ import java.util.Properties;
*/ */
public class AppWindow extends JFrame { 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 = private static final Properties config =
ConfigLoader.loadProperties( ConfigLoader.loadProperties(
"config/application.properties" "config/application.properties"
@ -23,9 +22,6 @@ public class AppWindow extends JFrame {
private PixelStreamingView streamingView; private PixelStreamingView streamingView;
private DashboardView dashboardView; private DashboardView dashboardView;
private JTabbedPane tabs; private JTabbedPane tabs;
private JSplitPane driveSplitPane;
private JPanel dashboardCard;
private JToggleButton dashboardToggleButton;
private JLabel mqttStatusLabel; private JLabel mqttStatusLabel;
private JLabel problemLevelLabel; private JLabel problemLevelLabel;
@ -37,12 +33,10 @@ public class AppWindow extends JFrame {
setTitle("Virtueller Gesundheitsassistent"); setTitle("Virtueller Gesundheitsassistent");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setMinimumSize(new Dimension(1280, 720)); setSize(1400, 850);
setSize(1700, 900);
setLocationRelativeTo(null); setLocationRelativeTo(null);
getContentPane().setBackground(new Color(15, 19, 26));
setLayout(new BorderLayout(12, 12)); setLayout(new BorderLayout(10,10));
String streamUrl = String streamUrl =
normalizeConfigValue( normalizeConfigValue(
@ -57,13 +51,11 @@ public class AppWindow extends JFrame {
dashboardView = new DashboardView(); dashboardView = new DashboardView();
add(createHeaderPanel(), BorderLayout.NORTH);
tabs = createTabPane(); tabs = createTabPane();
add(tabs, BorderLayout.CENTER); add(tabs, BorderLayout.CENTER);
add(createStatusBar(), BorderLayout.SOUTH); add(createStatusBar(), BorderLayout.SOUTH);
SwingUtilities.invokeLater(() -> setDashboardVisible(false));
Font uiFont = new Font("Segoe UI", Font.PLAIN, 16); Font uiFont = new Font("Segoe UI", Font.PLAIN, 14);
UIManager.put("Label.font", uiFont); UIManager.put("Label.font", uiFont);
UIManager.put("TabbedPane.font", uiFont); UIManager.put("TabbedPane.font", uiFont);
} }
@ -76,96 +68,17 @@ public class AppWindow extends JFrame {
private JTabbedPane createTabPane() { private JTabbedPane createTabPane() {
JTabbedPane tabs = new JTabbedPane(); JTabbedPane tabs = new JTabbedPane();
tabs.setBorder(BorderFactory.createEmptyBorder(0, 10, 6, 10)); tabs.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
tabs.setFocusable(false);
tabs.putClientProperty("JTabbedPane.tabHeight", 38); JPanel streamingPanel = new JPanel(new BorderLayout(10,10));
tabs.putClientProperty("JTabbedPane.hideTabAreaWithOneTab", true); streamingPanel.add(streamingView, BorderLayout.CENTER);
tabs.addTab("Drive View", createDriveViewPanel());
tabs.addTab("Avatar Streaming", streamingView);
tabs.addTab("Dashboard", dashboardView);
return tabs; 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. * Creates the status bar with MQTT and problem-level indicators.
* *
@ -173,18 +86,18 @@ public class AppWindow extends JFrame {
*/ */
private JPanel createStatusBar() { private JPanel createStatusBar() {
JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 12, 8)); JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT,15,5));
statusBar.setBackground(new Color(13, 16, 23));
statusBar.setBorder(BorderFactory.createCompoundBorder( statusBar.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(54, 64, 78)), BorderFactory.createMatteBorder(1,0,0,0, Color.LIGHT_GRAY),
BorderFactory.createEmptyBorder(6, 12, 8, 12) BorderFactory.createEmptyBorder(5,10,5,10)
)); ));
mqttStatusLabel = createStatusChip("MQTT: Disconnected"); mqttStatusLabel = new JLabel("MQTT: Disconnected");
problemLevelLabel = createStatusChip("Problem: NONE"); problemLevelLabel = new JLabel("Problem: NONE");
statusBar.add(mqttStatusLabel); statusBar.add(mqttStatusLabel);
statusBar.add(new JLabel(" | "));
statusBar.add(problemLevelLabel); statusBar.add(problemLevelLabel);
return statusBar; return statusBar;
@ -196,11 +109,8 @@ public class AppWindow extends JFrame {
* @param connected whether MQTT is currently connected * @param connected whether MQTT is currently connected
*/ */
public void updateMqttStatus(boolean connected) { public void updateMqttStatus(boolean connected) {
mqttStatusLabel.setText("MQTT: " mqttStatusLabel.setText("MQTT: " +
+ (connected ? "Connected" : "Disconnected")); (connected ? "Connected" : "Disconnected"));
mqttStatusLabel.setBackground(
connected ? new Color(30, 101, 70) : new Color(108, 47, 47)
);
} }
/** /**
@ -210,7 +120,6 @@ public class AppWindow extends JFrame {
*/ */
public void updateProblemLevel(String level) { public void updateProblemLevel(String level) {
problemLevelLabel.setText("Problem: " + level); problemLevelLabel.setText("Problem: " + level);
problemLevelLabel.setBackground(getProblemChipColor(level));
} }
/** /**
@ -240,160 +149,6 @@ public class AppWindow extends JFrame {
return tabs; 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. * Trims a config value and strips optional wrapping quotes.
* *

View File

@ -12,15 +12,18 @@ import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.ui.Layer; import org.jfree.chart.ui.Layer;
import org.jfree.chart.ui.RectangleAnchor; import org.jfree.chart.ui.RectangleAnchor;
import org.jfree.chart.ui.TextAnchor; import org.jfree.chart.ui.TextAnchor;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.time.Millisecond; import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection; import org.jfree.data.time.TimeSeriesCollection;
import vassistent.model.ProblemLevel;
import vassistent.model.RatioPoint; import vassistent.model.RatioPoint;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.text.SimpleDateFormat; import java.sql.Timestamp;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -39,30 +42,28 @@ public class DashboardView extends JPanel {
* Creates the dashboard panel with problem-level bar and time-series chart. * Creates the dashboard panel with problem-level bar and time-series chart.
*/ */
public DashboardView() { public DashboardView() {
setLayout(new BorderLayout(0, 12)); setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
setOpaque(false); setOpaque(false);
JPanel card = new JPanel(new BorderLayout(0, 14)); JPanel card = new JPanel(new BorderLayout(10,10));
card.setBackground(new Color(22, 28, 37)); card.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
card.setBackground(UIManager.getColor("Panel.background"));
card.setOpaque(true); card.setOpaque(true);
card.setBorder(BorderFactory.createCompoundBorder( card.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder( BorderFactory.createLineBorder(
new Color(58, 72, 90) UIManager.getColor("Component.borderColor")
), ),
BorderFactory.createEmptyBorder(14, 14, 14, 14) BorderFactory.createEmptyBorder(12,12,12,12)
)); ));
// ---------- TOP: Problem Level ---------- // ---------- TOP: Problem Level ----------
JPanel topWrapper = new JPanel(new BorderLayout(0, 8)); JPanel topWrapper = new JPanel(new BorderLayout());
topWrapper.setBorder(BorderFactory.createEmptyBorder(0, 0, 8, 0)); topWrapper.setBorder(BorderFactory.createEmptyBorder(0,0,10,0));
topWrapper.setOpaque(false); 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(); levelBar = new ProblemLevelBar();
topWrapper.add(levelBar, BorderLayout.CENTER); topWrapper.add(levelBar, BorderLayout.CENTER);
@ -82,28 +83,21 @@ public class DashboardView extends JPanel {
); );
XYPlot plot = chart.getXYPlot(); 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; long tenMinutes = 10 * 60 * 1000L;
DateAxis domainAxis = (DateAxis) plot.getDomainAxis(); DateAxis domainAxis = (DateAxis) plot.getDomainAxis();
domainAxis.setFixedAutoRange(tenMinutes); domainAxis.setFixedAutoRange(tenMinutes);
domainAxis.setAutoRange(true); 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 renderer =
(XYLineAndShapeRenderer) plot.getRenderer(); (XYLineAndShapeRenderer) plot.getRenderer();
renderer.setDefaultShapesVisible(false); renderer.setDefaultShapesVisible(false);
renderer.setSeriesPaint(0, new Color(67, 175, 255));
renderer.setSeriesStroke(0, renderer.setSeriesStroke(0,
new BasicStroke( new BasicStroke(
3.0f, 2.5f,
BasicStroke.CAP_ROUND, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND BasicStroke.JOIN_ROUND
)); ));
@ -116,25 +110,14 @@ public class DashboardView extends JPanel {
chart.getXYPlot().getRangeAxis().setRange(0.0, 1.02); chart.getXYPlot().getRangeAxis().setRange(0.0, 1.02);
NumberAxis rangeAxis = (NumberAxis) chart.getXYPlot().getRangeAxis(); NumberAxis rangeAxis = (NumberAxis) chart.getXYPlot().getRangeAxis();
rangeAxis.setTickUnit(new NumberTickUnit(0.1)); 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.setAntiAlias(true);
chart.setTextAntiAlias(true); chart.setTextAntiAlias(true);
chart.getPlot().setBackgroundPaint(new Color(245,245,245));
chart.getPlot().setOutlineVisible(false);
ChartPanel chartPanel = new ChartPanel(chart); ChartPanel chartPanel = new ChartPanel(chart);
chartPanel.setOpaque(false); chartPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
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); card.add(chartPanel, BorderLayout.CENTER);
@ -205,7 +188,7 @@ public class DashboardView extends JPanel {
ValueMarker marker = new ValueMarker(value); ValueMarker marker = new ValueMarker(value);
marker.setPaint(getThresholdColor(label)); marker.setPaint(new Color(150,150,150,150));
float[] dash = {6.0f, 6.0f}; float[] dash = {6.0f, 6.0f};
marker.setStroke(new BasicStroke( marker.setStroke(new BasicStroke(
@ -218,31 +201,12 @@ public class DashboardView extends JPanel {
)); ));
marker.setLabel(label); marker.setLabel(label);
marker.setLabelFont(new Font("Segoe UI Semibold", Font.PLAIN, 12)); marker.setLabelFont(new Font("Segoe UI", Font.PLAIN, 11));
marker.setLabelPaint(new Color(188, 203, 223)); marker.setLabelPaint(new Color(160,160,160));
marker.setLabelAnchor(RectangleAnchor.RIGHT); marker.setLabelAnchor(RectangleAnchor.RIGHT);
marker.setLabelTextAnchor(TextAnchor.TOP_RIGHT); marker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
plot.addRangeMarker(marker, Layer.BACKGROUND); 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);
}
}
} }

View File

@ -8,6 +8,8 @@ import org.cef.handler.CefAppHandlerAdapter;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/** /**
* Embedded JCEF browser panel used to display the avatar pixel stream. * Embedded JCEF browser panel used to display the avatar pixel stream.
@ -29,8 +31,6 @@ public class PixelStreamingView extends JPanel {
*/ */
public PixelStreamingView(String startURL, boolean useOSR, boolean isTransparent) { public PixelStreamingView(String startURL, boolean useOSR, boolean isTransparent) {
super(new BorderLayout()); super(new BorderLayout());
setOpaque(true);
setBackground(new Color(8, 11, 16));
CefApp.addAppHandler(new CefAppHandlerAdapter(null) { CefApp.addAppHandler(new CefAppHandlerAdapter(null) {
/** /**
@ -55,16 +55,7 @@ public class PixelStreamingView extends JPanel {
browser = client.createBrowser(startURL, useOSR, isTransparent); browser = client.createBrowser(startURL, useOSR, isTransparent);
browserUI_ = browser.getUIComponent(); browserUI_ = browser.getUIComponent();
JPanel browserContainer = new JPanel(new BorderLayout()); add(browserUI_, BorderLayout.CENTER);
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);
} }
/** /**

View File

@ -17,10 +17,9 @@ public class ProblemLevelBar extends JPanel {
* Creates the level bar component with preferred sizing and transparent background. * Creates the level bar component with preferred sizing and transparent background.
*/ */
public ProblemLevelBar() { public ProblemLevelBar() {
setPreferredSize(new Dimension(420, 88)); setPreferredSize(new Dimension(100, 50));
setMinimumSize(new Dimension(320, 72));
setOpaque(false); setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
} }
/** /**
@ -67,45 +66,47 @@ public class ProblemLevelBar extends JPanel {
Graphics2D g2 = (Graphics2D) g; Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int arc = Math.max(22, height / 2); // --- Hintergrund ---
GradientPaint bg = new GradientPaint(
GradientPaint trackGradient = new GradientPaint( 0, 0, new Color(235,235,235),
0, 0, new Color(51, 60, 76), 0, height, new Color(210,210,210)
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); int filledWidth = (int) (width * ratio);
if (filledWidth > 0) {
filledWidth = Math.max(filledWidth, arc / 2);
}
Color baseColor = getColorForLevel(level); Color baseColor = getColorForLevel(level);
GradientPaint fillGradient = new GradientPaint( Color darkerColor = baseColor.darker();
0, 0, baseColor.darker(),
Math.max(1, filledWidth), 0, baseColor GradientPaint gradient = new GradientPaint(
0, 0, darkerColor,
filledWidth, 0, baseColor
); );
g2.setPaint(fillGradient);
g2.fillRoundRect(0, 0, filledWidth, height, arc, arc); g2.setPaint(gradient);
g2.fillRoundRect(0, 0, filledWidth, height, 20, 20);
g2.setStroke(new BasicStroke(2f)); g2.setStroke(new BasicStroke(2f));
g2.setColor(new Color(175, 190, 212)); g2.setColor(baseColor.brighter());
g2.drawRoundRect(1, 1, width - 2, height - 2, arc, arc); g2.drawRoundRect(1,1,width-2,height-2,20,20);
String text = "Problem " + level.name() + " " + (int) (ratio * 100) + "%"; // --- Text ---
g2.setFont(new Font("Segoe UI Semibold", String text = level.name() + " (" + (int)(ratio * 100) + "%)";
Font.PLAIN,
Math.max(19, (int) (height * 0.33)))); g2.setColor(Color.BLACK);
g2.setColor(new Color(244, 248, 255));
FontMetrics fm = g2.getFontMetrics(); FontMetrics fm = g2.getFontMetrics();
int textX = (width - fm.stringWidth(text)) / 2; int textWidth = fm.stringWidth(text);
int textY = (height - fm.getHeight()) / 2 + fm.getAscent();
g2.drawString(text, textX, textY); g2.drawString(
text,
(width - textWidth) / 2,
(height + fm.getAscent()) / 2 - 3
);
} }
/** /**
@ -117,13 +118,13 @@ public class ProblemLevelBar extends JPanel {
private Color getColorForLevel(ProblemLevel level) { private Color getColorForLevel(ProblemLevel level) {
switch (level) { switch (level) {
case DISASTER: case DISASTER:
return new Color(214, 70, 70); return new Color(200, 0, 0);
case HIGH: case HIGH:
return new Color(234, 134, 45); return new Color(255, 140, 0);
case WARNING: case WARNING:
return new Color(219, 180, 52); return new Color(255, 215, 0);
default: default:
return new Color(53, 178, 107); return new Color(0, 170, 0);
} }
} }
} }

View File

@ -32,7 +32,7 @@ public class ConfigLoader {
} catch (Exception e) { } catch (Exception e) {
Logger.error("CONFIG", Logger.error("CONFIG",
"Failed to load properties", e); "Properties Laden fehlgeschlagen", e);
} }
return props; return props;

View File

@ -69,7 +69,7 @@ public class Logger {
props.getProperty("logger.max.size.mb", "10")); props.getProperty("logger.max.size.mb", "10"));
} catch (Exception e) { } catch (Exception e) {
System.err.println("Logger config error: " + e.getMessage()); System.err.println("Logger Config Fehler: " + e.getMessage());
} }
} }
@ -181,7 +181,7 @@ public class Logger {
System.out.println(logEntry); System.out.println(logEntry);
} catch (Exception e) { } catch (Exception e) {
System.err.println("Logger writer error: " + e.getMessage()); System.err.println("Logger Writer Fehler: " + e.getMessage());
} }
} }
@ -221,7 +221,7 @@ public class Logger {
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("Logger rotation error: " + e.getMessage()); System.err.println("Logger Rotation Fehler: " + e.getMessage());
} }
} }
@ -230,10 +230,10 @@ public class Logger {
*/ */
public static void shutdown() { public static void shutdown() {
System.out.println("Logger shutdown started"); System.out.println("Logger Shutdown gestartet");
try { try {
Thread.sleep(500); // Wait for queue flush Thread.sleep(500); // Warte auf Queue Flush
while (!logQueue.isEmpty()) { while (!logQueue.isEmpty()) {
String log = logQueue.poll(); String log = logQueue.poll();
@ -243,7 +243,7 @@ public class Logger {
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("Logger shutdown error: " + e.getMessage()); System.err.println("Logger Shutdown Fehler: " + e.getMessage());
} }
} }
} }