holger-ui 0.1.1

Operator/admin UI for holger over the HolgerObject core API — egui via facett, embedded (LocalHolger, direct core calls) or remote (RemoteHolger gRPC).
//! `holger-ui` — the holger operator/admin desktop UI (egui via facett).
//!
//! Usage:
//! ```text
//! holger-ui [ENDPOINT] [--token <TOKEN>] [--ca <CA.pem>] [--cert <CERT.pem> --key <KEY.pem>]
//! ```
//! `ENDPOINT` is the holger gRPC address (default `http://127.0.0.1:50051`). A
//! bearer token may be supplied with `--token <TOKEN>` or the `HOLGER_TOKEN`
//! environment variable (needed for write access on an auth-enabled server).
//!
//! TLS / mTLS: pass `--ca <CA.pem>` to verify the server against a custom CA,
//! and `--cert <CERT.pem> --key <KEY.pem>` to present a client certificate
//! (mTLS — the CN → role auth path). When any of these are present the UI
//! connects over TLS; otherwise it uses the plain (token-only) path. Use an
//! `https://` endpoint with TLS.
//!
//! The UI connects with `RemoteHolger`; because the view-model is generic over
//! `HolgerObject`, an embedded build could hand it a `LocalHolger` instead for
//! direct in-process core calls (no network).

use holger_ui::app::HolgerUiApp;
use holger_ui::data::UiData;

fn main() -> eframe::Result<()> {
    // Tiny hand-rolled arg parse: first non-flag positional is the endpoint;
    // `--token <t>` (or $HOLGER_TOKEN) is the bearer token; `--ca/--cert/--key`
    // are PEM paths for the TLS / mTLS path.
    let mut endpoint = "http://127.0.0.1:50051".to_string();
    let mut token = std::env::var("HOLGER_TOKEN").ok();
    let mut ca_path: Option<String> = None;
    let mut cert_path: Option<String> = None;
    let mut key_path: Option<String> = None;
    let mut args = std::env::args().skip(1);
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--token" => token = args.next(),
            "--ca" => ca_path = args.next(),
            "--cert" => cert_path = args.next(),
            "--key" => key_path = args.next(),
            other => endpoint = other.to_string(),
        }
    }

    // Read any PEM material up front so a missing file fails before the GUI opens.
    let read_pem = |path: &str| -> Result<Vec<u8>, ()> {
        std::fs::read(path).map_err(|e| {
            eprintln!("holger-ui: failed to read PEM file {path}: {e}");
        })
    };

    // mTLS client identity needs both --cert and --key together.
    let client_identity = match (cert_path.as_deref(), key_path.as_deref()) {
        (Some(c), Some(k)) => match (read_pem(c), read_pem(k)) {
            (Ok(cert), Ok(key)) => Some((cert, key)),
            _ => std::process::exit(1),
        },
        (None, None) => None,
        _ => {
            eprintln!("holger-ui: --cert and --key must be supplied together for mTLS");
            std::process::exit(1);
        }
    };
    let ca = match ca_path.as_deref() {
        Some(p) => match read_pem(p) {
            Ok(bytes) => Some(bytes),
            Err(()) => std::process::exit(1),
        },
        None => None,
    };

    // Any TLS material present → take the TLS path; otherwise the plain
    // (token-only) connect path.
    let use_tls = ca.is_some() || client_identity.is_some();
    let connect = if use_tls {
        UiData::connect_remote_with_tls(&endpoint, ca, client_identity, token.as_deref())
    } else {
        UiData::connect_remote_with_token(&endpoint, token.as_deref())
    };

    let data = match connect {
        Ok(d) => d,
        Err(e) => {
            eprintln!("holger-ui: failed to connect to holger at {endpoint}: {e:#}");
            std::process::exit(1);
        }
    };

    let native_options = eframe::NativeOptions {
        viewport: eframe::egui::ViewportBuilder::default()
            .with_inner_size([1100.0, 720.0])
            .with_min_inner_size([720.0, 480.0])
            .with_title("holger"),
        ..Default::default()
    };

    eframe::run_native(
        "holger-ui",
        native_options,
        Box::new(move |cc| {
            // Unified facett look & feel: the app installs the active preset's
            // full egui Style every frame (and publishes the derived legacy
            // palette so the facett Table facets re-theme coherently — COH-1).
            // Prime it once here so the first frame is already themed; replaces
            // the old per-crate `facett::Theme::sci_fi()`.
            let app = HolgerUiApp::new(data);
            app.apply_look(&cc.egui_ctx);
            Ok(Box::new(app))
        }),
    )
}