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
@@ -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>
+4
View File
@@ -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>
+3
View File
@@ -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>