// Verstak Bridge — Background Service Worker // Tracks active tab changes and queues browser events. // Pushes events to the Verstak HTTP bridge when available. const STORAGE_KEY = 'verstak_queue'; const BRIDGE_KEY = 'verstak_bridge_config'; const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: true }; const FLUSH_INTERVAL_MS = 30_000; // flush every 30s const IDLE_RESET_MS = 60_000; // reset session after 60s idle // Session state let session = { url: '', title: '', domain: '', tabId: -1, startedAt: null, }; // Start tracking on install chrome.runtime.onInstalled.addListener(() => { setupAlarms(); setupListeners(); }); // Wake up on browser start chrome.runtime.onStartup.addListener(() => { setupAlarms(); setupListeners(); }); function setupAlarms() { chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 }); chrome.alarms.create('pingBridge', { periodInMinutes: 1 }); } function setupListeners() { // Track active tab changes chrome.tabs.onActivated.addListener(onTabActivated); chrome.tabs.onUpdated.addListener(onTabUpdated); chrome.windows.onFocusChanged.addListener(onWindowFocusChanged); // Listen for flush alarm chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'flushEvents') flushSession(); if (alarm.name === 'pingBridge') pingBridge(); }); } // Called when user switches tabs function onTabActivated(activeInfo) { flushSession(); startTracking(activeInfo.tabId); } // Called when a tab's URL or title changes function onTabUpdated(tabId, changeInfo, tab) { if (changeInfo.status === 'complete' && tab.active) { flushSession(); startTracking(tabId); } } // Called when window focus changes function onWindowFocusChanged(windowId) { if (windowId === chrome.windows.WINDOW_ID_NONE) { // Window lost focus — flush and stop session flushSession(); session = { url: '', title: '', domain: '', tabId: -1, startedAt: null }; } else { // Window gained focus — get active tab and start tracking chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (tabs && tabs.length > 0) { startTracking(tabs[0].id); } }); } } function startTracking(tabId) { chrome.tabs.get(tabId, (tab) => { if (chrome.runtime.lastError || !tab || !tab.url) return; if (isInternalPage(tab.url)) return; session = { url: tab.url, title: tab.title || '', domain: extractDomain(tab.url), tabId: tabId, startedAt: Date.now(), }; }); } function flushSession() { if (!session.url || !session.startedAt) return; const duration = Math.round((Date.now() - session.startedAt) / 1000); if (duration < 2) { // Skip very short visits session.startedAt = Date.now(); // reset timer return; } const event = { id: generateId(), type: 'page_visit', url: session.url, title: session.title, domain: session.domain, active_seconds: Math.min(duration, IDLE_RESET_MS / 1000), ts_start: new Date(session.startedAt).toISOString(), ts_end: new Date().toISOString(), }; queueEvent(event); session.startedAt = Date.now(); // reset for next segment } function queueEvent(event) { chrome.storage.local.get(STORAGE_KEY, (data) => { const queue = data[STORAGE_KEY] || []; queue.push(event); // Keep max 500 events in queue if (queue.length > 500) { queue.splice(0, queue.length - 500); } chrome.storage.local.set({ [STORAGE_KEY]: queue }, () => { // Immediately try to flush if we have enough events if (queue.length >= 5) { flushQueue(); } }); }); } function flushQueue() { chrome.storage.local.get([STORAGE_KEY, BRIDGE_KEY], (data) => { const queue = data[STORAGE_KEY] || []; if (queue.length === 0) return; const config = data[BRIDGE_KEY] || DEFAULT_CONFIG; const port = config.port || 9786; const secret = config.secret || ''; const payload = { version: 1, device_id: 'chrome-' + (config.deviceId || 'unknown'), events: queue, }; fetch(`http://127.0.0.1:${port}/api/events`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Verstak-Secret': secret, }, body: JSON.stringify(payload), }) .then((res) => { if (res.ok) { // Update recent events for (const ev of queue) { updateRecent(ev); } // Clear queue on success chrome.storage.local.set({ [STORAGE_KEY]: [] }); // Notify popup if open chrome.runtime.sendMessage({ type: 'UI_UPDATE' }).catch(() => {}); } else if (res.status === 401) { console.warn('[verstak] bridge auth failed, check secret'); } }) .catch((err) => { // Bridge not available — keep events in queue if (queue.length % 10 === 0) { console.debug('[verstak] bridge unavailable:', err.message); } }); }); } function pingBridge() { chrome.storage.local.get(BRIDGE_KEY, (data) => { const config = data[BRIDGE_KEY] || DEFAULT_CONFIG; const port = config.port || 9786; fetch(`http://127.0.0.1:${port}/api/ping`) .then((res) => res.json()) .then((info) => { chrome.storage.local.set({ [BRIDGE_KEY]: { ...config, ...info, bridgeReachable: true, lastPing: Date.now() } }); }) .catch(() => { chrome.storage.local.set({ [BRIDGE_KEY]: { ...config, bridgeReachable: false, lastPing: Date.now() } }); }); }); } // Listen for messages from popup chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === 'FORCE_FLUSH') { flushSession(); flushQueue(); sendResponse({ ok: true }); } if (msg.type === 'SET_TRACKING') { // Tracking is always on at the background level; // the popup just shows the setting. chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled }); sendResponse({ ok: true }); } }); // Update recent events from flushed queue entries. // This is called by flushQueue on successful delivery. function updateRecent(event) { chrome.storage.local.get(RECENT_KEY, (data) => { const recent = data[RECENT_KEY] || []; recent.push({ domain: event.domain, title: event.title, url: event.url, active_seconds: event.active_seconds, ts: new Date().toISOString(), }); // Keep last 50 if (recent.length > 50) { recent.splice(0, recent.length - 50); } chrome.storage.local.set({ [RECENT_KEY]: recent }); }); } const RECENT_KEY = 'verstak_recent'; // ... existing code continues below ... function extractDomain(url) { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; } } function isInternalPage(url) { const internal = [ 'chrome://', 'chrome-extension://', 'about:', 'edge://', 'file://', 'data:', 'blob:', 'view-source:', 'devtools:', ]; return internal.some((p) => url.startsWith(p)); } function generateId() { const ts = Date.now().toString(36); const rand = Math.random().toString(36).substring(2, 8); return `evt_${ts}_${rand}`; }