Add web UI form wiring regression tests and preserve dashboard draft state

This commit is contained in:
William Valentin
2026-02-21 22:36:21 -08:00
parent 9707b5a5df
commit 387906ce4d
7 changed files with 1093 additions and 4 deletions
+97 -3
View File
@@ -14,6 +14,8 @@ let _assistantSaveState = null;
let _lastAssistantConfig = null;
let _assistantManualOverrides = new Set();
let _assistantModelDefaultsDraft = null;
let _assistantDraftState = new Map();
let _assistantDraftTouchedAt = 0;
let _lastCouncilTask = '';
let _lastCouncilResult = null;
let _lastCouncilError = null;
@@ -41,6 +43,7 @@ const SERVICE_TOGGLE_PATCH_PATHS = {
audio_transcription: 'audio.enabled',
sandbox: 'sandbox.enabled',
};
const ASSISTANT_DRAFT_TTL_MS = 2 * 60 * 1000;
function formatUptime(seconds) {
const d = Math.floor(seconds / 86400);
@@ -197,6 +200,77 @@ function setAssistantSaveState(message, tone = 'neutral') {
};
}
function writeAssistantDraftValue(control) {
if (!control || !control.id) {return;}
const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
if (isCheckbox) {
_assistantDraftState.set(control.id, { kind: 'checkbox', value: Boolean(control.checked) });
} else if (control.tagName === 'SELECT') {
const selectedOption = Array.from(control.options ?? []).find((option) => option.selected);
_assistantDraftState.set(control.id, { kind: 'value', value: selectedOption?.value ?? '' });
} else {
_assistantDraftState.set(control.id, { kind: 'value', value: control.value ?? '' });
}
_assistantDraftTouchedAt = Date.now();
}
function bindAssistantDraftTracking(rootEl) {
const controls = rootEl.querySelectorAll('input[id], select[id], textarea[id]');
controls.forEach((control) => {
control.addEventListener('input', () => writeAssistantDraftValue(control));
control.addEventListener('change', () => writeAssistantDraftValue(control));
});
}
function applyAssistantDraftState(rootEl) {
if (_assistantDraftState.size === 0) {return;}
const now = Date.now();
if (_assistantDraftTouchedAt > 0 && (now - _assistantDraftTouchedAt) > ASSISTANT_DRAFT_TTL_MS) {
_assistantDraftState = new Map();
_assistantDraftTouchedAt = 0;
return;
}
for (const [id, draft] of _assistantDraftState.entries()) {
const control = rootEl.querySelector(`#${id}`);
if (!control) {continue;}
const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
if (draft.kind === 'checkbox' && isCheckbox) {
control.checked = Boolean(draft.value);
} else if (control.tagName === 'SELECT') {
const options = Array.from(control.options ?? []);
const desired = String(draft.value ?? '');
let matchedIndex = -1;
for (let i = 0; i < options.length; i++) {
const option = options[i];
const isSelected = option.value === desired;
option.selected = isSelected;
if (isSelected) {
matchedIndex = i;
}
}
if (options.length > 0) {
const fallbackIndex = matchedIndex >= 0 ? matchedIndex : 0;
options[fallbackIndex].selected = true;
if ('selectedIndex' in control) {
control.selectedIndex = fallbackIndex;
}
}
try {
control.value = desired;
} catch {
// Some DOM shims expose readonly value on select nodes.
}
} else if ('value' in control) {
try {
control.value = String(draft.value ?? '');
} catch {
// Ignore rare non-writable value surfaces from non-browser DOM shims.
}
}
}
}
function renderAssistantSaveState() {
if (!_assistantSaveState) {
return '<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4">No recent save action.</div>';
@@ -1044,7 +1118,7 @@ function updateAssistantHealth(configData) {
</label>
</div>
<div class="flex flex-wrap gap-2">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-councils">
<button type="button" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-councils">
Save Councils
</button>
</div>
@@ -1053,7 +1127,7 @@ function updateAssistantHealth(configData) {
<div class="text-xs text-zinc-500 mb-3">${escapeHtml(councilSummary)}</div>
<div class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-2 mb-3">
<input id="assist-council-task" type="text" value="${escapeHtml(_lastCouncilTask)}" placeholder="Run councils on demand: e.g. design a 2-week experiment plan..." class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" title="Prompt for an ad-hoc council run; use a concrete decision or planning question." />
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="run-council">
<button type="button" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="run-council">
Run Council
</button>
</div>
@@ -1119,6 +1193,9 @@ function updateAssistantHealth(configData) {
${renderAssistantSaveState()}
`;
// Preserve local unsaved form edits across periodic dashboard refreshes.
applyAssistantDraftState(el);
const updateModelOptions = (inputId, provider) => {
const input = el.querySelector(`#${inputId}`);
const list = el.querySelector(`#${inputId}-list`);
@@ -1145,11 +1222,26 @@ function updateAssistantHealth(configData) {
});
}
// Refresh datalist options after draft re-application in case provider selects were restored.
for (const tier of tierRows) {
const providerSelect = el.querySelector(`#assist-tier-${tier}-provider`);
if (!providerSelect) {continue;}
updateModelOptions(`assist-tier-${tier}-model`, providerSelect.value);
}
for (const task of taskRowsForModels) {
const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
if (!providerSelect) {continue;}
updateModelOptions(`assist-bg-${task}-model`, providerSelect.value);
}
bindAssistantDraftTracking(el);
const statusEl = el.querySelector('#ops-assistant-status');
const buttons = el.querySelectorAll('.assistant-action-btn');
buttons.forEach((button) => {
button.addEventListener('click', async () => {
const action = button.getAttribute('data-action');
const rawAction = button.getAttribute('data-action');
const action = rawAction === 'save-council' ? 'save-councils' : rawAction;
let patches = null;
if (action === 'toggle-announce') {
patches = { 'automation.delivery_mode': announce ? 'shared_session' : 'announce' };
@@ -1746,5 +1838,7 @@ export const DashboardPage = {
_lastAssistantConfig = null;
_assistantManualOverrides = new Set();
_assistantModelDefaultsDraft = null;
_assistantDraftState = new Map();
_assistantDraftTouchedAt = 0;
},
};