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 @@
-
+
+
+
-
-
- Сервер
- Недоступен
-
-
- Очередь
- —
-
-
- Порт
- —
-
-
+
+
+ Сервер
+ Недоступен
+
+
+ Очередь
+ —
+
+
+ Адрес
+ —
+
+
-
+
-
+
-
+
+
+
+
+
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 @@
-
+
+
+
-
-
- Сервер
- Недоступен
-
-
- Очередь
- —
-
-
- Порт
- —
-
-
+
+
+ Сервер
+ Недоступен
+
+
+ Очередь
+ —
+
+
+ Адрес
+ —
+
+
-
+
-
+
-
+
+
+
+
+
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}
+
+
+
+
+
+
+
+
+
+
+ Статус
+
+ {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,
+ },
},
}
}