import * as plugins from '../plugins.js'; import * as shared from './shared/index.js'; import * as appstate from '../appstate.js'; import { appRouter } from '../router.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; const byteUnits = ['B', 'KB', 'MB', 'GB', 'TB']; function getByteUnitIndex(bytes: number): number { if (!bytes || bytes === 0) return 0; return Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), byteUnits.length - 1); } function formatBytes(bytes: number, forcedUnitIndex?: number): string { if ((!bytes || bytes === 0) && forcedUnitIndex === undefined) return '0 B'; const unitIndex = forcedUnitIndex ?? getByteUnitIndex(bytes); const value = bytes / Math.pow(1024, unitIndex); return `${value.toFixed(1)} ${byteUnits[unitIndex]}`; } @customElement('ob-view-dashboard') export class ObViewDashboard extends DeesElement { @state() accessor systemState: appstate.ISystemState = { status: null }; @state() accessor servicesState: appstate.IServicesState = { services: [], currentService: null, currentServiceLogs: [], currentServiceStats: null, platformServices: [], currentPlatformService: null, currentPlatformServiceStats: null, currentPlatformServiceLogs: [], }; @state() accessor networkState: appstate.INetworkState = { targets: [], stats: null, trafficStats: null, dnsRecords: [], domains: [], gatewayDomains: [], gatewayDnsRecords: [], certificates: [], }; constructor() { super(); const systemSub = appstate.systemStatePart .select((s) => s) .subscribe((newState) => { this.systemState = newState; }); this.rxSubscriptions.push(systemSub); const servicesSub = appstate.servicesStatePart .select((s) => s) .subscribe((newState) => { this.servicesState = newState; }); this.rxSubscriptions.push(servicesSub); const networkSub = appstate.networkStatePart .select((s) => s) .subscribe((newState) => { this.networkState = newState; }); this.rxSubscriptions.push(networkSub); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` .dashboard { display: flex; flex-direction: column; gap: 24px; } .section { display: flex; flex-direction: column; } .section-title { font-size: 18px; font-weight: 600; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; margin: 0 0 12px; } .services-grid { display: grid; grid-template-columns: 1fr; gap: 16px; align-items: stretch; } .services-grid > * { height: 100%; } @media (min-width: 768px) { .services-grid { grid-template-columns: 1fr 1fr; } } `, ]; async connectedCallback() { super.connectedCallback(); await Promise.all([ appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null), appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null), appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null), appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null), appstate.networkStatePart.dispatchAction(appstate.fetchTrafficStatsAction, null), appstate.networkStatePart.dispatchAction(appstate.fetchCertificatesAction, null), ]); } public render(): TemplateResult { const status = this.systemState.status; const services = this.servicesState.services; const platformServices = this.servicesState.platformServices; const networkStats = this.networkState.stats; const trafficStats = this.networkState.trafficStats; const certificates = this.networkState.certificates; const statusCounts = trafficStats?.statusCounts || {}; const runningServices = services.filter((s) => s.status === 'running').length; const stoppedServices = services.filter((s) => s.status === 'stopped').length; const memoryUnitIndex = getByteUnitIndex( status?.docker?.memoryTotal || status?.docker?.memoryUsage || 0, ); const validCerts = certificates.filter((c) => c.isValid).length; const expiringCerts = certificates.filter( (c) => c.isValid && c.expiresAt && c.expiresAt - Date.now() < 30 * 24 * 60 * 60 * 1000, ).length; const expiredCerts = certificates.filter((c) => !c.isValid).length; const dashboardData = { cluster: { totalServices: services.length, running: runningServices, stopped: stoppedServices, dockerStatus: status?.docker?.running ? 'running' as const : 'stopped' as const, }, resourceUsage: { cpu: status?.docker?.cpuUsage || 0, memoryUsed: formatBytes(status?.docker?.memoryUsage || 0, memoryUnitIndex), memoryTotal: formatBytes(status?.docker?.memoryTotal || 0, memoryUnitIndex), networkIn: formatBytes(status?.docker?.networkIn || 0), networkOut: formatBytes(status?.docker?.networkOut || 0), topConsumers: [], }, platformServices: platformServices .filter((ps) => ps.status === 'running' || ps.status === 'starting' || ps.status === 'stopping' || ps.isCore) .map((ps) => ({ name: ps.displayName, status: ps.status === 'running' ? 'Running' : ps.status === 'starting' ? 'Starting...' : ps.status === 'stopping' ? 'Stopping...' : 'Stopped', running: ps.status === 'running', })), traffic: { requests: trafficStats?.requestCount || 0, errors: trafficStats?.errorCount || 0, errorPercent: trafficStats?.errorRate || 0, avgResponse: trafficStats?.avgResponseTime || 0, reqPerMin: trafficStats?.requestsPerMinute || 0, status2xx: statusCounts['2xx'] || 0, status3xx: statusCounts['3xx'] || 0, status4xx: statusCounts['4xx'] || 0, status5xx: statusCounts['5xx'] || 0, }, proxy: { httpPort: String(networkStats?.proxy?.httpPort || 80), httpsPort: String(networkStats?.proxy?.httpsPort || 443), httpActive: networkStats?.proxy?.running || false, httpsActive: networkStats?.proxy?.running || false, routeCount: String(networkStats?.proxy?.routes || 0), }, certificates: { valid: validCerts, expiring: expiringCerts, expired: expiredCerts, }, dnsConfigured: status?.dns?.configured || false, acmeConfigured: status?.ssl?.configured || false, quickActions: [ { label: 'Deploy Service', icon: 'lucide:Plus', primary: true }, { label: 'Add Domain', icon: 'lucide:Globe' }, { label: 'View Logs', icon: 'lucide:FileText' }, ], }; return html` Dashboard

Cluster Overview

Services & Resources

this.handlePlatformServiceClick(e)} >

Network & Traffic

Infrastructure

this.handleQuickAction(e)} >
`; } private handleQuickAction(e: CustomEvent) { const action = e.detail?.action || e.detail?.label; if (action === 'Deploy Service') { appRouter.navigateToView('services'); } else if (action === 'Add Domain') { appRouter.navigateToView('network'); } } private handlePlatformServiceClick(e: CustomEvent) { // Find the platform service type from the click event const name = e.detail?.name; const ps = this.servicesState.platformServices.find( (p) => p.displayName === name, ); if (ps) { // Navigate to services tab — the ObViewServices component will pick up the type // Store the selected platform type so the services view can open it appstate.servicesStatePart.setState({ ...appstate.servicesStatePart.getState(), currentPlatformService: ps, }); appRouter.navigateToView('services'); } } }