feat: настройки Browser Bridge в Verstak и extension
Verstak (GUI): - SettingsBrowserBridge.svelte — новая секция в Settings: toggle вкл/выкл сервера, поле порта (default 9786), статус (Запущен/Остановлен), кнопки Сохранить/Перезапустить - SettingsWindow + SettingsSidebar — подключена секция browserBridge - BridgeConfig: добавлено поле Enabled (default true) - RestartBridge() — новый биндинг для перезапуска сервера - initVault: проверяет bc.Enabled перед запуском bridge Extension (Chrome + Firefox): - popup: панель настроек с полем порта (default 9786) - кнопка «Проверить» — fetch /api/ping с таймаутом 3с - кнопка «Сохранить» — сохраняет port в chrome.storage.local - статус соединения: ✓ Сервер отвечает / ✗ Недоступен / ✗ Таймаут - оба расширения работают только с 127.0.0.1
This commit is contained in:
parent
1cc0c407b1
commit
8cbc87cdad
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Main panel -->
|
||||
<div id="main-panel">
|
||||
<header>
|
||||
<h1>Verstak Bridge</h1>
|
||||
<span id="status-dot" class="dot offline"></span>
|
||||
|
|
@ -23,7 +25,7 @@
|
|||
<span id="queue-count" class="value">—</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Порт</span>
|
||||
<span class="label">Адрес</span>
|
||||
<span id="bridge-port" class="value">—</span>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -45,9 +47,35 @@
|
|||
|
||||
<footer>
|
||||
<button id="sync-btn" class="btn">🔄 Отправить сейчас</button>
|
||||
<button id="config-btn" class="btn secondary">⚙ Настройки</button>
|
||||
<button id="settings-btn" class="btn secondary">⚙</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Settings panel (hidden by default) -->
|
||||
<div id="settings-panel" style="display: none;">
|
||||
<header>
|
||||
<h1>Настройки</h1>
|
||||
</header>
|
||||
|
||||
<section class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="port-input">Порт сервера</label>
|
||||
<div class="port-row">
|
||||
<span class="port-prefix">127.0.0.1:</span>
|
||||
<input type="number" id="port-input" min="1024" max="65535" value="9786" />
|
||||
</div>
|
||||
<p class="hint">По умолчанию: 9786. Изменения вступят в силу при следующей отправке событий.</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button id="test-port-btn" class="btn">Проверить</button>
|
||||
<button id="save-port-btn" class="btn primary">Сохранить</button>
|
||||
</div>
|
||||
|
||||
<div id="port-status" class="port-status"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Main panel -->
|
||||
<div id="main-panel">
|
||||
<header>
|
||||
<h1>Verstak Bridge</h1>
|
||||
<span id="status-dot" class="dot offline"></span>
|
||||
|
|
@ -23,7 +25,7 @@
|
|||
<span id="queue-count" class="value">—</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Порт</span>
|
||||
<span class="label">Адрес</span>
|
||||
<span id="bridge-port" class="value">—</span>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -45,9 +47,35 @@
|
|||
|
||||
<footer>
|
||||
<button id="sync-btn" class="btn">🔄 Отправить сейчас</button>
|
||||
<button id="config-btn" class="btn secondary">⚙ Настройки</button>
|
||||
<button id="settings-btn" class="btn secondary">⚙</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Settings panel (hidden by default) -->
|
||||
<div id="settings-panel" style="display: none;">
|
||||
<header>
|
||||
<h1>Настройки</h1>
|
||||
</header>
|
||||
|
||||
<section class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="port-input">Порт сервера</label>
|
||||
<div class="port-row">
|
||||
<span class="port-prefix">127.0.0.1:</span>
|
||||
<input type="number" id="port-input" min="1024" max="65535" value="9786" />
|
||||
</div>
|
||||
<p class="hint">По умолчанию: 9786. Изменения вступят в силу при следующей отправке событий.</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button id="test-port-btn" class="btn">Проверить</button>
|
||||
<button id="save-port-btn" class="btn primary">Сохранить</button>
|
||||
</div>
|
||||
|
||||
<div id="port-status" class="port-status"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
<script>
|
||||
import { t } from './i18n'
|
||||
|
||||
export let appConfig = null
|
||||
|
||||
let bridgeEnabled = false
|
||||
let bridgePort = 9786
|
||||
let bridgeRunning = false
|
||||
let bridgeActualPort = 0
|
||||
let saving = false
|
||||
let statusMsg = ''
|
||||
let statusKind = '' // 'ok' | 'error' | ''
|
||||
|
||||
// Load current bridge state from config
|
||||
function loadFromConfig() {
|
||||
if (appConfig && appConfig.vault && appConfig.vault.bridge) {
|
||||
const bc = appConfig.vault.bridge
|
||||
bridgeEnabled = bc.enabled !== false
|
||||
bridgePort = bc.port || 9786
|
||||
}
|
||||
}
|
||||
|
||||
// Also fetch live bridge status from backend
|
||||
async function fetchBridgeStatus() {
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
const info = await window['go']['main']['App']['BridgeInfo']()
|
||||
bridgeRunning = info.running || false
|
||||
bridgeActualPort = info.port || 0
|
||||
}
|
||||
} catch (e) {
|
||||
bridgeRunning = false
|
||||
bridgeActualPort = 0
|
||||
}
|
||||
}
|
||||
|
||||
loadFromConfig()
|
||||
fetchBridgeStatus()
|
||||
|
||||
async function save() {
|
||||
saving = true
|
||||
statusMsg = ''
|
||||
statusKind = ''
|
||||
try {
|
||||
// Update config
|
||||
if (!appConfig) appConfig = {}
|
||||
if (!appConfig.vault) appConfig.vault = {}
|
||||
if (!appConfig.vault.bridge) appConfig.vault.bridge = {}
|
||||
appConfig.vault.bridge.enabled = bridgeEnabled
|
||||
appConfig.vault.bridge.port = bridgePort
|
||||
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
await window['go']['main']['App']['SaveAppConfig'](appConfig)
|
||||
}
|
||||
statusMsg = 'saved'
|
||||
statusKind = 'ok'
|
||||
// Refresh live status
|
||||
await fetchBridgeStatus()
|
||||
} catch (e) {
|
||||
statusMsg = String(e)
|
||||
statusKind = 'error'
|
||||
}
|
||||
saving = false
|
||||
}
|
||||
|
||||
async function restartBridge() {
|
||||
saving = true
|
||||
statusMsg = ''
|
||||
statusKind = ''
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
await window['go']['main']['App']['RestartBridge']()
|
||||
}
|
||||
statusMsg = 'bridge restarted'
|
||||
statusKind = 'ok'
|
||||
await fetchBridgeStatus()
|
||||
} catch (e) {
|
||||
statusMsg = String(e)
|
||||
statusKind = 'error'
|
||||
}
|
||||
saving = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Browser Bridge</h2>
|
||||
<p class="section-desc">
|
||||
Локальный HTTP-сервер для приёма событий от браузерного расширения Verstak Bridge.
|
||||
Работает только на 127.0.0.1.
|
||||
</p>
|
||||
|
||||
{#if statusMsg}
|
||||
<div class="status-msg" class:ok={statusKind === 'ok'} class:error={statusKind === 'error'}>
|
||||
{statusMsg}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" bind:checked={bridgeEnabled} />
|
||||
<span class="toggle-text">Включить Browser Bridge</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<span class="label-text">Порт</span>
|
||||
<input type="number" bind:value={bridgePort} min="1024" max="65535" disabled={!bridgeEnabled} />
|
||||
</label>
|
||||
<p class="field-hint">По умолчанию: 9786. Требуется перезапуск сервера при изменении.</p>
|
||||
</div>
|
||||
|
||||
<div class="bridge-status">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Статус</span>
|
||||
<span class="info-value" class:status-ok={bridgeRunning} class:status-off={!bridgeRunning}>
|
||||
{bridgeRunning ? 'Запущен' : 'Остановлен'}
|
||||
</span>
|
||||
</div>
|
||||
{#if bridgeRunning && bridgeActualPort}
|
||||
<div class="info-row">
|
||||
<span class="info-label">Активный порт</span>
|
||||
<span class="info-value mono">127.0.0.1:{bridgeActualPort}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" on:click={save} disabled={saving}>
|
||||
Сохранить
|
||||
</button>
|
||||
{#if bridgeEnabled}
|
||||
<button class="btn" on:click={restartBridge} disabled={saving}>
|
||||
Перезапустить сервер
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.settings-section {
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
.settings-section h2 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text, #e0e0e0);
|
||||
}
|
||||
.section-desc {
|
||||
color: var(--text-dim, #888);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.settings-card {
|
||||
background: var(--surface-alt, #1e1e30);
|
||||
border: 1px solid var(--border, #2a2a3e);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.label-text {
|
||||
display: block;
|
||||
color: var(--text-dim, #888);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 120px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--bg, #13131f);
|
||||
border: 1px solid var(--border, #2a2a3e);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #e0e0e0);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
input[type="number"]:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.field-hint {
|
||||
color: var(--text-dim, #666);
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-text {
|
||||
color: var(--text, #e0e0e0);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.bridge-status {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border, #2a2a3e);
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
padding: 0.3rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.info-label {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
color: var(--text-dim, #888);
|
||||
}
|
||||
.info-value {
|
||||
color: var(--text, #e0e0e0);
|
||||
}
|
||||
.info-value.mono {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.status-ok {
|
||||
color: #34d399;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-off {
|
||||
color: #888;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.status-msg {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.status-msg.ok {
|
||||
background: rgba(52, 211, 153, 0.1);
|
||||
border: 1px solid rgba(52, 211, 153, 0.3);
|
||||
color: #34d399;
|
||||
}
|
||||
.status-msg.error {
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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 @@
|
|||
<path d="M5 4h12l2 2v14H5z"/>
|
||||
<path d="M8 4v6h8V4"/>
|
||||
<path d="M8 20v-6h8v6"/>
|
||||
{:else if sec.id === 'browserBridge'}
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
{/if}
|
||||
</svg>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<SettingsSync onRefresh={handleSyncRefresh} />
|
||||
{:else if activeSection === 'backup'}
|
||||
<SettingsBackup />
|
||||
{:else if activeSection === 'browserBridge'}
|
||||
<SettingsBrowserBridge {appConfig} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ type SyncSettings struct {
|
|||
|
||||
// BridgeConfig holds local HTTP bridge settings for browser extension.
|
||||
type BridgeConfig struct {
|
||||
Port int `json:"port"`
|
||||
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
|
||||
}
|
||||
|
|
@ -79,6 +80,10 @@ func DefaultAppConfig() *AppConfig {
|
|||
EnabledPlugins: []string{},
|
||||
Vault: VaultAppConfig{
|
||||
FileWatcher: BoolPtr(true),
|
||||
Bridge: BridgeConfig{
|
||||
Enabled: true,
|
||||
Port: 9786,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue