(async function () {
'use strict';
var config = await fetch('/sf-config.json').then(function (r) { return r.json(); });
var app = document.getElementById('sf-app');
var currentPlan = null;
var backend = SF.createBackend({ baseUrl: '' });
var statusBar = SF.createStatusBar({ constraints: config.constraints });
var solver = SF.createSolver({
backend: backend,
statusBar: statusBar,
onSolution: function (snapshot) { renderAll(snapshot && snapshot.solution ? snapshot.solution : null); },
onPaused: function (snapshot) { renderAll(snapshot && snapshot.solution ? snapshot.solution : null); },
onCancelled: function (snapshot) { renderAll(snapshot && snapshot.solution ? snapshot.solution : null); },
onComplete: function (snapshot) { renderAll(snapshot && snapshot.solution ? snapshot.solution : null); },
onError: function (message) { console.error('Solver lifecycle failed:', message); },
});
var header = SF.createHeader({
logo: '/sf/img/ouroboros.svg',
title: config.title,
subtitle: config.subtitle,
tabs: [
{ id: 'sequences', label: 'Sequences', icon: 'fa-list-ol', active: true },
{ id: 'data', label: 'Data', icon: 'fa-table' },
{ id: 'api', label: 'REST API', icon: 'fa-book' },
],
actions: {
onSolve: function () { loadAndSolve(); },
onPause: function () { solver.pause().catch(function (err) { console.error('Pause failed:', err); }); },
onResume: function () { solver.resume().catch(function (err) { console.error('Resume failed:', err); }); },
onCancel: function () { solver.cancel().catch(function (err) { console.error('Cancel failed:', err); }); },
onAnalyze: function () { openAnalysis(); },
},
onTabChange: function (tab) {
sequencesPanel.style.display = tab === 'sequences' ? '' : 'none';
dataPanel.style.display = tab === 'data' ? '' : 'none';
apiPanel.style.display = tab === 'api' ? '' : 'none';
},
});
app.appendChild(header);
statusBar.bindHeader(header);
app.appendChild(statusBar.el);
var sequencesPanel = SF.el('div', { className: 'sf-content' });
var sequencesContainer = SF.el('div', { id: 'sf-sequences' });
sequencesPanel.appendChild(sequencesContainer);
app.appendChild(sequencesPanel);
var dataPanel = SF.el('div', { className: 'sf-content', style: { display: 'none' } });
var tablesContainer = SF.el('div', { id: 'sf-tables' });
dataPanel.appendChild(tablesContainer);
app.appendChild(dataPanel);
var apiPanel = SF.el('div', { className: 'sf-content', style: { display: 'none' } });
var guide = SF.createApiGuide({
endpoints: [
{ method: 'GET', path: '/demo-data/STANDARD', description: 'Fetch demo data', curl: 'curl http://localhost:7860/demo-data/STANDARD' },
{ method: 'POST', path: '/jobs', description: 'Create a retained solving job', curl: 'curl -X POST -H "Content-Type: application/json" http://localhost:7860/jobs -d @plan.json' },
{ method: 'GET', path: '/jobs/{id}', description: 'Get current job summary', curl: 'curl http://localhost:7860/jobs/{id}' },
{ method: 'GET', path: '/jobs/{id}/snapshot', description: 'Fetch the latest retained snapshot', curl: 'curl http://localhost:7860/jobs/{id}/snapshot' },
{ method: 'GET', path: '/jobs/{id}/analysis?snapshot_revision={n}', description: 'Analyze an exact snapshot revision', curl: 'curl "http://localhost:7860/jobs/{id}/analysis?snapshot_revision=3"' },
{ method: 'POST', path: '/jobs/{id}/pause', description: 'Request an exact runtime pause', curl: 'curl -X POST http://localhost:7860/jobs/{id}/pause' },
{ method: 'POST', path: '/jobs/{id}/resume', description: 'Resume a paused retained job', curl: 'curl -X POST http://localhost:7860/jobs/{id}/resume' },
{ method: 'POST', path: '/jobs/{id}/cancel', description: 'Cancel a live or paused job', curl: 'curl -X POST http://localhost:7860/jobs/{id}/cancel' },
{ method: 'DELETE', path: '/jobs/{id}', description: 'Delete a terminal retained job', curl: 'curl -X DELETE http://localhost:7860/jobs/{id}' },
{ method: 'GET', path: '/jobs/{id}/events', description: 'Stream job lifecycle updates (SSE)', curl: 'curl -N http://localhost:7860/jobs/{id}/events' },
],
});
apiPanel.appendChild(guide);
app.appendChild(apiPanel);
var footer = SF.createFooter({
links: [
{ label: 'SolverForge', url: 'https://www.solverforge.org' },
{ label: 'Docs', url: 'https://www.solverforge.org/docs' },
],
});
app.appendChild(footer);
var analysisModal = SF.createModal({ title: 'Score Analysis', width: '700px' });
fetch('/demo-data/STANDARD')
.then(function (r) { return r.json(); })
.then(function (data) { renderAll(data); })
.catch(function () {});
function loadAndSolve() {
cleanupTerminalJob()
.then(function () {
if (currentPlan) return currentPlan;
return fetch('/demo-data/STANDARD').then(function (r) { return r.json(); });
})
.then(function (data) {
return solver.start(clonePlan(data));
})
.catch(function (err) { console.error('Demo load failed:', err); });
}
function cleanupTerminalJob() {
var state = solver.getLifecycleState();
if (!solver.getJobId() || state === 'IDLE' || state === 'PAUSED' || solver.isRunning()) {
return Promise.resolve();
}
return solver.delete().catch(function (err) {
console.error('Delete failed:', err);
});
}
function openAnalysis() {
var id = solver.getJobId();
if (!id) return;
solver.analyzeSnapshot()
.then(function (analysis) {
analysisModal.setBody(buildAnalysisHtml(analysis));
analysisModal.open();
})
.catch(function () {});
}
function buildAnalysisHtml(analysis) {
if (!analysis || !analysis.constraints) return '<p>No analysis available.</p>';
var html = '<p><strong>Score:</strong> ' + SF.escHtml(analysis.score) + '</p>';
html += '<table class="sf-table"><thead><tr><th>Constraint</th><th>Type</th><th>Score</th><th>Matches</th></tr></thead><tbody>';
analysis.constraints.forEach(function (c) {
var matchCount = c.matchCount != null ? c.matchCount : (c.matches ? c.matches.length : 0);
html += '<tr><td>' + SF.escHtml(c.name) + '</td><td>' + SF.escHtml(c.constraintType || c.type || '') + '</td><td>' + SF.escHtml(c.score) + '</td><td>' + matchCount + '</td></tr>';
});
html += '</tbody></table>';
return html;
}
function renderAll(data) {
if (!data) return;
currentPlan = clonePlan(data);
renderSequences(data);
renderTables(data);
}
function clonePlan(data) {
return JSON.parse(JSON.stringify(data));
}
function renderSequences(data) {
sequencesContainer.innerHTML = '';
var containers = data.containers || [];
if (!containers.length) return;
var itemsById = buildItemsById(data);
var metrics = deriveSequenceMetrics(containers);
var sortedContainers = containers.slice().sort(compareContainers);
var horizon = Math.max(metrics.longestSequence, 1);
sequencesContainer.appendChild(buildSequenceOverview(metrics));
sequencesContainer.appendChild(SF.rail.createHeader({
label: config.entities[0] ? config.entities[0].label : 'Container',
labelWidth: 220,
columns: Array.from({ length: horizon }, function (_, i) { return String(i + 1); }),
}));
sortedContainers.forEach(function (container) {
sequencesContainer.appendChild(buildSequenceCard(container, itemsById, metrics, horizon).el);
});
}
function buildItemsById(data) {
var items = data.items || data.itemFacts || data.item_facts || [];
return items.reduce(function (map, item) {
if (item && item.id) map[item.id] = item;
return map;
}, {});
}
function deriveSequenceMetrics(containers) {
var lengths = containers.map(function (container) {
return (container.items || []).length;
});
var totalItems = lengths.reduce(function (sum, count) { return sum + count; }, 0);
var longestSequence = lengths.reduce(function (maxCount, count) {
return Math.max(maxCount, count);
}, 0);
var emptyContainers = lengths.filter(function (count) { return count === 0; }).length;
return {
totalContainers: containers.length,
totalItems: totalItems,
longestSequence: longestSequence,
emptyContainers: emptyContainers,
averageItems: containers.length ? (totalItems / containers.length).toFixed(1) : '0.0',
};
}
function compareContainers(a, b) {
var aCount = (a.items || []).length;
var bCount = (b.items || []).length;
if (bCount !== aCount) return bCount - aCount;
return String(a.name || '').localeCompare(String(b.name || ''));
}
function buildSequenceOverview(metrics) {
var section = SF.el('div', { className: 'sf-section' });
section.appendChild(SF.createTable({
columns: ['Containers', 'Items', 'Longest sequence', 'Empty containers', 'Average items / container'],
rows: [[
String(metrics.totalContainers),
String(metrics.totalItems),
String(metrics.longestSequence),
String(metrics.emptyContainers),
String(metrics.averageItems),
]],
}));
return section;
}
function buildSequenceCard(container, itemsById, metrics, horizon) {
var sequence = container.items || [];
var firstItem = sequence.length ? describeItem(sequence[0], itemsById).name : '—';
var lastItem = sequence.length ? describeItem(sequence[sequence.length - 1], itemsById).name : '—';
var length = sequence.length;
var fullnessPct = metrics.longestSequence > 0
? Math.round((length / metrics.longestSequence) * 100)
: 0;
var card = SF.rail.createCard({
id: 'container-' + String(container.id != null ? container.id : container.name),
name: container.name || 'Unnamed container',
labelWidth: 220,
columns: horizon,
type: 'Sequence',
badges: containerBadges(length, metrics.longestSequence),
gauges: [
{
label: 'Length',
pct: Math.min(fullnessPct, 100),
style: length === 0 ? 'heat' : 'load',
text: String(length) + '/' + String(Math.max(metrics.longestSequence, 1)),
},
],
stats: [
{ label: 'Items', value: length },
{ label: 'First', value: firstItem },
{ label: 'Last', value: lastItem },
],
});
sequence.forEach(function (itemId, index) {
var item = describeItem(itemId, itemsById);
card.addBlock({
id: 'container-' + String(container.id || container.name) + '-item-' + String(index),
label: item.name,
meta: 'Pos ' + String(index + 1),
start: index,
end: index + 1,
horizon: horizon,
color: SF.colors.pick(String(item.key)),
});
});
return card;
}
function containerBadges(length, longestSequence) {
if (length === 0) return ['Empty'];
var badges = [];
if (length === longestSequence) badges.push('Longest');
if (length === 1) badges.push('Single');
return badges;
}
function describeItem(itemId, itemsById) {
var item = itemsById[itemId];
if (!item) {
return { key: itemId || 'item', name: itemId || 'Unnamed' };
}
return {
key: item.id || itemId || 'item',
name: item.name || item.id || itemId || 'Unnamed',
};
}
function renderTables(data) {
tablesContainer.innerHTML = '';
config.entities.forEach(function (entity) {
var items = data[entity.plural] || data[entity.name + 's'] || [];
if (!items.length) return;
var cols = Object.keys(items[0]);
var rows = items.map(function (item) {
return cols.map(function (k) {
var v = item[k];
if (v === null || v === undefined) return '—';
if (Array.isArray(v)) return v.join(', ');
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
});
});
var section = SF.el('div', { className: 'sf-section' });
section.appendChild(SF.el('h3', null, entity.label));
section.appendChild(SF.createTable({ columns: cols, rows: rows }));
tablesContainer.appendChild(section);
});
config.facts.forEach(function (fact) {
var items = data[fact.plural] || data[fact.name + 's'] || [];
if (!items.length) return;
var cols = Object.keys(items[0]);
var rows = items.map(function (item) {
return cols.map(function (k) {
var v = item[k];
if (v === null || v === undefined) return '—';
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
});
});
var section = SF.el('div', { className: 'sf-section' });
section.appendChild(SF.el('h3', null, fact.label));
section.appendChild(SF.createTable({ columns: cols, rows: rows }));
tablesContainer.appendChild(section);
});
}
})();