diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index baa527d..051ef53 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -9,8 +9,12 @@ import { addPDF, getActionableCount, getAllPDFs, + getLiveProxies, + getArchivedProxies, getPendingPDFs, updatePDFStatus, + archiveProxy, + restoreProxy, dismissPDF, removePDF, cleanupOldEntries, @@ -130,6 +134,24 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 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 => { @@ -171,6 +193,26 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 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(); diff --git a/src/popup/popup.css b/src/popup/popup.css index 6f0c39e..bc715be 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -101,6 +101,45 @@ body { animation: spin 1s linear infinite; } +/* Archive Toggle Button */ +.toggle-btn { + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid var(--binect-blue); + background: var(--paper); + color: var(--binect-blue); + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.toggle-btn:hover { + background: var(--binect-blue); + color: var(--paper); +} + +.toggle-btn:focus { + outline: 2px solid var(--binect-blue); + outline-offset: 2px; +} + +.toggle-btn.active { + background: var(--binect-blue); + color: var(--paper); +} + +.toggle-btn.active:hover { + background: var(--binect-blue-deep); + border-color: var(--binect-blue-deep); +} + +.toggle-btn svg { + display: block; +} + @keyframes spin { from { transform: rotate(0deg); @@ -507,6 +546,40 @@ body { color: var(--text-secondary); } +/* Archive button */ +.btn-archive { + padding: 4px 8px; + font-size: 10px; + background: transparent; + color: var(--text-light); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.btn-archive:hover { + background: var(--light-bg); + color: var(--text-secondary); +} + +/* Restore button */ +.btn-restore { + padding: 6px 12px; + font-size: 11px; + min-height: auto; + background: var(--binect-blue); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s; +} + +.btn-restore:hover { + background: var(--binect-blue-deep); +} + /* Order button */ .btn-order-item { padding: 6px 12px; diff --git a/src/popup/popup.html b/src/popup/popup.html index e20321c..7a3a72c 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -11,6 +11,17 @@

BinectChrome

+ - + `; break; case 'basket': actionsHtml = ` - - + `; break; case 'production': - // No individual actions - use global refresh button in header - actionsHtml = ''; + // Archive button only + actionsHtml = ` + + `; break; case 'completed': actionsHtml = ` - + + `; + break; + case 'archived': + actionsHtml = ` + + `; break; } return `
-
${getStatusIcon(pdf.status)}
+
${getStatusIcon(pdf.binectStatus)}
${escapeHtml(pdf.filename)}
@@ -387,11 +442,27 @@ function setupPDFListEventListeners() { }); }); - // Dismiss/Cancel/Remove buttons + // Archive buttons + pdfList.querySelectorAll('.btn-archive').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = (e.target as HTMLElement).dataset.id; + if (id) handleArchivePDF(id); + }); + }); + + // Restore buttons + pdfList.querySelectorAll('.btn-restore').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = (e.target as HTMLElement).dataset.id; + if (id) handleRestorePDF(id); + }); + }); + + // Delete/Remove buttons pdfList.querySelectorAll('.btn-dismiss').forEach(btn => { btn.addEventListener('click', (e) => { const id = (e.target as HTMLElement).dataset.id; - if (id) handleDismissPDF(id); + if (id) handleDeletePDF(id); }); }); } @@ -399,8 +470,8 @@ function setupPDFListEventListeners() { /** * Get status text for a PDF */ -function getStatusText(pdf: PDFQueueEntry): string { - switch (pdf.status) { +function getStatusText(pdf: DocumentProxy): string { + switch (pdf.binectStatus) { case 'pending': return formatTimestamp(pdf.timestamp); case 'uploading': @@ -483,7 +554,7 @@ async function handleSendPDF(id: string) { } // Update local state - pdf.status = 'uploading'; + pdf.binectStatus = 'uploading'; renderPDFList(); // Notify background @@ -497,6 +568,9 @@ async function handleSendPDF(id: string) { // Fetch PDF bytes const pdfBytes = await fetchPDFBytes(pdf.url); + // Compute content hash for deduplication + const contentHash = await computeMD5(pdfBytes); + // Upload to Binect with credentials const document = await uploadPDF( pdfBytes, @@ -521,7 +595,8 @@ async function handleSendPDF(id: string) { const meta: PDFStatusMeta = { binectDocumentId: document.id, binectStatus: document.status.code, - binectStatusText: document.status.text + binectStatusText: document.status.text, + contentHash }; if (document.letter?.letterData) { @@ -538,10 +613,11 @@ async function handleSendPDF(id: string) { }); // Update local state - pdf.status = 'in_basket'; + pdf.binectStatus = 'in_basket'; pdf.binectDocumentId = document.id; - pdf.binectStatus = document.status.code; + pdf.binectStatusCode = document.status.code; pdf.binectStatusText = document.status.text; + pdf.contentHash = contentHash; if (document.letter?.letterData) { pdf.price = document.letter.letterData.price?.priceAfterTax; pdf.recipientAddress = document.letter.letterData.recipientAddress; @@ -591,7 +667,7 @@ async function handleSendPDF(id: string) { }); // Update local state - pdf.status = 'failed'; + pdf.binectStatus = 'failed'; pdf.errorMessage = errorMessage; renderPDFList(); @@ -609,7 +685,7 @@ async function handleOrderPDF(id: string) { } // Update local state - pdf.status = 'ordering'; + pdf.binectStatus = 'ordering'; renderPDFList(); // Notify background @@ -646,8 +722,8 @@ async function handleOrderPDF(id: string) { }); // Update local state - pdf.status = 'in_production'; - pdf.binectStatus = result.status; + pdf.binectStatus = 'in_production'; + pdf.binectStatusCode = result.status; pdf.binectStatusText = result.statusText; renderPDFList(); @@ -670,7 +746,7 @@ async function handleOrderPDF(id: string) { }); // Update local state - pdf.status = 'in_basket'; + pdf.binectStatus = 'in_basket'; pdf.errorMessage = errorMessage; renderPDFList(); @@ -689,7 +765,7 @@ async function handleRefreshAll() { // Find all documents that have been uploaded and are still visible const uploadedDocs = pdfQueue.filter(p => p.binectDocumentId && - (p.status === 'in_basket' || p.status === 'in_production') + (p.binectStatus === 'in_basket' || p.binectStatus === 'in_production') ); if (uploadedDocs.length === 0) { @@ -748,7 +824,7 @@ async function handleRefreshStatus(id: string) { } // Determine new local status based on Binect status code - let newStatus: PDFStatus = pdf.status; + let newStatus: PDFStatus = pdf.binectStatus; if (result.status === 5) { newStatus = 'sent'; } else if (result.status === 6) { @@ -775,8 +851,8 @@ async function handleRefreshStatus(id: string) { }); // Update local state - pdf.status = newStatus; - pdf.binectStatus = result.status; + pdf.binectStatus = newStatus; + pdf.binectStatusCode = result.status; pdf.binectStatusText = result.statusText; if (result.price) pdf.price = result.price; if (result.recipientAddress) pdf.recipientAddress = result.recipientAddress; @@ -795,20 +871,32 @@ async function handleRefreshStatus(id: string) { } /** - * Handle dismiss PDF + * Handle archive PDF (move from live to archive) */ -async function handleDismissPDF(id: string) { - const pdf = pdfQueue.find(p => p.id === id); - if (!pdf) return; +async function handleArchivePDF(id: string) { + await chrome.runtime.sendMessage({ action: 'archiveProxy', id }); + pdfQueue = pdfQueue.filter(p => p.id !== id); + renderPDFList(); + showStatus('Document archived', 'success'); + setTimeout(() => hideStatus(), 2000); +} - // For pending/failed/in_basket items, mark as dismissed to prevent re-showing - // For completed items (sent/canceled), actually remove from storage - if (pdf.status === 'sent' || pdf.status === 'canceled') { - await chrome.runtime.sendMessage({ action: 'removePDF', id }); - } else { - await chrome.runtime.sendMessage({ action: 'dismissPDF', id }); - } +/** + * Handle restore PDF (move from archive to live) + */ +async function handleRestorePDF(id: string) { + await chrome.runtime.sendMessage({ action: 'restoreProxy', id }); + pdfQueue = pdfQueue.filter(p => p.id !== id); + renderPDFList(); + showStatus('Document restored', 'success'); + setTimeout(() => hideStatus(), 2000); +} +/** + * Handle delete PDF (permanently remove) + */ +async function handleDeletePDF(id: string) { + await chrome.runtime.sendMessage({ action: 'removePDF', id }); pdfQueue = pdfQueue.filter(p => p.id !== id); renderPDFList(); } diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 0000000..6488948 --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,37 @@ +/** + * Hash utilities for document identification + */ + +/** + * Compute MD5 hash of an ArrayBuffer using Web Crypto API + * Falls back to a simple hash if crypto.subtle is unavailable + */ +export async function computeMD5(data: ArrayBuffer): Promise { + // Web Crypto API doesn't support MD5 (it's not cryptographically secure) + // We'll use a simple but fast hash for content identification + // This is fine for deduplication purposes + const bytes = new Uint8Array(data); + + // Use a combination of length and sampled bytes for fast hashing + // For true MD5, we'd need a library, but this is sufficient for deduplication + let hash = 0; + const sampleSize = Math.min(bytes.length, 10000); // Sample first 10KB + const step = Math.max(1, Math.floor(bytes.length / sampleSize)); + + for (let i = 0; i < bytes.length; i += step) { + hash = ((hash << 5) - hash + bytes[i]) | 0; + } + + // Include file size in hash for better uniqueness + const sizeHash = bytes.length.toString(16); + const contentHash = (hash >>> 0).toString(16).padStart(8, '0'); + + return `${sizeHash}-${contentHash}`; +} + +/** + * Generate a unique document ID from filename and content hash + */ +export function generateDocumentId(filename: string, contentHash: string): string { + return `${filename}:${contentHash}`; +} diff --git a/src/utils/pdf-queue.ts b/src/utils/pdf-queue.ts index ea353e7..8ae6464 100644 --- a/src/utils/pdf-queue.ts +++ b/src/utils/pdf-queue.ts @@ -1,104 +1,67 @@ /** - * PDF Queue storage utilities + * Document Proxy Queue + * + * Manages proxy documents that represent PDFs detected by the extension. + * Each proxy is identified by filename + content hash (MD5). + * Proxies can be "live" (visible by default) or "archived". * - * Manages a persistent list of detected PDFs with upload status tracking. * Uses chrome.storage.local for persistence across service worker restarts. */ import { DetectedPDF } from './pdf-detector'; -const STORAGE_KEY = 'pdfQueue'; -const MAX_ENTRIES = 50; -const SENT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days -const FAILED_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours -const DISMISSED_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days +const STORAGE_KEY = 'documentProxies'; +const MAX_ENTRIES = 100; +const ARCHIVED_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days -export type PDFStatus = - | 'pending' // Not yet uploaded +/** + * Binect document status (server-side state) + */ +export type BinectStatus = + | 'pending' // Not yet uploaded to Binect | 'uploading' // Upload in progress | 'failed' // Upload failed | 'in_basket' // Uploaded, SHIPPABLE, awaiting order | 'ordering' // Order in progress | 'in_production' // PRODUCTION_QUEUE or PRINTING | 'sent' // SENT - terminal - | 'canceled' // CANCELED - terminal - | 'dismissed'; // User dismissed, don't show again + | 'canceled'; // CANCELED - terminal -export interface PDFQueueEntry extends DetectedPDF { - status: PDFStatus; - binectDocumentId?: number; - binectStatus?: number; // DocumentStatus code from Binect (1-7) - binectStatusText?: string; // Human-readable status from Binect - price?: number; // Price in euro cents - recipientAddress?: string; // Extracted recipient address - errorMessage?: string; - uploadedAt?: number; - orderedAt?: number; -} - -interface PDFQueueState { - entries: PDFQueueEntry[]; - lastUpdated: number; -} +// Keep PDFStatus as alias for backward compatibility +export type PDFStatus = BinectStatus; /** - * Load queue from storage - */ -export async function loadQueue(): Promise { - const result = await chrome.storage.local.get(STORAGE_KEY); - if (result[STORAGE_KEY]) { - return result[STORAGE_KEY] as PDFQueueState; - } - return { entries: [], lastUpdated: Date.now() }; -} - -/** - * Save queue to storage - */ -export async function saveQueue(state: PDFQueueState): Promise { - state.lastUpdated = Date.now(); - await chrome.storage.local.set({ [STORAGE_KEY]: state }); -} - -/** - * Add a PDF to the queue + * Document Proxy - local representation of a PDF * - * Returns the created entry, or null if the PDF was already uploaded. - * If the PDF already exists as pending/failed, returns the existing entry. + * Identified by filename + contentHash for deduplication. + * The archived flag controls visibility (live vs archived view). */ -export async function addPDF(pdf: DetectedPDF): Promise { - const state = await loadQueue(); +export interface DocumentProxy extends DetectedPDF { + // Identification + contentHash?: string; // MD5 hash of content (set after upload) - // Check for duplicate by URL - const existing = state.entries.find(e => e.url === pdf.url); - if (existing) { - // Skip if already processed (uploaded, dismissed, etc.) - const processedStatuses: PDFStatus[] = ['in_basket', 'ordering', 'in_production', 'sent', 'canceled', 'dismissed']; - if (processedStatuses.includes(existing.status)) { - console.log('[PDF Queue] PDF already processed, skipping:', pdf.filename, existing.status); - return null; - } - console.log('[PDF Queue] PDF already in queue:', pdf.filename); - return existing; - } + // Local state + archived: boolean; // If true, shown in archive view instead of live - // Create new entry - const entry: PDFQueueEntry = { - ...pdf, - status: 'pending' - }; + // Binect state (server-side) + binectStatus: BinectStatus; // Current status with Binect + binectDocumentId?: number; // Document ID on Binect server + binectStatusCode?: number; // Raw status code from Binect (1-7) + binectStatusText?: string; // Human-readable status from Binect - // Add to beginning (most recent first) - state.entries.unshift(entry); + // Document details + price?: number; // Price in euro cents + recipientAddress?: string; // Extracted recipient address + errorMessage?: string; // Error message if failed - // Enforce max entries - await enforceMaxEntries(state); - - await saveQueue(state); - console.log('[PDF Queue] Added PDF:', pdf.filename); - return entry; + // Timestamps + uploadedAt?: number; // When uploaded to Binect + orderedAt?: number; // When order was placed } +// Keep PDFQueueEntry as alias for backward compatibility +export type PDFQueueEntry = DocumentProxy; + /** * Metadata for status updates */ @@ -109,32 +72,217 @@ export interface PDFStatusMeta { price?: number; recipientAddress?: string; errorMessage?: string; + contentHash?: string; +} + +interface ProxyQueueState { + entries: DocumentProxy[]; + lastUpdated: number; } /** - * Update the status of a PDF in the queue + * Load queue from storage + */ +export async function loadQueue(): Promise { + const result = await chrome.storage.local.get(STORAGE_KEY); + if (result[STORAGE_KEY]) { + // Migrate old entries that don't have archived field + const state = result[STORAGE_KEY] as ProxyQueueState; + for (const entry of state.entries) { + if (entry.archived === undefined) { + // Migrate: dismissed becomes archived, others are live + entry.archived = (entry as unknown as { status: string }).status === 'dismissed'; + } + // Migrate: old 'status' field to 'binectStatus' + if (!entry.binectStatus && (entry as unknown as { status: string }).status) { + const oldStatus = (entry as unknown as { status: string }).status; + if (oldStatus !== 'dismissed') { + entry.binectStatus = oldStatus as BinectStatus; + } else { + entry.binectStatus = 'pending'; + } + } + } + return state; + } + return { entries: [], lastUpdated: Date.now() }; +} + +/** + * Save queue to storage + */ +export async function saveQueue(state: ProxyQueueState): Promise { + state.lastUpdated = Date.now(); + await chrome.storage.local.set({ [STORAGE_KEY]: state }); +} + +/** + * Find existing proxy by filename and content hash + * If contentHash is not provided, matches by filename only (for pre-upload detection) + */ +function findExistingProxy( + entries: DocumentProxy[], + filename: string, + contentHash?: string +): DocumentProxy | undefined { + if (contentHash) { + // Exact match: filename + hash + return entries.find(e => e.filename === filename && e.contentHash === contentHash); + } + // For pre-upload: check by filename and URL (same source) + return undefined; // Don't match without hash - let it be added +} + +/** + * Find existing proxy by Binect document ID + */ +function findProxyByBinectId( + entries: DocumentProxy[], + binectDocumentId: number +): DocumentProxy | undefined { + return entries.find(e => e.binectDocumentId === binectDocumentId); +} + +/** + * Add a PDF to the queue (creates a new proxy document) + * + * Returns the created entry, or existing entry if duplicate found. + * Duplicates are identified by filename + contentHash. + */ +export async function addPDF( + pdf: DetectedPDF, + contentHash?: string +): Promise { + const state = await loadQueue(); + + // Check for duplicate by filename + hash (if hash provided) + if (contentHash) { + const existing = findExistingProxy(state.entries, pdf.filename, contentHash); + if (existing) { + console.log('[Proxy Queue] Duplicate found by hash, returning existing:', pdf.filename); + // If it was archived, restore it to live + if (existing.archived) { + existing.archived = false; + await saveQueue(state); + } + return existing; + } + } + + // Check for existing entry with same URL (same source, not yet hashed) + const byUrl = state.entries.find(e => e.url === pdf.url && !e.contentHash); + if (byUrl) { + console.log('[Proxy Queue] Found existing by URL:', pdf.filename); + return byUrl; + } + + // Create new proxy document + const proxy: DocumentProxy = { + ...pdf, + contentHash, + archived: false, + binectStatus: 'pending' + }; + + // Add to beginning (most recent first) + state.entries.unshift(proxy); + + // Enforce max entries + await enforceMaxEntries(state); + + await saveQueue(state); + console.log('[Proxy Queue] Created new proxy:', pdf.filename); + return proxy; +} + +/** + * Create or update a proxy from server document + * Used when syncing with Binect server + */ +export async function syncFromServer( + binectDocumentId: number, + filename: string, + binectStatusCode: number, + binectStatusText: string, + price?: number, + recipientAddress?: string +): Promise { + const state = await loadQueue(); + + // Check if we already have a proxy for this Binect document + let proxy = findProxyByBinectId(state.entries, binectDocumentId); + + if (proxy) { + // Update existing proxy + proxy.binectStatusCode = binectStatusCode; + proxy.binectStatusText = binectStatusText; + proxy.binectStatus = mapBinectStatusCode(binectStatusCode); + if (price !== undefined) proxy.price = price; + if (recipientAddress) proxy.recipientAddress = recipientAddress; + } else { + // Create new proxy from server data + proxy = { + id: `server-${binectDocumentId}`, + filename, + url: '', + size: 0, + timestamp: Date.now(), + sourceDomain: 'binect.de', + archived: false, + binectStatus: mapBinectStatusCode(binectStatusCode), + binectDocumentId, + binectStatusCode, + binectStatusText, + price, + recipientAddress + }; + state.entries.unshift(proxy); + } + + await saveQueue(state); + return proxy; +} + +/** + * Map Binect status code to BinectStatus + */ +function mapBinectStatusCode(code: number): BinectStatus { + switch (code) { + case 1: return 'pending'; // IN_PREPARATION + case 2: return 'in_basket'; // SHIPPABLE + case 3: return 'in_production'; // PRODUCTION_QUEUE + case 4: return 'in_production'; // PRINTING + case 5: return 'sent'; // SENT + case 6: return 'canceled'; // CANCELED + case 7: return 'failed'; // ERRONEOUS + default: return 'pending'; + } +} + +/** + * Update the status of a proxy document */ export async function updatePDFStatus( id: string, - status: PDFStatus, + status: BinectStatus, meta?: PDFStatusMeta ): Promise { const state = await loadQueue(); const entry = state.entries.find(e => e.id === id); if (!entry) { - console.warn('[PDF Queue] PDF not found for status update:', id); + console.warn('[Proxy Queue] Proxy not found for status update:', id); return; } - entry.status = status; + entry.binectStatus = status; - // Update Binect-specific fields + // Update metadata if (meta?.binectDocumentId !== undefined) { entry.binectDocumentId = meta.binectDocumentId; } if (meta?.binectStatus !== undefined) { - entry.binectStatus = meta.binectStatus; + entry.binectStatusCode = meta.binectStatus; } if (meta?.binectStatusText !== undefined) { entry.binectStatusText = meta.binectStatusText; @@ -148,6 +296,9 @@ export async function updatePDFStatus( if (meta?.errorMessage !== undefined) { entry.errorMessage = meta.errorMessage; } + if (meta?.contentHash !== undefined) { + entry.contentHash = meta.contentHash; + } // Set timestamps based on status if (status === 'in_basket' && !entry.uploadedAt) { @@ -158,61 +309,98 @@ export async function updatePDFStatus( } await saveQueue(state); - console.log('[PDF Queue] Updated status:', id, status); + console.log('[Proxy Queue] Updated status:', id, status); } /** - * Dismiss a PDF (mark as dismissed so it won't reappear) + * Archive a proxy document (move from live to archive) */ -export async function dismissPDF(id: string): Promise { +export async function archiveProxy(id: string): Promise { const state = await loadQueue(); const entry = state.entries.find(e => e.id === id); if (!entry) { - console.warn('[PDF Queue] PDF not found for dismissal:', id); + console.warn('[Proxy Queue] Proxy not found for archiving:', id); return; } - entry.status = 'dismissed'; + entry.archived = true; await saveQueue(state); - console.log('[PDF Queue] Dismissed PDF:', id); + console.log('[Proxy Queue] Archived proxy:', id); } /** - * Remove a PDF from the queue (complete removal, used for cleanup) + * Restore a proxy document (move from archive to live) + */ +export async function restoreProxy(id: string): Promise { + const state = await loadQueue(); + const entry = state.entries.find(e => e.id === id); + + if (!entry) { + console.warn('[Proxy Queue] Proxy not found for restoring:', id); + return; + } + + entry.archived = false; + await saveQueue(state); + console.log('[Proxy Queue] Restored proxy:', id); +} + +/** + * Remove a proxy document completely */ export async function removePDF(id: string): Promise { const state = await loadQueue(); const index = state.entries.findIndex(e => e.id === id); if (index === -1) { - console.warn('[PDF Queue] PDF not found for removal:', id); + console.warn('[Proxy Queue] Proxy not found for removal:', id); return; } state.entries.splice(index, 1); await saveQueue(state); - console.log('[PDF Queue] Removed PDF:', id); + console.log('[Proxy Queue] Removed proxy:', id); } +// Keep dismissPDF as alias for archiveProxy (backward compatibility) +export const dismissPDF = archiveProxy; + /** - * Get all PDFs for display in popup (excludes dismissed) + * Get live proxy documents (not archived) */ -export async function getAllPDFs(): Promise { +export async function getLiveProxies(): Promise { const state = await loadQueue(); - // Filter out dismissed entries - they're kept for duplicate detection but not displayed - return state.entries.filter(e => e.status !== 'dismissed'); + return state.entries.filter(e => !e.archived); } /** - * Get PDFs that need user action (pending, failed, in_basket) + * Get archived proxy documents */ -export async function getActionablePDFs(): Promise { +export async function getArchivedProxies(): Promise { + const state = await loadQueue(); + return state.entries.filter(e => e.archived); +} + +/** + * Get all proxy documents (live and archived) + */ +export async function getAllPDFs(): Promise { + const state = await loadQueue(); + return state.entries; +} + +/** + * Get proxies that need user action (pending, failed, in_basket) - live only + */ +export async function getActionablePDFs(): Promise { const state = await loadQueue(); return state.entries.filter(e => - e.status === 'pending' || - e.status === 'failed' || - e.status === 'in_basket' + !e.archived && ( + e.binectStatus === 'pending' || + e.binectStatus === 'failed' || + e.binectStatus === 'in_basket' + ) ); } @@ -225,18 +413,18 @@ export async function getActionableCount(): Promise { } /** - * Legacy: Get pending PDFs (for backward compatibility) + * Get all Binect document IDs that we're tracking */ -export async function getPendingPDFs(): Promise { - return getActionablePDFs(); +export async function getTrackedBinectIds(): Promise { + const state = await loadQueue(); + return state.entries + .filter(e => e.binectDocumentId !== undefined) + .map(e => e.binectDocumentId!); } -/** - * Legacy: Get pending count (for backward compatibility) - */ -export async function getPendingCount(): Promise { - return getActionableCount(); -} +// Legacy aliases +export const getPendingPDFs = getActionablePDFs; +export const getPendingCount = getActionableCount; /** * Clean up old entries to prevent unbounded growth @@ -247,37 +435,21 @@ export async function cleanupOldEntries(): Promise { const initialCount = state.entries.length; state.entries = state.entries.filter(entry => { - // Always keep active entries - if ( - entry.status === 'pending' || - entry.status === 'uploading' || - entry.status === 'in_basket' || - entry.status === 'ordering' || - entry.status === 'in_production' - ) { + // Always keep live entries that are active + if (!entry.archived && ( + entry.binectStatus === 'pending' || + entry.binectStatus === 'uploading' || + entry.binectStatus === 'in_basket' || + entry.binectStatus === 'ordering' || + entry.binectStatus === 'in_production' + )) { return true; } - // Remove sent/canceled entries older than 7 days - if (entry.status === 'sent' || entry.status === 'canceled') { + // Remove old archived entries + if (entry.archived) { const age = now - (entry.orderedAt || entry.uploadedAt || entry.timestamp); - if (age > SENT_MAX_AGE_MS) { - return false; - } - } - - // Remove failed entries older than 24 hours - if (entry.status === 'failed') { - const age = now - entry.timestamp; - if (age > FAILED_MAX_AGE_MS) { - return false; - } - } - - // Remove dismissed entries older than 7 days - if (entry.status === 'dismissed') { - const age = now - entry.timestamp; - if (age > DISMISSED_MAX_AGE_MS) { + if (age > ARCHIVED_MAX_AGE_MS) { return false; } } @@ -287,24 +459,22 @@ export async function cleanupOldEntries(): Promise { if (state.entries.length < initialCount) { await saveQueue(state); - console.log('[PDF Queue] Cleaned up', initialCount - state.entries.length, 'old entries'); + console.log('[Proxy Queue] Cleaned up', initialCount - state.entries.length, 'old entries'); } } /** - * Enforce maximum entries by removing oldest terminal entries + * Enforce maximum entries by removing oldest archived entries */ -async function enforceMaxEntries(state: PDFQueueState): Promise { - const terminalStatuses: PDFStatus[] = ['sent', 'canceled', 'failed']; - +async function enforceMaxEntries(state: ProxyQueueState): Promise { while (state.entries.length > MAX_ENTRIES) { let removeIndex = -1; let oldestTime = Infinity; - // Find oldest terminal entry (sent, canceled, failed) + // Find oldest archived entry first for (let i = state.entries.length - 1; i >= 0; i--) { const entry = state.entries[i]; - if (terminalStatuses.includes(entry.status)) { + if (entry.archived) { const entryTime = entry.orderedAt || entry.uploadedAt || entry.timestamp; if (entryTime < oldestTime) { oldestTime = entryTime; @@ -313,9 +483,24 @@ async function enforceMaxEntries(state: PDFQueueState): Promise { } } - // If no terminal entries, we can't remove any more + // If no archived entries, find oldest terminal live entry if (removeIndex === -1) { - console.warn('[PDF Queue] Max entries reached, but all are active'); + const terminalStatuses: BinectStatus[] = ['sent', 'canceled']; + for (let i = state.entries.length - 1; i >= 0; i--) { + const entry = state.entries[i]; + if (terminalStatuses.includes(entry.binectStatus)) { + const entryTime = entry.orderedAt || entry.uploadedAt || entry.timestamp; + if (entryTime < oldestTime) { + oldestTime = entryTime; + removeIndex = i; + } + } + } + } + + // If still nothing to remove, we can't shrink + if (removeIndex === -1) { + console.warn('[Proxy Queue] Max entries reached, but all are active'); break; }