305 lines
8.0 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|