// Verstak Bridge — Popup Script // Shows connection status, queue count, recent events, and settings. const STORAGE_KEY = 'verstak_queue'; const BRIDGE_KEY = 'verstak_bridge_config'; const RECENT_KEY = 'verstak_recent'; const DEFAULT_PORT = 9786; document.addEventListener('DOMContentLoaded', () => { updateUI(); restoreToggleState(); restoreSettingsPanel(); document.getElementById('sync-btn').addEventListener('click', forceSync); document.getElementById('track-toggle').addEventListener('change', onToggle); document.getElementById('settings-btn').addEventListener('click', toggleSettings); document.getElementById('save-port-btn').addEventListener('click', savePort); document.getElementById('test-port-btn').addEventListener('click', testPort); }); function updateUI() { chrome.storage.local.get([STORAGE_KEY, BRIDGE_KEY, RECENT_KEY], (data) => { const queue = data[STORAGE_KEY] || []; const config = data[BRIDGE_KEY] || {}; const recent = data[RECENT_KEY] || []; // Queue count document.getElementById('queue-count').textContent = queue.length; // Bridge status const statusEl = document.getElementById('bridge-status'); const portEl = document.getElementById('bridge-port'); const dotEl = document.getElementById('status-dot'); if (config.bridgeReachable) { statusEl.textContent = 'Доступен'; statusEl.className = 'value online'; dotEl.className = 'dot online'; portEl.textContent = '127.0.0.1:' + (config.port || DEFAULT_PORT); } else { statusEl.textContent = 'Недоступен'; statusEl.className = 'value offline'; dotEl.className = 'dot offline'; portEl.textContent = config.lastPing ? '—' : 'проверка...'; } // Update settings panel port field const portInput = document.getElementById('port-input'); if (portInput && !portInput.dataset.userEdited) { portInput.value = config.port || DEFAULT_PORT; } // Recent events renderRecent(recent, config.bridgeReachable); }); } function renderRecent(events, reachable) { const list = document.getElementById('event-list'); list.innerHTML = ''; if (!events || events.length === 0) { list.innerHTML = '

Нет событий

'; return; } const lastFive = events.slice(-5).reverse(); for (const ev of lastFive) { const item = document.createElement('div'); item.className = 'event-item'; item.innerHTML = `
${escapeHtml(ev.domain || '?')}
${escapeHtml(truncate(ev.title || ev.url, 60))}
${ev.active_seconds || 0}с${reachable ? '' : ' (ожидает)'}
`; list.appendChild(item); } } function forceSync() { const btn = document.getElementById('sync-btn'); btn.disabled = true; btn.textContent = '⏳ Отправка...'; chrome.runtime.sendMessage({ type: 'FORCE_FLUSH' }, () => { setTimeout(() => { updateUI(); btn.disabled = false; btn.textContent = '🔄 Отправить сейчас'; }, 1000); }); } function onToggle(e) { const enabled = e.target.checked; chrome.storage.local.set({ 'verstak_tracking_enabled': enabled }); chrome.runtime.sendMessage({ type: 'SET_TRACKING', enabled }); } function restoreToggleState() { chrome.storage.local.get('verstak_tracking_enabled', (data) => { const enabled = data.verstak_tracking_enabled !== false; document.getElementById('track-toggle').checked = enabled; }); } // --- Settings panel --- function toggleSettings() { const panel = document.getElementById('settings-panel'); const main = document.getElementById('main-panel'); const btn = document.getElementById('settings-btn'); if (panel.style.display === 'none' || !panel.style.display) { panel.style.display = 'block'; main.style.display = 'none'; btn.textContent = '← Назад'; } else { panel.style.display = 'none'; main.style.display = 'block'; btn.textContent = '⚙'; } } function restoreSettingsPanel() { chrome.storage.local.get(BRIDGE_KEY, (data) => { const config = data[BRIDGE_KEY] || {}; const portInput = document.getElementById('port-input'); if (portInput) { portInput.value = config.port || DEFAULT_PORT; } }); } function savePort() { const portInput = document.getElementById('port-input'); const port = parseInt(portInput.value, 10); const statusEl = document.getElementById('port-status'); if (isNaN(port) || port < 1024 || port > 65535) { statusEl.textContent = 'Порт: 1024–65535'; statusEl.className = 'port-status error'; return; } chrome.storage.local.get(BRIDGE_KEY, (data) => { const config = data[BRIDGE_KEY] || {}; config.port = port; chrome.storage.local.set({ [BRIDGE_KEY]: config }, () => { statusEl.textContent = 'Сохранено'; statusEl.className = 'port-status ok'; // Trigger immediate ping with new port chrome.runtime.sendMessage({ type: 'FORCE_FLUSH' }); setTimeout(() => { updateUI(); statusEl.textContent = ''; }, 1500); }); }); } function testPort() { const portInput = document.getElementById('port-input'); const port = parseInt(portInput.value, 10); const statusEl = document.getElementById('port-status'); if (isNaN(port) || port < 1024 || port > 65535) { statusEl.textContent = 'Порт: 1024–65535'; statusEl.className = 'port-status error'; return; } statusEl.textContent = 'Проверка...'; statusEl.className = 'port-status'; fetch(`http://127.0.0.1:${port}/api/ping`, { signal: AbortSignal.timeout(3000) }) .then((res) => { if (res.ok) { statusEl.textContent = '✓ Сервер отвечает'; statusEl.className = 'port-status ok'; } else { statusEl.textContent = `✗ Ошибка ${res.status}`; statusEl.className = 'port-status error'; } }) .catch((err) => { if (err.name === 'TimeoutError' || err.name === 'AbortError') { statusEl.textContent = '✗ Нет ответа (таймаут). Возможно, порт занят другим приложением или Verstak не запущен'; } else { statusEl.textContent = '✗ Сервер недоступен'; } statusEl.className = 'port-status error'; }); } // Listen for updates from background chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'UI_UPDATE') { updateUI(); } }); // --- Helpers --- function truncate(s, n) { if (!s) return ''; return s.length > n ? s.substring(0, n) + '...' : s; } function escapeHtml(s) { if (!s) return ''; return s .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }