Merge pull request 'pre_int' (#4) from pre_int into main
Reviewed-on: #4
This commit is contained in:
commit
11b4c98ef3
164
README.md
Normal file
164
README.md
Normal 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.
|
||||||
@ -1,62 +1,95 @@
|
|||||||
# Architecture
|
# 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,
|
1. ingest prediction events from MQTT,
|
||||||
2. stores accepted events in SQLite,
|
2. persist accepted values in SQLite,
|
||||||
3. computes risk ratios,
|
3. evaluate risk based on recent history,
|
||||||
4. updates UI state and visualization,
|
4. update UI state and rendering,
|
||||||
5. exports animation state for Unreal integration.
|
5. export animation state for external consumers.
|
||||||
|
|
||||||
## Layers
|
## Module Structure
|
||||||
|
|
||||||
- `bootstrap`
|
- `vassistent.bootstrap`
|
||||||
- builds and wires all services in `ApplicationInitializer`
|
Application startup/shutdown orchestration (`ApplicationInitializer`, `ApplicationShutdownManager`)
|
||||||
- handles orderly shutdown in `ApplicationShutdownManager`
|
- `vassistent.service`
|
||||||
- `service`
|
Runtime services and integrations (MQTT, persistence, statistics, evaluation, process management)
|
||||||
- integration and business logic (MQTT, DB, stats, evaluation, external processes)
|
- `vassistent.model`
|
||||||
- `model`
|
Core state/data objects (`AppState`, `ProblemLevel`, `DatabaseEntry`, `RatioPoint`)
|
||||||
- simple state and value objects (`AppState`, `ProblemLevel`, `DatabaseEntry`, `RatioPoint`)
|
- `vassistent.controller`
|
||||||
- `controller`
|
Connects app state/services with Swing views
|
||||||
- UI orchestration between state/services and Swing views
|
- `vassistent.ui`
|
||||||
- `ui`
|
Window and visual components
|
||||||
- window and visual components (tabs, chart, browser, status widgets)
|
- `vassistent.util`
|
||||||
- `util`
|
Configuration loading/validation and logging
|
||||||
- shared helpers for config and logging
|
|
||||||
|
|
||||||
## Runtime Data Flow
|
## Runtime Flow
|
||||||
|
|
||||||
1. `App.main` initializes look-and-feel and context.
|
### Startup sequence
|
||||||
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.
|
|
||||||
|
|
||||||
## 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`
|
### Event processing sequence
|
||||||
- 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
|
|
||||||
|
|
||||||
## 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`
|
## Core Domain Rules
|
||||||
- Tab 1: `PixelStreamingView` (JCEF browser)
|
|
||||||
- Tab 2: `DashboardView` (JFreeChart + `ProblemLevelBar`)
|
`EvaluationService` maps ratios to levels:
|
||||||
- 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.
|
||||||
|
|||||||
@ -1,82 +1,125 @@
|
|||||||
# Configuration
|
# 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`
|
## `application.properties`
|
||||||
|
|
||||||
| Key | Purpose | Current Default |
|
### Core runtime keys
|
||||||
|
|
||||||
|
| Key | Required | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `app.mode` | Generic mode flag | `test` |
|
| `mqtt.broker.url` | yes | MQTT broker URI (`tcp://...`, `ssl://...`, `ws://...`, `wss://...`) |
|
||||||
| `streaming.url` | Initial URL for pixel streaming view | `http://localhost` |
|
| `mqtt.client.id` | yes | MQTT client ID |
|
||||||
| `python.path` | Python interpreter for simulator process | `C:\\Program Files\\PyManager\\python.exe` |
|
| `mqtt.topic` | yes | MQTT topic to subscribe to |
|
||||||
| `mqtt.broker.url` | MQTT broker connection URL | `tcp://localhost:1883` |
|
| `streaming.url` | yes | URL for embedded streaming view (`http`/`https`) |
|
||||||
| `mqtt.client.id` | MQTT client identifier | `JavaClientPublisherSubscriber` |
|
| `animation.output.path` | yes | Target path for animation JSON output |
|
||||||
| `mqtt.topic` | MQTT topic to subscribe to | `PREDICTION` |
|
| `app.mode` | no | Generic runtime mode flag |
|
||||||
| `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` |
|
|
||||||
|
|
||||||
Notes:
|
### Optional simulator keys (`mqtt_sim.*`)
|
||||||
|
|
||||||
- Paths can be provided with or without wrapping quotes; startup sanitizes surrounding quotes.
|
These become required/validated when `mqtt_sim.enabled=true`.
|
||||||
- Unreal-related defaults are environment-specific and should be replaced per machine.
|
|
||||||
- `mqtt_sim.*` overrides are forwarded as environment variables to `mqtt_simulator.py`.
|
| Key | Description |
|
||||||
- `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 | Purpose | Current Default |
|
| Key | Description | Typical value |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `logger.level` | Minimum log level | `DEBUG` |
|
| `logger.level` | Minimum log level | `DEBUG` |
|
||||||
| `logger.file.enabled` | Enables file logging | `true` |
|
| `logger.file.enabled` | Toggle file logging | `true` |
|
||||||
| `logger.file` | Log output file | `logs/application.log` |
|
| `logger.file` | Log file path | `logs/application.log` |
|
||||||
| `logger.max.size.mb` | Rotation threshold | `10` |
|
| `logger.max.size.mb` | Rotation threshold in MB | `10` |
|
||||||
|
|
||||||
## Related Script Files
|
## Validation Behavior
|
||||||
|
|
||||||
- `src/main/resources/scripts/mqtt_simulator.py`
|
The validator checks:
|
||||||
- `src/main/resources/scripts/start_avatar.ps1`
|
|
||||||
|
|
||||||
## 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`
|
## Script Environment Mapping
|
||||||
- `--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`
|
|
||||||
|
|
||||||
`start_avatar.ps1` supports parameters and equivalent env vars:
|
### `mqtt_simulator.py`
|
||||||
|
|
||||||
- `-PidDir` / `VGA_PID_DIR`
|
App config keys are forwarded as environment variables:
|
||||||
- `-UeExe` / `VGA_UE_EXE`
|
|
||||||
- `-UeArgs` / `VGA_UE_ARGS`
|
- `mqtt_sim.broker` -> `MQTT_SIM_BROKER`
|
||||||
- `-SignallingBat` / `VGA_SIGNALLING_BAT`
|
- `mqtt_sim.port` -> `MQTT_SIM_PORT`
|
||||||
- `-UeWorkingDir` / `VGA_UE_WORKDIR`
|
- `mqtt_sim.topic` -> `MQTT_SIM_TOPIC`
|
||||||
- `-SignallingStartupDelaySeconds` / `VGA_SIGNALLING_DELAY`
|
- `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.
|
||||||
|
|||||||
@ -1,20 +1,30 @@
|
|||||||
# Documentation Index
|
# Documentation Index
|
||||||
|
|
||||||
This folder contains project-level documentation for the virtual health assistant.
|
This folder contains detailed technical documentation for the project.
|
||||||
|
|
||||||
## Contents
|
## Suggested Reading Order
|
||||||
|
|
||||||
- [Architecture](ARCHITECTURE.md)
|
1. [Project Review](PROJECT_REVIEW.md)
|
||||||
- [Setup and Run](SETUP_AND_RUN.md)
|
2. [Architecture](ARCHITECTURE.md)
|
||||||
- [Configuration](CONFIGURATION.md)
|
3. [Setup and Run](SETUP_AND_RUN.md)
|
||||||
- [Source Map](SOURCE_MAP.md)
|
4. [Configuration](CONFIGURATION.md)
|
||||||
- [Testing](TESTING.md)
|
5. [Testing](TESTING.md)
|
||||||
- [Known Issues](KNOWN_ISSUES.md)
|
6. [Known Issues](KNOWN_ISSUES.md)
|
||||||
|
7. [Source Map](SOURCE_MAP.md)
|
||||||
|
|
||||||
## Recommended Reading Order
|
## Document Guide
|
||||||
|
|
||||||
1. [Architecture](ARCHITECTURE.md)
|
- [Project Review](PROJECT_REVIEW.md)
|
||||||
2. [Setup and Run](SETUP_AND_RUN.md)
|
End-to-end implementation summary, design decisions, quality checks, and roadmap.
|
||||||
3. [Configuration](CONFIGURATION.md)
|
- [Architecture](ARCHITECTURE.md)
|
||||||
4. [Testing](TESTING.md)
|
Component responsibilities, runtime data flow, and integration boundaries.
|
||||||
5. [Known Issues](KNOWN_ISSUES.md)
|
- [Setup and Run](SETUP_AND_RUN.md)
|
||||||
|
Reproducible setup for minimal and full integration modes.
|
||||||
|
- [Configuration](CONFIGURATION.md)
|
||||||
|
Complete key reference, validation behavior, and script environment mapping.
|
||||||
|
- [Testing](TESTING.md)
|
||||||
|
Test strategy, inventory, execution, and remaining gaps.
|
||||||
|
- [Known Issues](KNOWN_ISSUES.md)
|
||||||
|
Active limitations and impact.
|
||||||
|
- [Source Map](SOURCE_MAP.md)
|
||||||
|
Package-level map for fast code navigation.
|
||||||
|
|||||||
@ -2,35 +2,32 @@
|
|||||||
|
|
||||||
## 1. Environment-Specific Defaults
|
## 1. Environment-Specific Defaults
|
||||||
|
|
||||||
Configuration is now path-driven, but several default values in `application.properties` are still machine-specific:
|
`application.properties` contains machine-specific Unreal paths and currently enables optional integrations by default.
|
||||||
|
|
||||||
- Unreal startup script paths
|
|
||||||
- Unreal/signalling PID file paths
|
|
||||||
|
|
||||||
Impact:
|
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:
|
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:
|
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:
|
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
74
docs/PROJECT_REVIEW.md
Normal 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.
|
||||||
@ -4,13 +4,30 @@
|
|||||||
|
|
||||||
- Java 17 (JDK)
|
- Java 17 (JDK)
|
||||||
- Maven 3.9+
|
- Maven 3.9+
|
||||||
- MQTT broker reachable at `localhost:1883`
|
- MQTT broker reachable at `localhost:1883` (or custom broker in config)
|
||||||
- Windows environment for bundled PowerShell/BAT process scripts
|
- Windows for bundled PowerShell/BAT process scripts
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
|
||||||
- Python (for MQTT simulator startup)
|
- Python (for automatic MQTT simulator startup)
|
||||||
- Unreal + Pixel Streaming environment (if enabled in config)
|
- 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
|
## Build
|
||||||
|
|
||||||
@ -18,21 +35,21 @@ Optional:
|
|||||||
mvn clean compile
|
mvn clean compile
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run (CLI)
|
## Run from 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 (IDE)
|
## Run from IDE
|
||||||
|
|
||||||
Run the class:
|
Start class:
|
||||||
|
|
||||||
- `vassistent.App`
|
- `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
|
```json
|
||||||
{
|
{
|
||||||
@ -42,17 +59,25 @@ Publish payloads to configured topic (default `PREDICTION`) in this format:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules:
|
Validation 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:
|
||||||
|
|
||||||
- disconnects MQTT
|
1. disconnects MQTT,
|
||||||
- stops managed external processes
|
2. stops managed external processes,
|
||||||
- flushes logger queue
|
3. deletes `data/health.db` (current behavior),
|
||||||
- deletes `data/health.db` (current implementation)
|
4. flushes and closes the logger.
|
||||||
|
|||||||
@ -1,53 +1,86 @@
|
|||||||
# Source Map
|
# 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
|
## Main Sources
|
||||||
|
|
||||||
### `vassistent`
|
### `vassistent`
|
||||||
|
|
||||||
- `App.java`: application entry point and shutdown hook logic
|
- `App.java`
|
||||||
|
Application entry point.
|
||||||
|
|
||||||
### `vassistent.bootstrap`
|
### `vassistent.bootstrap`
|
||||||
|
|
||||||
- `ApplicationContext.java`: container for shared services/state
|
- `ApplicationContext.java`
|
||||||
- `ApplicationInitializer.java`: wiring and startup sequence
|
In-memory container for shared services/state.
|
||||||
- `ApplicationShutdownManager.java`: managed shutdown sequence
|
- `ApplicationInitializer.java`
|
||||||
|
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`: main window lifecycle and state-driven UI updates
|
- `AppWindowController.java`
|
||||||
- `DashboardController.java`: chart and level updates on state changes
|
Main window lifecycle and cross-view coordination.
|
||||||
- `StreamingController.java`: stream view actions (reload/focus)
|
- `DashboardController.java`
|
||||||
|
Updates chart/level view from app state.
|
||||||
|
- `StreamingController.java`
|
||||||
|
Controls stream view actions.
|
||||||
|
|
||||||
### `vassistent.model`
|
### `vassistent.model`
|
||||||
|
|
||||||
- `AppState.java`: observable app state
|
- `AppState.java`
|
||||||
- `DatabaseEntry.java`: persisted event value + timestamp
|
Observable application state.
|
||||||
- `ProblemLevel.java`: risk level enum
|
- `DatabaseEntry.java`
|
||||||
- `RatioPoint.java`: chart point model
|
Persisted event value + timestamp.
|
||||||
|
- `ProblemLevel.java`
|
||||||
|
Risk-level enum.
|
||||||
|
- `RatioPoint.java`
|
||||||
|
Time-series point for chart data.
|
||||||
|
|
||||||
### `vassistent.service`
|
### `vassistent.service`
|
||||||
|
|
||||||
- `AnimationFileService.java`: writes animation state JSON file
|
- `AnimationFileService.java`
|
||||||
- `BinaryEventService.java`: payload validation + dispatch
|
Writes current animation state JSON.
|
||||||
- `DataPersistenceService.java`: SQLite init and CRUD-like methods
|
- `BinaryEventService.java`
|
||||||
- `EvaluationService.java`: ratio-to-problem-level mapping
|
Validates payloads and triggers persistence/evaluation.
|
||||||
- `MqttClientService.java`: MQTT connectivity and callbacks
|
- `DataPersistenceService.java`
|
||||||
- `ProcessManagerService.java`: optional process startup/shutdown
|
SQLite schema setup and event read/write methods.
|
||||||
- `StatisticsService.java`: ratio and rolling-avg computations
|
- `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`
|
### `vassistent.ui`
|
||||||
|
|
||||||
- `AppWindow.java`: root frame with tabbed layout
|
- `AppWindow.java`
|
||||||
- `DashboardView.java`: chart + threshold markers + level display
|
Root frame with tabbed layout.
|
||||||
- `PixelStreamingView.java`: JCEF browser panel for stream
|
- `DashboardView.java`
|
||||||
- `ProblemLevelBar.java`: custom level visualization widget
|
Chart and current-level visualization.
|
||||||
|
- `PixelStreamingView.java`
|
||||||
|
JCEF browser panel for stream URL.
|
||||||
|
- `ProblemLevelBar.java`
|
||||||
|
Custom UI component for level display.
|
||||||
|
|
||||||
### `vassistent.util`
|
### `vassistent.util`
|
||||||
|
|
||||||
- `ConfigLoader.java`: classpath properties loader
|
- `AppConfigValidator.java`
|
||||||
- `Logger.java`: async logger with file rotation
|
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
|
## Test Sources
|
||||||
|
|
||||||
|
|||||||
@ -1,43 +1,57 @@
|
|||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
## Run Tests
|
## Run the Test Suite
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
mvn test
|
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`
|
- **Unit tests** for isolated business logic and service behavior
|
||||||
- `src/test/java/vassistent/service/AnimationFileServiceTest`
|
- **Integration tests** for filesystem/SQLite behavior and output artifacts
|
||||||
- `src/test/java/vassistent/service/BinaryEventServiceTest`
|
- **Mock-based tests** for MQTT and process-launch behavior
|
||||||
- `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`
|
|
||||||
|
|
||||||
Coverage focus:
|
Frameworks:
|
||||||
|
|
||||||
- state change notifications
|
- JUnit Jupiter
|
||||||
- payload validation and processing flow
|
- Mockito JUnit Jupiter
|
||||||
- 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
|
|
||||||
|
|
||||||
## Current Status (Java 17)
|
## Test Inventory
|
||||||
|
|
||||||
- full suite passes with `mvn test`
|
| Test Class | Type | Main Focus |
|
||||||
- test stack:
|
|---|---|---|
|
||||||
- `org.junit.jupiter:junit-jupiter`
|
| `AppStateTest` | Unit | state defaults and listener notifications |
|
||||||
- `org.mockito:mockito-junit-jupiter`
|
| `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
|
- Core event pipeline behavior
|
||||||
- test output may include logger lines because services log during execution
|
- 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
168
readme.md
@ -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).
|
|
||||||
@ -92,14 +92,14 @@ public class AppWindowController {
|
|||||||
private void subscribeAppState() {
|
private void subscribeAppState() {
|
||||||
|
|
||||||
Logger.info("Controller",
|
Logger.info("Controller",
|
||||||
"Subscribe AppState Observer gestartet");
|
"Starting AppState observer subscription");
|
||||||
|
|
||||||
AppState state = context.getAppState();
|
AppState state = context.getAppState();
|
||||||
|
|
||||||
state.addListener(this::onStateChanged);
|
state.addListener(this::onStateChanged);
|
||||||
|
|
||||||
Logger.info("Controller",
|
Logger.info("Controller",
|
||||||
"AppState Observer registriert");
|
"AppState observer registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +111,7 @@ public class AppWindowController {
|
|||||||
|
|
||||||
if (window == null) {
|
if (window == null) {
|
||||||
Logger.warn("Controller",
|
Logger.warn("Controller",
|
||||||
"Window ist null, UI Update übersprungen");
|
"Window is null, UI update skipped");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ public class AppWindowController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Logger.debug("Controller",
|
Logger.debug("Controller",
|
||||||
"ProblemLevel UI aktualisiert → "
|
"Problem level UI updated -> "
|
||||||
+ appState.getProblemLevel());
|
+ appState.getProblemLevel());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,10 +58,10 @@ public class AnimationFileService {
|
|||||||
writer.write(json);
|
writer.write(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info("ANIMATION FILE", "Animation json geschrieben");
|
Logger.info("ANIMATION FILE", "Animation JSON written");
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logger.error("ANIMATION FILE", "Fehler beim Schreiben der Animation Datei", e);
|
Logger.error("ANIMATION FILE", "Failed to write animation file", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 ist nicht valid");
|
Logger.warn("EVENT", "Payload is not valid");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json.has("_id") || !json.has("prediction")) {
|
if (!json.has("_id") || !json.has("prediction")) {
|
||||||
Logger.warn("EVENT", "Payload unvollständig");
|
Logger.warn("EVENT", "Payload is incomplete");
|
||||||
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 bereits verarbeitet: " + id);
|
Logger.warn("EVENT", "Payload ID already processed: " + 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", "Ungültige Prediction: " + prediction);
|
Logger.warn("EVENT", "Invalid prediction: " + prediction);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
persistenceService.store(prediction);
|
persistenceService.store(prediction);
|
||||||
evaluationService.evaluate();
|
evaluationService.evaluate();
|
||||||
|
|
||||||
Logger.debug("EVENT", "Prediction verarbeitet: " + prediction);
|
Logger.debug("EVENT", "Prediction processed: " + prediction);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.error("EVENT", "Payload Verarbeitung fehlgeschlagen: " + payload, e);
|
Logger.error("EVENT", "Payload processing failed: " + payload, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public class DataPersistenceService {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Für Tests
|
// For 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", "Datenbank-Ordner erstellt");
|
Logger.info("DB", "Database folder created");
|
||||||
} else {
|
} 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) {
|
} 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.setInt(1, value);
|
||||||
ps.setString(2, LocalDateTime.now().toString());
|
ps.setString(2, LocalDateTime.now().toString());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
//Logger.debug("DB", "Database Insert erfolgreich: " + ps.toString());
|
//Logger.debug("DB", "Database insert successful: " + ps.toString());
|
||||||
} catch (SQLException e) {
|
} 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) {
|
} catch (SQLException e) {
|
||||||
Logger.error("DB",
|
Logger.error("DB",
|
||||||
"Fehler beim Laden der letzten Einträge", e);
|
"Failed to load latest entries", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Collections.reverse(result);
|
Collections.reverse(result);
|
||||||
|
|||||||
@ -111,7 +111,7 @@ public class MqttClientService implements MqttCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (MqttException e) {
|
} catch (MqttException e) {
|
||||||
Logger.error("MQTT", "Fehler beim Verbinden", e);
|
Logger.error("MQTT", "Failed to connect", 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", "Verbinde mit Broker " + brokerUrl);
|
Logger.info("MQTT", "Connecting to broker " + brokerUrl);
|
||||||
client.connect(options);
|
client.connect(options);
|
||||||
appState.setMqttConnected(true);
|
appState.setMqttConnected(true);
|
||||||
Logger.info("MQTT", "Verbindung hergestellt");
|
Logger.info("MQTT", "Connection established");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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", "Verbindung getrennt");
|
Logger.info("MQTT", "Connection closed");
|
||||||
}
|
}
|
||||||
} catch (MqttException e) {
|
} 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);
|
topicListeners.put(topic, listener);
|
||||||
try {
|
try {
|
||||||
client.subscribe(topic);
|
client.subscribe(topic);
|
||||||
Logger.info("MQTT", "Topic abonniert: " + topic);
|
Logger.info("MQTT", "Subscribed to topic: " + topic);
|
||||||
} catch (MqttException e) {
|
} 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
|
"Publish -> Topic=" + topic + ", QoS=" + qos + ", Payload=" + message
|
||||||
);
|
);
|
||||||
} catch (MqttException e) {
|
} 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) {
|
public void connectionLost(Throwable cause) {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
"MQTT",
|
"MQTT",
|
||||||
"Verbindung verloren: " + cause.getMessage()
|
"Connection lost: " + cause.getMessage()
|
||||||
);
|
);
|
||||||
|
|
||||||
appState.setMqttConnected(false);
|
appState.setMqttConnected(false);
|
||||||
@ -215,7 +215,7 @@ public class MqttClientService implements MqttCallback {
|
|||||||
|
|
||||||
//Logger.debug(
|
//Logger.debug(
|
||||||
// "MQTT",
|
// "MQTT",
|
||||||
// "Nachricht empfangen -> Topic=" + topic + ", Payload=" + payload
|
// "Message received -> 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",
|
||||||
"Keine Listener für Topic: " + topic
|
"No listener for topic: " + topic
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -336,13 +336,13 @@ public class ProcessManagerService {
|
|||||||
pythonProcess = processLauncher.launch(pb);
|
pythonProcess = processLauncher.launch(pb);
|
||||||
verifyPythonStartupHealth();
|
verifyPythonStartupHealth();
|
||||||
|
|
||||||
Logger.info("PROCESS", "Mqtt Simulator gestartet");
|
Logger.info("PROCESS", "MQTT simulator started");
|
||||||
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 Start fehlgeschlagen", e);
|
Logger.error("PROCESS", "MQTT simulator startup failed", 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 Start fehlgeschlagen", e);
|
Logger.error("PROCESS", "Unreal startup failed", 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 gestartet" + pb.command());
|
"Unreal Engine started " + pb.command());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -450,7 +450,7 @@ public class ProcessManagerService {
|
|||||||
*/
|
*/
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
|
||||||
Logger.info("PROCESS", "Shutdown externe Prozesse gestartet");
|
Logger.info("PROCESS", "External process shutdown started");
|
||||||
|
|
||||||
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", "Externe Prozesse beendet");
|
Logger.info("PROCESS", "External processes stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -486,11 +486,11 @@ public class ProcessManagerService {
|
|||||||
processLauncher.launch(pb).waitFor();
|
processLauncher.launch(pb).waitFor();
|
||||||
|
|
||||||
Logger.info("PROCESS",
|
Logger.info("PROCESS",
|
||||||
"Process Tree beendet → PID " + pid);
|
"Process tree terminated -> PID " + pid);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.error("PROCESS",
|
Logger.error("PROCESS",
|
||||||
"Fehler beim Prozess Kill", e);
|
"Failed to terminate process", 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 fehlgeschlagen", e);
|
Logger.error("PROCESS", "PID kill failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,6 +12,9 @@ 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"
|
||||||
@ -22,6 +23,9 @@ 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;
|
||||||
@ -33,10 +37,12 @@ public class AppWindow extends JFrame {
|
|||||||
|
|
||||||
setTitle("Virtueller Gesundheitsassistent");
|
setTitle("Virtueller Gesundheitsassistent");
|
||||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
setSize(1400, 850);
|
setMinimumSize(new Dimension(1280, 720));
|
||||||
|
setSize(1700, 900);
|
||||||
setLocationRelativeTo(null);
|
setLocationRelativeTo(null);
|
||||||
|
getContentPane().setBackground(new Color(15, 19, 26));
|
||||||
|
|
||||||
setLayout(new BorderLayout(10,10));
|
setLayout(new BorderLayout(12, 12));
|
||||||
|
|
||||||
String streamUrl =
|
String streamUrl =
|
||||||
normalizeConfigValue(
|
normalizeConfigValue(
|
||||||
@ -51,11 +57,13 @@ 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, 14);
|
Font uiFont = new Font("Segoe UI", Font.PLAIN, 16);
|
||||||
UIManager.put("Label.font", uiFont);
|
UIManager.put("Label.font", uiFont);
|
||||||
UIManager.put("TabbedPane.font", uiFont);
|
UIManager.put("TabbedPane.font", uiFont);
|
||||||
}
|
}
|
||||||
@ -68,17 +76,96 @@ public class AppWindow extends JFrame {
|
|||||||
private JTabbedPane createTabPane() {
|
private JTabbedPane createTabPane() {
|
||||||
|
|
||||||
JTabbedPane tabs = new JTabbedPane();
|
JTabbedPane tabs = new JTabbedPane();
|
||||||
tabs.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
|
tabs.setBorder(BorderFactory.createEmptyBorder(0, 10, 6, 10));
|
||||||
|
tabs.setFocusable(false);
|
||||||
JPanel streamingPanel = new JPanel(new BorderLayout(10,10));
|
tabs.putClientProperty("JTabbedPane.tabHeight", 38);
|
||||||
streamingPanel.add(streamingView, BorderLayout.CENTER);
|
tabs.putClientProperty("JTabbedPane.hideTabAreaWithOneTab", true);
|
||||||
|
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.
|
||||||
*
|
*
|
||||||
@ -86,18 +173,18 @@ public class AppWindow extends JFrame {
|
|||||||
*/
|
*/
|
||||||
private JPanel createStatusBar() {
|
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(
|
statusBar.setBorder(BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(1,0,0,0, Color.LIGHT_GRAY),
|
BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(54, 64, 78)),
|
||||||
BorderFactory.createEmptyBorder(5,10,5,10)
|
BorderFactory.createEmptyBorder(6, 12, 8, 12)
|
||||||
));
|
));
|
||||||
|
|
||||||
mqttStatusLabel = new JLabel("MQTT: Disconnected");
|
mqttStatusLabel = createStatusChip("MQTT: Disconnected");
|
||||||
problemLevelLabel = new JLabel("Problem: NONE");
|
problemLevelLabel = createStatusChip("Problem: NONE");
|
||||||
|
|
||||||
statusBar.add(mqttStatusLabel);
|
statusBar.add(mqttStatusLabel);
|
||||||
statusBar.add(new JLabel(" | "));
|
|
||||||
statusBar.add(problemLevelLabel);
|
statusBar.add(problemLevelLabel);
|
||||||
|
|
||||||
return statusBar;
|
return statusBar;
|
||||||
@ -109,8 +196,11 @@ 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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,6 +210,7 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,6 +240,160 @@ 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.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -12,18 +12,15 @@ 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.sql.Timestamp;
|
import java.text.SimpleDateFormat;
|
||||||
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;
|
||||||
|
|
||||||
@ -42,28 +39,30 @@ 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());
|
setLayout(new BorderLayout(0, 12));
|
||||||
setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
|
setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||||
setOpaque(false);
|
setOpaque(false);
|
||||||
|
|
||||||
JPanel card = new JPanel(new BorderLayout(10,10));
|
JPanel card = new JPanel(new BorderLayout(0, 14));
|
||||||
card.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
|
card.setBackground(new Color(22, 28, 37));
|
||||||
|
|
||||||
card.setBackground(UIManager.getColor("Panel.background"));
|
|
||||||
card.setOpaque(true);
|
card.setOpaque(true);
|
||||||
|
|
||||||
card.setBorder(BorderFactory.createCompoundBorder(
|
card.setBorder(BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createLineBorder(
|
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 ----------
|
// ---------- TOP: Problem Level ----------
|
||||||
JPanel topWrapper = new JPanel(new BorderLayout());
|
JPanel topWrapper = new JPanel(new BorderLayout(0, 8));
|
||||||
topWrapper.setBorder(BorderFactory.createEmptyBorder(0,0,10,0));
|
topWrapper.setBorder(BorderFactory.createEmptyBorder(0, 0, 8, 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);
|
||||||
|
|
||||||
@ -83,21 +82,28 @@ 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(
|
||||||
2.5f,
|
3.0f,
|
||||||
BasicStroke.CAP_ROUND,
|
BasicStroke.CAP_ROUND,
|
||||||
BasicStroke.JOIN_ROUND
|
BasicStroke.JOIN_ROUND
|
||||||
));
|
));
|
||||||
@ -110,14 +116,25 @@ 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.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);
|
card.add(chartPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
@ -188,7 +205,7 @@ public class DashboardView extends JPanel {
|
|||||||
|
|
||||||
ValueMarker marker = new ValueMarker(value);
|
ValueMarker marker = new ValueMarker(value);
|
||||||
|
|
||||||
marker.setPaint(new Color(150,150,150,150));
|
marker.setPaint(getThresholdColor(label));
|
||||||
|
|
||||||
float[] dash = {6.0f, 6.0f};
|
float[] dash = {6.0f, 6.0f};
|
||||||
marker.setStroke(new BasicStroke(
|
marker.setStroke(new BasicStroke(
|
||||||
@ -201,12 +218,31 @@ public class DashboardView extends JPanel {
|
|||||||
));
|
));
|
||||||
|
|
||||||
marker.setLabel(label);
|
marker.setLabel(label);
|
||||||
marker.setLabelFont(new Font("Segoe UI", Font.PLAIN, 11));
|
marker.setLabelFont(new Font("Segoe UI Semibold", Font.PLAIN, 12));
|
||||||
marker.setLabelPaint(new Color(160,160,160));
|
marker.setLabelPaint(new Color(188, 203, 223));
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,6 @@ 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.
|
||||||
@ -31,7 +29,9 @@ 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) {
|
||||||
/**
|
/**
|
||||||
* Handles JCEF application state transitions.
|
* Handles JCEF application state transitions.
|
||||||
@ -55,7 +55,16 @@ public class PixelStreamingView extends JPanel {
|
|||||||
browser = client.createBrowser(startURL, useOSR, isTransparent);
|
browser = client.createBrowser(startURL, useOSR, isTransparent);
|
||||||
browserUI_ = browser.getUIComponent();
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -17,9 +17,10 @@ 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(100, 50));
|
setPreferredSize(new Dimension(420, 88));
|
||||||
|
setMinimumSize(new Dimension(320, 72));
|
||||||
setOpaque(false);
|
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;
|
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);
|
||||||
|
|
||||||
// --- Hintergrund ---
|
int arc = Math.max(22, height / 2);
|
||||||
GradientPaint bg = new GradientPaint(
|
|
||||||
0, 0, new Color(235,235,235),
|
GradientPaint trackGradient = new GradientPaint(
|
||||||
0, height, new Color(210,210,210)
|
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);
|
int filledWidth = (int) (width * ratio);
|
||||||
|
if (filledWidth > 0) {
|
||||||
|
filledWidth = Math.max(filledWidth, arc / 2);
|
||||||
|
}
|
||||||
|
|
||||||
Color baseColor = getColorForLevel(level);
|
Color baseColor = getColorForLevel(level);
|
||||||
Color darkerColor = baseColor.darker();
|
GradientPaint fillGradient = new GradientPaint(
|
||||||
|
0, 0, baseColor.darker(),
|
||||||
GradientPaint gradient = new GradientPaint(
|
Math.max(1, filledWidth), 0, baseColor
|
||||||
0, 0, darkerColor,
|
|
||||||
filledWidth, 0, baseColor
|
|
||||||
);
|
);
|
||||||
|
g2.setPaint(fillGradient);
|
||||||
g2.setPaint(gradient);
|
g2.fillRoundRect(0, 0, filledWidth, height, arc, arc);
|
||||||
g2.fillRoundRect(0, 0, filledWidth, height, 20, 20);
|
|
||||||
|
|
||||||
g2.setStroke(new BasicStroke(2f));
|
g2.setStroke(new BasicStroke(2f));
|
||||||
g2.setColor(baseColor.brighter());
|
g2.setColor(new Color(175, 190, 212));
|
||||||
g2.drawRoundRect(1,1,width-2,height-2,20,20);
|
g2.drawRoundRect(1, 1, width - 2, height - 2, arc, arc);
|
||||||
|
|
||||||
// --- Text ---
|
String text = "Problem " + level.name() + " " + (int) (ratio * 100) + "%";
|
||||||
String text = level.name() + " (" + (int)(ratio * 100) + "%)";
|
g2.setFont(new Font("Segoe UI Semibold",
|
||||||
|
Font.PLAIN,
|
||||||
g2.setColor(Color.BLACK);
|
Math.max(19, (int) (height * 0.33))));
|
||||||
|
g2.setColor(new Color(244, 248, 255));
|
||||||
|
|
||||||
FontMetrics fm = g2.getFontMetrics();
|
FontMetrics fm = g2.getFontMetrics();
|
||||||
int textWidth = fm.stringWidth(text);
|
int textX = (width - fm.stringWidth(text)) / 2;
|
||||||
|
int textY = (height - fm.getHeight()) / 2 + fm.getAscent();
|
||||||
g2.drawString(
|
g2.drawString(text, textX, textY);
|
||||||
text,
|
|
||||||
(width - textWidth) / 2,
|
|
||||||
(height + fm.getAscent()) / 2 - 3
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,13 +117,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(200, 0, 0);
|
return new Color(214, 70, 70);
|
||||||
case HIGH:
|
case HIGH:
|
||||||
return new Color(255, 140, 0);
|
return new Color(234, 134, 45);
|
||||||
case WARNING:
|
case WARNING:
|
||||||
return new Color(255, 215, 0);
|
return new Color(219, 180, 52);
|
||||||
default:
|
default:
|
||||||
return new Color(0, 170, 0);
|
return new Color(53, 178, 107);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public class ConfigLoader {
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.error("CONFIG",
|
Logger.error("CONFIG",
|
||||||
"Properties Laden fehlgeschlagen", e);
|
"Failed to load properties", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
|
|||||||
@ -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 Fehler: " + e.getMessage());
|
System.err.println("Logger config error: " + 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 Fehler: " + e.getMessage());
|
System.err.println("Logger writer error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +221,7 @@ public class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} 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() {
|
public static void shutdown() {
|
||||||
|
|
||||||
System.out.println("Logger Shutdown gestartet");
|
System.out.println("Logger shutdown started");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(500); // Warte auf Queue Flush
|
Thread.sleep(500); // Wait for 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 Fehler: " + e.getMessage());
|
System.err.println("Logger shutdown error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user