github-actions[bot]
chore: sync uc-hospital Space
7596726
import { buildAnalysisBody } from './schedule/analysis-modal.mjs';
import { createAppShell } from './shell/app-shell.mjs';
import { createAppState } from './shell/app-state.mjs';
import { loadAppConfig } from './shell/config-loader.mjs';
import { renderDataTables } from './shell/data-panel.mjs';
import { createSolverController } from './shell/solver-controller.mjs';
import { createViewRegistry } from './views/registry.mjs';
// Browser entrypoint that wires together config loading, the shared UI shell,
// hospital-specific view renderers, and the retained-job controller.
export async function bootApp(root = globalThis) {
const document = root.document;
const sf = root.SF;
const appElement = document && document.getElementById('sf-app');
if (!document || !appElement) return null;
if (!sf) {
throw new Error('SolverForge UI must be loaded before bootApp()');
}
const { config, uiModel, backend, demoId } = await loadAppConfig(root);
const state = createAppState(uiModel.views[0].id);
const statusBar = sf.createStatusBar({ constraints: uiModel.constraints });
const shell = createAppShell({
root,
sf,
appElement,
config,
uiModel,
demoId,
statusBar,
activeTab: state.activeTab,
actions: {
onSolve: () => startSolve(),
onPause: () => controller.pause(),
onResume: () => controller.resume(),
onCancel: () => controller.cancel(),
onAnalyze: () => openAnalysis(),
onTabChange(tabId) {
state.activeTab = tabId;
},
},
});
const views = createViewRegistry();
const controller = createSolverController({
sf,
backend,
statusBar,
onPlan(plan) {
renderAll(plan);
},
onAnalysis() {},
onMeta() {},
onLifecycle(markers) {
shell.syncLifecycleMarkers(markers);
},
onError(error) {
root.console.error('Solver lifecycle failed:', error);
},
});
try {
const demoData = await backend.getDemoData(demoId);
renderAll(demoData);
} catch (error) {
root.console.error('Initial demo load failed:', error);
}
return {
backend,
controller,
shell,
state,
uiModel,
};
// Re-renders both schedule tabs and the raw data tables from the latest plan.
function renderAll(data) {
state.currentPlan = clonePlan(data);
renderViews(data);
renderDataTables({ sf, container: shell.dataRoot, uiModel, data });
}
// Dispatches each configured view to the renderer registered for its `kind`.
function renderViews(data) {
uiModel.views.forEach((view) => {
const container = shell.viewRoots[view.id];
const renderView = views[view.kind];
if (!container) return;
container.innerHTML = '';
if (!renderView) {
container.appendChild(sf.el('p', null, `No renderer is registered for ${view.kind}.`));
return;
}
renderView({ sf, container, data, view });
});
}
// Starts solving from the last rendered plan snapshot.
async function startSolve() {
const plan = await resolvePlanForSolve();
await controller.start(() => Promise.resolve(clonePlan(plan)));
}
// Lazily loads demo data if the user clicks Solve before the first fetch finishes.
async function resolvePlanForSolve() {
if (state.currentPlan) {
return state.currentPlan;
}
const demoData = await backend.getDemoData(demoId);
renderAll(demoData);
return state.currentPlan;
}
// Fetches exact retained-snapshot analysis and opens it in the shared modal.
async function openAnalysis() {
if (!controller.getJobId()) return Promise.resolve();
try {
const currentAnalysis = await controller.analyzeSnapshot();
shell.openAnalysis(buildAnalysisBody(document, currentAnalysis, uiModel.constraints));
} catch (error) {
root.console.error('Analysis failed:', error);
}
}
}
// Defensive deep clone so the UI never mutates the last backend payload in place.
function clonePlan(data) {
return JSON.parse(JSON.stringify(data));
}