fix: configure gitea webhook auto-deploy, fix NVS CRC, and improve flash UI
- Add deploy-host.sh for webhook-triggered docker redeploy - Update docker-compose.yml and nginx.conf for auto-pull setup - Fix vite.config.js and admin router for deployment environment - Fix NVS CRC seed to use 0xFFFFFFFF to match esp_rom_crc32_le - Add dual-panel flash UI: esptool log + live 115200 serial monitor - Auto-reset device via RTS after flash (no manual power cycle needed) - Clean up Header.jsx debug title text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
|
# Auto-deploy generated files
|
||||||
|
deploy.sh
|
||||||
|
deploy.log
|
||||||
|
.deploy-trigger
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.env
|
.env
|
||||||
firebase-service-account.json
|
firebase-service-account.json
|
||||||
@@ -25,4 +30,6 @@ dist/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
.MAIN-APP-REFERENCE/
|
.MAIN-APP-REFERENCE/
|
||||||
|
|
||||||
|
.project-vesper-plan.md
|
||||||
@@ -42,23 +42,12 @@ async def deploy(request: Request):
|
|||||||
|
|
||||||
logger.info("Auto-deploy triggered via Gitea webhook")
|
logger.info("Auto-deploy triggered via Gitea webhook")
|
||||||
|
|
||||||
project_path = settings.deploy_project_path
|
# Write a trigger file to the host-mounted project path.
|
||||||
cmd = f"cd {project_path} && git pull origin main && docker compose up -d --build"
|
# A host-side watcher service (bellsystems-deploy-watcher) polls for this
|
||||||
try:
|
# file and runs deploy-host.sh as the bellsystems user when it appears.
|
||||||
proc = await asyncio.create_subprocess_shell(
|
trigger_path = f"{settings.deploy_project_path}/.deploy-trigger"
|
||||||
cmd,
|
with open(trigger_path, "w") as f:
|
||||||
stdout=asyncio.subprocess.PIPE,
|
f.write("deploy\n")
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
|
||||||
)
|
|
||||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=300)
|
|
||||||
output = stdout.decode(errors="replace") if stdout else ""
|
|
||||||
|
|
||||||
if proc.returncode != 0:
|
logger.info("Auto-deploy trigger file written")
|
||||||
logger.error(f"Deploy failed (exit {proc.returncode}):\n{output}")
|
return {"ok": True, "message": "Deploy started"}
|
||||||
raise HTTPException(status_code=500, detail=f"Deploy script failed:\n{output[-500:]}")
|
|
||||||
|
|
||||||
logger.info(f"Deploy succeeded:\n{output[-300:]}")
|
|
||||||
return {"ok": True, "output": output[-1000:]}
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
raise HTTPException(status_code=504, detail="Deploy timed out after 300 seconds")
|
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ ENTRY_TYPE_STRING = 0x21
|
|||||||
|
|
||||||
|
|
||||||
def _crc32(data: bytes) -> int:
|
def _crc32(data: bytes) -> int:
|
||||||
return binascii.crc32(data) & 0xFFFFFFFF
|
# ESP-IDF uses 0xFFFFFFFF as the initial CRC seed (matches esp_rom_crc32_le)
|
||||||
|
return binascii.crc32(data, 0xFFFFFFFF) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
def _page_header_crc(seq: int, version: int) -> int:
|
def _page_header_crc(seq: int, version: int) -> int:
|
||||||
|
|||||||
11
deploy-host.sh
Executable file
11
deploy-host.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROJECT=/home/bellsystems/bellsystems-cp
|
||||||
|
|
||||||
|
echo "Deploy started at $(date)"
|
||||||
|
cd "$PROJECT"
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
docker compose up -d --build 2>&1
|
||||||
|
echo "Deploy finished at $(date)"
|
||||||
@@ -10,6 +10,8 @@ services:
|
|||||||
- ./data/built_melodies:/app/storage/built_melodies
|
- ./data/built_melodies:/app/storage/built_melodies
|
||||||
- ./data/firmware:/app/storage/firmware
|
- ./data/firmware:/app/storage/firmware
|
||||||
- ./data/firebase-service-account.json:/app/firebase-service-account.json:ro
|
- ./data/firebase-service-account.json:/app/firebase-service-account.json:ro
|
||||||
|
# Auto-deploy: project root so container can write the trigger file
|
||||||
|
- /home/bellsystems/bellsystems-cp:/home/bellsystems/bellsystems-cp
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
depends_on: []
|
depends_on: []
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default function Header() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>
|
<h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>
|
||||||
BellSystems - Control Panel
|
BellCloud™ - Console
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -41,3 +41,5 @@ export default function Header() {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* my test string */
|
||||||
@@ -360,13 +360,25 @@ function StepSelectDevice({ onSelected }) {
|
|||||||
function StepFlash({ device, onFlashed }) {
|
function StepFlash({ device, onFlashed }) {
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
const [flashing, setFlashing] = useState(false);
|
const [flashing, setFlashing] = useState(false);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
const [nvsProgress, setNvsProgress] = useState(0);
|
const [nvsProgress, setNvsProgress] = useState(0);
|
||||||
const [fwProgress, setFwProgress] = useState(0);
|
const [fwProgress, setFwProgress] = useState(0);
|
||||||
const [log, setLog] = useState([]);
|
const [log, setLog] = useState([]);
|
||||||
|
const [serial, setSerial] = useState([]);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const loaderRef = useRef(null);
|
const loaderRef = useRef(null);
|
||||||
|
const portRef = useRef(null);
|
||||||
|
const serialReaderRef = useRef(null);
|
||||||
|
const serialActiveRef = useRef(false);
|
||||||
|
const logEndRef = useRef(null);
|
||||||
|
const serialEndRef = useRef(null);
|
||||||
|
|
||||||
const appendLog = (msg) => setLog((prev) => [...prev, msg]);
|
const appendLog = (msg) => setLog((prev) => [...prev, String(msg)]);
|
||||||
|
const appendSerial = (msg) => setSerial((prev) => [...prev, String(msg)]);
|
||||||
|
|
||||||
|
// Auto-scroll both panels to bottom
|
||||||
|
const scrollLog = () => logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
const scrollSerial = () => serialEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
|
||||||
const fetchBinary = async (url) => {
|
const fetchBinary = async (url) => {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
@@ -382,17 +394,56 @@ function StepFlash({ device, onFlashed }) {
|
|||||||
const arrayBufferToString = (buf) => {
|
const arrayBufferToString = (buf) => {
|
||||||
const bytes = new Uint8Array(buf);
|
const bytes = new Uint8Array(buf);
|
||||||
let str = "";
|
let str = "";
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
|
||||||
str += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return str;
|
return str;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Start reading raw UART output from the device after flash+reset
|
||||||
|
const startSerialMonitor = async (port) => {
|
||||||
|
serialActiveRef.current = true;
|
||||||
|
try {
|
||||||
|
await port.open({ baudRate: 115200 });
|
||||||
|
} catch (_) {
|
||||||
|
// Port may already be open if esptool left it that way — ignore
|
||||||
|
}
|
||||||
|
const decoder = new TextDecoderStream();
|
||||||
|
const readableStreamClosed = port.readable.pipeTo(decoder.writable);
|
||||||
|
const reader = decoder.readable.getReader();
|
||||||
|
serialReaderRef.current = reader;
|
||||||
|
|
||||||
|
let lineBuffer = "";
|
||||||
|
try {
|
||||||
|
while (serialActiveRef.current) {
|
||||||
|
const { value, done: streamDone } = await reader.read();
|
||||||
|
if (streamDone) break;
|
||||||
|
lineBuffer += value;
|
||||||
|
const lines = lineBuffer.split(/\r?\n/);
|
||||||
|
lineBuffer = lines.pop(); // keep incomplete last fragment
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
appendSerial(line);
|
||||||
|
scrollSerial();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Reader cancelled on cleanup — expected
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSerialMonitor = async () => {
|
||||||
|
serialActiveRef.current = false;
|
||||||
|
try { await serialReaderRef.current?.cancel(); } catch (_) {}
|
||||||
|
try { portRef.current?.close(); } catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFlash = async () => {
|
const handleFlash = async () => {
|
||||||
setError("");
|
setError("");
|
||||||
setLog([]);
|
setLog([]);
|
||||||
|
setSerial([]);
|
||||||
setNvsProgress(0);
|
setNvsProgress(0);
|
||||||
setFwProgress(0);
|
setFwProgress(0);
|
||||||
|
setDone(false);
|
||||||
|
|
||||||
// 1. Open Web Serial port
|
// 1. Open Web Serial port
|
||||||
let port;
|
let port;
|
||||||
@@ -400,6 +451,7 @@ function StepFlash({ device, onFlashed }) {
|
|||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
appendLog("Opening port picker…");
|
appendLog("Opening port picker…");
|
||||||
port = await navigator.serial.requestPort();
|
port = await navigator.serial.requestPort();
|
||||||
|
portRef.current = port;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || "Port selection cancelled.");
|
setError(err.message || "Port selection cancelled.");
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
@@ -427,23 +479,18 @@ function StepFlash({ device, onFlashed }) {
|
|||||||
baudrate: FLASH_BAUD,
|
baudrate: FLASH_BAUD,
|
||||||
terminal: {
|
terminal: {
|
||||||
clean() {},
|
clean() {},
|
||||||
writeLine: (line) => appendLog(line),
|
writeLine: (line) => { appendLog(line); scrollLog(); },
|
||||||
write: (msg) => appendLog(msg),
|
write: (msg) => { appendLog(msg); scrollLog(); },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await loaderRef.current.main();
|
await loaderRef.current.main();
|
||||||
appendLog("ESP32 connected.");
|
appendLog("ESP32 connected.");
|
||||||
|
|
||||||
// 4. Flash NVS + firmware with progress callbacks
|
// 4. Flash NVS + firmware
|
||||||
const nvsData = arrayBufferToString(nvsBuffer);
|
const nvsData = arrayBufferToString(nvsBuffer);
|
||||||
const fwData = arrayBufferToString(fwBuffer);
|
const fwData = arrayBufferToString(fwBuffer);
|
||||||
|
|
||||||
// Track progress by watching the two images in sequence.
|
|
||||||
// esptool-js reports progress as { index, fileIndex, written, total }
|
|
||||||
const totalBytes = nvsBuffer.byteLength + fwBuffer.byteLength;
|
|
||||||
let writtenSoFar = 0;
|
|
||||||
|
|
||||||
await loaderRef.current.writeFlash({
|
await loaderRef.current.writeFlash({
|
||||||
fileArray: [
|
fileArray: [
|
||||||
{ data: nvsData, address: NVS_ADDRESS },
|
{ data: nvsData, address: NVS_ADDRESS },
|
||||||
@@ -461,41 +508,46 @@ function StepFlash({ device, onFlashed }) {
|
|||||||
setNvsProgress(100);
|
setNvsProgress(100);
|
||||||
setFwProgress((written / total) * 100);
|
setFwProgress((written / total) * 100);
|
||||||
}
|
}
|
||||||
writtenSoFar = written;
|
|
||||||
},
|
|
||||||
calculateMD5Hash: (image) => {
|
|
||||||
// MD5 is optional for progress verification; returning empty disables it
|
|
||||||
return "";
|
|
||||||
},
|
},
|
||||||
|
calculateMD5Hash: () => "",
|
||||||
});
|
});
|
||||||
|
|
||||||
setNvsProgress(100);
|
setNvsProgress(100);
|
||||||
setFwProgress(100);
|
setFwProgress(100);
|
||||||
appendLog("Flash complete. Disconnecting…");
|
appendLog("Flash complete. Resetting device…");
|
||||||
await transport.disconnect();
|
|
||||||
appendLog("Done.");
|
|
||||||
|
|
||||||
// 5. Update device status → flashed
|
// 5. Hard-reset via RTS — device boots into user code automatically
|
||||||
|
await loaderRef.current.after("hard_reset");
|
||||||
|
appendLog("Hard reset sent. Device is booting…");
|
||||||
|
|
||||||
|
// 6. Update device status → flashed
|
||||||
await api.request(`/manufacturing/devices/${device.serial_number}/status`, {
|
await api.request(`/manufacturing/devices/${device.serial_number}/status`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ status: "flashed", note: "Flashed via browser provisioning wizard" }),
|
body: JSON.stringify({ status: "flashed", note: "Flashed via browser provisioning wizard" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setFlashing(false);
|
||||||
|
setDone(true);
|
||||||
|
|
||||||
|
// 7. Open serial monitor at 115200 to show live device output
|
||||||
|
appendSerial("── Serial monitor started (115200 baud) ──");
|
||||||
|
startSerialMonitor(port);
|
||||||
|
|
||||||
onFlashed();
|
onFlashed();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || String(err));
|
setError(err.message || String(err));
|
||||||
setFlashing(false);
|
setFlashing(false);
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
try {
|
try { await loaderRef.current?.transport?.disconnect(); } catch (_) {}
|
||||||
if (loaderRef.current) await loaderRef.current.transport?.disconnect();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const webSerialAvailable = "serial" in navigator;
|
const webSerialAvailable = "serial" in navigator;
|
||||||
|
const busy = connecting || flashing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Device info header */}
|
||||||
<div
|
<div
|
||||||
className="rounded-lg border p-5"
|
className="rounded-lg border p-5"
|
||||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||||
@@ -535,42 +587,101 @@ function StepFlash({ device, onFlashed }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{log.length > 0 && (
|
{!busy && (
|
||||||
<div
|
|
||||||
className="rounded-md border p-3 mb-4 font-mono text-xs overflow-y-auto max-h-36 space-y-0.5"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "var(--bg-primary)",
|
|
||||||
borderColor: "var(--border-secondary)",
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{log.map((line, i) => (
|
|
||||||
<div key={i}>{line}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!flashing && (
|
|
||||||
<button
|
<button
|
||||||
onClick={handleFlash}
|
onClick={handleFlash}
|
||||||
disabled={!webSerialAvailable || connecting}
|
disabled={!webSerialAvailable}
|
||||||
className="flex items-center gap-2 px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
|
className="flex items-center gap-2 px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
|
||||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
</svg>
|
</svg>
|
||||||
{connecting ? "Connecting…" : "Connect & Flash Device"}
|
{done ? "Flash Again" : "Connect & Flash Device"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{flashing && nvsProgress < 100 && fwProgress < 100 && (
|
{flashing && (
|
||||||
<p className="text-sm" style={{ color: "var(--text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--text-muted)" }}>
|
||||||
Flashing in progress — do not disconnect…
|
Flashing in progress — do not disconnect…
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Two-column output: Flash log + Serial output */}
|
||||||
|
{(log.length > 0 || serial.length > 0) && (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{/* Left: flash / esptool output */}
|
||||||
|
<div className="rounded-lg border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
|
||||||
|
<div
|
||||||
|
className="px-3 py-2 text-xs font-semibold uppercase tracking-wide border-b"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-card)",
|
||||||
|
borderColor: "var(--border-primary)",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Flash Output
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="p-3 font-mono text-xs overflow-y-auto space-y-0.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-primary)",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
height: "240px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{log.map((line, i) => (
|
||||||
|
<div key={i}>{line}</div>
|
||||||
|
))}
|
||||||
|
<div ref={logEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: live device serial output */}
|
||||||
|
<div className="rounded-lg border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
|
||||||
|
<div
|
||||||
|
className="px-3 py-2 text-xs font-semibold uppercase tracking-wide border-b flex items-center justify-between"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-card)",
|
||||||
|
borderColor: "var(--border-primary)",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Serial Output</span>
|
||||||
|
{serialActiveRef.current && (
|
||||||
|
<span className="flex items-center gap-1.5" style={{ color: "#4dd6c8" }}>
|
||||||
|
<span
|
||||||
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
|
style={{ backgroundColor: "#4dd6c8", animation: "pulse 1.5s infinite" }}
|
||||||
|
/>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="p-3 font-mono text-xs overflow-y-auto space-y-0.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-primary)",
|
||||||
|
color: "#a3e635",
|
||||||
|
height: "240px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{serial.length === 0 ? (
|
||||||
|
<span style={{ color: "var(--text-muted)" }}>
|
||||||
|
{done ? "Waiting for device boot…" : "Available after flash completes."}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
serial.map((line, i) => (
|
||||||
|
<div key={i}>{line}</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={serialEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
|
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
|
||||||
Flash addresses: NVS at 0x9000 · Firmware at 0x10000 · Baud: {FLASH_BAUD}
|
Flash addresses: NVS at 0x9000 · Firmware at 0x10000 · Baud: {FLASH_BAUD}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
allowedHosts: ['console.bellsystems.net'],
|
||||||
hmr: {
|
hmr: {
|
||||||
clientPort: 80,
|
clientPort: 80,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
upstream backend {
|
client_max_body_size 10m;
|
||||||
server backend:8000;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream frontend {
|
|
||||||
server frontend:5173;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
|
# Use Docker's internal DNS so nginx re-resolves after container restarts
|
||||||
|
resolver 127.0.0.11 valid=5s;
|
||||||
|
set $backend_upstream http://backend:8000;
|
||||||
|
set $frontend_upstream http://frontend:5173;
|
||||||
|
|
||||||
# OTA firmware files — allow browser (esptool-js) to fetch .bin files directly
|
# OTA firmware files — allow browser (esptool-js) to fetch .bin files directly
|
||||||
location /ota/ {
|
location /ota/ {
|
||||||
root /srv;
|
root /srv;
|
||||||
@@ -29,7 +28,7 @@ http {
|
|||||||
|
|
||||||
# API requests → FastAPI backend
|
# API requests → FastAPI backend
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend;
|
proxy_pass $backend_upstream$request_uri;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@@ -38,7 +37,7 @@ http {
|
|||||||
|
|
||||||
# WebSocket support for MQTT live data
|
# WebSocket support for MQTT live data
|
||||||
location /api/mqtt/ws {
|
location /api/mqtt/ws {
|
||||||
proxy_pass http://backend;
|
proxy_pass $backend_upstream$request_uri;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -47,7 +46,7 @@ http {
|
|||||||
|
|
||||||
# Everything else → React frontend (Vite dev server)
|
# Everything else → React frontend (Vite dev server)
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://frontend;
|
proxy_pass $frontend_upstream$request_uri;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
Reference in New Issue
Block a user