ui: add contextual tooltips to web form controls

This commit is contained in:
William Valentin
2026-02-21 21:51:40 -08:00
parent b09bfc8373
commit 9707b5a5df
3 changed files with 86 additions and 88 deletions
+57 -59
View File
@@ -832,7 +832,7 @@ function updateAssistantHealth(configData) {
const modelDataList = (id, provider, selected) => {
const options = modelOptionsByProvider[provider] ?? [];
return `
<input id="${id}" list="${id}-list" value="${escapeHtml(selected ?? '')}" 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" />
<input id="${id}" list="${id}-list" value="${escapeHtml(selected ?? '')}" 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" title="Enter a model ID for the selected provider or pick one from the suggestion list." />
<datalist id="${id}-list">
${options.map((model) => `<option value="${escapeHtml(model)}"></option>`).join('')}
</datalist>
@@ -909,13 +909,13 @@ function updateAssistantHealth(configData) {
<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="grid grid-cols-1 md:grid-cols-2 gap-2">
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Provider used for this model tier.">
<span class="text-xs text-zinc-500">Provider</span>
<select id="assist-tier-${tier}-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-tier-${tier}-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" title="Select a provider for the ${escapeHtml(tier)} tier.">
${providerOption(provider)}
</select>
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Default model name for this tier.">
<span class="text-xs text-zinc-500">Model</span>
${modelDataList(`assist-tier-${tier}-model`, provider, model)}
</label>
@@ -925,9 +925,9 @@ function updateAssistantHealth(configData) {
}).join('')}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Main tier used when no task-specific routing rule applies.">
<span class="text-sm text-zinc-400">Primary tier</span>
<select id="assist-primary-tier" 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">
<select id="assist-primary-tier" 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="Choose the default model tier for general assistant responses.">
${tierOption(modelTier)}
</select>
</label>
@@ -943,23 +943,23 @@ function updateAssistantHealth(configData) {
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-2">
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Tier used when this task is delegated.">
<span class="text-xs text-zinc-500">Delegation tier</span>
<select id="assist-delegation-${task.key}" 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-delegation-${task.key}" 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" title="Select the tier used for ${escapeHtml(task.label.toLowerCase())} delegation.">
${tierOption(delegationTier)}
</select>
</label>
<label class="flex items-center gap-2 mt-5 md:mt-0">
<input id="assist-bg-${task.key}-enabled" type="checkbox" ${backgroundEnabled ? 'checked' : ''} />
<label class="flex items-center gap-2 mt-5 md:mt-0" title="When enabled, this task can use a custom provider/model override.">
<input id="assist-bg-${task.key}-enabled" type="checkbox" ${backgroundEnabled ? 'checked' : ''} title="Toggle background provider/model override for this task." />
<span class="text-xs text-zinc-400">Enable provider/model override</span>
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Provider used when override is enabled.">
<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" title="Pick the override provider for this task.">
${providerOption(draftTask.provider ?? background?.provider ?? tiers?.default?.provider ?? 'openai')}
</select>
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Model used when override is enabled.">
<span class="text-xs text-zinc-500">Model</span>
${modelDataList(
`assist-bg-${task.key}-model`,
@@ -967,9 +967,9 @@ function updateAssistantHealth(configData) {
draftTask.model ?? background?.model ?? '',
)}
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Tier to fall back to if the override model fails.">
<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" title="Choose the fallback tier for this task override.">
${tierOption(draftTask.fallbackTier ?? background?.fallback_tier ?? 'fast')}
</select>
</label>
@@ -988,59 +988,59 @@ function updateAssistantHealth(configData) {
<div class="text-sm font-semibold text-zinc-50 mb-3">Councils</div>
<div class="text-sm text-zinc-500 mb-3">On-demand council orchestration settings and council role model tiers.</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
<label class="flex items-center gap-2 mt-5 md:mt-0">
<input id="assist-councils-enabled" type="checkbox" ${councils.enabled ? 'checked' : ''} />
<label class="flex items-center gap-2 mt-5 md:mt-0" title="Enable on-demand council orchestration flows.">
<input id="assist-councils-enabled" type="checkbox" ${councils.enabled ? 'checked' : ''} title="Toggle council orchestration features." />
<span class="text-xs text-zinc-400">Enable councils</span>
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Model tier for D-council agents.">
<span class="text-sm text-zinc-400">D model tier</span>
<select id="assist-council-d-tier" 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">
<select id="assist-council-d-tier" 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="Set the model tier for D-council roles.">
${tierOption(councilsD.model_tier ?? 'complex')}
</select>
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Model tier for P-council agents.">
<span class="text-sm text-zinc-400">P model tier</span>
<select id="assist-council-p-tier" 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">
<select id="assist-council-p-tier" 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="Set the model tier for P-council roles.">
${tierOption(councilsP.model_tier ?? 'complex')}
</select>
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Model tier used for council meta synthesis.">
<span class="text-sm text-zinc-400">Meta model tier</span>
<select id="assist-council-meta-tier" 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">
<select id="assist-council-meta-tier" 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="Set the model tier for the council meta arbiter.">
${tierOption(councils.meta_model_tier ?? 'complex')}
</select>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Agent ID for the D arbiter role.">
<span class="text-sm text-zinc-400">D arbiter agent</span>
<input id="assist-council-d-arbiter" type="text" value="${escapeHtml(councilsD.arbiter_agent ?? 'council_d_arbiter')}" 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" />
<input id="assist-council-d-arbiter" type="text" value="${escapeHtml(councilsD.arbiter_agent ?? 'council_d_arbiter')}" 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="Configured agent key for the D arbiter role." />
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Agent ID for the D freethinker role.">
<span class="text-sm text-zinc-400">D freethinker agent</span>
<input id="assist-council-d-freethinker" type="text" value="${escapeHtml(councilsD.freethinker_agent ?? 'council_d_freethinker')}" 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" />
<input id="assist-council-d-freethinker" type="text" value="${escapeHtml(councilsD.freethinker_agent ?? 'council_d_freethinker')}" 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="Configured agent key for the D freethinker role." />
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Agent ID for the P arbiter role.">
<span class="text-sm text-zinc-400">P arbiter agent</span>
<input id="assist-council-p-arbiter" type="text" value="${escapeHtml(councilsP.arbiter_agent ?? 'council_p_arbiter')}" 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" />
<input id="assist-council-p-arbiter" type="text" value="${escapeHtml(councilsP.arbiter_agent ?? 'council_p_arbiter')}" 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="Configured agent key for the P arbiter role." />
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Agent ID for the P freethinker role.">
<span class="text-sm text-zinc-400">P freethinker agent</span>
<input id="assist-council-p-freethinker" type="text" value="${escapeHtml(councilsP.freethinker_agent ?? 'council_p_freethinker')}" 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" />
<input id="assist-council-p-freethinker" type="text" value="${escapeHtml(councilsP.freethinker_agent ?? 'council_p_freethinker')}" 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="Configured agent key for the P freethinker role." />
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Agent ID for the meta arbiter role.">
<span class="text-sm text-zinc-400">Meta arbiter agent</span>
<input id="assist-council-meta-arbiter" type="text" value="${escapeHtml(councils.meta_arbiter_agent ?? 'council_meta_arbiter')}" 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" />
<input id="assist-council-meta-arbiter" type="text" value="${escapeHtml(councils.meta_arbiter_agent ?? 'council_meta_arbiter')}" 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="Configured agent key for the council meta arbiter role." />
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Optional JSON scaffold used as council prompt template.">
<span class="text-sm text-zinc-400">Scaffold path (optional)</span>
<input id="assist-council-scaffold" type="text" value="${escapeHtml(councils.scaffold_path ?? '')}" placeholder="docs/councils/ai-council-production-scaffold.json" 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" />
<input id="assist-council-scaffold" type="text" value="${escapeHtml(councils.scaffold_path ?? '')}" placeholder="docs/councils/ai-council-production-scaffold.json" 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="Path to a scaffold JSON file under the project directory." />
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Upper bound on iterative council rounds per run.">
<span class="text-sm text-zinc-400">Max rounds</span>
<input id="assist-council-max-rounds" type="number" min="1" max="6" value="${escapeHtml(String(councilsDefaults.max_rounds ?? 2))}" 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" />
<input id="assist-council-max-rounds" type="number" min="1" max="6" value="${escapeHtml(String(councilsDefaults.max_rounds ?? 2))}" 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="Use 1-6 rounds; higher values increase cost and latency." />
</label>
</div>
<div class="flex flex-wrap gap-2">
@@ -1052,16 +1052,14 @@ function updateAssistantHealth(configData) {
<div class="text-sm font-semibold text-zinc-50 mb-2">Council Conversations</div>
<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" />
<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">
Run Council
</button>
</div>
<div id="assist-council-status" class="text-sm text-zinc-500 mb-3"></div>
<div class="max-h-72 overflow-y-auto space-y-2">
${councilConversations.length === 0
? '<div class="text-sm text-zinc-500">No conversation log yet.</div>'
: councilConversations.map((turn, idx) => `
${councilConversations.length === 0 ? '<div class="text-sm text-zinc-500">No conversation log yet.</div>' : councilConversations.map((turn, idx) => `
<details class="border border-zinc-800 rounded-md bg-zinc-900/70 p-2">
<summary class="cursor-pointer text-sm text-zinc-100">
#${idx + 1} ${escapeHtml(turn.call_id)} · ${escapeHtml(turn.agent)} @ ${escapeHtml(turn.tier)}
@@ -1086,13 +1084,13 @@ function updateAssistantHealth(configData) {
`).join('')}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Channel used for scheduled briefing messages.">
<span class="text-sm text-zinc-400">Briefing output channel</span>
<input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" 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" />
<input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" 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="Set the output adapter name for daily briefing delivery." />
</label>
<label class="flex flex-col gap-1.5">
<label class="flex flex-col gap-1.5" title="Destination peer/chat ID for scheduled briefings.">
<span class="text-sm text-zinc-400">Briefing output peer/chat id</span>
<input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" 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" />
<input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" 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="Set the recipient peer/chat ID for briefing delivery." />
</label>
</div>
<div class="flex flex-wrap gap-2">
@@ -1389,7 +1387,7 @@ function updateServices(servicesData) {
? 'text-green-500'
: svc.status === 'configured'
? 'text-blue-500'
: svc.status === 'error'
: svc.status === 'error'
? 'text-red-500'
: 'text-zinc-500';
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
@@ -1446,21 +1444,21 @@ function renderServiceConfigModal() {
const heartbeatSection = service.name === 'heartbeat'
? `
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Heartbeat check interval (examples: 1m, 5m, 30s).">
<span class="text-xs text-zinc-500">Interval (e.g. 5m)</span>
<input id="svc-heartbeat-interval" type="text" value="${escapeHtml(String(getConfigValue('automation.heartbeat.interval', '5m')))}" 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" />
<input id="svc-heartbeat-interval" type="text" value="${escapeHtml(String(getConfigValue('automation.heartbeat.interval', '5m')))}" 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" title="How often heartbeat checks are executed." />
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Minimum wait between repeated failure notifications.">
<span class="text-xs text-zinc-500">Notify cooldown</span>
<input id="svc-heartbeat-notify-cooldown" type="text" value="${escapeHtml(String(getConfigValue('automation.heartbeat.notify_cooldown', '30m')))}" 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" />
<input id="svc-heartbeat-notify-cooldown" type="text" value="${escapeHtml(String(getConfigValue('automation.heartbeat.notify_cooldown', '30m')))}" 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" title="Cooldown duration before sending another alert for the same issue." />
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Consecutive failed checks required before a service is marked unhealthy.">
<span class="text-xs text-zinc-500">Failure threshold</span>
<input id="svc-heartbeat-failure-threshold" type="number" min="1" max="10" value="${escapeHtml(String(getConfigValue('automation.heartbeat.failure_threshold', 2)))}" 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" />
<input id="svc-heartbeat-failure-threshold" type="number" min="1" max="10" value="${escapeHtml(String(getConfigValue('automation.heartbeat.failure_threshold', 2)))}" 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" title="Use 1 for aggressive alerting, higher values to reduce false positives." />
</label>
<label class="flex flex-col gap-1">
<label class="flex flex-col gap-1" title="Free disk space alert threshold in megabytes.">
<span class="text-xs text-zinc-500">Disk threshold (MB)</span>
<input id="svc-heartbeat-disk-threshold" type="number" min="10" value="${escapeHtml(String(getConfigValue('automation.heartbeat.disk_threshold_mb', 100)))}" 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" />
<input id="svc-heartbeat-disk-threshold" type="number" min="10" value="${escapeHtml(String(getConfigValue('automation.heartbeat.disk_threshold_mb', 100)))}" 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" title="Alert when free disk falls below this value." />
</label>
</div>
<div class="mt-2">
@@ -1470,8 +1468,8 @@ function renderServiceConfigModal() {
const selected = Array.isArray(getConfigValue('automation.heartbeat.checks', HEARTBEAT_CHECK_KEYS))
&& getConfigValue('automation.heartbeat.checks', HEARTBEAT_CHECK_KEYS).includes(check);
return `
<label class="flex items-center gap-2 text-xs text-zinc-300">
<input type="checkbox" data-heartbeat-check="${check}" ${selected ? 'checked' : ''} />
<label class="flex items-center gap-2 text-xs text-zinc-300" title="Enable or disable the ${escapeHtml(check)} heartbeat check.">
<input type="checkbox" data-heartbeat-check="${check}" ${selected ? 'checked' : ''} title="Toggle ${escapeHtml(check)} check." />
<span>${escapeHtml(check)}</span>
</label>
`;
@@ -1501,8 +1499,8 @@ function renderServiceConfigModal() {
<div class="mb-3 p-3 border border-zinc-800 rounded bg-zinc-950/60">
<div class="text-xs uppercase text-zinc-500 mb-2">Quick Settings</div>
${hasQuickToggle ? `
<label class="flex items-center gap-2 mb-2">
<input id="svc-quick-enabled" type="checkbox" ${quickToggleValue ? 'checked' : ''} />
<label class="flex items-center gap-2 mb-2" title="Master on/off for this service (when available).">
<input id="svc-quick-enabled" type="checkbox" ${quickToggleValue ? 'checked' : ''} title="Enable or disable this service." />
<span class="text-sm text-zinc-200">Enabled</span>
</label>
` : '<div class="text-xs text-zinc-500 mb-2">No quick toggle available for this service.</div>'}
@@ -1510,7 +1508,7 @@ function renderServiceConfigModal() {
</div>
<div class="mb-3 p-3 border border-zinc-800 rounded bg-zinc-950/60">
<div class="text-xs uppercase text-zinc-500 mb-2">Advanced Patch (optional JSON)</div>
<textarea id="svc-advanced-patch" rows="5" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm font-mono focus:border-blue-500 outline-none" placeholder='{"automation.heartbeat.enabled": true}'>${escapeHtml(_serviceConfigState.advancedPatch ?? '')}</textarea>
<textarea id="svc-advanced-patch" rows="5" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-2 py-1.5 text-sm font-mono focus:border-blue-500 outline-none" placeholder='{"automation.heartbeat.enabled": true}' title="Optional JSON object of config path/value pairs to patch on save.">${escapeHtml(_serviceConfigState.advancedPatch ?? '')}</textarea>
</div>
<div class="flex items-center justify-between gap-2">
<div id="svc-config-status" class="text-xs ${toneClass}">${escapeHtml(_serviceConfigState.status ?? '')}</div>