Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31cc3af1b4 | |||
| 6a3be55cee |
@@ -32,16 +32,8 @@ jobs:
|
|||||||
npm version ${{ steps.version.outputs.version_number }} --no-git-tag-version --allow-same-version
|
npm version ${{ steps.version.outputs.version_number }} --no-git-tag-version --allow-same-version
|
||||||
echo "export const VERSION = \"${{ steps.version.outputs.version_number }}\";" > ecoos_daemon/ts/version.ts
|
echo "export const VERSION = \"${{ steps.version.outputs.version_number }}\";" > ecoos_daemon/ts/version.ts
|
||||||
|
|
||||||
- name: Build daemon binary
|
- name: Build ISO
|
||||||
run: pnpm run daemon:bundle
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Build ISO with Docker
|
|
||||||
run: |
|
|
||||||
cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/
|
|
||||||
mkdir -p .nogit/iso
|
|
||||||
docker build -t ecoos-builder -f isobuild/Dockerfile .
|
|
||||||
docker run --rm --privileged -v ${{ github.workspace }}/.nogit/iso:/output ecoos-builder
|
|
||||||
ls -la .nogit/iso/
|
|
||||||
|
|
||||||
- name: Prepare release assets
|
- name: Prepare release assets
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-01-09 - 0.3.0 - feat(daemon)
|
||||||
|
add automatic update mechanism (Updater), switch to system journal logs, and expose update controls in the UI
|
||||||
|
|
||||||
|
- Introduce Updater class: fetches releases from Gitea, computes auto-upgrade eligibility, downloads daemon binary, replaces binary and restarts service.
|
||||||
|
- Integrate updater into EcoDaemon: new methods getUpdateInfo, checkForUpdates, upgradeToVersion; run initial update check on startup and periodic auto-upgrade checks (hourly).
|
||||||
|
- Replace serial console reader with a journalctl-based system journal reader; rename serialLogs → systemLogs and update related logic and limits.
|
||||||
|
- UI/server: add API endpoints /api/updates, /api/updates/check and /api/upgrade; add an Updates panel to show current version, available releases, auto-upgrade status, and client-side actions to check and trigger upgrades; poll update info periodically.
|
||||||
|
- Version bump to 0.2.2 (package.json and ecoos_daemon/ts/version.ts).
|
||||||
|
- Build/workflow changes: release workflow now runs build step (Build ISO) and package.json build script adjusted for CI and updated Docker build/run handling.
|
||||||
|
|
||||||
## 2026-01-09 - 0.2.1 - fix(ci)
|
## 2026-01-09 - 0.2.1 - fix(ci)
|
||||||
use GitHub Actions workspace for docker volume and add listing of build output directory for debugging
|
use GitHub Actions workspace for docker volume and add listing of build output directory for debugging
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { ProcessManager } from './process-manager.ts';
|
import { ProcessManager } from './process-manager.ts';
|
||||||
import { SystemInfo } from './system-info.ts';
|
import { SystemInfo } from './system-info.ts';
|
||||||
|
import { Updater } from './updater.ts';
|
||||||
import { UIServer } from '../ui/server.ts';
|
import { UIServer } from '../ui/server.ts';
|
||||||
import { runCommand } from '../utils/command.ts';
|
import { runCommand } from '../utils/command.ts';
|
||||||
import { VERSION } from '../version.ts';
|
import { VERSION } from '../version.ts';
|
||||||
@@ -28,12 +29,14 @@ export class EcoDaemon {
|
|||||||
private config: DaemonConfig;
|
private config: DaemonConfig;
|
||||||
private processManager: ProcessManager;
|
private processManager: ProcessManager;
|
||||||
private systemInfo: SystemInfo;
|
private systemInfo: SystemInfo;
|
||||||
|
private updater: Updater;
|
||||||
private uiServer: UIServer;
|
private uiServer: UIServer;
|
||||||
private logs: string[] = [];
|
private logs: string[] = [];
|
||||||
private serialLogs: string[] = [];
|
private systemLogs: string[] = [];
|
||||||
private swayStatus: ServiceStatus = { state: 'stopped' };
|
private swayStatus: ServiceStatus = { state: 'stopped' };
|
||||||
private chromiumStatus: ServiceStatus = { state: 'stopped' };
|
private chromiumStatus: ServiceStatus = { state: 'stopped' };
|
||||||
private manualRestartUntil: number = 0; // Timestamp until which auto-restart is disabled
|
private manualRestartUntil: number = 0; // Timestamp until which auto-restart is disabled
|
||||||
|
private lastAutoUpgradeCheck: number = 0; // Timestamp of last auto-upgrade check
|
||||||
|
|
||||||
constructor(config?: Partial<DaemonConfig>) {
|
constructor(config?: Partial<DaemonConfig>) {
|
||||||
this.config = {
|
this.config = {
|
||||||
@@ -45,6 +48,7 @@ export class EcoDaemon {
|
|||||||
|
|
||||||
this.processManager = new ProcessManager(this.config.user);
|
this.processManager = new ProcessManager(this.config.user);
|
||||||
this.systemInfo = new SystemInfo();
|
this.systemInfo = new SystemInfo();
|
||||||
|
this.updater = new Updater((msg) => this.log(msg));
|
||||||
this.uiServer = new UIServer(this.config.uiPort, this);
|
this.uiServer = new UIServer(this.config.uiPort, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +68,8 @@ export class EcoDaemon {
|
|||||||
return [...this.logs];
|
return [...this.logs];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSerialLogs(): string[] {
|
getSystemLogs(): string[] {
|
||||||
return [...this.serialLogs];
|
return [...this.systemLogs];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus(): Promise<Record<string, unknown>> {
|
async getStatus(): Promise<Record<string, unknown>> {
|
||||||
@@ -78,7 +82,7 @@ export class EcoDaemon {
|
|||||||
chromiumStatus: this.chromiumStatus,
|
chromiumStatus: this.chromiumStatus,
|
||||||
systemInfo,
|
systemInfo,
|
||||||
logs: this.logs.slice(-50),
|
logs: this.logs.slice(-50),
|
||||||
serialLogs: this.serialLogs.slice(-50),
|
systemLogs: this.systemLogs.slice(-50),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +135,18 @@ export class EcoDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUpdateInfo(): Promise<unknown> {
|
||||||
|
return this.updater.getUpdateInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkForUpdates(): Promise<void> {
|
||||||
|
await this.updater.checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
async upgradeToVersion(version: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
return this.updater.upgradeToVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.log('EcoOS Daemon starting...');
|
this.log('EcoOS Daemon starting...');
|
||||||
|
|
||||||
@@ -139,8 +155,11 @@ export class EcoDaemon {
|
|||||||
await this.uiServer.start();
|
await this.uiServer.start();
|
||||||
this.log('Management UI started successfully');
|
this.log('Management UI started successfully');
|
||||||
|
|
||||||
// Start serial console reader in the background
|
// Start system journal reader in the background
|
||||||
this.startSerialReader();
|
this.startJournalReader();
|
||||||
|
|
||||||
|
// Check for updates on startup
|
||||||
|
this.updater.checkForUpdates().catch((e) => this.log(`Initial update check failed: ${e}`));
|
||||||
|
|
||||||
// Start the Sway/Chromium initialization in the background
|
// Start the Sway/Chromium initialization in the background
|
||||||
// This allows the UI server to remain responsive even if Sway fails
|
// This allows the UI server to remain responsive even if Sway fails
|
||||||
@@ -313,12 +332,19 @@ export class EcoDaemon {
|
|||||||
return parseInt(result.stdout.trim(), 10);
|
return parseInt(result.stdout.trim(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startSerialReader(): void {
|
private startJournalReader(): void {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const file = await Deno.open('/dev/ttyS0', { read: true });
|
const cmd = new Deno.Command('journalctl', {
|
||||||
this.log('Serial console reader started on /dev/ttyS0');
|
args: ['-f', '--no-pager', '-n', '100', '-o', 'short-iso'],
|
||||||
const reader = file.readable.getReader();
|
stdout: 'piped',
|
||||||
|
stderr: 'piped',
|
||||||
|
});
|
||||||
|
|
||||||
|
const process = cmd.spawn();
|
||||||
|
this.log('System journal reader started');
|
||||||
|
|
||||||
|
const reader = process.stdout.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -326,14 +352,14 @@ export class EcoDaemon {
|
|||||||
if (done) break;
|
if (done) break;
|
||||||
const text = decoder.decode(value);
|
const text = decoder.decode(value);
|
||||||
for (const line of text.split('\n').filter((l) => l.trim())) {
|
for (const line of text.split('\n').filter((l) => l.trim())) {
|
||||||
this.serialLogs.push(line);
|
this.systemLogs.push(line);
|
||||||
if (this.serialLogs.length > 1000) {
|
if (this.systemLogs.length > 1000) {
|
||||||
this.serialLogs = this.serialLogs.slice(-1000);
|
this.systemLogs = this.systemLogs.slice(-1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`Serial reader not available: ${error}`);
|
this.log(`Journal reader not available: ${error}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@@ -383,6 +409,16 @@ export class EcoDaemon {
|
|||||||
await this.tryStartSwayAndChromium();
|
await this.tryStartSwayAndChromium();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for auto-upgrades every hour
|
||||||
|
const now = Date.now();
|
||||||
|
const oneHour = 60 * 60 * 1000;
|
||||||
|
if (now - this.lastAutoUpgradeCheck > oneHour) {
|
||||||
|
this.lastAutoUpgradeCheck = now;
|
||||||
|
this.updater.checkAutoUpgrade().catch((e) =>
|
||||||
|
this.log(`Auto-upgrade check failed: ${e}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`Error in monitoring loop: ${error}`);
|
this.log(`Error in monitoring loop: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
270
ecoos_daemon/ts/daemon/updater.ts
Normal file
270
ecoos_daemon/ts/daemon/updater.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* Updater
|
||||||
|
*
|
||||||
|
* Handles checking for updates, downloading new versions, and performing upgrades
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { VERSION } from '../version.ts';
|
||||||
|
import { runCommand } from '../utils/command.ts';
|
||||||
|
|
||||||
|
export interface Release {
|
||||||
|
version: string;
|
||||||
|
tagName: string;
|
||||||
|
publishedAt: Date;
|
||||||
|
downloadUrl: string;
|
||||||
|
isCurrent: boolean;
|
||||||
|
isNewer: boolean;
|
||||||
|
ageHours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoUpgradeStatus {
|
||||||
|
enabled: boolean;
|
||||||
|
targetVersion: string | null;
|
||||||
|
scheduledIn: string | null;
|
||||||
|
waitingForStability: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateInfo {
|
||||||
|
currentVersion: string;
|
||||||
|
releases: Release[];
|
||||||
|
autoUpgrade: AutoUpgradeStatus;
|
||||||
|
lastCheck: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GiteaRelease {
|
||||||
|
id: number;
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
body: string;
|
||||||
|
published_at: string;
|
||||||
|
assets: GiteaAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GiteaAsset {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Updater {
|
||||||
|
private repoApiUrl = 'https://code.foss.global/api/v1/repos/ecobridge/eco-os/releases';
|
||||||
|
private binaryPath = '/opt/eco/bin/eco-daemon';
|
||||||
|
private releases: Release[] = [];
|
||||||
|
private lastCheck: Date | null = null;
|
||||||
|
private logFn: (msg: string) => void;
|
||||||
|
|
||||||
|
constructor(logFn: (msg: string) => void) {
|
||||||
|
this.logFn = logFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(message: string): void {
|
||||||
|
this.logFn(`[Updater] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare semantic versions
|
||||||
|
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
||||||
|
*/
|
||||||
|
private compareVersions(a: string, b: string): number {
|
||||||
|
const partsA = a.replace(/^v/, '').split('.').map(Number);
|
||||||
|
const partsB = b.replace(/^v/, '').split('.').map(Number);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||||
|
const numA = partsA[i] || 0;
|
||||||
|
const numB = partsB[i] || 0;
|
||||||
|
if (numA < numB) return -1;
|
||||||
|
if (numA > numB) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch available releases from Gitea
|
||||||
|
*/
|
||||||
|
async checkForUpdates(): Promise<Release[]> {
|
||||||
|
this.log('Checking for updates...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.repoApiUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const giteaReleases: GiteaRelease[] = await response.json();
|
||||||
|
const currentVersion = VERSION;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
this.releases = giteaReleases
|
||||||
|
.filter((r) => r.tag_name.startsWith('v'))
|
||||||
|
.map((r) => {
|
||||||
|
const version = r.tag_name.replace(/^v/, '');
|
||||||
|
const publishedAt = new Date(r.published_at);
|
||||||
|
const ageMs = now.getTime() - publishedAt.getTime();
|
||||||
|
const ageHours = ageMs / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// Find the daemon binary asset
|
||||||
|
const daemonAsset = r.assets.find((a) =>
|
||||||
|
a.name.includes('eco-daemon')
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
tagName: r.tag_name,
|
||||||
|
publishedAt,
|
||||||
|
downloadUrl: daemonAsset?.browser_download_url || '',
|
||||||
|
isCurrent: version === currentVersion,
|
||||||
|
isNewer: this.compareVersions(version, currentVersion) > 0,
|
||||||
|
ageHours: Math.round(ageHours * 10) / 10,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((r) => r.downloadUrl) // Only include releases with daemon binary
|
||||||
|
.sort((a, b) => this.compareVersions(b.version, a.version)); // Newest first
|
||||||
|
|
||||||
|
this.lastCheck = now;
|
||||||
|
this.log(`Found ${this.releases.length} releases, ${this.releases.filter((r) => r.isNewer).length} newer than current`);
|
||||||
|
|
||||||
|
return this.releases;
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`Failed to check for updates: ${error}`);
|
||||||
|
return this.releases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached releases (call checkForUpdates first)
|
||||||
|
*/
|
||||||
|
getReleases(): Release[] {
|
||||||
|
return this.releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if auto-upgrade should happen and to which version
|
||||||
|
*/
|
||||||
|
getAutoUpgradeStatus(): AutoUpgradeStatus {
|
||||||
|
const newerReleases = this.releases.filter((r) => r.isNewer);
|
||||||
|
|
||||||
|
if (newerReleases.length === 0) {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
targetVersion: null,
|
||||||
|
scheduledIn: null,
|
||||||
|
waitingForStability: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest newer release
|
||||||
|
const latest = newerReleases[0];
|
||||||
|
const hoursUntilUpgrade = 24 - latest.ageHours;
|
||||||
|
|
||||||
|
if (hoursUntilUpgrade <= 0) {
|
||||||
|
// Ready to upgrade now
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
targetVersion: latest.version,
|
||||||
|
scheduledIn: 'now',
|
||||||
|
waitingForStability: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still waiting for stability period
|
||||||
|
const hours = Math.floor(hoursUntilUpgrade);
|
||||||
|
const minutes = Math.round((hoursUntilUpgrade - hours) * 60);
|
||||||
|
const scheduledIn = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
targetVersion: latest.version,
|
||||||
|
scheduledIn,
|
||||||
|
waitingForStability: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full update info for API response
|
||||||
|
*/
|
||||||
|
getUpdateInfo(): UpdateInfo {
|
||||||
|
return {
|
||||||
|
currentVersion: VERSION,
|
||||||
|
releases: this.releases,
|
||||||
|
autoUpgrade: this.getAutoUpgradeStatus(),
|
||||||
|
lastCheck: this.lastCheck?.toISOString() || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and install a specific version
|
||||||
|
*/
|
||||||
|
async upgradeToVersion(version: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
const release = this.releases.find((r) => r.version === version);
|
||||||
|
|
||||||
|
if (!release) {
|
||||||
|
return { success: false, message: `Version ${version} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (release.isCurrent) {
|
||||||
|
return { success: false, message: `Already running version ${version}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(`Starting upgrade to version ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download new binary
|
||||||
|
const tempPath = '/tmp/eco-daemon-new';
|
||||||
|
this.log(`Downloading from ${release.downloadUrl}...`);
|
||||||
|
|
||||||
|
const response = await fetch(release.downloadUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Download failed: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.arrayBuffer();
|
||||||
|
await Deno.writeFile(tempPath, new Uint8Array(data));
|
||||||
|
|
||||||
|
// Verify download
|
||||||
|
const stat = await Deno.stat(tempPath);
|
||||||
|
if (stat.size < 1000000) {
|
||||||
|
// Daemon should be at least 1MB
|
||||||
|
throw new Error(`Downloaded file too small: ${stat.size} bytes`);
|
||||||
|
}
|
||||||
|
this.log(`Downloaded ${stat.size} bytes`);
|
||||||
|
|
||||||
|
// Make executable
|
||||||
|
await Deno.chmod(tempPath, 0o755);
|
||||||
|
|
||||||
|
// Replace binary
|
||||||
|
this.log('Replacing binary...');
|
||||||
|
await runCommand('mv', [tempPath, this.binaryPath]);
|
||||||
|
await Deno.chmod(this.binaryPath, 0o755);
|
||||||
|
|
||||||
|
// Restart daemon via systemd
|
||||||
|
this.log('Restarting daemon...');
|
||||||
|
// Use spawn to avoid waiting for the restart
|
||||||
|
const restartCmd = new Deno.Command('systemctl', {
|
||||||
|
args: ['restart', 'eco-daemon'],
|
||||||
|
stdout: 'null',
|
||||||
|
stderr: 'null',
|
||||||
|
});
|
||||||
|
restartCmd.spawn();
|
||||||
|
|
||||||
|
return { success: true, message: `Upgrading to v${version}...` };
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`Upgrade failed: ${error}`);
|
||||||
|
return { success: false, message: String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and perform auto-upgrade if conditions are met
|
||||||
|
*/
|
||||||
|
async checkAutoUpgrade(): Promise<void> {
|
||||||
|
await this.checkForUpdates();
|
||||||
|
|
||||||
|
const status = this.getAutoUpgradeStatus();
|
||||||
|
|
||||||
|
if (status.targetVersion && status.scheduledIn === 'now') {
|
||||||
|
this.log(`Auto-upgrading to version ${status.targetVersion}...`);
|
||||||
|
await this.upgradeToVersion(status.targetVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,6 +104,31 @@ export class UIServer {
|
|||||||
return new Response(JSON.stringify(result), { headers });
|
return new Response(JSON.stringify(result), { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === '/api/updates') {
|
||||||
|
const updates = await this.daemon.getUpdateInfo();
|
||||||
|
return new Response(JSON.stringify(updates), { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/api/updates/check' && req.method === 'POST') {
|
||||||
|
await this.daemon.checkForUpdates();
|
||||||
|
const updates = await this.daemon.getUpdateInfo();
|
||||||
|
return new Response(JSON.stringify(updates), { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/api/upgrade' && req.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const version = body.version;
|
||||||
|
if (!version) {
|
||||||
|
return new Response(JSON.stringify({ success: false, message: 'Version required' }), { headers });
|
||||||
|
}
|
||||||
|
const result = await this.daemon.upgradeToVersion(version);
|
||||||
|
return new Response(JSON.stringify(result), { headers });
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ success: false, message: String(error) }), { headers });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
||||||
status: 404,
|
status: 404,
|
||||||
headers,
|
headers,
|
||||||
@@ -347,6 +372,18 @@ export class UIServer {
|
|||||||
</button>
|
</button>
|
||||||
<div id="control-status" style="margin-top: 8px; font-size: 12px; color: var(--text-dim);"></div>
|
<div id="control-status" style="margin-top: 8px; font-size: 12px; color: var(--text-dim);"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Updates</h2>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Current Version</div>
|
||||||
|
<div class="stat-value" id="current-version">-</div>
|
||||||
|
</div>
|
||||||
|
<div id="updates-list" style="margin: 12px 0;"></div>
|
||||||
|
<div id="auto-upgrade-status" style="font-size: 12px; color: var(--text-dim);"></div>
|
||||||
|
<button class="btn btn-primary" onclick="checkForUpdates()" style="margin-top: 8px;">
|
||||||
|
Check for Updates
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Input Devices</h2>
|
<h2>Input Devices</h2>
|
||||||
<div id="input-devices-list"></div>
|
<div id="input-devices-list"></div>
|
||||||
@@ -362,7 +399,7 @@ export class UIServer {
|
|||||||
<div class="card" style="grid-column: 1 / -1;">
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" onclick="switchTab('daemon')">Daemon Logs</div>
|
<div class="tab active" onclick="switchTab('daemon')">Daemon Logs</div>
|
||||||
<div class="tab" onclick="switchTab('serial')">Serial Console</div>
|
<div class="tab" onclick="switchTab('serial')">System Logs</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="daemon-tab" class="tab-content active">
|
<div id="daemon-tab" class="tab-content active">
|
||||||
<div class="logs" id="logs"></div>
|
<div class="logs" id="logs"></div>
|
||||||
@@ -522,13 +559,13 @@ export class UIServer {
|
|||||||
logsEl.scrollTop = logsEl.scrollHeight;
|
logsEl.scrollTop = logsEl.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serial Logs
|
// System Logs
|
||||||
if (data.serialLogs) {
|
if (data.systemLogs) {
|
||||||
const serialEl = document.getElementById('serial-logs');
|
const serialEl = document.getElementById('serial-logs');
|
||||||
if (data.serialLogs.length === 0) {
|
if (data.systemLogs.length === 0) {
|
||||||
serialEl.innerHTML = '<div style="color: var(--text-dim);">No serial data available</div>';
|
serialEl.innerHTML = '<div style="color: var(--text-dim);">No system logs available</div>';
|
||||||
} else {
|
} else {
|
||||||
serialEl.innerHTML = data.serialLogs.map(l =>
|
serialEl.innerHTML = data.systemLogs.map(l =>
|
||||||
'<div class="log-entry">' + l + '</div>'
|
'<div class="log-entry">' + l + '</div>'
|
||||||
).join('');
|
).join('');
|
||||||
serialEl.scrollTop = serialEl.scrollHeight;
|
serialEl.scrollTop = serialEl.scrollHeight;
|
||||||
@@ -590,6 +627,77 @@ export class UIServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkForUpdates() {
|
||||||
|
fetch('/api/updates/check', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(updateUpdatesUI)
|
||||||
|
.catch(err => console.error('Failed to check updates:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function upgradeToVersion(version) {
|
||||||
|
if (!confirm('Upgrade to version ' + version + '? The daemon will restart.')) return;
|
||||||
|
|
||||||
|
fetch('/api/upgrade', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ version: version })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
document.getElementById('auto-upgrade-status').textContent = result.message;
|
||||||
|
} else {
|
||||||
|
alert('Upgrade failed: ' + result.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('Upgrade error: ' + err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUpdatesUI(data) {
|
||||||
|
document.getElementById('current-version').textContent = 'v' + data.currentVersion;
|
||||||
|
|
||||||
|
const list = document.getElementById('updates-list');
|
||||||
|
const newerReleases = data.releases.filter(r => r.isNewer);
|
||||||
|
|
||||||
|
if (newerReleases.length === 0) {
|
||||||
|
list.innerHTML = '<div style="color: var(--text-dim);">No updates available</div>';
|
||||||
|
} else {
|
||||||
|
list.innerHTML = newerReleases.map(r =>
|
||||||
|
'<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border);">' +
|
||||||
|
'<span>v' + r.version + ' <span style="color: var(--text-dim);">(' + formatAge(r.ageHours) + ')</span></span>' +
|
||||||
|
'<button class="btn btn-primary" style="padding: 4px 12px; margin: 0;" onclick="upgradeToVersion(\\'' + r.version + '\\')">Upgrade</button>' +
|
||||||
|
'</div>'
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoStatus = document.getElementById('auto-upgrade-status');
|
||||||
|
if (data.autoUpgrade.targetVersion) {
|
||||||
|
if (data.autoUpgrade.waitingForStability) {
|
||||||
|
autoStatus.textContent = 'Auto-upgrade to v' + data.autoUpgrade.targetVersion + ' in ' + data.autoUpgrade.scheduledIn + ' (stability period)';
|
||||||
|
} else {
|
||||||
|
autoStatus.textContent = 'Auto-upgrade to v' + data.autoUpgrade.targetVersion + ' pending...';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
autoStatus.textContent = data.lastCheck ? 'Last checked: ' + new Date(data.lastCheck).toLocaleTimeString() : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAge(hours) {
|
||||||
|
if (hours < 1) return Math.round(hours * 60) + 'm ago';
|
||||||
|
if (hours < 24) return Math.round(hours) + 'h ago';
|
||||||
|
return Math.round(hours / 24) + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updates info periodically
|
||||||
|
function fetchUpdates() {
|
||||||
|
fetch('/api/updates')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(updateUpdatesUI)
|
||||||
|
.catch(err => console.error('Failed to fetch updates:', err));
|
||||||
|
}
|
||||||
|
fetchUpdates();
|
||||||
|
setInterval(fetchUpdates, 60000); // Check every minute
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
fetch('/api/status')
|
fetch('/api/status')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const VERSION = "0.1.3";
|
export const VERSION = "0.2.3";
|
||||||
|
|||||||
Binary file not shown.
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@ecobridge/eco-os",
|
"name": "@ecobridge/eco-os",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm version patch --no-git-tag-version && 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 --rm --privileged -v $(pwd)/.nogit/iso:/output ecoos-builder",
|
"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",
|
||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user