/** * EcoOS App - Main application component * Uses dees-simple-appdash as the dashboard shell */ import { html, DeesElement, customElement, state, css, type TemplateResult, } from '@design.estate/dees-element'; import { DeesSimpleAppdash, type IView } from '@design.estate/dees-catalog'; import type { IStatus } from '../../ts_interfaces/status.js'; import type { IDisplayInfo } from '../../ts_interfaces/display.js'; import type { IUpdateInfo } from '../../ts_interfaces/updates.js'; import { EcoosOverview } from './ecoos-overview.js'; import { EcoosDevices } from './ecoos-devices.js'; import { EcoosDisplays } from './ecoos-displays.js'; import { EcoosUpdates } from './ecoos-updates.js'; import { EcoosLogs } from './ecoos-logs.js'; @customElement('ecoos-app') export class EcoosApp extends DeesElement { @state() private accessor status: IStatus | null = null; @state() private accessor displays: IDisplayInfo[] = []; @state() private accessor updateInfo: IUpdateInfo | null = null; @state() private accessor initialVersion: string | null = null; private ws: WebSocket | null = null; private statusInterval: number | null = null; private displaysInterval: number | null = null; private updatesInterval: number | null = null; public static styles = [ css` :host { display: block; width: 100vw; height: 100vh; background: #0a0a0a; } dees-simple-appdash { width: 100%; height: 100%; } `, ]; private viewTabs: IView[] = [ { name: 'Overview', iconName: 'lucide:layoutGrid', element: EcoosOverview, }, { name: 'Devices', iconName: 'lucide:cpu', element: EcoosDevices, }, { name: 'Displays', iconName: 'lucide:monitor', element: EcoosDisplays, }, { name: 'Updates', iconName: 'lucide:download', element: EcoosUpdates, }, { name: 'Logs', iconName: 'lucide:scrollText', element: EcoosLogs, }, ]; connectedCallback(): void { super.connectedCallback(); this.startPolling(); this.connectWebSocket(); } disconnectedCallback(): void { super.disconnectedCallback(); this.stopPolling(); this.disconnectWebSocket(); } render(): TemplateResult { return html` `; } updated(changedProperties: Map): void { super.updated(changedProperties); // Pass data to view components when they're rendered this.updateViewData(); } private updateViewData(): void { // Find and update the active view component const appdash = this.shadowRoot?.querySelector('dees-simple-appdash'); if (!appdash) return; // Get the current view content const overview = appdash.shadowRoot?.querySelector('ecoos-overview') as EcoosOverview; const devices = appdash.shadowRoot?.querySelector('ecoos-devices') as EcoosDevices; const displays = appdash.shadowRoot?.querySelector('ecoos-displays') as EcoosDisplays; const updates = appdash.shadowRoot?.querySelector('ecoos-updates') as EcoosUpdates; const logs = appdash.shadowRoot?.querySelector('ecoos-logs') as EcoosLogs; if (overview && this.status) { overview.status = this.status; } if (devices && this.status?.systemInfo) { devices.systemInfo = this.status.systemInfo; } if (displays) { displays.displays = this.displays; } if (updates && this.updateInfo) { updates.updateInfo = this.updateInfo; } if (logs && this.status) { logs.daemonLogs = this.status.logs || []; logs.systemLogs = this.status.systemLogs || []; } } private handleViewSelect(event: CustomEvent): void { console.log('View selected:', event.detail.view.name); // Trigger a data update for the new view setTimeout(() => this.updateViewData(), 100); } private startPolling(): void { // Initial fetches this.fetchStatus(); this.fetchDisplays(); this.fetchUpdates(); // Periodic polling this.statusInterval = window.setInterval(() => this.fetchStatus(), 3000); this.displaysInterval = window.setInterval(() => this.fetchDisplays(), 5000); this.updatesInterval = window.setInterval(() => this.fetchUpdates(), 60000); } private stopPolling(): void { if (this.statusInterval) { clearInterval(this.statusInterval); this.statusInterval = null; } if (this.displaysInterval) { clearInterval(this.displaysInterval); this.displaysInterval = null; } if (this.updatesInterval) { clearInterval(this.updatesInterval); this.updatesInterval = null; } } private connectWebSocket(): void { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; this.ws = new WebSocket(wsUrl); this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data) as IStatus; this.handleStatusUpdate(data); } catch (e) { console.error('WebSocket message parse error:', e); } }; this.ws.onclose = () => { console.log('WebSocket disconnected, reconnecting in 3s...'); setTimeout(() => this.connectWebSocket(), 3000); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; } private disconnectWebSocket(): void { if (this.ws) { this.ws.close(); this.ws = null; } } private handleStatusUpdate(data: IStatus): void { // Check for version change and reload if needed if (data.version) { if (this.initialVersion === null) { this.initialVersion = data.version; } else if (data.version !== this.initialVersion) { console.log(`Version changed from ${this.initialVersion} to ${data.version}, reloading...`); window.location.reload(); return; } } this.status = data; this.updateViewData(); } private async fetchStatus(): Promise { try { const response = await fetch('/api/status'); const data = await response.json() as IStatus; this.handleStatusUpdate(data); } catch (error) { console.error('Failed to fetch status:', error); } } private async fetchDisplays(): Promise { try { const response = await fetch('/api/displays'); const data = await response.json(); this.displays = data.displays || []; this.updateViewData(); } catch (error) { console.error('Failed to fetch displays:', error); } } private async fetchUpdates(): Promise { try { const response = await fetch('/api/updates'); const data = await response.json() as IUpdateInfo; this.updateInfo = data; this.updateViewData(); } catch (error) { console.error('Failed to fetch updates:', error); } } }