Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev

This commit is contained in:
Fusion
2026-05-24 14:48:45 +02:00
15 changed files with 857 additions and 24 deletions
+17 -2
View File
@@ -86,13 +86,14 @@ async def terminal_websocket(
# Send connected status
await websocket.send_json({"type": "status", "status": "connected"})
# Start I/O loops
# Start I/O loops and heartbeat
read_task = asyncio.create_task(_read_loop(session, websocket))
write_task = asyncio.create_task(_write_loop(session, websocket))
heartbeat_task = asyncio.create_task(_heartbeat_loop(websocket))
# Wait for either task to complete (indicating disconnect or error)
done, pending = await asyncio.wait(
[read_task, write_task],
[read_task, write_task, heartbeat_task],
return_when=asyncio.FIRST_COMPLETED,
)
@@ -181,6 +182,20 @@ async def _write_loop(session, websocket) -> None:
pass
async def _heartbeat_loop(websocket: WebSocket) -> None:
"""Send periodic ping messages to detect disconnections."""
try:
while True:
await asyncio.sleep(30) # Ping every 30 seconds
try:
await websocket.send_json({"type": "ping"})
except Exception:
# WebSocket is closed or broken
break
except Exception:
pass
@router.post(
"/projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/terminal/reset",
summary="Reset terminal session",
+29
View File
@@ -54,6 +54,8 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
const stored = localStorage.getItem(FONT_SIZE_KEY);
return stored ? parseInt(stored, 10) : isMobile ? 16 : 14;
});
const lastPingRef = useRef<number>(0);
const heartbeatCheckRef = useRef<number | null>(null);
const calculateFontSize = useCallback(() => {
if (!isMobile) return fontSize;
@@ -75,6 +77,20 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
setStatus("connected");
setError(null);
reconnectAttemptsRef.current = 0;
lastPingRef.current = Date.now();
// Start heartbeat check
if (heartbeatCheckRef.current) {
window.clearInterval(heartbeatCheckRef.current);
}
heartbeatCheckRef.current = window.setInterval(() => {
const elapsed = Date.now() - lastPingRef.current;
if (elapsed > 60000) {
// No ping for 60 seconds, connection may be dead
console.warn("Terminal heartbeat timeout, reconnecting...");
ws.close(4000, "Heartbeat timeout");
}
}, 30000);
};
ws.onmessage = (event) => {
@@ -95,6 +111,12 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
} else if (msg.status === "resetting") {
setStatus("resetting");
}
} else if (msg.type === "ping") {
// Respond with pong and update last ping time
lastPingRef.current = Date.now();
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "pong" }));
}
}
} catch {
termRef.current?.write(event.data);
@@ -104,6 +126,13 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
ws.onclose = (event) => {
setStatus("disconnected");
// Clean up heartbeat check
if (heartbeatCheckRef.current) {
window.clearInterval(heartbeatCheckRef.current);
heartbeatCheckRef.current = null;
}
if (event.code !== 1000) {
setError(`Connection closed (code: ${event.code})`);