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 {
|
export class SystemInfo {
|
||||||
|
private lastCpuStats: { total: number; idle: number } | null = null;
|
||||||
|
|
||||||
async getInfo(): Promise<SystemInfoData> {
|
async getInfo(): Promise<SystemInfoData> {
|
||||||
const [hostname, cpu, memory, disks, network, gpu, uptime, inputDevices, speakers, microphones] =
|
const [hostname, cpu, memory, disks, network, gpu, uptime, inputDevices, speakers, microphones] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -112,13 +114,23 @@ export class SystemInfo {
|
|||||||
const modelMatch = cpuinfo.match(/model name\s*:\s*(.+)/);
|
const modelMatch = cpuinfo.match(/model name\s*:\s*(.+)/);
|
||||||
const coreMatches = cpuinfo.match(/processor\s*:/g);
|
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 stat = await Deno.readTextFile('/proc/stat');
|
||||||
const cpuLine = stat.split('\n')[0];
|
const cpuLine = stat.split('\n')[0];
|
||||||
const values = cpuLine.split(/\s+/).slice(1).map(Number);
|
const values = cpuLine.split(/\s+/).slice(1).map(Number);
|
||||||
const total = values.reduce((a, b) => a + b, 0);
|
const total = values.reduce((a, b) => a + b, 0);
|
||||||
const idle = values[3];
|
const idle = values[3] + values[4]; // idle + iowait
|
||||||
const usage = ((total - idle) / total) * 100;
|
|
||||||
|
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 {
|
return {
|
||||||
model: modelMatch ? modelMatch[1] : 'Unknown',
|
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
|
* EcoOS Devices View
|
||||||
* Shows network interfaces, disks, input devices, speakers, and microphones
|
* Card-based view for network, storage, input, and audio devices
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
import { sharedStyles, formatBytes } from '../styles/shared.js';
|
import { sharedStyles, formatBytes } from '../styles/shared.js';
|
||||||
import type { ISystemInfo } from '../../ts_interfaces/status.js';
|
import type { ISystemInfo } from '../../ts_interfaces/status.js';
|
||||||
@@ -25,106 +26,192 @@ export class EcoosDevices extends DeesElement {
|
|||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: 8px 0;
|
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;
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disk-item {
|
.device-row:first-child {
|
||||||
margin-bottom: 12px;
|
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 {
|
render(): TemplateResult {
|
||||||
if (!this.systemInfo) {
|
if (!this.systemInfo) {
|
||||||
return html`<div>Loading...</div>`;
|
return html`<div class="empty">Loading...</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="grid">
|
<div class="cards-grid">
|
||||||
<!-- Network Card -->
|
<!-- Network -->
|
||||||
<div class="card">
|
<dees-panel .title=${'Network'}>
|
||||||
<div class="card-title">Network</div>
|
|
||||||
${this.systemInfo.network?.length
|
${this.systemInfo.network?.length
|
||||||
? this.systemInfo.network.map(n => html`
|
? this.systemInfo.network.map(n => html`
|
||||||
<div class="network-item">
|
<div class="device-row">
|
||||||
<span>${n.name}</span>
|
<span class="device-name">${n.name}</span>
|
||||||
<span>${n.ip}</span>
|
<span class="device-info">${n.ip || '—'}</span>
|
||||||
|
<dees-badge .type=${n.state === 'up' ? 'success' : 'error'}>${n.state}</dees-badge>
|
||||||
</div>
|
</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 -->
|
<!-- Storage -->
|
||||||
<div class="card">
|
<dees-panel .title=${'Storage'}>
|
||||||
<div class="card-title">Disks</div>
|
|
||||||
${this.systemInfo.disks?.length
|
${this.systemInfo.disks?.length
|
||||||
? this.systemInfo.disks.map(d => html`
|
? this.systemInfo.disks.map(d => html`
|
||||||
<div class="disk-item">
|
<div class="device-row">
|
||||||
<div class="stat-label">${d.mountpoint}</div>
|
<div>
|
||||||
<div class="stat-value">${formatBytes(d.used)} / ${formatBytes(d.total)}</div>
|
<div class="device-name">${d.mountpoint}</div>
|
||||||
<div class="progress-bar">
|
<div class="device-secondary">${d.device}</div>
|
||||||
<div class="progress-fill" style="width: ${d.usagePercent}%"></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>
|
||||||
</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 -->
|
<!-- Input Devices -->
|
||||||
<div class="card">
|
<dees-panel .title=${'Input Devices'}>
|
||||||
<div class="card-title">Input Devices</div>
|
|
||||||
${this.systemInfo.inputDevices?.length
|
${this.systemInfo.inputDevices?.length
|
||||||
? this.systemInfo.inputDevices.map(d => html`
|
? this.systemInfo.inputDevices.map(d => html`
|
||||||
<div class="device-item">
|
<div class="device-row">
|
||||||
<span class="device-name">${d.name}</span>
|
<span class="device-name">${d.name}</span>
|
||||||
<span class="device-type">${d.type}</span>
|
<dees-badge .type=${'default'}>${d.type}</dees-badge>
|
||||||
</div>
|
</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 -->
|
<!-- Audio Output -->
|
||||||
<div class="card">
|
<dees-panel .title=${'Audio Output'}>
|
||||||
<div class="card-title">Speakers</div>
|
|
||||||
${this.systemInfo.speakers?.length
|
${this.systemInfo.speakers?.length
|
||||||
? this.systemInfo.speakers.map(s => html`
|
? this.systemInfo.speakers.map(s => html`
|
||||||
<div class="device-item">
|
<div class="device-row">
|
||||||
<span class="device-name">${s.description}</span>
|
<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>
|
</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 -->
|
<!-- Audio Input -->
|
||||||
<div class="card">
|
<dees-panel .title=${'Audio Input'}>
|
||||||
<div class="card-title">Microphones</div>
|
|
||||||
${this.systemInfo.microphones?.length
|
${this.systemInfo.microphones?.length
|
||||||
? this.systemInfo.microphones.map(m => html`
|
? this.systemInfo.microphones.map(m => html`
|
||||||
<div class="device-item">
|
<div class="device-row">
|
||||||
<span class="device-name">${m.description}</span>
|
<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>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUsageClass(usage: number): string {
|
||||||
|
if (usage > 90) return 'error';
|
||||||
|
if (usage > 75) return 'warning';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* EcoOS Displays View
|
* EcoOS Displays View
|
||||||
* Display management with enable/disable/primary controls
|
* Card-based display management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
import { sharedStyles } from '../styles/shared.js';
|
import { sharedStyles } from '../styles/shared.js';
|
||||||
import type { IDisplayInfo } from '../../ts_interfaces/display.js';
|
import type { IDisplayInfo } from '../../ts_interfaces/display.js';
|
||||||
@@ -24,73 +25,96 @@ export class EcoosDisplays extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor loading: boolean = false;
|
private accessor loading: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor message: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor messageError: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
sharedStyles,
|
sharedStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
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;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 0;
|
flex-wrap: wrap;
|
||||||
border-bottom: 1px solid var(--ecoos-border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-item:last-child {
|
.message-bar {
|
||||||
border-bottom: none;
|
margin-top: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-info {
|
.message-bar.success {
|
||||||
flex: 1;
|
background: hsla(142.1, 76.2%, 36.3%, 0.15);
|
||||||
min-width: 150px;
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-name {
|
.message-bar.error {
|
||||||
font-weight: 500;
|
background: hsla(0, 84.2%, 60.2%, 0.15);
|
||||||
font-size: 14px;
|
color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-details {
|
.empty-state {
|
||||||
font-size: 11px;
|
text-align: center;
|
||||||
color: var(--ecoos-text-dim);
|
padding: 32px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-actions {
|
.disabled-section {
|
||||||
display: flex;
|
margin-top: 16px;
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-actions .btn {
|
.disabled-header {
|
||||||
padding: 4px 12px;
|
font-size: var(--text-sm);
|
||||||
margin: 0;
|
color: var(--text-tertiary);
|
||||||
font-size: 11px;
|
margin-bottom: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ecoos-text-dim);
|
|
||||||
margin: 16px 0 8px 0;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header:hover {
|
.disabled-header::before {
|
||||||
color: var(--ecoos-text);
|
content: '▶';
|
||||||
|
font-size: 8px;
|
||||||
|
transition: transform 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-content {
|
.disabled-header.open::before {
|
||||||
display: none;
|
transform: rotate(90deg);
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed-content.expanded {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -99,102 +123,109 @@ export class EcoosDisplays extends DeesElement {
|
|||||||
const enabledDisplays = this.displays.filter(d => d.active);
|
const enabledDisplays = this.displays.filter(d => d.active);
|
||||||
const disabledDisplays = this.displays.filter(d => !d.active);
|
const disabledDisplays = this.displays.filter(d => !d.active);
|
||||||
|
|
||||||
|
if (this.displays.length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="empty-state">
|
||||||
|
No displays detected
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="card">
|
<div class="display-grid">
|
||||||
<div class="card-title">Displays</div>
|
${enabledDisplays.map(d => this.renderDisplayCard(d))}
|
||||||
|
|
||||||
${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>
|
</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>
|
||||||
|
</details>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${this.message ? html`
|
||||||
|
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
|
||||||
|
` : ''}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDisplayItem(display: IDisplayInfo): TemplateResult {
|
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="display-item">
|
<dees-panel
|
||||||
<div class="display-info">
|
class="display-card ${display.active ? '' : 'disabled'}"
|
||||||
<div class="display-name">${display.name}</div>
|
.title=${display.name}
|
||||||
<div class="display-details">
|
.subtitle=${`${display.width}×${display.height} @ ${display.refreshRate}Hz`}
|
||||||
${display.width}x${display.height} @ ${display.refreshRate}Hz
|
.variant=${display.active ? 'default' : 'ghost'}
|
||||||
${display.make !== 'Unknown' ? ` • ${display.make}` : ''}
|
>
|
||||||
|
${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>
|
||||||
</div>
|
` : ''}
|
||||||
<div class="display-actions">
|
|
||||||
${display.isPrimary
|
<div class="actions-row">
|
||||||
? html`<span class="device-default">Primary</span>`
|
${display.active && !display.isPrimary ? html`
|
||||||
: display.active
|
<dees-button
|
||||||
? html`
|
.type=${'default'}
|
||||||
<button
|
.text=${'Set Primary'}
|
||||||
class="btn btn-primary"
|
.disabled=${this.loading}
|
||||||
@click=${() => this.setKioskDisplay(display.name)}
|
@click=${() => this.setPrimary(display.name)}
|
||||||
?disabled=${this.loading}
|
></dees-button>
|
||||||
>
|
` : ''}
|
||||||
Set Primary
|
<dees-button
|
||||||
</button>
|
.type=${'default'}
|
||||||
`
|
.status=${display.active ? 'error' : 'success'}
|
||||||
: ''
|
.text=${display.active ? 'Disable' : 'Enable'}
|
||||||
}
|
.disabled=${this.loading}
|
||||||
<button
|
|
||||||
class="btn ${display.active ? 'btn-danger' : 'btn-primary'}"
|
|
||||||
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
||||||
?disabled=${this.loading}
|
></dees-button>
|
||||||
>
|
|
||||||
${display.active ? 'Disable' : 'Enable'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
|
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.message = '';
|
||||||
try {
|
try {
|
||||||
const action = enable ? 'enable' : 'disable';
|
const action = enable ? 'enable' : 'disable';
|
||||||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
|
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!result.success) {
|
this.message = result.message;
|
||||||
alert(result.message);
|
this.messageError = !result.success;
|
||||||
}
|
|
||||||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error: ${error}`);
|
this.message = `Error: ${error}`;
|
||||||
|
this.messageError = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setKioskDisplay(name: string): Promise<void> {
|
private async setPrimary(name: string): Promise<void> {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.message = '';
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
|
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!result.success) {
|
this.message = result.message;
|
||||||
alert(result.message);
|
this.messageError = !result.success;
|
||||||
}
|
|
||||||
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
this.dispatchEvent(new CustomEvent('refresh-displays'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error: ${error}`);
|
this.message = `Error: ${error}`;
|
||||||
|
this.messageError = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* EcoOS Logs View
|
* EcoOS Logs View
|
||||||
* Tabbed log viewer for daemon and system logs with auto-scroll
|
* Panel-wrapped terminal-style log viewer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { DeesPanel } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
import { sharedStyles } from '../styles/shared.js';
|
import { sharedStyles } from '../styles/shared.js';
|
||||||
|
|
||||||
@@ -26,46 +27,125 @@ export class EcoosLogs extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor activeTab: 'daemon' | 'system' = 'daemon';
|
private accessor activeTab: 'daemon' | 'system' = 'daemon';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor autoScroll: boolean = true;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
sharedStyles,
|
sharedStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-container {
|
.container {
|
||||||
background: var(--ecoos-card);
|
display: flex;
|
||||||
border: 1px solid var(--ecoos-border);
|
flex-direction: column;
|
||||||
border-radius: 8px;
|
height: calc(100vh - 140px);
|
||||||
overflow: hidden;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-header {
|
.tabs {
|
||||||
padding: 12px 16px;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--ecoos-border);
|
gap: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-content {
|
.tab {
|
||||||
height: 500px;
|
padding: 8px 16px;
|
||||||
overflow-y: auto;
|
font-size: var(--text-sm);
|
||||||
font-family: 'SF Mono', Monaco, monospace;
|
font-weight: 500;
|
||||||
font-size: 12px;
|
color: var(--text-tertiary);
|
||||||
line-height: 1.6;
|
cursor: pointer;
|
||||||
background: #0d0d0d;
|
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;
|
padding: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
.line {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
padding: 2px 0;
|
padding: 1px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-message {
|
.line.error {
|
||||||
color: var(--ecoos-text-dim);
|
color: var(--error);
|
||||||
padding: 20px;
|
}
|
||||||
text-align: center;
|
|
||||||
|
.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;
|
const logs = this.activeTab === 'daemon' ? this.daemonLogs : this.systemLogs;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logs-container">
|
<dees-panel .title=${'Logs'}>
|
||||||
<div class="logs-header">
|
<div class="container">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div
|
<div
|
||||||
class="tab ${this.activeTab === 'daemon' ? 'active' : ''}"
|
class="tab ${this.activeTab === 'daemon' ? 'active' : ''}"
|
||||||
@click=${() => this.switchTab('daemon')}
|
@click=${() => this.switchTab('daemon')}
|
||||||
>
|
>Daemon</div>
|
||||||
Daemon Logs
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="tab ${this.activeTab === 'system' ? 'active' : ''}"
|
class="tab ${this.activeTab === 'system' ? 'active' : ''}"
|
||||||
@click=${() => this.switchTab('system')}
|
@click=${() => this.switchTab('system')}
|
||||||
|
>System</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-row">
|
||||||
|
<span class="count">${logs.length} lines</span>
|
||||||
|
<div
|
||||||
|
class="auto-scroll ${this.autoScroll ? 'active' : ''}"
|
||||||
|
@click=${this.toggleAutoScroll}
|
||||||
>
|
>
|
||||||
System Logs
|
<span class="indicator"></span>
|
||||||
|
Auto-scroll
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="logs-content" id="logs-content">
|
<div class="terminal" id="terminal" @scroll=${this.handleScroll}>
|
||||||
${logs.length === 0
|
${logs.length === 0
|
||||||
? html`<div class="empty-message">No logs available</div>`
|
? html`<div class="empty-logs">No logs</div>`
|
||||||
: logs.map(log => html`<div class="log-entry">${log}</div>`)
|
: logs.map(log => html`<div class="line ${this.getLogLevel(log)}">${log}</div>`)
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</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 {
|
private switchTab(tab: 'daemon' | 'system'): void {
|
||||||
this.activeTab = tab;
|
this.activeTab = tab;
|
||||||
this.scrollToBottom();
|
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 {
|
updated(changedProperties: Map<string, unknown>): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
if ((changedProperties.has('daemonLogs') || changedProperties.has('systemLogs')) && this.autoScroll) {
|
||||||
// Auto-scroll when logs change
|
|
||||||
if (changedProperties.has('daemonLogs') || changedProperties.has('systemLogs')) {
|
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollToBottom(): void {
|
private scrollToBottom(): void {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const content = this.shadowRoot?.getElementById('logs-content');
|
const terminal = this.shadowRoot?.getElementById('terminal');
|
||||||
if (content) {
|
if (terminal) {
|
||||||
content.scrollTop = content.scrollHeight;
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* EcoOS Overview View
|
* EcoOS Overview View
|
||||||
* Shows services status, CPU, memory, system info, and controls
|
* Dashboard with stats grid, service panels, and system info
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
css,
|
css,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} 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 { sharedStyles, formatBytes, formatUptime } from '../styles/shared.js';
|
||||||
import type { IStatus, IServiceStatus } from '../../ts_interfaces/status.js';
|
import type { IStatus, IServiceStatus } from '../../ts_interfaces/status.js';
|
||||||
@@ -23,171 +30,275 @@ export class EcoosOverview extends DeesElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public accessor loading: boolean = false;
|
public accessor loading: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor controlMessage: string = '';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor controlError: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
sharedStyles,
|
sharedStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-status {
|
.page {
|
||||||
display: flex;
|
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;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-section {
|
.service-row:last-child {
|
||||||
margin-top: 16px;
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-status {
|
.service-row:first-child {
|
||||||
margin-top: 8px;
|
padding-top: 0;
|
||||||
font-size: 12px;
|
}
|
||||||
color: var(--ecoos-text-dim);
|
|
||||||
|
.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 {
|
render(): TemplateResult {
|
||||||
if (!this.status) {
|
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`
|
return html`
|
||||||
<div class="grid">
|
<div class="page">
|
||||||
<!-- Services Card -->
|
<!-- Stats Grid -->
|
||||||
<div class="card">
|
<dees-statsgrid
|
||||||
<div class="card-title">Services</div>
|
.tiles=${statsTiles}
|
||||||
<div class="service-status">
|
.minTileWidth=${200}
|
||||||
<span class="status-dot ${this.getStatusClass(swayStatus)}"></span>
|
.gap=${16}
|
||||||
<span>Sway Compositor</span>
|
></dees-statsgrid>
|
||||||
</div>
|
|
||||||
<div class="service-status">
|
<!-- Services & System Info -->
|
||||||
<span class="status-dot ${this.getStatusClass(chromiumStatus)}"></span>
|
<div class="cards-row">
|
||||||
<span>Chromium Browser</span>
|
<dees-panel .title=${'Services'}>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- CPU Card -->
|
<!-- Actions -->
|
||||||
<div class="card">
|
<dees-panel .title=${'Actions'}>
|
||||||
<div class="card-title">CPU</div>
|
<div class="actions-row">
|
||||||
<div class="stat">
|
<dees-button
|
||||||
<div class="stat-label">Model</div>
|
.type=${'default'}
|
||||||
<div class="stat-value">${systemInfo?.cpu?.model || '-'}</div>
|
.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>
|
</div>
|
||||||
<div class="stat">
|
</dees-panel>
|
||||||
<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"
|
|
||||||
@click=${this.restartChromium}
|
|
||||||
?disabled=${this.loading || !sway}
|
|
||||||
>
|
|
||||||
Restart Browser
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-danger"
|
|
||||||
@click=${this.rebootSystem}
|
|
||||||
?disabled=${this.loading}
|
|
||||||
>
|
|
||||||
Reboot System
|
|
||||||
</button>
|
|
||||||
<div class="control-status" id="control-status"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStatusClass(status: IServiceStatus): string {
|
private renderStatusBadge(status: IServiceStatus): TemplateResult {
|
||||||
switch (status?.state) {
|
const state = status?.state || 'stopped';
|
||||||
case 'running': return 'running';
|
let badgeType: 'default' | 'success' | 'warning' | 'error' = 'default';
|
||||||
case 'starting': return 'starting';
|
let label = 'Stopped';
|
||||||
default: return '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> {
|
private async restartChromium(): Promise<void> {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.controlMessage = '';
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/restart-chromium', { method: 'POST' });
|
const response = await fetch('/api/restart-chromium', { method: 'POST' });
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
this.showControlStatus(result.message, !result.success);
|
this.controlMessage = result.message;
|
||||||
|
this.controlError = !result.success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showControlStatus(`Error: ${error}`, true);
|
this.controlMessage = `Error: ${error}`;
|
||||||
|
this.controlError = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rebootSystem(): Promise<void> {
|
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.loading = true;
|
||||||
|
this.controlMessage = '';
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/reboot', { method: 'POST' });
|
const response = await fetch('/api/reboot', { method: 'POST' });
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
this.showControlStatus(result.message, !result.success);
|
this.controlMessage = result.message;
|
||||||
|
this.controlError = !result.success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showControlStatus(`Error: ${error}`, true);
|
this.controlMessage = `Error: ${error}`;
|
||||||
|
this.controlError = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
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
|
* EcoOS Updates View
|
||||||
* Version info, available updates, and upgrade controls
|
* Card-based update management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,9 +12,10 @@ import {
|
|||||||
css,
|
css,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
import { sharedStyles, formatAge } from '../styles/shared.js';
|
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')
|
@customElement('ecoos-updates')
|
||||||
export class EcoosUpdates extends DeesElement {
|
export class EcoosUpdates extends DeesElement {
|
||||||
@@ -24,130 +25,174 @@ export class EcoosUpdates extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor loading: boolean = false;
|
private accessor loading: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor message: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor messageError: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
sharedStyles,
|
sharedStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 10px 0;
|
||||||
border-bottom: 1px solid var(--ecoos-border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-item:last-child {
|
.update-row:last-child {
|
||||||
border-bottom: none;
|
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;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-age {
|
.update-age {
|
||||||
color: var(--ecoos-text-dim);
|
font-size: var(--text-xs);
|
||||||
font-size: 12px;
|
color: var(--text-tertiary);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-item .btn {
|
.empty-text {
|
||||||
padding: 4px 12px;
|
font-size: var(--text-sm);
|
||||||
margin: 0;
|
color: var(--text-tertiary);
|
||||||
font-size: 11px;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-upgrade-status {
|
.message-bar {
|
||||||
margin-top: 16px;
|
padding: 8px 12px;
|
||||||
font-size: 12px;
|
border-radius: 6px;
|
||||||
color: var(--ecoos-text-dim);
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.message-bar.success {
|
||||||
margin-top: 16px;
|
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 {
|
render(): TemplateResult {
|
||||||
if (!this.updateInfo) {
|
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 newerReleases = this.updateInfo.releases.filter(r => r.isNewer);
|
||||||
|
const { autoUpgrade, lastCheck } = this.updateInfo;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="card">
|
<div class="page">
|
||||||
<div class="card-title">Updates</div>
|
<!-- Current Version -->
|
||||||
|
<dees-panel .title=${'Current Version'}>
|
||||||
<div class="stat">
|
<div class="version-display">v${this.updateInfo.currentVersion}</div>
|
||||||
<div class="stat-label">Current Version</div>
|
${lastCheck ? html`<div class="last-check">Last check: ${new Date(lastCheck).toLocaleString()}</div>` : ''}
|
||||||
<div class="stat-value">v${this.updateInfo.currentVersion}</div>
|
<dees-button
|
||||||
</div>
|
.type=${'default'}
|
||||||
|
.text=${this.loading ? 'Checking...' : 'Check for Updates'}
|
||||||
<div style="margin: 16px 0;">
|
.disabled=${this.loading}
|
||||||
${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"
|
|
||||||
@click=${this.checkForUpdates}
|
@click=${this.checkForUpdates}
|
||||||
?disabled=${this.loading}
|
></dees-button>
|
||||||
>
|
</dees-panel>
|
||||||
Check for Updates
|
|
||||||
</button>
|
<!-- Auto-upgrade Banner -->
|
||||||
</div>
|
${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>
|
</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> {
|
private async checkForUpdates(): Promise<void> {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.message = '';
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/updates/check', { method: 'POST' });
|
const response = await fetch('/api/updates/check', { method: 'POST' });
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -155,15 +200,18 @@ export class EcoosUpdates extends DeesElement {
|
|||||||
this.dispatchEvent(new CustomEvent('updates-checked', { detail: result }));
|
this.dispatchEvent(new CustomEvent('updates-checked', { detail: result }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to check updates:', error);
|
console.error('Failed to check updates:', error);
|
||||||
|
this.message = `Failed: ${error}`;
|
||||||
|
this.messageError = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async upgradeToVersion(version: string): Promise<void> {
|
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.loading = true;
|
||||||
|
this.message = '';
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/upgrade', {
|
const response = await fetch('/api/upgrade', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -172,12 +220,16 @@ export class EcoosUpdates extends DeesElement {
|
|||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
this.message = result.message;
|
||||||
|
this.messageError = false;
|
||||||
this.dispatchEvent(new CustomEvent('upgrade-started', { detail: result }));
|
this.dispatchEvent(new CustomEvent('upgrade-started', { detail: result }));
|
||||||
} else {
|
} else {
|
||||||
alert(`Upgrade failed: ${result.message}`);
|
this.message = `Failed: ${result.message}`;
|
||||||
|
this.messageError = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Upgrade error: ${error}`);
|
this.message = `Error: ${error}`;
|
||||||
|
this.messageError = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,3 @@ import './elements/ecoos-logs.js';
|
|||||||
|
|
||||||
// Export the main app component
|
// Export the main app component
|
||||||
export { EcoosApp } from './elements/ecoos-app.js';
|
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';
|
import { css } from '@design.estate/dees-element';
|
||||||
|
|
||||||
export const sharedStyles = css`
|
export const sharedStyles = css`
|
||||||
:host {
|
:host {
|
||||||
--ecoos-bg: #0a0a0a;
|
/* Colors - dees-catalog theme (HSL) */
|
||||||
--ecoos-card: #141414;
|
--bg: hsl(0 0% 3.9%);
|
||||||
--ecoos-border: #2a2a2a;
|
--bg-elevated: hsl(0 0% 7.8%);
|
||||||
--ecoos-text: #e0e0e0;
|
--bg-hover: hsl(0 0% 14.9%);
|
||||||
--ecoos-text-dim: #888;
|
--border: hsl(0 0% 14.9%);
|
||||||
--ecoos-accent: #3b82f6;
|
--border-hover: hsl(0 0% 20.9%);
|
||||||
--ecoos-success: #22c55e;
|
--text: hsl(0 0% 95%);
|
||||||
--ecoos-warning: #f59e0b;
|
--text-secondary: hsl(215 20.2% 55.1%);
|
||||||
--ecoos-error: #ef4444;
|
--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;
|
display: block;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
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 {
|
/* Monospace utility */
|
||||||
background: var(--ecoos-card);
|
.mono {
|
||||||
border: 1px solid var(--ecoos-border);
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
/* Section - lightweight container */
|
||||||
font-size: 14px;
|
.section {
|
||||||
text-transform: uppercase;
|
padding: 12px 0;
|
||||||
color: var(--ecoos-text-dim);
|
border-bottom: 1px solid var(--border);
|
||||||
margin-bottom: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat {
|
.section:last-child {
|
||||||
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 {
|
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-name {
|
.section-title {
|
||||||
|
font-size: var(--text-xs);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-type {
|
/* Card - minimal styling */
|
||||||
font-size: 11px;
|
.card {
|
||||||
padding: 2px 6px;
|
background: var(--bg-elevated);
|
||||||
border-radius: 4px;
|
border: 1px solid var(--border);
|
||||||
background: var(--ecoos-border);
|
border-radius: 6px;
|
||||||
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;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
/* Table styling */
|
||||||
white-space: pre-wrap;
|
.table {
|
||||||
word-break: break-all;
|
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 {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--ecoos-border);
|
gap: 0;
|
||||||
margin-bottom: 12px;
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 8px 16px;
|
padding: 8px 12px;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-tertiary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--ecoos-text-dim);
|
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
font-size: 12px;
|
transition: color 150ms ease;
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
color: var(--ecoos-text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: var(--ecoos-accent);
|
color: var(--text);
|
||||||
border-bottom-color: var(--ecoos-accent);
|
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",
|
"name": "@ecobridge/eco-os",
|
||||||
"version": "0.6.0",
|
"version": "0.6.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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:dev": "cd ecoos_daemon && deno run --allow-all --watch mod.ts",
|
||||||
"daemon:start": "cd ecoos_daemon && deno run --allow-all mod.ts",
|
"daemon:start": "cd ecoos_daemon && deno run --allow-all mod.ts",
|
||||||
"daemon:typecheck": "cd ecoos_daemon && deno check 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": "pnpm run test:clean && cd isotest && ./run-test.sh",
|
||||||
"test:screenshot": "cd isotest && ./screenshot.sh",
|
"test:screenshot": "cd isotest && ./screenshot.sh",
|
||||||
"test:screenshot:loop": "while true; do pnpm run test:screenshot; sleep 5; done",
|
"test:screenshot:loop": "while true; do pnpm run test:screenshot; sleep 5; done",
|
||||||
|
|||||||
Reference in New Issue
Block a user