Files
HomeDash/extension/firefox/background.js
JianFeeeee 7e49d2b51c feat: HomeDash - unified self-hosted navigation portal
Single-container navigation service combining:
- Dashy dashboard + SearXNG metasearch + history sync API + admin WebUI
- Go services for history/admin APIs, nginx as unified entry
- Browser extensions (Chrome MV3 / Firefox MV2) with 30s batched sync
- Production guide: portal-nginx unified auth reverse proxy + webui4frpc/frp tunnel
- Fully sanitized (no internal IPs/domains/credentials)
2026-09-08 22:25:00 +08:00

133 lines
4.0 KiB
JavaScript

// JianF Nav Sync — Chrome/Edge MV3 service worker
'use strict';
const ALARM_NAME = 'navi-sync';
const SYNC_INTERVAL_MINUTES = 0.5; // 30 seconds
const SEARCH_DOMAINS = new Set([
'google.com', 'www.google.com', 'baidu.com', 'www.baidu.com',
'bing.com', 'www.bing.com', 'duckduckgo.com', 'www.duckduckgo.com',
'sogou.com', 'www.sogou.com', 'so.com', 'www.so.com',
'yahoo.com', 'www.yahoo.com', 'ask.com', 'www.ask.com',
'ecosia.org', 'www.ecosia.org', 'qwant.com', 'www.qwant.com',
]);
let pendingItems = [];
// Load pending from storage on startup
chrome.storage.local.get(['pending', 'serverUrl', 'syncToken'], (data) => {
pendingItems = data.pending || [];
});
// Listen to history visits
chrome.history.onVisited.addListener((historyItem) => {
try {
const url = new URL(historyItem.url);
// Skip non-http(s)
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
// Skip search engine URLs
const domain = url.hostname.replace(/^www\./, '');
if (SEARCH_DOMAINS.has(domain)) return;
pendingItems.push({
url: historyItem.url,
title: historyItem.title || '',
timestamp: historyItem.lastVisitTime / 1000, // ms -> seconds
visit_count: historyItem.visitCount || 1,
});
chrome.storage.local.set({ pending: pendingItems });
} catch (e) {
// invalid URL, skip
}
});
// Periodic sync alarm
chrome.alarms.create(ALARM_NAME, { periodInMinutes: SYNC_INTERVAL_MINUTES });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name !== ALARM_NAME) return;
syncNow();
});
// Manual sync from options page
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === 'sync') {
syncNow().then((result) => sendResponse(result)).catch((e) => sendResponse({ error: e.message }));
return true; // async response
}
if (msg.action === 'getConfig') {
chrome.storage.local.get(['serverUrl', 'syncToken', 'deviceName'], (data) => {
sendResponse(data);
});
return true;
}
if (msg.action === 'setConfig') {
chrome.storage.local.set(msg.data, () => sendResponse({ ok: true }));
return true;
}
if (msg.action === 'clearPending') {
pendingItems = [];
chrome.storage.local.set({ pending: [] }, () => sendResponse({ ok: true }));
return true;
}
});
function detectDevice() {
const ua = navigator.userAgent.toLowerCase();
if (/mobile|android|iphone|ipad/.test(ua)) {
if (/firefox/.test(ua)) return 'mobile-firefox';
if (/edg/.test(ua)) return 'mobile-edge';
return 'mobile-chrome';
}
if (/firefox/.test(ua)) return 'desktop-firefox';
if (/edg/.test(ua)) return 'desktop-edge';
return 'desktop-chrome';
}
async function syncNow() {
if (pendingItems.length === 0) return { ok: true, skipped: true, reason: 'no pending' };
const { serverUrl, syncToken, deviceName } = await new Promise((resolve) => {
chrome.storage.local.get(['serverUrl', 'syncToken', 'deviceName'], resolve);
});
if (!serverUrl) return { ok: false, reason: 'server not configured' };
// Snapshot and clear pending
const toSend = [...pendingItems];
pendingItems = [];
chrome.storage.local.set({ pending: [] });
const body = JSON.stringify({
items: toSend.map((item) => ({
...item,
device: deviceName || detectDevice(),
})),
});
try {
const headers = { 'Content-Type': 'application/json' };
if (syncToken) headers['Authorization'] = `Bearer ${syncToken}`;
const resp = await fetch(`${serverUrl}/api/history/sync`, {
method: 'POST',
headers,
body,
});
if (!resp.ok) {
const text = await resp.text();
// On failure, put items back
pendingItems = [...toSend, ...pendingItems];
chrome.storage.local.set({ pending: pendingItems });
return { ok: false, status: resp.status, error: text };
}
return await resp.json();
} catch (e) {
// Network error — put items back
pendingItems = [...toSend, ...pendingItems];
chrome.storage.local.set({ pending: pendingItems });
return { ok: false, error: e.message };
}
}