ddaf3c6cee
- Add JWT token auth to OPC WebSocket (unauthenticated → 403, per-user tracking) - Externalize Transmission RPC credentials to TRANSMISSION_USER/PASS env vars - Remove hardcoded SECRET_KEY fallback from dev compose - Rate-limit passkey register options endpoint at 5/minute - Add PDD (docs/improvement-plan.md) and sprint specs (specs/) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
90 lines
1.9 KiB
JavaScript
90 lines
1.9 KiB
JavaScript
/**
|
|
* OPC WebSocket Client - Real-time updates
|
|
*/
|
|
|
|
import { getToken } from "./api.js";
|
|
|
|
let ws = null;
|
|
let reconnectTimer = null;
|
|
let listeners = new Set();
|
|
|
|
export function connect() {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
const token = getToken();
|
|
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : "";
|
|
const wsUrl = `${protocol}//${window.location.host}/ws/opc${tokenParam}`;
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
console.log("OPC WebSocket connected");
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
|
|
// Send ping every 30 seconds to keep connection alive
|
|
const pingInterval = setInterval(() => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send("ping");
|
|
} else {
|
|
clearInterval(pingInterval);
|
|
}
|
|
}, 30000);
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
notifyListeners(message);
|
|
} catch (e) {
|
|
console.error("Failed to parse WebSocket message:", e);
|
|
}
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
console.error("WebSocket error:", error);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
console.log("OPC WebSocket disconnected");
|
|
ws = null;
|
|
|
|
// Reconnect after 5 seconds
|
|
reconnectTimer = setTimeout(() => {
|
|
console.log("Reconnecting WebSocket...");
|
|
connect();
|
|
}, 5000);
|
|
};
|
|
}
|
|
|
|
export function disconnect() {
|
|
if (ws) {
|
|
ws.close();
|
|
ws = null;
|
|
}
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
}
|
|
|
|
export function subscribe(callback) {
|
|
listeners.add(callback);
|
|
return () => listeners.delete(callback);
|
|
}
|
|
|
|
function notifyListeners(message) {
|
|
listeners.forEach((callback) => {
|
|
try {
|
|
callback(message);
|
|
} catch (e) {
|
|
console.error("Listener error:", e);
|
|
}
|
|
});
|
|
}
|