mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
socket log in admin panel
This commit is contained in:
@@ -6,6 +6,7 @@ import KeymapSettings from './KeymapSettings.jsx';
|
||||
import GamepadMappingSettings from './GamepadMappingSettings.jsx';
|
||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
|
||||
import SessionSnapshot from './SessionSnapshot.jsx';
|
||||
import SocketLogPanel from './SocketLogPanel.jsx';
|
||||
import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
|
||||
|
||||
const manualTabs = [
|
||||
@@ -110,6 +111,7 @@ export default function SettingsPanel() {
|
||||
<AuthPanel />
|
||||
<AdminPanel />
|
||||
<SessionSnapshot />
|
||||
<SocketLogPanel />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
|
||||
const MAX_ENTRIES = 300;
|
||||
const MAX_PAYLOAD_LENGTH = 240;
|
||||
|
||||
function formatValue(value, seen) {
|
||||
if (value === undefined) return 'undefined';
|
||||
if (value === null) return 'null';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (typeof value === 'function') return '[Function]';
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
|
||||
if (!seen) {
|
||||
seen = new WeakSet();
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(
|
||||
value,
|
||||
(key, nested) => {
|
||||
if (typeof nested === 'function') return '[Function]';
|
||||
if (nested instanceof Error) return `${nested.name}: ${nested.message}`;
|
||||
return nested;
|
||||
},
|
||||
0,
|
||||
);
|
||||
} catch (err) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function formatArgs(args) {
|
||||
const parts = args.map((arg) => formatValue(arg));
|
||||
const payload = parts.join(' ');
|
||||
if (payload.length > MAX_PAYLOAD_LENGTH) {
|
||||
return `${payload.slice(0, MAX_PAYLOAD_LENGTH)}…`;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export default function SocketLogPanel() {
|
||||
const socket = useSocket();
|
||||
const [entries, setEntries] = useState([]);
|
||||
|
||||
const counters = useMemo(() => {
|
||||
return entries.reduce(
|
||||
(acc, entry) => {
|
||||
if (entry.direction === 'in') acc.in += 1;
|
||||
if (entry.direction === 'out') acc.out += 1;
|
||||
return acc;
|
||||
},
|
||||
{ in: 0, out: 0 },
|
||||
);
|
||||
}, [entries]);
|
||||
|
||||
useEffect(() => {
|
||||
const pushEntry = (direction, event, args) => {
|
||||
setEntries((prev) => {
|
||||
const next = [
|
||||
...prev,
|
||||
{
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
direction,
|
||||
event,
|
||||
payload: formatArgs(args),
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
},
|
||||
];
|
||||
if (next.length > MAX_ENTRIES) {
|
||||
return next.slice(-MAX_ENTRIES);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleIncoming = (event, ...args) => pushEntry('in', event, args);
|
||||
const handleOutgoing = (event, ...args) => pushEntry('out', event, args);
|
||||
|
||||
socket.onAny(handleIncoming);
|
||||
|
||||
let restoreEmit = null;
|
||||
if (typeof socket.onAnyOutgoing === 'function') {
|
||||
socket.onAnyOutgoing(handleOutgoing);
|
||||
restoreEmit = () => socket.offAnyOutgoing(handleOutgoing);
|
||||
} else {
|
||||
const originalEmit = socket.emit.bind(socket);
|
||||
socket.emit = (event, ...args) => {
|
||||
handleOutgoing(event, ...args);
|
||||
return originalEmit(event, ...args);
|
||||
};
|
||||
restoreEmit = () => {
|
||||
socket.emit = originalEmit;
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket.offAny(handleIncoming);
|
||||
if (restoreEmit) {
|
||||
restoreEmit();
|
||||
}
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<div className="flex items-center justify-between text-sm text-slate-400">
|
||||
<span>Socket activity</span>
|
||||
<div className="flex items-center gap-0.5 text-xs text-slate-400">
|
||||
<span className="surface-muted">in {counters.in}</span>
|
||||
<span className="surface-muted">out {counters.out}</span>
|
||||
<button type="button" className="button-dark text-xs" onClick={() => setEntries([])}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface h-64 overflow-y-auto font-mono text-xs">
|
||||
{entries.length === 0 ? (
|
||||
<p>No socket activity yet.</p>
|
||||
) : (
|
||||
entries
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((entry) => (
|
||||
<div key={entry.id} className="surface">
|
||||
<span className="text-slate-400">{entry.timestamp}</span>{' '}
|
||||
<span className={entry.direction === 'in' ? 'text-emerald-400' : 'text-amber-400'}>
|
||||
[{entry.direction.toUpperCase()}]
|
||||
</span>{' '}
|
||||
<span className="text-cyan-300">{entry.event}</span>{' '}
|
||||
<span className="text-slate-200">{entry.payload}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Logs are collected only while this panel is open.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user