From 08add0d08716e2f1f34dad635fcb546f42728f49 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 26 Mar 2026 09:18:14 +0800 Subject: [PATCH] refactor(api): embed runtime in unit list/get/detail responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the standalone GET /api/unit/runtimes endpoint in favour of embedding runtime directly in existing responses: - GET /api/unit → each item now includes `runtime` field - GET /api/unit/:id → returns UnitWithRuntime - GET /api/unit/:id/detail → UnitDetail now includes `runtime` runtime is null when the engine has not yet initialised the unit. Frontend loadUnits() reads the embedded runtime field to populate state.runtimes — one request instead of two. Co-Authored-By: Claude Sonnet 4.6 --- src/handler/control.rs | 37 +++++++++++++++++++++++++------------ src/main.rs | 4 ---- web/js/units.js | 11 ++++------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/handler/control.rs b/src/handler/control.rs index 1e84b85..fe6adbb 100644 --- a/src/handler/control.rs +++ b/src/handler/control.rs @@ -26,6 +26,13 @@ pub struct GetUnitListQuery { pub pagination: PaginationParams, } +#[derive(serde::Serialize)] +pub struct UnitWithRuntime { + #[serde(flatten)] + pub unit: crate::model::ControlUnit, + pub runtime: Option, +} + pub async fn get_unit_list( State(state): State, Query(query): Query, @@ -41,6 +48,15 @@ pub async fn get_unit_list( ) .await?; + let all_runtimes = state.control_runtime.get_all().await; + let data = data + .into_iter() + .map(|unit| { + let runtime = all_runtimes.get(&unit.id).cloned(); + UnitWithRuntime { unit, runtime } + }) + .collect::>(); + Ok(Json(PaginatedResponse::new( data, total, @@ -118,10 +134,11 @@ pub async fn get_unit( State(state): State, Path(unit_id): Path, ) -> Result { - match crate::service::get_unit_by_id(&state.pool, unit_id).await? { - Some(unit) => Ok(Json(unit)), - None => Err(ApiErr::NotFound("Unit not found".to_string(), None)), - } + let unit = crate::service::get_unit_by_id(&state.pool, unit_id) + .await? + .ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?; + let runtime = state.control_runtime.get(unit_id).await; + Ok(Json(UnitWithRuntime { unit, runtime })) } #[derive(serde::Serialize)] @@ -142,6 +159,7 @@ pub struct EquipmentDetail { pub struct UnitDetail { #[serde(flatten)] pub unit: crate::model::ControlUnit, + pub runtime: Option, pub equipments: Vec, } @@ -153,6 +171,8 @@ pub async fn get_unit_detail( .await? .ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?; + let runtime = state.control_runtime.get(unit_id).await; + let equipments = crate::service::get_equipment_by_unit_id(&state.pool, unit_id).await?; let equipment_ids: Vec = equipments.iter().map(|e| e.id).collect(); let all_points = crate::service::get_points_by_equipment_ids(&state.pool, &equipment_ids).await?; @@ -177,7 +197,7 @@ pub async fn get_unit_detail( }) .collect(); - Ok(Json(UnitDetail { unit, equipments })) + Ok(Json(UnitDetail { unit, runtime, equipments })) } #[derive(Debug, Deserialize, Validate)] @@ -510,10 +530,3 @@ pub async fn get_unit_runtime( Ok(Json(runtime)) } -/// Returns all known runtimes as { unit_id: UnitRuntime }. -/// Used by the frontend on page load to populate initial state. -pub async fn get_all_unit_runtimes( - State(state): State, -) -> impl IntoResponse { - Json(state.control_runtime.get_all().await) -} diff --git a/src/main.rs b/src/main.rs index 5226d79..c9e96c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -209,10 +209,6 @@ fn build_router(state: AppState) -> Router { "/api/unit", get(handler::control::get_unit_list).post(handler::control::create_unit), ) - .route( - "/api/unit/runtimes", - get(handler::control::get_all_unit_runtimes), - ) .route( "/api/unit/{unit_id}", get(handler::control::get_unit) diff --git a/web/js/units.js b/web/js/units.js index 1d2e98e..ca3aa14 100644 --- a/web/js/units.js +++ b/web/js/units.js @@ -180,10 +180,7 @@ export function renderUnits() { } export async function loadUnits() { - const [response, runtimes] = await Promise.all([ - apiFetch("/api/unit?page=1&page_size=-1"), - apiFetch("/api/unit/runtimes").catch(() => ({})), - ]); + const response = await apiFetch("/api/unit?page=1&page_size=-1"); state.units = response.data || []; state.unitMap = new Map(state.units.map((unit) => [unit.id, unit])); @@ -191,9 +188,9 @@ export async function loadUnits() { state.selectedUnitId = null; } - for (const [unitId, runtime] of Object.entries(runtimes)) { - state.runtimes.set(unitId, runtime); - } + state.units.forEach((unit) => { + if (unit.runtime) state.runtimes.set(unit.id, unit.runtime); + }); renderUnits(); renderUnitOptions(dom.equipmentUnitId?.value || "", dom.equipmentUnitId);