dead socket kicking and connection info pill

This commit is contained in:
legop3
2026-05-28 22:05:18 -04:00
parent 94ef97f5bb
commit a3d709236c
10 changed files with 162 additions and 12 deletions
+3
View File
@@ -2,6 +2,9 @@
2. add "reboot your own rover" feature
1. time delay and you can only do it if youre the only one on. server locks the rover then tells it to reboot
3. add a clear socket disconnection indicator in the web UI
1. fix reconnection stuff, make sure identity always sends. maybe even on a timer
2. stop allowing dead sockets (no identity) on rovers... idk how to handle this
1. disconnect dead sockets after 2 minutes, if identity is sent every minute no matter what, no one should ever get disconnected while functional
4. add votekicking
1. simply just disconnect the users socket.
5. add faster way for admins to login, like an invisible button in top left or something
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-IYeJHM9w.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BsongPZ9.css">
<script type="module" crossorigin src="/assets/index-4wUtzGNB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BpQe7PYg.css">
</head>
<body>
<div id="root"></div>
@@ -14,11 +14,14 @@ function registerVerificationHooks(deps) {
} = deps;
io.on('connection', (socket) => {
socket.data = socket.data || {};
socket.data.connectedAt = Date.now();
identifySocket(socket, {});
socket.on('session:identify', (payload = {}, cb = () => {}) => {
try {
const result = identifySocket(socket, payload || {});
socket.data.lastClientIdentifyAt = Date.now();
cb({ success: true, ...result });
} catch (err) {
cb({ error: err.message });
@@ -28,6 +28,8 @@ const { createRequestFlow } = require('./requestFlow');
const { registerVerificationHooks } = require('./hooks');
const verificationEvents = new EventEmitter();
const IDENTITY_TIMEOUT_MS = 2 * 60 * 1000;
const IDENTITY_SWEEP_INTERVAL_MS = 15 * 1000;
function emitChange(reason, payload = {}) {
verificationEvents.emit('change', { reason, ...payload });
@@ -145,6 +147,27 @@ registerVerificationHooks({
emitChange,
});
setInterval(() => {
const now = Date.now();
io.sockets.sockets.forEach((socket) => {
if (!socket?.id) return;
const role = getRole(socket);
if (role === 'admin' || role === 'lockdown') return;
const connectedAt = Number(socket?.data?.connectedAt || 0);
const lastClientIdentifyAt = Number(socket?.data?.lastClientIdentifyAt || 0);
const referenceTs = lastClientIdentifyAt || connectedAt;
if (!referenceTs) return;
if (now - referenceTs < IDENTITY_TIMEOUT_MS) return;
logger.info('Disconnecting socket due to stale identity heartbeat', {
socketId: socket.id,
role,
ageMs: now - referenceTs,
hadClientIdentify: Boolean(lastClientIdentifyAt),
});
socket.disconnect(true);
});
}, IDENTITY_SWEEP_INTERVAL_MS);
module.exports = {
identifySocket,
getVerificationStatus: verificationFlow.getVerificationStatus,
+2
View File
@@ -42,6 +42,7 @@ import VipPanel from './components/VipPanel/index.jsx';
import { useSessionSelector } from './context/SessionContext.jsx';
import ButtonBoxPanel from './components/ButtonBoxPanel/index.jsx';
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
function useLayoutMode() {
const [mode, setMode] = useState(() => {
@@ -333,6 +334,7 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
<RewardRunOverlay />
<TurnAlertListener />
<ModeGateOverlay />
<SocketConnectionPill />
<HelpOverlay
visible={helpVisible}
@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from 'react';
import { useSocket } from '../../context/SocketContext.jsx';
const CONNECTED_FADE_DELAY_MS = 4000;
function cls(...values) {
return values.filter(Boolean).join(' ');
}
export default function SocketConnectionPill() {
const socket = useSocket();
const [connected, setConnected] = useState(Boolean(socket?.connected));
const [lastReason, setLastReason] = useState('');
const [visible, setVisible] = useState(true);
const [faded, setFaded] = useState(false);
const fadeTimerRef = useRef(null);
useEffect(() => {
const clearFadeTimer = () => {
if (fadeTimerRef.current) {
clearTimeout(fadeTimerRef.current);
fadeTimerRef.current = null;
}
};
const show = () => {
clearFadeTimer();
setVisible(true);
setFaded(false);
};
const scheduleFade = () => {
clearFadeTimer();
fadeTimerRef.current = setTimeout(() => {
setFaded(true);
}, CONNECTED_FADE_DELAY_MS);
};
const onConnect = () => {
setConnected(true);
setLastReason('');
show();
scheduleFade();
};
const onDisconnect = (reason) => {
setConnected(false);
setLastReason(typeof reason === 'string' ? reason : 'disconnected');
show();
};
const onConnectError = (err) => {
setConnected(false);
setLastReason(err?.message || 'connect error');
show();
};
const onReconnectAttempt = () => {
setConnected(false);
show();
};
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
socket.on('connect_error', onConnectError);
socket.io.on('reconnect_attempt', onReconnectAttempt);
if (socket.connected) {
scheduleFade();
}
return () => {
clearFadeTimer();
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
socket.off('connect_error', onConnectError);
socket.io.off('reconnect_attempt', onReconnectAttempt);
};
}, [socket]);
const offline = typeof navigator !== 'undefined' ? navigator.onLine === false : false;
const statusLabel = connected
? 'Connected to socket.io'
: offline
? 'Offline'
: 'Reconnecting...';
const detail = connected ? '' : lastReason;
const toneClass = connected
? 'border-emerald-300/40 bg-emerald-700/70 text-emerald-100'
: offline
? 'border-red-300/50 bg-red-800/80 text-red-100'
: 'border-amber-300/50 bg-amber-800/80 text-amber-100';
return (
<div
className={cls(
'fixed bottom-2 left-2 z-[120] transition-opacity duration-500',
!visible && 'hidden',
faded ? 'opacity-0 pointer-events-none' : 'opacity-100',
)}
aria-live="polite"
>
<div className={cls('rounded-md border px-2 py-1 text-xs shadow-lg backdrop-blur-sm', toneClass)}>
<span className="font-semibold">{statusLabel}</span>
{detail ? <span className="ml-1 opacity-85">({detail})</span> : null}
</div>
</div>
);
}
+9
View File
@@ -95,5 +95,14 @@ export default function useUserIdentitySync() {
};
}, [sendIdentify, socket?.connected]);
useEffect(() => {
if (!ready || !socket?.connected) return undefined;
const timer = setInterval(() => {
if (!socket?.connected) return;
sendIdentify();
}, 60000);
return () => clearInterval(timer);
}, [ready, sendIdentify, socket?.connected]);
useEffect(() => () => clearRetry(), [clearRetry]);
}