Files
eco_os/ecoos_daemon/ts_web/elements/ecoos-overview.ts
2026-01-12 14:34:56 +00:00

305 lines
8.0 KiB
TypeScript

/**
* EcoOS Overview View
* Dashboard with stats grid, service panels, and system info
*/
import {
html,
DeesElement,
customElement,
property,
css,
type TemplateResult,
} from '@design.estate/dees-element';
import {
DeesButton,
DeesPanel,
DeesStatsgrid,
DeesBadge,
type IStatsTile,
} from '@design.estate/dees-catalog';
import { sharedStyles, formatBytes, formatUptime } from '../styles/shared.js';
import type { IStatus, IServiceStatus } from '../../ts_interfaces/status.js';
@customElement('ecoos-overview')
export class EcoosOverview extends DeesElement {
@property({ type: Object })
public accessor status: IStatus | null = null;
@property({ type: Boolean })
public accessor loading: boolean = false;
@property({ type: String })
public accessor controlMessage: string = '';
@property({ type: Boolean })
public accessor controlError: boolean = false;
public static styles = [
sharedStyles,
css`
:host {
display: block;
padding: 16px;
}
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.cards-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (max-width: 768px) {
.cards-row {
grid-template-columns: 1fr;
}
}
.service-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border);
}
.service-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.service-row:first-child {
padding-top: 0;
}
.service-name {
font-weight: 500;
font-size: var(--text-sm);
}
.service-error {
font-size: var(--text-xs);
color: var(--error);
margin-top: 2px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.info-label {
font-size: var(--text-xs);
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.info-value {
font-size: var(--text-sm);
font-family: 'SF Mono', monospace;
}
.actions-row {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.message {
font-size: var(--text-xs);
padding: 4px 8px;
border-radius: 4px;
}
.message.success {
background: hsla(142.1, 76.2%, 36.3%, 0.15);
color: var(--success);
}
.message.error {
background: hsla(0, 84.2%, 60.2%, 0.15);
color: var(--error);
}
`,
];
render(): TemplateResult {
if (!this.status) {
return html`<div class="empty">Loading...</div>`;
}
const { systemInfo, swayStatus, chromiumStatus } = this.status;
const cpuUsage = systemInfo?.cpu?.usage || 0;
const memUsage = systemInfo?.memory?.usagePercent || 0;
const statsTiles: IStatsTile[] = [
{
id: 'cpu',
title: 'CPU',
value: Math.round(cpuUsage),
type: 'percentage',
icon: 'lucide:cpu',
description: `${systemInfo?.cpu?.cores || 0} cores`,
},
{
id: 'memory',
title: 'Memory',
value: Math.round(memUsage),
type: 'percentage',
icon: 'lucide:database',
description: `${formatBytes(systemInfo?.memory?.used || 0)} / ${formatBytes(systemInfo?.memory?.total || 0)}`,
},
{
id: 'uptime',
title: 'Uptime',
value: formatUptime(systemInfo?.uptime || 0),
type: 'text',
icon: 'lucide:clock',
},
];
return html`
<div class="page">
<!-- Stats Grid -->
<dees-statsgrid
.tiles=${statsTiles}
.minTileWidth=${200}
.gap=${16}
></dees-statsgrid>
<!-- Services & System Info -->
<div class="cards-row">
<dees-panel .title=${'Services'}>
<div class="service-row">
<div>
<div class="service-name">Sway Compositor</div>
${swayStatus?.error ? html`<div class="service-error">${swayStatus.error}</div>` : ''}
</div>
${this.renderStatusBadge(swayStatus)}
</div>
<div class="service-row">
<div>
<div class="service-name">Chromium Browser</div>
${chromiumStatus?.error ? html`<div class="service-error">${chromiumStatus.error}</div>` : ''}
</div>
${this.renderStatusBadge(chromiumStatus)}
</div>
</dees-panel>
<dees-panel .title=${'System'}>
<div class="info-grid">
<div class="info-item">
<span class="info-label">Hostname</span>
<span class="info-value">${systemInfo?.hostname || '—'}</span>
</div>
<div class="info-item">
<span class="info-label">CPU Model</span>
<span class="info-value">${this.truncate(systemInfo?.cpu?.model || '—', 20)}</span>
</div>
<div class="info-item" style="grid-column: span 2;">
<span class="info-label">GPU</span>
<span class="info-value">${systemInfo?.gpu?.length ? systemInfo.gpu.map(g => g.name).join(', ') : 'None'}</span>
</div>
</div>
</dees-panel>
</div>
<!-- Actions -->
<dees-panel .title=${'Actions'}>
<div class="actions-row">
<dees-button
.type=${'default'}
.text=${'Restart Browser'}
.disabled=${this.loading}
@click=${this.restartChromium}
></dees-button>
<dees-button
.type=${'default'}
.status=${'error'}
.text=${'Reboot System'}
.disabled=${this.loading}
@click=${this.rebootSystem}
></dees-button>
${this.controlMessage ? html`
<span class="message ${this.controlError ? 'error' : 'success'}">${this.controlMessage}</span>
` : ''}
</div>
</dees-panel>
</div>
`;
}
private renderStatusBadge(status: IServiceStatus): TemplateResult {
const state = status?.state || 'stopped';
let badgeType: 'default' | 'success' | 'warning' | 'error' = 'default';
let label = 'Stopped';
if (state === 'running') {
badgeType = 'success';
label = 'Running';
} else if (state === 'starting') {
badgeType = 'warning';
label = 'Starting';
} else if (state === 'failed') {
badgeType = 'error';
label = 'Failed';
}
return html`<dees-badge .type=${badgeType}>${label}</dees-badge>`;
}
private truncate(str: string, len: number): string {
return str.length > len ? str.substring(0, len) + '...' : str;
}
private async restartChromium(): Promise<void> {
this.loading = true;
this.controlMessage = '';
try {
const response = await fetch('/api/restart-chromium', { method: 'POST' });
const result = await response.json();
this.controlMessage = result.message;
this.controlError = !result.success;
} catch (error) {
this.controlMessage = `Error: ${error}`;
this.controlError = true;
} finally {
this.loading = false;
}
}
private async rebootSystem(): Promise<void> {
if (!confirm('Are you sure you want to reboot?')) return;
this.loading = true;
this.controlMessage = '';
try {
const response = await fetch('/api/reboot', { method: 'POST' });
const result = await response.json();
this.controlMessage = result.message;
this.controlError = !result.success;
} catch (error) {
this.controlMessage = `Error: ${error}`;
this.controlError = true;
} finally {
this.loading = false;
}
}
}