diff --git a/.gitignore b/.gitignore
index 755d3df..b383ded 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@ hola-py/benchmark_results/
CLAUDE.md
.claude/
paper/
+/issues/
# OS
.DS_Store
diff --git a/Cargo.lock b/Cargo.lock
index a1745f5..952749b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -112,7 +112,6 @@ dependencies = [
"tower",
"tower-layer",
"tower-service",
- "tracing",
]
[[package]]
@@ -131,7 +130,6 @@ dependencies = [
"sync_wrapper",
"tower-layer",
"tower-service",
- "tracing",
]
[[package]]
@@ -841,15 +839,6 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
-[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
[[package]]
name = "log"
version = "0.4.29"
@@ -1034,29 +1023,6 @@ dependencies = [
"tokio",
]
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
[[package]]
name = "paste"
version = "1.0.15"
@@ -1287,15 +1253,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
[[package]]
name = "reqwest"
version = "0.12.28"
@@ -1423,12 +1380,6 @@ dependencies = [
"bytemuck",
]
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
[[package]]
name = "serde"
version = "1.0.228"
@@ -1514,16 +1465,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
-dependencies = [
- "errno",
- "libc",
-]
-
[[package]]
name = "simba"
version = "0.9.1"
@@ -1687,9 +1628,7 @@ dependencies = [
"bytes",
"libc",
"mio",
- "parking_lot",
"pin-project-lite",
- "signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
@@ -1754,7 +1693,6 @@ dependencies = [
"tokio",
"tower-layer",
"tower-service",
- "tracing",
]
[[package]]
@@ -1803,7 +1741,6 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
- "log",
"pin-project-lite",
"tracing-core",
]
diff --git a/dashboard/app.js b/dashboard/app.js
index 9a0f343..b0b4a8e 100644
--- a/dashboard/app.js
+++ b/dashboard/app.js
@@ -33,6 +33,26 @@ const S = {
// ============================================================================
// Connection
// ============================================================================
+function apiToken() {
+ const urlToken = new URLSearchParams(window.location.search).get('token');
+ if (urlToken) {
+ localStorage.setItem('hola_api_token', urlToken);
+ return urlToken;
+ }
+ return localStorage.getItem('hola_api_token') || '';
+}
+
+function apiFetch(url, options = {}) {
+ const headers = new Headers(options.headers || {});
+ const token = apiToken();
+ if (token) headers.set('Authorization', `Bearer ${token}`);
+ return fetch(url, { ...options, headers });
+}
+
+function clearElement(el) {
+ el.replaceChildren();
+}
+
async function connectToServer() {
const url = document.getElementById('server-url').value.trim().replace(/\/+$/, '')
|| 'http://localhost:8000';
@@ -74,9 +94,9 @@ function startSSE() {
S.sse.onmessage = async (e) => {
const event = JSON.parse(e.data);
if (event.type === 'TrialCompleted') {
- // Re-fetch trials
- const resp = await fetch(`${S.serverUrl}/api/trials?sorted_by=index&include_infeasible=true`);
- S.trials = await resp.json();
+ const trial = event.trial || await fetchCompletedTrial(event.trial_id);
+ if (!trial) return;
+ upsertTrial(trial);
discoverMetrics();
S.lastTrialTime = Date.now();
if (S.previewActive) previewObjectives(); else renderAll();
@@ -84,6 +104,18 @@ function startSSE() {
};
}
+async function fetchCompletedTrial(trialId) {
+ const resp = await fetch(`${S.serverUrl}/api/trial/${trialId}?include_infeasible=true`);
+ if (!resp.ok) return null;
+ return resp.json();
+}
+
+function upsertTrial(trial) {
+ const idx = S.trials.findIndex(t => t.trial_id === trial.trial_id);
+ if (idx >= 0) S.trials[idx] = trial;
+ else S.trials.push(trial);
+}
+
function loadCheckpointFile(event) {
const file = event.target.files[0];
if (!file) return;
@@ -253,7 +285,7 @@ function renderConvergence() {
const h = 280;
// Clear any previous uPlot DOM content so it doesn't accumulate
- container.innerHTML = '';
+ clearElement(container);
const xs = S.trials.map((_, i) => i);
// Convert NaN to null so uPlot treats them as gaps instead of broken values
@@ -339,11 +371,11 @@ function renderParetoDropdowns() {
const allFields = [...S.metricNames];
const prevX = xSel.value;
const prevY = ySel.value;
- xSel.innerHTML = '';
- ySel.innerHTML = '';
+ clearElement(xSel);
+ clearElement(ySel);
for (const f of allFields) {
- xSel.innerHTML += ``;
- ySel.innerHTML += ``;
+ xSel.add(new Option(f, f, false, f === prevX));
+ ySel.add(new Option(f, f, false, f === prevY));
}
if (!prevX && allFields.length >= 2) {
xSel.value = allFields[0];
@@ -517,13 +549,22 @@ function attachParetoTooltip() {
}
if (nearest && nearestDist <= 10) {
- tooltip.innerHTML =
- `Trial ${nearest.trial.trial_id}
` +
- `${nearest.xField}: ${fmtCell(nearest.xVal)}
` +
- `${nearest.yField}: ${fmtCell(nearest.yVal)}
` +
- (nearest.onFront
- ? 'Pareto front'
- : 'Dominated');
+ clearElement(tooltip);
+
+ const title = document.createElement('strong');
+ title.textContent = `Trial ${fmtCell(nearest.trial.trial_id)}`;
+
+ const xLine = document.createElement('div');
+ xLine.textContent = `${nearest.xField}: ${fmtCell(nearest.xVal)}`;
+
+ const yLine = document.createElement('div');
+ yLine.textContent = `${nearest.yField}: ${fmtCell(nearest.yVal)}`;
+
+ const status = document.createElement('span');
+ status.textContent = nearest.onFront ? 'Pareto front' : 'Dominated';
+ status.style.color = nearest.onFront ? 'var(--accent-bright)' : 'var(--text-2)';
+
+ tooltip.append(title, xLine, yLine, status);
// Position tooltip near cursor but keep it inside the container
const container = canvas.parentElement;
const cw = container.clientWidth;
@@ -682,10 +723,14 @@ function renderTable() {
// Build columns
const cols = ['trial_id', 'rank', ...S.paramNames, ...S.metricNames];
- thead.innerHTML = cols.map(c => {
- const cls = S.sortCol === c ? (S.sortAsc ? 'sorted-asc' : 'sorted-desc') : '';
- return `
${c} | `;
- }).join('');
+ clearElement(thead);
+ for (const c of cols) {
+ const th = document.createElement('th');
+ th.textContent = c;
+ if (S.sortCol === c) th.className = S.sortAsc ? 'sorted-asc' : 'sorted-desc';
+ th.addEventListener('click', () => sortTable(c));
+ thead.appendChild(th);
+ }
// Sort trials
let sorted = [...S.trials];
@@ -697,12 +742,18 @@ function renderTable() {
});
}
- tbody.innerHTML = sorted.map(t => {
+ clearElement(tbody);
+ for (const t of sorted) {
const isBest = t.trial_id === (S.bestIdx >= 0 ? S.trials[S.bestIdx].trial_id : -1);
- return `` +
- cols.map(c => `| ${fmtCell(getCellValue(t, c))} | `).join('') +
- '
';
- }).join('');
+ const tr = document.createElement('tr');
+ if (isBest) tr.className = 'best-row';
+ for (const c of cols) {
+ const td = document.createElement('td');
+ td.textContent = fmtCell(getCellValue(t, c));
+ tr.appendChild(td);
+ }
+ tbody.appendChild(tr);
+ }
}
function getCellValue(trial, col) {
@@ -733,33 +784,84 @@ function sortTable(col) {
// ============================================================================
function renderObjectives() {
const container = document.getElementById('objectives-list');
+ clearElement(container);
if (S.objectives.length === 0) {
- container.innerHTML = 'No objectives configured
';
+ const empty = document.createElement('div');
+ empty.style.color = 'var(--text-2)';
+ empty.style.fontSize = '0.82rem';
+ empty.style.padding = '8px 0';
+ empty.textContent = 'No objectives configured';
+ container.appendChild(empty);
return;
}
- container.innerHTML = S.objectives.map((obj, i) => `
-
- ${obj.field}
- ${obj.obj_type || obj.type || 'minimize'}
-
-
-
-
-
- `).join('');
+ S.objectives.forEach((obj, i) => {
+ const row = document.createElement('div');
+ row.className = 'objective-row';
+
+ const field = document.createElement('span');
+ field.className = 'obj-field';
+ field.textContent = obj.field ?? '';
+
+ const type = document.createElement('span');
+ type.className = 'obj-type';
+ type.textContent = obj.obj_type || obj.type || 'minimize';
+
+ const priorityLabel = makeObjectiveLabel('Priority');
+ const priority = document.createElement('input');
+ priority.type = 'range';
+ priority.min = '0';
+ priority.max = '5';
+ priority.step = '0.1';
+ priority.value = obj.priority ?? 1;
+ const priorityValue = document.createElement('span');
+ priorityValue.className = 'obj-priority-value';
+ priorityValue.textContent = priority.value;
+ priority.addEventListener('input', () => {
+ S.objectives[i].priority = parseFloat(priority.value);
+ priorityValue.textContent = priority.value;
+ });
+ priorityLabel.append(priority, priorityValue);
+
+ const targetLabel = makeObjectiveLabel('Target');
+ const target = document.createElement('input');
+ target.type = 'number';
+ target.step = 'any';
+ target.value = obj.target ?? '';
+ target.addEventListener('change', () => {
+ S.objectives[i].target = target.value ? parseFloat(target.value) : null;
+ });
+ targetLabel.appendChild(target);
+
+ const limitLabel = makeObjectiveLabel('Limit');
+ const limit = document.createElement('input');
+ limit.type = 'number';
+ limit.step = 'any';
+ limit.value = obj.limit ?? '';
+ limit.addEventListener('change', () => {
+ S.objectives[i].limit = limit.value ? parseFloat(limit.value) : null;
+ });
+ limitLabel.appendChild(limit);
+
+ const groupLabel = makeObjectiveLabel('Group');
+ const group = document.createElement('input');
+ group.type = 'text';
+ group.className = 'obj-group-input';
+ group.value = obj.group ?? '';
+ group.addEventListener('change', () => {
+ S.objectives[i].group = group.value || null;
+ });
+ groupLabel.appendChild(group);
+
+ row.append(field, type, priorityLabel, targetLabel, limitLabel, groupLabel);
+ container.appendChild(row);
+ });
+}
+
+function makeObjectiveLabel(text) {
+ const label = document.createElement('label');
+ label.className = 'objective-label';
+ label.append(document.createTextNode(text));
+ return label;
}
// Client-side TLP rescalarization for preview mode.
@@ -813,7 +915,7 @@ async function applyObjectives() {
if (S.mode !== 'live') return;
if (!confirm('This will update the server objectives and rescalarize all trials. The server will use these objectives for future sampling. Continue?')) return;
try {
- const resp = await fetch(`${S.serverUrl}/api/objectives`, {
+ const resp = await apiFetch(`${S.serverUrl}/api/objectives`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ objectives: S.objectives }),
@@ -837,13 +939,16 @@ async function applyObjectives() {
async function saveCheckpoint() {
if (S.mode !== 'live') return;
try {
- const resp = await fetch(`${S.serverUrl}/api/checkpoint/save`, {
+ const resp = await apiFetch(`${S.serverUrl}/api/checkpoint/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: `Dashboard save at ${new Date().toISOString()}` }),
});
const data = await resp.json();
- if (resp.ok) alert(`Checkpoint saved: ${data.path} (${data.trials_saved} trials)`);
+ if (resp.ok) {
+ const kind = data.checkpoint_type ? `${data.checkpoint_type} checkpoint` : 'checkpoint';
+ alert(`Saved ${kind}: ${data.path} (${data.trials_saved} trials)`);
+ }
else alert('Save failed: ' + (data.error || 'unknown'));
} catch (e) {
alert('Save failed: ' + e.message);
diff --git a/dashboard/styles.css b/dashboard/styles.css
index edbc259..6badb94 100644
--- a/dashboard/styles.css
+++ b/dashboard/styles.css
@@ -309,6 +309,26 @@ tr.best-row {
background: var(--bg-3);
border-radius: 4px;
}
+.objective-label {
+ font-size: 0.75rem;
+ color: var(--text-2);
+}
+.obj-priority-value {
+ font-family: var(--mono);
+ color: var(--text-1);
+ min-width: 30px;
+ display: inline-block;
+}
+.obj-group-input {
+ width: 80px;
+ padding: 4px 8px;
+ background: var(--bg-2);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-0);
+ font-family: var(--mono);
+ font-size: 0.78rem;
+}
input[type="range"] {
-webkit-appearance: none;
width: 120px;
diff --git a/dashboard/xss-smoke-checkpoint.json b/dashboard/xss-smoke-checkpoint.json
new file mode 100644
index 0000000..52a8a80
--- /dev/null
+++ b/dashboard/xss-smoke-checkpoint.json
@@ -0,0 +1,22 @@
+{
+ "leaderboard": {
+ "trials": [
+ {
+ "trial_id": 0,
+ "candidate": {
+ "
": "