feat(sprint-3): refresh responsive UlHub portal

This commit is contained in:
Brent Perteet
2026-08-20 22:00:38 -05:00
parent e552ae4e5c
commit d0b2f28762
12 changed files with 1710 additions and 228 deletions

View File

@@ -1,5 +1,7 @@
import { useEffect, useRef } from 'react';
import type { LiveStatus, MapPoint } from '../components/map/types';
import { useEffect, useRef, useState } from "react";
import type { LiveStatus, MapPoint } from "../components/map/types";
export type JobStreamState = "connecting" | "live" | "offline" | "error";
// Subscribes to live points + live position status for a job over the
// backend WebSocket. Reconnects with capped exponential backoff; resubscribes
@@ -9,6 +11,8 @@ export function useJobStream(
onPoints: (points: MapPoint[]) => void,
onStatus?: (status: LiveStatus) => void,
) {
const [connectionState, setConnectionState] =
useState<JobStreamState>("connecting");
const pointsRef = useRef(onPoints);
pointsRef.current = onPoints;
const statusRef = useRef(onStatus);
@@ -16,6 +20,7 @@ export function useJobStream(
useEffect(() => {
if (!jobId) {
setConnectionState("offline");
return;
}
@@ -25,12 +30,17 @@ export function useJobStream(
let timer: ReturnType<typeof setTimeout> | null = null;
const connect = () => {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
let socketErrored = false;
setConnectionState(attempt === 0 ? "connecting" : "offline");
const proto = window.location.protocol === "https:" ? "wss" : "ws";
socket = new WebSocket(`${proto}://${window.location.host}/api/ws`);
socket.onopen = () => {
attempt = 0;
socket?.send(JSON.stringify({ type: 'subscribe', channel: `job:${jobId}` }));
setConnectionState("live");
socket?.send(
JSON.stringify({ type: "subscribe", channel: `job:${jobId}` }),
);
};
socket.onmessage = (event) => {
@@ -39,9 +49,9 @@ export function useJobStream(
if (msg.jobId !== jobId) {
return;
}
if (msg.type === 'points') {
if (msg.type === "points") {
pointsRef.current(msg.points);
} else if (msg.type === 'status') {
} else if (msg.type === "status") {
statusRef.current?.(msg as LiveStatus);
}
} catch {
@@ -49,10 +59,18 @@ export function useJobStream(
}
};
socket.onerror = () => {
socketErrored = true;
setConnectionState("error");
};
socket.onclose = () => {
if (closed) {
return;
}
if (!socketErrored) {
setConnectionState("offline");
}
attempt += 1;
const delay = Math.min(1000 * 2 ** attempt, 15000);
timer = setTimeout(connect, delay);
@@ -69,4 +87,6 @@ export function useJobStream(
socket?.close();
};
}, [jobId]);
return connectionState;
}