import { clonePlan, refreshPlan, refreshServerPlan, } from './models.mjs'; import { fetchJson, showError } from './ui/components.mjs'; import { renderData } from './ui/data-tables.mjs'; import { createLayout } from './ui/layout.mjs'; import { sameRouteIdentity, syncLifecycleDataset } from './ui/lifecycle.mjs'; import { buildAnalysisBody, buildRecommendationBody } from './ui/modals.mjs'; import { renderMap, renderRouteList, renderSummary, renderTimelines } from './ui/overview.mjs'; const DEFAULT_DEMO = 'PHILADELPHIA'; export async function boot() { const config = await fetchJson('/sf-config.json'); const backend = SF.createBackend({ baseUrl: '' }); const app = document.getElementById('sf-app'); let currentPlan = null; let currentRoutes = null; let currentDemo = DEFAULT_DEMO; let mapCtrl = null; let focusedVehicleId = null; let routeRequestToken = 0; // Route geometry is only trustworthy for one retained job, snapshot revision, // and routing mode. This identity prevents stale map lines from surviving // dataset switches, routing-mode changes, or newer solver snapshots. let activeRouteIdentity = null; let activeTab = 'overview'; let mapLocationSignature = null; const statusBar = SF.createStatusBar({ constraints: [ 'all_deliveries_assigned', 'vehicle_capacity', 'delivery_time_windows', 'total_travel_time', ], }); const layout = createLayout({ app, config, statusBar, actions: { onSolve: () => loadAndSolve(), onPause: () => solver.pause().catch(showError), onResume: () => solver.resume().catch(showError), onCancel: () => solver.cancel().catch(showError), onAnalyze: () => openAnalysis(), }, onTabChange: (tabId) => setActiveTab(tabId), }); const solver = SF.createSolver({ backend, statusBar, onProgress: syncLifecycle, onPauseRequested: syncLifecycle, onSolution: async (snapshot, meta) => handleSnapshotEvent(snapshot, meta), onPaused: async (snapshot, meta) => handleSnapshotEvent(snapshot, meta), onResumed: syncLifecycle, onCancelled: async (snapshot, meta) => handleSnapshotEvent(snapshot, meta), onComplete: async (snapshot, meta) => handleSnapshotEvent(snapshot, meta), onFailure: async (_message, meta, snapshot, analysis) => { await handleSnapshotEvent(snapshot, meta); if (analysis) layout.analysisModal.setBody(buildAnalysisBody(analysis)); }, onAnalysis: (analysis) => layout.analysisModal.setBody(buildAnalysisBody(analysis)), onError: showError, }); layout.reloadButton.addEventListener('click', async () => loadDemoData(layout.demoField.select.value)); layout.demoField.select.addEventListener('change', async () => loadDemoData(layout.demoField.select.value)); layout.routingField.select.addEventListener('change', () => { if (!currentPlan) return; invalidateRoutes(); currentPlan.routingMode = layout.routingField.select.value; renderDraftPlan(currentPlan); }); setActiveTab(activeTab); await loadDemoData(DEFAULT_DEMO); function setActiveTab(tabId) { activeTab = tabId; Object.entries(layout.panels).forEach(([id, panel]) => { panel.classList.toggle('is-active', id === tabId); }); } async function loadDemoData(demoId) { currentDemo = demoId; invalidateRoutes(); if (currentPlan) { renderMapView(); renderRouteListView(); } const plan = await fetchJson(`/demo-data/${demoId}`); plan.routingMode = plan.routingMode || layout.routingField.select.value; renderServerPlan(plan); } async function loadAndSolve() { if (!currentPlan) return; invalidateRoutes(); renderDraftPlan({ ...currentPlan, routingMode: layout.routingField.select.value }); await cleanupTerminalJob(); await solver.start(clonePlan(currentPlan)); syncLifecycle(); } async function cleanupTerminalJob() { const state = solver.getLifecycleState(); if (!solver.getJobId() || state === 'IDLE' || state === 'PAUSED' || solver.isRunning()) return; try { await solver.delete(); } catch (_error) { // Retained terminal cleanup is opportunistic before starting a new job. } } async function handleSnapshotEvent(snapshot, meta) { if (snapshot?.solution) { renderServerPlan(snapshot.solution, meta); } await loadRoutes(meta); syncLifecycle(meta); } async function loadRoutes(meta) { const requested = routeIdentityFrom(meta); if (!requested || !activeRouteIdentity || !sameRouteIdentity(requested, activeRouteIdentity)) return; const token = ++routeRequestToken; try { const routes = await fetchJson(`/jobs/${requested.jobId}/routes?snapshot_revision=${requested.snapshotRevision}`); const routingMode = routes.routingMode || requested.routingMode; if (!routeResponseStillCurrent(token, requested, routingMode)) return; currentRoutes = { ...routes, jobId: requested.jobId, snapshotRevision: requested.snapshotRevision, routingMode }; renderMapView(); renderRouteListView(); } catch (_error) { if (token === routeRequestToken && activeRouteIdentity && sameRouteIdentity(requested, activeRouteIdentity)) { currentRoutes = null; renderMapView(); renderRouteListView(); } } } function routeResponseStillCurrent(token, requested, routingMode) { return ( token === routeRequestToken && activeRouteIdentity && sameRouteIdentity(requested, activeRouteIdentity) && currentPlan?.routingMode === requested.routingMode && routingMode === requested.routingMode ); } async function openAnalysis() { if (!solver.getJobId()) return; const analysis = await solver.analyzeSnapshot(); layout.analysisModal.setBody(buildAnalysisBody(analysis)); layout.analysisModal.open(); } async function openRecommendations(deliveryId) { if (!currentPlan) return; const response = await fetch('/recommendations/delivery-insertions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ plan: currentPlan, deliveryId, limit: 8 }), }); if (!response.ok) throw new Error('Failed to load recommendations'); const payload = await response.json(); layout.recommendationModal.setBody(buildRecommendationBody(payload, (previewPlan) => { renderServerPlan(previewPlan); layout.recommendationModal.close(); })); layout.recommendationModal.open(); } function renderDraftPlan(plan) { renderPlan(plan, refreshPlan); } function renderServerPlan(plan, meta = null) { invalidateRoutes(); renderPlan(plan, refreshServerPlan); // A solver snapshot is the only source that can authorize `/routes` reads. activeRouteIdentity = meta ? routeIdentityFrom(meta) : null; } function renderPlan(plan, refresh) { const nextMapLocationSignature = locationSignature(plan); const shouldFitMap = nextMapLocationSignature !== mapLocationSignature; currentPlan = refresh({ ...plan, routingMode: plan.routingMode || layout.routingField.select.value }); mapLocationSignature = nextMapLocationSignature; layout.routingField.select.value = currentPlan.routingMode || 'road_network'; renderSummaryView(); renderRouteListView(); renderMapView({ fitBounds: shouldFitMap }); renderTimelinesView(); renderDataView(); } function invalidateRoutes() { routeRequestToken += 1; currentRoutes = null; focusedVehicleId = null; // Route tables and map geometry must be reloaded for the next snapshot. activeRouteIdentity = null; } function routeIdentityFrom(meta) { const jobId = meta?.jobId != null ? String(meta.jobId) : solver.getJobId(); const snapshotRevision = meta?.snapshotRevision != null ? meta.snapshotRevision : solver.getSnapshotRevision(); const routingMode = currentPlan?.routingMode || null; if (!jobId || snapshotRevision == null || !routingMode) return null; return { jobId: String(jobId), snapshotRevision: String(snapshotRevision), routingMode }; } function renderSummaryView() { renderSummary({ currentDemo, currentPlan, summaryCard: layout.summaryCard, summaryMetrics: layout.summaryMetrics }); } function renderRouteListView() { renderRouteList({ currentPlan, focusedVehicleId, routeList: layout.routeList, onFocusVehicle: (vehicleId) => { focusedVehicleId = focusedVehicleId === vehicleId ? null : vehicleId; renderMapView(); renderRouteListView(); }, }); } function renderMapView({ fitBounds = false } = {}) { renderMap({ currentPlan, currentRoutes, focusedVehicleId, fitBounds, mapCtrl, setMapCtrl: (next) => { mapCtrl = next; }, }); } function renderTimelinesView() { renderTimelines({ currentPlan, vehicleTimeline: layout.vehicleTimeline, deliveryTimeline: layout.deliveryTimeline, }); } function renderDataView() { renderData({ currentPlan, dataBody: layout.dataBody, onRecommend: async (deliveryId) => { try { await openRecommendations(deliveryId); } catch (error) { showError(error); } }, }); } function syncLifecycle(meta) { syncLifecycleDataset(app, solver, meta); } function locationSignature(plan) { const deliveries = (plan.deliveries || []).map((delivery) => `${delivery.id}:${delivery.lat}:${delivery.lng}`); const vehicles = (plan.vehicles || []).map((vehicle) => `${vehicle.id}:${vehicle.homeLat}:${vehicle.homeLng}`); return `${deliveries.join('|')}::${vehicles.join('|')}`; } }