diff --git a/public/manifest.json b/public/manifest.json index dce9743..bc85ac6 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -19,6 +19,7 @@ }, "action": { "default_popup": "popup.html", + "default_title": "BinectChrome - Send PDFs to postal mail", "default_icon": { "16": "icons/icon-16.png", "32": "icons/icon-32.png", diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index 6ccd91f..6f229a7 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -19,6 +19,8 @@ import { removePDF, cleanupOldEntries, syncFromServer, + clearServerFields, + attachServerDocument, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue'; @@ -334,6 +336,44 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 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 => { diff --git a/src/popup/popup.css b/src/popup/popup.css index 2f09bee..cdf20ee 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -156,6 +156,51 @@ body { align-items: center; } +/* Pin Reminder Banner */ +.pin-reminder { + background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%); + border-bottom: 1px solid var(--binect-blue); + padding: var(--spacing-sm) var(--spacing-md); +} + +.pin-reminder-content { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); +} + +.pin-reminder-icon { + font-size: 16px; + flex-shrink: 0; +} + +.pin-reminder-text { + flex: 1; + font-size: 12px; + color: var(--text-primary); + line-height: 1.4; +} + +.pin-reminder-text .puzzle-icon { + font-size: 11px; +} + +.pin-reminder-dismiss { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0; + margin-left: var(--spacing-xs); + flex-shrink: 0; +} + +.pin-reminder-dismiss:hover { + color: var(--text-primary); +} + /* Views */ .view { flex: 1; @@ -480,19 +525,44 @@ body { color: var(--binect-blue-deep); } -/* Local tag */ +/* Local tag - clickable to check server */ .tag-local { display: inline-block; margin-left: 6px; - padding: 1px 5px; + padding: 2px 6px; font-size: 9px; - font-weight: 500; + font-weight: 600; text-transform: uppercase; background: var(--light-bg); - color: var(--text-light); + color: var(--text-secondary); border: 1px solid var(--border-color); border-radius: 3px; vertical-align: middle; + cursor: pointer; + transition: all 0.15s ease; +} + +.tag-local:hover { + background: var(--binect-blue); + color: white; + border-color: var(--binect-blue); +} + +.tag-local:active { + transform: scale(0.95); +} + +.tag-local.checking { + background: var(--warning-bg); + color: var(--warning-text); + border-color: var(--warning-text); + cursor: wait; +} + +.tag-local.synced { + background: var(--success-bg); + color: var(--success-text); + border-color: var(--success-text); } /* Recipient address */ @@ -738,6 +808,235 @@ body { border-radius: 2px; } +/* Footer button styled as link */ +.footer-link-btn { + background: none; + border: none; + font-size: 12px; + color: var(--binect-blue); + cursor: pointer; + padding: 0; + font-family: inherit; +} + +.footer-link-btn:hover { + text-decoration: underline; +} + +.footer-link-btn:focus { + outline: 2px solid var(--binect-blue); + outline-offset: 2px; + border-radius: 2px; +} + +/* Issue Report Modal */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; +} + +.modal-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); +} + +.modal-content { + position: relative; + background: var(--paper); + border-radius: var(--border-radius); + max-height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-md); + border-bottom: 1px solid var(--border-color); + background: var(--light-bg); +} + +.modal-header h2 { + font-size: 16px; + font-weight: 600; + margin: 0; +} + +.modal-close { + background: none; + border: none; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + padding: 0; + line-height: 1; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.modal-close:hover { + background: var(--border-color); + color: var(--text-primary); +} + +.modal-body { + padding: var(--spacing-md); + overflow-y: auto; + flex: 1; +} + +.modal-body .form-group { + margin-bottom: var(--spacing-md); +} + +.modal-body textarea { + width: 100%; + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-family: inherit; + font-size: 14px; + resize: vertical; + min-height: 80px; +} + +.modal-body textarea:focus { + outline: none; + border-color: var(--binect-blue); + box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); +} + +.modal-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--border-color); +} + +.submit-link { + font-size: 13px; + color: var(--binect-blue); + text-decoration: none; +} + +.submit-link:hover { + text-decoration: underline; +} + +/* Context Sections */ +.context-sections { + margin-top: var(--spacing-md); +} + +.context-sections h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: var(--spacing-xs); + color: var(--text-primary); +} + +.context-hint { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: var(--spacing-md); +} + +.context-section { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + margin-bottom: var(--spacing-sm); + overflow: hidden; +} + +.context-section-header { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--light-bg); + padding: var(--spacing-xs) var(--spacing-sm); +} + +.context-toggle { + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: 13px; + color: var(--text-primary); + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs); +} + +.context-toggle:hover { + color: var(--binect-blue); +} + +.toggle-icon { + font-size: 10px; + transition: transform 0.2s; + display: inline-block; +} + +.toggle-icon.open { + transform: rotate(90deg); +} + +.exclude-checkbox { + display: flex; + align-items: center; + gap: var(--spacing-xs); + font-size: 11px; + color: var(--text-secondary); + cursor: pointer; +} + +.exclude-checkbox input { + margin: 0; + cursor: pointer; +} + +.context-section-content { + padding: var(--spacing-sm); + background: var(--paper); + border-top: 1px solid var(--border-color); +} + +.context-section-content pre { + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + font-size: 11px; + white-space: pre-wrap; + word-break: break-all; + margin: 0; + color: var(--text-secondary); + max-height: 150px; + overflow-y: auto; +} + +/* Copy success feedback */ +.btn-copy-success { + background: var(--signal-green) !important; +} + /* Accessibility */ @media (prefers-reduced-motion: reduce) { * { diff --git a/src/popup/popup.html b/src/popup/popup.html index 9f1ca5b..7ca19de 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -26,6 +26,18 @@ + + +

Please sign in to send PDFs to Binect

@@ -88,9 +100,115 @@ +
+ + + diff --git a/src/popup/popup.ts b/src/popup/popup.ts index 3c62c31..4004865 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -34,6 +34,28 @@ const togglePasswordBtn = document.getElementById('togglePassword') as HTMLButto const eyeIcon = document.getElementById('eyeIcon')!; const eyeOffIcon = document.getElementById('eyeOffIcon')!; +// Issue Report Modal Elements +const issueModal = document.getElementById('issueModal')!; +const reportIssueBtn = document.getElementById('reportIssueBtn')!; +const closeModalBtn = document.getElementById('closeModalBtn')!; +const modalBackdrop = issueModal.querySelector('.modal-backdrop')!; +const issueTitleInput = document.getElementById('issueTitle') as HTMLInputElement; +const issueDescriptionInput = document.getElementById('issueDescription') as HTMLTextAreaElement; +const copyToClipboardBtn = document.getElementById('copyToClipboardBtn') as HTMLButtonElement; + +// Context section content elements +const extensionInfoContent = document.getElementById('extensionInfoContent')!; +const browserInfoContent = document.getElementById('browserInfoContent')!; +const documentStatusContent = document.getElementById('documentStatusContent')!; +const recentErrorsContent = document.getElementById('recentErrorsContent')!; + +// Error tracking for issue reports +const recentErrors: Array<{ timestamp: number; message: string; stack?: string }> = []; + +// Pin Reminder Elements +const pinReminder = document.getElementById('pinReminder')!; +const dismissPinReminderBtn = document.getElementById('dismissPinReminder')!; + // State let pdfQueue: DocumentProxy[] = []; let currentCredentials: { username: string; password: string } | null = null; @@ -48,6 +70,9 @@ let refreshIndex = 0; * Initialize popup */ async function init() { + // Check and show first-run pin reminder + await checkFirstRunReminder(); + // Check if user has credentials const credentials = await loadCredentials(); @@ -79,6 +104,38 @@ async function init() { setupEventListeners(); } +/** + * Check if this is first run and show pin reminder + */ +async function checkFirstRunReminder() { + const STORAGE_KEY = 'pinReminderDismissed'; + + try { + const result = await chrome.storage.local.get(STORAGE_KEY); + if (!result[STORAGE_KEY]) { + // First run - show the reminder + pinReminder.style.display = 'block'; + } + } catch (error) { + console.error('[Popup] Error checking first run:', error); + } +} + +/** + * Dismiss the pin reminder and remember the choice + */ +async function dismissPinReminder() { + const STORAGE_KEY = 'pinReminderDismissed'; + + pinReminder.style.display = 'none'; + + try { + await chrome.storage.local.set({ [STORAGE_KEY]: true }); + } catch (error) { + console.error('[Popup] Error saving pin reminder state:', error); + } +} + /** * Setup event listeners */ @@ -96,6 +153,11 @@ function setupEventListeners() { handleTogglePassword(); }); } + + // Pin reminder dismiss button + if (dismissPinReminderBtn) { + dismissPinReminderBtn.addEventListener('click', dismissPinReminder); + } } /** @@ -588,7 +650,7 @@ function renderPDFItem(pdf: DocumentProxy, section: 'pending' | 'erroneous' | 'b
${escapeHtml(displayFilename)} - ${isLocalOnly ? 'local' : ''} + ${isLocalOnly ? `local` : ''}
${metaParts.length > 0 ? `
${metaParts.join(' · ')}
` : ''} ${pdf.recipientAddress ? `
${escapeHtml(pdf.recipientAddress.split('\n')[0])}
` : ''} @@ -653,6 +715,15 @@ function setupPDFListEventListeners() { if (id) handleDeletePDF(id); }); }); + + // Local tag clicks - check server for document + pdfList.querySelectorAll('.tag-local').forEach(tag => { + tag.addEventListener('click', (e) => { + e.stopPropagation(); // Don't bubble to parent + const id = (e.target as HTMLElement).dataset.id; + if (id) handleCheckServer(id); + }); + }); } /** @@ -1116,8 +1187,141 @@ async function handleRestorePDF(id: string) { setTimeout(() => hideStatus(), 2000); } +/** + * Handle check server for a local document + * Checks if document exists on server (by ID if known, or by filename) + */ +async function handleCheckServer(id: string) { + const pdf = pdfQueue.find(p => p.id === id); + if (!pdf || !currentCredentials) { + return; + } + + // Find the tag element to show visual feedback + const tagElement = pdfList.querySelector(`.tag-local[data-id="${id}"]`) as HTMLElement; + if (tagElement) { + tagElement.classList.add('checking'); + tagElement.textContent = '...'; + } + + try { + // First, try to find by known document ID if available + if (pdf.binectDocumentId) { + try { + const result = await chrome.runtime.sendMessage({ + action: 'getDocumentStatus', + documentId: pdf.binectDocumentId, + username: currentCredentials.username, + password: currentCredentials.password + }); + + if (result.success) { + // Document still exists on server, re-attach it + await chrome.runtime.sendMessage({ + action: 'attachServerDocument', + id, + binectDocumentId: pdf.binectDocumentId, + binectStatusCode: result.status, + binectStatusText: result.statusText, + price: result.price, + recipientAddress: result.recipientAddress, + errorMessage: result.errorDetails + }); + + // Update local state + pdf.binectStatusCode = result.status; + pdf.binectStatusText = result.statusText; + if (result.price !== undefined) pdf.price = result.price; + if (result.recipientAddress) pdf.recipientAddress = result.recipientAddress; + + renderPDFList(); + showStatus('Document found on server and re-linked', 'success'); + setTimeout(() => hideStatus(), 2000); + return; + } + // If 404, continue to filename search + } catch { + // Document not found by ID, try filename search + } + } + + // Try to find by filename in server documents + const listResult = await chrome.runtime.sendMessage({ + action: 'listServerDocuments', + username: currentCredentials.username, + password: currentCredentials.password + }); + + if (!listResult.success || !listResult.documents) { + throw new Error('Failed to list server documents'); + } + + // Extract just the filename for comparison (without path) + const targetFilename = extractFilename(pdf.filename).toLowerCase(); + + // Find matching document by filename + const match = (listResult.documents as Array<{ + id: number; + filename: string; + status: number; + statusText: string; + price?: number; + recipientAddress?: string; + errorDetails?: string; + }>).find(doc => { + const docFilename = extractFilename(doc.filename).toLowerCase(); + return docFilename === targetFilename; + }); + + if (match) { + // Found a matching document, attach it + await chrome.runtime.sendMessage({ + action: 'attachServerDocument', + id, + binectDocumentId: match.id, + binectStatusCode: match.status, + binectStatusText: match.statusText, + price: match.price, + recipientAddress: match.recipientAddress, + errorMessage: match.errorDetails + }); + + // Update local state + pdf.binectDocumentId = match.id; + pdf.binectStatusCode = match.status; + pdf.binectStatusText = match.statusText; + if (match.price !== undefined) pdf.price = match.price; + if (match.recipientAddress) pdf.recipientAddress = match.recipientAddress; + if (match.errorDetails) pdf.errorMessage = match.errorDetails; + + renderPDFList(); + showStatus(`Found on server (ID: ${match.id})`, 'success'); + setTimeout(() => hideStatus(), 2000); + } else { + // No matching document found + if (tagElement) { + tagElement.classList.remove('checking'); + tagElement.textContent = 'local'; + } + showStatus('Not found on server', 'error'); + setTimeout(() => hideStatus(), 2000); + } + + } catch (error) { + console.error('[Popup] Check server error:', error); + if (tagElement) { + tagElement.classList.remove('checking'); + tagElement.textContent = 'local'; + } + const errorMessage = error instanceof Error ? error.message : 'Check failed'; + showStatus(errorMessage, 'error'); + setTimeout(() => hideStatus(), 2000); + } +} + /** * Handle delete from server (for erroneous or canceled documents) + * After deleting, the proxy is archived and becomes local-only */ async function handleDeleteFromServer(id: string) { const pdf = pdfQueue.find(p => p.id === id); @@ -1137,12 +1341,26 @@ async function handleDeleteFromServer(id: string) { throw new Error(result.error || 'Failed to delete from server'); } - // Remove from local queue after successful server deletion - await chrome.runtime.sendMessage({ action: 'removePDF', id }); + // Clear server fields to make it local-only + await chrome.runtime.sendMessage({ action: 'clearServerFields', id }); + + // Archive the proxy (it's now local-only) + await chrome.runtime.sendMessage({ action: 'archiveProxy', id }); + + // Update local state + pdf.binectDocumentId = undefined; + pdf.binectStatusCode = undefined; + pdf.binectStatusText = undefined; + pdf.binectStatus = 'pending'; + pdf.price = undefined; + pdf.recipientAddress = undefined; + pdf.errorMessage = undefined; + + // Remove from current view (it's now archived) pdfQueue = pdfQueue.filter(p => p.id !== id); renderPDFList(); - showStatus('Document deleted from server', 'success'); + showStatus('Document deleted from server and archived', 'success'); setTimeout(() => hideStatus(), 2000); } catch (error) { @@ -1347,5 +1565,265 @@ function formatTimestamp(timestamp: number): string { } } +// ============================================================================= +// Issue Report Modal +// ============================================================================= + +/** + * Track errors for issue reports + */ +function trackError(error: Error | string, stack?: string) { + const errorEntry = { + timestamp: Date.now(), + message: typeof error === 'string' ? error : error.message, + stack: stack || (error instanceof Error ? error.stack : undefined) + }; + recentErrors.unshift(errorEntry); + // Keep only last 10 errors + if (recentErrors.length > 10) { + recentErrors.pop(); + } +} + +/** + * Get extension info for issue report + */ +function getExtensionInfo(): string { + const manifest = chrome.runtime.getManifest(); + return JSON.stringify({ + name: manifest.name, + version: manifest.version, + manifestVersion: manifest.manifest_version + }, null, 2); +} + +/** + * Get browser info for issue report + */ +function getBrowserInfo(): string { + const ua = navigator.userAgent; + let browserName = 'Unknown'; + let browserVersion = 'Unknown'; + + // Parse user agent for browser info + if (ua.includes('Chrome/')) { + browserName = 'Chrome'; + const match = ua.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/); + if (match) browserVersion = match[1]; + } else if (ua.includes('Firefox/')) { + browserName = 'Firefox'; + const match = ua.match(/Firefox\/(\d+\.\d+)/); + if (match) browserVersion = match[1]; + } else if (ua.includes('Edge/')) { + browserName = 'Edge'; + const match = ua.match(/Edge\/(\d+\.\d+)/); + if (match) browserVersion = match[1]; + } + + return JSON.stringify({ + browser: browserName, + version: browserVersion, + platform: navigator.platform, + language: navigator.language, + userAgent: ua + }, null, 2); +} + +/** + * Get document status for issue report + */ +function getDocumentStatusInfo(): string { + if (pdfQueue.length === 0) { + return 'No documents in queue'; + } + + const summary = { + total: pdfQueue.length, + byStatus: {} as Record, + documents: pdfQueue.map(doc => ({ + id: doc.id.substring(0, 8) + '...', + binectStatus: doc.binectStatus, + binectStatusCode: doc.binectStatusCode, + hasError: !!doc.errorMessage + })) + }; + + // Count by status + for (const doc of pdfQueue) { + summary.byStatus[doc.binectStatus] = (summary.byStatus[doc.binectStatus] || 0) + 1; + } + + return JSON.stringify(summary, null, 2); +} + +/** + * Get recent errors for issue report + */ +function getRecentErrorsInfo(): string { + if (recentErrors.length === 0) { + return 'No recent errors'; + } + + return recentErrors.map(err => { + const date = new Date(err.timestamp).toISOString(); + let text = `[${date}] ${err.message}`; + if (err.stack) { + text += `\n Stack: ${err.stack.split('\n').slice(0, 3).join('\n ')}`; + } + return text; + }).join('\n\n'); +} + +/** + * Open the issue report modal + */ +function openIssueModal() { + // Clear previous input + issueTitleInput.value = ''; + issueDescriptionInput.value = ''; + + // Reset checkboxes + issueModal.querySelectorAll('input[type="checkbox"]').forEach(cb => { + (cb as HTMLInputElement).checked = false; + }); + + // Populate context sections + extensionInfoContent.textContent = getExtensionInfo(); + browserInfoContent.textContent = getBrowserInfo(); + documentStatusContent.textContent = getDocumentStatusInfo(); + recentErrorsContent.textContent = getRecentErrorsInfo(); + + // Close all sections by default + issueModal.querySelectorAll('.context-section-content').forEach(section => { + (section as HTMLElement).style.display = 'none'; + }); + issueModal.querySelectorAll('.toggle-icon').forEach(icon => { + icon.textContent = 'â–¶'; + }); + + // Show modal + issueModal.style.display = 'flex'; +} + +/** + * Close the issue report modal + */ +function closeIssueModal() { + issueModal.style.display = 'none'; +} + +/** + * Toggle a context section + */ +function toggleContextSection(sectionId: string) { + const content = document.getElementById(sectionId); + const button = issueModal.querySelector(`[data-section="${sectionId}"]`); + const icon = button?.querySelector('.toggle-icon'); + + if (content && icon) { + const isVisible = content.style.display !== 'none'; + content.style.display = isVisible ? 'none' : 'block'; + icon.textContent = isVisible ? 'â–¶' : 'â–¼'; + } +} + +/** + * Format issue report as Markdown and copy to clipboard + */ +async function copyIssueToClipboard() { + const title = issueTitleInput.value.trim(); + const description = issueDescriptionInput.value.trim(); + + // Build markdown content + let markdown = ''; + + if (title) { + markdown += `# ${title}\n\n`; + } + + if (description) { + markdown += `## Description\n\n${description}\n\n`; + } + + // Add context sections (only those not excluded) + const sections = [ + { id: 'extensionInfo', name: 'Extension Info', content: extensionInfoContent.textContent }, + { id: 'browserInfo', name: 'Browser Info', content: browserInfoContent.textContent }, + { id: 'documentStatus', name: 'Document Status', content: documentStatusContent.textContent }, + { id: 'recentErrors', name: 'Recent Errors', content: recentErrorsContent.textContent } + ]; + + let hasContext = false; + for (const section of sections) { + const checkbox = issueModal.querySelector(`input[data-exclude="${section.id}"]`) as HTMLInputElement; + if (!checkbox?.checked && section.content && section.content !== 'No recent errors' && section.content !== 'No documents in queue') { + if (!hasContext) { + markdown += `## Context\n\n`; + hasContext = true; + } + markdown += `### ${section.name}\n\n\`\`\`json\n${section.content}\n\`\`\`\n\n`; + } + } + + // Copy to clipboard + try { + await navigator.clipboard.writeText(markdown); + + // Show success feedback + const originalText = copyToClipboardBtn.textContent; + copyToClipboardBtn.textContent = 'Copied!'; + copyToClipboardBtn.classList.add('btn-copy-success'); + + setTimeout(() => { + copyToClipboardBtn.textContent = originalText; + copyToClipboardBtn.classList.remove('btn-copy-success'); + }, 2000); + } catch (error) { + console.error('Failed to copy to clipboard:', error); + trackError(error as Error); + + // Show error feedback + copyToClipboardBtn.textContent = 'Copy failed'; + setTimeout(() => { + copyToClipboardBtn.textContent = 'Copy to Clipboard'; + }, 2000); + } +} + +/** + * Setup issue modal event listeners + */ +function setupIssueModalListeners() { + // Open modal + reportIssueBtn.addEventListener('click', openIssueModal); + + // Close modal + closeModalBtn.addEventListener('click', closeIssueModal); + modalBackdrop.addEventListener('click', closeIssueModal); + + // Close on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && issueModal.style.display !== 'none') { + closeIssueModal(); + } + }); + + // Context section toggles + issueModal.querySelectorAll('.context-toggle').forEach(toggle => { + toggle.addEventListener('click', () => { + const sectionId = (toggle as HTMLElement).dataset.section; + if (sectionId) { + toggleContextSection(sectionId); + } + }); + }); + + // Copy to clipboard + copyToClipboardBtn.addEventListener('click', copyIssueToClipboard); +} + // Initialize on load init(); + +// Setup issue modal listeners after DOM is ready +setupIssueModalListeners(); diff --git a/src/utils/pdf-queue.ts b/src/utils/pdf-queue.ts index 8c45690..52aef91 100644 --- a/src/utils/pdf-queue.ts +++ b/src/utils/pdf-queue.ts @@ -337,6 +337,67 @@ export async function archiveProxy(id: string): Promise { console.log('[Proxy Queue] Archived proxy:', id); } +/** + * Clear server-side fields from a proxy (when deleted from server) + * This makes the proxy "local only" again + */ +export async function clearServerFields(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 clearing server fields:', id); + return; + } + + // Clear all server-related fields + entry.binectDocumentId = undefined; + entry.binectStatusCode = undefined; + entry.binectStatusText = undefined; + entry.binectStatus = 'pending'; // Reset to pending since it's no longer on server + entry.price = undefined; + entry.recipientAddress = undefined; + entry.errorMessage = undefined; + entry.uploadedAt = undefined; + entry.orderedAt = undefined; + + await saveQueue(state); + console.log('[Proxy Queue] Cleared server fields for proxy:', id); +} + +/** + * Attach server document to a proxy + * Used when re-linking a local proxy to a server document + */ +export async function attachServerDocument( + id: string, + binectDocumentId: number, + binectStatusCode: number, + binectStatusText: string, + price?: number, + recipientAddress?: string, + errorMessage?: 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 attaching server document:', id); + return; + } + + entry.binectDocumentId = binectDocumentId; + entry.binectStatusCode = binectStatusCode; + entry.binectStatusText = binectStatusText; + entry.binectStatus = mapBinectStatusCode(binectStatusCode); + if (price !== undefined) entry.price = price; + if (recipientAddress) entry.recipientAddress = recipientAddress; + if (errorMessage) entry.errorMessage = errorMessage; + + await saveQueue(state); + console.log('[Proxy Queue] Attached server document', binectDocumentId, 'to proxy:', id); +} + /** * Restore a proxy document (move from archive to live) */