generated from coulomb/repo-seed
Server sync, erroneous doc handling, and @binect/js v0.1.0 integration
- Add server sync to discover documents uploaded elsewhere (fixes missing basket documents issue) - Handle erroneous uploads: preserve binectDocumentId for delete button - Add "Delete from server" button for erroneous and canceled documents - Remove archive button for active documents (in_basket, in_production) - Auto-restore archived documents that have active status - Refactor to use @binect/js v0.1.0 features: - DocumentStatus enum instead of magic numbers - isErroneous(), getErrors() helper functions - getStatusDescription() for status text - Add binect-js improvement requirements document Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
294
docs/binect-js-improvements.md
Normal file
294
docs/binect-js-improvements.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# @binect/js Library Improvement Requirements
|
||||||
|
|
||||||
|
**Version:** 1.1
|
||||||
|
**Date:** 2026-01-16
|
||||||
|
**Author:** BinectChrome Development Team
|
||||||
|
**Status:** Updated after v0.1.0 review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document outlines suggested improvements to the `@binect/js` library based on real-world integration experience building the BinectChrome browser extension.
|
||||||
|
|
||||||
|
**Update (v1.1):** After reviewing `@binect/js` v0.1.0, most requirements have been addressed. This document now reflects the current status and remaining gaps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements Status Summary
|
||||||
|
|
||||||
|
| Requirement | Status | Notes |
|
||||||
|
|-------------|--------|-------|
|
||||||
|
| REQ-1: Status Constants | ✅ **ADDRESSED** | `DocumentStatus` enum exported |
|
||||||
|
| REQ-2: listAll() Method | ❌ **OPEN** | Still requires 2 API calls |
|
||||||
|
| REQ-3: Error Accessibility | ✅ **ADDRESSED** | Helper functions added |
|
||||||
|
| REQ-4: Document ListResponse | ✅ **ADDRESSED** | JSDoc comments improved |
|
||||||
|
| REQ-5: Pagination Docs | ⚠️ **PARTIAL** | Interface exists, no fetchAll helper |
|
||||||
|
| REQ-6: Error Type Definitions | ✅ **ADDRESSED** | `ValidationMessage` interface |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Addressed Requirements
|
||||||
|
|
||||||
|
### REQ-1: Export Document Status Constants ✅
|
||||||
|
|
||||||
|
**Status:** Fully addressed in v0.1.0
|
||||||
|
|
||||||
|
The library now exports a `DocumentStatus` enum:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export enum DocumentStatus {
|
||||||
|
IN_PREPARATION = 1,
|
||||||
|
SHIPPABLE = 2,
|
||||||
|
PRODUCTION_QUEUE = 3,
|
||||||
|
PRINTING = 4,
|
||||||
|
SENT = 5,
|
||||||
|
CANCELED = 6,
|
||||||
|
ERRONEOUS = 7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
import { DocumentStatus } from '@binect/js';
|
||||||
|
|
||||||
|
if (doc.status.code === DocumentStatus.ERRONEOUS) {
|
||||||
|
// Handle erroneous document
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### REQ-3: Improve Error Information Accessibility ✅
|
||||||
|
|
||||||
|
**Status:** Fully addressed in v0.1.0
|
||||||
|
|
||||||
|
The library now exports comprehensive helper functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Status predicates
|
||||||
|
isShippable(doc) // status === 2
|
||||||
|
isErroneous(doc) // status === 7
|
||||||
|
isInPreparation(doc) // status === 1
|
||||||
|
isInProductionQueue(doc) // status === 3
|
||||||
|
isPrinting(doc) // status === 4
|
||||||
|
isSent(doc) // status === 5
|
||||||
|
isCanceled(doc) // status === 6
|
||||||
|
isTerminal(doc) // status in [5, 6, 7]
|
||||||
|
isCancelable(doc) // status in [3, 4]
|
||||||
|
|
||||||
|
// Error extraction
|
||||||
|
getErrors(doc) // ValidationMessage[] of type 'ERROR'
|
||||||
|
getWarnings(doc) // ValidationMessage[] of type 'WARNING'
|
||||||
|
getInfoMessages(doc) // ValidationMessage[] of type 'INFO'
|
||||||
|
hasErrors(doc) // boolean
|
||||||
|
hasWarnings(doc) // boolean
|
||||||
|
|
||||||
|
// Status description
|
||||||
|
getStatusDescription(status) // Human-readable string
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
import { isErroneous, getErrors } from '@binect/js';
|
||||||
|
|
||||||
|
if (isErroneous(doc)) {
|
||||||
|
const errors = getErrors(doc);
|
||||||
|
console.error('Errors:', errors.map(e => e.message).join('; '));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### REQ-4: Document ListResponse Structure ✅
|
||||||
|
|
||||||
|
**Status:** Addressed in v0.1.0
|
||||||
|
|
||||||
|
The `ListResponse<T>` interface now has clear JSDoc documentation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* List response wrapper
|
||||||
|
*/
|
||||||
|
export interface ListResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### REQ-6: Improve Type Definitions for Error Objects ✅
|
||||||
|
|
||||||
|
**Status:** Addressed in v0.1.0
|
||||||
|
|
||||||
|
The `ValidationMessage` interface is now properly typed:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ValidationMessage {
|
||||||
|
type: 'INFO' | 'WARNING' | 'ERROR';
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
page?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Requirements
|
||||||
|
|
||||||
|
### REQ-2: Add Method to List All Documents ❌
|
||||||
|
|
||||||
|
**Status:** NOT ADDRESSED
|
||||||
|
**Priority:** Medium
|
||||||
|
|
||||||
|
#### Problem Statement
|
||||||
|
|
||||||
|
There is still no single method to retrieve all documents regardless of status:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Current: Multiple calls still required
|
||||||
|
const shippable = await client.documents.list(); // Only status 2
|
||||||
|
const erroneous = await client.documents.listErrors(); // Only status 7
|
||||||
|
// Still missing: status 1, 3, 4, 5, 6
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Limitation
|
||||||
|
|
||||||
|
This may be a limitation of the Binect REST API itself, not the JS library. The API only provides:
|
||||||
|
- `GET /documents` - Returns shippable documents (status 2)
|
||||||
|
- `GET /documents/errors` - Returns erroneous documents (status 7)
|
||||||
|
|
||||||
|
There is no endpoint to list documents in other states (in_preparation, in_production, sent, canceled).
|
||||||
|
|
||||||
|
#### Proposed Solutions
|
||||||
|
|
||||||
|
**Option A: Library-level aggregation** (if API supports individual document lookup)
|
||||||
|
```typescript
|
||||||
|
// Library could provide a helper that fetches known IDs
|
||||||
|
async listByIds(documentIds: string[]): Promise<Document[]>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Document the limitation**
|
||||||
|
- Clearly document which documents each endpoint returns
|
||||||
|
- Explain that documents in production (3, 4) cannot be listed, only queried by ID
|
||||||
|
- Provide example of tracking document IDs locally
|
||||||
|
|
||||||
|
**Option C: API Enhancement Request**
|
||||||
|
- Request Binect API team to add `GET /documents/all` or status filter parameter:
|
||||||
|
```
|
||||||
|
GET /documents?status=1,2,3,4,5,6,7
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Impact on BinectChrome
|
||||||
|
|
||||||
|
Currently, BinectChrome can only discover:
|
||||||
|
- Documents ready to ship (status 2)
|
||||||
|
- Documents with errors (status 7)
|
||||||
|
|
||||||
|
Documents in production (3, 4), sent (5), or canceled (6) can only be tracked if we uploaded them and stored their IDs locally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### REQ-5: Document and Improve Pagination ⚠️
|
||||||
|
|
||||||
|
**Status:** PARTIALLY ADDRESSED
|
||||||
|
**Priority:** Low
|
||||||
|
|
||||||
|
#### What's Addressed
|
||||||
|
|
||||||
|
- `PaginationOptions` interface is exported
|
||||||
|
- Methods accept pagination parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface PaginationOptions {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### What's Missing
|
||||||
|
|
||||||
|
1. **Documentation of default values** - What is the default limit?
|
||||||
|
2. **fetchAll helper** - No built-in way to fetch all pages automatically
|
||||||
|
|
||||||
|
#### Proposed Addition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Optional helper for fetching all pages
|
||||||
|
async function fetchAllDocuments(client: BinectClient): Promise<Document[]> {
|
||||||
|
const allDocs: Document[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
const limit = 100;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const response = await client.documents.list({ limit, offset });
|
||||||
|
allDocs.push(...response.items);
|
||||||
|
if (response.items.length < limit) break;
|
||||||
|
offset += limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDocs;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This could be added to the helpers module as `fetchAll()` or similar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Features in v0.1.0
|
||||||
|
|
||||||
|
The following features were added that weren't in our original requirements:
|
||||||
|
|
||||||
|
### Polling Utilities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { pollUntil, waitForShippable } from '@binect/js';
|
||||||
|
|
||||||
|
// Wait for document to become shippable or erroneous
|
||||||
|
const doc = await waitForShippable(
|
||||||
|
() => client.documents.get(docId),
|
||||||
|
{ intervalMs: 2000, maxAttempts: 30 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generic polling
|
||||||
|
const result = await pollUntil(
|
||||||
|
() => fetchSomething(),
|
||||||
|
(result) => result.status === 'complete',
|
||||||
|
{ intervalMs: 1000 }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Encoding Helpers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fileToBase64, bufferToBase64 } from '@binect/js';
|
||||||
|
|
||||||
|
// Browser: File/Blob to base64
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
|
||||||
|
// Node.js: Buffer to base64
|
||||||
|
const base64 = bufferToBase64(buffer);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations for BinectChrome
|
||||||
|
|
||||||
|
Based on the updated library, we should:
|
||||||
|
|
||||||
|
1. **Refactor to use `DocumentStatus` enum** instead of magic numbers
|
||||||
|
2. **Use helper functions** like `isErroneous()`, `getErrors()` instead of manual checks
|
||||||
|
3. **Use `getStatusDescription()`** for human-readable status text
|
||||||
|
4. **Consider using `waitForShippable()`** for upload flow instead of manual polling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Version | Date | Author | Changes |
|
||||||
|
|---------|------|--------|---------|
|
||||||
|
| 1.0 | 2026-01-16 | BinectChrome Team | Initial draft |
|
||||||
|
| 1.1 | 2026-01-16 | BinectChrome Team | Updated after v0.1.0 review - marked addressed requirements |
|
||||||
@@ -18,10 +18,11 @@ import {
|
|||||||
dismissPDF,
|
dismissPDF,
|
||||||
removePDF,
|
removePDF,
|
||||||
cleanupOldEntries,
|
cleanupOldEntries,
|
||||||
|
syncFromServer,
|
||||||
PDFStatus,
|
PDFStatus,
|
||||||
PDFStatusMeta
|
PDFStatusMeta
|
||||||
} from '../utils/pdf-queue';
|
} from '../utils/pdf-queue';
|
||||||
import { shipDocument, getDocumentStatus } from '../utils/binect-api';
|
import { shipDocument, getDocumentStatus, deleteDocument, listServerDocuments } from '../utils/binect-api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize extension on install
|
* Initialize extension on install
|
||||||
@@ -267,6 +268,72 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete a document from the server
|
||||||
|
if (request.action === 'deleteServerDocument') {
|
||||||
|
const { documentId, username, password } = request as {
|
||||||
|
documentId: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteDocument(documentId, username, password)
|
||||||
|
.then(() => {
|
||||||
|
sendResponse({ success: true });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete document'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all documents from the server (for sync)
|
||||||
|
if (request.action === 'listServerDocuments') {
|
||||||
|
const { username, password } = request as {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
listServerDocuments(username, password)
|
||||||
|
.then(documents => {
|
||||||
|
sendResponse({ success: true, documents });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to list documents'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync a server document to local proxy (create or update)
|
||||||
|
if (request.action === 'syncFromServer') {
|
||||||
|
const { binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails } = request as {
|
||||||
|
binectDocumentId: number;
|
||||||
|
filename: string;
|
||||||
|
binectStatusCode: number;
|
||||||
|
binectStatusText: string;
|
||||||
|
price?: number;
|
||||||
|
recipientAddress?: string;
|
||||||
|
errorDetails?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
syncFromServer(binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails)
|
||||||
|
.then(proxy => {
|
||||||
|
sendResponse({ success: true, proxy });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to sync document'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Legacy handlers for backward compatibility
|
// Legacy handlers for backward compatibility
|
||||||
if (request.action === 'getLastPDF') {
|
if (request.action === 'getLastPDF') {
|
||||||
getPendingPDFs().then(entries => {
|
getPendingPDFs().then(entries => {
|
||||||
|
|||||||
@@ -611,6 +611,23 @@ body {
|
|||||||
background: var(--binect-blue-deep);
|
background: var(--binect-blue-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Delete from server button */
|
||||||
|
.btn-delete-server {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--red);
|
||||||
|
border: 1px solid var(--red);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-server:hover {
|
||||||
|
background: var(--red);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* Order button */
|
/* Order button */
|
||||||
.btn-order-item {
|
.btn-order-item {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
|
|||||||
@@ -253,6 +253,27 @@ async function loadPDFQueue() {
|
|||||||
pdfQueue = response?.entries || [];
|
pdfQueue = response?.entries || [];
|
||||||
console.log('[Popup] Got', pdfQueue.length, 'live entries from background');
|
console.log('[Popup] Got', pdfQueue.length, 'live entries from background');
|
||||||
|
|
||||||
|
// Sync with server to discover documents uploaded elsewhere or missing locally
|
||||||
|
if (currentCredentials) {
|
||||||
|
await syncWithServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-restore any archived documents that are in active states (in_basket, in_production)
|
||||||
|
// These should never be archived as it confuses user expectations
|
||||||
|
const archivedResponse = await chrome.runtime.sendMessage({ action: 'getArchivedProxies' });
|
||||||
|
const archivedEntries: DocumentProxy[] = archivedResponse?.entries || [];
|
||||||
|
for (const entry of archivedEntries) {
|
||||||
|
if (entry.binectStatus === 'in_basket' || entry.binectStatus === 'in_production') {
|
||||||
|
console.log('[Popup] Auto-restoring active document from archive:', entry.filename);
|
||||||
|
await chrome.runtime.sendMessage({ action: 'restoreProxy', id: entry.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload live proxies after potential restorations and server sync
|
||||||
|
response = await chrome.runtime.sendMessage({ action: 'getLiveProxies' });
|
||||||
|
pdfQueue = response?.entries || [];
|
||||||
|
console.log('[Popup] After auto-restore and sync, got', pdfQueue.length, 'live entries');
|
||||||
|
|
||||||
// Check current tab for PDF and add to persistent queue via background
|
// Check current tab for PDF and add to persistent queue via background
|
||||||
const currentTabPDF = await checkCurrentTabForPDF();
|
const currentTabPDF = await checkCurrentTabForPDF();
|
||||||
if (currentTabPDF) {
|
if (currentTabPDF) {
|
||||||
@@ -289,6 +310,70 @@ async function loadPDFQueue() {
|
|||||||
renderPDFList();
|
renderPDFList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync local proxies with server documents
|
||||||
|
* Creates local proxies for documents that exist on server but not locally
|
||||||
|
*/
|
||||||
|
async function syncWithServer() {
|
||||||
|
if (!currentCredentials) {
|
||||||
|
console.log('[Popup] No credentials, skipping server sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Popup] Syncing with Binect server...');
|
||||||
|
|
||||||
|
// Get list of documents from server
|
||||||
|
const result = await chrome.runtime.sendMessage({
|
||||||
|
action: 'listServerDocuments',
|
||||||
|
username: currentCredentials.username,
|
||||||
|
password: currentCredentials.password
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Popup] listServerDocuments result:', result);
|
||||||
|
|
||||||
|
if (!result.success || !result.documents) {
|
||||||
|
console.warn('[Popup] Failed to list server documents:', result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverDocs = result.documents as Array<{
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
price?: number;
|
||||||
|
recipientAddress?: string;
|
||||||
|
errorDetails?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
console.log('[Popup] Found', serverDocs.length, 'documents on server');
|
||||||
|
for (const doc of serverDocs) {
|
||||||
|
console.log('[Popup] Server doc:', doc.id, doc.filename, 'status:', doc.status, doc.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync each server document to local proxy
|
||||||
|
for (const doc of serverDocs) {
|
||||||
|
console.log('[Popup] Syncing doc', doc.id, 'to local proxy...');
|
||||||
|
const syncResult = await chrome.runtime.sendMessage({
|
||||||
|
action: 'syncFromServer',
|
||||||
|
binectDocumentId: doc.id,
|
||||||
|
filename: doc.filename,
|
||||||
|
binectStatusCode: doc.status,
|
||||||
|
binectStatusText: doc.statusText,
|
||||||
|
price: doc.price,
|
||||||
|
recipientAddress: doc.recipientAddress,
|
||||||
|
errorDetails: doc.errorDetails
|
||||||
|
});
|
||||||
|
console.log('[Popup] Sync result for doc', doc.id, ':', syncResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Popup] Server sync complete');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Popup] Server sync error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check recent downloads for PDFs (fallback mechanism)
|
* Check recent downloads for PDFs (fallback mechanism)
|
||||||
*/
|
*/
|
||||||
@@ -428,33 +513,44 @@ function renderPDFItem(pdf: DocumentProxy, section: 'pending' | 'basket' | 'prod
|
|||||||
|
|
||||||
let actionsHtml = '';
|
let actionsHtml = '';
|
||||||
|
|
||||||
|
// Check if document can be deleted from server (erroneous or canceled)
|
||||||
|
const canDeleteFromServer = pdf.binectDocumentId && (pdf.binectStatusCode === 7 || pdf.binectStatusCode === 6);
|
||||||
|
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
<button class="btn-send-item" data-id="${escapeHtml(pdf.id)}" ${pdf.binectStatus === 'uploading' ? 'disabled' : ''}>
|
<button class="btn-send-item" data-id="${escapeHtml(pdf.id)}" ${pdf.binectStatus === 'uploading' ? 'disabled' : ''}>
|
||||||
${pdf.binectStatus === 'uploading' ? 'Uploading...' : (pdf.binectStatus === 'failed' ? 'Retry' : 'Upload')}
|
${pdf.binectStatus === 'uploading' ? 'Uploading...' : (pdf.binectStatus === 'failed' ? 'Retry' : 'Upload')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
${canDeleteFromServer
|
||||||
|
? `<button class="btn-delete-server" data-id="${escapeHtml(pdf.id)}">Delete from server</button>`
|
||||||
|
: `<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>`
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
case 'basket':
|
case 'basket':
|
||||||
|
// No archive button for in_basket - these are active documents
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
<button class="btn-order-item" data-id="${escapeHtml(pdf.id)}" ${pdf.binectStatus === 'ordering' ? 'disabled' : ''}>
|
<button class="btn-order-item" data-id="${escapeHtml(pdf.id)}" ${pdf.binectStatus === 'ordering' ? 'disabled' : ''}>
|
||||||
${pdf.binectStatus === 'ordering' ? 'Sending...' : 'Send'}
|
${pdf.binectStatus === 'ordering' ? 'Sending...' : 'Send'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
case 'production':
|
case 'production':
|
||||||
// Archive button only
|
// No archive button for in_production - these are active documents
|
||||||
actionsHtml = `
|
actionsHtml = '';
|
||||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
|
||||||
`;
|
|
||||||
break;
|
break;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
actionsHtml = `
|
// For sent/canceled documents - offer delete from server if applicable
|
||||||
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
if (canDeleteFromServer) {
|
||||||
`;
|
actionsHtml = `
|
||||||
|
<button class="btn-delete-server" data-id="${escapeHtml(pdf.id)}">Delete from server</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
actionsHtml = `
|
||||||
|
<button class="btn-archive" data-id="${escapeHtml(pdf.id)}">Archive</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'archived':
|
case 'archived':
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
@@ -526,6 +622,14 @@ function setupPDFListEventListeners() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete from server buttons
|
||||||
|
pdfList.querySelectorAll('.btn-delete-server').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const id = (e.target as HTMLElement).dataset.id;
|
||||||
|
if (id) handleDeleteFromServer(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Delete/Remove buttons
|
// Delete/Remove buttons
|
||||||
pdfList.querySelectorAll('.btn-dismiss').forEach(btn => {
|
pdfList.querySelectorAll('.btn-dismiss').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
@@ -647,13 +751,14 @@ async function handleSendPDF(id: string) {
|
|||||||
currentCredentials.password
|
currentCredentials.password
|
||||||
);
|
);
|
||||||
|
|
||||||
// Track successful transfer
|
// Track transfer
|
||||||
await addTrackingEntry({
|
await addTrackingEntry({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
sourceDomain: pdf.sourceDomain,
|
sourceDomain: pdf.sourceDomain,
|
||||||
destinationUrl: 'https://api.binect.de/binectapi/v1/documents',
|
destinationUrl: 'https://api.binect.de/binectapi/v1/documents',
|
||||||
pdfSize: pdfBytes.byteLength,
|
pdfSize: pdfBytes.byteLength,
|
||||||
result: 'success'
|
result: document.status.code === 7 ? 'failure' : 'success',
|
||||||
|
errorMessage: document.status.code === 7 ? document.status.text : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update last use timestamp
|
// Update last use timestamp
|
||||||
@@ -672,27 +777,57 @@ async function handleSendPDF(id: string) {
|
|||||||
meta.recipientAddress = document.letter.letterData.recipientAddress;
|
meta.recipientAddress = document.letter.letterData.recipientAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status to in_basket (document is now SHIPPABLE)
|
// Check if document is erroneous (status 7)
|
||||||
await chrome.runtime.sendMessage({
|
if (document.status.code === 7) {
|
||||||
action: 'updatePDFStatus',
|
// Extract error message
|
||||||
id,
|
let errorMessage = document.status.text || 'Document has errors';
|
||||||
status: 'in_basket',
|
if (document.letter?.errors && document.letter.errors.length > 0) {
|
||||||
meta
|
errorMessage = document.letter.errors.map(e => e.text || e.blankText).join('; ');
|
||||||
});
|
}
|
||||||
|
meta.errorMessage = errorMessage;
|
||||||
|
|
||||||
// Update local state
|
// Update status to failed (erroneous)
|
||||||
pdf.binectStatus = 'in_basket';
|
await chrome.runtime.sendMessage({
|
||||||
pdf.binectDocumentId = document.id;
|
action: 'updatePDFStatus',
|
||||||
pdf.binectStatusCode = document.status.code;
|
id,
|
||||||
pdf.binectStatusText = document.status.text;
|
status: 'failed',
|
||||||
pdf.contentHash = contentHash;
|
meta
|
||||||
if (document.letter?.letterData) {
|
});
|
||||||
pdf.price = document.letter.letterData.price?.priceAfterTax;
|
|
||||||
pdf.recipientAddress = document.letter.letterData.recipientAddress;
|
// Update local state
|
||||||
|
pdf.binectStatus = 'failed';
|
||||||
|
pdf.binectDocumentId = document.id;
|
||||||
|
pdf.binectStatusCode = document.status.code;
|
||||||
|
pdf.binectStatusText = document.status.text;
|
||||||
|
pdf.contentHash = contentHash;
|
||||||
|
pdf.errorMessage = errorMessage;
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
showStatus(`Document has errors: ${errorMessage}`, 'error');
|
||||||
|
} else {
|
||||||
|
// Document is shippable (status 2) - in basket
|
||||||
|
// Update status to in_basket
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'updatePDFStatus',
|
||||||
|
id,
|
||||||
|
status: 'in_basket',
|
||||||
|
meta
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
pdf.binectStatus = 'in_basket';
|
||||||
|
pdf.binectDocumentId = document.id;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
showStatus(`Uploaded! Ready to send (${(pdf.price || 0) / 100} €)`, 'success');
|
||||||
}
|
}
|
||||||
renderPDFList();
|
|
||||||
|
|
||||||
showStatus(`Uploaded! Ready to order (${(pdf.price || 0) / 100} €)`, 'success');
|
|
||||||
|
|
||||||
// Start auto-refresh sequence
|
// Start auto-refresh sequence
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
@@ -961,6 +1096,42 @@ async function handleRestorePDF(id: string) {
|
|||||||
setTimeout(() => hideStatus(), 2000);
|
setTimeout(() => hideStatus(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle delete from server (for erroneous or canceled documents)
|
||||||
|
*/
|
||||||
|
async function handleDeleteFromServer(id: string) {
|
||||||
|
const pdf = pdfQueue.find(p => p.id === id);
|
||||||
|
if (!pdf || !currentCredentials || !pdf.binectDocumentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await chrome.runtime.sendMessage({
|
||||||
|
action: 'deleteServerDocument',
|
||||||
|
documentId: pdf.binectDocumentId,
|
||||||
|
username: currentCredentials.username,
|
||||||
|
password: currentCredentials.password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
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 });
|
||||||
|
pdfQueue = pdfQueue.filter(p => p.id !== id);
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
showStatus('Document deleted from server', 'success');
|
||||||
|
setTimeout(() => hideStatus(), 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Delete failed';
|
||||||
|
showStatus(errorMessage, 'error');
|
||||||
|
setTimeout(() => hideStatus(), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle delete PDF (permanently remove)
|
* Handle delete PDF (permanently remove)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
BinectClient,
|
BinectClient,
|
||||||
BinectApiError,
|
BinectApiError,
|
||||||
BinectAuthError,
|
BinectAuthError,
|
||||||
|
DocumentStatus,
|
||||||
|
isErroneous,
|
||||||
|
getErrors,
|
||||||
|
getStatusDescription,
|
||||||
type Document as BinectDocument,
|
type Document as BinectDocument,
|
||||||
type DocumentUploadOptions,
|
type DocumentUploadOptions,
|
||||||
EnvelopeType,
|
EnvelopeType,
|
||||||
@@ -225,25 +229,14 @@ export async function uploadPDF(
|
|||||||
console.log('[Binect API] Document ID:', doc.id);
|
console.log('[Binect API] Document ID:', doc.id);
|
||||||
console.log('[Binect API] Document status:', doc.status);
|
console.log('[Binect API] Document status:', doc.status);
|
||||||
|
|
||||||
// Check if document has errors
|
// Log if document has errors (status ERRONEOUS)
|
||||||
if (doc.letter?.letterType === 'Error' && doc.letter.errors) {
|
// But still return the document so we can track it and offer delete
|
||||||
console.warn('[Binect API] Document has errors:', doc.letter.errors);
|
if (isErroneous(doc)) {
|
||||||
const errorMessages = doc.letter.errors.map(e => e.message).join('; ');
|
console.warn('[Binect API] Document is erroneous:', doc.status.text);
|
||||||
throw new BinectAPIError(
|
const errors = getErrors(doc);
|
||||||
`Document validation failed: ${errorMessages}`,
|
if (errors.length > 0) {
|
||||||
200,
|
console.warn('[Binect API] Document errors:', errors.map(e => e.message).join('; '));
|
||||||
doc
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status code 7 = erroneous
|
|
||||||
if (doc.status.code === 7) {
|
|
||||||
console.error('[Binect API] Document is erroneous:', doc.status.text);
|
|
||||||
throw new BinectAPIError(
|
|
||||||
`Document is erroneous: ${doc.status.text}`,
|
|
||||||
200,
|
|
||||||
doc
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapDocument(doc);
|
return mapDocument(doc);
|
||||||
@@ -369,7 +362,7 @@ export async function shipDocument(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: sending.status,
|
status: sending.status,
|
||||||
statusText: getStatusText(sending.status),
|
statusText: getStatusDescription(sending.status),
|
||||||
price: sending.price,
|
price: sending.price,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -433,15 +426,18 @@ export async function getDocumentStatus(
|
|||||||
recipientAddress = doc.letter.letterData.recipientAddress;
|
recipientAddress = doc.letter.letterData.recipientAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract error details for erroneous documents (status 7)
|
// Extract error details for erroneous documents
|
||||||
if (doc.status.code === 7 && doc.letter?.errors && doc.letter.errors.length > 0) {
|
if (isErroneous(doc)) {
|
||||||
errorDetails = doc.letter.errors.map(e => e.message).join('; ');
|
const errors = getErrors(doc);
|
||||||
console.log('[Binect API] Document errors:', errorDetails);
|
if (errors.length > 0) {
|
||||||
|
errorDetails = errors.map(e => e.message).join('; ');
|
||||||
|
console.log('[Binect API] Document errors:', errorDetails);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: doc.status.code,
|
status: doc.status.code,
|
||||||
statusText: doc.status.text || getStatusText(doc.status.code),
|
statusText: doc.status.text || getStatusDescription(doc.status.code),
|
||||||
price,
|
price,
|
||||||
recipientAddress,
|
recipientAddress,
|
||||||
errorDetails,
|
errorDetails,
|
||||||
@@ -472,17 +468,139 @@ export async function getDocumentStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get human-readable status text for a Binect status code
|
* Server document info for sync
|
||||||
*/
|
*/
|
||||||
function getStatusText(statusCode: number): string {
|
export interface ServerDocument {
|
||||||
switch (statusCode) {
|
id: number;
|
||||||
case 1: return 'In preparation';
|
filename: string;
|
||||||
case 2: return 'Ready to ship';
|
status: number;
|
||||||
case 3: return 'In production queue';
|
statusText: string;
|
||||||
case 4: return 'Printing';
|
price?: number;
|
||||||
case 5: return 'Sent';
|
recipientAddress?: string;
|
||||||
case 6: return 'Canceled';
|
errorDetails?: string;
|
||||||
case 7: return 'Has errors';
|
}
|
||||||
default: return 'Unknown status';
|
|
||||||
|
/**
|
||||||
|
* List all shippable documents from the server
|
||||||
|
*
|
||||||
|
* @param username - Binect username
|
||||||
|
* @param password - Binect password
|
||||||
|
* @returns Array of server documents
|
||||||
|
*/
|
||||||
|
export async function listServerDocuments(
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<ServerDocument[]> {
|
||||||
|
console.log('[Binect API] Listing server documents...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new BinectClient({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get shippable documents (status 2)
|
||||||
|
console.log('[Binect API] Fetching shippable documents...');
|
||||||
|
const shippableResponse = await client.documents.list();
|
||||||
|
console.log('[Binect API] Shippable response:', JSON.stringify(shippableResponse));
|
||||||
|
const shippable = shippableResponse.items || [];
|
||||||
|
console.log('[Binect API] Found', shippable.length, 'shippable documents');
|
||||||
|
|
||||||
|
// Get erroneous documents (status 7)
|
||||||
|
console.log('[Binect API] Fetching erroneous documents...');
|
||||||
|
const errorsResponse = await client.documents.listErrors();
|
||||||
|
console.log('[Binect API] Errors response:', JSON.stringify(errorsResponse));
|
||||||
|
const erroneous = errorsResponse.items || [];
|
||||||
|
console.log('[Binect API] Found', erroneous.length, 'erroneous documents');
|
||||||
|
|
||||||
|
// Combine and map to our format
|
||||||
|
const allDocs = [...shippable, ...erroneous];
|
||||||
|
|
||||||
|
console.log('[Binect API] Total documents on server:', allDocs.length);
|
||||||
|
|
||||||
|
return allDocs.map(doc => {
|
||||||
|
let errorDetails: string | undefined;
|
||||||
|
if (isErroneous(doc)) {
|
||||||
|
const errors = getErrors(doc);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
errorDetails = errors.map(e => e.message).join('; ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
filename: doc.filename || 'document.pdf',
|
||||||
|
status: doc.status.code,
|
||||||
|
statusText: doc.status.text || getStatusDescription(doc.status.code),
|
||||||
|
price: doc.letter?.letterData?.price?.priceAfterTax,
|
||||||
|
recipientAddress: doc.letter?.letterData?.recipientAddress,
|
||||||
|
errorDetails,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Binect API] List documents error:', error);
|
||||||
|
|
||||||
|
if (error instanceof BinectAuthError) {
|
||||||
|
throw new BinectAPIError('Invalid credentials', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectApiError) {
|
||||||
|
throw BinectAPIError.fromBinectError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BinectAPIError(
|
||||||
|
`Failed to list documents: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document from the server
|
||||||
|
*
|
||||||
|
* @param documentId - Binect document ID
|
||||||
|
* @param username - Binect username
|
||||||
|
* @param password - Binect password
|
||||||
|
*/
|
||||||
|
export async function deleteDocument(
|
||||||
|
documentId: number,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log('[Binect API] Deleting document:', documentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new BinectClient({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.documents.delete(String(documentId));
|
||||||
|
console.log('[Binect API] Document deleted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Binect API] Delete error:', error);
|
||||||
|
|
||||||
|
if (error instanceof BinectAuthError) {
|
||||||
|
throw new BinectAPIError('Invalid credentials', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectApiError) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
// Already deleted, treat as success
|
||||||
|
console.log('[Binect API] Document already deleted (404)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw BinectAPIError.fromBinectError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectAPIError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BinectAPIError(
|
||||||
|
`Failed to delete document: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export DocumentStatus enum for use in other modules
|
||||||
|
export { DocumentStatus };
|
||||||
|
|||||||
@@ -205,7 +205,8 @@ export async function syncFromServer(
|
|||||||
binectStatusCode: number,
|
binectStatusCode: number,
|
||||||
binectStatusText: string,
|
binectStatusText: string,
|
||||||
price?: number,
|
price?: number,
|
||||||
recipientAddress?: string
|
recipientAddress?: string,
|
||||||
|
errorMessage?: string
|
||||||
): Promise<DocumentProxy> {
|
): Promise<DocumentProxy> {
|
||||||
const state = await loadQueue();
|
const state = await loadQueue();
|
||||||
|
|
||||||
@@ -219,6 +220,7 @@ export async function syncFromServer(
|
|||||||
proxy.binectStatus = mapBinectStatusCode(binectStatusCode);
|
proxy.binectStatus = mapBinectStatusCode(binectStatusCode);
|
||||||
if (price !== undefined) proxy.price = price;
|
if (price !== undefined) proxy.price = price;
|
||||||
if (recipientAddress) proxy.recipientAddress = recipientAddress;
|
if (recipientAddress) proxy.recipientAddress = recipientAddress;
|
||||||
|
if (errorMessage) proxy.errorMessage = errorMessage;
|
||||||
} else {
|
} else {
|
||||||
// Create new proxy from server data
|
// Create new proxy from server data
|
||||||
proxy = {
|
proxy = {
|
||||||
@@ -234,7 +236,8 @@ export async function syncFromServer(
|
|||||||
binectStatusCode,
|
binectStatusCode,
|
||||||
binectStatusText,
|
binectStatusText,
|
||||||
price,
|
price,
|
||||||
recipientAddress
|
recipientAddress,
|
||||||
|
errorMessage
|
||||||
};
|
};
|
||||||
state.entries.unshift(proxy);
|
state.entries.unshift(proxy);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user