generated from coulomb/repo-seed
- Local tag is now clickable - checks if document exists on server by ID or filename, and re-links if found - Delete from server now archives the proxy instead of removing it, making it a local-only document that can be re-uploaded - Added first-run pin reminder banner to help users pin the extension - Added issue report modal with context sections (extension info, browser info, document status, recent errors) and copy to clipboard as Markdown - Added clearServerFields and attachServerDocument functions to pdf-queue - Improved local tag styling with hover states and visual feedback Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
/**
|
|
* Service Worker (Background Script)
|
|
* Handles PDF detection, queue management, and credential expiry checks
|
|
*/
|
|
|
|
import { startPDFDetection, DetectedPDF } from '../utils/pdf-detector';
|
|
import { loadCredentials } from '../utils/storage';
|
|
import {
|
|
addPDF,
|
|
getActionableCount,
|
|
getAllPDFs,
|
|
getLiveProxies,
|
|
getArchivedProxies,
|
|
getPendingPDFs,
|
|
updatePDFStatus,
|
|
archiveProxy,
|
|
restoreProxy,
|
|
dismissPDF,
|
|
removePDF,
|
|
cleanupOldEntries,
|
|
syncFromServer,
|
|
clearServerFields,
|
|
attachServerDocument,
|
|
PDFStatus,
|
|
PDFStatusMeta
|
|
} from '../utils/pdf-queue';
|
|
import { shipDocument, getDocumentStatus, deleteDocument, listServerDocuments } from '../utils/binect-api';
|
|
|
|
/**
|
|
* Initialize extension on install
|
|
*/
|
|
chrome.runtime.onInstalled.addListener((details) => {
|
|
console.log('[Service Worker] onInstalled event:', details.reason);
|
|
if (details.reason === 'install') {
|
|
console.log('[Service Worker] BinectChrome installed');
|
|
setupAlarms();
|
|
} else if (details.reason === 'update') {
|
|
console.log('[Service Worker] BinectChrome updated');
|
|
setupAlarms();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Handle extension startup
|
|
*/
|
|
chrome.runtime.onStartup.addListener(() => {
|
|
console.log('[Service Worker] onStartup event - BinectChrome started');
|
|
setupAlarms();
|
|
updateBadge();
|
|
});
|
|
|
|
/**
|
|
* Set up alarms for periodic tasks
|
|
*/
|
|
function setupAlarms() {
|
|
// Credential expiry check
|
|
chrome.alarms.create('checkCredentialExpiry', {
|
|
delayInMinutes: 1,
|
|
periodInMinutes: 24 * 60 // Every 24 hours
|
|
});
|
|
|
|
// PDF queue cleanup
|
|
chrome.alarms.create('cleanupPDFQueue', {
|
|
delayInMinutes: 60, // First cleanup in 1 hour
|
|
periodInMinutes: 6 * 60 // Every 6 hours
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle alarm events
|
|
*/
|
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
if (alarm.name === 'checkCredentialExpiry') {
|
|
checkAndDeleteExpiredCredentials();
|
|
}
|
|
if (alarm.name === 'cleanupPDFQueue') {
|
|
cleanupOldEntries();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Check if credentials are expired and delete them
|
|
*/
|
|
async function checkAndDeleteExpiredCredentials() {
|
|
const credentials = await loadCredentials();
|
|
if (credentials === null) {
|
|
console.log('[Service Worker] Credentials expired and deleted');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update badge with actionable PDF count
|
|
*/
|
|
async function updateBadge() {
|
|
const count = await getActionableCount();
|
|
const text = count > 0 ? count.toString() : '•';
|
|
chrome.action.setBadgeText({ text });
|
|
chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue
|
|
console.log('[Service Worker] Badge updated:', text);
|
|
}
|
|
|
|
// Initialize badge on load
|
|
updateBadge();
|
|
|
|
/**
|
|
* Start PDF detection
|
|
*/
|
|
console.log('[Service Worker] Initializing PDF detection...');
|
|
startPDFDetection(async (pdf: DetectedPDF) => {
|
|
console.log('[Service Worker] PDF DETECTED:', pdf.filename);
|
|
|
|
// Add to persistent queue
|
|
const entry = await addPDF(pdf);
|
|
|
|
if (entry) {
|
|
console.log('[Service Worker] PDF added to queue:', entry.filename);
|
|
} else {
|
|
console.log('[Service Worker] PDF skipped (already uploaded):', pdf.filename);
|
|
}
|
|
|
|
// Update badge
|
|
await updateBadge();
|
|
});
|
|
|
|
/**
|
|
* Handle messages from popup
|
|
*/
|
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
console.log('[Service Worker] Message received:', request.action);
|
|
|
|
// Get all PDFs (including completed ones for display)
|
|
if (request.action === 'getAllPDFs') {
|
|
getAllPDFs().then(entries => {
|
|
console.log('[Service Worker] Returning all PDFs:', entries.length, 'entries');
|
|
sendResponse({ entries });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Get live proxy documents (not archived)
|
|
if (request.action === 'getLiveProxies') {
|
|
getLiveProxies().then(entries => {
|
|
console.log('[Service Worker] Returning live proxies:', entries.length, 'entries');
|
|
sendResponse({ entries });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Get archived proxy documents
|
|
if (request.action === 'getArchivedProxies') {
|
|
getArchivedProxies().then(entries => {
|
|
console.log('[Service Worker] Returning archived proxies:', entries.length, 'entries');
|
|
sendResponse({ entries });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Add a PDF to the queue (from popup discovery)
|
|
if (request.action === 'addPDF') {
|
|
addPDF(request.pdf).then(entry => {
|
|
if (entry) {
|
|
console.log('[Service Worker] PDF added via message:', entry.filename);
|
|
}
|
|
return updateBadge().then(() => entry);
|
|
}).then(entry => {
|
|
sendResponse({ entry });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Legacy: Get only actionable PDFs
|
|
if (request.action === 'getPDFQueue') {
|
|
getPendingPDFs().then(entries => {
|
|
console.log('[Service Worker] Returning PDF queue:', entries.length, 'entries');
|
|
sendResponse({ entries });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'updatePDFStatus') {
|
|
const { id, status, meta } = request as { id: string; status: PDFStatus; meta?: PDFStatusMeta };
|
|
updatePDFStatus(id, status, meta).then(() => {
|
|
return updateBadge();
|
|
}).then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'dismissPDF') {
|
|
dismissPDF(request.id).then(() => {
|
|
return updateBadge();
|
|
}).then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Archive a proxy document (move to archive view)
|
|
if (request.action === 'archiveProxy') {
|
|
archiveProxy(request.id).then(() => {
|
|
return updateBadge();
|
|
}).then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Restore a proxy document (move back to live view)
|
|
if (request.action === 'restoreProxy') {
|
|
restoreProxy(request.id).then(() => {
|
|
return updateBadge();
|
|
}).then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'removePDF') {
|
|
removePDF(request.id).then(() => {
|
|
return updateBadge();
|
|
}).then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Ship a document (place order for production)
|
|
if (request.action === 'shipDocument') {
|
|
const { documentId, username, password } = request as {
|
|
documentId: number;
|
|
username: string;
|
|
password: string;
|
|
};
|
|
|
|
shipDocument(documentId, username, password)
|
|
.then(result => {
|
|
sendResponse({ success: true, ...result });
|
|
})
|
|
.catch(error => {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to ship document'
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Get document status from Binect
|
|
if (request.action === 'getDocumentStatus') {
|
|
const { documentId, username, password } = request as {
|
|
documentId: number;
|
|
username: string;
|
|
password: string;
|
|
};
|
|
|
|
getDocumentStatus(documentId, username, password)
|
|
.then(result => {
|
|
sendResponse({ success: true, ...result });
|
|
})
|
|
.catch(error => {
|
|
// Include error code for 404 detection
|
|
const errorCode = (error as { statusCode?: number }).statusCode;
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to get status',
|
|
errorCode
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Delete a document from the server
|
|
if (request.action === 'deleteServerDocument') {
|
|
const { documentId, username, password } = request as {
|
|
documentId: number;
|
|
username: string;
|
|
password: string;
|
|
};
|
|
|
|
deleteDocument(documentId, username, password)
|
|
.then(() => {
|
|
sendResponse({ success: true });
|
|
})
|
|
.catch(error => {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to delete document'
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// List all documents from the server (for sync)
|
|
if (request.action === 'listServerDocuments') {
|
|
const { username, password } = request as {
|
|
username: string;
|
|
password: string;
|
|
};
|
|
|
|
listServerDocuments(username, password)
|
|
.then(documents => {
|
|
sendResponse({ success: true, documents });
|
|
})
|
|
.catch(error => {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to list documents'
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Sync a server document to local proxy (create or update)
|
|
if (request.action === 'syncFromServer') {
|
|
const { binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails } = request as {
|
|
binectDocumentId: number;
|
|
filename: string;
|
|
binectStatusCode: number;
|
|
binectStatusText: string;
|
|
price?: number;
|
|
recipientAddress?: string;
|
|
errorDetails?: string;
|
|
};
|
|
|
|
syncFromServer(binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails)
|
|
.then(proxy => {
|
|
sendResponse({ success: true, proxy });
|
|
})
|
|
.catch(error => {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to sync document'
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Clear server fields from a proxy (when deleted from server)
|
|
if (request.action === 'clearServerFields') {
|
|
clearServerFields(request.id).then(() => {
|
|
return updateBadge();
|
|
}).then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Attach a server document to a local proxy
|
|
if (request.action === 'attachServerDocument') {
|
|
const { id, binectDocumentId, binectStatusCode, binectStatusText, price, recipientAddress, errorMessage } = request as {
|
|
id: string;
|
|
binectDocumentId: number;
|
|
binectStatusCode: number;
|
|
binectStatusText: string;
|
|
price?: number;
|
|
recipientAddress?: string;
|
|
errorMessage?: string;
|
|
};
|
|
|
|
attachServerDocument(id, binectDocumentId, binectStatusCode, binectStatusText, price, recipientAddress, errorMessage)
|
|
.then(() => {
|
|
return updateBadge();
|
|
})
|
|
.then(() => {
|
|
sendResponse({ success: true });
|
|
})
|
|
.catch(error => {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to attach document'
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Legacy handlers for backward compatibility
|
|
if (request.action === 'getLastPDF') {
|
|
getPendingPDFs().then(entries => {
|
|
const pdf = entries.length > 0 ? entries[0] : null;
|
|
sendResponse({ pdf });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (request.action === 'clearLastPDF' || request.action === 'pdfSent') {
|
|
updateBadge().then(() => {
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
console.log('[Service Worker] ===== BinectChrome service worker loaded =====');
|
|
console.log('[Service Worker] Timestamp:', new Date().toISOString());
|