feat(http): ML model source status endpoints

Expose the ML `ModelSourceState` over HTTP so the dashboard can render a
Dormant / Active / Error panel and admins can idempotently revert to Dormant.

- `GET /api/ml/models/current` returns `{ label, status }` where `label` is
  one of `"active" | "dormant" | "error"` and `status` is the serde-tagged
  `ModelSourceStatus` payload (carries `ModelInfo` on Active, `msg` /
  `since_secs` / `last_attempted_path` on Error).
- `DELETE /api/ml/models/current` swaps the `Inference` state to Dormant and
  returns `{ already_dormant: bool }`. Repeated calls stay Ok so the client
  can retry safely.
- `http_server` injects `Arc<Inference>` as an Actix `app_data` extractor.

Backend half of I-5; UI card to consume these endpoints lands with the
frontend milestone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-04-18 16:05:27 +08:00
parent 56395f8027
commit 663b3829b6
2 changed files with 41 additions and 1 deletions

View File

@ -1,9 +1,16 @@
use std::sync::Arc;
use actix_web::{HttpResponse, Responder, Scope, web};
use crate::core::ml::adapter::ModelSourceState;
use crate::core::ml::engine::Engine;
use crate::core::ml::inference::Inference;
pub fn initialize() -> Scope {
web::scope("/ml").route("/status", web::get().to(get_status))
web::scope("/ml")
.route("/status", web::get().to(get_status))
.route("/models/current", web::get().to(get_current_model))
.route("/models/current", web::delete().to(delete_current_model))
}
async fn get_status(engine: web::Data<Engine>) -> impl Responder {
@ -20,3 +27,34 @@ async fn get_status(engine: web::Data<Engine>) -> impl Responder {
"inference_interval_secs": engine.inference_interval_secs(),
}))
}
/// `GET /api/ml/models/current` — wire-format snapshot of the ML source
/// state the dashboard's ML Status panel renders.
async fn get_current_model(inference: web::Data<Arc<Inference>>) -> impl Responder {
let status = inference.current_status();
let label = if status.is_active() {
"active"
} else if status.is_dormant() {
"dormant"
} else {
"error"
};
HttpResponse::Ok().json(serde_json::json!({
"label": label,
"status": status,
}))
}
/// `DELETE /api/ml/models/current` — admin action: force the ML source back
/// to dormant. No-op when already dormant so the client can retry idempotently.
async fn delete_current_model(inference: web::Data<Arc<Inference>>) -> impl Responder {
if inference.current_status().is_dormant() {
return HttpResponse::Ok().json(serde_json::json!({
"already_dormant": true,
}));
}
inference.swap_state(ModelSourceState::Dormant);
HttpResponse::Ok().json(serde_json::json!({
"already_dormant": false,
}))
}

View File

@ -208,6 +208,7 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
let health = params.app_services.health.clone();
let ml_alert = params.app_services.ml_alert.clone();
let ml_engine = params.app_services.ml_engine.clone();
let ml_inference = params.app_services.ml_inference.clone();
let flow_statistics = params.app_services.flow_statistics.clone();
let drop_monitor = params.ebpf_services.drop_monitor.clone();
let app_config = params.app_config;
@ -246,6 +247,7 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
.app_data(web::Data::from(health.clone()))
.app_data(web::Data::from(ml_alert.clone()))
.app_data(web::Data::from(ml_engine.clone()))
.app_data(web::Data::from(ml_inference.clone()))
.app_data(web::Data::from(flow_statistics.clone()))
.app_data(web::Data::from(drop_monitor.clone()))
.app_data(web::Data::from(db.clone() as Arc<dyn AppRepo>))