diff --git a/cmd/verstak-gui/bindings_bridge.go b/cmd/verstak-gui/bindings_bridge.go index 0a6729c..fc945ce 100644 --- a/cmd/verstak-gui/bindings_bridge.go +++ b/cmd/verstak-gui/bindings_bridge.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "verstak/internal/core/bridge" @@ -112,3 +113,59 @@ func bridgeToBrowser(ev bridge.Event) browser.Event { Note: ev.Note, } } + +// RestartBridge stops and restarts the bridge server with current config. +func (a *App) RestartBridge() error { + a.mu.Lock() + defer a.mu.Unlock() + + // Stop existing server + if a.bridge != nil { + a.bridge.Stop() + a.bridge = nil + } + + // Load config + appCfg, err := config.LoadAppConfig() + if err != nil || appCfg == nil { + appCfg = config.DefaultAppConfig() + } + + bc := a.bridgeConfig(appCfg) + if !bc.Enabled { + return nil + } + + handler := func(events []bridge.Event) { + be := make([]browser.Event, 0, len(events)) + for _, ev := range events { + be = append(be, bridgeToBrowser(ev)) + } + n, err := a.browser.InsertEvents(be) + if err != nil { + log.Printf("[bridge] store events: %v", err) + return + } + if n > 0 { + log.Printf("[bridge] stored %d/%d events", n, len(be)) + } + } + + srv := bridge.NewServer(bridge.Config{ + Port: bc.Port, + Secret: bc.Secret, + }, handler) + + port, err := srv.Start(bridge.Config{ + Port: bc.Port, + AutoGenPort: bc.AutoGenPort, + Secret: bc.Secret, + }) + if err != nil { + return fmt.Errorf("bridge restart: %w", err) + } + + a.bridge = srv + log.Printf("[bridge] restarted on port %d", port) + return nil +} diff --git a/cmd/verstak-gui/bindings_config.go b/cmd/verstak-gui/bindings_config.go index 1dd4a79..48ed148 100644 --- a/cmd/verstak-gui/bindings_config.go +++ b/cmd/verstak-gui/bindings_config.go @@ -284,8 +284,12 @@ func (a *App) initVault(vaultPath string) error { // Start auto-sync loop go a.autoSyncLoop() - // Start bridge server for browser extension integration. - a.startBridge(appCfg) + // Start bridge server for browser extension integration (if enabled). + if appCfg == nil || appCfg.Vault.Bridge.Enabled { + a.startBridge(appCfg) + } else { + log.Println("[bridge] disabled by config") + } return nil } diff --git a/extension-firefox/popup/popup.css b/extension-firefox/popup/popup.css index 0f0fec3..738e9d5 100644 --- a/extension-firefox/popup/popup.css +++ b/extension-firefox/popup/popup.css @@ -228,3 +228,94 @@ footer { opacity: 0.5; cursor: not-allowed; } + +/* Settings panel */ +#settings-panel { + padding-top: 4px; +} + +.settings-form { + background: var(--surface); + border-radius: var(--radius); + padding: 12px; + margin-bottom: 8px; +} + +.form-group { + margin-bottom: 10px; +} + +.form-group label { + display: block; + color: var(--text-dim); + font-size: 11px; + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.port-row { + display: flex; + align-items: center; + gap: 4px; +} + +.port-prefix { + color: var(--text-dim); + font-size: 12px; + font-family: monospace; +} + +#port-input { + width: 80px; + padding: 6px 8px; + background: var(--bg); + border: 1px solid var(--surface2); + border-radius: 4px; + color: var(--text); + font-size: 13px; + font-family: monospace; +} + +#port-input:focus { + outline: none; + border-color: var(--accent); +} + +.hint { + color: var(--text-dim); + font-size: 10px; + margin-top: 4px; + line-height: 1.3; +} + +.settings-actions { + display: flex; + gap: 6px; +} + +.settings-actions .btn { + flex: 1; +} + +.btn.primary { + background: var(--accent); +} + +.btn.primary:hover { + background: #0d8a7e; +} + +.port-status { + margin-top: 8px; + font-size: 11px; + min-height: 16px; +} + +.port-status.ok { + color: var(--success); +} + +.port-status.error { + color: var(--danger); +} diff --git a/extension-firefox/popup/popup.html b/extension-firefox/popup/popup.html index 3aba3bb..ca98d1c 100644 --- a/extension-firefox/popup/popup.html +++ b/extension-firefox/popup/popup.html @@ -8,45 +8,73 @@
-
-

Verstak Bridge

- -
+ +
+
+

Verstak Bridge

+ +
-
-
- Сервер - Недоступен -
-
- Очередь - -
-
- Порт - -
-
+
+
+ Сервер + Недоступен +
+
+ Очередь + +
+
+ Адрес + +
+
-
- -
+
+ +
-
-

Последние события

-
-

Нет событий

-
-
+
+

Последние события

+
+

Нет событий

