update
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -78,6 +78,8 @@ export interface SystemInfoData {
|
||||
}
|
||||
|
||||
export class SystemInfo {
|
||||
private lastCpuStats: { total: number; idle: number } | null = null;
|
||||
|
||||
async getInfo(): Promise<SystemInfoData> {
|
||||
const [hostname, cpu, memory, disks, network, gpu, uptime, inputDevices, speakers, microphones] =
|
||||
await Promise.all([
|
||||
@@ -112,13 +114,23 @@ export class SystemInfo {
|
||||
const modelMatch = cpuinfo.match(/model name\s*:\s*(.+)/);
|
||||
const coreMatches = cpuinfo.match(/processor\s*:/g);
|
||||
|
||||
// Get CPU usage from /proc/stat
|
||||
// Get CPU usage from /proc/stat (delta between readings)
|
||||
const stat = await Deno.readTextFile('/proc/stat');
|
||||
const cpuLine = stat.split('\n')[0];
|
||||
const values = cpuLine.split(/\s+/).slice(1).map(Number);
|
||||
const total = values.reduce((a, b) => a + b, 0);
|
||||
const idle = values[3];
|
||||
const usage = ((total - idle) / total) * 100;
|
||||
const idle = values[3] + values[4]; // idle + iowait
|
||||
|
||||
let usage = 0;
|
||||
if (this.lastCpuStats) {
|
||||
const totalDelta = total - this.lastCpuStats.total;
|
||||
const idleDelta = idle - this.lastCpuStats.idle;
|
||||
if (totalDelta > 0) {
|
||||
usage = ((totalDelta - idleDelta) / totalDelta) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
this.lastCpuStats = { total, idle };
|
||||
|
||||
return {
|
||||
model: modelMatch ? modelMatch[1] : 'Unknown',
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "0.4.14";
|
||||
export const VERSION = "0.6.3";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* EcoOS Devices View
|
||||
* Shows network interfaces, disks, input devices, speakers, and microphones
|
||||
* Card-based view for network, storage, input, and audio devices
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
css,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
|
||||
|
||||
import { sharedStyles, formatBytes } from '../styles/shared.js';
|
||||
import type { ISystemInfo } from '../../ts_interfaces/status.js';
|
||||
@@ -25,106 +26,192 @@ export class EcoosDevices extends DeesElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cards-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.device-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.network-item:last-child {
|
||||
.device-row:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.disk-item {
|
||||
margin-bottom: 12px;
|
||||
.device-row:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-weight: 500;
|
||||
font-size: var(--text-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.device-secondary {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.progress-mini {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.progress-mini-bar {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
transition: width 300ms ease;
|
||||
}
|
||||
|
||||
.progress-mini-bar.warning {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.progress-mini-bar.error {
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.usage-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-text {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-family: 'SF Mono', monospace;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-tertiary);
|
||||
padding: 8px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.systemInfo) {
|
||||
return html`<div>Loading...</div>`;
|
||||
return html`<div class="empty">Loading...</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="grid">
|
||||
<!-- Network Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Network</div>
|
||||
<div class="cards-grid">
|
||||
<!-- Network -->
|
||||
<dees-panel .title=${'Network'}>
|
||||
${this.systemInfo.network?.length
|
||||
? this.systemInfo.network.map(n => html`
|
||||
<div class="network-item">
|
||||
<span>${n.name}</span>
|
||||
<span>${n.ip}</span>
|
||||
<div class="device-row">
|
||||
<span class="device-name">${n.name}</span>
|
||||
<span class="device-info">${n.ip || '—'}</span>
|
||||
<dees-badge .type=${n.state === 'up' ? 'success' : 'error'}>${n.state}</dees-badge>
|
||||
</div>
|
||||
`)
|
||||
: html`<div style="color: var(--ecoos-text-dim)">No interfaces detected</div>`
|
||||
: html`<div class="empty-text">No network interfaces</div>`
|
||||
}
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Disks Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Disks</div>
|
||||
<!-- Storage -->
|
||||
<dees-panel .title=${'Storage'}>
|
||||
${this.systemInfo.disks?.length
|
||||
? this.systemInfo.disks.map(d => html`
|
||||
<div class="disk-item">
|
||||
<div class="stat-label">${d.mountpoint}</div>
|
||||
<div class="stat-value">${formatBytes(d.used)} / ${formatBytes(d.total)}</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${d.usagePercent}%"></div>
|
||||
<div class="device-row">
|
||||
<div>
|
||||
<div class="device-name">${d.mountpoint}</div>
|
||||
<div class="device-secondary">${d.device}</div>
|
||||
</div>
|
||||
<div class="usage-info">
|
||||
<span class="usage-text">${formatBytes(d.used)} / ${formatBytes(d.total)}</span>
|
||||
<div class="progress-mini">
|
||||
<div class="progress-mini-bar ${this.getUsageClass(d.usagePercent || 0)}" style="width: ${d.usagePercent || 0}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
: html`<div style="color: var(--ecoos-text-dim)">No disks detected</div>`
|
||||
: html`<div class="empty-text">No disks</div>`
|
||||
}
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Input Devices Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Input Devices</div>
|
||||
<!-- Input Devices -->
|
||||
<dees-panel .title=${'Input Devices'}>
|
||||
${this.systemInfo.inputDevices?.length
|
||||
? this.systemInfo.inputDevices.map(d => html`
|
||||
<div class="device-item">
|
||||
<div class="device-row">
|
||||
<span class="device-name">${d.name}</span>
|
||||
<span class="device-type">${d.type}</span>
|
||||
<dees-badge .type=${'default'}>${d.type}</dees-badge>
|
||||
</div>
|
||||
`)
|
||||
: html`<div style="color: var(--ecoos-text-dim)">No input devices detected</div>`
|
||||
: html`<div class="empty-text">No input devices</div>`
|
||||
}
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Speakers Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Speakers</div>
|
||||
<!-- Audio Output -->
|
||||
<dees-panel .title=${'Audio Output'}>
|
||||
${this.systemInfo.speakers?.length
|
||||
? this.systemInfo.speakers.map(s => html`
|
||||
<div class="device-item">
|
||||
<div class="device-row">
|
||||
<span class="device-name">${s.description}</span>
|
||||
${s.isDefault ? html`<span class="device-default">Default</span>` : ''}
|
||||
${s.isDefault ? html`<dees-badge .type=${'success'}>Default</dees-badge>` : ''}
|
||||
</div>
|
||||
`)
|
||||
: html`<div style="color: var(--ecoos-text-dim)">No speakers detected</div>`
|
||||
: html`<div class="empty-text">No speakers</div>`
|
||||
}
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Microphones Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Microphones</div>
|
||||
<!-- Audio Input -->
|
||||
<dees-panel .title=${'Audio Input'}>
|
||||
${this.systemInfo.microphones?.length
|
||||
? this.systemInfo.microphones.map(m => html`
|
||||
<div class="device-item">
|
||||
<div class="device-row">
|
||||
<span class="device-name">${m.description}</span>
|
||||
${m.isDefault ? html`<span class="device-default">Default</span>` : ''}
|
||||
${m.isDefault ? html`<dees-badge .type=${'success'}>Default</dees-badge>` : ''}
|
||||
</div>
|
||||
`)
|
||||
: html`<div style="color: var(--ecoos-text-dim)">No microphones detected</div>`
|
||||
: html`<div class="empty-text">No microphones</div>`
|
||||
}
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getUsageClass(usage: number): string {
|
||||
if (usage > 90) return 'error';
|
||||
if (usage > 75) return 'warning';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* EcoOS Displays View
|
||||
* Display management with enable/disable/primary controls
|
||||
* Card-based display management
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
css,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
|
||||
|
||||
import { sharedStyles } from '../styles/shared.js';
|
||||
import type { IDisplayInfo } from '../../ts_interfaces/display.js';
|
||||
@@ -24,73 +25,96 @@ export class EcoosDisplays extends DeesElement {
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor message: string = '';
|
||||
|
||||
@state()
|
||||
private accessor messageError: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
sharedStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.display-item {
|
||||
.display-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.display-card {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.display-card.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.display-meta {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.display-item:last-child {
|
||||
border-bottom: none;
|
||||
.message-bar {
|
||||
margin-top: 16px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.display-info {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
.message-bar.success {
|
||||
background: hsla(142.1, 76.2%, 36.3%, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
.message-bar.error {
|
||||
background: hsla(0, 84.2%, 60.2%, 0.15);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.display-details {
|
||||
font-size: 11px;
|
||||
color: var(--ecoos-text-dim);
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.display-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
.disabled-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.display-actions .btn {
|
||||
padding: 4px 12px;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 12px;
|
||||
color: var(--ecoos-text-dim);
|
||||
margin: 16px 0 8px 0;
|
||||
.disabled-header {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
color: var(--ecoos-text);
|
||||
.disabled-header::before {
|
||||
content: '▶';
|
||||
font-size: 8px;
|
||||
transition: transform 150ms ease;
|
||||
}
|
||||
|
||||
.collapsed-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed-content.expanded {
|
||||
display: block;
|
||||
.disabled-header.open::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -99,102 +123,109 @@ export class EcoosDisplays extends DeesElement {
|
||||
const enabledDisplays = this.displays.filter(d => d.active);
|
||||
const disabledDisplays = this.displays.filter(d => !d.active);
|
||||
|
||||
if (this.displays.length === 0) {
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Displays</div>
|
||||
|
||||
${this.displays.length === 0
|
||||
? html`<div style="color: var(--ecoos-text-dim)">No displays detected</div>`
|
||||
: html`
|
||||
<!-- Enabled Displays -->
|
||||
${enabledDisplays.map(d => this.renderDisplayItem(d))}
|
||||
|
||||
<!-- Disabled Displays -->
|
||||
${disabledDisplays.length > 0 ? html`
|
||||
<details style="margin-top: 12px;">
|
||||
<summary class="section-header">
|
||||
Disabled Displays (${disabledDisplays.length})
|
||||
</summary>
|
||||
<div style="margin-top: 8px;">
|
||||
${disabledDisplays.map(d => this.renderDisplayItem(d))}
|
||||
</div>
|
||||
</details>
|
||||
` : ''}
|
||||
`
|
||||
}
|
||||
<div class="empty-state">
|
||||
No displays detected
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDisplayItem(display: IDisplayInfo): TemplateResult {
|
||||
return html`
|
||||
<div class="display-item">
|
||||
<div class="display-info">
|
||||
<div class="display-name">${display.name}</div>
|
||||
<div class="display-details">
|
||||
${display.width}x${display.height} @ ${display.refreshRate}Hz
|
||||
${display.make !== 'Unknown' ? ` • ${display.make}` : ''}
|
||||
<div class="display-grid">
|
||||
${enabledDisplays.map(d => this.renderDisplayCard(d))}
|
||||
</div>
|
||||
|
||||
${disabledDisplays.length > 0 ? html`
|
||||
<details class="disabled-section">
|
||||
<summary class="disabled-header">
|
||||
Disabled Displays (${disabledDisplays.length})
|
||||
</summary>
|
||||
<div class="display-grid" style="margin-top: 12px;">
|
||||
${disabledDisplays.map(d => this.renderDisplayCard(d))}
|
||||
</div>
|
||||
<div class="display-actions">
|
||||
${display.isPrimary
|
||||
? html`<span class="device-default">Primary</span>`
|
||||
: display.active
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click=${() => this.setKioskDisplay(display.name)}
|
||||
?disabled=${this.loading}
|
||||
>
|
||||
Set Primary
|
||||
</button>
|
||||
`
|
||||
: ''
|
||||
</details>
|
||||
` : ''}
|
||||
|
||||
${this.message ? html`
|
||||
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
<button
|
||||
class="btn ${display.active ? 'btn-danger' : 'btn-primary'}"
|
||||
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
||||
?disabled=${this.loading}
|
||||
|
||||
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
|
||||
return html`
|
||||
<dees-panel
|
||||
class="display-card ${display.active ? '' : 'disabled'}"
|
||||
.title=${display.name}
|
||||
.subtitle=${`${display.width}×${display.height} @ ${display.refreshRate}Hz`}
|
||||
.variant=${display.active ? 'default' : 'ghost'}
|
||||
>
|
||||
${display.active ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
${display.make && display.make !== 'Unknown' ? html`
|
||||
<div class="display-meta">${display.make}${display.model ? ` ${display.model}` : ''}</div>
|
||||
` : ''}
|
||||
|
||||
${display.isPrimary ? html`
|
||||
<div class="badge-row">
|
||||
<dees-badge .type=${'primary'}>Primary</dees-badge>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="actions-row">
|
||||
${display.active && !display.isPrimary ? html`
|
||||
<dees-button
|
||||
.type=${'default'}
|
||||
.text=${'Set Primary'}
|
||||
.disabled=${this.loading}
|
||||
@click=${() => this.setPrimary(display.name)}
|
||||
></dees-button>
|
||||
` : ''}
|
||||
<dees-button
|
||||
.type=${'default'}
|
||||
.status=${display.active ? 'error' : 'success'}
|
||||
.text=${display.active ? 'Disable' : 'Enable'}
|
||||
.disabled=${this.loading}
|
||||
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
||||
></dees-button>
|
||||
</div>
|
||||
</dees-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
try {
|
||||
const action = enable ? 'enable' : 'disable';
|
||||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.message);
|
||||
}
|
||||
this.message = result.message;
|
||||
this.messageError = !result.success;
|
||||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`);
|
||||
this.message = `Error: ${error}`;
|
||||
this.messageError = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async setKioskDisplay(name: string): Promise<void> {
|
||||
private async setPrimary(name: string): Promise<void> {
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
try {
|
||||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.message);
|
||||
}
|
||||
this.message = result.message;
|
||||
this.messageError = !result.success;
|
||||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`);
|
||||
this.message = `Error: ${error}`;
|
||||
this.messageError = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* EcoOS Logs View
|
||||
* Tabbed log viewer for daemon and system logs with auto-scroll
|
||||
* Panel-wrapped terminal-style log viewer
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
css,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesPanel } from '@design.estate/dees-catalog';
|
||||
|
||||
import { sharedStyles } from '../styles/shared.js';
|
||||
|
||||
@@ -26,46 +27,125 @@ export class EcoosLogs extends DeesElement {
|
||||
@state()
|
||||
private accessor activeTab: 'daemon' | 'system' = 'daemon';
|
||||
|
||||
@state()
|
||||
private accessor autoScroll: boolean = true;
|
||||
|
||||
public static styles = [
|
||||
sharedStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background: var(--ecoos-card);
|
||||
border: 1px solid var(--ecoos-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 140px);
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.logs-content {
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
background: #0d0d0d;
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
font-family: 'SF Mono', monospace;
|
||||
}
|
||||
|
||||
.auto-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.auto-scroll:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.auto-scroll.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.auto-scroll .indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.auto-scroll.active .indicator {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.terminal {
|
||||
flex: 1;
|
||||
background: hsl(0 0% 2%);
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
.line {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
padding: 2px 0;
|
||||
padding: 1px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: var(--ecoos-text-dim);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
.line.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.line.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -74,53 +154,88 @@ export class EcoosLogs extends DeesElement {
|
||||
const logs = this.activeTab === 'daemon' ? this.daemonLogs : this.systemLogs;
|
||||
|
||||
return html`
|
||||
<div class="logs-container">
|
||||
<div class="logs-header">
|
||||
<dees-panel .title=${'Logs'}>
|
||||
<div class="container">
|
||||
<div class="tabs">
|
||||
<div
|
||||
class="tab ${this.activeTab === 'daemon' ? 'active' : ''}"
|
||||
@click=${() => this.switchTab('daemon')}
|
||||
>
|
||||
Daemon Logs
|
||||
</div>
|
||||
>Daemon</div>
|
||||
<div
|
||||
class="tab ${this.activeTab === 'system' ? 'active' : ''}"
|
||||
@click=${() => this.switchTab('system')}
|
||||
>
|
||||
System Logs
|
||||
>System</div>
|
||||
</div>
|
||||
|
||||
<div class="header-row">
|
||||
<span class="count">${logs.length} lines</span>
|
||||
<div
|
||||
class="auto-scroll ${this.autoScroll ? 'active' : ''}"
|
||||
@click=${this.toggleAutoScroll}
|
||||
>
|
||||
<span class="indicator"></span>
|
||||
Auto-scroll
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-content" id="logs-content">
|
||||
<div class="terminal" id="terminal" @scroll=${this.handleScroll}>
|
||||
${logs.length === 0
|
||||
? html`<div class="empty-message">No logs available</div>`
|
||||
: logs.map(log => html`<div class="log-entry">${log}</div>`)
|
||||
? html`<div class="empty-logs">No logs</div>`
|
||||
: logs.map(log => html`<div class="line ${this.getLogLevel(log)}">${log}</div>`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private getLogLevel(log: string): string {
|
||||
const lower = log.toLowerCase();
|
||||
if (lower.includes('error') || lower.includes('fail') || lower.includes('fatal')) {
|
||||
return 'error';
|
||||
}
|
||||
if (lower.includes('warn')) {
|
||||
return 'warning';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private switchTab(tab: 'daemon' | 'system'): void {
|
||||
this.activeTab = tab;
|
||||
if (this.autoScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private toggleAutoScroll(): void {
|
||||
this.autoScroll = !this.autoScroll;
|
||||
if (this.autoScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private handleScroll(): void {
|
||||
const terminal = this.shadowRoot?.getElementById('terminal');
|
||||
if (terminal) {
|
||||
const isAtBottom = terminal.scrollHeight - terminal.scrollTop <= terminal.clientHeight + 50;
|
||||
if (!isAtBottom && this.autoScroll) {
|
||||
this.autoScroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Auto-scroll when logs change
|
||||
if (changedProperties.has('daemonLogs') || changedProperties.has('systemLogs')) {
|
||||
if ((changedProperties.has('daemonLogs') || changedProperties.has('systemLogs')) && this.autoScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
requestAnimationFrame(() => {
|
||||
const content = this.shadowRoot?.getElementById('logs-content');
|
||||
if (content) {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
const terminal = this.shadowRoot?.getElementById('terminal');
|
||||
if (terminal) {
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* EcoOS Overview View
|
||||
* Shows services status, CPU, memory, system info, and controls
|
||||
* Dashboard with stats grid, service panels, and system info
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -11,6 +11,13 @@ import {
|
||||
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';
|
||||
@@ -23,171 +30,275 @@ export class EcoosOverview extends DeesElement {
|
||||
@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: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.service-status {
|
||||
.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);
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
margin-top: 16px;
|
||||
.service-row:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.control-status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--ecoos-text-dim);
|
||||
.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>Loading...</div>`;
|
||||
return html`<div class="empty">Loading...</div>`;
|
||||
}
|
||||
|
||||
const { systemInfo, sway, chromium, swayStatus, chromiumStatus } = this.status;
|
||||
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="grid">
|
||||
<!-- Services Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Services</div>
|
||||
<div class="service-status">
|
||||
<span class="status-dot ${this.getStatusClass(swayStatus)}"></span>
|
||||
<span>Sway Compositor</span>
|
||||
<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>
|
||||
<div class="service-status">
|
||||
<span class="status-dot ${this.getStatusClass(chromiumStatus)}"></span>
|
||||
<span>Chromium Browser</span>
|
||||
${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>
|
||||
|
||||
<!-- CPU Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">CPU</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Model</div>
|
||||
<div class="stat-value">${systemInfo?.cpu?.model || '-'}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Cores</div>
|
||||
<div class="stat-value">${systemInfo?.cpu?.cores || '-'}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Usage</div>
|
||||
<div class="stat-value">${systemInfo?.cpu?.usage || 0}%</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${systemInfo?.cpu?.usage || 0}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Memory</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Used / Total</div>
|
||||
<div class="stat-value">
|
||||
${formatBytes(systemInfo?.memory?.used || 0)} / ${formatBytes(systemInfo?.memory?.total || 0)}
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${systemInfo?.memory?.usagePercent || 0}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">System</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Hostname</div>
|
||||
<div class="stat-value">${systemInfo?.hostname || '-'}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Uptime</div>
|
||||
<div class="stat-value">${formatUptime(systemInfo?.uptime || 0)}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">GPU</div>
|
||||
<div class="stat-value">
|
||||
${systemInfo?.gpu?.length ? systemInfo.gpu.map(g => g.name).join(', ') : 'None detected'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Controls</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
<!-- Actions -->
|
||||
<dees-panel .title=${'Actions'}>
|
||||
<div class="actions-row">
|
||||
<dees-button
|
||||
.type=${'default'}
|
||||
.text=${'Restart Browser'}
|
||||
.disabled=${this.loading}
|
||||
@click=${this.restartChromium}
|
||||
?disabled=${this.loading || !sway}
|
||||
>
|
||||
Restart Browser
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
></dees-button>
|
||||
<dees-button
|
||||
.type=${'default'}
|
||||
.status=${'error'}
|
||||
.text=${'Reboot System'}
|
||||
.disabled=${this.loading}
|
||||
@click=${this.rebootSystem}
|
||||
?disabled=${this.loading}
|
||||
>
|
||||
Reboot System
|
||||
</button>
|
||||
<div class="control-status" id="control-status"></div>
|
||||
></dees-button>
|
||||
${this.controlMessage ? html`
|
||||
<span class="message ${this.controlError ? 'error' : 'success'}">${this.controlMessage}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getStatusClass(status: IServiceStatus): string {
|
||||
switch (status?.state) {
|
||||
case 'running': return 'running';
|
||||
case 'starting': return 'starting';
|
||||
default: return 'stopped';
|
||||
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.showControlStatus(result.message, !result.success);
|
||||
this.controlMessage = result.message;
|
||||
this.controlError = !result.success;
|
||||
} catch (error) {
|
||||
this.showControlStatus(`Error: ${error}`, true);
|
||||
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 the system?')) return;
|
||||
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.showControlStatus(result.message, !result.success);
|
||||
this.controlMessage = result.message;
|
||||
this.controlError = !result.success;
|
||||
} catch (error) {
|
||||
this.showControlStatus(`Error: ${error}`, true);
|
||||
this.controlMessage = `Error: ${error}`;
|
||||
this.controlError = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private showControlStatus(message: string, isError: boolean): void {
|
||||
const el = this.shadowRoot?.getElementById('control-status');
|
||||
if (el) {
|
||||
el.textContent = message;
|
||||
el.style.color = isError ? 'var(--ecoos-error)' : 'var(--ecoos-success)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* EcoOS Updates View
|
||||
* Version info, available updates, and upgrade controls
|
||||
* Card-based update management
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
css,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
|
||||
|
||||
import { sharedStyles, formatAge } from '../styles/shared.js';
|
||||
import type { IUpdateInfo, IRelease } from '../../ts_interfaces/updates.js';
|
||||
import type { IUpdateInfo } from '../../ts_interfaces/updates.js';
|
||||
|
||||
@customElement('ecoos-updates')
|
||||
export class EcoosUpdates extends DeesElement {
|
||||
@@ -24,130 +25,174 @@ export class EcoosUpdates extends DeesElement {
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor message: string = '';
|
||||
|
||||
@state()
|
||||
private accessor messageError: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
sharedStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.release-item {
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.version-display {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', monospace;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.last-check {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.banner-upgrade {
|
||||
background: hsla(217.2, 91.2%, 59.8%, 0.1);
|
||||
border-color: hsla(217.2, 91.2%, 59.8%, 0.3);
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.banner-content strong {
|
||||
font-family: 'SF Mono', monospace;
|
||||
}
|
||||
|
||||
.update-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.release-item:last-child {
|
||||
.update-row:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.release-version {
|
||||
.update-row:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.update-version {
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.release-age {
|
||||
color: var(--ecoos-text-dim);
|
||||
font-size: 12px;
|
||||
.update-age {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.release-item .btn {
|
||||
padding: 4px 12px;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
.empty-text {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-tertiary);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.auto-upgrade-status {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--ecoos-text-dim);
|
||||
.message-bar {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
.message-bar.success {
|
||||
background: hsla(142.1, 76.2%, 36.3%, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.message-bar.error {
|
||||
background: hsla(0, 84.2%, 60.2%, 0.15);
|
||||
color: var(--error);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.updateInfo) {
|
||||
return html`<div>Loading...</div>`;
|
||||
return html`<div class="empty">Loading...</div>`;
|
||||
}
|
||||
|
||||
const newerReleases = this.updateInfo.releases.filter(r => r.isNewer);
|
||||
const { autoUpgrade, lastCheck } = this.updateInfo;
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Updates</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-label">Current Version</div>
|
||||
<div class="stat-value">v${this.updateInfo.currentVersion}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin: 16px 0;">
|
||||
${newerReleases.length === 0
|
||||
? html`<div style="color: var(--ecoos-text-dim)">No updates available</div>`
|
||||
: newerReleases.map(r => this.renderRelease(r))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="auto-upgrade-status">
|
||||
${this.renderAutoUpgradeStatus()}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
<div class="page">
|
||||
<!-- Current Version -->
|
||||
<dees-panel .title=${'Current Version'}>
|
||||
<div class="version-display">v${this.updateInfo.currentVersion}</div>
|
||||
${lastCheck ? html`<div class="last-check">Last check: ${new Date(lastCheck).toLocaleString()}</div>` : ''}
|
||||
<dees-button
|
||||
.type=${'default'}
|
||||
.text=${this.loading ? 'Checking...' : 'Check for Updates'}
|
||||
.disabled=${this.loading}
|
||||
@click=${this.checkForUpdates}
|
||||
?disabled=${this.loading}
|
||||
>
|
||||
Check for Updates
|
||||
</button>
|
||||
></dees-button>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Auto-upgrade Banner -->
|
||||
${autoUpgrade?.targetVersion ? html`
|
||||
<dees-panel .variant=${'outline'} class="banner-upgrade">
|
||||
<div class="banner-content">
|
||||
${autoUpgrade.waitingForStability
|
||||
? html`Auto-upgrade to <strong>v${autoUpgrade.targetVersion}</strong> in ${autoUpgrade.scheduledIn}`
|
||||
: html`Auto-upgrade to <strong>v${autoUpgrade.targetVersion}</strong> pending`
|
||||
}
|
||||
</div>
|
||||
</dees-panel>
|
||||
` : ''}
|
||||
|
||||
<!-- Available Updates -->
|
||||
<dees-panel .title=${'Available Updates'}>
|
||||
${newerReleases.length === 0
|
||||
? html`<div class="empty-text">You're up to date</div>`
|
||||
: newerReleases.map(r => html`
|
||||
<div class="update-row">
|
||||
<span class="update-version">v${r.version}</span>
|
||||
<span class="update-age">${formatAge(r.ageHours)}</span>
|
||||
<dees-button
|
||||
.type=${'default'}
|
||||
.status=${'success'}
|
||||
.text=${'Upgrade'}
|
||||
.disabled=${this.loading}
|
||||
@click=${() => this.upgradeToVersion(r.version)}
|
||||
></dees-button>
|
||||
</div>
|
||||
`)
|
||||
}
|
||||
</dees-panel>
|
||||
|
||||
<!-- Message -->
|
||||
${this.message ? html`
|
||||
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRelease(release: IRelease): TemplateResult {
|
||||
return html`
|
||||
<div class="release-item">
|
||||
<span>
|
||||
<span class="release-version">v${release.version}</span>
|
||||
<span class="release-age">(${formatAge(release.ageHours)})</span>
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click=${() => this.upgradeToVersion(release.version)}
|
||||
?disabled=${this.loading}
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAutoUpgradeStatus(): TemplateResult {
|
||||
const { autoUpgrade, lastCheck } = this.updateInfo!;
|
||||
|
||||
if (autoUpgrade.targetVersion) {
|
||||
if (autoUpgrade.waitingForStability) {
|
||||
return html`Auto-upgrade to v${autoUpgrade.targetVersion} in ${autoUpgrade.scheduledIn} (stability period)`;
|
||||
}
|
||||
return html`Auto-upgrade to v${autoUpgrade.targetVersion} pending...`;
|
||||
}
|
||||
|
||||
if (lastCheck) {
|
||||
return html`Last checked: ${new Date(lastCheck).toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
return html``;
|
||||
}
|
||||
|
||||
private async checkForUpdates(): Promise<void> {
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
try {
|
||||
const response = await fetch('/api/updates/check', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
@@ -155,15 +200,18 @@ export class EcoosUpdates extends DeesElement {
|
||||
this.dispatchEvent(new CustomEvent('updates-checked', { detail: result }));
|
||||
} catch (error) {
|
||||
console.error('Failed to check updates:', error);
|
||||
this.message = `Failed: ${error}`;
|
||||
this.messageError = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async upgradeToVersion(version: string): Promise<void> {
|
||||
if (!confirm(`Upgrade to version ${version}? The daemon will restart.`)) return;
|
||||
if (!confirm(`Upgrade to v${version}?`)) return;
|
||||
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
try {
|
||||
const response = await fetch('/api/upgrade', {
|
||||
method: 'POST',
|
||||
@@ -172,12 +220,16 @@ export class EcoosUpdates extends DeesElement {
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.message = result.message;
|
||||
this.messageError = false;
|
||||
this.dispatchEvent(new CustomEvent('upgrade-started', { detail: result }));
|
||||
} else {
|
||||
alert(`Upgrade failed: ${result.message}`);
|
||||
this.message = `Failed: ${result.message}`;
|
||||
this.messageError = true;
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Upgrade error: ${error}`);
|
||||
this.message = `Error: ${error}`;
|
||||
this.messageError = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -13,15 +13,3 @@ import './elements/ecoos-logs.js';
|
||||
|
||||
// Export the main app component
|
||||
export { EcoosApp } from './elements/ecoos-app.js';
|
||||
|
||||
// Create and mount the app when DOM is ready
|
||||
function init() {
|
||||
const app = document.createElement('ecoos-app');
|
||||
document.body.appendChild(app);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -1,200 +1,410 @@
|
||||
/**
|
||||
* Shared styles for EcoOS UI components
|
||||
* EcoOS UI Design System
|
||||
* Based on dees-catalog design patterns
|
||||
*/
|
||||
|
||||
import { css } from '@design.estate/dees-element';
|
||||
|
||||
export const sharedStyles = css`
|
||||
:host {
|
||||
--ecoos-bg: #0a0a0a;
|
||||
--ecoos-card: #141414;
|
||||
--ecoos-border: #2a2a2a;
|
||||
--ecoos-text: #e0e0e0;
|
||||
--ecoos-text-dim: #888;
|
||||
--ecoos-accent: #3b82f6;
|
||||
--ecoos-success: #22c55e;
|
||||
--ecoos-warning: #f59e0b;
|
||||
--ecoos-error: #ef4444;
|
||||
/* Colors - dees-catalog theme (HSL) */
|
||||
--bg: hsl(0 0% 3.9%);
|
||||
--bg-elevated: hsl(0 0% 7.8%);
|
||||
--bg-hover: hsl(0 0% 14.9%);
|
||||
--border: hsl(0 0% 14.9%);
|
||||
--border-hover: hsl(0 0% 20.9%);
|
||||
--text: hsl(0 0% 95%);
|
||||
--text-secondary: hsl(215 20.2% 55.1%);
|
||||
--text-tertiary: hsl(215 20.2% 45%);
|
||||
|
||||
/* Semantic colors */
|
||||
--accent: hsl(217.2 91.2% 59.8%);
|
||||
--success: hsl(142.1 76.2% 36.3%);
|
||||
--warning: hsl(45.4 93.4% 47.5%);
|
||||
--error: hsl(0 84.2% 60.2%);
|
||||
|
||||
/* Typography scale */
|
||||
--text-xs: 11px;
|
||||
--text-sm: 12px;
|
||||
--text-base: 13px;
|
||||
--text-lg: 15px;
|
||||
--text-xl: 18px;
|
||||
--text-2xl: 24px;
|
||||
|
||||
display: block;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: var(--ecoos-text);
|
||||
font-size: var(--text-base);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--ecoos-card);
|
||||
border: 1px solid var(--ecoos-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
/* Monospace utility */
|
||||
.mono {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: var(--ecoos-text-dim);
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
/* Section - lightweight container */
|
||||
.section {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--ecoos-text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: var(--ecoos-border);
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--ecoos-accent);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-dot.running {
|
||||
background: var(--ecoos-success);
|
||||
}
|
||||
|
||||
.status-dot.stopped {
|
||||
background: var(--ecoos-error);
|
||||
}
|
||||
|
||||
.status-dot.starting {
|
||||
background: var(--ecoos-warning);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ecoos-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--ecoos-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
}
|
||||
|
||||
.device-item:last-child {
|
||||
.section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
.section-title {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.device-type {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--ecoos-border);
|
||||
color: var(--ecoos-text-dim);
|
||||
}
|
||||
|
||||
.device-default {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--ecoos-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logs {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
background: #0d0d0d;
|
||||
/* Card - minimal styling */
|
||||
.card {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
/* Table styling */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--text-tertiary);
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tr:hover td {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.table .mono {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Status dot */
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.dot.success { background: var(--success); }
|
||||
.dot.warning { background: var(--warning); }
|
||||
.dot.error { background: var(--error); }
|
||||
.dot.accent { background: var(--accent); }
|
||||
|
||||
.dot.pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Status with dot and text */
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.status.success { color: var(--success); }
|
||||
.status.warning { color: var(--warning); }
|
||||
.status.error { color: var(--error); }
|
||||
|
||||
/* Progress bar - thin */
|
||||
.progress {
|
||||
height: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 1.5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
transition: width 300ms ease;
|
||||
}
|
||||
|
||||
.progress-bar.success { background: var(--success); }
|
||||
.progress-bar.warning { background: var(--warning); }
|
||||
.progress-bar.error { background: var(--error); }
|
||||
|
||||
/* Badge - compact */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.badge.primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge.success {
|
||||
background: rgba(12, 206, 107, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.badge.warning {
|
||||
background: rgba(245, 166, 35, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.badge.error {
|
||||
background: rgba(238, 0, 0, 0.15);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* Data row - key value pair */
|
||||
.data-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.data-row + .data-row {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Stat - large value display */
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', monospace;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.stat-value.sm {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* Grid layouts */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-auto {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
/* Flex utilities */
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-4 { gap: 4px; }
|
||||
.gap-6 { gap: 6px; }
|
||||
.gap-8 { gap: 8px; }
|
||||
.gap-12 { gap: 12px; }
|
||||
|
||||
/* Tabs - underline style */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--ecoos-border);
|
||||
margin-bottom: 12px;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
padding: 8px 12px;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
color: var(--ecoos-text-dim);
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--ecoos-text);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--ecoos-accent);
|
||||
border-bottom-color: var(--ecoos-accent);
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--text);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* Actions row */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Collapsible details */
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details summary::before {
|
||||
content: '▶';
|
||||
font-size: 8px;
|
||||
transition: transform 150ms ease;
|
||||
}
|
||||
|
||||
details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.details-content {
|
||||
padding: 8px 0 8px 14px;
|
||||
}
|
||||
|
||||
/* Alert/Banner - slim */
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: var(--text-sm);
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 112, 243, 0.1);
|
||||
border: 1px solid rgba(0, 112, 243, 0.2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.banner.success {
|
||||
background: rgba(12, 206, 107, 0.1);
|
||||
border-color: rgba(12, 206, 107, 0.2);
|
||||
}
|
||||
|
||||
.banner.warning {
|
||||
background: rgba(245, 166, 35, 0.1);
|
||||
border-color: rgba(245, 166, 35, 0.2);
|
||||
}
|
||||
|
||||
.banner.error {
|
||||
background: rgba(238, 0, 0, 0.1);
|
||||
border-color: rgba(238, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Scrollbar - minimal */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
}
|
||||
|
||||
/* Text utilities */
|
||||
.text-xs { font-size: var(--text-xs); }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
.text-base { font-size: var(--text-base); }
|
||||
.text-lg { font-size: var(--text-lg); }
|
||||
.text-secondary { color: var(--text-secondary); }
|
||||
.text-tertiary { color: var(--text-tertiary); }
|
||||
`;
|
||||
|
||||
/**
|
||||
|
||||
Binary file not shown.
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "@ecobridge/eco-os",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "[ -z \"$CI\" ] && npm version patch --no-git-tag-version || true && node -e \"const v=require('./package.json').version; require('fs').writeFileSync('ecoos_daemon/ts/version.ts', 'export const VERSION = \\\"'+v+'\\\";\\n');\" && pnpm run daemon:bundle && cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/ && mkdir -p .nogit/iso && docker build --no-cache -t ecoos-builder -f isobuild/Dockerfile . && docker run --privileged --name ecoos-build ecoos-builder && docker cp ecoos-build:/output/ecoos.iso .nogit/iso/ecoos.iso && docker rm ecoos-build",
|
||||
"build": "[ -z \"$CI\" ] && npm version patch --no-git-tag-version || true && node -e \"const v=require('./package.json').version; require('fs').writeFileSync('ecoos_daemon/ts/version.ts', 'export const VERSION = \\\"'+v+'\\\";\\n');\" && pnpm run daemon:ui && pnpm run daemon:bundle && cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/ && mkdir -p .nogit/iso && docker build --no-cache -t ecoos-builder -f isobuild/Dockerfile . && docker run --privileged --name ecoos-build ecoos-builder && docker cp ecoos-build:/output/ecoos.iso .nogit/iso/ecoos.iso && docker rm ecoos-build",
|
||||
"daemon:dev": "cd ecoos_daemon && deno run --allow-all --watch mod.ts",
|
||||
"daemon:start": "cd ecoos_daemon && deno run --allow-all mod.ts",
|
||||
"daemon:typecheck": "cd ecoos_daemon && deno check mod.ts",
|
||||
"daemon:bundle": "cd ecoos_daemon && deno compile --allow-all --output bundle/eco-daemon mod.ts",
|
||||
"daemon:ui": "cd ecoos_daemon && pnpm run build",
|
||||
"daemon:bundle": "cd ecoos_daemon && pnpm run build && deno compile --allow-all --output bundle/eco-daemon mod.ts",
|
||||
"test": "pnpm run test:clean && cd isotest && ./run-test.sh",
|
||||
"test:screenshot": "cd isotest && ./screenshot.sh",
|
||||
"test:screenshot:loop": "while true; do pnpm run test:screenshot; sleep 5; done",
|
||||
|
||||
Reference in New Issue
Block a user