diff --git a/controller/templates/index.html b/controller/templates/index.html
index b6a25bee..ce92b38a 100644
--- a/controller/templates/index.html
+++ b/controller/templates/index.html
@@ -1883,7 +1883,7 @@
// Poll main API to check if upgrade is complete
// Use absolute URL to main WROLPi API (HTTPS on port 443), not Controller
- const mainApiUrl = `https://${window.location.hostname}/api/status`;
+ const mainApiUrl = `https://${window.location.hostname}${window.location.port === '8087' ? ':8443' : ''}/api/status`;
const checkUpgradeComplete = async () => {
try {
const resp = await fetch(mainApiUrl, {
@@ -1900,7 +1900,7 @@
// Redirect to main app after 2 seconds
setTimeout(() => {
- window.location.href = `https://${window.location.hostname}/`;
+ window.location.href = `https://${window.location.hostname}${window.location.port === '8087' ? ':8443' : ''}/`;
}, 2000);
}
} catch (e) {
@@ -1923,16 +1923,28 @@
// Show fallback banner
document.getElementById('fallback-banner').classList.add('show');
- // Poll main API to check if it's back up
- // Use absolute URL to main WROLPi API (HTTPS on port 443), not Controller
- const mainApiUrl = `https://${window.location.hostname}/api/status`;
+ // Poll main API and React app to check if they're back up
const checkMainApp = async () => {
try {
- // Check main API status endpoint (not Controller)
- const resp = await fetch(mainApiUrl, {method: 'HEAD'});
- if (resp.ok) {
- // Main app is back, redirect to main app
- window.location.href = `https://${window.location.hostname}/`;
+ const httpsBase = `https://${window.location.hostname}${window.location.port === '8087' ? ':8443' : ''}`;
+ // Check both API and React app are up before redirecting.
+ // API check uses fetch; app check uses an Image probe to avoid CORS (http→https).
+ // When the app is down, Caddy redirects to the controller, so the image src won't
+ // resolve to a valid image and will error.
+ const appUp = new Promise((resolve) => {
+ const img = new Image();
+ img.onload = () => resolve(true);
+ img.onerror = () => resolve(false);
+ img.src = `${httpsBase}/favicon.ico?_=${Date.now()}`;
+ setTimeout(() => resolve(false), 5000);
+ });
+ const [apiResp, isAppUp] = await Promise.all([
+ fetch(`${httpsBase}/api/status`, {method: 'HEAD', signal: AbortSignal.timeout(5000)}),
+ appUp
+ ]);
+ // API must respond OK; app must serve its favicon (not redirect to controller).
+ if (apiResp.ok && isAppUp) {
+ window.location.href = `${httpsBase}/`;
}
} catch (e) {
// Still down, continue polling
diff --git a/docker/web/Caddyfile b/docker/web/Caddyfile
index dc27327c..aa31d732 100644
--- a/docker/web/Caddyfile
+++ b/docker/web/Caddyfile
@@ -25,6 +25,10 @@
:443 {
tls /etc/ssl/caddy/cert.crt /etc/ssl/caddy/cert.key
+ header Access-Control-Allow-Origin "*"
+ header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ header Access-Control-Allow-Headers "Content-Type, Accept"
+
handle /favicon.ico {
root * /etc/caddy
rewrite * /favicon.ico
@@ -84,13 +88,11 @@
# React app with fallback to Controller.
handle {
- reverse_proxy app:3000 {
- @bad status 502 503 504
- handle_response @bad {
- header Location /controller/?fallback=true
- respond "" 302
- }
- }
+ reverse_proxy app:3000
+ }
+
+ handle_errors 502 503 504 {
+ redir * "http://{host}:8087/?fallback=true" 302
}
}
diff --git a/etc/raspberrypios/Caddyfile b/etc/raspberrypios/Caddyfile
index a459bd76..c9353046 100644
--- a/etc/raspberrypios/Caddyfile
+++ b/etc/raspberrypios/Caddyfile
@@ -88,22 +88,11 @@
# React app with fallback to Controller.
handle {
- reverse_proxy 0.0.0.0:3000 {
- @bad status 502 503 504
- handle_response @bad {
- header Location /controller/?fallback=true
- respond "" 302
- }
- }
+ reverse_proxy 0.0.0.0:3000
}
- handle_errors {
- header Access-Control-Allow-Origin "*"
- header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
- header Access-Control-Allow-Headers "Content-Type, Accept"
- root * /var/www
- rewrite * /50x.html
- file_server
+ handle_errors 502 503 504 {
+ redir * "http://{host}:8087/?fallback=true" 302
}
}