feat: ship HTTP dashboard and harden daemon/API flows

Add the static HTTP dashboard example and wire in the recent daemon/API polish:
CORS-aware API routing, service-install behavior cleanup, safer systemd unit
ExecStart quoting, and friendly-name validation for path-safe targeting.

Also refresh README/API/roadmap docs, remove the temporary claude observations
file, and include the related tests for API/status and daemon validation.
This commit is contained in:
44r0n7
2026-04-18 16:45:12 -04:00
parent 795aa2f713
commit 59fb56558f
17 changed files with 999 additions and 726 deletions
+54 -2
View File
@@ -59,6 +59,8 @@ impl TestDaemon {
http_enabled: true,
http_port: port,
http_host: "127.0.0.1".to_string(),
cors_enabled: false,
cors_allowed_origins: Vec::new(),
log_level: "info".to_string(),
},
discovery: tvctl::daemon::config::DiscoveryConfig {
@@ -153,6 +155,10 @@ impl Drop for TestDaemon {
impl InProcessApi {
async fn start() -> Self {
Self::start_with_daemon_config(DaemonConfig::default()).await
}
async fn start_with_daemon_config(daemon_config: DaemonConfig) -> Self {
let temp_dir = tempfile::tempdir().expect("temp dir should exist");
let root = temp_dir.path();
let config_home = root.join("config");
@@ -176,7 +182,9 @@ impl InProcessApi {
http_enabled: true,
http_port: port,
http_host: "127.0.0.1".to_string(),
log_level: "info".to_string(),
cors_enabled: daemon_config.cors_enabled,
cors_allowed_origins: daemon_config.cors_allowed_origins,
log_level: daemon_config.log_level,
},
discovery: DiscoveryConfig {
auto_discover: false,
@@ -233,6 +241,7 @@ impl InProcessApi {
} else {
registry.ensure_default();
}
let daemon_http_config = config.daemon.clone();
let daemon: SharedDaemon = Arc::new(Mutex::new(Daemon {
config,
paths: paths.clone(),
@@ -243,7 +252,7 @@ impl InProcessApi {
discovery: DiscoveryService::new(adapters),
}));
let app = tvctl::api::router(daemon);
let app = tvctl::api::router_with_config(daemon, &daemon_http_config);
let server = tokio::spawn(async move {
axum::serve(listener, app)
.await
@@ -397,3 +406,46 @@ async fn http_api_routes_requests_without_unix_socket_loopback() {
assert_eq!(devices_json["ok"], true);
assert_eq!(devices_json["data"][0]["id"], api.device.id.to_string());
}
#[tokio::test]
async fn http_api_cors_is_disabled_by_default() {
let api = InProcessApi::start().await;
let client = Client::new();
let response = client
.get(format!("{}/devices", api.base_url))
.header("Origin", "http://127.0.0.1:8080")
.send()
.await
.expect("cors default response should arrive");
assert!(
response
.headers()
.get("access-control-allow-origin")
.is_none(),
"CORS headers should be absent by default"
);
}
#[tokio::test]
async fn http_api_cors_allows_configured_loopback_origin() {
let api = InProcessApi::start_with_daemon_config(DaemonConfig {
cors_enabled: true,
cors_allowed_origins: vec!["http://127.0.0.1:8080".to_string()],
..DaemonConfig::default()
})
.await;
let client = Client::new();
let response = client
.get(format!("{}/devices", api.base_url))
.header("Origin", "http://127.0.0.1:8080")
.send()
.await
.expect("cors allowed response should arrive");
assert_eq!(
response
.headers()
.get("access-control-allow-origin")
.and_then(|value| value.to_str().ok()),
Some("http://127.0.0.1:8080")
);
}