@@ -9696,6 +9696,116 @@ pub async fn wolfflow_all_containers_exec(req: HttpRequest, state: web::Data<App
96969696 }
96979697}
96989698
9699+ // ─── WolfUSB API ───
9700+
9701+ /// GET /api/wolfusb/status — installation status, service status, config
9702+ pub async fn wolfusb_status(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9703+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9704+ let config = crate::wolfusb::WolfUsbConfig::load();
9705+ HttpResponse::Ok().json(serde_json::json!({
9706+ "installed": crate::wolfusb::is_installed(),
9707+ "version": crate::wolfusb::installed_version(),
9708+ "running": crate::wolfusb::is_running(),
9709+ "config": {
9710+ "enabled": config.enabled,
9711+ "bind_address": config.bind_address,
9712+ "port": config.port,
9713+ "has_auth_key": !config.auth_key.is_empty(),
9714+ },
9715+ "assignment_count": config.assignments.len(),
9716+ }))
9717+ }
9718+
9719+ /// POST /api/wolfusb/install — install WolfUSB from setup script
9720+ pub async fn wolfusb_install(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9721+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9722+ match crate::wolfusb::install().await {
9723+ Ok(output) => HttpResponse::Ok().json(serde_json::json!({ "ok": true, "output": output })),
9724+ Err(e) => HttpResponse::Ok().json(serde_json::json!({ "ok": false, "error": e })),
9725+ }
9726+ }
9727+
9728+ /// GET /api/wolfusb/config — get full config
9729+ pub async fn wolfusb_get_config(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9730+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9731+ let config = crate::wolfusb::WolfUsbConfig::load();
9732+ HttpResponse::Ok().json(&config)
9733+ }
9734+
9735+ /// POST /api/wolfusb/config — save config and optionally start/stop service
9736+ pub async fn wolfusb_save_config(req: HttpRequest, state: web::Data<AppState>, body: web::Json<serde_json::Value>) -> HttpResponse {
9737+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9738+ let mut config = crate::wolfusb::WolfUsbConfig::load();
9739+
9740+ if let Some(v) = body.get("enabled").and_then(|v| v.as_bool()) { config.enabled = v; }
9741+ if let Some(v) = body.get("bind_address").and_then(|v| v.as_str()) { config.bind_address = v.to_string(); }
9742+ if let Some(v) = body.get("port").and_then(|v| v.as_u64()) { config.port = v as u16; }
9743+ if let Some(v) = body.get("auth_key").and_then(|v| v.as_str()) {
9744+ if !v.contains("••••") { config.auth_key = v.to_string(); }
9745+ }
9746+
9747+ if let Err(e) = config.save() {
9748+ return HttpResponse::InternalServerError().json(serde_json::json!({ "error": e }));
9749+ }
9750+
9751+ // Start or stop service based on enabled flag
9752+ if config.enabled && crate::wolfusb::is_installed() {
9753+ if let Err(e) = crate::wolfusb::start_service(&config) {
9754+ return HttpResponse::Ok().json(serde_json::json!({ "ok": true, "warning": format!("Config saved but failed to start service: {}", e) }));
9755+ }
9756+ } else if !config.enabled {
9757+ let _ = crate::wolfusb::stop_service();
9758+ }
9759+
9760+ HttpResponse::Ok().json(serde_json::json!({ "ok": true }))
9761+ }
9762+
9763+ /// GET /api/wolfusb/devices — list USB devices on this node
9764+ pub async fn wolfusb_devices(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9765+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9766+ let config = crate::wolfusb::WolfUsbConfig::load();
9767+ let devices = crate::wolfusb::list_local_devices(&config);
9768+ HttpResponse::Ok().json(serde_json::json!({
9769+ "devices": devices,
9770+ "assignments": config.assignments,
9771+ }))
9772+ }
9773+
9774+ /// POST /api/wolfusb/assign — assign a USB device to a container/VM
9775+ pub async fn wolfusb_assign(req: HttpRequest, state: web::Data<AppState>, body: web::Json<serde_json::Value>) -> HttpResponse {
9776+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9777+ let mut config = crate::wolfusb::WolfUsbConfig::load();
9778+
9779+ let bus = body.get("bus").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9780+ let address = body.get("address").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9781+ let label = body.get("label").and_then(|v| v.as_str()).unwrap_or("");
9782+ let target_type = body.get("target_type").and_then(|v| v.as_str()).unwrap_or("");
9783+ let target_name = body.get("target_name").and_then(|v| v.as_str()).unwrap_or("");
9784+
9785+ if bus == 0 || target_type.is_empty() || target_name.is_empty() {
9786+ return HttpResponse::BadRequest().json(serde_json::json!({ "error": "bus, target_type, and target_name are required" }));
9787+ }
9788+
9789+ match crate::wolfusb::assign_device(&mut config, bus, address, label, target_type, target_name) {
9790+ Ok(msg) => HttpResponse::Ok().json(serde_json::json!({ "ok": true, "message": msg })),
9791+ Err(e) => HttpResponse::Ok().json(serde_json::json!({ "ok": false, "error": e })),
9792+ }
9793+ }
9794+
9795+ /// POST /api/wolfusb/unassign — remove a USB device assignment
9796+ pub async fn wolfusb_unassign(req: HttpRequest, state: web::Data<AppState>, body: web::Json<serde_json::Value>) -> HttpResponse {
9797+ if let Err(resp) = require_auth(&req, &state) { return resp; }
9798+ let mut config = crate::wolfusb::WolfUsbConfig::load();
9799+
9800+ let bus = body.get("bus").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9801+ let address = body.get("address").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9802+
9803+ match crate::wolfusb::unassign_device(&mut config, bus, address) {
9804+ Ok(msg) => HttpResponse::Ok().json(serde_json::json!({ "ok": true, "message": msg })),
9805+ Err(e) => HttpResponse::Ok().json(serde_json::json!({ "ok": false, "error": e })),
9806+ }
9807+ }
9808+
96999809// ─── MySQL Database Editor API ───
97009810
97019811/// GET /api/mysql/detect — check if MySQL is installed on this node
@@ -16092,6 +16202,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
1609216202 .route("/api/wolfflow/container-exec", web::post().to(wolfflow_container_exec))
1609316203 .route("/api/wolfflow/all-containers-exec", web::post().to(wolfflow_all_containers_exec))
1609416204 .route("/api/wolfflow/infrastructure", web::get().to(wolfflow_infrastructure))
16205+ // WolfUSB — USB over IP
16206+ .route("/api/wolfusb/status", web::get().to(wolfusb_status))
16207+ .route("/api/wolfusb/install", web::post().to(wolfusb_install))
16208+ .route("/api/wolfusb/config", web::get().to(wolfusb_get_config))
16209+ .route("/api/wolfusb/config", web::post().to(wolfusb_save_config))
16210+ .route("/api/wolfusb/devices", web::get().to(wolfusb_devices))
16211+ .route("/api/wolfusb/assign", web::post().to(wolfusb_assign))
16212+ .route("/api/wolfusb/unassign", web::post().to(wolfusb_unassign))
1609516213 // VR Terminal
1609616214 .route("/api/vr-terminal/create", web::post().to(crate::vr_terminal::vr_term_create))
1609716215 .route("/api/vr-terminal/{id}/output", web::get().to(crate::vr_terminal::vr_term_output))
0 commit comments