Added a workflow to let the agent automatically deploy frontend onto the esp32.

This commit is contained in:
2026-03-08 15:19:17 -04:00
parent 9388bf17af
commit 56acf92f75
10 changed files with 240 additions and 2 deletions

View File

@@ -9,6 +9,8 @@
"build:esp32": "vite build && node scripts/gzip.js",
"ota:package": "node scripts/package.js",
"ota:bundle": "npm run ota:package && node scripts/bundle.js",
"ota:deploy": "node scripts/deploy.js --frontend",
"ota:deploy-bundle": "node scripts/deploy.js --bundle",
"preview": "vite preview"
},
"devDependencies": {

View File

@@ -0,0 +1,128 @@
/**
* OTA Deployment Script
* Uploads www.bin or universal.bundle to the ESP32 device.
*/
import { readFileSync, readdirSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import http from 'http';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, '..');
const binDir = resolve(projectRoot, 'bin');
const bundleDir = resolve(projectRoot, 'bundles');
/**
* Simple .env parser to load VITE_API_BASE
*/
function loadEnv() {
const envPath = resolve(projectRoot, '.env');
if (existsSync(envPath)) {
const content = readFileSync(envPath, 'utf8');
content.split('\n').forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
process.env[key.trim()] = value;
}
});
}
}
loadEnv();
const targetIP = process.env.VITE_API_BASE ? process.env.VITE_API_BASE.replace('http://', '') : null;
if (!targetIP) {
console.error('Error: VITE_API_BASE not found in frontend/.env. Please set it to the target device IP.');
process.exit(1);
}
const args = process.argv.slice(2);
const isBundle = args.includes('--bundle');
const isFrontend = args.includes('--frontend');
if (!isBundle && !isFrontend) {
console.error('Error: Specify --frontend or --bundle.');
process.exit(1);
}
function getLatestFile(dir, pattern) {
if (!existsSync(dir)) return null;
const files = readdirSync(dir)
.filter(f => f.match(pattern))
.sort((a, b) => {
const getParts = (s) => {
const m = s.match(/v(\d+)\.(\d+)\.(\d+)/);
return m ? m.slice(1).map(Number) : [0, 0, 0];
};
const [aMajor, aMinor, aRev] = getParts(a);
const [bMajor, bMinor, bRev] = getParts(b);
return (bMajor - aMajor) || (bMinor - aMinor) || (bRev - aRev);
});
return files.length > 0 ? resolve(dir, files[0]) : null;
}
const filePath = isBundle
? getLatestFile(bundleDir, /^universal_v.*\.bundle$/)
: getLatestFile(binDir, /^www_v.*\.bin$/);
if (!filePath) {
console.error(`Error: No recent ${isBundle ? 'bundle' : 'frontend bin'} found in ${isBundle ? bundleDir : binDir}`);
process.exit(1);
}
const fileName = filePath.split(/[\\/]/).pop();
const versionMatch = fileName.match(/v(\d+\.\d+\.\d+)/);
const version = versionMatch ? versionMatch[1] : 'unknown';
console.log('-------------------------------------------');
console.log(`🚀 Deployment Started`);
console.log(`📍 Target device: http://${targetIP}`);
console.log(`📦 Packaging type: ${isBundle ? 'Universal Bundle' : 'Frontend Only'}`);
console.log(`📄 File: ${fileName}`);
console.log(`🔖 Version: ${version}`);
console.log('-------------------------------------------');
const fileBuf = readFileSync(filePath);
const urlPath = isBundle ? '/api/ota/bundle' : '/api/ota/frontend';
const url = `http://${targetIP}${urlPath}`;
console.log(`Uploading ${fileBuf.length} bytes using fetch API... (This mimics the browser UI upload)`);
async function deploy() {
try {
const response = await fetch(url, {
method: 'POST',
body: fileBuf,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': fileBuf.length.toString()
}
});
if (response.ok) {
const text = await response.text();
console.log('\n✅ Success: Update committed!');
console.log('Server response:', text);
console.log('The device will reboot in ~1 second.');
// Wait a few seconds to let the socket close gracefully before killing the process
setTimeout(() => {
console.log('Deployment script finished.');
process.exit(0);
}, 3000);
} else {
const text = await response.text();
console.error(`\n❌ Error (${response.status}):`, text);
process.exit(1);
}
} catch (e) {
console.error(`\n❌ Problem with request:`, e.message);
if (e.cause) console.error(e.cause);
process.exit(1);
}
}
deploy();

View File

@@ -158,6 +158,7 @@
<main class="main-content">
<!-- Mobile Top Bar -->
<header class="mobile-header">
<!-- svelte-ignore a11y_consider_explicit_label -->
<button class="hamburger" onclick={() => mobileMenuOpen = true}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"></line>
@@ -172,7 +173,7 @@
<div class="w-full max-w-6xl mx-auto space-y-8">
<!-- Header -->
<div class="text-center">
<h1 class="text-2xl font-bold text-accent">Calendink Provider 📅📅🚀🚀</h1>
<h1 class="text-2xl font-bold text-accent">Calendink Provider 📅📅🚀🚀⚡✨🌈</h1>
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
<!-- Status Badge -->

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 1,
"revision": 16
"revision": 23
}