feat(gateway): add system.services and dashboard services grid

This commit is contained in:
William Valentin
2026-02-14 00:42:41 -08:00
parent 4f3810ba4c
commit 0493660e7d
9 changed files with 369 additions and 11 deletions
+107
View File
@@ -0,0 +1,107 @@
/**
* Service discovery helpers for the Web UI dashboard.
*
* The gateway can surface a single "services" list so operators can see:
* - which channel adapters are configured vs connected
* - which automation subsystems are enabled (cron, webhooks, gmail, etc.)
*/
import type { ChannelRegistry } from '../../channels/index.js';
import type { Config } from '../../config/index.js';
export type ServiceType = 'channel' | 'automation' | 'tool';
// Channel adapters expose: disconnected|connecting|connected|error.
// For config-driven subsystems we additionally use:
// - configured: enabled in config, runtime status not tracked at this layer
// - not_configured: disabled / missing config
export type ServiceStatus =
| 'disconnected'
| 'connecting'
| 'connected'
| 'error'
| 'configured'
| 'not_configured';
export interface ServiceInfo {
name: string;
type: ServiceType;
status: ServiceStatus;
description: string;
itemCount?: number;
error?: string;
metadata?: Record<string, unknown>;
}
/**
* Discover all services from config and channel registry.
*
* Returns configured channels and detects potential services from config
* that may not yet be registered (e.g., gcal configured but no adapter created).
*/
export function discoverServices(
config: Config,
channelRegistry: ChannelRegistry,
): ServiceInfo[] {
const services: ServiceInfo[] = [];
const registeredChannels = channelRegistry.list();
const channelConfigs: Array<{ key: keyof Config; name: string; description: string }> = [
{ key: 'telegram', name: 'telegram', description: 'Telegram bot' },
{ key: 'discord', name: 'discord', description: 'Discord bot' },
{ key: 'slack', name: 'slack', description: 'Slack app' },
{ key: 'whatsapp', name: 'whatsapp', description: 'WhatsApp gateway' },
];
for (const { key, name, description } of channelConfigs) {
const configured = Boolean((config as unknown as Record<string, unknown>)[String(key)]);
const registered = registeredChannels.find(c => c.name === name);
if (!configured) {
services.push({ name, type: 'channel', status: 'not_configured', description });
continue;
}
if (registered) {
services.push({ name, type: 'channel', status: registered.status as ServiceStatus, description });
continue;
}
// Config present but adapter not registered (or gateway running without it).
services.push({ name, type: 'channel', status: 'disconnected', description });
}
const automation = config.automation;
const automationConfigs: Array<{ enabled: boolean; name: string; description: string; itemCount?: number }> = [
{ enabled: automation.cron.length > 0, name: 'cron', description: 'Cron scheduler', itemCount: automation.cron.length },
{ enabled: automation.webhooks.length > 0, name: 'webhooks', description: 'Webhook handler', itemCount: automation.webhooks.length },
{ enabled: automation.gmail?.enabled ?? false, name: 'gmail', description: 'Gmail watcher' },
{ enabled: automation.heartbeat?.enabled ?? false, name: 'heartbeat', description: 'Heartbeat monitor' },
{ enabled: automation.gcal?.enabled ?? false, name: 'gcal', description: 'Google Calendar' },
{ enabled: automation.gdocs?.enabled ?? false, name: 'gdocs', description: 'Google Docs' },
{ enabled: automation.gdrive?.enabled ?? false, name: 'gdrive', description: 'Google Drive' },
{ enabled: automation.gtasks?.enabled ?? false, name: 'gtasks', description: 'Google Tasks' },
];
for (const auto of automationConfigs) {
services.push({
name: auto.name,
type: 'automation',
status: auto.enabled ? 'configured' : 'not_configured',
description: auto.description,
...(auto.itemCount !== undefined ? { itemCount: auto.itemCount } : {}),
});
}
services.push({
name: 'mcp',
type: 'automation',
status: config.mcp.servers.length > 0 ? 'configured' : 'not_configured',
description: 'MCP servers',
metadata: { serverCount: config.mcp.servers.length },
});
return services;
}