diff --git a/README.md b/README.md new file mode 100644 index 0000000..c30d640 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Vantage + +A desktop dashboard for a self-hosted homelab: per-host CPU / memory / disk / load / uptime, +Docker container control, and HTTP / TCP health checks. Built with C# and Avalonia 11, +targeting `net6.0`. + +Vantage is designed to be dogfooded against a real VPS and lab hosts over SSH, but it runs +fully offline out of the box using deterministic mock providers, so the UI and the whole +data flow work without a single live host. + +## Architecture + +Two projects plus two test projects: + +- `src/Vantage.Core` - models, provider abstractions, parsers, health checks, config store. + No UI dependency, fully unit tested. +- `src/Vantage.App` - the Avalonia application: views, view models, a hand-rolled sparkline + control, dependency injection wiring. +- `tests/Vantage.Core.Tests` - parsers, providers, health checks, config store. +- `tests/Vantage.App.Tests` - view models and a headless Avalonia smoke test that loads the + real window and binds the real view model. + +### Provider abstractions + +Everything that touches a remote host sits behind an interface, with a real SSH implementation +and a mock implementation: + +- `IMetricsProvider` - `SshMetricsProvider` runs one batched shell command per poll + (`/proc/loadavg`, `/proc/uptime`, `/proc/meminfo`, `df`, two `/proc/stat` snapshots for the + CPU delta) and parses it; `MockMetricsProvider` synthesizes time-varying, per-host-stable + metrics so sparklines animate offline. +- `IDockerProvider` - `SshDockerProvider` shells out to `docker ps` / `docker start|stop|restart`; + `MockDockerProvider` keeps an in-memory container set whose state transitions on actions. +- `IHealthChecker` - `HealthChecker` is a real implementation (HttpClient for HTTP, TcpClient + for TCP) and works against any reachable endpoint right now. + +The SSH transport is isolated in `ISshCommandRunner` / `SshNetCommandRunner`. Because of that, +the SSH providers are exercised in tests with a fake runner feeding captured command output - +no live host needed to test the parse path. + +### Metrics parse path + +The remote command emits marker lines (`@@vantage:loadavg`, `@@vantage:df`, ...). Each section +has its own pure parser (`LoadAvgParser`, `MemInfoParser`, `DfParser`, `ProcStatParser`, +`CpuUsage`), and `MetricsBundleParser` stitches them into a `HostMetrics`. Every parser is a +pure function with a fixture-based test. + +## Wave 2 (depth) + +Built additively on top of v1, all in `Vantage.Core` behind the same interface-plus-mock pattern: + +- **Metrics history** - `IMetricsStore` / `SqliteMetricsStore` writes a time-series of cpu / memory / + disk / load into SQLite (`~/.config/vantage/history.db`), with range queries and rolling + retention (`PruneOlderThanAsync`). The monitor loop records every successful poll. +- **Alerts** - `AlertRule` (threshold + comparison + `For` duration + severity) evaluated per host + by `AlertRuleEvaluator` (Ok -> Pending -> Firing, with a Resolved transition on recovery). + `AlertEngine` fans transitions out to sinks: `LogAlertSink` (in-memory alert log), + `DesktopAlertSink` (via an `IDesktopNotifier` seam), and `WebhookAlertSink` (POSTs JSON). + The app wires the engine into the poll loop and shows live alert state in an Alerts tab. +- **More collectors** - each is parser + `ISomethingProvider` + SSH impl + mock: + network rx/tx (`/proc/net/dev` + throughput delta), top processes (`ps`), systemd services + (`systemctl list-units`), temperatures (`/sys/class/thermal`) and GPU (`nvidia-smi`). +- **Log viewer** - `LogCommand` builds `journalctl -u ... -o short-iso` or `tail -n ... file` + (with an optional shell-quoted `grep` filter), `LogLineParser` splits timestamp / unit / message, + behind `ILogProvider` (SSH + mock). +- **Prometheus provider** - `PrometheusTextParser` parses the text exposition format (labels with + escapes, `+Inf` / `-Inf` / `NaN`, optional timestamp); `PrometheusMetricsProvider` scrapes a + node_exporter `/metrics` endpoint and maps it to `HostMetrics` (CPU from a two-scrape counter + delta). Select it with `VANTAGE_PROVIDERS=prometheus`. +- **Multi-host view** - `OverviewViewModel` aggregates online/offline counts, average and peak CPU, + and firing-alert count; surfaced as a Fleet grid tab. + +## Running + +Requires the .NET 6 SDK. + +``` +dotnet build +dotnet test +dotnet run --project src/Vantage.App +``` + +By default the app uses the mock providers, so it comes up populated with demo hosts and +health checks and needs nothing configured. To point it at real hosts over SSH: + +``` +VANTAGE_PROVIDERS=ssh dotnet run --project src/Vantage.App +``` + +`VANTAGE_PROVIDERS` accepts `mock` (default), `ssh`, or `prometheus`. + +### Configuration + +Host and health-check configuration persists as JSON at +`%APPDATA%/vantage/config.json` (Linux: `~/.config/vantage/config.json`). On first run with no +config file, the app seeds demo data. The persistence layer (`JsonConfigStore`) is done and +tested; for now edit that JSON file and restart to change hosts. An in-app editor is the next +piece of UI (see below). + +## Verification status + +- `dotnet build` is clean (0 warnings, 0 errors). +- `dotnet test` is green (110 tests): v1 parsers and providers, plus wave-2 coverage - SQLite + time-series write/read/retention, alert-rule evaluation (pending/firing/resolved and the + counterfactual "never fires early" cases), alert sinks (webhook posts JSON to a stubbed + handler, log/desktop sinks), the network / process / systemd / thermal / GPU parsers, log + command building and line parsing, the Prometheus text parser and node_exporter mapping + (including two-scrape CPU rate), and the app-side wiring test proving a poll records history + and fires an alert. +- A headless Avalonia test loads the actual `MainWindow` (Fleet / Host / Alerts / Docker / Health + tabs), binds the real view model, and asserts the visual tree materialized. + +This environment is headless (no X server), so the window's **appearance has not been visually +verified**. Screenshots need to be taken on a machine with a display. + +## What is intentionally not in v1 + +- In-app host / health-check editor forms (config is file-based for now). +- SSH connection pooling (a fresh connection per poll today) and SSH-agent auth + (`SshAuthMethod.Agent` throws a clear NotSupported until wired). +- Packaging / installers, mobile, live charting library (the sparkline is hand-rolled).