refactor(api): embed runtime in unit list/get/detail responses

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 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-26 09:18:14 +08:00
parent 42cdbbc0cc
commit 08add0d087
3 changed files with 29 additions and 23 deletions

View File

@ -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<crate::control::runtime::UnitRuntime>,
}
pub async fn get_unit_list(
State(state): State<AppState>,
Query(query): Query<GetUnitListQuery>,
@ -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::<Vec<_>>();
Ok(Json(PaginatedResponse::new(
data,
total,
@ -118,10 +134,11 @@ pub async fn get_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
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<crate::control::runtime::UnitRuntime>,
pub equipments: Vec<EquipmentDetail>,
}
@ -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<Uuid> = 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<AppState>,
) -> impl IntoResponse {
Json(state.control_runtime.get_all().await)
}

View File

@ -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)

View File

@ -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);