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:
AetherForge
2026-05-31 18:56:43 -07:00
parent 2f528229f2
commit feba06e008
80 changed files with 2897 additions and 675 deletions

View File

@@ -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();

View File

@@ -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(); }