fix(dashboard): preserve unsaved model tier selections across refresh
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user