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:
2026-06-07 01:03:35 +08:00
parent 1cc0c407b1
commit 8cbc87cdad
12 changed files with 840 additions and 101 deletions
+91
View File
@@ -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);
}
+63 -35
View File
@@ -8,45 +8,73 @@
</head>
<body>
<div class="container">
<header>
<h1>Verstak Bridge</h1>
<span id="status-dot" class="dot offline"></span>
</header>
<!-- Main panel -->
<div id="main-panel">
<header>
<h1>Verstak Bridge</h1>
<span id="status-dot" class="dot offline"></span>
</header>
<section class="info">
<div class="row">
<span class="label">Сервер</span>
<span id="bridge-status" class="value offline">Недоступен</span>
</div>
<div class="row">
<span class="label">Очередь</span>
<span id="queue-count" class="value"></span>
</div>
<div class="row">
<span class="label">Порт</span>
<span id="bridge-port" class="value"></span>
</div>
</section>
<section class="info">
<div class="row">
<span class="label">Сервер</span>
<span id="bridge-status" class="value offline">Недоступен</span>
</div>
<div class="row">
<span class="label">Очередь</span>
<span id="queue-count" class="value"></span>
</div>
<div class="row">
<span class="label">Адрес</span>
<span id="bridge-port" class="value"></span>
</div>
</section>
<section class="controls">
<label class="toggle">
<input type="checkbox" id="track-toggle" checked>
<span class="slider"></span>
<span class="toggle-label">Отслеживать вкладки</span>
</label>
</section>
<section class="controls">
<label class="toggle">
<input type="checkbox" id="track-toggle" checked>
<span class="slider"></span>
<span class="toggle-label">Отслеживать вкладки</span>
</label>
</section>
<section class="recent">
<h2>Последние события</h2>
<div id="event-list">
<p class="empty">Нет событий</p>
</div>
</section>
<section class="recent">
<h2>Последние события</h2>
<div id="event-list">
<p class="empty">Нет событий</p>
</div>
</section>
<footer>
<button id="sync-btn" class="btn">🔄 Отправить сейчас</button>
<button id="config-btn" class="btn secondary"> Настройки</button>
</footer>
<footer>
<button id="sync-btn" class="btn">🔄 Отправить сейчас</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>
+100 -13
View File
@@ -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 = 'Порт: 102465535';
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 = 'Порт: 102465535';
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') {