fix(web): fetch all unit runtimes on page load

Root cause: state.runtimes was empty after refresh because the engine
only pushes UnitRuntimeChanged on state transitions — if the engine
is mid-wait-phase, no push occurs and badges show OFFLINE.

Fix: add GET /api/unit/runtimes batch endpoint (returns all known
runtimes as { unit_id: UnitRuntime }) and call it in parallel with
the unit list fetch inside loadUnits(), so runtime badges are correct
immediately after page load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-26 09:11:47 +08:00
parent b3f92867bc
commit 42cdbbc0cc
4 changed files with 24 additions and 1 deletions

View File

@ -81,6 +81,10 @@ impl ControlRuntimeStore {
.clone() .clone()
} }
pub async fn get_all(&self) -> HashMap<Uuid, UnitRuntime> {
self.inner.read().await.clone()
}
/// Wake the engine task for a unit (e.g., when auto_enabled or fault_locked changes). /// Wake the engine task for a unit (e.g., when auto_enabled or fault_locked changes).
pub async fn notify_unit(&self, unit_id: Uuid) { pub async fn notify_unit(&self, unit_id: Uuid) {
if let Some(n) = self.notifiers.read().await.get(&unit_id) { if let Some(n) = self.notifiers.read().await.get(&unit_id) {

View File

@ -509,3 +509,11 @@ pub async fn get_unit_runtime(
let runtime = state.control_runtime.get_or_init(unit_id).await; let runtime = state.control_runtime.get_or_init(unit_id).await;
Ok(Json(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,6 +209,10 @@ fn build_router(state: AppState) -> Router {
"/api/unit", "/api/unit",
get(handler::control::get_unit_list).post(handler::control::create_unit), get(handler::control::get_unit_list).post(handler::control::create_unit),
) )
.route(
"/api/unit/runtimes",
get(handler::control::get_all_unit_runtimes),
)
.route( .route(
"/api/unit/{unit_id}", "/api/unit/{unit_id}",
get(handler::control::get_unit) get(handler::control::get_unit)

View File

@ -180,7 +180,10 @@ export function renderUnits() {
} }
export async function loadUnits() { export async function loadUnits() {
const response = await apiFetch("/api/unit?page=1&page_size=-1"); const [response, runtimes] = await Promise.all([
apiFetch("/api/unit?page=1&page_size=-1"),
apiFetch("/api/unit/runtimes").catch(() => ({})),
]);
state.units = response.data || []; state.units = response.data || [];
state.unitMap = new Map(state.units.map((unit) => [unit.id, unit])); state.unitMap = new Map(state.units.map((unit) => [unit.id, unit]));
@ -188,6 +191,10 @@ export async function loadUnits() {
state.selectedUnitId = null; state.selectedUnitId = null;
} }
for (const [unitId, runtime] of Object.entries(runtimes)) {
state.runtimes.set(unitId, runtime);
}
renderUnits(); renderUnits();
renderUnitOptions(dom.equipmentUnitId?.value || "", dom.equipmentUnitId); renderUnitOptions(dom.equipmentUnitId?.value || "", dom.equipmentUnitId);
renderUnitOptions(dom.equipmentBatchUnitId?.value || "", dom.equipmentBatchUnitId); renderUnitOptions(dom.equipmentBatchUnitId?.value || "", dom.equipmentBatchUnitId);