Files
binect-chrome/src/background/service-worker.ts
tegwick 1df93bd385 Add local tag server check, archive on delete, and first-run pin reminder
- 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>
2026-01-16 23:39:06 +01:00

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());