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 } }