generated from coulomb/repo-seed
Implement document proxy concept with archive/live views
- Add content hash (MD5) for document deduplication - Separate local state (archived) from server state (binectStatus) - Add archive toggle button to switch between live/archived views - Add archive/restore/delete actions for documents - Refactor pdf-queue.ts with DocumentProxy interface - Add hash.ts utility for content hashing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
<div class="header">
|
||||
<h1>BinectChrome</h1>
|
||||
<div class="header-actions">
|
||||
<button id="archiveToggleBtn" class="toggle-btn" aria-label="Toggle archive view" title="Show archived">
|
||||
<svg id="archiveIcon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||||
<rect x="1" y="3" width="22" height="5"></rect>
|
||||
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||||
</svg>
|
||||
<svg id="liveIcon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="refreshBtn" class="icon-btn" aria-label="Refresh status from server" title="Refresh status">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
|
||||
@@ -7,7 +7,8 @@ import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } fr
|
||||
import { uploadPDF, testConnection, BinectAPIError, Document } from '../utils/binect-api';
|
||||
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
|
||||
import { addTrackingEntry } from '../tracking/tracker';
|
||||
import { PDFQueueEntry, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue';
|
||||
import { DocumentProxy, PDFQueueEntry, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue';
|
||||
import { computeMD5 } from '../utils/hash';
|
||||
|
||||
// DOM Elements
|
||||
const authView = document.getElementById('authView')!;
|
||||
@@ -25,6 +26,9 @@ const loginBtn = document.getElementById('loginBtn') as HTMLButtonElement;
|
||||
const authError = document.getElementById('authError')!;
|
||||
|
||||
const logoutBtn = document.getElementById('logoutBtn')!;
|
||||
const archiveToggleBtn = document.getElementById('archiveToggleBtn')!;
|
||||
const archiveIcon = document.getElementById('archiveIcon')!;
|
||||
const liveIcon = document.getElementById('liveIcon')!;
|
||||
const refreshBtn = document.getElementById('refreshBtn')!;
|
||||
const helpBtn = document.getElementById('helpBtn')!;
|
||||
const togglePasswordBtn = document.getElementById('togglePassword') as HTMLButtonElement;
|
||||
@@ -32,8 +36,9 @@ const eyeIcon = document.getElementById('eyeIcon')!;
|
||||
const eyeOffIcon = document.getElementById('eyeOffIcon')!;
|
||||
|
||||
// State
|
||||
let pdfQueue: PDFQueueEntry[] = [];
|
||||
let pdfQueue: DocumentProxy[] = [];
|
||||
let currentCredentials: { username: string; password: string } | null = null;
|
||||
let showingArchive = false; // false = live view, true = archive view
|
||||
|
||||
/**
|
||||
* Initialize popup
|
||||
@@ -76,11 +81,35 @@ async function init() {
|
||||
function setupEventListeners() {
|
||||
loginForm.addEventListener('submit', handleLogin);
|
||||
logoutBtn.addEventListener('click', handleLogout);
|
||||
archiveToggleBtn.addEventListener('click', handleToggleArchiveView);
|
||||
refreshBtn.addEventListener('click', handleRefreshAll);
|
||||
helpBtn.addEventListener('click', handleHelp);
|
||||
togglePasswordBtn.addEventListener('click', handleTogglePassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle toggle between live and archive view
|
||||
*/
|
||||
async function handleToggleArchiveView() {
|
||||
showingArchive = !showingArchive;
|
||||
|
||||
// Update toggle button appearance
|
||||
if (showingArchive) {
|
||||
archiveIcon.style.display = 'none';
|
||||
liveIcon.style.display = 'block';
|
||||
archiveToggleBtn.title = 'Show live documents';
|
||||
archiveToggleBtn.classList.add('active');
|
||||
} else {
|
||||
archiveIcon.style.display = 'block';
|
||||
liveIcon.style.display = 'none';
|
||||
archiveToggleBtn.title = 'Show archived';
|
||||
archiveToggleBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
// Reload the appropriate list
|
||||
await loadPDFQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle password visibility toggle
|
||||
*/
|
||||
@@ -152,43 +181,50 @@ async function handleLogin(e: Event) {
|
||||
* Load PDF queue from background and current tab
|
||||
*/
|
||||
async function loadPDFQueue() {
|
||||
console.log('[Popup] Loading PDF queue...');
|
||||
console.log('[Popup] Loading PDF queue, showingArchive:', showingArchive);
|
||||
|
||||
// Get all PDFs from background script (including completed ones)
|
||||
let response = await chrome.runtime.sendMessage({ action: 'getAllPDFs' });
|
||||
pdfQueue = response?.entries || [];
|
||||
console.log('[Popup] Got', pdfQueue.length, 'entries from background');
|
||||
if (showingArchive) {
|
||||
// Get archived proxies only
|
||||
const response = await chrome.runtime.sendMessage({ action: 'getArchivedProxies' });
|
||||
pdfQueue = response?.entries || [];
|
||||
console.log('[Popup] Got', pdfQueue.length, 'archived entries');
|
||||
} else {
|
||||
// Get live proxies
|
||||
let response = await chrome.runtime.sendMessage({ action: 'getLiveProxies' });
|
||||
pdfQueue = response?.entries || [];
|
||||
console.log('[Popup] Got', pdfQueue.length, 'live entries from background');
|
||||
|
||||
// Check current tab for PDF and add to persistent queue via background
|
||||
const currentTabPDF = await checkCurrentTabForPDF();
|
||||
if (currentTabPDF) {
|
||||
// Add via background service (will check for duplicates/dismissed)
|
||||
const addResult = await chrome.runtime.sendMessage({
|
||||
action: 'addPDF',
|
||||
pdf: currentTabPDF
|
||||
});
|
||||
if (addResult?.entry) {
|
||||
console.log('[Popup] Added current tab PDF to persistent queue:', currentTabPDF.filename);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check recent downloads if queue is still empty
|
||||
if (pdfQueue.length === 0) {
|
||||
console.log('[Popup] Queue empty, checking recent downloads...');
|
||||
const recentPDFs = await checkRecentDownloads();
|
||||
for (const pdf of recentPDFs) {
|
||||
// Add each PDF via background service (will check for duplicates/dismissed)
|
||||
await chrome.runtime.sendMessage({
|
||||
// Check current tab for PDF and add to persistent queue via background
|
||||
const currentTabPDF = await checkCurrentTabForPDF();
|
||||
if (currentTabPDF) {
|
||||
// Add via background service (will check for duplicates)
|
||||
const addResult = await chrome.runtime.sendMessage({
|
||||
action: 'addPDF',
|
||||
pdf
|
||||
pdf: currentTabPDF
|
||||
});
|
||||
if (addResult?.entry) {
|
||||
console.log('[Popup] Added current tab PDF to persistent queue:', currentTabPDF.filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload queue after potential additions
|
||||
response = await chrome.runtime.sendMessage({ action: 'getAllPDFs' });
|
||||
pdfQueue = response?.entries || [];
|
||||
console.log('[Popup] Final queue count:', pdfQueue.length);
|
||||
// Fallback: check recent downloads if queue is still empty
|
||||
if (pdfQueue.length === 0) {
|
||||
console.log('[Popup] Queue empty, checking recent downloads...');
|
||||
const recentPDFs = await checkRecentDownloads();
|
||||
for (const pdf of recentPDFs) {
|
||||
// Add each PDF via background service (will check for duplicates)
|
||||
await chrome.runtime.sendMessage({
|
||||
action: 'addPDF',
|
||||
pdf
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reload live queue after potential additions
|
||||
response = await chrome.runtime.sendMessage({ action: 'getLiveProxies' });
|
||||
pdfQueue = response?.entries || [];
|
||||
console.log('[Popup] Final live queue count:', pdfQueue.length);
|
||||
}
|
||||
|
||||
// Render the list
|
||||
renderPDFList();
|
||||
@@ -244,15 +280,6 @@ async function checkRecentDownloads(): Promise<DetectedPDF[]> {
|
||||
* Render the PDF list with grouped sections
|
||||
*/
|
||||
function renderPDFList() {
|
||||
// Group PDFs by status category
|
||||
const pendingUpload = pdfQueue.filter(p => p.status === 'pending' || p.status === 'uploading' || p.status === 'failed');
|
||||
const inBasket = pdfQueue.filter(p => p.status === 'in_basket' || p.status === 'ordering');
|
||||
const inProduction = pdfQueue.filter(p => p.status === 'in_production');
|
||||
const completed = pdfQueue.filter(p => p.status === 'sent' || p.status === 'canceled');
|
||||
|
||||
// Count actionable items
|
||||
const actionableCount = pendingUpload.length + inBasket.length;
|
||||
|
||||
if (pdfQueue.length === 0) {
|
||||
showNoPDF();
|
||||
return;
|
||||
@@ -261,46 +288,66 @@ function renderPDFList() {
|
||||
noPdfView.style.display = 'none';
|
||||
pdfListView.style.display = 'block';
|
||||
|
||||
// Update count
|
||||
if (actionableCount > 0) {
|
||||
pdfCount.textContent = `${actionableCount} PDF${actionableCount > 1 ? 's' : ''} need attention`;
|
||||
} else if (inProduction.length > 0) {
|
||||
pdfCount.textContent = `${inProduction.length} in production`;
|
||||
} else {
|
||||
pdfCount.textContent = `${completed.length} completed`;
|
||||
}
|
||||
|
||||
// Build HTML for each section
|
||||
let html = '';
|
||||
|
||||
if (pendingUpload.length > 0) {
|
||||
html += `<div class="pdf-section">
|
||||
<div class="pdf-section-header">Ready to Upload</div>
|
||||
${pendingUpload.map(pdf => renderPDFItem(pdf, 'pending')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
if (showingArchive) {
|
||||
// Archive view - show all archived documents in a flat list
|
||||
pdfCount.textContent = `${pdfQueue.length} archived document${pdfQueue.length !== 1 ? 's' : ''}`;
|
||||
|
||||
if (inBasket.length > 0) {
|
||||
html += `<div class="pdf-section">
|
||||
<div class="pdf-section-header">In Basket</div>
|
||||
${inBasket.map(pdf => renderPDFItem(pdf, 'basket')).join('')}
|
||||
html = `<div class="pdf-section">
|
||||
<div class="pdf-section-header">Archived Documents</div>
|
||||
${pdfQueue.map(pdf => renderPDFItem(pdf, 'archived')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
} else {
|
||||
// Live view - group by Binect status
|
||||
const pendingUpload = pdfQueue.filter(p => p.binectStatus === 'pending' || p.binectStatus === 'uploading' || p.binectStatus === 'failed');
|
||||
const inBasket = pdfQueue.filter(p => p.binectStatus === 'in_basket' || p.binectStatus === 'ordering');
|
||||
const inProduction = pdfQueue.filter(p => p.binectStatus === 'in_production');
|
||||
const completed = pdfQueue.filter(p => p.binectStatus === 'sent' || p.binectStatus === 'canceled');
|
||||
|
||||
if (inProduction.length > 0) {
|
||||
html += `<div class="pdf-section">
|
||||
<div class="pdf-section-header">In Production</div>
|
||||
${inProduction.map(pdf => renderPDFItem(pdf, 'production')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
// Count actionable items
|
||||
const actionableCount = pendingUpload.length + inBasket.length;
|
||||
|
||||
if (completed.length > 0) {
|
||||
// Show only last 5 completed items
|
||||
const recentCompleted = completed.slice(0, 5);
|
||||
html += `<div class="pdf-section pdf-section-completed">
|
||||
<div class="pdf-section-header">Recently Completed</div>
|
||||
${recentCompleted.map(pdf => renderPDFItem(pdf, 'completed')).join('')}
|
||||
</div>`;
|
||||
// Update count
|
||||
if (actionableCount > 0) {
|
||||
pdfCount.textContent = `${actionableCount} PDF${actionableCount > 1 ? 's' : ''} need attention`;
|
||||
} else if (inProduction.length > 0) {
|
||||
pdfCount.textContent = `${inProduction.length} in production`;
|
||||
} else if (completed.length > 0) {
|
||||
pdfCount.textContent = `${completed.length} completed`;
|
||||
} else {
|
||||
pdfCount.textContent = 'No documents';
|
||||
}
|
||||
|
||||
if (pendingUpload.length > 0) {
|
||||
html += `<div class="pdf-section">
|
||||
<div class="pdf-section-header">Ready to Upload</div>
|
||||
${pendingUpload.map(pdf => renderPDFItem(pdf, 'pending')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (inBasket.length > 0) {
|
||||
html += `<div class="pdf-section">
|
||||
<div class="pdf-section-header">In Basket</div>
|
||||
${inBasket.map(pdf => renderPDFItem(pdf, 'basket')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (inProduction.length > 0) {
|
||||
html += `<div class="pdf-section">
|
||||
<div class="pdf-section-header">In Production</div>
|
||||
${inProduction.map(pdf => renderPDFItem(pdf, 'production')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (completed.length > 0) {
|
||||
// Show only last 5 completed items in live view
|
||||
const recentCompleted = completed.slice(0, 5);
|
||||
html += `<div class="pdf-section pdf-section-completed">
|
||||
<div class="pdf-section-header">Recently Completed</div>
|
||||
${recentCompleted.map(pdf => renderPDFItem(pdf, 'completed')).join('')}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
pdfList.innerHTML = html;
|
||||
@@ -313,8 +360,8 @@ function renderPDFList() {
|
||||
/**
|
||||
* Render a single PDF item
|
||||
*/
|
||||
function renderPDFItem(pdf: PDFQueueEntry, section: 'pending' | 'basket' | 'production' | 'completed'): string {
|
||||
const statusClass = getStatusClass(pdf.status);
|
||||
function renderPDFItem(pdf: DocumentProxy, section: 'pending' | 'basket' | 'production' | 'completed' | 'archived'): string {
|
||||
const statusClass = getStatusClass(pdf.binectStatus);
|
||||
const statusText = getStatusText(pdf);
|
||||
const priceText = pdf.price ? `${(pdf.price / 100).toFixed(2)} €` : '';
|
||||
|
||||
@@ -323,34 +370,42 @@ function renderPDFItem(pdf: PDFQueueEntry, section: 'pending' | 'basket' | 'prod
|
||||
switch (section) {
|
||||
case 'pending':
|
||||
actionsHtml = `
|
||||
<button class="btn-send-item" data-id="${escapeHtml(pdf.id)}" ${pdf.status === 'uploading' ? 'disabled' : ''}>
|
||||
${pdf.status === 'uploading' ? 'Uploading...' : (pdf.status === 'failed' ? 'Retry' : 'Upload')}
|
||||
<button class="btn-send-item" data-id="${escapeHtml(pdf.id)}" ${pdf.binectStatus === 'uploading' ? 'disabled' : ''}>
|
||||
${pdf.binectStatus === 'uploading' ? 'Uploading...' : (pdf.binectStatus === 'failed' ? 'Retry' : 'Upload')}
|
||||
</button>
|
||||
<button class="btn-dismiss" data-id="${escapeHtml(pdf.id)}">Dismiss</button>
|
||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
||||
`;
|
||||
break;
|
||||
case 'basket':
|
||||
actionsHtml = `
|
||||
<button class="btn-order-item" data-id="${escapeHtml(pdf.id)}" ${pdf.status === 'ordering' ? 'disabled' : ''}>
|
||||
${pdf.status === 'ordering' ? 'Ordering...' : 'Order'}
|
||||
<button class="btn-order-item" data-id="${escapeHtml(pdf.id)}" ${pdf.binectStatus === 'ordering' ? 'disabled' : ''}>
|
||||
${pdf.binectStatus === 'ordering' ? 'Ordering...' : 'Order'}
|
||||
</button>
|
||||
<button class="btn-dismiss" data-id="${escapeHtml(pdf.id)}">Cancel</button>
|
||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
||||
`;
|
||||
break;
|
||||
case 'production':
|
||||
// No individual actions - use global refresh button in header
|
||||
actionsHtml = '';
|
||||
// Archive button only
|
||||
actionsHtml = `
|
||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
||||
`;
|
||||
break;
|
||||
case 'completed':
|
||||
actionsHtml = `
|
||||
<button class="btn-dismiss btn-remove" data-id="${escapeHtml(pdf.id)}">Remove</button>
|
||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
||||
`;
|
||||
break;
|
||||
case 'archived':
|
||||
actionsHtml = `
|
||||
<button class="btn-restore" data-id="${escapeHtml(pdf.id)}">Restore</button>
|
||||
<button class="btn-dismiss btn-remove" data-id="${escapeHtml(pdf.id)}">Delete</button>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="pdf-list-item ${statusClass}" data-id="${escapeHtml(pdf.id)}">
|
||||
<div class="pdf-item-icon">${getStatusIcon(pdf.status)}</div>
|
||||
<div class="pdf-item-icon">${getStatusIcon(pdf.binectStatus)}</div>
|
||||
<div class="pdf-item-details">
|
||||
<div class="pdf-item-filename" title="${escapeHtml(pdf.filename)}">${escapeHtml(pdf.filename)}</div>
|
||||
<div class="pdf-item-meta">
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
37
src/utils/hash.ts
Normal file
37
src/utils/hash.ts
Normal file
@@ -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<string> {
|
||||
// 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}`;
|
||||
}
|
||||
@@ -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<PDFQueueState> {
|
||||
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<void> {
|
||||
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<PDFQueueEntry | null> {
|
||||
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<ProxyQueueState> {
|
||||
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<void> {
|
||||
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<DocumentProxy | null> {
|
||||
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<DocumentProxy> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
export async function archiveProxy(id: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<PDFQueueEntry[]> {
|
||||
export async function getLiveProxies(): Promise<DocumentProxy[]> {
|
||||
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<PDFQueueEntry[]> {
|
||||
export async function getArchivedProxies(): Promise<DocumentProxy[]> {
|
||||
const state = await loadQueue();
|
||||
return state.entries.filter(e => e.archived);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all proxy documents (live and archived)
|
||||
*/
|
||||
export async function getAllPDFs(): Promise<DocumentProxy[]> {
|
||||
const state = await loadQueue();
|
||||
return state.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get proxies that need user action (pending, failed, in_basket) - live only
|
||||
*/
|
||||
export async function getActionablePDFs(): Promise<DocumentProxy[]> {
|
||||
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<number> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy: Get pending PDFs (for backward compatibility)
|
||||
* Get all Binect document IDs that we're tracking
|
||||
*/
|
||||
export async function getPendingPDFs(): Promise<PDFQueueEntry[]> {
|
||||
return getActionablePDFs();
|
||||
export async function getTrackedBinectIds(): Promise<number[]> {
|
||||
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<number> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
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<void> {
|
||||
const terminalStatuses: PDFStatus[] = ['sent', 'canceled', 'failed'];
|
||||
|
||||
async function enforceMaxEntries(state: ProxyQueueState): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user