Add web UI form wiring regression tests and preserve dashboard draft state
This commit is contained in:
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user