feat(daemon): add serial console reader and UI tab for serial logs; add version propagation and CI/release workflows
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "0.1.1";
|
||||
export const VERSION = "0.1.3";
|
||||
|
||||
Reference in New Issue
Block a user