// Background service worker — detects when the active tab is a NewBook // booking page and shows a badge, so staff notice the extension has // something to offer before even opening the popup. Tab-update/activated/ // history-state listeners and the booking-ID regex are adapted directly from // chrome-newbook-assistant/background.js (~/laptop-archive) — that pattern // is already proven working against real NewBook navigation (including its // SPA-style in-page routing, hence the webNavigation listener), not // reinvented here. See project_checkin memory's "Standalone push extension". // Confirmed 2026-09-01 by the user directly (real NewBook page URLs): // bookings_view/, bookings_edit/, bookings_checkin/. const BOOKING_URL_RE = /\/bookings_(?:view|edit|checkin)\/(\d+)/i; const NEWBOOK_HOSTS = ['appeu.newbook.cloud', 'login.newbook.cloud']; function isNewBookUrl(url) { try { const u = new URL(url); return NEWBOOK_HOSTS.some(h => u.hostname === h); } catch { return false; } } async function handleTabUpdate(tabId, url) { if (!url || !isNewBookUrl(url)) { await chrome.action.setBadgeText({ tabId, text: '' }); return; } const match = url.match(BOOKING_URL_RE); if (match) { const bookingId = match[1]; await chrome.storage.session.set({ [`bookingId:${tabId}`]: bookingId }); await chrome.action.setBadgeText({ tabId, text: '→' }); // → await chrome.action.setBadgeBackgroundColor({ tabId, color: '#be123c' }); await chrome.action.setTitle({ tabId, title: `Send booking #${bookingId} to a check-in tablet` }); } else { await chrome.storage.session.remove(`bookingId:${tabId}`); await chrome.action.setBadgeText({ tabId, text: '' }); await chrome.action.setTitle({ tabId, title: 'Send to Check-In Tablet' }); } } chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' && tab.url) handleTabUpdate(tabId, tab.url); }); chrome.tabs.onActivated.addListener(async ({ tabId }) => { const tab = await chrome.tabs.get(tabId); if (tab.url) handleTabUpdate(tabId, tab.url); }); // NewBook's booking view is (per chrome-newbook-assistant's own findings) an // SPA that can change URL without a full page load — this catches that case, // same as the assistant already has to. chrome.webNavigation.onHistoryStateUpdated.addListener(details => { handleTabUpdate(details.tabId, details.url); }); // ── Messages from content-script.js (bookings_lists_checkin row buttons) ─── // Content scripts route their actual network calls through here rather than // fetching directly — keeps the baseUrl/apiKey read-and-fetch logic in one // place instead of duplicated across popup.js and content-script.js. chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.action === 'tcpListDevices') { listDevices().then(sendResponse); return true; // keep the message channel open for the async response } if (message.action === 'tcpPush') { pushBooking(message.bookingId, message.deviceId).then(sendResponse); return true; } return false; }); async function getConfig() { const { baseUrl, apiKey } = await chrome.storage.local.get(['baseUrl', 'apiKey']); return { baseUrl, apiKey }; } async function listDevices() { const { baseUrl, apiKey } = await getConfig(); if (!baseUrl || !apiKey) return { ok: false, error: 'not configured' }; try { const res = await fetch(`${baseUrl}/api/staff/devices`, { headers: { Authorization: `Bearer ${apiKey}` }, }); const body = await res.json().catch(() => ({})); if (!res.ok) return { ok: false, error: body.error || `HTTP ${res.status}` }; return { ok: true, devices: body }; } catch (err) { return { ok: false, error: err.message }; } } async function pushBooking(bookingId, deviceId) { const { baseUrl, apiKey } = await getConfig(); if (!baseUrl || !apiKey) return { ok: false, error: 'not configured' }; try { const res = await fetch(`${baseUrl}/api/staff/push`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ booking_id: bookingId, device_id: deviceId }), }); const body = await res.json().catch(() => ({})); if (!res.ok) return { ok: false, error: body.error || `HTTP ${res.status}` }; return { ok: true }; } catch (err) { return { ok: false, error: err.message }; } }