+
+
-
- - -
+
+ + +
+
+ + +
diff --git a/extension-firefox/popup/popup.js b/extension-firefox/popup/popup.js index f993f01..629b8c0 100644 --- a/extension-firefox/popup/popup.js +++ b/extension-firefox/popup/popup.js @@ -1,22 +1,21 @@ // Verstak Bridge — Popup Script -// Shows connection status, queue count, and recent events. +// 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('config-btn').addEventListener('click', () => { - // For now, just show bridge config in console - chrome.storage.local.get(BRIDGE_KEY, (data) => { - console.log('[verstak] bridge config:', data[BRIDGE_KEY]); - }); - }); + 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() { @@ -37,7 +36,7 @@ function updateUI() { statusEl.textContent = 'Доступен'; statusEl.className = 'value online'; dotEl.className = 'dot online'; - portEl.textContent = config.port || '?'; + portEl.textContent = '127.0.0.1:' + (config.port || DEFAULT_PORT); } else { statusEl.textContent = 'Недоступен'; statusEl.className = 'value offline'; @@ -45,6 +44,12 @@ function updateUI() { 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); }); @@ -59,7 +64,6 @@ function renderRecent(events, reachable) { return; } - // Show last 5 const lastFive = events.slice(-5).reverse(); for (const ev of lastFive) { const item = document.createElement('div'); @@ -78,8 +82,7 @@ function forceSync() { btn.disabled = true; btn.textContent = '⏳ Отправка...'; - // Ask background to flush - chrome.runtime.sendMessage({ type: 'FORCE_FLUSH' }, (response) => { + chrome.runtime.sendMessage({ type: 'FORCE_FLUSH' }, () => { setTimeout(() => { updateUI(); btn.disabled = false; @@ -91,8 +94,6 @@ function forceSync() { function onToggle(e) { const enabled = e.target.checked; chrome.storage.local.set({ 'verstak_tracking_enabled': enabled }); - - // Notify background chrome.runtime.sendMessage({ type: 'SET_TRACKING', enabled }); } @@ -103,6 +104,92 @@ function restoreToggleState() { }); } +// --- 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 = '✗ Нет ответа (таймаут)'; + } else { + statusEl.textContent = '✗ Сервер недоступен'; + } + statusEl.className = 'port-status error'; + }); +} + // Listen for updates from background chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'UI_UPDATE') { diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 0f0fec3..738e9d5 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -228,3 +228,94 @@ footer { opacity: 0.5; cursor: not-allowed; } + +/* Settings panel */ +#settings-panel { + padding-top: 4px; +} + +.settings-form { + background: var(--surface); + border-radius: var(--radius); + padding: 12px; + margin-bottom: 8px; +} + +.form-group { + margin-bottom: 10px; +} + +.form-group label { + display: block; + color: var(--text-dim); + font-size: 11px; + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.port-row { + display: flex; + align-items: center; + gap: 4px; +} + +.port-prefix { + color: var(--text-dim); + font-size: 12px; + font-family: monospace; +} + +#port-input { + width: 80px; + padding: 6px 8px; + background: var(--bg); + border: 1px solid var(--surface2); + border-radius: 4px; + color: var(--text); + font-size: 13px; + font-family: monospace; +} + +#port-input:focus { + outline: none; + border-color: var(--accent); +} + +.hint { + color: var(--text-dim); + font-size: 10px; + margin-top: 4px; + line-height: 1.3; +} + +.settings-actions { + display: flex; + gap: 6px; +} + +.settings-actions .btn { + flex: 1; +} + +.btn.primary { + background: var(--accent); +} + +.btn.primary:hover { + background: #0d8a7e; +} + +.port-status { + margin-top: 8px; + font-size: 11px; + min-height: 16px; +} + +.port-status.ok { + color: var(--success); +} + +.port-status.error { + color: var(--danger); +} diff --git a/extension/popup/popup.html b/extension/popup/popup.html index 3aba3bb..ca98d1c 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -8,45 +8,73 @@
-
-

Verstak Bridge

- -
+ +
+
+

Verstak Bridge

+ +
-
-
- Сервер - Недоступен -
-
- Очередь - -
-
- Порт - -
-
+
+
+ Сервер + Недоступен +
+
+ Очередь + +
+
+ Адрес + +
+
-
- -
+
+ +
-
-

Последние события

-
-

Нет событий

-
-
+
+

Последние события

+
+

Нет событий

