Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d01da261ee | |||
| 2c5ea744f1 | |||
| 2d4846cfed |
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-01-13 - 0.7.0 - feat(isobuild)
|
||||||
|
add multi-architecture build and Raspberry Pi support in installer and build tooling
|
||||||
|
|
||||||
|
- Bump package version to 0.6.6 and update ecoos_daemon version export
|
||||||
|
- Installer: detect target architecture (amd64, arm64, rpi) and adapt partitioning, formatting, mounting, fstab and boot configuration accordingly
|
||||||
|
- Installer: add full Raspberry Pi support (MBR partitioning for Pi, boot partition layout, config.txt and cmdline.txt generation, copying kernel/initrd/DTBs/firmware) and conditional GRUB vs native Pi boot handling
|
||||||
|
- Add create-rpi-image.sh to generate Raspberry Pi bootable .img from squashfs or chroot
|
||||||
|
- Dockerfile: add TARGET_ARCH build arg and conditional package installation and build steps for amd64, arm64 and rpi flows (including producing ecoos.iso, ecoos-arm64.iso or ecoos-rpi.img)
|
||||||
|
- Add architecture-specific package lists (base-amd64, base-arm64, base-rpi) and update base.list.chroot to delegate EFI packages to arch-specific lists
|
||||||
|
- Build/test tooling: update docker-build.sh, isotest/run-test.sh and package.json scripts to support build:amd64|arm64|rpi and corresponding test targets; improve output naming and automation
|
||||||
|
- Installer and scripts: improved device name handling (nvme/mmcblk), boot partition naming, mount/unmount cleanup, and logging
|
||||||
|
|
||||||
## 2026-01-12 - 0.6.0 - feat(ecoos-daemon)
|
## 2026-01-12 - 0.6.0 - feat(ecoos-daemon)
|
||||||
integrate a bundled daemon web UI with components, interfaces, styles, bundling config, and server support
|
integrate a bundled daemon web UI with components, interfaces, styles, bundling config, and server support
|
||||||
|
|
||||||
|
|||||||
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.7";
|
||||||
|
|||||||
@@ -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`
|
return html`
|
||||||
<div class="card">
|
<div class="empty-state">
|
||||||
<div class="card-title">Displays</div>
|
No displays detected
|
||||||
|
|
||||||
${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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDisplayItem(display: IDisplayInfo): TemplateResult {
|
|
||||||
return html`
|
return html`
|
||||||
<div class="display-item">
|
<div class="display-grid">
|
||||||
<div class="display-info">
|
${enabledDisplays.map(d => this.renderDisplayCard(d))}
|
||||||
<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>
|
</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>
|
||||||
<div class="display-actions">
|
</details>
|
||||||
${display.isPrimary
|
` : ''}
|
||||||
? html`<span class="device-default">Primary</span>`
|
|
||||||
: display.active
|
${this.message ? html`
|
||||||
? html`
|
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
|
||||||
<button
|
` : ''}
|
||||||
class="btn btn-primary"
|
`;
|
||||||
@click=${() => this.setKioskDisplay(display.name)}
|
|
||||||
?disabled=${this.loading}
|
|
||||||
>
|
|
||||||
Set Primary
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
}
|
}
|
||||||
<button
|
|
||||||
class="btn ${display.active ? 'btn-danger' : 'btn-primary'}"
|
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
|
||||||
@click=${() => this.toggleDisplay(display.name, !display.active)}
|
return html`
|
||||||
?disabled=${this.loading}
|
<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'}
|
${display.make && display.make !== 'Unknown' ? html`
|
||||||
</button>
|
<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 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>
|
</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>
|
||||||
System Logs
|
|
||||||
</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>
|
</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;
|
||||||
|
if (this.autoScroll) {
|
||||||
this.scrollToBottom();
|
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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
<div class="service-status">
|
${this.renderStatusBadge(swayStatus)}
|
||||||
<span class="status-dot ${this.getStatusClass(chromiumStatus)}"></span>
|
|
||||||
<span>Chromium Browser</span>
|
|
||||||
</div>
|
</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'}
|
||||||
</div>
|
.disabled=${this.loading}
|
||||||
<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"
|
|
||||||
@click=${this.restartChromium}
|
@click=${this.restartChromium}
|
||||||
?disabled=${this.loading || !sway}
|
></dees-button>
|
||||||
>
|
<dees-button
|
||||||
Restart Browser
|
.type=${'default'}
|
||||||
</button>
|
.status=${'error'}
|
||||||
<button
|
.text=${'Reboot System'}
|
||||||
class="btn btn-danger"
|
.disabled=${this.loading}
|
||||||
@click=${this.rebootSystem}
|
@click=${this.rebootSystem}
|
||||||
?disabled=${this.loading}
|
></dees-button>
|
||||||
>
|
${this.controlMessage ? html`
|
||||||
Reboot System
|
<span class="message ${this.controlError ? 'error' : 'success'}">${this.controlMessage}</span>
|
||||||
</button>
|
` : ''}
|
||||||
<div class="control-status" id="control-status"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</dees-panel>
|
||||||
</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 -->
|
||||||
|
${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>
|
</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); }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,33 +1,52 @@
|
|||||||
# EcoOS ISO Builder
|
# EcoOS ISO Builder
|
||||||
# Build from eco_os directory:
|
# Build from eco_os directory:
|
||||||
# docker build -t ecoos-builder -f isobuild/Dockerfile .
|
# docker build --build-arg TARGET_ARCH=amd64 -t ecoos-builder -f isobuild/Dockerfile .
|
||||||
# docker run --privileged -v $(pwd)/isobuild/output:/output ecoos-builder
|
# docker run --privileged -v $(pwd)/isobuild/output:/output ecoos-builder
|
||||||
|
#
|
||||||
|
# Supported architectures: amd64, arm64, rpi
|
||||||
|
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ARG TARGET_ARCH=amd64
|
||||||
|
ENV TARGET_ARCH=${TARGET_ARCH}
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install build dependencies
|
# Install common build dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
live-build \
|
live-build \
|
||||||
debootstrap \
|
debootstrap \
|
||||||
xorriso \
|
xorriso \
|
||||||
squashfs-tools \
|
squashfs-tools \
|
||||||
|
mtools \
|
||||||
|
dosfstools \
|
||||||
|
curl \
|
||||||
|
unzip \
|
||||||
|
git \
|
||||||
|
parted \
|
||||||
|
fdisk \
|
||||||
|
e2fsprogs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install architecture-specific packages
|
||||||
|
RUN apt-get update && \
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ]; then \
|
||||||
|
apt-get install -y \
|
||||||
grub-efi-amd64-bin \
|
grub-efi-amd64-bin \
|
||||||
grub-efi-amd64-signed \
|
grub-efi-amd64-signed \
|
||||||
grub-pc-bin \
|
grub-pc-bin \
|
||||||
shim-signed \
|
shim-signed \
|
||||||
mtools \
|
|
||||||
dosfstools \
|
|
||||||
syslinux-utils \
|
syslinux-utils \
|
||||||
syslinux \
|
syslinux \
|
||||||
syslinux-common \
|
syslinux-common \
|
||||||
isolinux \
|
isolinux; \
|
||||||
curl \
|
elif [ "$TARGET_ARCH" = "arm64" ]; then \
|
||||||
unzip \
|
apt-get install -y \
|
||||||
git \
|
grub-efi-arm64-bin \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
grub-efi-arm64-signed; \
|
||||||
&& ln -sf /usr/bin/isohybrid /usr/local/bin/isohybrid 2>/dev/null || true
|
elif [ "$TARGET_ARCH" = "rpi" ]; then \
|
||||||
|
apt-get install -y \
|
||||||
|
grub-efi-arm64-bin; \
|
||||||
|
fi && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Deno
|
# Install Deno
|
||||||
RUN curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh
|
RUN curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh
|
||||||
@@ -45,16 +64,29 @@ COPY isobuild/config/hooks/ /build/hooks/
|
|||||||
# Copy daemon source (for bundling)
|
# Copy daemon source (for bundling)
|
||||||
COPY ecoos_daemon/ /daemon/
|
COPY ecoos_daemon/ /daemon/
|
||||||
|
|
||||||
# Bundle the daemon
|
# Bundle the daemon - cross-compile for target architecture
|
||||||
RUN cd /daemon && deno compile --allow-all --output /build/daemon-bundle/eco-daemon mod.ts
|
RUN cd /daemon && \
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ]; then \
|
||||||
|
deno compile --allow-all --target x86_64-unknown-linux-gnu --output /build/daemon-bundle/eco-daemon mod.ts; \
|
||||||
|
else \
|
||||||
|
deno compile --allow-all --target aarch64-unknown-linux-gnu --output /build/daemon-bundle/eco-daemon mod.ts; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Download Chromium during Docker build (network works here, not in chroot hooks)
|
# Download Chromium during Docker build (network works here, not in chroot hooks)
|
||||||
RUN echo "Downloading Chromium from official snapshots..." && \
|
# Note: ARM64 Chromium snapshots may be less reliable, fallback to known version
|
||||||
|
RUN echo "Downloading Chromium for $TARGET_ARCH..." && \
|
||||||
cd /tmp && \
|
cd /tmp && \
|
||||||
LATEST=$(curl -fsSL "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media" 2>/dev/null || echo "1368529") && \
|
if [ "$TARGET_ARCH" = "amd64" ]; then \
|
||||||
echo "Using Chromium build: $LATEST" && \
|
PLATFORM="Linux_x64"; \
|
||||||
curl -fsSL "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${LATEST}%2Fchrome-linux.zip?alt=media" -o chromium.zip || \
|
FALLBACK_VERSION="1368529"; \
|
||||||
curl -fsSL "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F1368529%2Fchrome-linux.zip?alt=media" -o chromium.zip && \
|
else \
|
||||||
|
PLATFORM="Linux_ARM64"; \
|
||||||
|
FALLBACK_VERSION="1368529"; \
|
||||||
|
fi && \
|
||||||
|
LATEST=$(curl -fsSL "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${PLATFORM}%2FLAST_CHANGE?alt=media" 2>/dev/null || echo "$FALLBACK_VERSION") && \
|
||||||
|
echo "Using Chromium build: $LATEST for platform $PLATFORM" && \
|
||||||
|
curl -fsSL "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${PLATFORM}%2F${LATEST}%2Fchrome-linux.zip?alt=media" -o chromium.zip || \
|
||||||
|
curl -fsSL "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${PLATFORM}%2F${FALLBACK_VERSION}%2Fchrome-linux.zip?alt=media" -o chromium.zip && \
|
||||||
mkdir -p /build/chromium && \
|
mkdir -p /build/chromium && \
|
||||||
unzip -q chromium.zip -d /tmp && \
|
unzip -q chromium.zip -d /tmp && \
|
||||||
mv /tmp/chrome-linux/* /build/chromium/ && \
|
mv /tmp/chrome-linux/* /build/chromium/ && \
|
||||||
@@ -71,21 +103,45 @@ RUN echo '#!/bin/sh' > /usr/local/bin/isohybrid && \
|
|||||||
echo 'exit 0' >> /usr/local/bin/isohybrid && \
|
echo 'exit 0' >> /usr/local/bin/isohybrid && \
|
||||||
chmod +x /usr/local/bin/isohybrid
|
chmod +x /usr/local/bin/isohybrid
|
||||||
|
|
||||||
# Build script
|
# Build script - parameterized for architecture
|
||||||
COPY <<'EOF' /build/docker-build.sh
|
COPY <<'EOF' /build/docker-build.sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
export PATH="/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:$PATH"
|
export PATH="/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:$PATH"
|
||||||
|
|
||||||
|
# Get architecture from environment (passed from docker run -e)
|
||||||
|
TARGET_ARCH="${TARGET_ARCH:-amd64}"
|
||||||
|
|
||||||
echo "=== EcoOS ISO Builder (Docker) ==="
|
echo "=== EcoOS ISO Builder (Docker) ==="
|
||||||
|
echo "Target architecture: $TARGET_ARCH"
|
||||||
|
|
||||||
cd /build
|
cd /build
|
||||||
|
|
||||||
# Initialize live-build - UEFI only (no syslinux/BIOS)
|
# Determine live-build architecture and image format
|
||||||
# Using German mirror for faster/more stable downloads
|
case "$TARGET_ARCH" in
|
||||||
|
amd64)
|
||||||
|
LB_ARCH="amd64"
|
||||||
|
IMAGE_FORMAT="iso-hybrid"
|
||||||
|
BOOTLOADER_OPT="--bootloader grub-efi"
|
||||||
|
;;
|
||||||
|
arm64)
|
||||||
|
LB_ARCH="arm64"
|
||||||
|
IMAGE_FORMAT="iso-hybrid"
|
||||||
|
BOOTLOADER_OPT="--bootloader grub-efi"
|
||||||
|
;;
|
||||||
|
rpi)
|
||||||
|
LB_ARCH="arm64"
|
||||||
|
IMAGE_FORMAT="hdd"
|
||||||
|
BOOTLOADER_OPT="" # RPi uses native bootloader
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Live-build arch: $LB_ARCH, format: $IMAGE_FORMAT"
|
||||||
|
|
||||||
|
# Initialize live-build
|
||||||
lb config \
|
lb config \
|
||||||
--architectures amd64 \
|
--architectures $LB_ARCH \
|
||||||
--distribution noble \
|
--distribution noble \
|
||||||
--archive-areas "main restricted universe multiverse" \
|
--archive-areas "main restricted universe multiverse" \
|
||||||
--mirror-bootstrap "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
--mirror-bootstrap "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
||||||
@@ -93,16 +149,33 @@ lb config \
|
|||||||
--mirror-chroot-security "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
--mirror-chroot-security "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
||||||
--mirror-binary "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
--mirror-binary "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
||||||
--mirror-binary-security "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
--mirror-binary-security "http://ftp.halifax.rwth-aachen.de/ubuntu/" \
|
||||||
--binary-images iso-hybrid \
|
--binary-images $IMAGE_FORMAT \
|
||||||
--debian-installer false \
|
--debian-installer false \
|
||||||
--memtest none \
|
--memtest none \
|
||||||
--bootloader grub-efi \
|
$BOOTLOADER_OPT \
|
||||||
--iso-application "EcoOS" \
|
--iso-application "EcoOS" \
|
||||||
--iso-publisher "EcoBridge" \
|
--iso-publisher "EcoBridge" \
|
||||||
--iso-volume "EcoOS"
|
--iso-volume "EcoOS"
|
||||||
|
|
||||||
# Copy package lists
|
# Copy common package lists (excluding architecture-specific ones)
|
||||||
cp /build/config/live-build/package-lists/*.list.chroot config/package-lists/
|
for f in /build/config/live-build/package-lists/*.list.chroot; do
|
||||||
|
filename=$(basename "$f")
|
||||||
|
# Skip architecture-specific files (base-amd64, base-arm64, base-rpi)
|
||||||
|
case "$filename" in
|
||||||
|
base-amd64.list.chroot|base-arm64.list.chroot|base-rpi.list.chroot)
|
||||||
|
echo "Skipping arch-specific list: $filename"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
cp "$f" config/package-lists/
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Append architecture-specific packages to base.list.chroot
|
||||||
|
if [ -f "/build/config/live-build/package-lists/base-${TARGET_ARCH}.list.chroot" ]; then
|
||||||
|
echo "Adding architecture-specific packages for $TARGET_ARCH..."
|
||||||
|
cat "/build/config/live-build/package-lists/base-${TARGET_ARCH}.list.chroot" >> config/package-lists/base.list.chroot
|
||||||
|
fi
|
||||||
|
|
||||||
# Prepare includes.chroot
|
# Prepare includes.chroot
|
||||||
mkdir -p config/includes.chroot/opt/eco/bin
|
mkdir -p config/includes.chroot/opt/eco/bin
|
||||||
@@ -154,26 +227,28 @@ mkdir -p config/includes.binary/autoinstall
|
|||||||
cp /build/config/autoinstall/user-data config/includes.binary/autoinstall/
|
cp /build/config/autoinstall/user-data config/includes.binary/autoinstall/
|
||||||
touch config/includes.binary/autoinstall/meta-data
|
touch config/includes.binary/autoinstall/meta-data
|
||||||
|
|
||||||
# Prepare EFI boot files in includes.binary
|
# Architecture-specific EFI/boot setup
|
||||||
echo "Preparing EFI boot structure..."
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
mkdir -p config/includes.binary/EFI/BOOT
|
# AMD64 EFI boot setup
|
||||||
mkdir -p config/includes.binary/boot/grub
|
echo "Preparing AMD64 EFI boot structure..."
|
||||||
|
mkdir -p config/includes.binary/EFI/BOOT
|
||||||
|
mkdir -p config/includes.binary/boot/grub
|
||||||
|
|
||||||
# Copy signed EFI files from host (installed in Docker image)
|
# Copy signed EFI files from host (installed in Docker image)
|
||||||
cp /usr/lib/shim/shimx64.efi.signed.latest config/includes.binary/EFI/BOOT/BOOTX64.EFI || \
|
cp /usr/lib/shim/shimx64.efi.signed.latest config/includes.binary/EFI/BOOT/BOOTX64.EFI || \
|
||||||
cp /usr/lib/shim/shimx64.efi.signed config/includes.binary/EFI/BOOT/BOOTX64.EFI || \
|
cp /usr/lib/shim/shimx64.efi.signed config/includes.binary/EFI/BOOT/BOOTX64.EFI || \
|
||||||
cp /usr/lib/shim/shimx64.efi config/includes.binary/EFI/BOOT/BOOTX64.EFI || true
|
cp /usr/lib/shim/shimx64.efi config/includes.binary/EFI/BOOT/BOOTX64.EFI || true
|
||||||
|
|
||||||
cp /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed config/includes.binary/EFI/BOOT/grubx64.efi || \
|
cp /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed config/includes.binary/EFI/BOOT/grubx64.efi || \
|
||||||
cp /usr/lib/grub/x86_64-efi/grubx64.efi config/includes.binary/EFI/BOOT/grubx64.efi || true
|
cp /usr/lib/grub/x86_64-efi/grubx64.efi config/includes.binary/EFI/BOOT/grubx64.efi || true
|
||||||
|
|
||||||
# Also provide mmx64.efi for some UEFI implementations
|
# Also provide mmx64.efi for some UEFI implementations
|
||||||
if [ -f config/includes.binary/EFI/BOOT/grubx64.efi ]; then
|
if [ -f config/includes.binary/EFI/BOOT/grubx64.efi ]; then
|
||||||
cp config/includes.binary/EFI/BOOT/grubx64.efi config/includes.binary/EFI/BOOT/mmx64.efi
|
cp config/includes.binary/EFI/BOOT/grubx64.efi config/includes.binary/EFI/BOOT/mmx64.efi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create grub.cfg for live boot with installer option
|
# Create grub.cfg for live boot with installer option
|
||||||
cat > config/includes.binary/boot/grub/grub.cfg << 'GRUBCFG'
|
cat > config/includes.binary/boot/grub/grub.cfg << 'GRUBCFG'
|
||||||
set default=0
|
set default=0
|
||||||
set timeout=10
|
set timeout=10
|
||||||
|
|
||||||
@@ -198,82 +273,174 @@ menuentry "EcoOS Live (Safe Mode)" {
|
|||||||
}
|
}
|
||||||
GRUBCFG
|
GRUBCFG
|
||||||
|
|
||||||
# Also put grub.cfg in EFI/BOOT for fallback
|
# Also put grub.cfg in EFI/BOOT for fallback
|
||||||
cp config/includes.binary/boot/grub/grub.cfg config/includes.binary/EFI/BOOT/grub.cfg
|
cp config/includes.binary/boot/grub/grub.cfg config/includes.binary/EFI/BOOT/grub.cfg
|
||||||
|
|
||||||
# Build ISO - use individual lb stages to control the process
|
elif [ "$TARGET_ARCH" = "arm64" ]; then
|
||||||
lb bootstrap
|
# ARM64 EFI boot setup
|
||||||
lb chroot
|
echo "Preparing ARM64 EFI boot structure..."
|
||||||
|
mkdir -p config/includes.binary/EFI/BOOT
|
||||||
|
mkdir -p config/includes.binary/boot/grub
|
||||||
|
|
||||||
# Try lb binary, but continue even if isohybrid fails
|
# Copy ARM64 GRUB EFI
|
||||||
lb binary || {
|
cp /usr/lib/grub/arm64-efi-signed/grubaa64.efi.signed config/includes.binary/EFI/BOOT/BOOTAA64.EFI || \
|
||||||
echo "lb binary had errors, checking if ISO was created anyway..."
|
cp /usr/lib/grub/arm64-efi/grubaa64.efi config/includes.binary/EFI/BOOT/BOOTAA64.EFI || true
|
||||||
if ls /build/*.iso 2>/dev/null; then
|
|
||||||
echo "ISO exists despite errors, continuing..."
|
|
||||||
else
|
|
||||||
echo "No ISO found, build truly failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if EFI was created properly
|
# Create grub.cfg for ARM64
|
||||||
echo "Checking binary directory for EFI..."
|
cat > config/includes.binary/boot/grub/grub.cfg << 'GRUBCFG'
|
||||||
ls -la binary/EFI/BOOT/ 2>/dev/null || echo "EFI/BOOT not found in binary dir"
|
|
||||||
|
|
||||||
# Find the ISO file
|
|
||||||
echo "Searching for ISO file..."
|
|
||||||
find /build -name "*.iso" -type f 2>/dev/null
|
|
||||||
ls -la /build/*.iso 2>/dev/null || true
|
|
||||||
|
|
||||||
ISO_FILE=$(find /build -name "*.iso" -type f 2>/dev/null | head -1)
|
|
||||||
if [ -z "$ISO_FILE" ]; then
|
|
||||||
echo "ERROR: No ISO file found in build directory"
|
|
||||||
echo "Listing /build contents:"
|
|
||||||
ls -la /build/
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Found ISO: $ISO_FILE"
|
|
||||||
|
|
||||||
# Always create proper EFI boot image and rebuild ISO
|
|
||||||
echo "Creating UEFI-bootable ISO..."
|
|
||||||
|
|
||||||
# Extract ISO contents
|
|
||||||
mkdir -p /tmp/iso_extract
|
|
||||||
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso_extract
|
|
||||||
|
|
||||||
# Find the actual kernel and initrd names
|
|
||||||
VMLINUZ=$(ls /tmp/iso_extract/casper/vmlinuz* 2>/dev/null | head -1 | xargs basename)
|
|
||||||
INITRD=$(ls /tmp/iso_extract/casper/initrd* 2>/dev/null | head -1 | xargs basename)
|
|
||||||
|
|
||||||
echo "Found kernel: $VMLINUZ, initrd: $INITRD"
|
|
||||||
|
|
||||||
# Ensure EFI structure exists with proper files
|
|
||||||
mkdir -p /tmp/iso_extract/EFI/BOOT
|
|
||||||
mkdir -p /tmp/iso_extract/boot/grub
|
|
||||||
|
|
||||||
# Copy EFI files from host
|
|
||||||
cp /usr/lib/shim/shimx64.efi.signed.latest /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI 2>/dev/null || \
|
|
||||||
cp /usr/lib/shim/shimx64.efi.signed /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI 2>/dev/null || \
|
|
||||||
cp /usr/lib/shim/shimx64.efi /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI 2>/dev/null || true
|
|
||||||
|
|
||||||
cp /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed /tmp/iso_extract/EFI/BOOT/grubx64.efi 2>/dev/null || \
|
|
||||||
cp /usr/lib/grub/x86_64-efi/grubx64.efi /tmp/iso_extract/EFI/BOOT/grubx64.efi 2>/dev/null || true
|
|
||||||
|
|
||||||
# Copy mmx64.efi for secure boot compatibility
|
|
||||||
if [ -f /tmp/iso_extract/EFI/BOOT/grubx64.efi ]; then
|
|
||||||
cp /tmp/iso_extract/EFI/BOOT/grubx64.efi /tmp/iso_extract/EFI/BOOT/mmx64.efi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create grub.cfg with correct filenames and installer option
|
|
||||||
cat > /tmp/iso_extract/boot/grub/grub.cfg << GRUBCFG2
|
|
||||||
set default=0
|
set default=0
|
||||||
set timeout=10
|
set timeout=10
|
||||||
|
|
||||||
insmod part_gpt
|
insmod part_gpt
|
||||||
insmod fat
|
insmod fat
|
||||||
insmod efi_gop
|
insmod efi_gop
|
||||||
insmod efi_uga
|
|
||||||
|
menuentry "Install EcoOS (auto-selects in 10s)" {
|
||||||
|
linux /casper/vmlinuz boot=casper noprompt quiet splash ecoos_install=1 ---
|
||||||
|
initrd /casper/initrd
|
||||||
|
}
|
||||||
|
|
||||||
|
menuentry "EcoOS Live (Try without installing)" {
|
||||||
|
linux /casper/vmlinuz boot=casper noprompt quiet splash ---
|
||||||
|
initrd /casper/initrd
|
||||||
|
}
|
||||||
|
|
||||||
|
menuentry "EcoOS Live (Safe Mode)" {
|
||||||
|
linux /casper/vmlinuz boot=casper noprompt nomodeset ---
|
||||||
|
initrd /casper/initrd
|
||||||
|
}
|
||||||
|
GRUBCFG
|
||||||
|
|
||||||
|
cp config/includes.binary/boot/grub/grub.cfg config/includes.binary/EFI/BOOT/grub.cfg
|
||||||
|
|
||||||
|
elif [ "$TARGET_ARCH" = "rpi" ]; then
|
||||||
|
# Raspberry Pi boot setup (native bootloader, no GRUB)
|
||||||
|
echo "Preparing Raspberry Pi boot structure..."
|
||||||
|
mkdir -p config/includes.binary/boot
|
||||||
|
|
||||||
|
# Create config.txt for Raspberry Pi
|
||||||
|
cat > config/includes.binary/boot/config.txt << 'PICFG'
|
||||||
|
# EcoOS Raspberry Pi Configuration
|
||||||
|
# Supports Pi 3, 4, and 5
|
||||||
|
|
||||||
|
# Enable 64-bit mode
|
||||||
|
arm_64bit=1
|
||||||
|
|
||||||
|
# Kernel and initrd
|
||||||
|
kernel=vmlinuz
|
||||||
|
initramfs initrd.img followkernel
|
||||||
|
|
||||||
|
# Enable serial console for debugging
|
||||||
|
enable_uart=1
|
||||||
|
|
||||||
|
# GPU/display settings
|
||||||
|
dtoverlay=vc4-kms-v3d
|
||||||
|
gpu_mem=256
|
||||||
|
|
||||||
|
# USB and power settings (Pi 4/5)
|
||||||
|
max_usb_current=1
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
dtparam=audio=on
|
||||||
|
|
||||||
|
# Camera/display interfaces
|
||||||
|
camera_auto_detect=1
|
||||||
|
display_auto_detect=1
|
||||||
|
|
||||||
|
# Pi 5 specific (ignored on older models)
|
||||||
|
[pi5]
|
||||||
|
dtoverlay=dwc2,dr_mode=host
|
||||||
|
PICFG
|
||||||
|
|
||||||
|
# Create cmdline.txt
|
||||||
|
cat > config/includes.binary/boot/cmdline.txt << 'CMDLINE'
|
||||||
|
console=serial0,115200 console=tty1 root=LABEL=EcoOS rootfstype=ext4 fsck.repair=yes rootwait quiet splash
|
||||||
|
CMDLINE
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build - use individual lb stages to control the process
|
||||||
|
echo "Running lb bootstrap..."
|
||||||
|
lb bootstrap
|
||||||
|
|
||||||
|
echo "Running lb chroot..."
|
||||||
|
lb chroot
|
||||||
|
|
||||||
|
# Try lb binary, but continue even if isohybrid fails
|
||||||
|
echo "Running lb binary..."
|
||||||
|
lb binary || {
|
||||||
|
echo "lb binary had errors, checking if output was created anyway..."
|
||||||
|
if ls /build/*.iso 2>/dev/null || ls /build/*.img 2>/dev/null; then
|
||||||
|
echo "Output exists despite errors, continuing..."
|
||||||
|
else
|
||||||
|
echo "No output found, build truly failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Post-processing based on architecture
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ] || [ "$TARGET_ARCH" = "arm64" ]; then
|
||||||
|
# Find the ISO file
|
||||||
|
echo "Searching for ISO file..."
|
||||||
|
find /build -name "*.iso" -type f 2>/dev/null
|
||||||
|
ls -la /build/*.iso 2>/dev/null || true
|
||||||
|
|
||||||
|
ISO_FILE=$(find /build -name "*.iso" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ISO_FILE" ]; then
|
||||||
|
echo "ERROR: No ISO file found in build directory"
|
||||||
|
ls -la /build/
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found ISO: $ISO_FILE"
|
||||||
|
|
||||||
|
# Rebuild ISO with proper EFI boot support
|
||||||
|
echo "Creating UEFI-bootable ISO..."
|
||||||
|
|
||||||
|
# Extract ISO contents
|
||||||
|
mkdir -p /tmp/iso_extract
|
||||||
|
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso_extract
|
||||||
|
|
||||||
|
# Find the actual kernel and initrd names
|
||||||
|
VMLINUZ=$(ls /tmp/iso_extract/casper/vmlinuz* 2>/dev/null | head -1 | xargs basename)
|
||||||
|
INITRD=$(ls /tmp/iso_extract/casper/initrd* 2>/dev/null | head -1 | xargs basename)
|
||||||
|
|
||||||
|
echo "Found kernel: $VMLINUZ, initrd: $INITRD"
|
||||||
|
|
||||||
|
# Ensure EFI structure exists with proper files
|
||||||
|
mkdir -p /tmp/iso_extract/EFI/BOOT
|
||||||
|
mkdir -p /tmp/iso_extract/boot/grub
|
||||||
|
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
|
# Copy AMD64 EFI files
|
||||||
|
cp /usr/lib/shim/shimx64.efi.signed.latest /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI 2>/dev/null || \
|
||||||
|
cp /usr/lib/shim/shimx64.efi.signed /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI 2>/dev/null || \
|
||||||
|
cp /usr/lib/shim/shimx64.efi /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI 2>/dev/null || true
|
||||||
|
|
||||||
|
cp /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed /tmp/iso_extract/EFI/BOOT/grubx64.efi 2>/dev/null || \
|
||||||
|
cp /usr/lib/grub/x86_64-efi/grubx64.efi /tmp/iso_extract/EFI/BOOT/grubx64.efi 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ -f /tmp/iso_extract/EFI/BOOT/grubx64.efi ]; then
|
||||||
|
cp /tmp/iso_extract/EFI/BOOT/grubx64.efi /tmp/iso_extract/EFI/BOOT/mmx64.efi
|
||||||
|
fi
|
||||||
|
|
||||||
|
EFI_BOOT_FILE="BOOTX64.EFI"
|
||||||
|
else
|
||||||
|
# Copy ARM64 EFI files
|
||||||
|
cp /usr/lib/grub/arm64-efi-signed/grubaa64.efi.signed /tmp/iso_extract/EFI/BOOT/BOOTAA64.EFI 2>/dev/null || \
|
||||||
|
cp /usr/lib/grub/arm64-efi/grubaa64.efi /tmp/iso_extract/EFI/BOOT/BOOTAA64.EFI 2>/dev/null || true
|
||||||
|
|
||||||
|
EFI_BOOT_FILE="BOOTAA64.EFI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update grub.cfg with correct filenames
|
||||||
|
cat > /tmp/iso_extract/boot/grub/grub.cfg << GRUBCFG2
|
||||||
|
set default=0
|
||||||
|
set timeout=10
|
||||||
|
|
||||||
|
insmod part_gpt
|
||||||
|
insmod fat
|
||||||
|
insmod efi_gop
|
||||||
|
$([ "$TARGET_ARCH" = "amd64" ] && echo "insmod efi_uga")
|
||||||
|
|
||||||
menuentry "Install EcoOS (auto-selects in 10s)" {
|
menuentry "Install EcoOS (auto-selects in 10s)" {
|
||||||
linux /casper/${VMLINUZ} boot=casper noprompt quiet splash ecoos_install=1 ---
|
linux /casper/${VMLINUZ} boot=casper noprompt quiet splash ecoos_install=1 ---
|
||||||
@@ -291,22 +458,24 @@ menuentry "EcoOS Live (Safe Mode)" {
|
|||||||
}
|
}
|
||||||
GRUBCFG2
|
GRUBCFG2
|
||||||
|
|
||||||
cp /tmp/iso_extract/boot/grub/grub.cfg /tmp/iso_extract/EFI/BOOT/grub.cfg
|
cp /tmp/iso_extract/boot/grub/grub.cfg /tmp/iso_extract/EFI/BOOT/grub.cfg
|
||||||
|
|
||||||
# Create EFI boot image (FAT filesystem for UEFI El Torito boot)
|
# Create EFI boot image (FAT filesystem for UEFI El Torito boot)
|
||||||
echo "Creating EFI boot image..."
|
echo "Creating EFI boot image..."
|
||||||
dd if=/dev/zero of=/tmp/efi.img bs=1M count=10
|
dd if=/dev/zero of=/tmp/efi.img bs=1M count=10
|
||||||
mkfs.fat -F 12 /tmp/efi.img
|
mkfs.fat -F 12 /tmp/efi.img
|
||||||
mmd -i /tmp/efi.img ::/EFI
|
mmd -i /tmp/efi.img ::/EFI
|
||||||
mmd -i /tmp/efi.img ::/EFI/BOOT
|
mmd -i /tmp/efi.img ::/EFI/BOOT
|
||||||
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/BOOTX64.EFI ::/EFI/BOOT/
|
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/$EFI_BOOT_FILE ::/EFI/BOOT/
|
||||||
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/grubx64.efi ::/EFI/BOOT/ 2>/dev/null || true
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/mmx64.efi ::/EFI/BOOT/ 2>/dev/null || true
|
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/grubx64.efi ::/EFI/BOOT/ 2>/dev/null || true
|
||||||
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/grub.cfg ::/EFI/BOOT/
|
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/mmx64.efi ::/EFI/BOOT/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
mcopy -i /tmp/efi.img /tmp/iso_extract/EFI/BOOT/grub.cfg ::/EFI/BOOT/
|
||||||
|
|
||||||
# Rebuild ISO with EFI boot support (UEFI-only, no BIOS boot)
|
# Rebuild ISO with EFI boot support
|
||||||
echo "Rebuilding ISO with UEFI boot support..."
|
echo "Rebuilding ISO with UEFI boot support..."
|
||||||
xorriso -as mkisofs \
|
xorriso -as mkisofs \
|
||||||
-r -V "EcoOS" \
|
-r -V "EcoOS" \
|
||||||
-o /tmp/ecoos-efi.iso \
|
-o /tmp/ecoos-efi.iso \
|
||||||
-J -joliet-long \
|
-J -joliet-long \
|
||||||
@@ -316,29 +485,59 @@ xorriso -as mkisofs \
|
|||||||
-append_partition 2 0xef /tmp/efi.img \
|
-append_partition 2 0xef /tmp/efi.img \
|
||||||
/tmp/iso_extract
|
/tmp/iso_extract
|
||||||
|
|
||||||
if [ -f /tmp/ecoos-efi.iso ]; then
|
if [ -f /tmp/ecoos-efi.iso ]; then
|
||||||
ISO_FILE=/tmp/ecoos-efi.iso
|
ISO_FILE=/tmp/ecoos-efi.iso
|
||||||
echo "Created UEFI-bootable ISO: $ISO_FILE"
|
echo "Created UEFI-bootable ISO: $ISO_FILE"
|
||||||
else
|
else
|
||||||
echo "ERROR: Failed to create EFI ISO"
|
echo "ERROR: Failed to create EFI ISO"
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf /tmp/iso_extract
|
||||||
|
|
||||||
|
# Determine output filename
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
|
OUTPUT_NAME="ecoos.iso"
|
||||||
|
else
|
||||||
|
OUTPUT_NAME="ecoos-arm64.iso"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy to output
|
||||||
|
mkdir -p /output
|
||||||
|
cp "$ISO_FILE" /output/$OUTPUT_NAME
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Final ISO EFI check ==="
|
||||||
|
xorriso -indev /output/$OUTPUT_NAME -find / -maxdepth 2 -type d 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Build Complete ==="
|
||||||
|
echo "ISO: /output/$OUTPUT_NAME"
|
||||||
|
ls -lh /output/$OUTPUT_NAME
|
||||||
|
|
||||||
|
elif [ "$TARGET_ARCH" = "rpi" ]; then
|
||||||
|
# Raspberry Pi image creation
|
||||||
|
echo "Creating Raspberry Pi bootable image..."
|
||||||
|
|
||||||
|
# Find the live-build output
|
||||||
|
HDD_FILE=$(find /build -name "*.img" -type f 2>/dev/null | head -1)
|
||||||
|
SQUASHFS_FILE=$(find /build -name "filesystem.squashfs" -type f 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -z "$SQUASHFS_FILE" ]; then
|
||||||
|
echo "Looking for squashfs in chroot..."
|
||||||
|
SQUASHFS_FILE=$(find /build/chroot -name "filesystem.squashfs" -type f 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found squashfs: $SQUASHFS_FILE"
|
||||||
|
|
||||||
|
# Create RPi image using the helper script
|
||||||
|
/build/scripts/create-rpi-image.sh "$SQUASHFS_FILE" /output/ecoos-rpi.img
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Build Complete ==="
|
||||||
|
echo "Image: /output/ecoos-rpi.img"
|
||||||
|
ls -lh /output/ecoos-rpi.img
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf /tmp/iso_extract
|
|
||||||
|
|
||||||
# Copy to output
|
|
||||||
mkdir -p /output
|
|
||||||
cp "$ISO_FILE" /output/ecoos.iso
|
|
||||||
|
|
||||||
# Final verification
|
|
||||||
echo ""
|
|
||||||
echo "=== Final ISO EFI check ==="
|
|
||||||
xorriso -indev /output/ecoos.iso -find / -maxdepth 2 -type d 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Build Complete ==="
|
|
||||||
echo "ISO: /output/ecoos.iso"
|
|
||||||
ls -lh /output/ecoos.iso
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RUN chmod +x /build/docker-build.sh
|
RUN chmod +x /build/docker-build.sh
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# EcoOS Installer
|
# EcoOS Installer
|
||||||
# Installs EcoOS from live USB to disk
|
# Installs EcoOS from live USB/SD to disk
|
||||||
|
# Supports: x86_64 (UEFI), ARM64 (UEFI), Raspberry Pi (native boot)
|
||||||
#
|
#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -12,6 +13,45 @@ HOSTNAME="ecoos"
|
|||||||
USERNAME="ecouser"
|
USERNAME="ecouser"
|
||||||
SQUASHFS_PATH="/run/live/medium/live/filesystem.squashfs"
|
SQUASHFS_PATH="/run/live/medium/live/filesystem.squashfs"
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
detect_architecture() {
|
||||||
|
local arch=$(uname -m)
|
||||||
|
local is_rpi="no"
|
||||||
|
|
||||||
|
# Check if running on Raspberry Pi
|
||||||
|
if [ -f /sys/firmware/devicetree/base/model ]; then
|
||||||
|
local model=$(cat /sys/firmware/devicetree/base/model 2>/dev/null || echo "")
|
||||||
|
if [[ "$model" == *"Raspberry Pi"* ]]; then
|
||||||
|
is_rpi="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$arch" in
|
||||||
|
x86_64)
|
||||||
|
ARCH_TYPE="amd64"
|
||||||
|
BOOT_TYPE="uefi"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
if [ "$is_rpi" = "yes" ]; then
|
||||||
|
ARCH_TYPE="rpi"
|
||||||
|
BOOT_TYPE="rpi"
|
||||||
|
else
|
||||||
|
ARCH_TYPE="arm64"
|
||||||
|
BOOT_TYPE="uefi"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ARCH_TYPE="unknown"
|
||||||
|
BOOT_TYPE="unknown"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export ARCH_TYPE BOOT_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call architecture detection early
|
||||||
|
detect_architecture
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
@@ -191,12 +231,25 @@ select_disk() {
|
|||||||
partition_disk() {
|
partition_disk() {
|
||||||
local disk=$1
|
local disk=$1
|
||||||
|
|
||||||
log "Partitioning $disk..."
|
log "Partitioning $disk for $ARCH_TYPE ($BOOT_TYPE boot)..."
|
||||||
|
|
||||||
# Wipe existing partition table
|
# Wipe existing partition table
|
||||||
wipefs -a "$disk" >/dev/null 2>&1 || true
|
wipefs -a "$disk" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# Create GPT partition table
|
if [ "$BOOT_TYPE" = "rpi" ]; then
|
||||||
|
# Raspberry Pi uses MBR partition table
|
||||||
|
log "Creating MBR partition table for Raspberry Pi..."
|
||||||
|
parted -s "$disk" mklabel msdos
|
||||||
|
|
||||||
|
# Create boot partition (FAT32, 256MB)
|
||||||
|
parted -s "$disk" mkpart primary fat32 1MiB 257MiB
|
||||||
|
parted -s "$disk" set 1 boot on
|
||||||
|
|
||||||
|
# Create root partition (rest of disk)
|
||||||
|
parted -s "$disk" mkpart primary ext4 257MiB 100%
|
||||||
|
else
|
||||||
|
# x86_64 and generic ARM64 use GPT with EFI
|
||||||
|
log "Creating GPT partition table with EFI..."
|
||||||
parted -s "$disk" mklabel gpt
|
parted -s "$disk" mklabel gpt
|
||||||
|
|
||||||
# Create EFI partition (512MB)
|
# Create EFI partition (512MB)
|
||||||
@@ -205,29 +258,39 @@ partition_disk() {
|
|||||||
|
|
||||||
# Create root partition (rest of disk)
|
# Create root partition (rest of disk)
|
||||||
parted -s "$disk" mkpart root ext4 513MiB 100%
|
parted -s "$disk" mkpart root ext4 513MiB 100%
|
||||||
|
fi
|
||||||
|
|
||||||
# Wait for partitions to appear
|
# Wait for partitions to appear
|
||||||
sleep 2
|
sleep 2
|
||||||
partprobe "$disk"
|
partprobe "$disk"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
# Determine partition names (nvme vs sd)
|
# Determine partition names (nvme vs sd vs mmcblk)
|
||||||
if [[ "$disk" == *"nvme"* ]]; then
|
if [[ "$disk" == *"nvme"* ]] || [[ "$disk" == *"mmcblk"* ]]; then
|
||||||
EFI_PART="${disk}p1"
|
BOOT_PART="${disk}p1"
|
||||||
ROOT_PART="${disk}p2"
|
ROOT_PART="${disk}p2"
|
||||||
else
|
else
|
||||||
EFI_PART="${disk}1"
|
BOOT_PART="${disk}1"
|
||||||
ROOT_PART="${disk}2"
|
ROOT_PART="${disk}2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Created partitions: EFI=$EFI_PART, Root=$ROOT_PART"
|
# For UEFI systems, BOOT_PART is the EFI partition
|
||||||
|
if [ "$BOOT_TYPE" = "uefi" ]; then
|
||||||
|
EFI_PART="$BOOT_PART"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Created partitions: Boot=$BOOT_PART, Root=$ROOT_PART"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Format partitions
|
# Format partitions
|
||||||
format_partitions() {
|
format_partitions() {
|
||||||
log "Formatting partitions..."
|
log "Formatting partitions..."
|
||||||
|
|
||||||
|
if [ "$BOOT_TYPE" = "rpi" ]; then
|
||||||
|
mkfs.fat -F 32 -n "boot" "$BOOT_PART"
|
||||||
|
else
|
||||||
mkfs.fat -F 32 -n "EFI" "$EFI_PART"
|
mkfs.fat -F 32 -n "EFI" "$EFI_PART"
|
||||||
|
fi
|
||||||
mkfs.ext4 -F -L "EcoOS" "$ROOT_PART"
|
mkfs.ext4 -F -L "EcoOS" "$ROOT_PART"
|
||||||
|
|
||||||
log "Partitions formatted"
|
log "Partitions formatted"
|
||||||
@@ -240,8 +303,15 @@ mount_partitions() {
|
|||||||
mkdir -p /mnt/target
|
mkdir -p /mnt/target
|
||||||
mount "$ROOT_PART" /mnt/target
|
mount "$ROOT_PART" /mnt/target
|
||||||
|
|
||||||
|
if [ "$BOOT_TYPE" = "rpi" ]; then
|
||||||
|
# Raspberry Pi mounts boot at /boot
|
||||||
|
mkdir -p /mnt/target/boot
|
||||||
|
mount "$BOOT_PART" /mnt/target/boot
|
||||||
|
else
|
||||||
|
# UEFI systems mount EFI at /boot/efi
|
||||||
mkdir -p /mnt/target/boot/efi
|
mkdir -p /mnt/target/boot/efi
|
||||||
mount "$EFI_PART" /mnt/target/boot/efi
|
mount "$EFI_PART" /mnt/target/boot/efi
|
||||||
|
fi
|
||||||
|
|
||||||
log "Partitions mounted at /mnt/target"
|
log "Partitions mounted at /mnt/target"
|
||||||
}
|
}
|
||||||
@@ -292,14 +362,22 @@ configure_system() {
|
|||||||
|
|
||||||
# Get UUIDs
|
# Get UUIDs
|
||||||
local root_uuid=$(blkid -s UUID -o value "$ROOT_PART")
|
local root_uuid=$(blkid -s UUID -o value "$ROOT_PART")
|
||||||
local efi_uuid=$(blkid -s UUID -o value "$EFI_PART")
|
local boot_uuid=$(blkid -s UUID -o value "$BOOT_PART")
|
||||||
|
|
||||||
# Create fstab
|
# Create fstab based on boot type
|
||||||
|
if [ "$BOOT_TYPE" = "rpi" ]; then
|
||||||
|
cat > /mnt/target/etc/fstab << EOF
|
||||||
|
# EcoOS fstab - Raspberry Pi
|
||||||
|
UUID=$root_uuid / ext4 defaults,noatime 0 1
|
||||||
|
UUID=$boot_uuid /boot vfat defaults 0 2
|
||||||
|
EOF
|
||||||
|
else
|
||||||
cat > /mnt/target/etc/fstab << EOF
|
cat > /mnt/target/etc/fstab << EOF
|
||||||
# EcoOS fstab
|
# EcoOS fstab
|
||||||
UUID=$root_uuid / ext4 defaults,noatime 0 1
|
UUID=$root_uuid / ext4 defaults,noatime 0 1
|
||||||
UUID=$efi_uuid /boot/efi vfat umask=0077 0 1
|
UUID=$boot_uuid /boot/efi vfat umask=0077 0 1
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
# Set hostname
|
# Set hostname
|
||||||
echo "$HOSTNAME" > /mnt/target/etc/hostname
|
echo "$HOSTNAME" > /mnt/target/etc/hostname
|
||||||
@@ -406,9 +484,89 @@ NETEOF
|
|||||||
log "System configured"
|
log "System configured"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Raspberry Pi boot files
|
||||||
|
configure_rpi_boot() {
|
||||||
|
log "Configuring Raspberry Pi boot files..."
|
||||||
|
|
||||||
|
local root_uuid=$(blkid -s UUID -o value "$ROOT_PART")
|
||||||
|
|
||||||
|
# Copy kernel and initrd to boot partition
|
||||||
|
local kernel=$(ls /mnt/target/boot/vmlinuz-* 2>/dev/null | sort -V | tail -1)
|
||||||
|
local initrd=$(ls /mnt/target/boot/initrd.img-* 2>/dev/null | sort -V | tail -1)
|
||||||
|
|
||||||
|
if [ -n "$kernel" ]; then
|
||||||
|
cp "$kernel" /mnt/target/boot/vmlinuz
|
||||||
|
log "Copied kernel: $(basename $kernel)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$initrd" ]; then
|
||||||
|
cp "$initrd" /mnt/target/boot/initrd.img
|
||||||
|
log "Copied initrd: $(basename $initrd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy device tree blobs
|
||||||
|
local dtb_dir=$(ls -d /mnt/target/usr/lib/linux-image-*-raspi 2>/dev/null | tail -1)
|
||||||
|
if [ -d "$dtb_dir/broadcom" ]; then
|
||||||
|
cp "$dtb_dir/broadcom"/*.dtb /mnt/target/boot/ 2>/dev/null || true
|
||||||
|
log "Copied device tree blobs"
|
||||||
|
fi
|
||||||
|
if [ -d "$dtb_dir/overlays" ]; then
|
||||||
|
mkdir -p /mnt/target/boot/overlays
|
||||||
|
cp -r "$dtb_dir/overlays"/* /mnt/target/boot/overlays/ 2>/dev/null || true
|
||||||
|
log "Copied device tree overlays"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy Pi firmware files
|
||||||
|
if [ -d /mnt/target/usr/lib/raspi-firmware ]; then
|
||||||
|
cp /mnt/target/usr/lib/raspi-firmware/*.bin /mnt/target/boot/ 2>/dev/null || true
|
||||||
|
cp /mnt/target/usr/lib/raspi-firmware/*.elf /mnt/target/boot/ 2>/dev/null || true
|
||||||
|
cp /mnt/target/usr/lib/raspi-firmware/*.dat /mnt/target/boot/ 2>/dev/null || true
|
||||||
|
log "Copied Raspberry Pi firmware"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create config.txt
|
||||||
|
cat > /mnt/target/boot/config.txt << 'EOF'
|
||||||
|
# EcoOS Raspberry Pi Configuration
|
||||||
|
# Supports Pi 3, 4, and 5
|
||||||
|
|
||||||
|
# Enable 64-bit mode
|
||||||
|
arm_64bit=1
|
||||||
|
|
||||||
|
# Kernel and initrd
|
||||||
|
kernel=vmlinuz
|
||||||
|
initramfs initrd.img followkernel
|
||||||
|
|
||||||
|
# Enable serial console for debugging
|
||||||
|
enable_uart=1
|
||||||
|
|
||||||
|
# GPU/display settings
|
||||||
|
dtoverlay=vc4-kms-v3d
|
||||||
|
gpu_mem=256
|
||||||
|
|
||||||
|
# USB and power settings (Pi 4/5)
|
||||||
|
max_usb_current=1
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
dtparam=audio=on
|
||||||
|
|
||||||
|
# Camera/display interfaces
|
||||||
|
camera_auto_detect=1
|
||||||
|
display_auto_detect=1
|
||||||
|
|
||||||
|
# Pi 5 specific (ignored on older models)
|
||||||
|
[pi5]
|
||||||
|
dtoverlay=dwc2,dr_mode=host
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create cmdline.txt with root UUID
|
||||||
|
echo "console=serial0,115200 console=tty1 root=UUID=$root_uuid rootfstype=ext4 fsck.repair=yes rootwait quiet splash" > /mnt/target/boot/cmdline.txt
|
||||||
|
|
||||||
|
log "Raspberry Pi boot configured"
|
||||||
|
}
|
||||||
|
|
||||||
# Install bootloader
|
# Install bootloader
|
||||||
install_bootloader() {
|
install_bootloader() {
|
||||||
log "Installing GRUB bootloader..."
|
log "Installing bootloader for $ARCH_TYPE ($BOOT_TYPE)..."
|
||||||
|
|
||||||
# Mount necessary filesystems for chroot
|
# Mount necessary filesystems for chroot
|
||||||
mount --bind /dev /mnt/target/dev
|
mount --bind /dev /mnt/target/dev
|
||||||
@@ -417,22 +575,7 @@ install_bootloader() {
|
|||||||
mount --bind /sys /mnt/target/sys
|
mount --bind /sys /mnt/target/sys
|
||||||
mount --bind /run /mnt/target/run
|
mount --bind /run /mnt/target/run
|
||||||
|
|
||||||
# Fix GRUB default config - remove casper/live boot parameters and add serial console
|
# Disable casper-related services (common to all architectures)
|
||||||
if [ -f /mnt/target/etc/default/grub ]; then
|
|
||||||
# Remove any boot=casper or live-related parameters
|
|
||||||
sed -i 's/boot=casper//g' /mnt/target/etc/default/grub
|
|
||||||
# Update GRUB_CMDLINE_LINUX_DEFAULT with serial console for debugging
|
|
||||||
sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"/' /mnt/target/etc/default/grub
|
|
||||||
# If line doesn't exist, add it
|
|
||||||
if ! grep -q "GRUB_CMDLINE_LINUX_DEFAULT" /mnt/target/etc/default/grub; then
|
|
||||||
echo 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"' >> /mnt/target/etc/default/grub
|
|
||||||
fi
|
|
||||||
# Enable serial terminal in GRUB
|
|
||||||
echo 'GRUB_TERMINAL="console serial"' >> /mnt/target/etc/default/grub
|
|
||||||
echo 'GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"' >> /mnt/target/etc/default/grub
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Disable casper-related services
|
|
||||||
log "Disabling live boot services..."
|
log "Disabling live boot services..."
|
||||||
chroot /mnt/target systemctl disable casper.service 2>/dev/null || true
|
chroot /mnt/target systemctl disable casper.service 2>/dev/null || true
|
||||||
chroot /mnt/target systemctl disable casper-md5check.service 2>/dev/null || true
|
chroot /mnt/target systemctl disable casper-md5check.service 2>/dev/null || true
|
||||||
@@ -454,11 +597,38 @@ install_bootloader() {
|
|||||||
# Ensure proper boot target
|
# Ensure proper boot target
|
||||||
chroot /mnt/target systemctl set-default multi-user.target 2>/dev/null || true
|
chroot /mnt/target systemctl set-default multi-user.target 2>/dev/null || true
|
||||||
|
|
||||||
# Install GRUB
|
if [ "$BOOT_TYPE" = "rpi" ]; then
|
||||||
|
# Raspberry Pi uses native bootloader (no GRUB)
|
||||||
|
configure_rpi_boot
|
||||||
|
else
|
||||||
|
# UEFI systems use GRUB
|
||||||
|
log "Installing GRUB bootloader..."
|
||||||
|
|
||||||
|
# Fix GRUB default config - remove casper/live boot parameters and add serial console
|
||||||
|
if [ -f /mnt/target/etc/default/grub ]; then
|
||||||
|
# Remove any boot=casper or live-related parameters
|
||||||
|
sed -i 's/boot=casper//g' /mnt/target/etc/default/grub
|
||||||
|
# Update GRUB_CMDLINE_LINUX_DEFAULT with serial console for debugging
|
||||||
|
sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"/' /mnt/target/etc/default/grub
|
||||||
|
# If line doesn't exist, add it
|
||||||
|
if ! grep -q "GRUB_CMDLINE_LINUX_DEFAULT" /mnt/target/etc/default/grub; then
|
||||||
|
echo 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"' >> /mnt/target/etc/default/grub
|
||||||
|
fi
|
||||||
|
# Enable serial terminal in GRUB
|
||||||
|
echo 'GRUB_TERMINAL="console serial"' >> /mnt/target/etc/default/grub
|
||||||
|
echo 'GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"' >> /mnt/target/etc/default/grub
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install GRUB based on architecture
|
||||||
|
if [ "$ARCH_TYPE" = "amd64" ]; then
|
||||||
chroot /mnt/target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=EcoOS --recheck
|
chroot /mnt/target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=EcoOS --recheck
|
||||||
|
elif [ "$ARCH_TYPE" = "arm64" ]; then
|
||||||
|
chroot /mnt/target grub-install --target=arm64-efi --efi-directory=/boot/efi --bootloader-id=EcoOS --recheck
|
||||||
|
fi
|
||||||
|
|
||||||
# Generate GRUB config
|
# Generate GRUB config
|
||||||
chroot /mnt/target update-grub
|
chroot /mnt/target update-grub
|
||||||
|
fi
|
||||||
|
|
||||||
# Cleanup mounts (use lazy unmount for stubborn mounts, reverse order)
|
# Cleanup mounts (use lazy unmount for stubborn mounts, reverse order)
|
||||||
sync
|
sync
|
||||||
@@ -478,8 +648,12 @@ cleanup_and_reboot() {
|
|||||||
# Sync disks
|
# Sync disks
|
||||||
sync
|
sync
|
||||||
|
|
||||||
# Unmount
|
# Unmount based on boot type
|
||||||
|
if [ "$BOOT_TYPE" = "rpi" ]; then
|
||||||
|
umount /mnt/target/boot
|
||||||
|
else
|
||||||
umount /mnt/target/boot/efi
|
umount /mnt/target/boot/efi
|
||||||
|
fi
|
||||||
umount /mnt/target
|
umount /mnt/target
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# AMD64-specific packages
|
||||||
|
# These are appended to base.list.chroot for amd64 builds
|
||||||
|
|
||||||
|
# EFI bootloader (required for UEFI boot on x86_64)
|
||||||
|
grub-efi-amd64
|
||||||
|
grub-efi-amd64-signed
|
||||||
|
shim-signed
|
||||||
|
|
||||||
|
# x86_64 kernel (same as generic, included for clarity)
|
||||||
|
# linux-image-generic is already in base.list.chroot
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ARM64-specific packages
|
||||||
|
# These are appended to base.list.chroot for arm64 builds
|
||||||
|
|
||||||
|
# EFI bootloader (required for UEFI boot on ARM64)
|
||||||
|
grub-efi-arm64
|
||||||
|
grub-efi-arm64-signed
|
||||||
|
|
||||||
|
# ARM64 kernel (generic works for most ARM64 UEFI systems)
|
||||||
|
# linux-image-generic is already in base.list.chroot
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Raspberry Pi specific packages
|
||||||
|
# These are appended to base.list.chroot for rpi builds
|
||||||
|
|
||||||
|
# Raspberry Pi kernel (optimized for Pi hardware)
|
||||||
|
linux-image-raspi
|
||||||
|
linux-modules-extra-raspi
|
||||||
|
|
||||||
|
# Raspberry Pi firmware and utilities
|
||||||
|
linux-firmware-raspi
|
||||||
|
raspi-firmware
|
||||||
|
libraspberrypi-bin
|
||||||
|
libraspberrypi0
|
||||||
|
|
||||||
|
# Pi-specific hardware support
|
||||||
|
pi-bluetooth
|
||||||
|
rpi-eeprom
|
||||||
|
|
||||||
|
# Note: No GRUB packages - Pi uses native bootloader (config.txt + start.elf)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
# EcoOS Base Packages
|
# EcoOS Base Packages
|
||||||
|
# Common packages for all architectures
|
||||||
|
# Architecture-specific packages are in base-{amd64,arm64,rpi}.list.chroot
|
||||||
|
|
||||||
# System essentials
|
# System essentials
|
||||||
linux-image-generic
|
linux-image-generic
|
||||||
linux-headers-generic
|
linux-headers-generic
|
||||||
@@ -8,10 +11,8 @@ network-manager
|
|||||||
openssh-server
|
openssh-server
|
||||||
sudo
|
sudo
|
||||||
|
|
||||||
# EFI bootloader (required for UEFI boot)
|
# Note: EFI bootloader packages are architecture-specific
|
||||||
grub-efi-amd64
|
# See base-amd64.list.chroot, base-arm64.list.chroot, base-rpi.list.chroot
|
||||||
grub-efi-amd64-signed
|
|
||||||
shim-signed
|
|
||||||
|
|
||||||
# Sway + Wayland
|
# Sway + Wayland
|
||||||
sway
|
sway
|
||||||
|
|||||||
234
isobuild/scripts/create-rpi-image.sh
Executable file
234
isobuild/scripts/create-rpi-image.sh
Executable file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Create Raspberry Pi bootable image from live-build output
|
||||||
|
# This script creates a proper Pi-bootable image with:
|
||||||
|
# - Partition 1: FAT32 boot partition (256MB) with Pi firmware
|
||||||
|
# - Partition 2: ext4 root filesystem
|
||||||
|
#
|
||||||
|
# Usage: ./create-rpi-image.sh <squashfs_or_chroot_path> <output_image>
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INPUT_PATH="$1"
|
||||||
|
OUTPUT_IMG="$2"
|
||||||
|
IMG_SIZE="${3:-8G}"
|
||||||
|
|
||||||
|
if [ -z "$INPUT_PATH" ] || [ -z "$OUTPUT_IMG" ]; then
|
||||||
|
echo "Usage: $0 <squashfs_or_chroot_path> <output_image> [size]"
|
||||||
|
echo ""
|
||||||
|
echo "Arguments:"
|
||||||
|
echo " squashfs_or_chroot_path Path to filesystem.squashfs or chroot directory"
|
||||||
|
echo " output_image Output .img file path"
|
||||||
|
echo " size Image size (default: 8G)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Creating Raspberry Pi Image ==="
|
||||||
|
echo "Input: $INPUT_PATH"
|
||||||
|
echo "Output: $OUTPUT_IMG"
|
||||||
|
echo "Size: $IMG_SIZE"
|
||||||
|
|
||||||
|
# Create empty image
|
||||||
|
echo "Creating empty image..."
|
||||||
|
truncate -s $IMG_SIZE "$OUTPUT_IMG"
|
||||||
|
|
||||||
|
# Create partition table (MBR for Pi compatibility)
|
||||||
|
echo "Creating partition table..."
|
||||||
|
parted -s "$OUTPUT_IMG" mklabel msdos
|
||||||
|
parted -s "$OUTPUT_IMG" mkpart primary fat32 1MiB 257MiB
|
||||||
|
parted -s "$OUTPUT_IMG" mkpart primary ext4 257MiB 100%
|
||||||
|
parted -s "$OUTPUT_IMG" set 1 boot on
|
||||||
|
|
||||||
|
# Setup loop device
|
||||||
|
echo "Setting up loop device..."
|
||||||
|
LOOP_DEV=$(losetup --find --show --partscan "$OUTPUT_IMG")
|
||||||
|
echo "Loop device: $LOOP_DEV"
|
||||||
|
|
||||||
|
# Wait for partitions to appear
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
BOOT_PART="${LOOP_DEV}p1"
|
||||||
|
ROOT_PART="${LOOP_DEV}p2"
|
||||||
|
|
||||||
|
# Verify partitions exist
|
||||||
|
if [ ! -b "$BOOT_PART" ] || [ ! -b "$ROOT_PART" ]; then
|
||||||
|
echo "ERROR: Partitions not found. Trying partx..."
|
||||||
|
partx -a "$LOOP_DEV" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Boot partition: $BOOT_PART"
|
||||||
|
echo "Root partition: $ROOT_PART"
|
||||||
|
|
||||||
|
# Format partitions
|
||||||
|
echo "Formatting partitions..."
|
||||||
|
mkfs.vfat -F 32 -n "boot" "$BOOT_PART"
|
||||||
|
mkfs.ext4 -L "EcoOS" "$ROOT_PART"
|
||||||
|
|
||||||
|
# Create mount points
|
||||||
|
BOOT_MNT=$(mktemp -d)
|
||||||
|
ROOT_MNT=$(mktemp -d)
|
||||||
|
|
||||||
|
# Mount partitions
|
||||||
|
echo "Mounting partitions..."
|
||||||
|
mount "$BOOT_PART" "$BOOT_MNT"
|
||||||
|
mount "$ROOT_PART" "$ROOT_MNT"
|
||||||
|
|
||||||
|
# Extract or copy rootfs
|
||||||
|
echo "Copying root filesystem..."
|
||||||
|
if [ -f "$INPUT_PATH" ] && file "$INPUT_PATH" | grep -q "Squashfs"; then
|
||||||
|
# It's a squashfs file - extract it
|
||||||
|
echo "Extracting squashfs..."
|
||||||
|
unsquashfs -f -d "$ROOT_MNT" "$INPUT_PATH"
|
||||||
|
elif [ -d "$INPUT_PATH" ]; then
|
||||||
|
# It's a directory (chroot) - copy it
|
||||||
|
echo "Copying chroot directory..."
|
||||||
|
cp -a "$INPUT_PATH"/* "$ROOT_MNT"/
|
||||||
|
else
|
||||||
|
echo "ERROR: Input path is neither a squashfs file nor a directory"
|
||||||
|
umount "$BOOT_MNT" "$ROOT_MNT"
|
||||||
|
losetup -d "$LOOP_DEV"
|
||||||
|
rm -rf "$BOOT_MNT" "$ROOT_MNT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy kernel and initrd to boot partition
|
||||||
|
echo "Setting up boot partition..."
|
||||||
|
|
||||||
|
# Find kernel and initrd
|
||||||
|
KERNEL=$(ls "$ROOT_MNT"/boot/vmlinuz-* 2>/dev/null | sort -V | tail -1)
|
||||||
|
INITRD=$(ls "$ROOT_MNT"/boot/initrd.img-* 2>/dev/null | sort -V | tail -1)
|
||||||
|
|
||||||
|
if [ -n "$KERNEL" ]; then
|
||||||
|
cp "$KERNEL" "$BOOT_MNT/vmlinuz"
|
||||||
|
echo "Copied kernel: $(basename $KERNEL)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$INITRD" ]; then
|
||||||
|
cp "$INITRD" "$BOOT_MNT/initrd.img"
|
||||||
|
echo "Copied initrd: $(basename $INITRD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy device tree blobs if present
|
||||||
|
if [ -d "$ROOT_MNT/usr/lib/linux-image-"*"-raspi" ]; then
|
||||||
|
DTB_DIR=$(ls -d "$ROOT_MNT/usr/lib/linux-image-"*"-raspi" 2>/dev/null | tail -1)
|
||||||
|
if [ -d "$DTB_DIR/broadcom" ]; then
|
||||||
|
cp -r "$DTB_DIR/broadcom"/*.dtb "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
echo "Copied device tree blobs"
|
||||||
|
fi
|
||||||
|
if [ -d "$DTB_DIR/overlays" ]; then
|
||||||
|
mkdir -p "$BOOT_MNT/overlays"
|
||||||
|
cp -r "$DTB_DIR/overlays"/* "$BOOT_MNT/overlays/" 2>/dev/null || true
|
||||||
|
echo "Copied device tree overlays"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy Pi firmware files
|
||||||
|
echo "Copying Raspberry Pi firmware..."
|
||||||
|
if [ -d "$ROOT_MNT/usr/lib/raspi-firmware" ]; then
|
||||||
|
cp "$ROOT_MNT/usr/lib/raspi-firmware"/*.bin "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
cp "$ROOT_MNT/usr/lib/raspi-firmware"/*.elf "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
cp "$ROOT_MNT/usr/lib/raspi-firmware"/*.dat "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
echo "Copied firmware files from raspi-firmware"
|
||||||
|
elif [ -d "$ROOT_MNT/boot/firmware" ]; then
|
||||||
|
cp "$ROOT_MNT/boot/firmware"/*.bin "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
cp "$ROOT_MNT/boot/firmware"/*.elf "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
cp "$ROOT_MNT/boot/firmware"/*.dat "$BOOT_MNT/" 2>/dev/null || true
|
||||||
|
echo "Copied firmware files from /boot/firmware"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create config.txt if not present
|
||||||
|
if [ ! -f "$BOOT_MNT/config.txt" ]; then
|
||||||
|
echo "Creating config.txt..."
|
||||||
|
cat > "$BOOT_MNT/config.txt" << 'EOF'
|
||||||
|
# EcoOS Raspberry Pi Configuration
|
||||||
|
# Supports Pi 3, 4, and 5
|
||||||
|
|
||||||
|
# Enable 64-bit mode
|
||||||
|
arm_64bit=1
|
||||||
|
|
||||||
|
# Kernel and initrd
|
||||||
|
kernel=vmlinuz
|
||||||
|
initramfs initrd.img followkernel
|
||||||
|
|
||||||
|
# Enable serial console for debugging
|
||||||
|
enable_uart=1
|
||||||
|
|
||||||
|
# GPU/display settings
|
||||||
|
dtoverlay=vc4-kms-v3d
|
||||||
|
gpu_mem=256
|
||||||
|
|
||||||
|
# USB and power settings (Pi 4/5)
|
||||||
|
max_usb_current=1
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
dtparam=audio=on
|
||||||
|
|
||||||
|
# Camera/display interfaces
|
||||||
|
camera_auto_detect=1
|
||||||
|
display_auto_detect=1
|
||||||
|
|
||||||
|
# Pi 5 specific (ignored on older models)
|
||||||
|
[pi5]
|
||||||
|
dtoverlay=dwc2,dr_mode=host
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create cmdline.txt if not present
|
||||||
|
if [ ! -f "$BOOT_MNT/cmdline.txt" ]; then
|
||||||
|
echo "Creating cmdline.txt..."
|
||||||
|
# Get the UUID of the root partition
|
||||||
|
ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART")
|
||||||
|
if [ -n "$ROOT_UUID" ]; then
|
||||||
|
echo "console=serial0,115200 console=tty1 root=UUID=$ROOT_UUID rootfstype=ext4 fsck.repair=yes rootwait quiet splash" > "$BOOT_MNT/cmdline.txt"
|
||||||
|
else
|
||||||
|
echo "console=serial0,115200 console=tty1 root=LABEL=EcoOS rootfstype=ext4 fsck.repair=yes rootwait quiet splash" > "$BOOT_MNT/cmdline.txt"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update fstab in the root filesystem
|
||||||
|
echo "Updating /etc/fstab..."
|
||||||
|
BOOT_UUID=$(blkid -s UUID -o value "$BOOT_PART")
|
||||||
|
ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART")
|
||||||
|
|
||||||
|
cat > "$ROOT_MNT/etc/fstab" << EOF
|
||||||
|
# EcoOS fstab - Raspberry Pi
|
||||||
|
# <file system> <mount point> <type> <options> <dump> <pass>
|
||||||
|
|
||||||
|
# Root filesystem
|
||||||
|
UUID=$ROOT_UUID / ext4 defaults,noatime 0 1
|
||||||
|
|
||||||
|
# Boot partition
|
||||||
|
UUID=$BOOT_UUID /boot vfat defaults 0 2
|
||||||
|
|
||||||
|
# Swap (if needed)
|
||||||
|
# /swapfile none swap sw 0 0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create symlink for boot files in rootfs
|
||||||
|
mkdir -p "$ROOT_MNT/boot"
|
||||||
|
echo "Boot partition will be mounted at /boot"
|
||||||
|
|
||||||
|
# Set hostname
|
||||||
|
echo "ecoos-rpi" > "$ROOT_MNT/etc/hostname"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
echo "Cleaning up..."
|
||||||
|
sync
|
||||||
|
umount "$BOOT_MNT"
|
||||||
|
umount "$ROOT_MNT"
|
||||||
|
losetup -d "$LOOP_DEV"
|
||||||
|
rm -rf "$BOOT_MNT" "$ROOT_MNT"
|
||||||
|
|
||||||
|
# Final size
|
||||||
|
FINAL_SIZE=$(ls -lh "$OUTPUT_IMG" | awk '{print $5}')
|
||||||
|
echo ""
|
||||||
|
echo "=== Raspberry Pi Image Created ==="
|
||||||
|
echo "Output: $OUTPUT_IMG"
|
||||||
|
echo "Size: $FINAL_SIZE"
|
||||||
|
echo ""
|
||||||
|
echo "To flash to SD card:"
|
||||||
|
echo " sudo dd if=$OUTPUT_IMG of=/dev/sdX bs=4M status=progress"
|
||||||
|
echo ""
|
||||||
|
echo "Or use Raspberry Pi Imager for a safer flash."
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
# Build EcoOS ISO using Docker
|
# Build EcoOS ISO using Docker
|
||||||
# This avoids needing to install live-build on the host
|
# This avoids needing to install live-build on the host
|
||||||
#
|
#
|
||||||
|
# Usage: ./docker-build.sh [--arch=amd64|arm64|rpi]
|
||||||
|
#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -10,25 +12,110 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
ISOBUILD_DIR="$(dirname "$SCRIPT_DIR")"
|
ISOBUILD_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
ECO_OS_DIR="$(dirname "$ISOBUILD_DIR")"
|
ECO_OS_DIR="$(dirname "$ISOBUILD_DIR")"
|
||||||
|
|
||||||
|
# Default architecture
|
||||||
|
TARGET_ARCH="amd64"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
--arch=*)
|
||||||
|
TARGET_ARCH="${arg#*=}"
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [--arch=amd64|arm64|rpi]"
|
||||||
|
echo ""
|
||||||
|
echo "Architectures:"
|
||||||
|
echo " amd64 - x86_64 with UEFI/GRUB boot (default)"
|
||||||
|
echo " arm64 - Generic ARM64 with UEFI/GRUB boot"
|
||||||
|
echo " rpi - Raspberry Pi 3/4/5 with native bootloader"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $arg"
|
||||||
|
echo "Use --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate architecture
|
||||||
|
case "$TARGET_ARCH" in
|
||||||
|
amd64|arm64|rpi)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Invalid architecture '$TARGET_ARCH'"
|
||||||
|
echo "Valid options: amd64, arm64, rpi"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Determine output filename based on architecture
|
||||||
|
case "$TARGET_ARCH" in
|
||||||
|
amd64)
|
||||||
|
OUTPUT_FILE="ecoos.iso"
|
||||||
|
;;
|
||||||
|
arm64)
|
||||||
|
OUTPUT_FILE="ecoos-arm64.iso"
|
||||||
|
;;
|
||||||
|
rpi)
|
||||||
|
OUTPUT_FILE="ecoos-rpi.img"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "=== EcoOS ISO Builder (Docker) ==="
|
echo "=== EcoOS ISO Builder (Docker) ==="
|
||||||
|
echo "Target architecture: $TARGET_ARCH"
|
||||||
|
echo "Output file: $OUTPUT_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
cd "$ECO_OS_DIR"
|
cd "$ECO_OS_DIR"
|
||||||
|
|
||||||
# Build the Docker image
|
# Build the Docker image with architecture argument
|
||||||
echo "[1/2] Building Docker image..."
|
echo "[1/2] Building Docker image for $TARGET_ARCH..."
|
||||||
docker build -t ecoos-builder -f isobuild/Dockerfile .
|
|
||||||
|
# For ARM builds on x86 hosts, we need to use buildx with platform emulation
|
||||||
|
if [ "$TARGET_ARCH" = "arm64" ] || [ "$TARGET_ARCH" = "rpi" ]; then
|
||||||
|
HOST_ARCH=$(uname -m)
|
||||||
|
if [ "$HOST_ARCH" = "x86_64" ]; then
|
||||||
|
echo "Cross-building ARM on x86_64 host - using Docker buildx with QEMU emulation"
|
||||||
|
echo "Note: This requires QEMU binfmt. If this fails, run:"
|
||||||
|
echo " docker run --privileged --rm tonistiigi/binfmt --install all"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ensure buildx is available and create builder if needed
|
||||||
|
docker buildx inspect ecoos-builder >/dev/null 2>&1 || \
|
||||||
|
docker buildx create --name ecoos-builder --use
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--build-arg TARGET_ARCH="$TARGET_ARCH" \
|
||||||
|
--load \
|
||||||
|
-t ecoos-builder-$TARGET_ARCH \
|
||||||
|
-f isobuild/Dockerfile .
|
||||||
|
else
|
||||||
|
# Running on ARM host, use regular build
|
||||||
|
docker build \
|
||||||
|
--build-arg TARGET_ARCH="$TARGET_ARCH" \
|
||||||
|
-t ecoos-builder-$TARGET_ARCH \
|
||||||
|
-f isobuild/Dockerfile .
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
docker build \
|
||||||
|
--build-arg TARGET_ARCH="$TARGET_ARCH" \
|
||||||
|
-t ecoos-builder-$TARGET_ARCH \
|
||||||
|
-f isobuild/Dockerfile .
|
||||||
|
fi
|
||||||
|
|
||||||
# Run the build
|
# Run the build
|
||||||
echo ""
|
echo ""
|
||||||
echo "[2/2] Building ISO (this may take 15-30 minutes)..."
|
echo "[2/2] Building image (this may take 15-30 minutes)..."
|
||||||
mkdir -p "$ISOBUILD_DIR/output"
|
mkdir -p "$ISOBUILD_DIR/output"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--privileged \
|
--privileged \
|
||||||
|
-e TARGET_ARCH="$TARGET_ARCH" \
|
||||||
-v "$ISOBUILD_DIR/output:/output" \
|
-v "$ISOBUILD_DIR/output:/output" \
|
||||||
ecoos-builder
|
ecoos-builder-$TARGET_ARCH
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Build Complete ==="
|
echo "=== Build Complete ==="
|
||||||
echo "ISO: $ISOBUILD_DIR/output/ecoos.iso"
|
echo "Output: $ISOBUILD_DIR/output/$OUTPUT_FILE"
|
||||||
|
|||||||
@@ -5,35 +5,101 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
AUTO_MODE=false
|
AUTO_MODE=false
|
||||||
|
TARGET_ARCH="amd64"
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case $arg in
|
case $arg in
|
||||||
--auto)
|
--auto)
|
||||||
AUTO_MODE=true
|
AUTO_MODE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--arch=*)
|
||||||
|
TARGET_ARCH="${arg#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [--arch=amd64|arm64|rpi] [--auto]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --arch=ARCH Target architecture (default: amd64)"
|
||||||
|
echo " amd64 - x86_64 with UEFI/OVMF"
|
||||||
|
echo " arm64 - Generic ARM64 with UEFI"
|
||||||
|
echo " rpi - Raspberry Pi 3 emulation"
|
||||||
|
echo " --auto Run in automatic mode for CI/testing"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Validate architecture
|
||||||
|
case "$TARGET_ARCH" in
|
||||||
|
amd64|arm64|rpi)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Invalid architecture '$TARGET_ARCH'"
|
||||||
|
echo "Valid options: amd64, arm64, rpi"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
PROJECT_ROOT="$SCRIPT_DIR/.."
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
VM_DIR="$PROJECT_ROOT/.nogit/vm"
|
VM_DIR="$PROJECT_ROOT/.nogit/vm"
|
||||||
ISO_PATH="$PROJECT_ROOT/.nogit/iso/ecoos.iso"
|
|
||||||
DISK_PATH="$VM_DIR/test-disk.qcow2"
|
|
||||||
MONITOR_SOCK="$VM_DIR/qemu-monitor.sock"
|
MONITOR_SOCK="$VM_DIR/qemu-monitor.sock"
|
||||||
SERIAL_SOCK="$VM_DIR/serial.sock"
|
SERIAL_SOCK="$VM_DIR/serial.sock"
|
||||||
SERIAL_LOG="$VM_DIR/serial.log"
|
SERIAL_LOG="$VM_DIR/serial.log"
|
||||||
PID_FILE="$VM_DIR/qemu.pid"
|
PID_FILE="$VM_DIR/qemu.pid"
|
||||||
|
|
||||||
|
# Architecture-specific settings
|
||||||
|
case "$TARGET_ARCH" in
|
||||||
|
amd64)
|
||||||
|
ISO_PATH="$PROJECT_ROOT/.nogit/iso/ecoos.iso"
|
||||||
|
DISK_PATH="$VM_DIR/test-disk.qcow2"
|
||||||
|
QEMU_CMD="qemu-system-x86_64"
|
||||||
|
QEMU_MACHINE=""
|
||||||
|
QEMU_BIOS="-bios /usr/share/qemu/OVMF.fd"
|
||||||
|
KVM_CHECK="/dev/kvm"
|
||||||
|
DISK_IF="virtio"
|
||||||
|
;;
|
||||||
|
arm64)
|
||||||
|
ISO_PATH="$PROJECT_ROOT/.nogit/iso/ecoos-arm64.iso"
|
||||||
|
DISK_PATH="$VM_DIR/test-disk-arm64.qcow2"
|
||||||
|
QEMU_CMD="qemu-system-aarch64"
|
||||||
|
QEMU_MACHINE="-M virt -cpu cortex-a72"
|
||||||
|
QEMU_BIOS="-bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd"
|
||||||
|
KVM_CHECK="" # ARM KVM only works on ARM hosts
|
||||||
|
DISK_IF="virtio"
|
||||||
|
;;
|
||||||
|
rpi)
|
||||||
|
IMG_PATH="$PROJECT_ROOT/.nogit/iso/ecoos-rpi.img"
|
||||||
|
DISK_PATH="" # RPi uses the image directly
|
||||||
|
QEMU_CMD="qemu-system-aarch64"
|
||||||
|
QEMU_MACHINE="-M raspi3b -cpu cortex-a53"
|
||||||
|
QEMU_BIOS="" # RPi uses direct kernel boot
|
||||||
|
KVM_CHECK=""
|
||||||
|
DISK_IF="sd"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Create VM directory if not exists
|
# Create VM directory if not exists
|
||||||
mkdir -p "$VM_DIR"
|
mkdir -p "$VM_DIR"
|
||||||
|
|
||||||
# Check if ISO exists
|
# Check if image exists
|
||||||
if [ ! -f "$ISO_PATH" ]; then
|
if [ "$TARGET_ARCH" = "rpi" ]; then
|
||||||
echo "ERROR: ISO not found at $ISO_PATH"
|
if [ ! -f "$IMG_PATH" ]; then
|
||||||
echo "Run 'pnpm run build' first to create the ISO"
|
echo "ERROR: RPi image not found at $IMG_PATH"
|
||||||
|
echo "Run 'pnpm run build:rpi' first to create the image"
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ ! -f "$ISO_PATH" ]; then
|
||||||
|
echo "ERROR: ISO not found at $ISO_PATH"
|
||||||
|
echo "Run 'pnpm run build:$TARGET_ARCH' first to create the ISO"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create test disk if not exists
|
# Create test disk if not exists (not needed for RPi)
|
||||||
if [ ! -f "$DISK_PATH" ]; then
|
if [ -n "$DISK_PATH" ] && [ ! -f "$DISK_PATH" ]; then
|
||||||
echo "Creating test disk (20GB)..."
|
echo "Creating test disk (20GB)..."
|
||||||
qemu-img create -f qcow2 "$DISK_PATH" 20G
|
qemu-img create -f qcow2 "$DISK_PATH" 20G
|
||||||
fi
|
fi
|
||||||
@@ -48,15 +114,19 @@ if [ -f "$PID_FILE" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting QEMU with EcoOS ISO..."
|
echo "Starting QEMU with EcoOS for $TARGET_ARCH..."
|
||||||
|
|
||||||
# Check if KVM is available
|
# Check if KVM is available (only for amd64 on x86 hosts)
|
||||||
KVM_OPTS=""
|
KVM_OPTS=""
|
||||||
if [ -e /dev/kvm ] && [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
|
if [ -n "$KVM_CHECK" ] && [ -e "$KVM_CHECK" ] && [ -r "$KVM_CHECK" ] && [ -w "$KVM_CHECK" ]; then
|
||||||
KVM_OPTS="-enable-kvm -cpu host"
|
KVM_OPTS="-enable-kvm -cpu host"
|
||||||
echo "Using KVM acceleration"
|
echo "Using KVM acceleration"
|
||||||
else
|
else
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
echo "KVM not available, using software emulation (slower)"
|
echo "KVM not available, using software emulation (slower)"
|
||||||
|
else
|
||||||
|
echo "Running ARM emulation on x86 host (slower)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Cleanup function
|
# Cleanup function
|
||||||
@@ -89,13 +159,16 @@ cleanup() {
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
# Start QEMU with virtio-gpu multi-head (3 outputs)
|
# Start QEMU based on architecture
|
||||||
> "$SERIAL_LOG" # Clear old log
|
> "$SERIAL_LOG" # Clear old log
|
||||||
qemu-system-x86_64 \
|
|
||||||
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
|
# AMD64 with multi-display support
|
||||||
|
$QEMU_CMD \
|
||||||
$KVM_OPTS \
|
$KVM_OPTS \
|
||||||
-m 4G \
|
-m 4G \
|
||||||
-smp 4 \
|
-smp 4 \
|
||||||
-bios /usr/share/qemu/OVMF.fd \
|
$QEMU_BIOS \
|
||||||
-drive file="$ISO_PATH",media=cdrom \
|
-drive file="$ISO_PATH",media=cdrom \
|
||||||
-drive file="$DISK_PATH",format=qcow2,if=virtio \
|
-drive file="$DISK_PATH",format=qcow2,if=virtio \
|
||||||
-device qxl-vga,id=video0,ram_size=67108864,vram_size=67108864,vgamem_mb=64 \
|
-device qxl-vga,id=video0,ram_size=67108864,vram_size=67108864,vgamem_mb=64 \
|
||||||
@@ -111,68 +184,132 @@ qemu-system-x86_64 \
|
|||||||
-nic user,model=virtio-net-pci,hostfwd=tcp::3006-:3006,hostfwd=tcp::2222-:22 \
|
-nic user,model=virtio-net-pci,hostfwd=tcp::3006-:3006,hostfwd=tcp::2222-:22 \
|
||||||
-pidfile "$PID_FILE" &
|
-pidfile "$PID_FILE" &
|
||||||
|
|
||||||
|
elif [ "$TARGET_ARCH" = "arm64" ]; then
|
||||||
|
# ARM64 with UEFI
|
||||||
|
$QEMU_CMD \
|
||||||
|
$QEMU_MACHINE \
|
||||||
|
-m 4G \
|
||||||
|
-smp 4 \
|
||||||
|
$QEMU_BIOS \
|
||||||
|
-drive file="$ISO_PATH",media=cdrom,if=none,id=cdrom \
|
||||||
|
-device virtio-blk-device,drive=cdrom \
|
||||||
|
-drive file="$DISK_PATH",format=qcow2,if=none,id=hd0 \
|
||||||
|
-device virtio-blk-device,drive=hd0 \
|
||||||
|
-device virtio-gpu-pci \
|
||||||
|
-display none \
|
||||||
|
-serial unix:"$SERIAL_SOCK",server,nowait \
|
||||||
|
-monitor unix:"$MONITOR_SOCK",server,nowait \
|
||||||
|
-device virtio-net-device,netdev=net0 \
|
||||||
|
-netdev user,id=net0,hostfwd=tcp::3006-:3006,hostfwd=tcp::2222-:22 \
|
||||||
|
-pidfile "$PID_FILE" &
|
||||||
|
|
||||||
|
elif [ "$TARGET_ARCH" = "rpi" ]; then
|
||||||
|
# Raspberry Pi 3B emulation
|
||||||
|
# Note: raspi3b machine has limited support, uses direct kernel boot
|
||||||
|
echo "NOTE: Raspberry Pi emulation is limited."
|
||||||
|
echo " For full testing, use real hardware."
|
||||||
|
|
||||||
|
# Extract kernel and initrd from image for direct boot
|
||||||
|
TEMP_MNT=$(mktemp -d)
|
||||||
|
LOOP_DEV=$(sudo losetup --find --show --partscan "$IMG_PATH")
|
||||||
|
sudo mount "${LOOP_DEV}p1" "$TEMP_MNT"
|
||||||
|
|
||||||
|
KERNEL="$TEMP_MNT/vmlinuz"
|
||||||
|
INITRD="$TEMP_MNT/initrd.img"
|
||||||
|
DTB="$TEMP_MNT/bcm2710-rpi-3-b.dtb"
|
||||||
|
|
||||||
|
if [ ! -f "$KERNEL" ]; then
|
||||||
|
echo "ERROR: Kernel not found in RPi image"
|
||||||
|
sudo umount "$TEMP_MNT"
|
||||||
|
sudo losetup -d "$LOOP_DEV"
|
||||||
|
rm -rf "$TEMP_MNT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy kernel/initrd to temp location for QEMU
|
||||||
|
cp "$KERNEL" "$VM_DIR/rpi-kernel"
|
||||||
|
cp "$INITRD" "$VM_DIR/rpi-initrd" 2>/dev/null || true
|
||||||
|
cp "$DTB" "$VM_DIR/rpi-dtb" 2>/dev/null || true
|
||||||
|
|
||||||
|
sudo umount "$TEMP_MNT"
|
||||||
|
sudo losetup -d "$LOOP_DEV"
|
||||||
|
rm -rf "$TEMP_MNT"
|
||||||
|
|
||||||
|
$QEMU_CMD \
|
||||||
|
$QEMU_MACHINE \
|
||||||
|
-m 1G \
|
||||||
|
-kernel "$VM_DIR/rpi-kernel" \
|
||||||
|
-initrd "$VM_DIR/rpi-initrd" \
|
||||||
|
-dtb "$VM_DIR/rpi-dtb" \
|
||||||
|
-append "console=ttyAMA0,115200 root=LABEL=EcoOS rootfstype=ext4 rootwait" \
|
||||||
|
-drive file="$IMG_PATH",format=raw,if=sd \
|
||||||
|
-serial unix:"$SERIAL_SOCK",server,nowait \
|
||||||
|
-display none \
|
||||||
|
-pidfile "$PID_FILE" &
|
||||||
|
fi
|
||||||
|
|
||||||
QEMU_PID=$!
|
QEMU_PID=$!
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== EcoOS Test VM Started ==="
|
echo "=== EcoOS Test VM Started ($TARGET_ARCH) ==="
|
||||||
echo "QEMU PID: $QEMU_PID"
|
echo "QEMU PID: $QEMU_PID"
|
||||||
echo "Management UI: http://localhost:3006"
|
if [ "$TARGET_ARCH" != "rpi" ]; then
|
||||||
|
echo "Management UI: http://localhost:3006"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Wait for QEMU to start and SPICE to be ready
|
# AMD64-specific display setup
|
||||||
echo "Waiting for SPICE server..."
|
if [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
sleep 3
|
# Wait for QEMU to start and SPICE to be ready
|
||||||
|
echo "Waiting for SPICE server..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
# Check if remote-viewer is available
|
# Check if remote-viewer is available
|
||||||
if ! command -v remote-viewer &> /dev/null; then
|
if ! command -v remote-viewer &> /dev/null; then
|
||||||
echo "WARNING: remote-viewer not installed"
|
echo "WARNING: remote-viewer not installed"
|
||||||
echo "Install with: sudo apt install virt-viewer"
|
echo "Install with: sudo apt install virt-viewer"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Running without display viewer. Press Ctrl-C to stop."
|
echo "Running without display viewer. Press Ctrl-C to stop."
|
||||||
wait $QEMU_PID
|
wait $QEMU_PID
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up virt-viewer settings for multi-display
|
# Set up virt-viewer settings for multi-display
|
||||||
VIRT_VIEWER_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/virt-viewer"
|
VIRT_VIEWER_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/virt-viewer"
|
||||||
mkdir -p "$VIRT_VIEWER_CONFIG_DIR"
|
mkdir -p "$VIRT_VIEWER_CONFIG_DIR"
|
||||||
if [ -f "$SCRIPT_DIR/virt-viewer-settings" ]; then
|
if [ -f "$SCRIPT_DIR/virt-viewer-settings" ]; then
|
||||||
cp "$SCRIPT_DIR/virt-viewer-settings" "$VIRT_VIEWER_CONFIG_DIR/settings"
|
cp "$SCRIPT_DIR/virt-viewer-settings" "$VIRT_VIEWER_CONFIG_DIR/settings"
|
||||||
echo "Configured virt-viewer for 3 displays"
|
echo "Configured virt-viewer for 3 displays"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect DISPLAY if not set
|
# Detect DISPLAY if not set
|
||||||
if [ -z "$DISPLAY" ]; then
|
if [ -z "$DISPLAY" ]; then
|
||||||
# Try to find an active X display
|
|
||||||
if [ -S /tmp/.X11-unix/X0 ]; then
|
if [ -S /tmp/.X11-unix/X0 ]; then
|
||||||
export DISPLAY=:0
|
export DISPLAY=:0
|
||||||
elif [ -S /tmp/.X11-unix/X1 ]; then
|
elif [ -S /tmp/.X11-unix/X1 ]; then
|
||||||
export DISPLAY=:1
|
export DISPLAY=:1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect WAYLAND_DISPLAY if not set
|
# Detect WAYLAND_DISPLAY if not set
|
||||||
if [ -z "$WAYLAND_DISPLAY" ] && [ -z "$DISPLAY" ]; then
|
if [ -z "$WAYLAND_DISPLAY" ] && [ -z "$DISPLAY" ]; then
|
||||||
# Try common Wayland sockets
|
|
||||||
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ]; then
|
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ]; then
|
||||||
export WAYLAND_DISPLAY=wayland-0
|
export WAYLAND_DISPLAY=wayland-0
|
||||||
elif [ -S "/run/user/$(id -u)/wayland-0" ]; then
|
elif [ -S "/run/user/$(id -u)/wayland-0" ]; then
|
||||||
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||||
export WAYLAND_DISPLAY=wayland-0
|
export WAYLAND_DISPLAY=wayland-0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Launch remote-viewer - use dummy X server with 3 monitors if no display available
|
# Launch remote-viewer
|
||||||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
||||||
echo "No display found, starting headless X server with 3 virtual monitors..."
|
echo "No display found, starting headless X server with 3 virtual monitors..."
|
||||||
|
|
||||||
# Find an available display number
|
|
||||||
XDISPLAY=99
|
XDISPLAY=99
|
||||||
while [ -S "/tmp/.X11-unix/X$XDISPLAY" ]; do
|
while [ -S "/tmp/.X11-unix/X$XDISPLAY" ]; do
|
||||||
XDISPLAY=$((XDISPLAY + 1))
|
XDISPLAY=$((XDISPLAY + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
# Start Xorg with dummy driver config for 3 monitors
|
|
||||||
XORG_CONFIG="$SCRIPT_DIR/xorg-dummy.conf"
|
XORG_CONFIG="$SCRIPT_DIR/xorg-dummy.conf"
|
||||||
Xorg :$XDISPLAY -config "$XORG_CONFIG" -noreset +extension GLX +extension RANDR +extension RENDER &
|
Xorg :$XDISPLAY -config "$XORG_CONFIG" -noreset +extension GLX +extension RANDR +extension RENDER &
|
||||||
XORG_PID=$!
|
XORG_PID=$!
|
||||||
@@ -180,57 +317,48 @@ if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
|||||||
|
|
||||||
export DISPLAY=:$XDISPLAY
|
export DISPLAY=:$XDISPLAY
|
||||||
|
|
||||||
# Configure 3 virtual monitors using xrandr
|
|
||||||
# Add mode to disconnected DUMMY outputs and position them
|
|
||||||
xrandr --newmode "1920x1080" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync 2>/dev/null || true
|
xrandr --newmode "1920x1080" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync 2>/dev/null || true
|
||||||
|
|
||||||
# Add mode to DUMMY1 and DUMMY2, then enable them
|
|
||||||
xrandr --addmode DUMMY1 "1920x1080" 2>/dev/null || true
|
xrandr --addmode DUMMY1 "1920x1080" 2>/dev/null || true
|
||||||
xrandr --addmode DUMMY2 "1920x1080" 2>/dev/null || true
|
xrandr --addmode DUMMY2 "1920x1080" 2>/dev/null || true
|
||||||
|
|
||||||
# Position the outputs side by side
|
|
||||||
xrandr --output DUMMY0 --mode 1920x1080 --pos 0x0 --primary
|
xrandr --output DUMMY0 --mode 1920x1080 --pos 0x0 --primary
|
||||||
xrandr --output DUMMY1 --mode 1920x1080 --pos 1920x0 2>/dev/null || true
|
xrandr --output DUMMY1 --mode 1920x1080 --pos 1920x0 2>/dev/null || true
|
||||||
xrandr --output DUMMY2 --mode 1920x1080 --pos 3840x0 2>/dev/null || true
|
xrandr --output DUMMY2 --mode 1920x1080 --pos 3840x0 2>/dev/null || true
|
||||||
|
|
||||||
echo "Headless X server started on :$XDISPLAY"
|
echo "Headless X server started on :$XDISPLAY"
|
||||||
|
|
||||||
# Launch remote-viewer in fullscreen to request all monitors
|
|
||||||
remote-viewer --full-screen spice://localhost:5930 &
|
remote-viewer --full-screen spice://localhost:5930 &
|
||||||
VIEWER_PID=$!
|
VIEWER_PID=$!
|
||||||
echo "remote-viewer running headlessly (PID: $VIEWER_PID)"
|
echo "remote-viewer running headlessly (PID: $VIEWER_PID)"
|
||||||
else
|
else
|
||||||
echo "Launching remote-viewer with fullscreen for multi-display (DISPLAY=$DISPLAY, WAYLAND_DISPLAY=$WAYLAND_DISPLAY)..."
|
echo "Launching remote-viewer with fullscreen for multi-display..."
|
||||||
remote-viewer --full-screen spice://localhost:5930 &
|
remote-viewer --full-screen spice://localhost:5930 &
|
||||||
VIEWER_PID=$!
|
VIEWER_PID=$!
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "=== Press Ctrl-C to stop ==="
|
# Enable all 3 displays via SPICE protocol
|
||||||
echo ""
|
if [ -f "$SCRIPT_DIR/enable-displays.py" ]; then
|
||||||
|
|
||||||
# Enable all 3 displays via SPICE protocol (waits for agent automatically)
|
|
||||||
# Using 300s timeout since ISO boot can take several minutes
|
|
||||||
if [ -f "$SCRIPT_DIR/enable-displays.py" ]; then
|
|
||||||
echo "Enabling displays (waiting for SPICE agent, up to 5 minutes)..."
|
echo "Enabling displays (waiting for SPICE agent, up to 5 minutes)..."
|
||||||
python3 "$SCRIPT_DIR/enable-displays.py" --timeout 300 2>&1 &
|
python3 "$SCRIPT_DIR/enable-displays.py" --timeout 300 2>&1 &
|
||||||
ENABLE_PID=$!
|
ENABLE_PID=$!
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start screenshot loop in background
|
||||||
|
echo "Starting screenshot loop..."
|
||||||
|
(while true; do "$SCRIPT_DIR/screenshot.sh" 2>/dev/null; sleep 5; done) &
|
||||||
|
SCREENSHOT_LOOP_PID=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Tips:"
|
echo "Tips:"
|
||||||
echo " - http://localhost:3006 - Management UI"
|
|
||||||
echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)"
|
echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)"
|
||||||
|
if [ "$TARGET_ARCH" != "rpi" ]; then
|
||||||
|
echo " - http://localhost:3006 - Management UI"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Start screenshot loop in background (takes screenshots every 5 seconds)
|
if [ "$AUTO_MODE" = true ] && [ "$TARGET_ARCH" = "amd64" ]; then
|
||||||
echo "Starting screenshot loop..."
|
|
||||||
(while true; do "$SCRIPT_DIR/screenshot.sh" 2>/dev/null; sleep 5; done) &
|
|
||||||
SCREENSHOT_LOOP_PID=$!
|
|
||||||
|
|
||||||
if [ "$AUTO_MODE" = true ]; then
|
|
||||||
echo "=== Auto mode: waiting for display setup ==="
|
echo "=== Auto mode: waiting for display setup ==="
|
||||||
|
|
||||||
# Wait for enable-displays.py to complete
|
|
||||||
if [ -n "$ENABLE_PID" ]; then
|
if [ -n "$ENABLE_PID" ]; then
|
||||||
wait $ENABLE_PID
|
wait $ENABLE_PID
|
||||||
ENABLE_EXIT=$?
|
ENABLE_EXIT=$?
|
||||||
@@ -240,11 +368,9 @@ if [ "$AUTO_MODE" = true ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Take screenshot
|
|
||||||
echo "Taking screenshot..."
|
echo "Taking screenshot..."
|
||||||
"$SCRIPT_DIR/screenshot.sh"
|
"$SCRIPT_DIR/screenshot.sh"
|
||||||
|
|
||||||
# Verify screenshot dimensions (should be 5760x1080 for 3 displays)
|
|
||||||
SCREENSHOT="$PROJECT_ROOT/.nogit/screenshots/latest.png"
|
SCREENSHOT="$PROJECT_ROOT/.nogit/screenshots/latest.png"
|
||||||
if [ -f "$SCREENSHOT" ]; then
|
if [ -f "$SCREENSHOT" ]; then
|
||||||
WIDTH=$(identify -format "%w" "$SCREENSHOT" 2>/dev/null || echo "0")
|
WIDTH=$(identify -format "%w" "$SCREENSHOT" 2>/dev/null || echo "0")
|
||||||
@@ -260,6 +386,7 @@ if [ "$AUTO_MODE" = true ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Interactive mode - wait for QEMU to exit
|
echo "=== Press Ctrl-C to stop ==="
|
||||||
|
echo ""
|
||||||
wait $QEMU_PID 2>/dev/null || true
|
wait $QEMU_PID 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,19 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "@ecobridge/eco-os",
|
"name": "@ecobridge/eco-os",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"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": "pnpm run build:amd64",
|
||||||
|
"build:prepare": "[ -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",
|
||||||
|
"build:amd64": "pnpm run build:prepare && mkdir -p .nogit/iso && ./isobuild/scripts/docker-build.sh --arch=amd64 && cp isobuild/output/ecoos.iso .nogit/iso/",
|
||||||
|
"build:arm64": "pnpm run build:prepare && mkdir -p .nogit/iso && ./isobuild/scripts/docker-build.sh --arch=arm64 && cp isobuild/output/ecoos-arm64.iso .nogit/iso/",
|
||||||
|
"build:rpi": "pnpm run build:prepare && mkdir -p .nogit/iso && ./isobuild/scripts/docker-build.sh --arch=rpi && cp isobuild/output/ecoos-rpi.img .nogit/iso/",
|
||||||
"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:arm64": "pnpm run test:clean && cd isotest && ./run-test.sh --arch=arm64",
|
||||||
|
"test:rpi": "pnpm run test:clean && cd isotest && ./run-test.sh --arch=rpi",
|
||||||
"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",
|
||||||
"test:stop": "cd isotest && ./stop.sh",
|
"test:stop": "cd isotest && ./stop.sh",
|
||||||
"test:clean": "pnpm run test:stop && rm -rf .nogit/vm/*.qcow2 .nogit/screenshots/*",
|
"test:clean": "pnpm run test:stop && rm -rf .nogit/vm/*.qcow2 .nogit/screenshots/*",
|
||||||
"clean": "rm -rf .nogit/iso/*.iso && pnpm run test:clean"
|
"clean": "rm -rf .nogit/iso/*.iso .nogit/iso/*.img && pnpm run test:clean"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user