feat(daemon): add serial console reader and UI tab for serial logs; add version propagation and CI/release workflows

This commit is contained in:
2026-01-09 14:34:51 +00:00
parent 5234411c9d
commit 6dd6ead1c9
8 changed files with 262 additions and 7 deletions

View File

@@ -8,6 +8,7 @@ import { ProcessManager } from './process-manager.ts';
import { SystemInfo } from './system-info.ts';
import { UIServer } from '../ui/server.ts';
import { runCommand } from '../utils/command.ts';
import { VERSION } from '../version.ts';
export interface DaemonConfig {
uiPort: number;
@@ -29,6 +30,7 @@ export class EcoDaemon {
private systemInfo: SystemInfo;
private uiServer: UIServer;
private logs: string[] = [];
private serialLogs: string[] = [];
private swayStatus: ServiceStatus = { state: 'stopped' };
private chromiumStatus: ServiceStatus = { state: 'stopped' };
private manualRestartUntil: number = 0; // Timestamp until which auto-restart is disabled
@@ -62,15 +64,21 @@ export class EcoDaemon {
return [...this.logs];
}
getSerialLogs(): string[] {
return [...this.serialLogs];
}
async getStatus(): Promise<Record<string, unknown>> {
const systemInfo = await this.systemInfo.getInfo();
return {
version: VERSION,
sway: this.swayStatus.state === 'running',
swayStatus: this.swayStatus,
chromium: this.chromiumStatus.state === 'running',
chromiumStatus: this.chromiumStatus,
systemInfo,
logs: this.logs.slice(-50),
serialLogs: this.serialLogs.slice(-50),
};
}
@@ -131,6 +139,9 @@ export class EcoDaemon {
await this.uiServer.start();
this.log('Management UI started successfully');
// Start serial console reader in the background
this.startSerialReader();
// Start the Sway/Chromium initialization in the background
// This allows the UI server to remain responsive even if Sway fails
this.startServicesInBackground();
@@ -302,6 +313,31 @@ export class EcoDaemon {
return parseInt(result.stdout.trim(), 10);
}
private startSerialReader(): void {
(async () => {
try {
const file = await Deno.open('/dev/ttyS0', { read: true });
this.log('Serial console reader started on /dev/ttyS0');
const reader = file.readable.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n').filter((l) => l.trim())) {
this.serialLogs.push(line);
if (this.serialLogs.length > 1000) {
this.serialLogs = this.serialLogs.slice(-1000);
}
}
}
} catch (error) {
this.log(`Serial reader not available: ${error}`);
}
})();
}
private async runForever(): Promise<void> {
// Monitor processes and restart if needed
while (true) {

View File

@@ -249,6 +249,27 @@ export class UIServer {
background: var(--success);
color: white;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
.tab {
padding: 8px 16px;
cursor: pointer;
color: var(--text-dim);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
font-size: 12px;
text-transform: uppercase;
}
.tab:hover { color: var(--text); }
.tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
@@ -339,8 +360,16 @@ export class UIServer {
<div id="microphones-list"></div>
</div>
<div class="card" style="grid-column: 1 / -1;">
<h2>Logs</h2>
<div class="logs" id="logs"></div>
<div class="tabs">
<div class="tab active" onclick="switchTab('daemon')">Daemon Logs</div>
<div class="tab" onclick="switchTab('serial')">Serial Console</div>
</div>
<div id="daemon-tab" class="tab-content active">
<div class="logs" id="logs"></div>
</div>
<div id="serial-tab" class="tab-content">
<div class="logs" id="serial-logs"></div>
</div>
</div>
</div>
</div>
@@ -362,7 +391,20 @@ export class UIServer {
return mins + 'm';
}
let initialVersion = null;
function updateStatus(data) {
// Check for version change and reload if needed
if (data.version) {
if (initialVersion === null) {
initialVersion = data.version;
} else if (data.version !== initialVersion) {
console.log('Server version changed from ' + initialVersion + ' to ' + data.version + ', reloading...');
location.reload();
return;
}
}
// Services
document.getElementById('sway-status').className =
'status-dot ' + (data.sway ? 'running' : 'stopped');
@@ -471,7 +513,7 @@ export class UIServer {
}
}
// Logs
// Daemon Logs
if (data.logs) {
const logsEl = document.getElementById('logs');
logsEl.innerHTML = data.logs.map(l =>
@@ -479,6 +521,31 @@ export class UIServer {
).join('');
logsEl.scrollTop = logsEl.scrollHeight;
}
// Serial Logs
if (data.serialLogs) {
const serialEl = document.getElementById('serial-logs');
if (data.serialLogs.length === 0) {
serialEl.innerHTML = '<div style="color: var(--text-dim);">No serial data available</div>';
} else {
serialEl.innerHTML = data.serialLogs.map(l =>
'<div class="log-entry">' + l + '</div>'
).join('');
serialEl.scrollTop = serialEl.scrollHeight;
}
}
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
if (tab === 'daemon') {
document.querySelector('.tab:first-child').classList.add('active');
document.getElementById('daemon-tab').classList.add('active');
} else {
document.querySelector('.tab:last-child').classList.add('active');
document.getElementById('serial-tab').classList.add('active');
}
}
function setControlStatus(msg, isError) {

View File

@@ -1 +1 @@
export const VERSION = "0.1.1";
export const VERSION = "0.1.3";