feat(push): add phase7 offline push queue, worker, APIs, and simulator inbox

This commit is contained in:
2026-01-24 15:20:00 +00:00
parent bccc049fc3
commit 6d6c77f77e
9 changed files with 392 additions and 6 deletions

View File

@@ -134,6 +134,9 @@
<option value="camera">camera</option>
</select>
<label>Push Token (simulated)</label>
<input id="pushToken" placeholder="optional push token for offline delivery" />
<div class="row">
<button id="registerBtn">Register Device</button>
<button id="loadSavedBtn" class="alt">Load Saved</button>
@@ -189,6 +192,7 @@
lastMotionEventId: null,
lastStreamSessionId: null,
lastRecordingId: null,
latestPushNotificationId: null,
};
const $ = (id) => document.getElementById(id);
@@ -208,7 +212,11 @@
$('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 },
{
lastMotionEventId: state.lastMotionEventId,
lastRecordingId: state.lastRecordingId,
latestPushNotificationId: state.latestPushNotificationId,
},
null,
2,
);
@@ -338,6 +346,7 @@
name: name || undefined,
platform: 'web',
appVersion: 'sim-1',
pushToken: $('pushToken').value.trim() || undefined,
}),
});
@@ -361,6 +370,21 @@
log('loaded saved device', parsed);
});
$('loadSavedBtn').addEventListener('click', async () => {
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 });
} catch (error) {
log('push token update failed', { error: error.message });
}
});
$('connectBtn').addEventListener('click', () => {
try {
connectSocket();
@@ -534,6 +558,55 @@
}
});
const pushPanel = document.createElement('section');
pushPanel.className = 'panel';
pushPanel.style.marginTop = '16px';
pushPanel.innerHTML = `
<h2>Push Inbox (Offline Fallback)</h2>
<div class="row">
<button id="dispatchPushWorkerBtn" class="alt">Dispatch Push Worker</button>
<button id="pollPushInboxBtn" class="alt">Poll Push Inbox</button>
</div>
<button id="markLatestPushReadBtn" class="alt">Mark Latest Push Read</button>
`;
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 });
}
});
$('pollPushInboxBtn').addEventListener('click', async () => {
try {
const payload = await deviceFetch('/push-notifications/me');
const latest = payload.notifications?.[0];
if (latest) {
state.latestPushNotificationId = latest.id;
}
render();
log('push inbox', payload);
} catch (error) {
log('poll push inbox failed', { error: error.message });
}
});
$('markLatestPushReadBtn').addEventListener('click', async () => {
try {
if (!state.latestPushNotificationId) throw new Error('No push notification selected');
const payload = await deviceFetch(`/push-notifications/${state.latestPushNotificationId}/read`, {
method: 'POST',
body: JSON.stringify({}),
});
log('push marked read', payload);
} catch (error) {
log('mark push read failed', { error: error.message });
}
});
render();
</script>
</body>