Complete LinkSyncServer and LinkSyncExtension implementation
LinkSyncServer: - Fix app.py imports, add CORS middleware, lifespan events - Create api/routes.py router aggregator - Create config/settings.py for centralized configuration - Rewrite models/base.py with proper relationships and serialization - Rewrite all API endpoints with real DB integration (auth, links, collections, sync, queries, tags) - Add admin endpoints (user management, stats, audit log) - Complete query parser with recursive descent and proper precedence - Complete query executor with set operations and field filters - Set up Alembic migrations with initial schema - Create web interface (templates, CSS, JS) - Add 42 passing tests (auth, links, collections, queries) - Add deploy.ps1 and deploy.sh scripts - Update README with deployment workflow LinkSyncExtension: - Create utils/api.js (REST client with retries, auth, error handling) - Create utils/sync.js (3 sync modes + conflict detection) - Create utils/collection.js (collection management) - Create utils/query-engine.js (client-side query parser) - Rewrite background.js (sync loop, bookmark events, message routing) - Rewrite popup.js (tabs, settings modal, notifications, CRUD) - Update popup.html (tabbed interface, query builder, modal) - Update popup.css (full redesign) - Create content/content.js (page metadata extraction) - Create options.html/js (dedicated settings page) - Generate icons (48x48, 96x96) - Update manifest.json (host permissions, content scripts, options) - Create AGENTS.md
This commit is contained in:
@@ -1,312 +1,145 @@
|
||||
// LinkSync Background Service Worker
|
||||
// LinkSync Background Script
|
||||
// Handles bookmark synchronization with LinkSyncServer
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
const Background = {
|
||||
syncInterval: null,
|
||||
SYNC_CHECK_INTERVAL: 300000,
|
||||
|
||||
const Background = {
|
||||
// Configuration
|
||||
API_BASE_URL: '',
|
||||
SYNC_CHECK_INTERVAL: 60000, // 1 minute
|
||||
OFFLINE_QUEUE_TIMEOUT: 300000, // 5 minutes
|
||||
|
||||
// Storage keys
|
||||
STORAGE: {
|
||||
API_KEY: 'linksync_api_key',
|
||||
COLLECTION: 'linksync_collection',
|
||||
MODE: 'linksync_sync_mode',
|
||||
DELETIONS: 'linksync_deletions',
|
||||
AUTO_SYNC: 'linksync_auto_sync',
|
||||
URL: 'linksync_server_url',
|
||||
LAST_SYNC: 'linksync_last_sync',
|
||||
PENDING: 'linksync_pending'
|
||||
},
|
||||
|
||||
// Sync modes
|
||||
SYNC_MODES: {
|
||||
BIDIRECTIONAL: 'bi-directional',
|
||||
BROWSER_AUTHORITY: 'browser-authoritative',
|
||||
SERVER_AUTHORITY: 'server-authoritative'
|
||||
},
|
||||
|
||||
// Initialize on install/update
|
||||
async init() {
|
||||
console.log('LinkSync: Initializing...');
|
||||
|
||||
// Restore API key if available
|
||||
await this.restoreApiKey();
|
||||
|
||||
// Setup sync interval
|
||||
if (await this.getSetting(this.STORAGE.AUTO_SYNC)) {
|
||||
this.startAutoSync();
|
||||
}
|
||||
|
||||
// Listen for messages
|
||||
browser.runtime.onMessage.addListener(this.handleMessage.bind(this));
|
||||
},
|
||||
|
||||
// Restore API key from storage
|
||||
async restoreApiKey() {
|
||||
try {
|
||||
const apiKey = await this.getSetting(this.STORAGE.API_KEY);
|
||||
if (apiKey) {
|
||||
this.API_BASE_URL = await this.getSetting(this.STORAGE.URL) || 'http://localhost:5000';
|
||||
this.setupAuthHeaders();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Failed to restore API key:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Setup auth headers
|
||||
setupAuthHeaders() {
|
||||
const headers = new Headers();
|
||||
const apiKey = this.getApiKey();
|
||||
if (apiKey) {
|
||||
headers.set('Authorization', `Token ${apiKey}`);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
|
||||
// Get API key
|
||||
getApiKey() {
|
||||
return localStorage.getItem(this.STORAGE.API_KEY) || '';
|
||||
},
|
||||
|
||||
// Save API key encrypted
|
||||
async saveApiKey(key) {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
await window.crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false
|
||||
),
|
||||
key
|
||||
);
|
||||
localStorage.setItem(`${this.STORAGE.API_KEY}_iv`, btoa(String.fromCharCode(...iv)));
|
||||
localStorage.setItem(`${this.STORAGE.API_KEY}_data`, btoa(String.fromCharCode(...new Uint8Array(encrypted))));
|
||||
},
|
||||
|
||||
// Start auto-sync timer
|
||||
startAutoSync() {
|
||||
const sync = this.checkSync.bind(this);
|
||||
setInterval(sync, this.SYNC_CHECK_INTERVAL);
|
||||
sync(); // Initial sync
|
||||
},
|
||||
|
||||
// Handle messages from popup/content scripts
|
||||
async handleMessage(message, sender) {
|
||||
switch (message.type) {
|
||||
case 'SYNC_NOW':
|
||||
return this.checkSync();
|
||||
|
||||
case 'GET_BOOKMARKS':
|
||||
return this.getBookmarks();
|
||||
|
||||
case 'ADD_BOOKMARK':
|
||||
return this.addBookmark(message.data);
|
||||
|
||||
case 'UPDATE_BOOKMARK':
|
||||
return this.updateBookmark(message.data);
|
||||
|
||||
case 'DELETE_BOOKMARK':
|
||||
return this.deleteBookmark(message.data);
|
||||
|
||||
case 'SYNC_MODE':
|
||||
await this.setSetting(this.STORAGE.MODE, message.data.mode);
|
||||
return { success: true };
|
||||
|
||||
case 'GET_SETTINGS':
|
||||
return this.getSettings();
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Check for pending syncs
|
||||
async checkSync() {
|
||||
try {
|
||||
const config = await this.getSettings();
|
||||
const bookmarks = await this.getBrowserBookmarks();
|
||||
|
||||
// Update pending count
|
||||
await this.setSetting(this.STORAGE.PENDING, 0);
|
||||
|
||||
console.log('LinkSync: Sync completed');
|
||||
browser.runtime.sendMessage({ type: 'SYNC_COMPLETE' });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
pending: 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Sync error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Get browser bookmarks
|
||||
async getBrowserBookmarks() {
|
||||
try {
|
||||
const bookmarks = await browser.bookmarks.getTree();
|
||||
const flatBookmarks = this.flattenBookmarks(bookmarks);
|
||||
|
||||
// Filter out deleted items
|
||||
const existingIds = await this.getExistingBookmarkIds();
|
||||
flatBookmarks = flatBookmarks.filter(b => !existingIds.includes(b.id));
|
||||
|
||||
return flatBookmarks;
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Failed to get browser bookmarks:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Flatten bookmark tree to array
|
||||
flattenBookmarks(tree) {
|
||||
const result = [];
|
||||
function traverse(nodes) {
|
||||
nodes.forEach(node => {
|
||||
if (node.dateAdded) {
|
||||
result.push({
|
||||
id: node.id,
|
||||
url: node.url,
|
||||
title: node.title,
|
||||
dateAdded: new Date(node.dateAdded).toISOString(),
|
||||
lastModified: node.lastModified || new Date(node.dateAdded).toISOString()
|
||||
});
|
||||
}
|
||||
if (node.children) {
|
||||
traverse(node.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
traverse(tree);
|
||||
return result;
|
||||
},
|
||||
|
||||
// Get existing bookmark IDs from server
|
||||
async getExistingBookmarkIds() {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/links/`, {
|
||||
headers: this.setupAuthHeaders()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.links?.map(l => l.id) || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Failed to get existing bookmarks:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Add bookmark
|
||||
async addBookmark(bookmark) {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/links/`, {
|
||||
method: 'POST',
|
||||
headers: this.setupAuthHeaders(),
|
||||
body: JSON.stringify(bookmark)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
return { success: true, id: result.id };
|
||||
}
|
||||
|
||||
return { success: false, error: response.statusText };
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Add bookmark error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// Update bookmark
|
||||
async updateBookmark(bookmark) {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/links/${bookmark.id}/`, {
|
||||
method: 'PUT',
|
||||
headers: this.setupAuthHeaders(),
|
||||
body: JSON.stringify(bookmark)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return { success: false, error: response.statusText };
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Update bookmark error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// Delete bookmark
|
||||
async deleteBookmark(bookmarkId) {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}/api/links/${bookmarkId}/`, {
|
||||
method: 'DELETE',
|
||||
headers: this.setupAuthHeaders()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return { success: false, error: response.statusText };
|
||||
} catch (error) {
|
||||
console.error('LinkSync: Delete bookmark error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// Get settings
|
||||
async getSettings() {
|
||||
return {
|
||||
url: await this.getSetting(this.STORAGE.URL),
|
||||
apiKey: await this.getSetting(this.STORAGE.API_KEY),
|
||||
mode: await this.getSetting(this.STORAGE.MODE),
|
||||
deletions: await this.getSetting(this.STORAGE.DELETIONS),
|
||||
autoSync: await this.getSetting(this.STORAGE.AUTO_SYNC)
|
||||
};
|
||||
},
|
||||
|
||||
// Get single setting
|
||||
async getSetting(key) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get(key, result => resolve(result[key]));
|
||||
});
|
||||
},
|
||||
|
||||
// Set setting
|
||||
async setSetting(key, value) {
|
||||
await browser.storage.local.set({ [key]: value });
|
||||
},
|
||||
|
||||
// Get all bookmarks from tree
|
||||
getAllBookmarks() {
|
||||
return new Promise(resolve => {
|
||||
browser.bookmarks.getTree((tree) => {
|
||||
resolve(this.flattenBookmarks(tree));
|
||||
});
|
||||
});
|
||||
async init() {
|
||||
console.log("LinkSync: Initializing background script");
|
||||
|
||||
await API.init();
|
||||
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
console.log("LinkSync: Extension installed");
|
||||
this.startAutoSync();
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener(this.handleMessage.bind(this));
|
||||
|
||||
browser.bookmarks.onCreated.addListener(this.onBookmarkChanged.bind(this));
|
||||
browser.bookmarks.onChanged.addListener(this.onBookmarkChanged.bind(this));
|
||||
browser.bookmarks.onRemoved.addListener(this.onBookmarkChanged.bind(this));
|
||||
|
||||
const settings = await browser.storage.local.get(["linksync_auto_sync"]);
|
||||
if (settings.linksync_auto_sync) {
|
||||
this.startAutoSync();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on install/update
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
Background.init();
|
||||
});
|
||||
|
||||
// Expose to window
|
||||
window.Background = Background;
|
||||
|
||||
})();
|
||||
},
|
||||
|
||||
startAutoSync() {
|
||||
if (this.syncInterval) {
|
||||
clearInterval(this.syncInterval);
|
||||
}
|
||||
this.syncInterval = setInterval(() => this.runSync(), this.SYNC_CHECK_INTERVAL);
|
||||
console.log("LinkSync: Auto-sync started (every 5 minutes)");
|
||||
},
|
||||
|
||||
stopAutoSync() {
|
||||
if (this.syncInterval) {
|
||||
clearInterval(this.syncInterval);
|
||||
this.syncInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
async onBookmarkChanged(id, changeInfo) {
|
||||
const settings = await browser.storage.local.get(["linksync_auto_sync"]);
|
||||
if (settings.linksync_auto_sync) {
|
||||
await browser.storage.local.set({ linksync_pending: true });
|
||||
}
|
||||
},
|
||||
|
||||
async handleMessage(message, sender) {
|
||||
switch (message.type) {
|
||||
case "SYNC_NOW":
|
||||
return this.runSync();
|
||||
|
||||
case "GET_SETTINGS":
|
||||
return browser.storage.local.get([
|
||||
"linksync_server_url",
|
||||
"linksync_api_key",
|
||||
"linksync_sync_mode",
|
||||
"linksync_deletions",
|
||||
"linksync_auto_sync",
|
||||
"linksync_last_sync",
|
||||
]);
|
||||
|
||||
case "SAVE_SETTINGS":
|
||||
await browser.storage.local.set(message.data);
|
||||
await API.init();
|
||||
if (message.data.linksync_auto_sync) {
|
||||
this.startAutoSync();
|
||||
} else {
|
||||
this.stopAutoSync();
|
||||
}
|
||||
return { success: true };
|
||||
|
||||
case "TEST_CONNECTION":
|
||||
try {
|
||||
await API.testConnection();
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
|
||||
case "LOGIN":
|
||||
try {
|
||||
const data = await API.login(message.data.username, message.data.password);
|
||||
return { success: true, token: data.access_token };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
|
||||
case "GET_BOOKMARKS":
|
||||
return API.getLinks(message.data || {});
|
||||
|
||||
case "CREATE_BOOKMARK":
|
||||
return API.createLink(message.data);
|
||||
|
||||
case "UPDATE_BOOKMARK":
|
||||
return API.updateLink(message.data.id, message.data);
|
||||
|
||||
case "DELETE_BOOKMARK":
|
||||
return API.deleteLink(message.data.id);
|
||||
|
||||
case "GET_COLLECTIONS":
|
||||
return CollectionManager.listCollections();
|
||||
|
||||
case "EXECUTE_QUERY":
|
||||
return CollectionManager.executeQuery(message.data.expression, message.data.limit);
|
||||
|
||||
case "PARSE_QUERY":
|
||||
return CollectionManager.parseQuery(message.data.expression);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async runSync() {
|
||||
try {
|
||||
await browser.storage.local.set({ linksync_syncing: true });
|
||||
|
||||
const actions = await SyncEngine.runSync();
|
||||
|
||||
await browser.storage.local.set({
|
||||
linksync_syncing: false,
|
||||
linksync_last_sync: new Date().toISOString(),
|
||||
linksync_pending: false,
|
||||
linksync_sync_result: actions,
|
||||
});
|
||||
|
||||
console.log(`LinkSync: Sync completed with ${actions.length} actions`);
|
||||
return { success: true, actions };
|
||||
} catch (error) {
|
||||
console.error("LinkSync: Sync failed:", error);
|
||||
await browser.storage.local.set({
|
||||
linksync_syncing: false,
|
||||
linksync_sync_error: error.message,
|
||||
});
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => Background.init());
|
||||
|
||||
Reference in New Issue
Block a user