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

@@ -0,0 +1,49 @@
---
description: Build, package, and deploy frontend updates via OTA
---
This workflow automates the process of building the frontend, packaging it into a LittleFS binary, and uploading it to the ESP32 device.
> [!IMPORTANT]
> Ensure `VITE_API_BASE` in `frontend/.env` is set to the target device's IP address.
### Steps
1. **Build and Package Frontend**
Run the following command in the `frontend/` directory to build the production assets and create the versioned `.bin` file:
```bash
npm run build:esp32 && npm run ota:package
```
2. **Deploy Update**
Run the deployment script to upload the latest version to the device:
```bash
npm run ota:deploy
```
3. **Verify Update**
Wait for the device to reboot and verify the changes on the dashboard.
---
### Universal Bundle Option
If you also need to update the firmware, use the universal bundle workflow:
1. **Build Firmware**
From the project root:
```bash
idf.py build
```
2. **Create Bundle**
In the `frontend/` directory:
```bash
npm run ota:bundle
```
3. **Deploy Bundle**
In the `frontend/` directory:
```bash
npm run ota:deploy-bundle
```

View File

@@ -86,3 +86,19 @@ Once the backend supports it (Phase 2+), you can update the frontend without usi
For a safer and more convenient update experience, you can bundle both the Firmware and Frontend into a single file. For a safer and more convenient update experience, you can bundle both the Firmware and Frontend into a single file.
See the [Universal OTA Bundle Guide](build_bundle.md) for details. See the [Universal OTA Bundle Guide](build_bundle.md) for details.
## 7. AI Agent Workflow
If you are an AI agent, you should use the automated deployment scripts to speed up your work.
### Deployment Prerequisites
- Ensure `VITE_API_BASE` in `frontend/.env` is set to the target device IP.
- Ensure `MKLITTLEFS_PATH` is correctly set if you are on Windows.
### Standard OTA Workflow
Follow the [Frontend OTA Workflow](file:///w:/Classified/Calendink/Provider/.agents/workflows/frontend-ota.md) for automated building and deployment.
**Summary of commands (run in `frontend/`):**
1. `npm run build:esp32` - Build production assets.
2. `npm run ota:package` - Create LittleFS image.
3. `npm run ota:deploy` - Upload to device via HTTP.

View File

@@ -9,6 +9,8 @@
"build:esp32": "vite build && node scripts/gzip.js", "build:esp32": "vite build && node scripts/gzip.js",
"ota:package": "node scripts/package.js", "ota:package": "node scripts/package.js",
"ota:bundle": "npm run ota:package && node scripts/bundle.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" "preview": "vite preview"
}, },
"devDependencies": { "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"> <main class="main-content">
<!-- Mobile Top Bar --> <!-- Mobile Top Bar -->
<header class="mobile-header"> <header class="mobile-header">
<!-- svelte-ignore a11y_consider_explicit_label -->
<button class="hamburger" onclick={() => mobileMenuOpen = true}> <button class="hamburger" onclick={() => mobileMenuOpen = true}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> <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> <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"> <div class="w-full max-w-6xl mx-auto space-y-8">
<!-- Header --> <!-- Header -->
<div class="text-center"> <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> <p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
<!-- Status Badge --> <!-- 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, "major": 0,
"minor": 1, "minor": 1,
"revision": 16 "revision": 23
} }