fix(dashboard): preserve unsaved model tier selections across refresh

This commit is contained in:
William Valentin
2026-02-19 10:29:36 -08:00
parent 708683297a
commit 4e40878ad5
2 changed files with 72 additions and 13 deletions
+11
View File
@@ -5710,6 +5710,17 @@
"docs/plans/state.json" "docs/plans/state.json"
], ],
"test_status": "pnpm test:run src/gateway/handlers/handlers.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/gateway/handlers/handlers.test.ts + pnpm typecheck passing"
},
"dashboard-model-defaults-draft-preservation": {
"status": "completed",
"date": "2026-02-19",
"updated": "2026-02-19",
"summary": "Fixed Model Tier Defaults dropdown reset during periodic dashboard refresh by preserving unsaved form draft state (tiers, delegation, and background override settings) across Assistant Health rerenders until saved.",
"files_modified": [
"src/gateway/ui/pages/dashboard.js",
"docs/plans/state.json"
],
"test_status": "pnpm typecheck passing"
} }
}, },
"overall_progress": { "overall_progress": {
+61 -13
View File
@@ -13,6 +13,10 @@ let _lastBriefingTestAt = null;
let _assistantSaveState = null; let _assistantSaveState = null;
let _lastAssistantConfig = null; let _lastAssistantConfig = null;
let _assistantManualOverrides = new Set(); let _assistantManualOverrides = new Set();
let _assistantModelDefaultsDraft = null;
const MODEL_DEFAULT_TASK_KEYS = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning'];
const MODEL_DEFAULT_TIER_KEYS = ['default', 'fast', 'complex', 'local'];
function formatUptime(seconds) { function formatUptime(seconds) {
const d = Math.floor(seconds / 86400); const d = Math.floor(seconds / 86400);
@@ -666,6 +670,7 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
function updateAssistantHealth(configData) { function updateAssistantHealth(configData) {
const el = document.getElementById('ops-assistant-health'); const el = document.getElementById('ops-assistant-health');
if (!el) {return;} if (!el) {return;}
_assistantModelDefaultsDraft = readAssistantModelDefaultsDraft(el) ?? _assistantModelDefaultsDraft;
const snapshot = getAssistantStateSnapshot(configData); const snapshot = getAssistantStateSnapshot(configData);
@@ -692,7 +697,7 @@ function updateAssistantHealth(configData) {
: 'not configured'; : 'not configured';
const briefingReady = dailyBriefing && Boolean(briefingOutput?.channel && briefingOutput?.peer); const briefingReady = dailyBriefing && Boolean(briefingOutput?.channel && briefingOutput?.peer);
const playbookLikeReady = announce || (memoryDaily && memoryProactive); const playbookLikeReady = announce || (memoryDaily && memoryProactive);
const modelTier = configData?.agents?.primary_tier ?? 'default'; const modelTier = _assistantModelDefaultsDraft?.primaryTier ?? configData?.agents?.primary_tier ?? 'default';
const delegation = configData?.agents?.delegation ?? {}; const delegation = configData?.agents?.delegation ?? {};
const backgroundModels = configData?.agents?.background_models ?? {}; const backgroundModels = configData?.agents?.background_models ?? {};
const tiers = configData?.models ?? {}; const tiers = configData?.models ?? {};
@@ -787,10 +792,10 @@ function updateAssistantHealth(configData) {
<div class="text-sm font-semibold text-zinc-50 mb-3">Model Tier Defaults</div> <div class="text-sm font-semibold text-zinc-50 mb-3">Model Tier Defaults</div>
<div class="text-sm text-zinc-500 mb-3">Tier provider/model definitions</div> <div class="text-sm text-zinc-500 mb-3">Tier provider/model definitions</div>
<div class="space-y-3 mb-4"> <div class="space-y-3 mb-4">
${['default', 'fast', 'complex', 'local'].map((tier) => { ${MODEL_DEFAULT_TIER_KEYS.map((tier) => {
const cfg = tiers?.[tier] ?? {}; const cfg = tiers?.[tier] ?? {};
const provider = cfg.provider ?? tiers?.default?.provider ?? 'openai'; const provider = _assistantModelDefaultsDraft?.tiers?.[tier]?.provider ?? cfg.provider ?? tiers?.default?.provider ?? 'openai';
const model = cfg.model ?? ''; const model = _assistantModelDefaultsDraft?.tiers?.[tier]?.model ?? cfg.model ?? '';
return ` return `
<div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60"> <div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
<div class="text-sm text-zinc-50 mb-2">${escapeHtml(tier)} tier</div> <div class="text-sm text-zinc-50 mb-2">${escapeHtml(tier)} tier</div>
@@ -822,8 +827,9 @@ function updateAssistantHealth(configData) {
<div class="space-y-3"> <div class="space-y-3">
${taskRows.map((task) => { ${taskRows.map((task) => {
const background = backgroundModels?.[task.key] ?? {}; const background = backgroundModels?.[task.key] ?? {};
const delegationTier = delegation?.[task.key] ?? 'fast'; const draftTask = _assistantModelDefaultsDraft?.tasks?.[task.key] ?? {};
const backgroundEnabled = Boolean(backgroundModels?.[task.key] && background?.enabled !== false); const delegationTier = draftTask.delegationTier ?? delegation?.[task.key] ?? 'fast';
const backgroundEnabled = draftTask.backgroundEnabled ?? Boolean(backgroundModels?.[task.key] && background?.enabled !== false);
return ` return `
<div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60"> <div class="p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
<div class="text-sm text-zinc-50 mb-2">${escapeHtml(task.label)}</div> <div class="text-sm text-zinc-50 mb-2">${escapeHtml(task.label)}</div>
@@ -841,17 +847,21 @@ function updateAssistantHealth(configData) {
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Provider</span> <span class="text-xs text-zinc-500">Provider</span>
<select id="assist-bg-${task.key}-provider" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none"> <select id="assist-bg-${task.key}-provider" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none">
${providerOption(background?.provider ?? tiers?.default?.provider ?? 'openai')} ${providerOption(draftTask.provider ?? background?.provider ?? tiers?.default?.provider ?? 'openai')}
</select> </select>
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Model</span> <span class="text-xs text-zinc-500">Model</span>
${modelDataList(`assist-bg-${task.key}-model`, background?.provider ?? tiers?.default?.provider ?? 'openai', background?.model ?? '')} ${modelDataList(
`assist-bg-${task.key}-model`,
draftTask.provider ?? background?.provider ?? tiers?.default?.provider ?? 'openai',
draftTask.model ?? background?.model ?? '',
)}
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
<span class="text-xs text-zinc-500">Fallback tier</span> <span class="text-xs text-zinc-500">Fallback tier</span>
<select id="assist-bg-${task.key}-fallback" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none"> <select id="assist-bg-${task.key}-fallback" class="bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm focus:border-blue-500 outline-none">
${tierOption(background?.fallback_tier ?? 'fast')} ${tierOption(draftTask.fallbackTier ?? background?.fallback_tier ?? 'fast')}
</select> </select>
</label> </label>
</div> </div>
@@ -928,7 +938,7 @@ function updateAssistantHealth(configData) {
}); });
} }
const taskRowsForModels = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning']; const taskRowsForModels = MODEL_DEFAULT_TASK_KEYS;
for (const task of taskRowsForModels) { for (const task of taskRowsForModels) {
const providerSelect = el.querySelector(`#assist-bg-${task}-provider`); const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
if (!providerSelect) {continue;} if (!providerSelect) {continue;}
@@ -996,11 +1006,11 @@ function updateAssistantHealth(configData) {
}; };
_assistantManualOverrides.add('automation.daily_briefing.enabled'); _assistantManualOverrides.add('automation.daily_briefing.enabled');
} else if (action === 'save-model-defaults') { } else if (action === 'save-model-defaults') {
const tasks = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning']; const tasks = MODEL_DEFAULT_TASK_KEYS;
patches = { patches = {
'agents.primary_tier': (el.querySelector('#assist-primary-tier')?.value ?? 'default'), 'agents.primary_tier': (el.querySelector('#assist-primary-tier')?.value ?? 'default'),
}; };
const tiers = ['default', 'fast', 'complex', 'local']; const tiers = MODEL_DEFAULT_TIER_KEYS;
for (const tier of tiers) { for (const tier of tiers) {
const provider = (el.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim(); const provider = (el.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
const model = (el.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim(); const model = (el.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
@@ -1033,7 +1043,12 @@ function updateAssistantHealth(configData) {
} }
} }
if (!patches) {return;} if (!patches) {return;}
await applyAssistantPatch(patches, statusEl); const patchResult = await applyAssistantPatch(patches, statusEl);
if (action === 'save-model-defaults' && patchResult.applied.length > 0 && patchResult.rejected.length === 0) {
_assistantModelDefaultsDraft = null;
} else if (action === 'save-model-defaults') {
_assistantModelDefaultsDraft = readAssistantModelDefaultsDraft(el) ?? _assistantModelDefaultsDraft;
}
// Force immediate refresh of slow sections after applying. // Force immediate refresh of slow sections after applying.
const refreshed = await fetchSlow(_dashboardClient); const refreshed = await fetchSlow(_dashboardClient);
if (refreshed) { if (refreshed) {
@@ -1052,6 +1067,38 @@ function updateAssistantHealth(configData) {
}); });
} }
function readAssistantModelDefaultsDraft(rootEl) {
const primaryTier = rootEl.querySelector('#assist-primary-tier')?.value;
if (!primaryTier) {
return null;
}
const tiers = {};
for (const tier of MODEL_DEFAULT_TIER_KEYS) {
const provider = (rootEl.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
const model = (rootEl.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
if (!provider && !model) {continue;}
tiers[tier] = { provider, model };
}
const tasks = {};
for (const task of MODEL_DEFAULT_TASK_KEYS) {
tasks[task] = {
delegationTier: rootEl.querySelector(`#assist-delegation-${task}`)?.value ?? 'fast',
backgroundEnabled: Boolean(rootEl.querySelector(`#assist-bg-${task}-enabled`)?.checked),
provider: (rootEl.querySelector(`#assist-bg-${task}-provider`)?.value ?? '').trim(),
model: (rootEl.querySelector(`#assist-bg-${task}-model`)?.value ?? '').trim(),
fallbackTier: rootEl.querySelector(`#assist-bg-${task}-fallback`)?.value ?? 'fast',
};
}
return {
primaryTier,
tiers,
tasks,
};
}
function _updateChannels(channelsData) { function _updateChannels(channelsData) {
const el = document.getElementById('ops-channels'); const el = document.getElementById('ops-channels');
if (!el) {return;} if (!el) {return;}
@@ -1248,5 +1295,6 @@ export const DashboardPage = {
_assistantSaveState = null; _assistantSaveState = null;
_lastAssistantConfig = null; _lastAssistantConfig = null;
_assistantManualOverrides = new Set(); _assistantManualOverrides = new Set();
_assistantModelDefaultsDraft = null;
}, },
}; };