roboticus-api 0.11.3

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
use axum::extract::State;
use axum::response::{Html, IntoResponse, Response};

use crate::api::AppState;

pub async fn dashboard_handler(State(state): State<AppState>) -> Response {
    let config = state.config.read().await;
    let key = config.server.api_key.as_deref();

    // Generate a cryptographically random nonce for this request.
    let nonce = uuid::Uuid::new_v4().to_string();
    let html = build_dashboard_html(key, &nonce);

    let csp = format!(
        "default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'"
    );

    let mut response = Html(html).into_response();
    if let Ok(csp_value) = axum::http::HeaderValue::from_str(&csp) {
        response.headers_mut().insert(
            axum::http::header::HeaderName::from_static("content-security-policy"),
            csp_value,
        );
    } else {
        // Fallback CSP without nonce — still safer than no CSP at all.
        tracing::error!("CSP header contained non-ASCII; applying fallback CSP");
        response.headers_mut().insert(
            axum::http::header::HeaderName::from_static("content-security-policy"),
            axum::http::HeaderValue::from_static(
                "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'"
            ),
        );
    }
    response
}

pub fn build_dashboard_html(_api_key: Option<&str>, nonce: &str) -> String {
    // ── Compile-time assembly from modular files ─────────────────
    // Each file is embedded via include_str!() and concatenated into
    // the template's {CSS} and {JS} placeholders. The browser still
    // receives a single HTML document.

    let css = [
        include_str!("dashboard/css/variables.css"),
        include_str!("dashboard/css/layout.css"),
        include_str!("dashboard/css/components.css"),
        include_str!("dashboard/css/animations.css"),
    ]
    .join("\n");

    let js = [
        include_str!("dashboard/js/utils.js"),
        include_str!("dashboard/js/api.js"),
        include_str!("dashboard/js/hints.js"),
        include_str!("dashboard/js/charts.js"),
        include_str!("dashboard/js/app-core.js"),
        include_str!("dashboard/js/pages/overview.js"),
        include_str!("dashboard/js/pages/sessions.js"),
        include_str!("dashboard/js/pages/memory.js"),
        include_str!("dashboard/js/pages/skills.js"),
        include_str!("dashboard/js/pages/scheduler.js"),
        include_str!("dashboard/js/pages/integrations.js"),
        include_str!("dashboard/js/pages/metrics.js"),
        include_str!("dashboard/js/pages/efficiency.js"),
        include_str!("dashboard/js/pages/agents.js"),
        include_str!("dashboard/js/pages/context.js"),
        include_str!("dashboard/js/pages/settings.js"),
        include_str!("dashboard/js/pages/wallet.js"),
        include_str!("dashboard/js/pages/workspace.js"),
        include_str!("dashboard/js/themes.js"),
        include_str!("dashboard/js/help-modal.js"),
        include_str!("dashboard/js/events.js"),
        include_str!("dashboard/js/websocket.js"),
    ]
    .join("\n");

    let template = include_str!("dashboard/template.html");
    let assembled = template
        .replace("/* {CSS} */", &css)
        .replace("/* {JS} */", &js);

    let with_vars = assembled.replace("var BASE = '';", "var BASE = ''; var API_KEY = null;");
    // Inject the nonce into every inline <script> tag.
    with_vars.replace("<script>", &format!("<script nonce=\"{nonce}\">"))
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEST_NONCE: &str = "test-nonce-value";

    #[test]
    fn dashboard_html_contains_title() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert!(html.contains("<title>Roboticus Dashboard</title>"));
        assert!(html.contains("/api/health"));
    }

    #[test]
    fn build_dashboard_html_contains_all_sections() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert!(html.contains("Roboticus"));
        assert!(html.contains("Overview"));
        assert!(html.contains("/api/sessions"));
        assert!(html.contains("/api/memory/episodic"));
        assert!(html.contains("/api/cron/jobs"));
        assert!(html.contains("/api/stats/costs"));
        assert!(html.contains("/api/skills"));
        assert!(html.contains("/api/wallet/balance"));
        assert!(html.contains("/api/breaker/status"));
    }

    #[test]
    fn dashboard_html_contains_catalog_controls() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert!(html.contains("/api/skills/catalog"));
        assert!(html.contains("/api/skills/catalog/install"));
        assert!(html.contains("/api/skills/catalog/activate"));
        assert!(html.contains("btn-catalog-install"));
        assert!(html.contains("btn-catalog-install-activate"));
        assert!(html.contains("cat-skill-check"));
    }

    #[test]
    fn dashboard_html_without_key_has_api_health() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert!(html.contains("<title>Roboticus Dashboard</title>"));
        assert!(html.contains("/api/health"));
    }

    #[test]
    fn dashboard_never_injects_api_key() {
        let html = build_dashboard_html(Some("test-dashboard-key"), TEST_NONCE);
        assert!(
            html.contains("API_KEY = null"),
            "API key must never be embedded"
        );
    }

    #[test]
    fn dashboard_null_api_key_always() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert!(html.contains("API_KEY = null"));
    }

    #[test]
    fn dashboard_html_contains_single_html_close_tag() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert_eq!(html.matches("</html>").count(), 1);
    }

    #[test]
    fn dashboard_html_injects_nonce_into_script_tags() {
        let html = build_dashboard_html(None, TEST_NONCE);
        assert!(
            html.contains(&format!("<script nonce=\"{TEST_NONCE}\">")),
            "script tags must include nonce attribute"
        );
        assert!(
            !html.contains("<script>"),
            "no bare <script> tags should remain"
        );
    }

    #[test]
    fn dashboard_nonce_is_unique_per_call() {
        let nonce_a = uuid::Uuid::new_v4().to_string();
        let nonce_b = uuid::Uuid::new_v4().to_string();
        assert_ne!(nonce_a, nonce_b);

        let html_a = build_dashboard_html(None, &nonce_a);
        let html_b = build_dashboard_html(None, &nonce_b);
        assert!(html_a.contains(&nonce_a));
        assert!(html_b.contains(&nonce_b));
        assert!(!html_a.contains(&nonce_b));
    }
}