Improve portable launch, forge persistence, and operator auth UX.
Persist build extra_files for Build Manager history, print dashboard login on every start, add libp2p for Mesh P2P forge, defer WebSocket until login, and split devrun.bat from LAUNCH.bat with USB deck auto-detection.
This commit is contained in:
@@ -48,6 +48,7 @@ describe('WebSocketProvider', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
MockWebSocket.instances = [];
|
||||
setStoredAuth('testuser', 'testpass', { silent: true });
|
||||
vi.stubGlobal('WebSocket', MockWebSocket as unknown as typeof WebSocket);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { protocol: 'http:', host: 'localhost:8080' },
|
||||
@@ -69,6 +70,7 @@ describe('WebSocketProvider', () => {
|
||||
|
||||
it('connects to ws dashboard with auth token query param', () => {
|
||||
setStoredAuth('drjones', 'secret');
|
||||
MockWebSocket.instances = [];
|
||||
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
|
||||
|
||||
const ws = latestSocket();
|
||||
@@ -79,10 +81,11 @@ describe('WebSocketProvider', () => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
it('connects without token when logged out', () => {
|
||||
clearStoredAuth();
|
||||
it('does not connect when logged out', () => {
|
||||
clearStoredAuth({ silent: true });
|
||||
MockWebSocket.instances = [];
|
||||
renderHook(() => useWebSocketContext(), { wrapper });
|
||||
expect(latestSocket().url).toBe('ws://localhost:8080/ws/dashboard');
|
||||
expect(MockWebSocket.instances).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('useWebSocket re-exports context hook', () => {
|
||||
@@ -160,6 +163,7 @@ describe('WebSocketProvider', () => {
|
||||
|
||||
it('schedules reconnect after close', () => {
|
||||
vi.useFakeTimers();
|
||||
MockWebSocket.instances = [];
|
||||
renderHook(() => useWebSocketContext(), { wrapper });
|
||||
const first = latestSocket();
|
||||
|
||||
|
||||
@@ -40,14 +40,25 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
|
||||
reconnectTimer.current = null;
|
||||
}
|
||||
|
||||
const token = getStoredAuth();
|
||||
if (!token) {
|
||||
const existing = wsRef.current;
|
||||
if (existing) {
|
||||
existing.onclose = null;
|
||||
existing.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
setIsConnected(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = wsRef.current;
|
||||
if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
|
||||
existing.close();
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const token = getStoredAuth();
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard?token=${encodeURIComponent(token)}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
@@ -57,6 +68,7 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
|
||||
if (unmounted.current) return;
|
||||
setIsConnected(false);
|
||||
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
|
||||
if (!getStoredAuth()) return;
|
||||
reconnectTimer.current = setTimeout(connect, 3000);
|
||||
};
|
||||
|
||||
@@ -201,8 +213,11 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
unmounted.current = false;
|
||||
connect();
|
||||
const onAuthChange = () => connect();
|
||||
window.addEventListener('aetherforge-auth', onAuthChange);
|
||||
return () => {
|
||||
unmounted.current = true;
|
||||
window.removeEventListener('aetherforge-auth', onAuthChange);
|
||||
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
|
||||
const ws = wsRef.current;
|
||||
if (ws) { ws.onclose = null; ws.close(); }
|
||||
|
||||
Reference in New Issue
Block a user