+
- Welcome
+Sign in or create an account to use this phone as a camera or client.
+ + + + + + +
+
+
+
-
-
-
-
Device Bootstrap
- - - - - - - - - -
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
@@ -219,38 +360,146 @@
device: null,
deviceToken: null,
socket: null,
+ currentScreen: 'auth',
lastMotionEventId: null,
lastStreamSessionId: null,
lastRecordingId: null,
latestPushNotificationId: null,
};
+ const screens = ['auth', 'onboarding', 'home-client', 'home-camera', 'activity', 'account'];
+
const $ = (id) => document.getElementById(id);
+ const safeJson = (value) => JSON.stringify(value, null, 2);
+
const log = (message, payload) => {
const ts = new Date().toISOString();
const line = `[${ts}] ${message}`;
- const current = $('log').textContent;
- $('log').textContent = `${line}${payload ? `\n${JSON.stringify(payload, null, 2)}` : ''}\n\n${current}`;
+ const next = `${line}${payload ? `\n${safeJson(payload)}` : ''}\n\n${$('eventLog').textContent}`;
+ $('eventLog').textContent = next;
};
const saveLocal = () => {
localStorage.setItem('mobileSimDevice', JSON.stringify({ device: state.device, deviceToken: state.deviceToken }));
};
- const render = () => {
- $('authState').textContent = JSON.stringify({ session: state.session }, null, 2);
- $('deviceState').textContent = JSON.stringify({ device: state.device, hasToken: Boolean(state.deviceToken) }, null, 2);
- $('clientState').textContent = JSON.stringify({ lastStreamSessionId: state.lastStreamSessionId }, null, 2);
- $('cameraState').textContent = JSON.stringify(
- {
- lastMotionEventId: state.lastMotionEventId,
- lastRecordingId: state.lastRecordingId,
- latestPushNotificationId: state.latestPushNotificationId,
+ const loadLocal = () => {
+ const raw = localStorage.getItem('mobileSimDevice');
+ if (!raw) return;
+ try {
+ const parsed = JSON.parse(raw);
+ state.device = parsed.device ?? null;
+ state.deviceToken = parsed.deviceToken ?? null;
+ } catch {
+ state.device = null;
+ state.deviceToken = null;
+ }
+ };
+
+ const setScreen = (name) => {
+ state.currentScreen = name;
+ for (const screen of screens) {
+ $(`screen-${screen}`).classList.toggle('active', screen === name);
+ }
+ };
+
+ const updateTop = () => {
+ const signedIn = Boolean(state.session?.session);
+ $('authChip').textContent = signedIn ? 'signed in' : 'signed out';
+ $('authChip').className = `chip ${signedIn ? 'online' : 'offline'}`;
+
+ const socketConnected = Boolean(state.socket?.connected);
+ $('socketChip').textContent = socketConnected ? 'online' : 'offline';
+ $('socketChip').className = `chip ${socketConnected ? 'online' : 'offline'}`;
+
+ if (!signedIn) {
+ $('topSubtitle').textContent = 'Sign in to continue';
+ return;
+ }
+
+ if (!state.device) {
+ $('topSubtitle').textContent = 'Set up this phone as camera or client';
+ return;
+ }
+
+ $('topSubtitle').textContent = `${state.device.role} mode ยท ${state.device.name || state.device.id}`;
+ };
+
+ const renderState = () => {
+ $('activityState').textContent = safeJson({
+ device: state.device,
+ lastStreamSessionId: state.lastStreamSessionId,
+ lastRecordingId: state.lastRecordingId,
+ latestPushNotificationId: state.latestPushNotificationId,
+ });
+
+ $('accountState').textContent = safeJson({
+ session: state.session,
+ deviceTokenPresent: Boolean(state.deviceToken),
+ });
+ };
+
+ const updateFlow = () => {
+ const signedIn = Boolean(state.session?.session);
+
+ if (!signedIn) {
+ $('bottomNav').classList.add('hidden');
+ setScreen('auth');
+ updateTop();
+ renderState();
+ return;
+ }
+
+ if (!state.device || !state.deviceToken) {
+ $('bottomNav').classList.add('hidden');
+ setScreen('onboarding');
+ updateTop();
+ renderState();
+ return;
+ }
+
+ $('bottomNav').classList.remove('hidden');
+
+ if (!['home-client', 'home-camera', 'activity', 'account'].includes(state.currentScreen)) {
+ setScreen(state.device.role === 'camera' ? 'home-camera' : 'home-client');
+ }
+
+ updateTop();
+ renderState();
+ };
+
+ const authFetch = async (url, options = {}) => {
+ const response = await fetch(url, {
+ credentials: 'include',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {}),
},
- null,
- 2,
- );
+ });
+
+ const payload = await response.json().catch(() => ({}));
+
+ if (!response.ok) {
+ throw new Error(payload.message || response.statusText);
+ }
+
+ return payload;
+ };
+
+ const deviceFetch = async (url, options = {}) => {
+ if (!state.deviceToken) {
+ throw new Error('No device token');
+ }
+
+ return authFetch(url, {
+ ...options,
+ headers: {
+ Authorization: `Bearer ${state.deviceToken}`,
+ ...(options.headers || {}),
+ },
+ });
};
const getAuthPayload = () => ({
@@ -259,12 +508,83 @@
password: $('authPassword').value,
});
+ const connectSocket = () => {
+ if (!state.deviceToken) throw new Error('Register device first');
+
+ if (state.socket) {
+ state.socket.disconnect();
+ }
+
+ state.socket = io({ auth: { token: state.deviceToken } });
+
+ state.socket.on('connect', () => {
+ updateTop();
+ log('socket connected');
+ });
+
+ state.socket.on('disconnect', () => {
+ updateTop();
+ log('socket disconnected');
+ });
+
+ state.socket.on('connected', (payload) => log('connected', payload));
+ state.socket.on('command:status', (payload) => log('command:status', payload));
+ state.socket.on('stream:requested', (payload) => {
+ state.lastStreamSessionId = payload.streamSessionId;
+ renderState();
+ log('stream:requested', payload);
+ });
+ state.socket.on('stream:started', (payload) => {
+ state.lastStreamSessionId = payload.streamSessionId;
+ renderState();
+ log('stream:started', payload);
+ if (state.device?.role === 'client') {
+ deviceFetch(`/streams/${payload.streamSessionId}/subscribe-credentials`)
+ .then((x) => log('auto subscribe credentials', x))
+ .catch((e) => log('auto subscribe credentials failed', { error: e.message }));
+ }
+ });
+ state.socket.on('stream:ended', (payload) => log('stream:ended', payload));
+ state.socket.on('motion:detected', (payload) => log('motion:detected', payload));
+ state.socket.on('motion:ended', (payload) => log('motion:ended', payload));
+
+ state.socket.on('command:received', async (payload) => {
+ log('command:received', payload);
+
+ try {
+ if (payload.commandType === 'start_stream' && payload.payload?.streamSessionId) {
+ await deviceFetch(`/streams/${payload.payload.streamSessionId}/accept`, {
+ method: 'POST',
+ body: JSON.stringify({}),
+ });
+
+ const creds = await deviceFetch(`/streams/${payload.payload.streamSessionId}/publish-credentials`);
+ log('auto publish credentials', creds);
+ }
+
+ if (payload.commandType === 'stop_stream' && payload.payload?.streamSessionId) {
+ await deviceFetch(`/streams/${payload.payload.streamSessionId}/end`, {
+ method: 'POST',
+ body: JSON.stringify({ reason: 'completed' }),
+ });
+ }
+
+ state.socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' });
+ } catch (error) {
+ state.socket.emit('command:ack', {
+ commandId: payload.commandId,
+ status: 'rejected',
+ error: error.message,
+ });
+ }
+ });
+ };
+
$('signUpBtn').addEventListener('click', async () => {
try {
- const authPayload = getAuthPayload();
const payload = await authFetch('/api/auth/sign-up/email', {
method: 'POST',
- body: JSON.stringify(authPayload),
+ body: JSON.stringify(getAuthPayload()),
});
log('sign up', payload);
$('sessionBtn').click();
@@ -291,151 +611,39 @@
try {
const payload = await authFetch('/api/auth/get-session');
state.session = payload?.session ? payload : null;
- render();
- log('session check', payload);
+ log('session', payload);
} catch (error) {
state.session = null;
- render();
log('session check failed', { error: error.message });
}
+ updateFlow();
});
$('signOutBtn').addEventListener('click', async () => {
try {
- const payload = await authFetch('/api/auth/sign-out', { method: 'POST', body: JSON.stringify({}) });
- state.session = null;
- render();
- log('sign out', payload);
+ await authFetch('/api/auth/sign-out', { method: 'POST', body: JSON.stringify({}) });
} catch (error) {
log('sign out failed', { error: error.message });
}
+
+ state.session = null;
+ state.device = null;
+ state.deviceToken = null;
+ state.socket?.disconnect();
+ state.socket = null;
+ localStorage.removeItem('mobileSimDevice');
+ updateFlow();
});
- const authFetch = async (url, options = {}) => {
- const response = await fetch(url, {
- credentials: 'include',
- ...options,
- headers: {
- 'Content-Type': 'application/json',
- ...(options.headers || {}),
- },
- });
-
- const payload = await response.json().catch(() => ({}));
-
- if (!response.ok) {
- throw new Error(payload.message || response.statusText);
- }
-
- return payload;
- };
-
- const deviceFetch = async (url, options = {}) => {
- if (!state.deviceToken) {
- throw new Error('No device token. Register device first.');
- }
-
- return authFetch(url, {
- ...options,
- headers: {
- Authorization: `Bearer ${state.deviceToken}`,
- ...(options.headers || {}),
- },
- });
- };
-
- const connectSocket = () => {
- if (!state.deviceToken) {
- throw new Error('No device token');
- }
-
- if (state.socket) {
- state.socket.disconnect();
- }
-
- state.socket = io({ auth: { token: state.deviceToken } });
-
- state.socket.on('connect', () => {
- $('connectionState').textContent = 'Socket connected';
- $('connectionState').className = 'online';
- log('socket connected');
- });
-
- state.socket.on('disconnect', () => {
- $('connectionState').textContent = 'Socket disconnected';
- $('connectionState').className = 'offline';
- log('socket disconnected');
- });
-
- state.socket.on('connected', (payload) => log('connected', payload));
- state.socket.on('command:status', (payload) => log('command:status', payload));
- state.socket.on('stream:requested', (payload) => {
- state.lastStreamSessionId = payload.streamSessionId;
- render();
- log('stream:requested', payload);
- });
- state.socket.on('stream:started', (payload) => {
- state.lastStreamSessionId = payload.streamSessionId;
- render();
- log('stream:started', payload);
-
- if (state.device?.role === 'client') {
- deviceFetch(`/streams/${payload.streamSessionId}/subscribe-credentials`)
- .then((credentials) => log('auto subscribe credentials', credentials))
- .catch((error) => log('auto subscribe credentials failed', { error: error.message }));
- }
- });
- state.socket.on('stream:ended', (payload) => log('stream:ended', payload));
- state.socket.on('motion:detected', (payload) => log('motion:detected', payload));
- state.socket.on('motion:ended', (payload) => log('motion:ended', payload));
-
- // Mobile-camera behavior: accept start_stream commands and acknowledge.
- state.socket.on('command:received', async (payload) => {
- log('command:received', payload);
-
- try {
- if (payload.commandType === 'start_stream' && payload.payload?.streamSessionId) {
- await deviceFetch(`/streams/${payload.payload.streamSessionId}/accept`, {
- method: 'POST',
- body: JSON.stringify({}),
- });
-
- const publishCredentials = await deviceFetch(
- `/streams/${payload.payload.streamSessionId}/publish-credentials`,
- );
- log('auto publish credentials', publishCredentials);
- }
-
- if (payload.commandType === 'stop_stream' && payload.payload?.streamSessionId) {
- await deviceFetch(`/streams/${payload.payload.streamSessionId}/end`, {
- method: 'POST',
- body: JSON.stringify({ reason: 'completed' }),
- });
- }
-
- state.socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' });
- } catch (error) {
- state.socket.emit('command:ack', {
- commandId: payload.commandId,
- status: 'rejected',
- error: error.message,
- });
- }
- });
- };
-
$('registerBtn').addEventListener('click', async () => {
try {
- if (!state.session) throw new Error('Authenticate first');
- const role = $('role').value;
- const name = $('deviceName').value.trim();
const payload = await authFetch('/devices/register', {
method: 'POST',
body: JSON.stringify({
- role,
- name: name || undefined,
+ role: $('role').value,
+ name: $('deviceName').value.trim() || undefined,
platform: 'web',
- appVersion: 'sim-1',
+ appVersion: 'sim-ux-1',
pushToken: $('pushToken').value.trim() || undefined,
}),
});
@@ -443,94 +651,59 @@
state.device = payload.device;
state.deviceToken = payload.deviceToken;
saveLocal();
- render();
log('device registered', payload);
+ updateFlow();
} catch (error) {
log('register failed', { error: error.message });
}
});
- $('loadSavedBtn').addEventListener('click', () => {
- const raw = localStorage.getItem('mobileSimDevice');
- if (!raw) return;
- const parsed = JSON.parse(raw);
- state.device = parsed.device;
- state.deviceToken = parsed.deviceToken;
- render();
- log('loaded saved device', parsed);
- });
-
- const auditPanel = document.createElement('section');
- auditPanel.className = 'panel';
- auditPanel.style.marginTop = '16px';
- auditPanel.innerHTML = `
-
+
-
- Set Up This Phone
+Choose how this phone should behave in your home security setup.
+ + + + + + + +
-
-
-
-
- Socket disconnected
-
Client Actions
- - -
-
-
+
+
-
-
+
-
-
+
-
-
-
-
-
-
Client Home
+Monitor cameras and request live streams.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Camera Actions
- - - - - +
+
Camera Home
+Detect motion, stream live, and finalize recordings.
+
+
+
+
+
+
+
+
+
+
+
+
+ Live Event Log
- -
+
+ Activity
+Realtime events and push inbox.
+
+
+
+
+
+
+
+
+
+
+
+
+ Account
+
+
+
+
+
+
+
+
+
+
+ Audit Logs
- - `; - document.querySelector('.page').appendChild(auditPanel); - - $('fetchAuditBtn').addEventListener('click', async () => { - try { - const payload = await deviceFetch('/audit/device'); - log('audit logs', payload); - } catch (error) { - log('audit fetch failed', { error: error.message }); - } - }); - $('loadSavedBtn').addEventListener('click', async () => { + loadLocal(); + try { - if (!state.device?.id) return; - const token = $('pushToken').value.trim(); - if (!token) return; - await authFetch(`/devices/${state.device.id}`, { - method: 'PATCH', - body: JSON.stringify({ pushToken: token }), - }); - log('push token updated', { deviceId: state.device.id }); + if (state.device?.id && $('pushToken').value.trim()) { + await authFetch(`/devices/${state.device.id}`, { + method: 'PATCH', + body: JSON.stringify({ pushToken: $('pushToken').value.trim() }), + }); + } } catch (error) { log('push token update failed', { error: error.message }); } + + updateFlow(); }); - $('connectBtn').addEventListener('click', () => { - try { - connectSocket(); - } catch (error) { - log('connect failed', { error: error.message }); - } - }); + const bindConnectButtons = (connectId, disconnectId) => { + $(connectId).addEventListener('click', () => { + try { + connectSocket(); + } catch (error) { + log('connect failed', { error: error.message }); + } + }); - $('disconnectBtn').addEventListener('click', () => { - state.socket?.disconnect(); - }); + $(disconnectId).addEventListener('click', () => { + state.socket?.disconnect(); + }); + }; - $('refreshDevicesBtn').addEventListener('click', async () => { - try { - const payload = await authFetch('/devices'); - log('devices', payload); - } catch (error) { - log('fetch devices failed', { error: error.message }); - } - }); + bindConnectButtons('connectBtn', 'disconnectBtn'); + bindConnectButtons('connectBtnCam', 'disconnectBtnCam'); $('linkBtn').addEventListener('click', async () => { try { - if (!state.device?.id) throw new Error('Register/load a device first'); - if (state.device.role !== 'client') throw new Error('This simulator device is not a client role'); + if (!state.device?.id || state.device.role !== 'client') throw new Error('Current phone is not in client mode'); const cameraDeviceId = $('targetCameraId').value.trim(); - if (!cameraDeviceId) throw new Error('Enter target camera device id'); + if (!cameraDeviceId) throw new Error('Enter camera device id'); const payload = await authFetch('/device-links', { method: 'POST', - body: JSON.stringify({ - cameraDeviceId, - clientDeviceId: state.device.id, - }), + body: JSON.stringify({ cameraDeviceId, clientDeviceId: state.device.id }), }); - - log('link created', payload); + log('camera linked', payload); } catch (error) { log('link failed', { error: error.message }); } @@ -539,7 +712,7 @@ $('requestStreamBtn').addEventListener('click', async () => { try { const cameraDeviceId = $('targetCameraId').value.trim(); - if (!cameraDeviceId) throw new Error('Enter target camera device id'); + if (!cameraDeviceId) throw new Error('Enter camera device id'); const payload = await deviceFetch('/streams/request', { method: 'POST', @@ -547,17 +720,26 @@ }); state.lastStreamSessionId = payload.streamSession.id; - render(); + renderState(); log('stream requested', payload); } catch (error) { log('stream request failed', { error: error.message }); } }); + $('fetchSubscribeBtn').addEventListener('click', async () => { + try { + if (!state.lastStreamSessionId) throw new Error('No stream session yet'); + const payload = await deviceFetch(`/streams/${state.lastStreamSessionId}/subscribe-credentials`); + log('subscribe credentials', payload); + } catch (error) { + log('subscribe credentials failed', { error: error.message }); + } + }); + $('fetchPlaybackBtn').addEventListener('click', async () => { try { - if (!state.lastStreamSessionId) throw new Error('No known stream session'); - + if (!state.lastStreamSessionId) throw new Error('No stream session yet'); const payload = await deviceFetch(`/streams/${state.lastStreamSessionId}/playback-token`); log('playback token', payload); } catch (error) { @@ -568,19 +750,19 @@ $('listRecordingsBtn').addEventListener('click', async () => { try { const payload = await deviceFetch('/recordings/me/list'); - if (payload.recordings?.length > 0) { + if (payload.recordings?.length) { state.lastRecordingId = payload.recordings[0].id; - render(); } + renderState(); log('recordings', payload); } catch (error) { - log('list recordings failed', { error: error.message }); + log('recordings list failed', { error: error.message }); } }); $('downloadLatestRecordingBtn').addEventListener('click', async () => { try { - if (!state.lastRecordingId) throw new Error('No recording id available'); + if (!state.lastRecordingId) throw new Error('No recording selected'); const payload = await deviceFetch(`/recordings/${state.lastRecordingId}/download-url`); log('recording download url', payload); } catch (error) { @@ -588,25 +770,14 @@ } }); - $('fetchSubscribeBtn').addEventListener('click', async () => { - try { - if (!state.lastStreamSessionId) throw new Error('No known stream session'); - const payload = await deviceFetch(`/streams/${state.lastStreamSessionId}/subscribe-credentials`); - log('subscribe credentials', payload); - } catch (error) { - log('subscribe credentials failed', { error: error.message }); - } - }); - $('startMotionBtn').addEventListener('click', async () => { try { const payload = await deviceFetch('/events/motion/start', { method: 'POST', - body: JSON.stringify({ title: 'Motion from web simulator', triggeredBy: 'motion' }), + body: JSON.stringify({ title: 'Motion from simulator', triggeredBy: 'motion' }), }); - state.lastMotionEventId = payload.event.id; - render(); + renderState(); log('motion started', payload); } catch (error) { log('motion start failed', { error: error.message }); @@ -615,13 +786,11 @@ $('endMotionBtn').addEventListener('click', async () => { try { - if (!state.lastMotionEventId) throw new Error('No motion event to end'); - + if (!state.lastMotionEventId) throw new Error('No active motion event id'); const payload = await deviceFetch(`/events/${state.lastMotionEventId}/motion/end`, { method: 'POST', body: JSON.stringify({ status: 'completed' }), }); - log('motion ended', payload); } catch (error) { log('motion end failed', { error: error.message }); @@ -630,7 +799,7 @@ $('fetchPublishBtn').addEventListener('click', async () => { try { - if (!state.lastStreamSessionId) throw new Error('No known stream session'); + if (!state.lastStreamSessionId) throw new Error('No stream session yet'); const payload = await deviceFetch(`/streams/${state.lastStreamSessionId}/publish-credentials`); log('publish credentials', payload); } catch (error) { @@ -641,14 +810,12 @@ $('finalizeRecordingBtn').addEventListener('click', async () => { try { if (!state.lastStreamSessionId) throw new Error('No stream session yet'); - - const listPayload = await deviceFetch('/recordings/me/list'); - const target = listPayload.recordings?.find((r) => r.streamSessionId === state.lastStreamSessionId) ?? listPayload.recordings?.[0]; - - if (!target) throw new Error('No recording placeholder exists'); + const list = await deviceFetch('/recordings/me/list'); + const target = list.recordings?.find((r) => r.streamSessionId === state.lastStreamSessionId) ?? list.recordings?.[0]; + if (!target) throw new Error('No recording placeholder found'); state.lastRecordingId = target.id; - render(); + renderState(); const payload = await deviceFetch(`/recordings/${target.id}/finalize`, { method: 'POST', @@ -656,49 +823,25 @@ objectKey: `recordings/${target.id}.mp4`, bucket: 'videos', durationSeconds: 12, - sizeBytes: 1024 * 1024 * 8, + sizeBytes: 8 * 1024 * 1024, }), }); - log('recording finalized', payload); } catch (error) { - log('finalize recording failed', { error: error.message }); - } - }); - - const pushPanel = document.createElement('section'); - pushPanel.className = 'panel'; - pushPanel.style.marginTop = '16px'; - pushPanel.innerHTML = ` -Push Inbox (Offline Fallback)
-
-
-
-
-
- `;
- document.querySelector('.page').appendChild(pushPanel);
-
- $('dispatchPushWorkerBtn').addEventListener('click', async () => {
- try {
- const payload = await deviceFetch('/push-notifications/worker/dispatch', { method: 'POST', body: JSON.stringify({}) });
- log('push worker dispatch', payload);
- } catch (error) {
- log('push worker dispatch failed', { error: error.message });
+ log('recording finalize failed', { error: error.message });
}
});
$('pollPushInboxBtn').addEventListener('click', async () => {
try {
const payload = await deviceFetch('/push-notifications/me');
- const latest = payload.notifications?.[0];
- if (latest) {
- state.latestPushNotificationId = latest.id;
+ if (payload.notifications?.length) {
+ state.latestPushNotificationId = payload.notifications[0].id;
}
- render();
+ renderState();
log('push inbox', payload);
} catch (error) {
- log('poll push inbox failed', { error: error.message });
+ log('push inbox failed', { error: error.message });
}
});
@@ -715,18 +858,26 @@
}
});
- const opsPanel = document.createElement('section');
- opsPanel.className = 'panel';
- opsPanel.style.marginTop = '16px';
- opsPanel.innerHTML = `
- Ops Checks
-
-
-
-
-
- `;
- document.querySelector('.page').appendChild(opsPanel);
+ $('dispatchPushWorkerBtn').addEventListener('click', async () => {
+ try {
+ const payload = await deviceFetch('/push-notifications/worker/dispatch', {
+ method: 'POST',
+ body: JSON.stringify({}),
+ });
+ log('push worker dispatch', payload);
+ } catch (error) {
+ log('push worker dispatch failed', { error: error.message });
+ }
+ });
+
+ $('fetchAuditBtn').addEventListener('click', async () => {
+ try {
+ const payload = await deviceFetch('/audit/device');
+ log('audit logs', payload);
+ } catch (error) {
+ log('audit fetch failed', { error: error.message });
+ }
+ });
$('checkLiveBtn').addEventListener('click', async () => {
try {
@@ -755,7 +906,38 @@
}
});
- render();
+ $('navHome').addEventListener('click', () => {
+ setScreen(state.device?.role === 'camera' ? 'home-camera' : 'home-client');
+ updateFlow();
+ });
+
+ $('navActivity').addEventListener('click', () => {
+ setScreen('activity');
+ updateFlow();
+ });
+
+ $('navAccount').addEventListener('click', () => {
+ setScreen('account');
+ updateFlow();
+ });
+
+ const init = async () => {
+ loadLocal();
+ try {
+ const payload = await authFetch('/api/auth/get-session');
+ state.session = payload?.session ? payload : null;
+ } catch {
+ state.session = null;
+ }
+
+ updateFlow();
+ log('simulator initialized', {
+ hasSession: Boolean(state.session),
+ hasSavedDevice: Boolean(state.device),
+ });
+ };
+
+ init();