Merge pull request 'pre_int' (#4) from pre_int into main

Reviewed-on: #4
This commit is contained in:
Niklas Aumueller 2026-03-23 13:45:28 +00:00
commit 11b4c98ef3
22 changed files with 1019 additions and 505 deletions

164
README.md Normal file
View File

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

View File

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

View File

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

View File

@ -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
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)
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)
- [Source Map](SOURCE_MAP.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)
## 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)
Active limitations and impact.
- [Source Map](SOURCE_MAP.md)
Package-level map for fast code navigation.

View File

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

74
docs/PROJECT_REVIEW.md Normal file
View File

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

View File

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

View File

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

View File

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

168
readme.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,6 +29,8 @@ 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) {
/**
@ -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);
}
/**

View File

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

View File

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

View File

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