feat(daemon): add serial console reader and UI tab for serial logs; add version propagation and CI/release workflows
This commit is contained in:
26
.gitea/workflows/ci.yml
Normal file
26
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run daemon:typecheck
|
||||
|
||||
- name: Bundle
|
||||
run: pnpm run daemon:bundle
|
||||
108
.gitea/workflows/release.yml
Normal file
108
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set version in package.json and version.ts
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Build daemon binary
|
||||
run: pnpm run daemon:bundle
|
||||
|
||||
- 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 $(pwd)/.nogit/iso:/output ecoos-builder
|
||||
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
mkdir -p dist
|
||||
cp .nogit/iso/ecoos.iso "dist/ecoos-${VERSION}.iso"
|
||||
cp ecoos_daemon/bundle/eco-daemon "dist/eco-daemon-${VERSION}"
|
||||
cd dist && sha256sum * > SHA256SUMS.txt
|
||||
ls -lh
|
||||
|
||||
- name: Delete existing release if exists
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
EXISTING=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/${{ gitea.repository }}/releases/tags/$VERSION" | jq -r '.id // empty')
|
||||
if [ -n "$EXISTING" ]; then
|
||||
echo "Deleting existing release $EXISTING"
|
||||
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/${{ gitea.repository }}/releases/$EXISTING"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
- name: Create Release and upload assets
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
# Create release
|
||||
RELEASE_ID=$(curl -X POST -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://code.foss.global/api/v1/repos/${{ gitea.repository }}/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"$VERSION\",
|
||||
\"name\": \"EcoOS $VERSION\",
|
||||
\"body\": \"## EcoOS $VERSION\n\n### Assets\n- **ecoos-${VERSION}.iso** - Full bootable ISO image\n- **eco-daemon-${VERSION}** - Daemon binary for in-place upgrades\n\n### Checksums\nSHA256 checksums in SHA256SUMS.txt\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" | jq -r '.id')
|
||||
|
||||
echo "Created release with ID: $RELEASE_ID"
|
||||
|
||||
# Upload assets
|
||||
for asset in dist/*; do
|
||||
filename=$(basename "$asset")
|
||||
echo "Uploading $filename..."
|
||||
curl -X POST -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$asset" \
|
||||
"https://code.foss.global/api/v1/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets?name=$filename"
|
||||
done
|
||||
|
||||
- name: Cleanup old releases (keep 3 latest)
|
||||
run: |
|
||||
echo "Checking for old releases to clean up..."
|
||||
RELEASES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/${{ gitea.repository }}/releases" | \
|
||||
jq -r 'sort_by(.created_at) | reverse | .[3:] | .[].id')
|
||||
|
||||
for id in $RELEASES; do
|
||||
echo "Deleting old release $id"
|
||||
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/${{ gitea.repository }}/releases/$id"
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
16
changelog.md
Normal file
16
changelog.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-09 - 0.2.0 - feat(daemon)
|
||||
add serial console reader and UI tab for serial logs; add version propagation and CI/release workflows
|
||||
|
||||
- Start a background serial reader that reads /dev/ttyS0, retains up to 1000 lines and exposes serial logs via the daemon API
|
||||
- Add a Serial Console tab in the management UI to view serial logs and a tab switcher; UI will auto-reload when daemon version changes
|
||||
- Expose VERSION from ecoos_daemon and include it in status responses
|
||||
- Bump package version to 0.1.3 and update daemon version constant
|
||||
- Add .gitea workflows for CI (typecheck + bundle) and Release (build daemon, build ISO via Docker, upload releases to Gitea), and add a daemon:typecheck npm script; update test/clean scripts
|
||||
|
||||
## 2026-01-09 - 0.1.1 - initial project setup & minor update
|
||||
Consolidated initial project creation and a follow-up update into the initial release (0.1.1).
|
||||
|
||||
- 2026-01-08: initial commit — project scaffold and first files added.
|
||||
- 2026-01-09: minor update and version bump to 0.1.1 — small edits and housekeeping.
|
||||
@@ -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";
|
||||
|
||||
Binary file not shown.
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"name": "@ecobridge/eco-os",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"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",
|
||||
"daemon:dev": "cd ecoos_daemon && deno run --allow-all --watch mod.ts",
|
||||
"daemon:start": "cd ecoos_daemon && deno run --allow-all 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",
|
||||
"test": "cd isotest && ./run-test.sh",
|
||||
"test": "pnpm run test:clean && cd isotest && ./run-test.sh",
|
||||
"test:screenshot": "cd isotest && ./screenshot.sh",
|
||||
"test:screenshot:loop": "while true; do pnpm run test:screenshot; sleep 5; done",
|
||||
"test:stop": "cd isotest && ./stop.sh",
|
||||
"clean": "rm -rf .nogit/iso/*.iso .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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user