+
+
-
- - -
+
+ + +
+
+ + +
diff --git a/extension/popup/popup.js b/extension/popup/popup.js index f993f01..629b8c0 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -1,22 +1,21 @@ // Verstak Bridge — Popup Script -// Shows connection status, queue count, and recent events. +// 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('config-btn').addEventListener('click', () => { - // For now, just show bridge config in console - chrome.storage.local.get(BRIDGE_KEY, (data) => { - console.log('[verstak] bridge config:', data[BRIDGE_KEY]); - }); - }); + 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() { @@ -37,7 +36,7 @@ function updateUI() { statusEl.textContent = 'Доступен'; statusEl.className = 'value online'; dotEl.className = 'dot online'; - portEl.textContent = config.port || '?'; + portEl.textContent = '127.0.0.1:' + (config.port || DEFAULT_PORT); } else { statusEl.textContent = 'Недоступен'; statusEl.className = 'value offline'; @@ -45,6 +44,12 @@ function updateUI() { 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); }); @@ -59,7 +64,6 @@ function renderRecent(events, reachable) { return; } - // Show last 5 const lastFive = events.slice(-5).reverse(); for (const ev of lastFive) { const item = document.createElement('div'); @@ -78,8 +82,7 @@ function forceSync() { btn.disabled = true; btn.textContent = '⏳ Отправка...'; - // Ask background to flush - chrome.runtime.sendMessage({ type: 'FORCE_FLUSH' }, (response) => { + chrome.runtime.sendMessage({ type: 'FORCE_FLUSH' }, () => { setTimeout(() => { updateUI(); btn.disabled = false; @@ -91,8 +94,6 @@ function forceSync() { function onToggle(e) { const enabled = e.target.checked; chrome.storage.local.set({ 'verstak_tracking_enabled': enabled }); - - // Notify background chrome.runtime.sendMessage({ type: 'SET_TRACKING', enabled }); } @@ -103,6 +104,92 @@ function restoreToggleState() { }); } +// --- 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 = '✗ Нет ответа (таймаут)'; + } else { + statusEl.textContent = '✗ Сервер недоступен'; + } + statusEl.className = 'port-status error'; + }); +} + // Listen for updates from background chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'UI_UPDATE') { diff --git a/frontend/src/lib/SettingsBrowserBridge.svelte b/frontend/src/lib/SettingsBrowserBridge.svelte new file mode 100644 index 0000000..e760a1b --- /dev/null +++ b/frontend/src/lib/SettingsBrowserBridge.svelte @@ -0,0 +1,254 @@ + + +
+

Browser Bridge

+

+ Локальный HTTP-сервер для приёма событий от браузерного расширения Verstak Bridge. + Работает только на 127.0.0.1. +

+ + {#if statusMsg} +
+ {statusMsg} +
+ {/if} + +
+
+ +
+ +
+ +

По умолчанию: 9786. Требуется перезапуск сервера при изменении.

+
+ +
+
+ Статус + + {bridgeRunning ? 'Запущен' : 'Остановлен'} + +
+ {#if bridgeRunning && bridgeActualPort} +
+ Активный порт + 127.0.0.1:{bridgeActualPort} +
+ {/if} +
+
+ +
+ + {#if bridgeEnabled} + + {/if} +
+
+ + diff --git a/frontend/src/lib/SettingsSidebar.svelte b/frontend/src/lib/SettingsSidebar.svelte index 74bc4fd..c229cc4 100644 --- a/frontend/src/lib/SettingsSidebar.svelte +++ b/frontend/src/lib/SettingsSidebar.svelte @@ -13,6 +13,7 @@ { id: 'activity', label: t('settings.activity') }, { id: 'sync', label: t('settings.sync') }, { id: 'backup', label: t('settings.backup') }, + { id: 'browserBridge', label: 'Browser Bridge' }, ] function select(id) { @@ -57,6 +58,9 @@ + {:else if sec.id === 'browserBridge'} + + {/if} diff --git a/frontend/src/lib/SettingsWindow.svelte b/frontend/src/lib/SettingsWindow.svelte index 717a7a4..e325619 100644 --- a/frontend/src/lib/SettingsWindow.svelte +++ b/frontend/src/lib/SettingsWindow.svelte @@ -9,6 +9,7 @@ import SettingsActivity from './SettingsActivity.svelte' import SettingsSync from './SettingsSync.svelte' import SettingsBackup from './SettingsBackup.svelte' + import SettingsBrowserBridge from './SettingsBrowserBridge.svelte' export let onClose = null export let onSyncRefresh = null @@ -90,6 +91,8 @@ {:else if activeSection === 'backup'} + {:else if activeSection === 'browserBridge'} + {/if} diff --git a/internal/core/config/appconfig.go b/internal/core/config/appconfig.go index e6deb61..228ce6a 100644 --- a/internal/core/config/appconfig.go +++ b/internal/core/config/appconfig.go @@ -65,9 +65,10 @@ type SyncSettings struct { // BridgeConfig holds local HTTP bridge settings for browser extension. type BridgeConfig struct { - Port int `json:"port"` - Secret string `json:"secret,omitempty"` // shared secret for extension auth - AutoGenPort bool `json:"auto_gen_port,omitempty"` // pick random port if port taken + Enabled bool `json:"enabled,omitempty"` // enable/disable bridge server + Port int `json:"port"` // listen port (default 9786) + Secret string `json:"secret,omitempty"` // shared secret for extension auth + AutoGenPort bool `json:"auto_gen_port,omitempty"` // pick random port if port taken } func DefaultAppConfig() *AppConfig { @@ -79,6 +80,10 @@ func DefaultAppConfig() *AppConfig { EnabledPlugins: []string{}, Vault: VaultAppConfig{ FileWatcher: BoolPtr(true), + Bridge: BridgeConfig{ + Enabled: true, + Port: 9786, + }, }, } }