fix: исправление 6 пунктов из ревью
Critical: - bridge: AutoGenPort=false по умолчанию, не генерируем secret если пустой → extension и bridge совпадают на port 9786 и empty secret - bridgeConfig: убрана авто-генерация secret, убран secret из BridgeInfo High: - extension/background.js + extension-firefox/background.js: все chrome.* listeners вынесены в global scope (не внутри onInstalled/onStartup) → MV3 service worker корректно перезапускается - UI: acceptBrowserEvent вызывает AcceptBrowserEvent, attachBrowserEvent вызывает AttachBrowserEventToNode (к текущему selectedNode), а не DismissBrowserEvent - watcher: при Create проверяется isUnderVault(absPath, vaultRoot) — если файл уже в vault, используется AddExternal вместо CopyIntoVault → нет дублирования файлов с timestamp-суффиксом Medium: - bridge.Event: добавлено поле DeviceID, handleEvents обогащает events из batch.DeviceID → device_id сохраняется в DB как chrome-*/firefox-*, а не evt_* - config: FileWatcher изменён на *bool — nil означает default true, false = явно выключено → старые config.json без поля file_watcher получают true
This commit is contained in:
parent
b676ac675a
commit
1cc0c407b1
|
|
@ -44,13 +44,10 @@ func (a *App) startBridge(appCfg *config.AppConfig) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the actual port and secret back to config if auto-generated.
|
// Save the actual port back to config if auto-generated.
|
||||||
if bc.AutoGenPort {
|
if bc.AutoGenPort {
|
||||||
bc.Port = port
|
bc.Port = port
|
||||||
}
|
}
|
||||||
if bc.Secret == "" {
|
|
||||||
bc.Secret = srv.Secret()
|
|
||||||
}
|
|
||||||
a.saveBridgeConfig(appCfg, bc)
|
a.saveBridgeConfig(appCfg, bc)
|
||||||
|
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
|
|
@ -58,20 +55,14 @@ func (a *App) startBridge(appCfg *config.AppConfig) {
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// bridgeConfig extracts bridge config from app config, generating defaults if needed.
|
// bridgeConfig extracts bridge config from app config.
|
||||||
func (a *App) bridgeConfig(appCfg *config.AppConfig) *config.BridgeConfig {
|
func (a *App) bridgeConfig(appCfg *config.AppConfig) *config.BridgeConfig {
|
||||||
if appCfg != nil && appCfg.Vault.Bridge.Port != 0 {
|
if appCfg != nil && appCfg.Vault.Bridge.Port != 0 {
|
||||||
bc := &appCfg.Vault.Bridge
|
return &appCfg.Vault.Bridge
|
||||||
// If secret is empty, generate one on first run
|
|
||||||
if bc.Secret == "" {
|
|
||||||
bc.Secret = bridge.GenerateSecret()
|
|
||||||
}
|
|
||||||
return bc
|
|
||||||
}
|
}
|
||||||
return &config.BridgeConfig{
|
return &config.BridgeConfig{
|
||||||
Port: 9786,
|
Port: 9786,
|
||||||
AutoGenPort: true,
|
AutoGenPort: false,
|
||||||
Secret: bridge.GenerateSecret(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,20 +91,15 @@ func (a *App) BridgeInfo() map[string]interface{} {
|
||||||
if a.bridge != nil {
|
if a.bridge != nil {
|
||||||
info["running"] = a.bridge.Running()
|
info["running"] = a.bridge.Running()
|
||||||
info["port"] = a.bridge.Port()
|
info["port"] = a.bridge.Port()
|
||||||
info["secret"] = a.bridge.Secret()
|
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
// bridgeToBrowser converts a bridge.Event to a browser.Event.
|
// bridgeToBrowser converts a bridge.Event to a browser.Event.
|
||||||
func bridgeToBrowser(ev bridge.Event) browser.Event {
|
func bridgeToBrowser(ev bridge.Event) browser.Event {
|
||||||
deviceID := ev.ID
|
|
||||||
if idx := indexOf(ev.ID, "_"); idx > 0 {
|
|
||||||
deviceID = ev.ID[:idx]
|
|
||||||
}
|
|
||||||
return browser.Event{
|
return browser.Event{
|
||||||
ID: ev.ID,
|
ID: ev.ID,
|
||||||
DeviceID: deviceID,
|
DeviceID: ev.DeviceID,
|
||||||
Type: ev.Type,
|
Type: ev.Type,
|
||||||
URL: ev.URL,
|
URL: ev.URL,
|
||||||
Title: ev.Title,
|
Title: ev.Title,
|
||||||
|
|
@ -126,12 +112,3 @@ func bridgeToBrowser(ev bridge.Event) browser.Event {
|
||||||
Note: ev.Note,
|
Note: ev.Note,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexOf(s string, sub string) int {
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if string(s[i]) == sub {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -237,8 +237,8 @@ func (a *App) initVault(vaultPath string) error {
|
||||||
// Determine if real-time watching is enabled.
|
// Determine if real-time watching is enabled.
|
||||||
// Priority: CLI --no-watcher > env VERSTAK_NO_WATCHER > config file > default (true)
|
// Priority: CLI --no-watcher > env VERSTAK_NO_WATCHER > config file > default (true)
|
||||||
fileWatcherEnabled := true
|
fileWatcherEnabled := true
|
||||||
if appCfg != nil {
|
if appCfg != nil && appCfg.Vault.FileWatcher != nil {
|
||||||
fileWatcherEnabled = appCfg.Vault.FileWatcher
|
fileWatcherEnabled = *appCfg.Vault.FileWatcher
|
||||||
}
|
}
|
||||||
// Env override
|
// Env override
|
||||||
if os.Getenv("VERSTAK_NO_WATCHER") == "1" {
|
if os.Getenv("VERSTAK_NO_WATCHER") == "1" {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func (a *App) ToggleFileWatcher(enable bool) error {
|
||||||
if err != nil || cfg == nil {
|
if err != nil || cfg == nil {
|
||||||
return fmt.Errorf("config: %w", err)
|
return fmt.Errorf("config: %w", err)
|
||||||
}
|
}
|
||||||
cfg.Vault.FileWatcher = enable
|
cfg.Vault.FileWatcher = config.BoolPtr(enable)
|
||||||
if err := config.SaveAppConfig(cfg); err != nil {
|
if err := config.SaveAppConfig(cfg); err != nil {
|
||||||
return fmt.Errorf("save config: %w", err)
|
return fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// Verstak Bridge for Firefox — Background Service Worker
|
// Verstak Bridge for Firefox — Background Service Worker
|
||||||
// Uses browser.* WebExtensions API with chrome.* polyfill layer.
|
// Uses browser.* WebExtensions API with chrome.* polyfill layer.
|
||||||
// Functionally identical to the Chrome version.
|
//
|
||||||
|
// IMPORTANT: All event listeners are registered at global scope,
|
||||||
|
// NOT inside onInstalled/onStartup. MV3 service workers can be restarted at
|
||||||
|
// any time; listeners inside lifecycle hooks are lost on restart.
|
||||||
|
|
||||||
// Firefox uses browser.* namespace; provide chrome.* aliases for shared code.
|
// Firefox uses browser.* namespace; provide chrome.* aliases for shared code.
|
||||||
if (typeof browser !== 'undefined' && typeof chrome === 'undefined') {
|
if (typeof browser !== 'undefined' && typeof chrome === 'undefined') {
|
||||||
|
|
@ -10,7 +13,7 @@ if (typeof browser !== 'undefined' && typeof chrome === 'undefined') {
|
||||||
const STORAGE_KEY = 'verstak_queue';
|
const STORAGE_KEY = 'verstak_queue';
|
||||||
const BRIDGE_KEY = 'verstak_bridge_config';
|
const BRIDGE_KEY = 'verstak_bridge_config';
|
||||||
const RECENT_KEY = 'verstak_recent';
|
const RECENT_KEY = 'verstak_recent';
|
||||||
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: true };
|
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: false };
|
||||||
const DEFAULT_DEVICE_ID = generateDeviceId();
|
const DEFAULT_DEVICE_ID = generateDeviceId();
|
||||||
|
|
||||||
// Session state
|
// Session state
|
||||||
|
|
@ -22,11 +25,10 @@ let session = {
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Global event listeners (registered once at script load) ---
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
setupAlarms();
|
setupAlarms();
|
||||||
setupListeners();
|
|
||||||
// Generate a stable device ID on first install
|
// Generate a stable device ID on first install
|
||||||
chrome.storage.local.get(BRIDGE_KEY, (data) => {
|
chrome.storage.local.get(BRIDGE_KEY, (data) => {
|
||||||
const cfg = data[BRIDGE_KEY] || {};
|
const cfg = data[BRIDGE_KEY] || {};
|
||||||
|
|
@ -38,38 +40,40 @@ chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
|
||||||
chrome.runtime.onStartup.addListener(() => {
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
setupAlarms();
|
setupAlarms();
|
||||||
setupListeners();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tab tracking — global scope
|
||||||
|
chrome.tabs.onActivated.addListener(onTabActivated);
|
||||||
|
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||||
|
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
||||||
|
|
||||||
|
// Alarm handler — global scope
|
||||||
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
|
if (alarm.name === 'flushEvents') flushSession();
|
||||||
|
if (alarm.name === 'pingBridge') pingBridge();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message handler — global scope
|
||||||
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
if (msg.type === 'FORCE_FLUSH') {
|
||||||
|
flushSession();
|
||||||
|
flushQueue();
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
}
|
||||||
|
if (msg.type === 'SET_TRACKING') {
|
||||||
|
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Alarm setup ---
|
||||||
|
|
||||||
function setupAlarms() {
|
function setupAlarms() {
|
||||||
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
||||||
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupListeners() {
|
|
||||||
chrome.tabs.onActivated.addListener(onTabActivated);
|
|
||||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
|
||||||
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
||||||
if (alarm.name === 'flushEvents') flushSession();
|
|
||||||
if (alarm.name === 'pingBridge') pingBridge();
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
||||||
if (msg.type === 'FORCE_FLUSH') {
|
|
||||||
flushSession();
|
|
||||||
flushQueue();
|
|
||||||
sendResponse({ ok: true });
|
|
||||||
}
|
|
||||||
if (msg.type === 'SET_TRACKING') {
|
|
||||||
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
|
||||||
sendResponse({ ok: true });
|
|
||||||
}
|
|
||||||
return true; // keep channel open for async response
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Tab tracking ---
|
// --- Tab tracking ---
|
||||||
|
|
||||||
function onTabActivated(activeInfo) {
|
function onTabActivated(activeInfo) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
// Verstak Bridge — Background Service Worker
|
// Verstak Bridge — Background Service Worker (Chrome)
|
||||||
// Tracks active tab changes and queues browser events.
|
// Tracks active tab changes and queues browser events.
|
||||||
// Pushes events to the Verstak HTTP bridge when available.
|
// Pushes events to the Verstak HTTP bridge when available.
|
||||||
|
//
|
||||||
|
// IMPORTANT: All chrome.* event listeners are registered at global scope,
|
||||||
|
// NOT inside onInstalled/onStartup. MV3 service workers can be restarted at
|
||||||
|
// any time; listeners inside lifecycle hooks are lost on restart.
|
||||||
|
// See: https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/events
|
||||||
|
|
||||||
const STORAGE_KEY = 'verstak_queue';
|
const STORAGE_KEY = 'verstak_queue';
|
||||||
const BRIDGE_KEY = 'verstak_bridge_config';
|
const BRIDGE_KEY = 'verstak_bridge_config';
|
||||||
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: true };
|
const RECENT_KEY = 'verstak_recent';
|
||||||
const FLUSH_INTERVAL_MS = 30_000; // flush every 30s
|
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: false };
|
||||||
const IDLE_RESET_MS = 60_000; // reset session after 60s idle
|
const IDLE_RESET_MS = 60_000;
|
||||||
|
|
||||||
// Session state
|
// Session state
|
||||||
let session = {
|
let session = {
|
||||||
|
|
@ -17,43 +22,55 @@ let session = {
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start tracking on install
|
// --- Global event listeners (registered once at script load) ---
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
setupAlarms();
|
setupAlarms();
|
||||||
setupListeners();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wake up on browser start
|
|
||||||
chrome.runtime.onStartup.addListener(() => {
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
setupAlarms();
|
setupAlarms();
|
||||||
setupListeners();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tab tracking — global scope
|
||||||
|
chrome.tabs.onActivated.addListener(onTabActivated);
|
||||||
|
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||||
|
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
||||||
|
|
||||||
|
// Alarm handler — global scope
|
||||||
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
|
if (alarm.name === 'flushEvents') flushSession();
|
||||||
|
if (alarm.name === 'pingBridge') pingBridge();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message handler — global scope
|
||||||
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
if (msg.type === 'FORCE_FLUSH') {
|
||||||
|
flushSession();
|
||||||
|
flushQueue();
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
}
|
||||||
|
if (msg.type === 'SET_TRACKING') {
|
||||||
|
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Alarm setup ---
|
||||||
|
|
||||||
function setupAlarms() {
|
function setupAlarms() {
|
||||||
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
||||||
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupListeners() {
|
// --- Tab tracking ---
|
||||||
// Track active tab changes
|
|
||||||
chrome.tabs.onActivated.addListener(onTabActivated);
|
|
||||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
|
||||||
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
|
||||||
|
|
||||||
// Listen for flush alarm
|
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
||||||
if (alarm.name === 'flushEvents') flushSession();
|
|
||||||
if (alarm.name === 'pingBridge') pingBridge();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when user switches tabs
|
|
||||||
function onTabActivated(activeInfo) {
|
function onTabActivated(activeInfo) {
|
||||||
flushSession();
|
flushSession();
|
||||||
startTracking(activeInfo.tabId);
|
startTracking(activeInfo.tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when a tab's URL or title changes
|
|
||||||
function onTabUpdated(tabId, changeInfo, tab) {
|
function onTabUpdated(tabId, changeInfo, tab) {
|
||||||
if (changeInfo.status === 'complete' && tab.active) {
|
if (changeInfo.status === 'complete' && tab.active) {
|
||||||
flushSession();
|
flushSession();
|
||||||
|
|
@ -61,14 +78,11 @@ function onTabUpdated(tabId, changeInfo, tab) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when window focus changes
|
|
||||||
function onWindowFocusChanged(windowId) {
|
function onWindowFocusChanged(windowId) {
|
||||||
if (windowId === chrome.windows.WINDOW_ID_NONE) {
|
if (windowId === chrome.windows.WINDOW_ID_NONE) {
|
||||||
// Window lost focus — flush and stop session
|
|
||||||
flushSession();
|
flushSession();
|
||||||
session = { url: '', title: '', domain: '', tabId: -1, startedAt: null };
|
session = { url: '', title: '', domain: '', tabId: -1, startedAt: null };
|
||||||
} else {
|
} else {
|
||||||
// Window gained focus — get active tab and start tracking
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
if (tabs && tabs.length > 0) {
|
if (tabs && tabs.length > 0) {
|
||||||
startTracking(tabs[0].id);
|
startTracking(tabs[0].id);
|
||||||
|
|
@ -86,19 +100,20 @@ function startTracking(tabId) {
|
||||||
url: tab.url,
|
url: tab.url,
|
||||||
title: tab.title || '',
|
title: tab.title || '',
|
||||||
domain: extractDomain(tab.url),
|
domain: extractDomain(tab.url),
|
||||||
tabId: tabId,
|
tabId,
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Session flush ---
|
||||||
|
|
||||||
function flushSession() {
|
function flushSession() {
|
||||||
if (!session.url || !session.startedAt) return;
|
if (!session.url || !session.startedAt) return;
|
||||||
|
|
||||||
const duration = Math.round((Date.now() - session.startedAt) / 1000);
|
const duration = Math.round((Date.now() - session.startedAt) / 1000);
|
||||||
if (duration < 2) {
|
if (duration < 2) {
|
||||||
// Skip very short visits
|
session.startedAt = Date.now();
|
||||||
session.startedAt = Date.now(); // reset timer
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +129,7 @@ function flushSession() {
|
||||||
};
|
};
|
||||||
|
|
||||||
queueEvent(event);
|
queueEvent(event);
|
||||||
session.startedAt = Date.now(); // reset for next segment
|
session.startedAt = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueEvent(event) {
|
function queueEvent(event) {
|
||||||
|
|
@ -122,13 +137,11 @@ function queueEvent(event) {
|
||||||
const queue = data[STORAGE_KEY] || [];
|
const queue = data[STORAGE_KEY] || [];
|
||||||
queue.push(event);
|
queue.push(event);
|
||||||
|
|
||||||
// Keep max 500 events in queue
|
|
||||||
if (queue.length > 500) {
|
if (queue.length > 500) {
|
||||||
queue.splice(0, queue.length - 500);
|
queue.splice(0, queue.length - 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.storage.local.set({ [STORAGE_KEY]: queue }, () => {
|
chrome.storage.local.set({ [STORAGE_KEY]: queue }, () => {
|
||||||
// Immediately try to flush if we have enough events
|
|
||||||
if (queue.length >= 5) {
|
if (queue.length >= 5) {
|
||||||
flushQueue();
|
flushQueue();
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +149,8 @@ function queueEvent(event) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Bridge communication ---
|
||||||
|
|
||||||
function flushQueue() {
|
function flushQueue() {
|
||||||
chrome.storage.local.get([STORAGE_KEY, BRIDGE_KEY], (data) => {
|
chrome.storage.local.get([STORAGE_KEY, BRIDGE_KEY], (data) => {
|
||||||
const queue = data[STORAGE_KEY] || [];
|
const queue = data[STORAGE_KEY] || [];
|
||||||
|
|
@ -161,23 +176,17 @@ function flushQueue() {
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// Update recent events
|
|
||||||
for (const ev of queue) {
|
for (const ev of queue) {
|
||||||
updateRecent(ev);
|
updateRecent(ev);
|
||||||
}
|
}
|
||||||
// Clear queue on success
|
|
||||||
chrome.storage.local.set({ [STORAGE_KEY]: [] });
|
chrome.storage.local.set({ [STORAGE_KEY]: [] });
|
||||||
// Notify popup if open
|
|
||||||
chrome.runtime.sendMessage({ type: 'UI_UPDATE' }).catch(() => {});
|
chrome.runtime.sendMessage({ type: 'UI_UPDATE' }).catch(() => {});
|
||||||
} else if (res.status === 401) {
|
} else if (res.status === 401) {
|
||||||
console.warn('[verstak] bridge auth failed, check secret');
|
console.warn('[verstak] bridge auth failed, check secret');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(() => {
|
||||||
// Bridge not available — keep events in queue
|
// Bridge not available — keep events in queue
|
||||||
if (queue.length % 10 === 0) {
|
|
||||||
console.debug('[verstak] bridge unavailable:', err.message);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -202,23 +211,8 @@ function pingBridge() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for messages from popup
|
// --- Recent events ---
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
||||||
if (msg.type === 'FORCE_FLUSH') {
|
|
||||||
flushSession();
|
|
||||||
flushQueue();
|
|
||||||
sendResponse({ ok: true });
|
|
||||||
}
|
|
||||||
if (msg.type === 'SET_TRACKING') {
|
|
||||||
// Tracking is always on at the background level;
|
|
||||||
// the popup just shows the setting.
|
|
||||||
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
|
||||||
sendResponse({ ok: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update recent events from flushed queue entries.
|
|
||||||
// This is called by flushQueue on successful delivery.
|
|
||||||
function updateRecent(event) {
|
function updateRecent(event) {
|
||||||
chrome.storage.local.get(RECENT_KEY, (data) => {
|
chrome.storage.local.get(RECENT_KEY, (data) => {
|
||||||
const recent = data[RECENT_KEY] || [];
|
const recent = data[RECENT_KEY] || [];
|
||||||
|
|
@ -229,7 +223,6 @@ function updateRecent(event) {
|
||||||
active_seconds: event.active_seconds,
|
active_seconds: event.active_seconds,
|
||||||
ts: new Date().toISOString(),
|
ts: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
// Keep last 50
|
|
||||||
if (recent.length > 50) {
|
if (recent.length > 50) {
|
||||||
recent.splice(0, recent.length - 50);
|
recent.splice(0, recent.length - 50);
|
||||||
}
|
}
|
||||||
|
|
@ -237,9 +230,7 @@ function updateRecent(event) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const RECENT_KEY = 'verstak_recent';
|
// --- Helpers ---
|
||||||
|
|
||||||
// ... existing code continues below ...
|
|
||||||
|
|
||||||
function extractDomain(url) {
|
function extractDomain(url) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1541,8 +1541,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptBrowserEvent(ev) {
|
async function acceptBrowserEvent(ev) {
|
||||||
await wailsCall('DismissBrowserEvent', ev.id)
|
// Accept: mark as accepted (worklog linking comes later via suggestions)
|
||||||
await loadBrowserEvents()
|
await wailsCall('AcceptBrowserEvent', ev.id, '')
|
||||||
|
browserEvents = browserEvents.filter(e => e.id !== ev.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dismissBrowserEvent(ev) {
|
async function dismissBrowserEvent(ev) {
|
||||||
|
|
@ -1551,8 +1552,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachBrowserEvent(ev) {
|
async function attachBrowserEvent(ev) {
|
||||||
await wailsCall('DismissBrowserEvent', ev.id)
|
// Attach to currently selected node, if any
|
||||||
await loadBrowserEvents()
|
if (selectedNode && selectedNode.id) {
|
||||||
|
await wailsCall('AttachBrowserEventToNode', ev.id, selectedNode.id)
|
||||||
|
browserEvents = browserEvents.filter(e => e.id !== ev.id)
|
||||||
|
} else {
|
||||||
|
// No node selected — just dismiss for now
|
||||||
|
await wailsCall('DismissBrowserEvent', ev.id)
|
||||||
|
browserEvents = browserEvents.filter(e => e.id !== ev.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeDebugLog(msg) {
|
function writeDebugLog(msg) {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ type EventHandler func(events []Event)
|
||||||
// Event represents a single browser event (page visit, note capture, etc.).
|
// Event represents a single browser event (page visit, note capture, etc.).
|
||||||
type Event struct {
|
type Event struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"` // page_visit, note_capture, screenshot
|
DeviceID string `json:"device_id,omitempty"`
|
||||||
|
Type string `json:"type"` // page_visit, note_capture, screenshot
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
|
|
@ -62,7 +63,7 @@ type Config struct {
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Port: 9786,
|
Port: 9786,
|
||||||
AutoGenPort: true,
|
AutoGenPort: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,14 +77,11 @@ func GenerateSecret() string {
|
||||||
return hex.EncodeToString(b)
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a bridge server. If cfg.Secret is empty, one is generated.
|
// NewServer creates a bridge server.
|
||||||
|
// If cfg.Secret is empty, no authentication is required.
|
||||||
func NewServer(cfg Config, handler EventHandler) *Server {
|
func NewServer(cfg Config, handler EventHandler) *Server {
|
||||||
secret := cfg.Secret
|
|
||||||
if secret == "" {
|
|
||||||
secret = GenerateSecret()
|
|
||||||
}
|
|
||||||
return &Server{
|
return &Server{
|
||||||
secret: secret,
|
secret: cfg.Secret,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +217,12 @@ func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.handler != nil {
|
if s.handler != nil {
|
||||||
|
// Enrich events with device_id from the batch.
|
||||||
|
for i := range batch.Events {
|
||||||
|
if batch.Events[i].DeviceID == "" {
|
||||||
|
batch.Events[i].DeviceID = batch.DeviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
s.handler(batch.Events)
|
s.handler(batch.Events)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ type VaultAppConfig struct {
|
||||||
VaultID string `json:"vault_id,omitempty"`
|
VaultID string `json:"vault_id,omitempty"`
|
||||||
CreatedAt string `json:"created_at,omitempty"`
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
Sync SyncSettings `json:"sync,omitempty"`
|
Sync SyncSettings `json:"sync,omitempty"`
|
||||||
FileWatcher bool `json:"file_watcher"`
|
FileWatcher *bool `json:"file_watcher,omitempty"`
|
||||||
Bridge BridgeConfig `json:"bridge,omitempty"`
|
Bridge BridgeConfig `json:"bridge,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ func DefaultAppConfig() *AppConfig {
|
||||||
EnabledTemplates: []string{"folder.default", "project.default", "client.default", "document.default", "recipe.default"},
|
EnabledTemplates: []string{"folder.default", "project.default", "client.default", "document.default", "recipe.default"},
|
||||||
EnabledPlugins: []string{},
|
EnabledPlugins: []string{},
|
||||||
Vault: VaultAppConfig{
|
Vault: VaultAppConfig{
|
||||||
FileWatcher: true,
|
FileWatcher: BoolPtr(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,3 +160,8 @@ func DefaultVaultPath() (string, error) {
|
||||||
}
|
}
|
||||||
return filepath.Join(dir, "vault"), nil
|
return filepath.Join(dir, "vault"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BoolPtr returns a pointer to the given bool value.
|
||||||
|
func BoolPtr(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,11 +236,20 @@ func (w *Watcher) handleEvent(rel, absPath string, ev fsnotify.Event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New file — create record.
|
// New file — check if it's already inside the vault.
|
||||||
_, err = w.files.CopyIntoVault(node.ID, absPath, parentDir)
|
// If absPath is under vaultRoot, don't copy — just create a record.
|
||||||
if err != nil {
|
if isUnderVault(absPath, w.vaultRoot) {
|
||||||
log.Printf("[watcher] auto-add file %s: %v", rel, err)
|
_, err = w.files.AddExternal(node.ID, absPath)
|
||||||
return
|
if err != nil {
|
||||||
|
log.Printf("[watcher] auto-add in-vault file %s: %v", rel, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = w.files.CopyIntoVault(node.ID, absPath, parentDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[watcher] auto-add file %s: %v", rel, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.logActivity(node.ID, activity.TypeFileAdded, fi.Name(), rel)
|
w.logActivity(node.ID, activity.TypeFileAdded, fi.Name(), rel)
|
||||||
|
|
||||||
|
|
@ -342,3 +351,10 @@ func hashFileFast(absPath string) (string, int64) {
|
||||||
}
|
}
|
||||||
return hex.EncodeToString(h.Sum(nil)), n
|
return hex.EncodeToString(h.Sum(nil)), n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUnderVault reports whether absPath is inside vaultRoot.
|
||||||
|
func isUnderVault(absPath, vaultRoot string) bool {
|
||||||
|
absPath, _ = filepath.Abs(absPath)
|
||||||
|
vaultRoot, _ = filepath.Abs(vaultRoot)
|
||||||
|
return strings.HasPrefix(absPath, vaultRoot+string(filepath.Separator)) || absPath == vaultRoot
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue