fix: HTTPS/WebSocket mixed-content — add PHP SSE relay for scale gateway

The browser (HTTPS) cannot connect to ws:// directly (mixed-content block).
Solution: PHP SSE relay bridges the gap server-side.

- api/scale_relay.php: GET ?url=ws://ip:port
  PHP opens WS connection to Android gateway, streams JSON frames as
  Server-Sent Events to the browser over existing HTTPS connection.
  Includes WS handshake, masked client frames, frame decoder, keep-alive.

- api/scale_ping.php: GET ?url=ws://ip:port
  One-shot connectivity test, returns {"ok":true} or {"ok":false,"error"}.

- app.js: WebSocket -> EventSource (SSE)
  _scaleWs -> _scaleEs, connects to /api/scale_relay.php
  testScaleConnection() -> fetch /api/scale_ping.php (no more direct ws://)
  readScaleWeight(): removed get_weight send (scale streams continuously)
This commit is contained in:
dadaloop82
2026-04-15 20:31:54 +00:00
parent a146ba124a
commit 099a6cc4e8
3 changed files with 327 additions and 40 deletions
+31 -40
View File
@@ -61,7 +61,7 @@ const API_BASE = 'api/index.php';
// ===== SMART SCALE GATEWAY =====
// Connects to the Android BLE-WebSocket gateway and provides auto weight reading.
let _scaleWs = null;
let _scaleEs = null; // EventSource for the SSE relay
let _scaleConnected = false;
let _scaleDevice = null;
let _scaleBattery = null;
@@ -74,7 +74,7 @@ function scaleInit() {
const indicator = document.getElementById('scale-status-indicator');
if (!s.scale_enabled || !s.scale_gateway_url) {
if (indicator) indicator.style.display = 'none';
if (_scaleWs) { try { _scaleWs.close(); } catch(e) {} _scaleWs = null; }
if (_scaleEs) { try { _scaleEs.close(); } catch(e) {} _scaleEs = null; }
return;
}
if (indicator) indicator.style.display = '';
@@ -82,28 +82,21 @@ function scaleInit() {
}
function _scaleConnect(url) {
if (_scaleWs) { try { _scaleWs.close(); } catch(e) {} _scaleWs = null; }
if (_scaleEs) { try { _scaleEs.close(); } catch(e) {} _scaleEs = null; }
if (_scaleReconnectTimer) { clearTimeout(_scaleReconnectTimer); _scaleReconnectTimer = null; }
try {
_scaleWs = new WebSocket(url);
_scaleWs.onopen = () => {
_scaleUpdateStatus('searching');
try { _scaleWs.send(JSON.stringify({ type: 'get_status' })); } catch(e) {}
};
_scaleWs.onmessage = (evt) => {
// Connect via the PHP SSE relay so the HTTPS page is not blocked by mixed-content
_scaleEs = new EventSource('/api/scale_relay.php?url=' + encodeURIComponent(url));
_scaleEs.onopen = () => _scaleUpdateStatus('searching');
_scaleEs.onmessage = (evt) => {
try { _scaleOnMessage(JSON.parse(evt.data)); } catch(e) {}
};
_scaleWs.onclose = () => {
_scaleEs.onerror = () => {
_scaleConnected = false;
_scaleDevice = null;
_scaleUpdateStatus('disconnected');
_scaleReconnectTimer = setTimeout(() => {
_scaleReconnectTimer = null;
const s = getSettings();
if (s.scale_enabled && s.scale_gateway_url) _scaleConnect(s.scale_gateway_url);
}, 8000);
// EventSource auto-reconnects; no manual timer needed
};
_scaleWs.onerror = () => _scaleUpdateStatus('error');
} catch(e) {
_scaleUpdateStatus('error');
}
@@ -148,7 +141,7 @@ function _scaleUpdateStatus(state) {
* @param {Function} getUnit function that returns the current unit string ('g', 'ml', 'kg')
*/
function readScaleWeight(targetInputId, getUnit) {
if (!_scaleWs || _scaleWs.readyState !== WebSocket.OPEN) {
if (!_scaleConnected) {
showToast('⚖️ ' + t('scale.not_connected'), 'error');
return;
}
@@ -170,7 +163,7 @@ function readScaleWeight(targetInputId, getUnit) {
closeModal();
showToast(`⚖️ ${val} ${unit}`, 'success');
};
try { _scaleWs.send(JSON.stringify({ type: 'get_weight' })); } catch(e) {}
// Weight data streams continuously via SSE; _scaleWeightCallback fires on the next stable reading
}
function _scaleShowReadingModal(targetInputId, unit) {
@@ -227,33 +220,31 @@ function testScaleConnection() {
statusEl.className = 'settings-status';
statusEl.style.display = 'block';
let testWs;
const ac = new AbortController();
const timeout = setTimeout(() => {
if (testWs) testWs.close();
ac.abort();
statusEl.textContent = '❌ ' + t('scale.timeout');
statusEl.className = 'settings-status error';
}, 6000);
try {
testWs = new WebSocket(url);
testWs.onopen = () => {
try { testWs.send(JSON.stringify({ type: 'ping' })); } catch(e) {}
};
testWs.onmessage = () => {
}, 8000);
fetch('/api/scale_ping.php?url=' + encodeURIComponent(url), { signal: ac.signal })
.then(r => r.json())
.then(data => {
clearTimeout(timeout);
testWs.close();
statusEl.textContent = '✅ ' + t('scale.connected_ok');
statusEl.className = 'settings-status success';
};
testWs.onerror = () => {
if (data.ok) {
statusEl.textContent = '✅ ' + t('scale.connected_ok');
statusEl.className = 'settings-status success';
} else {
statusEl.textContent = '❌ ' + (data.error || t('scale.error_connect'));
statusEl.className = 'settings-status error';
}
})
.catch(e => {
clearTimeout(timeout);
statusEl.textContent = '❌ ' + t('scale.error_connect');
statusEl.className = 'settings-status error';
};
} catch(e) {
clearTimeout(timeout);
statusEl.textContent = '❌ ' + (e.message || t('scale.error_connect'));
statusEl.className = 'settings-status error';
}
if (e.name !== 'AbortError') {
statusEl.textContent = '❌ ' + t('scale.error_connect');
statusEl.className = 'settings-status error';
}
});
}
// ===== i18n TRANSLATION SYSTEM =====