import * as shared from './shared/index.js'; import * as appstate from '../appstate.js'; import './ops-platform-events.js'; import { DeesElement, customElement, html, state, css, cssManager, } from '@design.estate/dees-element'; @customElement('ops-view-logs') export class OpsViewLogs extends DeesElement { @state() accessor logState: appstate.ILogState = { recentLogs: [], isStreaming: false, filters: {}, }; @state() accessor activeTab: 'application' | 'platform' = 'application'; @state() accessor unacknowledgedCount = 0; private lastPushedCount = 0; constructor() { super(); const subscription = appstate.logStatePart .select((stateArg) => stateArg) .subscribe((logState) => { this.logState = logState; }); this.rxSubscriptions.push(subscription); const configEventSubscription = appstate.configEventStatePart .select((stateArg) => stateArg) .subscribe((eventState) => { this.unacknowledgedCount = eventState.unacknowledgedCount; }); this.rxSubscriptions.push(configEventSubscription); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` .logTabs { display: flex; gap: 8px; margin-bottom: 12px; } .logTab { border: 1px solid var(--dees-color-border-default); background: transparent; color: var(--dees-color-text-secondary); border-radius: 6px; padding: 4px 12px; font-size: 12px; cursor: pointer; transition: all 0.15s ease; } .logTab:hover { background: var(--dees-color-hover); color: var(--dees-color-text-primary); } .logTab.active { border-color: var(--dees-color-border-strong); color: var(--dees-color-text-primary); } .logTab .countBadge { display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 8px; font-size: 11px; font-weight: 600; background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(220, 38, 38, 0.25)')}; color: ${cssManager.bdTheme('#dc2626', '#f87171')}; } `, ]; public render() { return html` Logs
${this.activeTab === 'application' ? html` ` : html` `} `; } private switchTab(tab: 'application' | 'platform') { if (this.activeTab === tab) return; this.activeTab = tab; if (tab === 'application') { // The chart element is recreated on tab switch; re-push everything. this.lastPushedCount = 0; } } async connectedCallback() { super.connectedCallback(); this.lastPushedCount = 0; // Populate the platform-events badge without requiring a tab visit. void appstate.configEventStatePart.dispatchAction(appstate.fetchConfigEventsAction, null); // Only fetch if state is empty (streaming will handle new entries) if (this.logState.recentLogs.length === 0) { await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 }); } } async updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('logState')) { this.pushLogsToChart(); } } private async pushLogsToChart() { const chartLog = this.shadowRoot?.querySelector('dees-chart-log') as any; if (!chartLog) return; // Ensure the chart element has finished its own initialization await chartLog.updateComplete; // Wait for xterm terminal to finish initializing (CDN load) if (!chartLog.terminalReady) { await new Promise((resolve) => { let attempts = 0; const maxAttempts = 200; // 200 * 50ms = 10 seconds const check = () => { if (chartLog.terminalReady) { resolve(); return; } if (++attempts >= maxAttempts) { console.warn('ops-view-logs: terminal ready timeout after 10s'); resolve(); // resolve gracefully to avoid blocking return; } setTimeout(check, 50); }; check(); }); } const allEntries = this.getMappedLogEntries(); if (this.lastPushedCount === 0 && allEntries.length > 0) { // Initial load: push all entries chartLog.updateLog(allEntries); this.lastPushedCount = allEntries.length; } else if (allEntries.length > this.lastPushedCount) { // Incremental: only push new entries const newEntries = allEntries.slice(this.lastPushedCount); chartLog.updateLog(newEntries); this.lastPushedCount = allEntries.length; } } private getMappedLogEntries() { return this.logState.recentLogs.map((log) => ({ timestamp: new Date(log.timestamp).toISOString(), level: log.level as 'debug' | 'info' | 'warn' | 'error', message: log.message, source: log.category, })); } }