gateway: add local backend daemon controls to dashboard
This commit is contained in:
@@ -20,6 +20,8 @@ let _lastCouncilTask = '';
|
||||
let _lastCouncilResult = null;
|
||||
let _lastCouncilError = null;
|
||||
let _lastServices = [];
|
||||
let _lastLocalBackends = [];
|
||||
let _localBackendActionState = new Map();
|
||||
let _serviceConfigState = {
|
||||
open: false,
|
||||
serviceName: null,
|
||||
@@ -31,6 +33,11 @@ let _serviceConfigState = {
|
||||
const MODEL_DEFAULT_TASK_KEYS = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning'];
|
||||
const MODEL_DEFAULT_TIER_KEYS = ['default', 'fast', 'complex', 'local'];
|
||||
const HEARTBEAT_CHECK_KEYS = ['gateway', 'model', 'channels', 'memory', 'disk', 'process_memory', 'backup', 'provider_errors'];
|
||||
const LOCAL_BACKEND_ACTION_LABELS = {
|
||||
start: 'Start',
|
||||
restart: 'Restart',
|
||||
stop: 'Stop',
|
||||
};
|
||||
const SERVICE_TOGGLE_PATCH_PATHS = {
|
||||
heartbeat: 'automation.heartbeat.enabled',
|
||||
daily_briefing: 'automation.daily_briefing.enabled',
|
||||
@@ -372,6 +379,12 @@ function renderSkeleton(el) {
|
||||
<div id="ops-services" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div class="text-sm text-zinc-500">Loading...</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Local LLM Backends</h2>
|
||||
<div class="text-xs text-zinc-500 mb-2">User-level daemon status and controls for local providers.</div>
|
||||
<div id="ops-local-backends" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="text-sm text-zinc-500">Loading...</div>
|
||||
</div>
|
||||
<div id="ops-service-config-modal-root"></div>
|
||||
`;
|
||||
}
|
||||
@@ -1406,6 +1419,7 @@ function updateAssistantHealth(configData) {
|
||||
_lastAssistantConfig = refreshed.config;
|
||||
}
|
||||
updateServices(refreshed.services);
|
||||
updateLocalBackends(refreshed.localBackends);
|
||||
updateSessionAnalytics(refreshed.sessionAnalytics);
|
||||
updateContextHealth(refreshed.contextUsage);
|
||||
// Only re-render assistant controls from a confirmed config snapshot.
|
||||
@@ -1527,6 +1541,114 @@ function updateServices(servicesData) {
|
||||
renderServiceConfigModal();
|
||||
}
|
||||
|
||||
function updateLocalBackends(localBackendsData) {
|
||||
const el = document.getElementById('ops-local-backends');
|
||||
if (!el) {return;}
|
||||
|
||||
const backends = localBackendsData?.backends ?? [];
|
||||
_lastLocalBackends = backends;
|
||||
|
||||
if (backends.length === 0) {
|
||||
el.innerHTML = '<div class="text-sm text-zinc-500">No local backends detected</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = backends.map((backend) => {
|
||||
const backendId = String(backend.id ?? '');
|
||||
const actionState = _localBackendActionState.get(backendId) ?? null;
|
||||
const isPending = Boolean(actionState?.pending);
|
||||
const toneClass = backend.activeState === 'active'
|
||||
? 'text-green-500'
|
||||
: backend.activeState === 'failed'
|
||||
? 'text-red-500'
|
||||
: 'text-zinc-400';
|
||||
const configuredText = backend.configured ? 'configured' : 'not configured';
|
||||
const configuredClass = backend.configured ? 'text-blue-400' : 'text-zinc-500';
|
||||
const pidText = backend.pid ? String(backend.pid) : '—';
|
||||
const unitFileText = backend.unitFileState || 'unknown';
|
||||
const loadText = backend.loadState || 'unknown';
|
||||
const resultText = backend.result || 'unknown';
|
||||
const availableActions = Array.isArray(backend.availableActions)
|
||||
? backend.availableActions.filter((value) => ['start', 'restart', 'stop'].includes(String(value)))
|
||||
: [];
|
||||
const actionMessage = actionState?.message
|
||||
? `<div class="text-xs ${actionState.tone === 'error' ? 'text-red-400' : actionState.tone === 'success' ? 'text-green-400' : 'text-zinc-400'}">${escapeHtml(String(actionState.message))}</div>`
|
||||
: '';
|
||||
|
||||
const actionButtons = availableActions.length > 0
|
||||
? availableActions.map((action) => {
|
||||
const key = String(action);
|
||||
const label = LOCAL_BACKEND_ACTION_LABELS[key] ?? key;
|
||||
return `<button class="local-backend-action-btn px-2 py-1 text-xs border border-zinc-700 rounded text-zinc-200 hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
data-backend-id="${escapeHtml(backendId)}"
|
||||
data-action="${escapeHtml(key)}"
|
||||
${isPending ? 'disabled' : ''}
|
||||
title="${escapeHtml(`${label} ${backend.name ?? backendId}`)}">${escapeHtml(label)}</button>`;
|
||||
}).join('')
|
||||
: '<span class="text-xs text-zinc-500">No actions available</span>';
|
||||
|
||||
return `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-sm font-semibold text-zinc-50">${escapeHtml(String(backend.name ?? backendId))}</div>
|
||||
<span class="text-xs uppercase ${toneClass}">${escapeHtml(String(backend.statusText ?? backend.activeState ?? 'unknown'))}</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">Unit: <span class="font-mono text-zinc-400">${escapeHtml(String(backend.unit ?? 'unknown'))}</span></div>
|
||||
<div class="text-xs text-zinc-500">PID: <span class="font-mono text-zinc-400">${escapeHtml(pidText)}</span> · Load: <span class="font-mono text-zinc-400">${escapeHtml(loadText)}</span> · Result: <span class="font-mono text-zinc-400">${escapeHtml(resultText)}</span></div>
|
||||
<div class="text-xs ${configuredClass}">${escapeHtml(configuredText)} · unit file: ${escapeHtml(unitFileText)}</div>
|
||||
${backend.error ? `<div class="text-xs text-red-400">Error: ${escapeHtml(String(backend.error))}</div>` : ''}
|
||||
${actionMessage}
|
||||
<div class="flex flex-wrap gap-2">${actionButtons}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.querySelectorAll('.local-backend-action-btn').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const backendId = button.getAttribute('data-backend-id');
|
||||
const action = button.getAttribute('data-action');
|
||||
if (!backendId || !action) {return;}
|
||||
await handleLocalBackendAction(backendId, action);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLocalBackendAction(backendId, action) {
|
||||
if (!_dashboardClient) {return;}
|
||||
const actionLabel = LOCAL_BACKEND_ACTION_LABELS[action] ?? action;
|
||||
_localBackendActionState.set(backendId, { pending: true, tone: 'neutral', message: `${actionLabel} requested…` });
|
||||
updateLocalBackends({ backends: _lastLocalBackends });
|
||||
|
||||
try {
|
||||
const result = await _dashboardClient.call('system.localBackendControl', {
|
||||
backend: backendId,
|
||||
action,
|
||||
});
|
||||
const status = result?.status;
|
||||
if (status && typeof status === 'object') {
|
||||
_lastLocalBackends = _lastLocalBackends.map((backend) =>
|
||||
backend.id === backendId ? status : backend);
|
||||
}
|
||||
_localBackendActionState.set(backendId, {
|
||||
pending: false,
|
||||
tone: 'success',
|
||||
message: `${actionLabel} completed`,
|
||||
});
|
||||
updateLocalBackends({ backends: _lastLocalBackends });
|
||||
|
||||
const refreshed = await fetchSlow(_dashboardClient);
|
||||
if (refreshed?.localBackends) {
|
||||
updateLocalBackends(refreshed.localBackends);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
_localBackendActionState.set(backendId, {
|
||||
pending: false,
|
||||
tone: 'error',
|
||||
message: `${actionLabel} failed: ${message}`,
|
||||
});
|
||||
updateLocalBackends({ backends: _lastLocalBackends });
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigValue(path, fallbackValue) {
|
||||
const value = getByPath(_lastAssistantConfig, path);
|
||||
return value === undefined ? fallbackValue : value;
|
||||
@@ -1697,6 +1819,7 @@ function renderServiceConfigModal() {
|
||||
const refreshed = await fetchSlow(_dashboardClient);
|
||||
if (refreshed) {
|
||||
updateServices(refreshed.services);
|
||||
updateLocalBackends(refreshed.localBackends);
|
||||
updateSessionAnalytics(refreshed.sessionAnalytics);
|
||||
updateContextHealth(refreshed.contextUsage);
|
||||
if (refreshed.config) {
|
||||
@@ -1729,9 +1852,10 @@ async function fetchFast(client) {
|
||||
}
|
||||
|
||||
async function fetchSlow(client) {
|
||||
const [health, services, sessionAnalytics, contextUsage, config, modelCatalog] = await Promise.allSettled([
|
||||
const [health, services, localBackends, sessionAnalytics, contextUsage, config, modelCatalog] = await Promise.allSettled([
|
||||
client.call('system.health'),
|
||||
client.call('system.services'),
|
||||
client.call('system.localBackends'),
|
||||
client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }),
|
||||
client.call('system.contextUsage'),
|
||||
client.call('config.get'),
|
||||
@@ -1749,6 +1873,7 @@ async function fetchSlow(client) {
|
||||
return {
|
||||
health: unwrap(health),
|
||||
services: unwrap(services),
|
||||
localBackends: unwrap(localBackends),
|
||||
sessionAnalytics: unwrap(sessionAnalytics),
|
||||
contextUsage: unwrap(contextUsage),
|
||||
config: configValue,
|
||||
@@ -1785,6 +1910,9 @@ async function loadDashboard(el, client) {
|
||||
if (slow?.services) {
|
||||
updateServices(slow.services);
|
||||
}
|
||||
if (slow?.localBackends) {
|
||||
updateLocalBackends(slow.localBackends);
|
||||
}
|
||||
if (slow?.sessionAnalytics) {
|
||||
updateSessionAnalytics(slow.sessionAnalytics);
|
||||
}
|
||||
@@ -1820,6 +1948,9 @@ async function loadDashboard(el, client) {
|
||||
if (data.services) {
|
||||
updateServices(data.services);
|
||||
}
|
||||
if (data.localBackends) {
|
||||
updateLocalBackends(data.localBackends);
|
||||
}
|
||||
if (data.sessionAnalytics) {
|
||||
updateSessionAnalytics(data.sessionAnalytics);
|
||||
}
|
||||
@@ -1857,5 +1988,18 @@ export const DashboardPage = {
|
||||
_assistantModelDefaultsDraft = null;
|
||||
_assistantDraftState = new Map();
|
||||
_assistantDraftTouchedAt = 0;
|
||||
_lastServices = [];
|
||||
_lastLocalBackends = [];
|
||||
_localBackendActionState = new Map();
|
||||
_serviceConfigState = {
|
||||
open: false,
|
||||
serviceName: null,
|
||||
status: null,
|
||||
tone: 'neutral',
|
||||
advancedPatch: '',
|
||||
};
|
||||
_lastCouncilTask = '';
|
||||
_lastCouncilResult = null;
|
||||
_lastCouncilError = null;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user