diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a35d348 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..269def5 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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" diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..af6b86c --- /dev/null +++ b/changelog.md @@ -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. \ No newline at end of file diff --git a/ecoos_daemon/ts/daemon/index.ts b/ecoos_daemon/ts/daemon/index.ts index e2b051d..c648f5c 100644 --- a/ecoos_daemon/ts/daemon/index.ts +++ b/ecoos_daemon/ts/daemon/index.ts @@ -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> { 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 { // Monitor processes and restart if needed while (true) { diff --git a/ecoos_daemon/ts/ui/server.ts b/ecoos_daemon/ts/ui/server.ts index 468c206..64a4cf5 100644 --- a/ecoos_daemon/ts/ui/server.ts +++ b/ecoos_daemon/ts/ui/server.ts @@ -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; } @@ -339,8 +360,16 @@ export class UIServer {
-

Logs

-
+
+
Daemon Logs
+
Serial Console
+
+
+
+
+
+
+
@@ -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 = '
No serial data available
'; + } else { + serialEl.innerHTML = data.serialLogs.map(l => + '
' + l + '
' + ).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) { diff --git a/ecoos_daemon/ts/version.ts b/ecoos_daemon/ts/version.ts index 6fa18df..782835e 100644 --- a/ecoos_daemon/ts/version.ts +++ b/ecoos_daemon/ts/version.ts @@ -1 +1 @@ -export const VERSION = "0.1.1"; +export const VERSION = "0.1.3"; diff --git a/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon b/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon index ceb084a..ba03775 100755 Binary files a/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon and b/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon differ diff --git a/package.json b/package.json index a9db80d..933ea40 100644 --- a/package.json +++ b/package.json @@ -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" } }