Compare commits

17 Commits
v0.1 ... main

Author SHA1 Message Date
a1597c23fa Added session cost file 2026-01-16 23:42:22 +01:00
1df93bd385 Add local tag server check, archive on delete, and first-run pin reminder
- Local tag is now clickable - checks if document exists on server by ID
  or filename, and re-links if found
- Delete from server now archives the proxy instead of removing it,
  making it a local-only document that can be re-uploaded
- Added first-run pin reminder banner to help users pin the extension
- Added issue report modal with context sections (extension info, browser
  info, document status, recent errors) and copy to clipboard as Markdown
- Added clearServerFields and attachServerDocument functions to pdf-queue
- Improved local tag styling with hover states and visual feedback

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 23:39:06 +01:00
e5f3f583d1 Refresh erroneous documents to detect server-side fixes
- Include erroneous server documents in auto-refresh
- Documents can transition from ERRONEOUS to SHIPPABLE when fixed on server
- Clear error message when document status becomes non-erroneous
- Properly handle status transitions in refresh flow

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 22:56:39 +01:00
24daa4bf82 Improve consistency for server-discovered documents
- Separate "Has Errors" section for erroneous server documents (already
  uploaded but have validation errors)
- "Ready to Upload" section now only shows truly local documents
- Erroneous server docs show only "Delete from server" button (no retry)
- Improved metadata display: show document ID for server docs, hide
  unknown file size
- Clean up verbose debug logging

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 22:49:07 +01:00
327943bc18 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>
2026-01-16 22:41:43 +01:00
f4c0481eda Show detailed error information for erroneous documents
- Extract error details from Binect API for documents with status 7
- Display error details in a highlighted box below the status
- Map status 7 (ERRONEOUS) to 'failed' status in refresh handler
- Add CSS styling for error details display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 21:29:45 +01:00
4f0f7ed9eb UI refinements: Send button, server deletion detection, remove manual refresh
- Rename "Order" button to "Send" throughout UI
- Detect server-side document deletions (404) and auto-archive
- Remove manual refresh button (auto-refresh handles this)
- Fix password toggle button with preventDefault/stopPropagation
- Make auto-refresh silent (no status messages)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 17:51:14 +01:00
dd78c24e98 UI improvements: filename display, local tag, auto-refresh, password toggle
- Show only filename in list with full path as tooltip on hover
- Add "local" tag for documents not yet uploaded to server
- Auto-refresh after user interactions (upload/order) with Fibonacci-like
  sequence: 10, 20, 30, 50, 80, 130, 210 seconds
- Fix password toggle button (add pointer-events: none to SVG icons)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 17:30:16 +01:00
facae724bf 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>
2026-01-16 17:11:15 +01:00
5cb0194533 Move refresh to global header button
- Add refresh button in header (before help button)
- Refresh updates all uploaded documents at once (in_basket, in_production)
- Remove individual refresh buttons from production items
- Add spinning animation while refresh is in progress
- Show count of refreshed documents in status message

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 13:28:16 +01:00
468473f03b Fix state persistence when popup is closed
- Add 'dismissed' status to prevent dismissed PDFs from reappearing
- Persist PDFs discovered from current tab and recent downloads via background
- Add dismissPDF function that marks PDFs as dismissed instead of removing
- Dismissed PDFs are kept for 7 days for duplicate detection, then cleaned up
- Completed items (sent/canceled) can still be fully removed
- Add addPDF message handler to service worker for popup-discovered PDFs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:20:28 +01:00
3a48d4f497 Add document lifecycle tracking with order/production status
- Extended PDFStatus with full lifecycle: pending → uploading → in_basket →
  ordering → in_production → sent/canceled
- Added shipDocument() and getDocumentStatus() API methods
- Grouped UI sections: Ready to Upload, In Basket, In Production, Completed
- Order button for documents in basket to place production order
- Refresh button to check current status from Binect server
- Display price and recipient address for uploaded documents
- Status icons and color-coded indicators for each state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:17:28 +01:00
724940ebf7 Fix: Restore fallback to check recent downloads
When the persistent queue is empty (e.g., after extension reload),
fall back to checking recent downloads from Chrome downloads API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 15:14:04 +01:00
3e86bb126b Add PDF list view with upload status tracking
- Show all pending PDFs in a scrollable list instead of single PDF
- Track upload status (pending/uploading/uploaded/failed) per PDF
- Store queue in chrome.storage.local for persistence
- Prevent duplicate uploads by checking URL against uploaded PDFs
- Add Dismiss button to remove PDFs from queue
- Show badge with count of pending PDFs
- Auto-cleanup old entries (uploaded >7 days, failed >24h)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 14:55:32 +01:00
5bde27dcdd Fix base64 encoding for browser environment
The bufferToBase64 function from @binect/js expects Node.js Buffer
objects but was receiving browser ArrayBuffer, causing "[object ArrayBuffer]"
to be sent instead of valid base64. Use browser-native btoa() instead.

Also updates tests to work with @binect/js integration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 14:41:44 +01:00
be4377253e Switch to HTTP Basic Auth and improve PDF detection
- Replace token-based auth with HTTP Basic Authentication per Binect API v1 spec
- Improve PDF detection: check current tab first, then background service, fallback to recent downloads
- Add password visibility toggle in login form
- Add extensive debug logging throughout for troubleshooting
- Update manifest with alarms, activeTab permissions and <all_urls> host permission
- Add documentation files and development helper scripts
- Add Binect API specs for reference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:50:57 +01:00
0be7b56506 Fix: Add default_locale to manifest for Chrome extension compatibility
Added 'default_locale': 'en' to manifest.json to resolve Chrome error
when loading extension with _locales directory.

Error was: 'Lokalisierung wurde verwendet, in der Manifest-Datei war
jedoch kein Wert für default_locale angegeben'

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 00:49:32 +01:00
25 changed files with 9057 additions and 331 deletions

289
DEBUG_DOWNLOAD_DETECTION.md Normal file
View File

@@ -0,0 +1,289 @@
# Debugging Download Detection
This guide helps you debug why PDF download detection may not be working.
## Prerequisites
1. **Rebuild and reload the extension**
```bash
npm run build
```
Then go to `chrome://extensions/` and click the reload icon for BinectChrome
2. **Open the Service Worker console**
- Go to `chrome://extensions/`
- Find BinectChrome
- Click "service worker" link (or "Inspect views: service worker")
- This opens DevTools for the service worker
- **Keep this window open** while testing
## Step-by-Step Download Detection Test
### Step 1: Verify Service Worker is Running
**In the Service Worker console, you should see:**
```
[Service Worker] ===== BinectChrome service worker loaded =====
[Service Worker] Timestamp: 2024-01-14T...
[Service Worker] Initializing PDF detection...
[PDF Detector] Starting PDF detection, registering download listener
[PDF Detector] Listener registered successfully
```
**If you don't see these messages:**
- The service worker hasn't loaded yet
- Try clicking the extension icon (this wakes it up)
- Check for any red errors in the console
### Step 2: Test with a Simple PDF Download
**Download a test PDF:**
1. Right-click this link and select "Save link as...":
```
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
```
2. Save it to your Downloads folder
3. Watch the Service Worker console
**Expected console output:**
```
[PDF Detector] Download changed: {id: 123, state: {...}, stateValue: "in_progress"}
[PDF Detector] Download not complete, ignoring
[PDF Detector] Download changed: {id: 123, state: {...}, stateValue: "complete"}
[PDF Detector] Download complete, searching for item: 123
[PDF Detector] Search results: 1 items
[PDF Detector] Download item: {id: 123, filename: "dummy.pdf", mime: "application/pdf", ...}
[PDF Detector] PDF detected!
[Service Worker] PDF DETECTED CALLBACK: dummy.pdf
[Service Worker] Badge updated, PDF stored in memory
```
**After successful detection:**
- Extension badge should show "1"
- Badge color should be blue (#4A90E2)
### Step 3: Test with Direct Link Download
**Alternative method:**
1. Paste this URL directly in the address bar:
```
https://www.africau.edu/images/default/sample.pdf
```
2. Chrome will start downloading it
3. Watch the Service Worker console for the same log messages
### Step 4: Verify PDF is Stored
**Click the extension icon to open the popup**
- You should see the PDF details
- Filename: "dummy.pdf" or "sample.pdf"
- Size: displayed in KB or MB
- Domain: source domain
- "Send PDF to Binect" button should be enabled
## Troubleshooting: No Logs Appearing
### Issue 1: Service Worker Not Running
**Symptoms:** No console output at all, even "[Service Worker] loaded" message
**Solutions:**
1. **Wake up the service worker:**
- Click the extension icon
- OR go to `chrome://serviceworker-internals/` and find BinectChrome
- Click "Start" if it's stopped
2. **Check for startup errors:**
- Look for red errors in the service worker console
- Check `chrome://serviceworker-internals/` for registration errors
3. **Hard reload the extension:**
- Go to `chrome://extensions/`
- Remove BinectChrome completely
- Click "Load unpacked" and select the `dist/` folder again
### Issue 2: Service Worker Sleeping Before Download Completes
**Symptoms:** Service worker console shows "loaded" message, but no download events
**This is the most likely issue!** In Manifest V3, service workers shut down after 30 seconds of inactivity.
**Test if this is the issue:**
1. Open service worker console
2. Click the extension icon to wake it up
3. **Immediately** (within 30 seconds) download a PDF
4. Watch the console
**If you see logs now, the issue is service worker lifecycle!**
**Solution:** The service worker should automatically wake up when downloads complete, but there might be a timing issue. See "Potential Fixes" below.
### Issue 3: Download Events Not Firing
**Symptoms:** Service worker is running, but no "[PDF Detector] Download changed" logs
**Possible causes:**
1. **Downloads permission not granted:**
- Go to `chrome://extensions/`
- Click "Details" on BinectChrome
- Check "Permissions" section
- Should show "Read your browsing history" and "Manage your downloads"
- If missing, the manifest is wrong
2. **Event listener not registered:**
- Look for "[PDF Detector] Listener registered successfully" in console
- If missing, there's a code issue
3. **Chrome not triggering download events:**
- Try opening the PDF instead of downloading it (should trigger viewer detection)
- Check `chrome://downloads/` to verify download completed
### Issue 4: PDF Not Detected (Logs Show It's Not a PDF)
**Symptoms:** Logs show "Not a PDF, ignoring"
**Debug the detection logic:**
Look at the download item log:
```
[PDF Detector] Download item: {
id: 123,
filename: "document.pdf", // ← Should end with .pdf
mime: "application/pdf", // ← Should be "application/pdf"
...
}
```
**If filename doesn't end with .pdf and mime is not "application/pdf":**
- The file isn't actually a PDF
- OR the server sent wrong headers
## Advanced Debugging
### Check Download API Directly
**Run this in the Service Worker console:**
```javascript
chrome.downloads.search({ limit: 10, orderBy: ['-startTime'] }, (items) => {
console.log('Recent downloads:', items);
items.forEach(item => {
console.log(`${item.filename} - mime: ${item.mime} - state: ${item.state}`);
});
});
```
This shows your recent downloads and their properties.
### Manual Test: Trigger Detection Manually
**Run this in the Service Worker console:**
```javascript
// Get the most recent download
chrome.downloads.search({ limit: 1, orderBy: ['-startTime'] }, (items) => {
if (items.length > 0) {
const item = items[0];
console.log('Most recent download:', item);
// Check if it's a PDF
const isPDF = item.filename.toLowerCase().endsWith('.pdf') || item.mime === 'application/pdf';
console.log('Is PDF?', isPDF);
}
});
```
### Check if Listener is Registered
**Run this in the Service Worker console:**
```javascript
// This won't show listeners directly, but you can test by downloading a file
console.log('Testing listener by downloading a test file...');
chrome.downloads.download({
url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
filename: 'test-binect.pdf'
}, (downloadId) => {
console.log('Download started with ID:', downloadId);
});
```
Watch for "[PDF Detector] Download changed" logs.
## Potential Fixes
If download detection is unreliable due to service worker lifecycle:
### Option 1: Use chrome.storage for Persistence
Store detected PDFs in chrome.storage instead of memory, so they survive service worker restarts.
### Option 2: Add Download Completion Listener
Use `chrome.downloads.onCreated` in addition to `onChanged` to catch downloads earlier.
### Option 3: Poll Recent Downloads
When popup opens, check recent downloads even if callback didn't fire.
## Testing Different PDF Sources
### Test 1: Direct PDF Link
```
https://www.africau.edu/images/default/sample.pdf
```
**Expected:** Direct download, should detect
### Test 2: PDF Behind Link
```
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
```
**Expected:** Right-click → Save as, should detect
### Test 3: Large PDF
```
https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf
```
**Expected:** Larger file, may take longer to download
### Test 4: Google Drive PDF
1. Upload a PDF to your Google Drive
2. Click it to view
3. **Important:** Use the PDF viewer detection (new feature)
4. If you download it, should also detect
## Success Criteria
**Download detection is working when:**
1. ✅ Service worker console shows all log messages
2. ✅ Badge updates to "1" after download
3. ✅ Popup shows PDF details
4. ✅ "Send PDF to Binect" button is enabled
5. ✅ Works consistently across multiple downloads
## Still Not Working?
If download detection still doesn't work after following this guide:
1. **Export debug logs:**
- Right-click in service worker console → "Save as..."
- Save the console output
2. **Check service worker status:**
- Go to `chrome://serviceworker-internals/`
- Find BinectChrome
- Screenshot the status section
3. **Get downloads permission status:**
```javascript
chrome.permissions.contains({
permissions: ['downloads']
}, (result) => {
console.log('Has downloads permission:', result);
});
```
4. **Report the issue:**
- Email bernd.worsch@binect.de
- Include: console logs, screenshots, Chrome version
- Describe: what you tried, what happened
## Workaround: Use PDF Viewer Detection
**If download detection is unreliable, use the new PDF viewer detection:**
1. Open a PDF in Chrome (paste URL in address bar)
2. Click the extension icon
3. Should detect the PDF from the current tab
4. This bypasses download detection entirely!
See `TESTING_PDF_VIEWER.md` for details.

248
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,248 @@
# Development & Test-Fix Workflow for BinectChrome
This guide explains how to set up an efficient development workflow for testing and debugging the Chrome extension.
## Quick Start
### 1. Development Mode (Auto-rebuild on changes)
```bash
npm run dev
```
This starts webpack in watch mode - any changes to source files will automatically rebuild the extension.
### 2. Load Extension in Chrome
**Initial Load:**
1. Open Chrome and navigate to `chrome://extensions/`
2. Enable "Developer mode" (toggle in top-right corner)
3. Click "Load unpacked"
4. Select the `/home/worsch/binect-chrome/dist` directory
5. Note the extension ID (you'll need this for debugging)
**Reload After Changes:**
- **Option A (Manual):** Click the reload icon on the extension card at `chrome://extensions/`
- **Option B (Shortcut):** Click the extensions icon in Chrome toolbar → Manage Extensions → Reload
- **Option C (Recommended for service worker):** Navigate to `chrome://serviceworker-internals/` and click "Stop" then reload
### 3. Debug Service Worker
**View Service Worker Logs:**
1. Go to `chrome://extensions/`
2. Find your extension
3. Click "service worker" link (under "Inspect views")
4. This opens DevTools for the service worker
**Alternative Method:**
1. Go to `chrome://serviceworker-internals/`
2. Find "chrome-extension://[your-extension-id]"
3. Click "Inspect" to open DevTools
**View Service Worker Status:**
- `chrome://serviceworker-internals/` - Shows all service workers, their status, and errors
### 4. Debug Popup & UI
**Popup DevTools:**
1. Click the extension icon to open the popup
2. Right-click inside the popup → "Inspect"
3. This opens DevTools for the popup
**Tracking Page DevTools:**
1. Open the tracking page from the popup
2. Right-click → "Inspect"
## Complete Test-Fix Loop
### Workflow Pattern
```
┌─────────────────────────────────────────────────┐
│ 1. Make code changes in src/ │
│ 2. Webpack auto-rebuilds (if using npm run dev)│
│ 3. Reload extension in Chrome │
│ 4. Check DevTools for errors │
│ 5. Test functionality │
│ 6. Repeat from step 1 │
└─────────────────────────────────────────────────┘
```
### Detailed Steps
1. **Start Development Mode**
```bash
npm run dev
```
2. **Make Your Changes**
- Edit files in `src/`
- Webpack will automatically rebuild
3. **Reload Extension**
- Go to `chrome://extensions/`
- Click reload icon on BinectChrome card
- For service worker changes: Go to `chrome://serviceworker-internals/` → Stop → Reload
4. **Check for Errors**
- **Service Worker:** Click "service worker" link or check `chrome://serviceworker-internals/`
- **Popup:** Right-click popup → Inspect
- **Console Errors:** Check the browser console in all DevTools windows
5. **Test Functionality**
- Download a PDF to test detection
- Check badge updates
- Test sending PDFs
- Verify tracking data
6. **Common Debugging Points**
- Service worker errors: Usually permissions or API usage issues
- PDF detection not working: Check Downloads API events
- Badge not updating: Check service worker is running
- Alarms not firing: Check `chrome://serviceworker-internals/` for service worker lifecycle
## Common Issues & Solutions
### Issue: Service Worker Registration Failed (Status Code 15)
**Cause:** Webpack output format doesn't match manifest type
**Solution:** Ensure webpack.config.js has:
```javascript
output: {
module: true,
environment: { module: true }
},
experiments: {
outputModule: true
}
```
### Issue: "Cannot read properties of undefined (reading 'onAlarm')"
**Cause:** Missing "alarms" permission
**Solution:** Add to manifest.json:
```json
"permissions": ["downloads", "storage", "alarms"]
```
### Issue: Service Worker Stops Running
**Cause:** Chrome stops inactive service workers after 30 seconds (Manifest V3 behavior)
**Solution:** This is normal! Service workers wake up on events (downloads, alarms, messages)
### Issue: Changes Not Reflecting
**Cause:** Extension not reloaded, or cached service worker
**Solution:**
1. Always reload after rebuilding
2. For stubborn issues: Stop service worker at `chrome://serviceworker-internals/`
3. Hard reload: Remove extension and re-add it
## Testing Specific Features
### Test PDF Detection
1. Ensure extension is loaded and service worker is running
2. Download any PDF file from the web
3. Check service worker console for "PDF detected:" log
4. Verify badge shows "1"
5. Open popup and verify PDF info is displayed
### Test Credential Management
1. Open popup
2. Enter credentials
3. Check Chrome DevTools → Application → Storage → Extension Storage
4. Verify credentials are encrypted
5. Test credential expiry by manipulating storage
### Test Alarm/Expiry Check
1. Open `chrome://serviceworker-internals/`
2. Check for alarm registration
3. Or use: `chrome.alarms.getAll(console.log)` in service worker console
## Production Build & Testing
```bash
# Build for production
npm run build
# Build is in dist/ directory
# Test the production build by loading dist/ as unpacked extension
```
## Running Unit Tests
```bash
# Run tests once
npm test
# Run tests in watch mode
npm run test:watch
# Type checking
npm run type-check
# Linting
npm run lint
npm run lint:fix
```
## Useful Chrome URLs for Extension Development
- `chrome://extensions/` - Manage extensions
- `chrome://serviceworker-internals/` - Debug service workers
- `chrome://inspect/#service-workers` - Inspect active service workers
- `chrome://downloads/` - View downloads (for testing PDF detection)
- `chrome://version/` - Check Chrome version
## Debugging Tips
1. **Console.log is your friend**: Add logs liberally in development
2. **Breakpoints**: Use debugger; statements or set breakpoints in DevTools
3. **Storage inspection**: Check Application → Storage → Extension Storage in DevTools
4. **Network tab**: Monitor API calls to Binect
5. **Performance**: Use Performance tab to check service worker lifecycle
6. **Memory**: Watch for memory leaks in long-running service workers
## VS Code Integration (Optional)
Add to `.vscode/launch.json` for debugging:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Debug Extension",
"url": "chrome://extensions/",
"webRoot": "${workspaceFolder}/dist"
}
]
}
```
## Quick Reference Commands
```bash
# Development
npm run dev # Watch mode with auto-rebuild
npm run build # Production build
npm test # Run tests
npm run lint # Check code quality
npm run type-check # TypeScript validation
# Chrome URLs
chrome://extensions/ # Extension management
chrome://serviceworker-internals/ # Service worker debugging
```
## Emergency Cleanup
If extension gets stuck or behaves oddly:
```bash
# 1. Stop service worker
# Go to chrome://serviceworker-internals/ → Stop
# 2. Clear extension storage
# DevTools → Application → Storage → Clear site data
# 3. Remove and reload extension
# chrome://extensions/ → Remove → Load unpacked again
# 4. Clean rebuild
rm -rf dist/
npm run build
```

303
DOWNLOAD_DETECTION_FIXES.md Normal file
View File

@@ -0,0 +1,303 @@
# Download Detection Debugging & Fixes
## Summary
I've added comprehensive debugging and a fallback mechanism to help diagnose and work around download detection issues.
## Changes Made
### 1. **Comprehensive Debug Logging**
**Service Worker (src/background/service-worker.ts):**
- All events now log with `[Service Worker]` prefix
- Shows when service worker loads/reloads
- Logs PDF detection callbacks
- Logs message handling from popup
**PDF Detector (src/utils/pdf-detector.ts):**
- All download events logged with `[PDF Detector]` prefix
- Shows download state changes (in_progress → complete)
- Logs download item details (filename, mime, state, url)
- Confirms when PDF is detected vs. ignored
**Popup (src/popup/popup.ts):**
- All PDF loading operations logged with `[Popup]` prefix
- Shows detection priority: tab viewer → background → fallback
- Logs results of each detection method
### 2. **Fallback Mechanism**
**New function: `checkRecentDownloads()` (src/popup/popup.ts:254-298)**
- Directly queries Chrome's downloads API for recent PDFs
- Checks last 20 downloads
- Finds most recent completed PDF
- **Works even if service worker missed the download event**
**PDF Detection Priority:**
1. Current tab (PDF viewer) - NEW
2. Background service worker (download event listener)
3. **Recent downloads fallback** - NEW
4. No PDF detected
This triple-layer approach ensures PDFs are detected even if the service worker is sleeping.
## How to Debug
### Step 1: Reload Extension
```bash
npm run build
```
Then reload at `chrome://extensions/`
### Step 2: Open Consoles
**Service Worker Console:**
1. Go to `chrome://extensions/`
2. Click "service worker" link under BinectChrome
3. **Keep this open while testing**
**Popup Console:**
1. Click the extension icon
2. Right-click inside popup → "Inspect"
3. View console
### Step 3: Download a Test PDF
**Test URL:**
```
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
```
**Method 1: Direct download**
- Right-click link → "Save link as..."
- Save to Downloads
**Method 2: View in browser**
- Paste URL in address bar
- Let Chrome open it in viewer
- Click extension icon
### Step 4: Check Logs
**Expected Service Worker logs:**
```
[Service Worker] ===== BinectChrome service worker loaded =====
[Service Worker] Initializing PDF detection...
[PDF Detector] Starting PDF detection, registering download listener
[PDF Detector] Listener registered successfully
# When download completes:
[PDF Detector] Download changed: {id: X, state: {...}, stateValue: "complete"}
[PDF Detector] Download complete, searching for item: X
[PDF Detector] Search results: 1 items
[PDF Detector] Download item: {id: X, filename: "dummy.pdf", mime: "application/pdf", ...}
[PDF Detector] PDF detected!
[Service Worker] PDF DETECTED CALLBACK: dummy.pdf
[Service Worker] Badge updated, PDF stored in memory
```
**Expected Popup logs:**
```
[Popup] Loading last PDF...
[Popup] No PDF in current tab, checking background script...
[Service Worker] Message received: getLastPDF
[Service Worker] Returning last PDF: dummy.pdf
[Popup] Background returned PDF: dummy.pdf
```
**If service worker missed it (fallback):**
```
[Popup] Loading last PDF...
[Popup] No PDF in current tab, checking background script...
[Service Worker] Message received: getLastPDF
[Service Worker] Returning last PDF: none
[Popup] Background has no PDF, checking recent downloads as fallback...
[Popup] Checked recent downloads: 15 items
[Popup] Found recent PDF: dummy.pdf
```
## Common Issues & Solutions
### Issue 1: No Service Worker Logs
**Symptom:** Service worker console is empty or shows "Inactive"
**Solution:**
- Click the extension icon (wakes it up)
- Or go to `chrome://serviceworker-internals/` and click "Start"
- Check for registration errors
### Issue 2: Service Worker Not Waking for Downloads
**Symptom:** Download completes but no `[PDF Detector]` logs appear
**This is the main issue with Manifest V3 service workers!**
**Diagnosis:**
1. Open service worker console
2. Download a PDF
3. Watch if service worker console gets new logs
**If no logs:** Service worker didn't wake up for the download event
**Workaround:** The fallback mechanism handles this! When you open the popup, it checks recent downloads directly.
### Issue 3: Badge Not Updating
**Symptom:** PDF downloads but badge stays empty
**Cause:** Service worker detected PDF but couldn't update badge (possibly terminated)
**Solution:** The fallback still works - open popup to see the PDF
### Issue 4: PDF Shows in Popup But Disappears
**Symptom:** PDF shows up, but after a few minutes it's gone
**Cause:** Service worker memory is ephemeral - when it terminates, `lastDetectedPDF` is lost
**Solution:** The fallback mechanism re-fetches from recent downloads each time popup opens
## Testing Checklist
- [ ] Build extension: `npm run build`
- [ ] Reload extension at `chrome://extensions/`
- [ ] Open service worker console
- [ ] Download test PDF (right-click → Save link as)
- [ ] Check service worker logs for detection
- [ ] Check if badge shows "1"
- [ ] Open popup - should show PDF
- [ ] Wait 2 minutes (service worker sleeps)
- [ ] Download another PDF
- [ ] Open popup - should show PDF (even if service worker missed it)
- [ ] Open PDF in browser tab (view, don't download)
- [ ] Open popup - should detect tab viewer
## Known Limitations
### 1. Service Worker Lifecycle (Manifest V3)
Service workers shut down after 30 seconds of inactivity. This is by design in Manifest V3.
**Impact:**
- Download event listener may not fire if service worker is sleeping
- Badge may not update immediately after download
- `lastDetectedPDF` is lost when service worker terminates
**Mitigation:**
- Fallback mechanism checks recent downloads
- Works reliably despite service worker sleep
- Badge updates when popup opens
### 2. File URLs
Chrome extensions can't access `file://` URLs by default.
**Impact:** If PDF is downloaded and opened from disk, can't re-fetch for upload
**Solution:** Use the original download URL (which we store)
### 3. Authenticated Downloads
Some PDFs require authentication cookies.
**Impact:** Re-fetching PDF may fail if session expired
**Solution:** `fetchPDFBytes()` uses `credentials: 'include'` to send cookies
## Advanced Debugging
### Manual Download Check
Run in Service Worker console:
```javascript
chrome.downloads.search({ limit: 10, orderBy: ['-startTime'] }, (items) => {
console.log('Recent downloads:');
items.forEach(item => {
const isPDF = item.filename.toLowerCase().endsWith('.pdf') ||
item.mime === 'application/pdf';
console.log(` ${item.filename} - PDF: ${isPDF} - State: ${item.state}`);
});
});
```
### Test Download Listener
Run in Service Worker console:
```javascript
chrome.downloads.download({
url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
filename: 'test-download.pdf'
}, (downloadId) => {
console.log('Started download:', downloadId);
});
```
Watch for `[PDF Detector]` logs.
### Check Permissions
Run in Service Worker console:
```javascript
chrome.permissions.contains({ permissions: ['downloads'] }, (result) => {
console.log('Has downloads permission:', result);
});
```
## Recommended Testing Flow
**For Development:**
1. Use PDF viewer detection (open PDF in tab)
2. This bypasses download detection entirely
3. Most reliable for testing API integration
**For Download Detection Testing:**
1. Open both service worker and popup consoles
2. Download a PDF
3. Watch logs in real-time
4. Verify fallback works even if service worker missed it
## Next Steps
**If download detection is still unreliable:**
### Option A: Accept Viewer Detection as Primary
- PDF viewer detection works reliably
- Download detection becomes secondary
- Users can open PDFs in browser instead of downloading
### Option B: Persist Detected PDFs
- Store detected PDFs in `chrome.storage` instead of memory
- Survives service worker restarts
- Requires storage cleanup logic
### Option C: Poll Downloads Periodically
- Set up an alarm to check recent downloads every minute
- More resource intensive
- Very reliable
## Files Changed
1. `src/background/service-worker.ts` - Added debug logging
2. `src/utils/pdf-detector.ts` - Added debug logging
3. `src/popup/popup.ts` - Added debug logging and fallback mechanism
4. `DEBUG_DOWNLOAD_DETECTION.md` - Comprehensive debugging guide
5. `DOWNLOAD_DETECTION_FIXES.md` - This file
## Support
If download detection still doesn't work:
1. Follow `DEBUG_DOWNLOAD_DETECTION.md`
2. Export console logs from both service worker and popup
3. Include Chrome version: `chrome://version/`
4. Report to bernd.worsch@binect.de
## Workaround
**The PDF viewer detection is fully functional and reliable!**
Instead of relying on download detection:
1. Open PDFs in Chrome (paste URL in address bar)
2. Click extension icon
3. PDF is detected from current tab
4. Send to Binect
This approach:
- ✅ Always works
- ✅ No service worker timing issues
- ✅ Better user experience (no downloads folder clutter)
- ✅ Easier to test API integration

229
QUICK_TEST_GUIDE.md Normal file
View File

@@ -0,0 +1,229 @@
# Quick Test Guide - BinectChrome
## 1. Setup (2 minutes)
```bash
# Rebuild extension
npm run build
# Load in Chrome
# 1. Open chrome://extensions/
# 2. Enable "Developer mode"
# 3. Click "Reload" on BinectChrome (or "Load unpacked" if first time, select dist/)
# 4. Accept new permissions when prompted
```
## 2. Open Debug Consoles
**Service Worker:**
- `chrome://extensions/` → Click "service worker" under BinectChrome
- Keep this window open
**Popup (optional):**
- Click extension icon → Right-click → "Inspect"
## 3. Test PDF Viewer Detection (RECOMMENDED - Most Reliable)
**This is the easiest way to test the API integration!**
1. **Open a test PDF in Chrome:**
```
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
```
Just paste the URL in the address bar and press Enter
2. **Click the extension icon**
- Should detect the PDF immediately
- Shows filename, domain, "Send PDF to Binect" button
3. **Sign in and send:**
- Enter your Binect credentials
- Click "Send PDF to Binect"
- Watch for "Uploading..." → "Success!"
**Expected Popup Logs:**
```
[Popup] Loading last PDF...
[Popup] Found PDF in current tab: dummy.pdf
```
**✅ This method always works - no service worker timing issues!**
## 4. Test Download Detection (May Need Fallback)
**Method 1: Right-click → Save**
1. Right-click this link: https://www.africau.edu/images/default/sample.pdf
2. Select "Save link as..."
3. Save to Downloads
4. Watch service worker console for logs
**Method 2: Direct download**
1. Paste URL: https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf
2. Chrome starts download
3. Watch service worker console
**Expected Service Worker Logs (if working):**
```
[PDF Detector] Download changed: {...}
[PDF Detector] Download complete, searching for item: X
[PDF Detector] PDF detected!
[Service Worker] PDF DETECTED CALLBACK: sample.pdf
[Service Worker] Badge updated, PDF stored in memory
```
**If badge shows "1":** Download detection worked! ✅
**If no badge:** Fallback will still work! Continue...
5. **Click extension icon**
- Even if service worker missed it, popup checks recent downloads
- PDF should appear
**Expected Popup Logs (fallback):**
```
[Popup] Background has no PDF, checking recent downloads as fallback...
[Popup] Checked recent downloads: X items
[Popup] Found recent PDF: sample.pdf
```
## 5. Test Binect API Integration
**Prerequisites:**
- Have Binect credentials ready
- PDF is detected (from viewer OR download)
**Steps:**
1. Click extension icon
2. If not signed in:
- Enter username and password
- Click "Sign In"
- Should show main view
3. PDF details should be visible
4. Click "Send PDF to Binect"
5. Wait for upload
**Expected Results:**
```
Status: Uploading...
→ Success! Document ID: [id]
```
**Check Tracking:**
- Click "?" button in popup
- Opens tracking page
- Should show the transfer with success status
## 6. Test Error Handling
**Invalid Credentials:**
1. Sign out (if signed in)
2. Enter wrong username/password
3. Click "Sign In"
4. Should show error: "Invalid credentials"
**Network Error:**
1. Disconnect from internet
2. Try to authenticate or send PDF
3. Should show network error
## Quick Debug Checklist
**If PDF viewer detection doesn't work:**
- [ ] Is the URL actually a PDF? (ends with .pdf)
- [ ] Check popup console for error logs
- [ ] Try refreshing the PDF tab
**If download detection doesn't work:**
- [ ] Check service worker console for `[PDF Detector]` logs
- [ ] Look for "Listener registered successfully"
- [ ] Try downloading again while service worker console is open
- [ ] Open popup - fallback should still find it
**If API upload fails:**
- [ ] Check credentials are correct
- [ ] Verify PDF URL is accessible (try opening in new tab)
- [ ] Check popup console for detailed error
- [ ] Look at Network tab in popup DevTools for API calls
## Expected Console Output Summary
### Service Worker (when working):
```
[Service Worker] ===== BinectChrome service worker loaded =====
[Service Worker] Initializing PDF detection...
[PDF Detector] Starting PDF detection, registering download listener
[PDF Detector] Listener registered successfully
# On download:
[PDF Detector] Download changed: {id: X, ...}
[PDF Detector] PDF detected!
[Service Worker] PDF DETECTED CALLBACK: filename.pdf
[Service Worker] Badge updated, PDF stored in memory
```
### Popup (viewer detection):
```
[Popup] Loading last PDF...
[Popup] Found PDF in current tab: document.pdf
```
### Popup (fallback):
```
[Popup] Loading last PDF...
[Popup] No PDF in current tab, checking background script...
[Service Worker] Returning last PDF: none
[Popup] Background has no PDF, checking recent downloads as fallback...
[Popup] Found recent PDF: document.pdf
```
## Common Questions
**Q: Badge doesn't show but popup finds the PDF?**
A: Normal! Service worker may have been asleep. Fallback handled it.
**Q: PDF disappears after a few minutes?**
A: Service worker memory is cleared when it sleeps. Fallback will re-fetch it.
**Q: Which detection method should I use?**
A: **PDF viewer detection** is more reliable. Just open PDFs in Chrome instead of downloading.
**Q: Can I test without Binect credentials?**
A: You can test PDF detection, but not the upload. You'll see authentication errors when trying to send.
## Test URLs
**Small PDFs (quick tests):**
- https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf (1 page)
- https://www.africau.edu/images/default/sample.pdf (1 page)
**Medium PDFs:**
- https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf (multi-page)
## Next Steps
1. **Test viewer detection** - Most reliable
2. **Test download detection** - May need fallback
3. **Test API integration** - Requires credentials
4. **Check tracking page** - Verify data is recorded
## Getting Help
If something doesn't work:
1. Check the detailed guides:
- `DEBUG_DOWNLOAD_DETECTION.md` - Download debugging
- `TESTING_PDF_VIEWER.md` - Viewer testing
- `DOWNLOAD_DETECTION_FIXES.md` - Technical details
2. Export console logs (right-click → Save as)
3. Note your Chrome version: `chrome://version/`
4. Email: bernd.worsch@binect.de
## Success Criteria
✅ PDF viewer detection works consistently
✅ Download detection works OR fallback finds PDFs
✅ API authentication succeeds
✅ PDF upload succeeds
✅ Tracking records the transfer
✅ Badge updates (when service worker is awake)
**You're ready to use BinectChrome!**

180
RECENT_CHANGES.md Normal file
View File

@@ -0,0 +1,180 @@
# Recent Changes - BinectChrome
## Summary
Three new features have been implemented:
1. ✅ Password visibility toggle with eye icon
2. ✅ Default badge on extension icon
3. ✅ Enhanced API error logging for debugging
## 1. Password Visibility Toggle
**Location:** Login form in popup
**What it does:**
- Adds an eye icon button next to the password field
- Click to toggle between showing and hiding password
- Icon changes: 👁️ (show) ↔ 🚫👁️ (hide)
**Files changed:**
- `src/popup/popup.html` - Added password wrapper and eye icon SVGs
- `src/popup/popup.css` - Styled the toggle button
- `src/popup/popup.ts` - Added toggle functionality
**How to use:**
1. Open the extension popup
2. Enter username
3. Start typing password
4. Click the eye icon to reveal/hide password
## 2. Default Badge on Extension Icon
**What it does:**
- Extension icon now always shows a blue dot (•) badge
- Badge changes to "1" when PDF is detected
- Badge resets to dot (•) after PDF is sent or cleared
- Makes extension more visible in the toolbar
**Files changed:**
- `src/background/service-worker.ts` - Added `initializeBadge()` function
**Badge states:**
- `•` (blue dot) - Extension active, no PDF detected
- `1` (blue) - PDF detected and ready to send
- Returns to `•` after sending or clearing
**Visual:**
- Before: No badge (extension hard to notice)
- Now: Blue dot always visible (easy to spot in toolbar)
## 3. Enhanced API Error Logging
**What it does:**
- Detailed console logging for all Binect API calls
- Shows request details (URL, username, payload)
- Logs response status and headers
- Captures and displays error responses
- Better error messages for common issues
**Files changed:**
- `src/utils/binect-api.ts` - Added console logging throughout
**Console output example (authentication):**
```
[Binect API] Authenticating with Binect API...
[Binect API] URL: https://api.binect.de/auth/login
[Binect API] Username: testuser
[Binect API] Response status: 200
[Binect API] Response content-type: application/json
[Binect API] Authentication successful!
[Binect API] Response data: {token: "...", expiresAt: "..."}
```
**Console output example (error):**
```
[Binect API] Authenticating with Binect API...
[Binect API] URL: https://api.binect.de/auth/login
[Binect API] Username: wronguser
[Binect API] Response status: 401
[Binect API] Error response body: {"error": "Invalid credentials"}
[Binect API] Authentication error: BinectAPIError: Invalid credentials
```
**Error improvements:**
- Network errors now show: "Cannot reach Binect API at https://api.binect.de. Please check your internet connection."
- Auth errors show: "Invalid credentials" (401)
- Upload errors show specific issue (file format, size limit, etc.)
## How to Test
### 1. Reload Extension
```bash
# Extension is already built
# Just reload at chrome://extensions/
```
### 2. Test Password Toggle
1. Click extension icon
2. If not logged in, you'll see the login form
3. Type in the password field
4. Click the eye icon - password should become visible
5. Click again - password should hide
### 3. Test Default Badge
1. Look at your Chrome toolbar
2. Find the BinectChrome icon
3. Should see a small blue dot (•) badge
4. Download or view a PDF
5. Badge should change to "1"
### 4. Test API Error Logging
1. Right-click popup → "Inspect" (opens DevTools)
2. Go to Console tab
3. Try to sign in (with wrong or correct credentials)
4. Watch for `[Binect API]` log messages
5. All API calls are now logged with details
## Debugging Binect API Login Issues
**With the new logging, you can now:**
1. **See the exact error from Binect:**
- Open popup DevTools (right-click → Inspect)
- Try to sign in
- Check console for `[Binect API] Error response body:`
- This shows what the Binect API actually returned
2. **Verify the request is correct:**
- Console shows the URL being called
- Shows the username being sent
- Confirms request format
3. **Check network connectivity:**
- If you see "Cannot reach Binect API", it's a network issue
- If you see status codes (401, 400, etc.), the API is reachable but rejecting the request
**Common login issues:**
| Console Log | Problem | Solution |
|------------|---------|----------|
| "Cannot reach Binect API" | Network issue | Check internet connection |
| "Response status: 401" + "Invalid credentials" | Wrong username/password | Verify credentials |
| "Response status: 404" | API endpoint changed | Check API_BASE_URL in code |
| "Response status: 500" | Server error | Check Binect API status |
| "TypeError: Failed to fetch" | CORS or network | Check browser permissions |
## Testing Checklist
- [ ] Extension icon shows blue dot badge
- [ ] Password field has eye icon
- [ ] Clicking eye icon toggles password visibility
- [ ] Console shows `[Binect API]` logs when signing in
- [ ] Error messages are clear and helpful
- [ ] Badge updates when PDF detected
- [ ] Badge resets after sending PDF
## Next Steps for API Debugging
1. **Try to sign in with your Binect credentials**
2. **Open popup DevTools** (right-click popup → Inspect)
3. **Check the Console tab** for `[Binect API]` messages
4. **Share the console output** if login fails
The detailed logs will show exactly what's happening with the API request and response, making it much easier to diagnose the login problem.
## Files Modified
```
src/popup/popup.html - Added password toggle UI
src/popup/popup.css - Styled password toggle
src/popup/popup.ts - Added toggle functionality
src/background/service-worker.ts - Added default badge
src/utils/binect-api.ts - Enhanced error logging
```
## Build Info
- Build completed successfully
- Extension size: ~17KB (popup.js: 10.4KB, background.js: 4.18KB)
- All assets compiled and minified
- Ready for testing!

291
TESTING_PDF_VIEWER.md Normal file
View File

@@ -0,0 +1,291 @@
# Testing PDF Sending from Chrome's Integrated PDF Viewer
This guide explains how to test the new PDF viewer integration that allows sending PDFs directly from Chrome's built-in PDF viewer to the Binect API.
## What Changed
The extension now detects PDFs in two ways:
1. **PDF Downloads** (Original) - Detects when you download a PDF file
2. **PDF Viewer** (New) - Detects when you're viewing a PDF in Chrome's integrated viewer
The popup will prioritize showing PDFs from the current tab viewer over previously downloaded PDFs.
## Testing the PDF Viewer Integration
### Setup
1. **Reload the Extension**
```
chrome://extensions/ → Find BinectChrome → Click reload icon
```
2. **Check Permissions**
- The extension now requires `activeTab` and `<all_urls>` permissions
- Chrome will ask you to approve these new permissions
- Click "Allow" when prompted
### Test Scenario 1: Open a PDF in a New Tab
**Steps:**
1. Find any PDF URL on the web (examples below)
2. Right-click the PDF link → "Open link in new tab"
3. Chrome will open the PDF in its integrated viewer
4. Click the BinectChrome extension icon
5. The popup should show the PDF details with "Send PDF to Binect" button
**Test PDF URLs:**
```
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf
https://www.africau.edu/images/default/sample.pdf
```
**Expected Results:**
- PDF filename extracted from URL or tab title
- Domain shows the source domain
- Size shows "Size unknown" (normal for viewed PDFs)
- Timestamp shows "Just now"
- "Send PDF to Binect" button is active
### Test Scenario 2: Navigate Directly to a PDF URL
**Steps:**
1. Copy a PDF URL (use test URLs above)
2. Paste it into Chrome's address bar and press Enter
3. Chrome loads the PDF in the viewer
4. Click the BinectChrome extension icon
5. The popup should detect and show the PDF
**Expected Results:**
- Same as Scenario 1
### Test Scenario 3: View PDF from Google Drive / Cloud Storage
**Steps:**
1. Go to Google Drive, Dropbox, or any cloud storage
2. Click on a PDF file to view it
3. Wait for the PDF to load in the viewer
4. Click the BinectChrome extension icon
**Expected Results:**
- Extension detects the PDF
- Domain shows the cloud provider's domain
- File can be sent to Binect
**Note:** Some cloud providers use blob URLs or viewer applications that may not be directly detected. If this happens, download the PDF instead (it will be detected via download detection).
### Test Scenario 4: Send PDF to Binect API
**Steps:**
1. Open a PDF in Chrome's viewer (use Scenario 1 or 2)
2. Click the BinectChrome extension icon
3. If not signed in:
- Enter your Binect credentials
- Click "Sign In"
4. Once authenticated, the PDF should be shown
5. Click "Send PDF to Binect"
**Expected Results:**
- Status shows "Uploading..."
- After successful upload: "Success! Document ID: [id]"
- Tracking entry is created with the transfer details
- After 3 seconds, the popup clears
**Expected Errors (if testing with invalid credentials):**
- Authentication errors show in red
- Network errors are displayed
- Failed transfers are tracked with error messages
## API Integration Testing
### Test with Real Binect API
If you have Binect API credentials:
1. Sign in with valid credentials
2. Open a test PDF
3. Send it to Binect
4. Check the tracking info (click "?" button) to see the transfer log
5. Verify the document appears in your Binect account
### Test with Invalid Credentials
1. Sign in with invalid credentials
2. Should show "Invalid credentials" error
3. Extension should return to login screen
### Test with Network Errors
1. Disconnect from internet
2. Try to authenticate or send PDF
3. Should show "Network error" message
### Test Session Expiry
1. Sign in successfully
2. Wait for token to expire (or manipulate storage to simulate)
3. Try to send PDF
4. Should show "Session expired. Please sign in again."
5. Should automatically log out after 2 seconds
## Debugging Tips
### Check Console Logs
**Popup Console:**
1. Click extension icon to open popup
2. Right-click inside popup → "Inspect"
3. Check console for errors or debug messages
**Background Service Worker Console:**
1. Go to `chrome://extensions/`
2. Find BinectChrome
3. Click "service worker" link
4. Check console for detection logs
### Common Issues
**Issue: PDF not detected**
- **Solution 1:** The URL might not end with `.pdf` - this is normal for some cloud services
- **Solution 2:** Try refreshing the PDF tab
- **Solution 3:** Download the PDF instead (download detection should work)
**Issue: "Failed to fetch PDF: 403 Forbidden"**
- **Cause:** The PDF URL requires authentication/cookies that the extension can't access
- **Solution:** Download the PDF instead
**Issue: "Failed to fetch PDF: CORS error"**
- **Cause:** The server doesn't allow cross-origin requests
- **Solution:** Download the PDF instead
**Issue: Extension shows "No PDF detected"**
- **Check:** Is the current tab actually showing a PDF?
- **Check:** Does the URL end with `.pdf` or contain PDF indicators?
- **Try:** Download the PDF to test the download detection
## Verifying API Requests
### Using Chrome DevTools Network Tab
1. Open popup with DevTools open (right-click → Inspect)
2. Go to Network tab
3. Send a PDF
4. Look for requests to `https://api.binect.de/`
5. Check request details:
- **POST /auth/login** - Authentication request
- **POST /documents/upload** - PDF upload request
**Authentication Request:**
```json
POST https://api.binect.de/auth/login
Content-Type: application/json
{
"username": "your-username",
"password": "your-password"
}
```
**Upload Request:**
```
POST https://api.binect.de/documents/upload
Authorization: Bearer [token]
Content-Type: multipart/form-data
[PDF file data]
```
### Expected API Responses
**Successful Authentication:**
```json
{
"token": "eyJhbGc...",
"expiresAt": "2024-01-15T12:00:00Z"
}
```
**Successful Upload:**
```json
{
"documentId": "doc_abc123",
"status": "uploaded",
"uploadedAt": "2024-01-14T12:00:00Z"
}
```
**Authentication Error (401):**
```json
{
"error": "Invalid credentials"
}
```
**Upload Error (400):**
```json
{
"error": "Invalid file format"
}
```
## Tracking Data
After sending PDFs, check the tracking data:
1. Click the "?" button in the extension popup
2. Opens tracking page
3. Shows list of all transfer attempts
4. Includes:
- Timestamp
- Source domain
- PDF size
- Result (success/failure)
- Error message (if failed)
## Comparison: Download Detection vs. Viewer Detection
| Feature | Download Detection | PDF Viewer Detection |
|---------|-------------------|---------------------|
| **Trigger** | PDF file download completes | User opens PDF in browser |
| **File Size** | Known (from download) | Unknown (estimated after fetch) |
| **Reliability** | High | Depends on URL format |
| **Use Case** | Downloading PDFs from web | Viewing PDFs directly in browser |
| **Badge** | Shows "1" after download | No badge (on-demand) |
## Next Steps
Once you've verified the PDF viewer integration works:
1. Test with various PDF sources (Google Drive, Dropbox, direct links)
2. Verify all error cases are handled gracefully
3. Check that tracking data is accurate
4. Test the download detection still works alongside viewer detection
5. Consider edge cases:
- Very large PDFs (10MB+)
- PDFs with special characters in filename
- PDFs from authenticated sources
- PDFs from blob URLs
## Known Limitations
1. **Blob URLs**: Some web apps create temporary blob URLs that can't be re-fetched
- **Workaround**: Download the PDF instead
2. **Authenticated PDFs**: PDFs behind login walls may not be accessible
- **Workaround**: Download the PDF instead
3. **Embedded PDFs**: PDFs embedded in iframes may not be detected
- **Workaround**: Open the PDF in a new tab or download it
4. **Size Unknown**: PDF size is not known until fetch, so tracking may show 0 initially
- **Note**: Actual size is recorded after successful upload
## Support
If you encounter issues:
1. Check the console logs (popup and service worker)
2. Verify the PDF URL format
3. Try downloading the PDF as an alternative
4. Report issues to bernd.worsch@binect.de with:
- PDF URL (if public)
- Error message
- Console logs
- Tracking data export

198
dev-helper.sh Executable file
View File

@@ -0,0 +1,198 @@
#!/bin/bash
# Development Helper Script for BinectChrome
# Quick commands for common development tasks
set -e
EXTENSION_NAME="BinectChrome"
DIST_DIR="./dist"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Print colored message
print_msg() {
local color=$1
local msg=$2
echo -e "${color}${msg}${NC}"
}
# Show usage
usage() {
echo "BinectChrome Development Helper"
echo ""
echo "Usage: ./dev-helper.sh [command]"
echo ""
echo "Commands:"
echo " build - Production build"
echo " dev - Start development mode (watch)"
echo " clean - Clean dist directory and rebuild"
echo " test - Run tests"
echo " check - Run type-check and lint"
echo " open-chrome - Open Chrome extension management"
echo " open-sw - Open Chrome service worker internals"
echo " verify - Verify dist build is valid"
echo " help - Show this help"
echo ""
echo "Quick Test-Fix Loop:"
echo " 1. Run: ./dev-helper.sh dev (in one terminal)"
echo " 2. Make changes in src/"
echo " 3. Run: ./dev-helper.sh verify"
echo " 4. Reload extension in Chrome (chrome://extensions/)"
}
# Build production
build() {
print_msg "$BLUE" "📦 Building extension..."
npm run build
print_msg "$GREEN" "✅ Build complete!"
verify
}
# Start development mode
dev() {
print_msg "$BLUE" "🔧 Starting development mode (watch)..."
print_msg "$YELLOW" "Press Ctrl+C to stop"
print_msg "$YELLOW" "Tip: After changes, reload extension at chrome://extensions/"
npm run dev
}
# Clean and rebuild
clean() {
print_msg "$BLUE" "🧹 Cleaning dist directory..."
rm -rf "$DIST_DIR"
print_msg "$GREEN" "✅ Cleaned!"
build
}
# Run tests
test() {
print_msg "$BLUE" "🧪 Running tests..."
npm test
}
# Type check and lint
check() {
print_msg "$BLUE" "🔍 Running type-check..."
npm run type-check
print_msg "$BLUE" "🔍 Running lint..."
npm run lint
print_msg "$GREEN" "✅ All checks passed!"
}
# Open Chrome extensions page
open_chrome() {
print_msg "$BLUE" "🌐 Opening Chrome extensions page..."
if command -v google-chrome &> /dev/null; then
google-chrome chrome://extensions/ &
elif command -v chromium &> /dev/null; then
chromium chrome://extensions/ &
else
print_msg "$YELLOW" "⚠️ Chrome not found. Please open chrome://extensions/ manually"
fi
}
# Open Chrome service worker internals
open_sw() {
print_msg "$BLUE" "🌐 Opening Chrome service worker internals..."
if command -v google-chrome &> /dev/null; then
google-chrome chrome://serviceworker-internals/ &
elif command -v chromium &> /dev/null; then
chromium chrome://serviceworker-internals/ &
else
print_msg "$YELLOW" "⚠️ Chrome not found. Please open chrome://serviceworker-internals/ manually"
fi
}
# Verify build
verify() {
print_msg "$BLUE" "🔍 Verifying build..."
# Check if dist exists
if [ ! -d "$DIST_DIR" ]; then
print_msg "$RED" "❌ dist/ directory not found. Run build first."
exit 1
fi
# Check required files
REQUIRED_FILES=(
"$DIST_DIR/manifest.json"
"$DIST_DIR/background.js"
"$DIST_DIR/popup.html"
"$DIST_DIR/popup.js"
)
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$file" ]; then
print_msg "$RED" "❌ Missing required file: $file"
exit 1
fi
done
# Check manifest has required permissions
if ! grep -q '"alarms"' "$DIST_DIR/manifest.json"; then
print_msg "$RED" "❌ manifest.json missing 'alarms' permission"
exit 1
fi
# Check background.js is an ES module (should start with export/import)
if ! grep -qE '^(export|import)' "$DIST_DIR/background.js"; then
print_msg "$YELLOW" "⚠️ background.js might not be a proper ES module"
print_msg "$YELLOW" " First line: $(head -n 1 $DIST_DIR/background.js)"
fi
print_msg "$GREEN" "✅ Build verification passed!"
print_msg "$BLUE" "📋 Build info:"
echo " - Manifest version: $(grep -o '"version": "[^"]*"' $DIST_DIR/manifest.json | cut -d'"' -f4)"
echo " - Background size: $(du -h $DIST_DIR/background.js | cut -f1)"
echo " - Popup size: $(du -h $DIST_DIR/popup.js | cut -f1)"
echo ""
print_msg "$YELLOW" "Next steps:"
echo " 1. Go to chrome://extensions/"
echo " 2. Click 'Load unpacked'"
echo " 3. Select: $DIST_DIR"
echo " 4. Or reload if already loaded"
}
# Main
case "${1:-}" in
build)
build
;;
dev)
dev
;;
clean)
clean
;;
test)
test
;;
check)
check
;;
open-chrome)
open_chrome
;;
open-sw)
open_sw
;;
verify)
verify
;;
help|--help|-h)
usage
;;
*)
if [ -n "${1:-}" ]; then
print_msg "$RED" "❌ Unknown command: $1"
echo ""
fi
usage
exit 1
;;
esac

View 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 |

7
history/26016-cost.txt Normal file
View File

@@ -0,0 +1,7 @@
Total cost: $47.63
Total duration (API): 1h 13m 56s
Total duration (wall): 1d 9h 37m
Total code changes: 5197 lines added, 1404 lines removed
Usage by model:
claude-haiku: 89.0k input, 10.2k output, 187.2k cache read, 65.5k cache write ($0.2404)
claude-opus-4-5: 13.7k input, 224.9k output, 46.6m cache read, 2.9m cache write ($47.39)

View File

@@ -9,6 +9,9 @@ module.exports = {
'src/**/*.{ts,tsx}', 'src/**/*.{ts,tsx}',
'!src/**/*.d.ts' '!src/**/*.d.ts'
], ],
moduleNameMapper: {
'^@binect/js$': '<rootDir>/tests/__mocks__/@binect/js.ts'
},
globals: { globals: {
'ts-jest': { 'ts-jest': {
tsconfig: 'tsconfig.test.json' tsconfig: 'tsconfig.test.json'

20
package-lock.json generated
View File

@@ -8,6 +8,9 @@
"name": "binect-chrome", "name": "binect-chrome",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": {
"@binect/js": "file:../binect-js"
},
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.260", "@types/chrome": "^0.0.260",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
@@ -28,6 +31,19 @@
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
} }
}, },
"../binect-js": {
"name": "@binect/js",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
@@ -545,6 +561,10 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@binect/js": {
"resolved": "../binect-js",
"link": true
},
"node_modules/@discoveryjs/json-ext": { "node_modules/@discoveryjs/json-ext": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",

View File

@@ -2,6 +2,9 @@
"name": "binect-chrome", "name": "binect-chrome",
"version": "1.0.0", "version": "1.0.0",
"description": "Chrome extension to send PDFs from cloud applications directly to Binect for physical mail delivery", "description": "Chrome extension to send PDFs from cloud applications directly to Binect for physical mail delivery",
"dependencies": {
"@binect/js": "file:../binect-js"
},
"scripts": { "scripts": {
"build": "webpack --mode production", "build": "webpack --mode production",
"dev": "webpack --mode development --watch", "dev": "webpack --mode development --watch",

View File

@@ -3,19 +3,23 @@
"name": "BinectChrome", "name": "BinectChrome",
"version": "1.0.0", "version": "1.0.0",
"description": "Send PDFs from cloud applications directly to Binect for physical mail delivery", "description": "Send PDFs from cloud applications directly to Binect for physical mail delivery",
"default_locale": "en",
"permissions": [ "permissions": [
"downloads", "downloads",
"storage" "storage",
"alarms",
"activeTab"
], ],
"host_permissions": [ "host_permissions": [
"https://api.binect.de/*" "https://api.binect.de/*",
"<all_urls>"
], ],
"background": { "background": {
"service_worker": "background.js", "service_worker": "background.js"
"type": "module"
}, },
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_title": "BinectChrome - Send PDFs to postal mail",
"default_icon": { "default_icon": {
"16": "icons/icon-16.png", "16": "icons/icon-16.png",
"32": "icons/icon-32.png", "32": "icons/icon-32.png",

BIN
specs/binectapi_rest.pdf Executable file

Binary file not shown.

2825
specs/v1_swagger_api_kernel.json Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,42 @@
/** /**
* Service Worker (Background Script) * Service Worker (Background Script)
* Handles PDF detection and credential expiry checks * Handles PDF detection, queue management, and credential expiry checks
*/ */
import { startPDFDetection, DetectedPDF } from '../utils/pdf-detector'; import { startPDFDetection, DetectedPDF } from '../utils/pdf-detector';
import { loadCredentials } from '../utils/storage'; import { loadCredentials } from '../utils/storage';
import {
// Store last detected PDF in memory (ephemeral) addPDF,
let lastDetectedPDF: DetectedPDF | null = null; getActionableCount,
getAllPDFs,
getLiveProxies,
getArchivedProxies,
getPendingPDFs,
updatePDFStatus,
archiveProxy,
restoreProxy,
dismissPDF,
removePDF,
cleanupOldEntries,
syncFromServer,
clearServerFields,
attachServerDocument,
PDFStatus,
PDFStatusMeta
} from '../utils/pdf-queue';
import { shipDocument, getDocumentStatus, deleteDocument, listServerDocuments } from '../utils/binect-api';
/** /**
* Initialize extension on install * Initialize extension on install
*/ */
chrome.runtime.onInstalled.addListener((details) => { chrome.runtime.onInstalled.addListener((details) => {
console.log('[Service Worker] onInstalled event:', details.reason);
if (details.reason === 'install') { if (details.reason === 'install') {
console.log('BinectChrome installed'); console.log('[Service Worker] BinectChrome installed');
setupCredentialExpiryAlarm(); setupAlarms();
} else if (details.reason === 'update') { } else if (details.reason === 'update') {
console.log('BinectChrome updated'); console.log('[Service Worker] BinectChrome updated');
setupAlarms();
} }
}); });
@@ -25,17 +44,25 @@ chrome.runtime.onInstalled.addListener((details) => {
* Handle extension startup * Handle extension startup
*/ */
chrome.runtime.onStartup.addListener(() => { chrome.runtime.onStartup.addListener(() => {
console.log('BinectChrome started'); console.log('[Service Worker] onStartup event - BinectChrome started');
setupCredentialExpiryAlarm(); setupAlarms();
updateBadge();
}); });
/** /**
* Set up alarm to check credential expiry daily * Set up alarms for periodic tasks
*/ */
function setupCredentialExpiryAlarm() { function setupAlarms() {
// Credential expiry check
chrome.alarms.create('checkCredentialExpiry', { chrome.alarms.create('checkCredentialExpiry', {
delayInMinutes: 1, // First check in 1 minute delayInMinutes: 1,
periodInMinutes: 24 * 60 // Then every 24 hours periodInMinutes: 24 * 60 // Every 24 hours
});
// PDF queue cleanup
chrome.alarms.create('cleanupPDFQueue', {
delayInMinutes: 60, // First cleanup in 1 hour
periodInMinutes: 6 * 60 // Every 6 hours
}); });
} }
@@ -46,6 +73,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'checkCredentialExpiry') { if (alarm.name === 'checkCredentialExpiry') {
checkAndDeleteExpiredCredentials(); checkAndDeleteExpiredCredentials();
} }
if (alarm.name === 'cleanupPDFQueue') {
cleanupOldEntries();
}
}); });
/** /**
@@ -53,49 +83,315 @@ chrome.alarms.onAlarm.addListener((alarm) => {
*/ */
async function checkAndDeleteExpiredCredentials() { async function checkAndDeleteExpiredCredentials() {
const credentials = await loadCredentials(); const credentials = await loadCredentials();
// loadCredentials already handles expiry check and deletion
// If credentials are expired, it returns null and deletes them
if (credentials === null) { if (credentials === null) {
console.log('Credentials expired and deleted'); console.log('[Service Worker] Credentials expired and deleted');
} }
} }
/**
* Update badge with actionable PDF count
*/
async function updateBadge() {
const count = await getActionableCount();
const text = count > 0 ? count.toString() : '•';
chrome.action.setBadgeText({ text });
chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue
console.log('[Service Worker] Badge updated:', text);
}
// Initialize badge on load
updateBadge();
/** /**
* Start PDF detection * Start PDF detection
*/ */
startPDFDetection((pdf: DetectedPDF) => { console.log('[Service Worker] Initializing PDF detection...');
console.log('PDF detected:', pdf.filename); startPDFDetection(async (pdf: DetectedPDF) => {
lastDetectedPDF = pdf; console.log('[Service Worker] PDF DETECTED:', pdf.filename);
// Update badge to indicate PDF detected // Add to persistent queue
chrome.action.setBadgeText({ text: '1' }); const entry = await addPDF(pdf);
chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue
if (entry) {
console.log('[Service Worker] PDF added to queue:', entry.filename);
} else {
console.log('[Service Worker] PDF skipped (already uploaded):', pdf.filename);
}
// Update badge
await updateBadge();
}); });
/** /**
* Handle messages from popup * Handle messages from popup
*/ */
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('[Service Worker] Message received:', request.action);
// Get all PDFs (including completed ones for display)
if (request.action === 'getAllPDFs') {
getAllPDFs().then(entries => {
console.log('[Service Worker] Returning all PDFs:', entries.length, 'entries');
sendResponse({ entries });
});
return true;
}
// Get live proxy documents (not archived)
if (request.action === 'getLiveProxies') {
getLiveProxies().then(entries => {
console.log('[Service Worker] Returning live proxies:', entries.length, 'entries');
sendResponse({ entries });
});
return true;
}
// Get archived proxy documents
if (request.action === 'getArchivedProxies') {
getArchivedProxies().then(entries => {
console.log('[Service Worker] Returning archived proxies:', entries.length, 'entries');
sendResponse({ entries });
});
return true;
}
// Add a PDF to the queue (from popup discovery)
if (request.action === 'addPDF') {
addPDF(request.pdf).then(entry => {
if (entry) {
console.log('[Service Worker] PDF added via message:', entry.filename);
}
return updateBadge().then(() => entry);
}).then(entry => {
sendResponse({ entry });
});
return true;
}
// Legacy: Get only actionable PDFs
if (request.action === 'getPDFQueue') {
getPendingPDFs().then(entries => {
console.log('[Service Worker] Returning PDF queue:', entries.length, 'entries');
sendResponse({ entries });
});
return true;
}
if (request.action === 'updatePDFStatus') {
const { id, status, meta } = request as { id: string; status: PDFStatus; meta?: PDFStatusMeta };
updatePDFStatus(id, status, meta).then(() => {
return updateBadge();
}).then(() => {
sendResponse({ success: true });
});
return true;
}
if (request.action === 'dismissPDF') {
dismissPDF(request.id).then(() => {
return updateBadge();
}).then(() => {
sendResponse({ success: true });
});
return true;
}
// Archive a proxy document (move to archive view)
if (request.action === 'archiveProxy') {
archiveProxy(request.id).then(() => {
return updateBadge();
}).then(() => {
sendResponse({ success: true });
});
return true;
}
// Restore a proxy document (move back to live view)
if (request.action === 'restoreProxy') {
restoreProxy(request.id).then(() => {
return updateBadge();
}).then(() => {
sendResponse({ success: true });
});
return true;
}
if (request.action === 'removePDF') {
removePDF(request.id).then(() => {
return updateBadge();
}).then(() => {
sendResponse({ success: true });
});
return true;
}
// Ship a document (place order for production)
if (request.action === 'shipDocument') {
const { documentId, username, password } = request as {
documentId: number;
username: string;
password: string;
};
shipDocument(documentId, username, password)
.then(result => {
sendResponse({ success: true, ...result });
})
.catch(error => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Failed to ship document'
});
});
return true;
}
// Get document status from Binect
if (request.action === 'getDocumentStatus') {
const { documentId, username, password } = request as {
documentId: number;
username: string;
password: string;
};
getDocumentStatus(documentId, username, password)
.then(result => {
sendResponse({ success: true, ...result });
})
.catch(error => {
// Include error code for 404 detection
const errorCode = (error as { statusCode?: number }).statusCode;
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Failed to get status',
errorCode
});
});
return true;
}
// Delete a document from the server
if (request.action === 'deleteServerDocument') {
const { documentId, username, password } = request as {
documentId: number;
username: string;
password: string;
};
deleteDocument(documentId, username, password)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Failed to delete document'
});
});
return true;
}
// List all documents from the server (for sync)
if (request.action === 'listServerDocuments') {
const { username, password } = request as {
username: string;
password: string;
};
listServerDocuments(username, password)
.then(documents => {
sendResponse({ success: true, documents });
})
.catch(error => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Failed to list documents'
});
});
return true;
}
// Sync a server document to local proxy (create or update)
if (request.action === 'syncFromServer') {
const { binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails } = request as {
binectDocumentId: number;
filename: string;
binectStatusCode: number;
binectStatusText: string;
price?: number;
recipientAddress?: string;
errorDetails?: string;
};
syncFromServer(binectDocumentId, filename, binectStatusCode, binectStatusText, price, recipientAddress, errorDetails)
.then(proxy => {
sendResponse({ success: true, proxy });
})
.catch(error => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Failed to sync document'
});
});
return true;
}
// Clear server fields from a proxy (when deleted from server)
if (request.action === 'clearServerFields') {
clearServerFields(request.id).then(() => {
return updateBadge();
}).then(() => {
sendResponse({ success: true });
});
return true;
}
// Attach a server document to a local proxy
if (request.action === 'attachServerDocument') {
const { id, binectDocumentId, binectStatusCode, binectStatusText, price, recipientAddress, errorMessage } = request as {
id: string;
binectDocumentId: number;
binectStatusCode: number;
binectStatusText: string;
price?: number;
recipientAddress?: string;
errorMessage?: string;
};
attachServerDocument(id, binectDocumentId, binectStatusCode, binectStatusText, price, recipientAddress, errorMessage)
.then(() => {
return updateBadge();
})
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Failed to attach document'
});
});
return true;
}
// Legacy handlers for backward compatibility
if (request.action === 'getLastPDF') { if (request.action === 'getLastPDF') {
sendResponse({ pdf: lastDetectedPDF }); getPendingPDFs().then(entries => {
const pdf = entries.length > 0 ? entries[0] : null;
sendResponse({ pdf });
});
return true; return true;
} }
if (request.action === 'clearLastPDF') { if (request.action === 'clearLastPDF' || request.action === 'pdfSent') {
lastDetectedPDF = null; updateBadge().then(() => {
chrome.action.setBadgeText({ text: '' }); sendResponse({ success: true });
sendResponse({ success: true }); });
return true;
}
if (request.action === 'pdfSent') {
// Clear badge after successful send
chrome.action.setBadgeText({ text: '' });
sendResponse({ success: true });
return true; return true;
} }
return false; return false;
}); });
console.log('BinectChrome service worker loaded'); console.log('[Service Worker] ===== BinectChrome service worker loaded =====');
console.log('[Service Worker] Timestamp:', new Date().toISOString());

View File

@@ -93,6 +93,114 @@ body {
outline-offset: 2px; outline-offset: 2px;
} }
.icon-btn svg {
display: block;
}
.icon-btn.refreshing svg {
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);
}
to {
transform: rotate(360deg);
}
}
/* Header actions */
.header-actions {
display: flex;
gap: var(--spacing-sm);
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 */ /* Views */
.view { .view {
flex: 1; flex: 1;
@@ -133,6 +241,47 @@ body {
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
} }
/* Password Input Wrapper */
.password-input-wrapper {
position: relative;
}
.password-input-wrapper input {
padding-right: 44px; /* Make room for the eye icon */
}
.password-toggle {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
border-radius: 4px;
transition: all 0.2s;
}
.password-toggle:hover {
background: var(--light-bg);
color: var(--binect-blue);
}
.password-toggle:focus {
outline: 2px solid var(--binect-blue);
outline-offset: 2px;
}
.password-toggle svg {
display: block;
pointer-events: none;
}
/* Buttons */ /* Buttons */
.btn { .btn {
width: 100%; width: 100%;
@@ -232,6 +381,369 @@ body {
margin-top: var(--spacing-xs); margin-top: var(--spacing-xs);
} }
/* PDF List */
.pdf-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
}
.pdf-count {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
}
.pdf-list {
max-height: 280px;
overflow-y: auto;
}
.pdf-list-item {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--light-bg);
border-radius: var(--border-radius);
margin-bottom: var(--spacing-sm);
transition: background 0.2s;
}
.pdf-list-item:last-child {
margin-bottom: 0;
}
.pdf-list-item:hover {
background: var(--border-color);
}
/* Status-specific item styles */
.pdf-list-item.uploading {
opacity: 0.7;
}
.pdf-list-item.in-basket {
background: rgba(74, 144, 226, 0.1);
border-left: 3px solid var(--binect-blue);
}
.pdf-list-item.in-production {
background: rgba(0, 188, 212, 0.1);
border-left: 3px solid var(--cyan);
}
.pdf-list-item.sent {
background: rgba(76, 175, 80, 0.1);
border-left: 3px solid var(--signal-green);
}
.pdf-list-item.canceled {
background: rgba(153, 153, 153, 0.1);
border-left: 3px solid var(--text-light);
}
.pdf-list-item.failed {
background: rgba(229, 57, 53, 0.1);
border-left: 3px solid var(--red);
}
.pdf-item-icon {
font-size: 24px;
flex-shrink: 0;
}
.pdf-item-details {
flex: 1;
min-width: 0;
}
.pdf-item-filename {
font-weight: 500;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pdf-item-meta {
font-size: 11px;
color: var(--text-secondary);
margin-top: 2px;
}
.pdf-item-status {
font-size: 10px;
color: var(--text-light);
margin-top: 2px;
}
.pdf-item-status.success {
color: var(--signal-green);
}
.pdf-item-status.error {
color: var(--red);
}
.pdf-item-status.in-basket {
color: var(--binect-blue);
}
.pdf-item-status.in-production {
color: var(--cyan);
}
.pdf-item-status.sent {
color: var(--signal-green);
}
.pdf-item-status.canceled {
color: var(--text-light);
}
.pdf-item-status.failed {
color: var(--red);
}
/* Error details for erroneous documents */
.pdf-item-error {
font-size: 10px;
color: var(--red);
margin-top: 4px;
padding: 4px 6px;
background: rgba(229, 57, 53, 0.08);
border-radius: 3px;
line-height: 1.3;
}
/* Price display */
.pdf-price {
font-weight: 600;
color: var(--binect-blue-deep);
}
/* Local tag - clickable to check server */
.tag-local {
display: inline-block;
margin-left: 6px;
padding: 2px 6px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
background: var(--light-bg);
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 */
.pdf-item-recipient {
font-size: 11px;
color: var(--text-secondary);
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Section headers */
.pdf-section {
margin-bottom: var(--spacing-md);
}
.pdf-section:last-child {
margin-bottom: 0;
}
.pdf-section-header {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: var(--spacing-sm);
padding-bottom: var(--spacing-xs);
border-bottom: 1px solid var(--border-color);
}
.pdf-section-completed {
opacity: 0.8;
}
.pdf-section-completed .pdf-section-header {
color: var(--text-light);
}
.pdf-item-actions {
display: flex;
flex-direction: column;
gap: 4px;
flex-shrink: 0;
}
.btn-send-item {
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-send-item:hover {
background: var(--binect-blue-deep);
}
.btn-send-item:disabled {
background: var(--border-color);
cursor: not-allowed;
}
.btn-dismiss {
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-dismiss:hover {
background: var(--light-bg);
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);
}
/* 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 */
.btn-order-item {
padding: 6px 12px;
font-size: 11px;
min-height: auto;
background: var(--signal-green);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
font-weight: 500;
}
.btn-order-item:hover {
background: #43a047;
}
.btn-order-item:disabled {
background: var(--border-color);
cursor: not-allowed;
}
/* Refresh button */
.btn-refresh-item {
padding: 6px 12px;
font-size: 11px;
min-height: auto;
background: var(--light-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.btn-refresh-item:hover {
background: var(--border-color);
border-color: var(--text-light);
}
/* Remove button */
.btn-remove {
opacity: 0.7;
}
/* Status Messages */ /* Status Messages */
.status-message { .status-message {
padding: var(--spacing-md); padding: var(--spacing-md);
@@ -296,6 +808,235 @@ body {
border-radius: 2px; 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 */ /* Accessibility */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { * {

View File

@@ -4,14 +4,38 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BinectChrome</title> <title>BinectChrome</title>
<link rel="stylesheet" href="popup.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- Header --> <!-- Header -->
<div class="header"> <div class="header">
<h1>BinectChrome</h1> <h1>BinectChrome</h1>
<button id="helpBtn" class="icon-btn" aria-label="Help and tracking info" title="Help & Info">?</button> <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="helpBtn" class="icon-btn" aria-label="Help and tracking info" title="Help & Info">?</button>
</div>
</div>
<!-- First-run Pin Reminder -->
<div id="pinReminder" class="pin-reminder" style="display: none;">
<div class="pin-reminder-content">
<span class="pin-reminder-icon">📌</span>
<span class="pin-reminder-text">
<strong>Tip:</strong> Pin this extension to your toolbar for quick access.
Click the <span class="puzzle-icon">🧩</span> icon → find BinectChrome → click the pin.
</span>
<button id="dismissPinReminder" class="pin-reminder-dismiss" aria-label="Dismiss">&times;</button>
</div>
</div> </div>
<!-- Authentication View --> <!-- Authentication View -->
@@ -26,7 +50,19 @@
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password"> <div class="password-input-wrapper">
<input type="password" id="password" name="password" required autocomplete="current-password">
<button type="button" id="togglePassword" class="password-toggle" aria-label="Show password" title="Show password">
<svg id="eyeIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg id="eyeOffIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
</div> </div>
<button type="submit" class="btn btn-primary" id="loginBtn">Sign In</button> <button type="submit" class="btn btn-primary" id="loginBtn">Sign In</button>
@@ -39,27 +75,20 @@
<div id="mainView" class="view" style="display: none;"> <div id="mainView" class="view" style="display: none;">
<!-- No PDF Detected --> <!-- No PDF Detected -->
<div id="noPdfView" class="content-section"> <div id="noPdfView" class="content-section">
<p class="info-text">No PDF detected. Download a PDF to get started.</p> <p class="info-text">No PDF detected. Open or download a PDF to get started.</p>
</div> </div>
<!-- PDF Detected --> <!-- PDF List -->
<div id="pdfView" class="content-section" style="display: none;"> <div id="pdfListView" class="content-section" style="display: none;">
<div class="pdf-info"> <div class="pdf-list-header">
<div class="pdf-icon">📄</div> <span class="pdf-count" id="pdfCount">0 PDFs ready</span>
<div class="pdf-details">
<div class="pdf-filename" id="pdfFilename"></div>
<div class="pdf-meta">
<span id="pdfSize"></span><span id="pdfDomain"></span>
</div>
<div class="pdf-timestamp" id="pdfTimestamp"></div>
</div>
</div> </div>
<button id="sendBtn" class="btn btn-primary btn-large"> <div id="pdfList" class="pdf-list">
Send PDF to Binect <!-- PDF items will be inserted here dynamically -->
</button> </div>
<!-- Progress/Status --> <!-- Global status message -->
<div id="statusMessage" class="status-message" style="display: none;"></div> <div id="statusMessage" class="status-message" style="display: none;"></div>
</div> </div>
@@ -71,9 +100,115 @@
<!-- Footer --> <!-- Footer -->
<div class="footer"> <div class="footer">
<a href="mailto:bernd.worsch@binect.de?subject=BinectChrome Feedback" class="footer-link"> <button id="reportIssueBtn" class="footer-link-btn">
Report Issue / Request Feature Report Issue / Request Feature
</a> </button>
</div>
</div>
<!-- Issue Report Modal -->
<div id="issueModal" class="modal" style="display: none;">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Report Issue / Request Feature</h2>
<button id="closeModalBtn" class="modal-close" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="issueTitle">Title</label>
<input type="text" id="issueTitle" placeholder="Brief summary of the issue or feature request">
</div>
<div class="form-group">
<label for="issueDescription">Description</label>
<textarea id="issueDescription" rows="4" placeholder="Describe the issue or feature in detail..."></textarea>
</div>
<div class="modal-actions">
<button id="copyToClipboardBtn" class="btn btn-primary">Copy to Clipboard</button>
<a href="http://92.205.130.254:32166/coulomb/binect-chrome/issues/new"
target="_blank"
rel="noopener noreferrer"
class="submit-link">
Submit here! Thanks!
</a>
</div>
<div class="context-sections">
<h3>Context Information</h3>
<p class="context-hint">The following information will be included to help diagnose issues:</p>
<!-- Extension Info Section -->
<div class="context-section">
<div class="context-section-header">
<button class="context-toggle" data-section="extensionInfo">
<span class="toggle-icon"></span>
Extension Info
</button>
<label class="exclude-checkbox">
<input type="checkbox" data-exclude="extensionInfo">
do not include
</label>
</div>
<div class="context-section-content" id="extensionInfo" style="display: none;">
<pre id="extensionInfoContent">Loading...</pre>
</div>
</div>
<!-- Browser Info Section -->
<div class="context-section">
<div class="context-section-header">
<button class="context-toggle" data-section="browserInfo">
<span class="toggle-icon"></span>
Browser Info
</button>
<label class="exclude-checkbox">
<input type="checkbox" data-exclude="browserInfo">
do not include
</label>
</div>
<div class="context-section-content" id="browserInfo" style="display: none;">
<pre id="browserInfoContent">Loading...</pre>
</div>
</div>
<!-- Document Status Section -->
<div class="context-section">
<div class="context-section-header">
<button class="context-toggle" data-section="documentStatus">
<span class="toggle-icon"></span>
Document Status
</button>
<label class="exclude-checkbox">
<input type="checkbox" data-exclude="documentStatus">
do not include
</label>
</div>
<div class="context-section-content" id="documentStatus" style="display: none;">
<pre id="documentStatusContent">Loading...</pre>
</div>
</div>
<!-- Recent Errors Section -->
<div class="context-section">
<div class="context-section-header">
<button class="context-toggle" data-section="recentErrors">
<span class="toggle-icon"></span>
Recent Errors
</button>
<label class="exclude-checkbox">
<input type="checkbox" data-exclude="recentErrors">
do not include
</label>
</div>
<div class="context-section-content" id="recentErrors" style="display: none;">
<pre id="recentErrorsContent">No recent errors</pre>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,68 @@
/** /**
* Binect API client * Binect API client
*
* This module wraps the @binect/js library to provide a simplified API
* for the Chrome extension. It delegates all API integration to the
* upstream library.
*/ */
const API_BASE_URL = 'https://api.binect.de'; import {
BinectClient,
BinectApiError,
BinectAuthError,
DocumentStatus,
isErroneous,
getErrors,
getStatusDescription,
type Document as BinectDocument,
type DocumentUploadOptions,
EnvelopeType,
FrankingType,
} from '@binect/js';
export interface AuthToken { // Re-export types for backward compatibility
token: string; export interface Options {
expiresAt: string; simplex?: boolean; // if false, it's duplex
color?: boolean; // if false, it's black and white
envelope?: 'DINLANG' | 'C4';
franking?: 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING';
} }
export interface UploadResult { // Document type matching what popup.ts expects
documentId: string; export interface Document {
status: string; id: number;
uploadedAt: string; filename: string;
numberOfPages?: number;
status: {
code: number;
text: string;
};
documentType: 'Letter' | 'SerialLetter';
letter?: {
letterType: 'LetterData' | 'Error';
letterData?: {
recipientAddress: string;
price: {
priceBeforeTax: number;
priceAfterTax: number;
unit: string;
taxInPercent: number;
};
international: boolean;
options: Options;
};
errors?: Array<{
code: number;
text: string;
blankText: string;
}>;
};
} }
/**
* Custom error class for Binect API errors
* Wraps errors from @binect/js for backward compatibility
*/
export class BinectAPIError extends Error { export class BinectAPIError extends Error {
constructor( constructor(
message: string, message: string,
@@ -24,98 +72,208 @@ export class BinectAPIError extends Error {
super(message); super(message);
this.name = 'BinectAPIError'; this.name = 'BinectAPIError';
} }
}
/** /**
* Authenticate with Binect API * Create from a @binect/js error
*/ */
export async function authenticate( static fromBinectError(error: BinectApiError | BinectAuthError): BinectAPIError {
username: string, if (error instanceof BinectAuthError) {
password: string return new BinectAPIError('Invalid credentials', 401);
): Promise<AuthToken> {
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
if (response.status === 401) {
throw new BinectAPIError('Invalid credentials', 401);
}
throw new BinectAPIError(
`Authentication failed: ${response.statusText}`,
response.status
);
} }
return new BinectAPIError(
return await response.json(); error.message,
} catch (error) { error.status,
if (error instanceof BinectAPIError) { error.response
throw error;
}
throw new BinectAPIError(
`Network error: ${error instanceof Error ? error.message : 'Unknown error'}`
); );
} }
} }
/** /**
* Upload PDF to Binect * Convert ArrayBuffer to base64 string (browser-compatible)
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Map local Options to DocumentUploadOptions
*/
function mapOptions(options?: Options): Partial<DocumentUploadOptions> {
if (!options) {
return {
simplex: false, // duplex by default
color: false, // black and white by default
};
}
const mapped: Partial<DocumentUploadOptions> = {
simplex: options.simplex ?? false,
color: options.color ?? false,
};
if (options.envelope) {
mapped.envelope = options.envelope === 'C4' ? EnvelopeType.C4 : EnvelopeType.DINLANG;
}
if (options.franking) {
switch (options.franking) {
case 'STANDARD_FRANKING':
mapped.franking = FrankingType.STANDARD_FRANKING;
break;
case 'DV_FRANKING':
mapped.franking = FrankingType.DV_FRANKING;
break;
default:
mapped.franking = FrankingType.UNSPECIFIED;
}
}
return mapped;
}
/**
* Convert BinectDocument to our Document interface
*/
function mapDocument(doc: BinectDocument): Document {
// Build letter data if present
let letterData: Document['letter'] = undefined;
if (doc.letter) {
const ld = doc.letter.letterData;
letterData = {
letterType: (doc.letter.letterType || 'LetterData') as 'LetterData' | 'Error',
letterData: ld ? {
recipientAddress: ld.recipientAddress || '',
price: ld.price || { priceBeforeTax: 0, priceAfterTax: 0, unit: 'EUROCENT', taxInPercent: 0 },
international: ld.international || false,
options: {
simplex: ld.options?.simplex,
color: ld.options?.color,
envelope: ld.options?.envelope as 'DINLANG' | 'C4' | undefined,
franking: ld.options?.franking as 'UNSPECIFIED' | 'STANDARD_FRANKING' | 'DV_FRANKING' | undefined,
},
} : undefined,
errors: doc.letter.errors?.map(e => ({
code: parseInt(e.code, 10) || 0,
text: e.message,
blankText: e.message,
})),
};
}
return {
id: doc.id,
filename: doc.filename || 'document.pdf',
numberOfPages: doc.numberOfPages,
status: {
code: doc.status.code,
text: doc.status.text,
},
documentType: (doc.documentType === 'SERIALLETTER' ? 'SerialLetter' : 'Letter') as 'Letter' | 'SerialLetter',
letter: letterData,
};
}
/**
* Upload PDF to Binect API
*
* Uses the @binect/js library to upload the PDF.
*
* @param pdfData - PDF file as ArrayBuffer
* @param filename - Name of the PDF file
* @param username - Binect username for authentication
* @param password - Binect password for authentication
* @param options - Optional printing options (simplex/duplex, color/bw, etc.)
* @returns Document object with ID and status
*/ */
export async function uploadPDF( export async function uploadPDF(
pdfData: ArrayBuffer, pdfData: ArrayBuffer,
filename: string, filename: string,
token: string username: string,
): Promise<UploadResult> { password: string,
try { options?: Options
const formData = new FormData(); ): Promise<Document> {
const blob = new Blob([pdfData], { type: 'application/pdf' }); console.log('[Binect API] Uploading PDF to Binect via @binect/js...');
formData.append('file', blob, filename); console.log('[Binect API] Filename:', filename);
formData.append('filename', filename); console.log('[Binect API] PDF size:', pdfData.byteLength, 'bytes');
const response = await fetch(`${API_BASE_URL}/documents/upload`, { try {
method: 'POST', // Create client with credentials
headers: { const client = new BinectClient({
Authorization: `Bearer ${token}` username,
}, password,
body: formData
}); });
if (!response.ok) { // Convert PDF to base64
const errorData = await response.json().catch(() => ({})); console.log('[Binect API] Converting PDF to base64...');
const base64Content = arrayBufferToBase64(pdfData);
console.log('[Binect API] Base64 length:', base64Content.length, 'characters');
if (response.status === 401) { // Map options
throw new BinectAPIError('Authentication required', 401, errorData); const uploadOptions = mapOptions(options);
console.log('[Binect API] Upload options:', uploadOptions);
// Upload document
const doc = await client.documents.upload({
content: base64Content,
filename,
...uploadOptions,
});
console.log('[Binect API] Upload successful!');
console.log('[Binect API] Document ID:', doc.id);
console.log('[Binect API] Document status:', doc.status);
// Log if document has errors (status ERRONEOUS)
// But still return the document so we can track it and offer delete
if (isErroneous(doc)) {
console.warn('[Binect API] Document is erroneous:', doc.status.text);
const errors = getErrors(doc);
if (errors.length > 0) {
console.warn('[Binect API] Document errors:', errors.map(e => e.message).join('; '));
} }
if (response.status === 400) {
throw new BinectAPIError(
errorData.error || 'Invalid file format',
400,
errorData
);
}
if (response.status === 413) {
throw new BinectAPIError('File size exceeds limit', 413, errorData);
}
throw new BinectAPIError(
`Upload failed: ${response.statusText}`,
response.status,
errorData
);
} }
return await response.json(); return mapDocument(doc);
} catch (error) { } catch (error) {
console.error('[Binect API] Upload error:', error);
// Handle @binect/js errors
if (error instanceof BinectAuthError) {
throw new BinectAPIError('Invalid credentials', 401);
}
if (error instanceof BinectApiError) {
// Map specific status codes
if (error.status === 400) {
throw new BinectAPIError(
'Invalid request. Please check the PDF format and size.',
400
);
}
if (error.status === 413) {
throw new BinectAPIError('File size exceeds limit (12 MB)', 413);
}
throw BinectAPIError.fromBinectError(error);
}
// Already a BinectAPIError
if (error instanceof BinectAPIError) { if (error instanceof BinectAPIError) {
throw error; throw error;
} }
// Network or other errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new BinectAPIError(
'Cannot reach Binect API. Please check your internet connection.'
);
}
throw new BinectAPIError( throw new BinectAPIError(
`Network error: ${error instanceof Error ? error.message : 'Unknown error'}` `Network error: ${error instanceof Error ? error.message : 'Unknown error'}`
); );
@@ -123,15 +281,320 @@ export async function uploadPDF(
} }
/** /**
* Test API connectivity * Test API connectivity by fetching account information
*
* @param username - Binect username
* @param password - Binect password
* @returns true if authentication successful, false otherwise
*/ */
export async function testConnection(): Promise<boolean> { export async function testConnection(username: string, password: string): Promise<boolean> {
console.log('[Binect API] Testing connection via @binect/js...');
try { try {
const response = await fetch(`${API_BASE_URL}/health`, { const client = new BinectClient({
method: 'GET' username,
password,
}); });
return response.ok;
} catch { // Attempt to get account info
await client.accounts.get();
console.log('[Binect API] Connection successful');
return true;
} catch (error) {
if (error instanceof BinectAuthError) {
console.log('[Binect API] Authentication failed');
return false;
}
if (error instanceof BinectApiError) {
console.warn('[Binect API] API error:', error.message);
// If we get any other API error, credentials might still be valid
// but there's another issue
return false;
}
console.error('[Binect API] Connection test error:', error);
return false; return false;
} }
} }
/**
* Document status information returned by getDocumentStatus
*/
export interface DocumentStatusInfo {
status: number;
statusText: string;
price?: number;
recipientAddress?: string;
errorDetails?: string; // Error details for erroneous documents
}
/**
* Ship a document (place order for production)
*
* This announces the document for delivery, transitioning it from
* SHIPPABLE to PRODUCTION_QUEUE.
*
* @param documentId - Binect document ID
* @param username - Binect username
* @param password - Binect password
* @returns Updated status info
*/
export async function shipDocument(
documentId: number,
username: string,
password: string
): Promise<DocumentStatusInfo> {
console.log('[Binect API] Shipping document:', documentId);
try {
const client = new BinectClient({
username,
password,
});
// Send the document for production
const sending = await client.sendings.send(String(documentId));
console.log('[Binect API] Document shipped successfully');
console.log('[Binect API] New status:', sending.status);
return {
status: sending.status,
statusText: getStatusDescription(sending.status),
price: sending.price,
};
} catch (error) {
console.error('[Binect API] Ship error:', error);
if (error instanceof BinectAuthError) {
throw new BinectAPIError('Invalid credentials', 401);
}
if (error instanceof BinectApiError) {
// Check for insufficient balance (error code 2330)
if (error.message.includes('2330') || error.message.includes('balance')) {
throw new BinectAPIError('Insufficient account balance', 402);
}
throw BinectAPIError.fromBinectError(error);
}
if (error instanceof BinectAPIError) {
throw error;
}
throw new BinectAPIError(
`Failed to ship document: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Get current status of a document
*
* @param documentId - Binect document ID
* @param username - Binect username
* @param password - Binect password
* @returns Current status info
*/
export async function getDocumentStatus(
documentId: number,
username: string,
password: string
): Promise<DocumentStatusInfo> {
console.log('[Binect API] Getting document status:', documentId);
try {
const client = new BinectClient({
username,
password,
});
// Fetch document details
const doc = await client.documents.get(String(documentId));
console.log('[Binect API] Document status:', doc.status);
// Extract price and recipient if available
let price: number | undefined;
let recipientAddress: string | undefined;
let errorDetails: string | undefined;
if (doc.letter?.letterData) {
price = doc.letter.letterData.price?.priceAfterTax;
recipientAddress = doc.letter.letterData.recipientAddress;
}
// Extract error details for erroneous documents
if (isErroneous(doc)) {
const errors = getErrors(doc);
if (errors.length > 0) {
errorDetails = errors.map(e => e.message).join('; ');
console.log('[Binect API] Document errors:', errorDetails);
}
}
return {
status: doc.status.code,
statusText: doc.status.text || getStatusDescription(doc.status.code),
price,
recipientAddress,
errorDetails,
};
} catch (error) {
console.error('[Binect API] Get status error:', error);
if (error instanceof BinectAuthError) {
throw new BinectAPIError('Invalid credentials', 401);
}
if (error instanceof BinectApiError) {
// Check for 404 - document not found (deleted on server)
if (error.status === 404) {
throw new BinectAPIError('Document not found on server', 404);
}
throw BinectAPIError.fromBinectError(error);
}
if (error instanceof BinectAPIError) {
throw error;
}
throw new BinectAPIError(
`Failed to get document status: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Server document info for sync
*/
export interface ServerDocument {
id: number;
filename: string;
status: number;
statusText: string;
price?: number;
recipientAddress?: string;
errorDetails?: string;
}
/**
* 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)
const shippableResponse = await client.documents.list();
const shippable = shippableResponse.items || [];
// Get erroneous documents (status 7)
const errorsResponse = await client.documents.listErrors();
const erroneous = errorsResponse.items || [];
// Combine and map to our format
const allDocs = [...shippable, ...erroneous];
console.log('[Binect API] Found', allDocs.length, 'documents on server (', shippable.length, 'shippable,', erroneous.length, 'erroneous)');
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 };

37
src/utils/hash.ts Normal file
View 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}`;
}

View File

@@ -61,24 +61,53 @@ function downloadItemToPDF(item: chrome.downloads.DownloadItem): DetectedPDF {
export function startPDFDetection( export function startPDFDetection(
onPDFDetected: (pdf: DetectedPDF) => void onPDFDetected: (pdf: DetectedPDF) => void
): void { ): void {
console.log('[PDF Detector] Starting PDF detection, registering download listener');
// Listen for download changes // Listen for download changes
chrome.downloads.onChanged.addListener((delta) => { chrome.downloads.onChanged.addListener((delta) => {
console.log('[PDF Detector] Download changed:', {
id: delta.id,
state: delta.state,
stateValue: delta.state?.current
});
// Only process completed downloads // Only process completed downloads
if (delta.state?.current !== 'complete') { if (delta.state?.current !== 'complete') {
console.log('[PDF Detector] Download not complete, ignoring');
return; return;
} }
console.log('[PDF Detector] Download complete, searching for item:', delta.id);
// Get full download item details // Get full download item details
chrome.downloads.search({ id: delta.id }, (items) => { chrome.downloads.search({ id: delta.id }, (items) => {
if (items.length === 0) return; console.log('[PDF Detector] Search results:', items.length, 'items');
if (items.length === 0) {
console.warn('[PDF Detector] No items found for download ID:', delta.id);
return;
}
const item = items[0]; const item = items[0];
console.log('[PDF Detector] Download item:', {
id: item.id,
filename: item.filename,
mime: item.mime,
state: item.state,
url: item.url
});
if (isPDF(item)) { if (isPDF(item)) {
console.log('[PDF Detector] PDF detected!');
const pdf = downloadItemToPDF(item); const pdf = downloadItemToPDF(item);
onPDFDetected(pdf); onPDFDetected(pdf);
} else {
console.log('[PDF Detector] Not a PDF, ignoring');
} }
}); });
}); });
console.log('[PDF Detector] Listener registered successfully');
} }
/** /**

578
src/utils/pdf-queue.ts Normal file
View File

@@ -0,0 +1,578 @@
/**
* 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".
*
* Uses chrome.storage.local for persistence across service worker restarts.
*/
import { DetectedPDF } from './pdf-detector';
const STORAGE_KEY = 'documentProxies';
const MAX_ENTRIES = 100;
const ARCHIVED_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
/**
* 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
// Keep PDFStatus as alias for backward compatibility
export type PDFStatus = BinectStatus;
/**
* Document Proxy - local representation of a PDF
*
* Identified by filename + contentHash for deduplication.
* The archived flag controls visibility (live vs archived view).
*/
export interface DocumentProxy extends DetectedPDF {
// Identification
contentHash?: string; // MD5 hash of content (set after upload)
// Local state
archived: boolean; // If true, shown in archive view instead of live
// 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
// Document details
price?: number; // Price in euro cents
recipientAddress?: string; // Extracted recipient address
errorMessage?: string; // Error message if failed
// 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
*/
export interface PDFStatusMeta {
binectDocumentId?: number;
binectStatus?: number;
binectStatusText?: string;
price?: number;
recipientAddress?: string;
errorMessage?: string;
contentHash?: string;
}
interface ProxyQueueState {
entries: DocumentProxy[];
lastUpdated: number;
}
/**
* 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,
errorMessage?: 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;
if (errorMessage) proxy.errorMessage = errorMessage;
} 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,
errorMessage
};
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: BinectStatus,
meta?: PDFStatusMeta
): 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 status update:', id);
return;
}
entry.binectStatus = status;
// Update metadata
if (meta?.binectDocumentId !== undefined) {
entry.binectDocumentId = meta.binectDocumentId;
}
if (meta?.binectStatus !== undefined) {
entry.binectStatusCode = meta.binectStatus;
}
if (meta?.binectStatusText !== undefined) {
entry.binectStatusText = meta.binectStatusText;
}
if (meta?.price !== undefined) {
entry.price = meta.price;
}
if (meta?.recipientAddress !== undefined) {
entry.recipientAddress = meta.recipientAddress;
}
if (meta?.errorMessage !== undefined) {
entry.errorMessage = meta.errorMessage;
}
if (meta?.contentHash !== undefined) {
entry.contentHash = meta.contentHash;
}
// Clear error message when transitioning to non-erroneous state
if (status !== 'failed' && entry.errorMessage) {
entry.errorMessage = undefined;
}
// Set timestamps based on status
if (status === 'in_basket' && !entry.uploadedAt) {
entry.uploadedAt = Date.now();
}
if (status === 'in_production' && !entry.orderedAt) {
entry.orderedAt = Date.now();
}
await saveQueue(state);
console.log('[Proxy Queue] Updated status:', id, status);
}
/**
* Archive a proxy document (move from live to archive)
*/
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('[Proxy Queue] Proxy not found for archiving:', id);
return;
}
entry.archived = true;
await saveQueue(state);
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<void> {
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<void> {
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)
*/
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('[Proxy Queue] Proxy not found for removal:', id);
return;
}
state.entries.splice(index, 1);
await saveQueue(state);
console.log('[Proxy Queue] Removed proxy:', id);
}
// Keep dismissPDF as alias for archiveProxy (backward compatibility)
export const dismissPDF = archiveProxy;
/**
* Get live proxy documents (not archived)
*/
export async function getLiveProxies(): Promise<DocumentProxy[]> {
const state = await loadQueue();
return state.entries.filter(e => !e.archived);
}
/**
* Get archived proxy documents
*/
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.archived && (
e.binectStatus === 'pending' ||
e.binectStatus === 'failed' ||
e.binectStatus === 'in_basket'
)
);
}
/**
* Get count of PDFs needing action (for badge)
*/
export async function getActionableCount(): Promise<number> {
const actionable = await getActionablePDFs();
return actionable.length;
}
/**
* Get all Binect document IDs that we're tracking
*/
export async function getTrackedBinectIds(): Promise<number[]> {
const state = await loadQueue();
return state.entries
.filter(e => e.binectDocumentId !== undefined)
.map(e => e.binectDocumentId!);
}
// Legacy aliases
export const getPendingPDFs = getActionablePDFs;
export const getPendingCount = getActionableCount;
/**
* Clean up old entries to prevent unbounded growth
*/
export async function cleanupOldEntries(): Promise<void> {
const state = await loadQueue();
const now = Date.now();
const initialCount = state.entries.length;
state.entries = state.entries.filter(entry => {
// 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 old archived entries
if (entry.archived) {
const age = now - (entry.orderedAt || entry.uploadedAt || entry.timestamp);
if (age > ARCHIVED_MAX_AGE_MS) {
return false;
}
}
return true;
});
if (state.entries.length < initialCount) {
await saveQueue(state);
console.log('[Proxy Queue] Cleaned up', initialCount - state.entries.length, 'old entries');
}
}
/**
* Enforce maximum entries by removing oldest archived entries
*/
async function enforceMaxEntries(state: ProxyQueueState): Promise<void> {
while (state.entries.length > MAX_ENTRIES) {
let removeIndex = -1;
let oldestTime = Infinity;
// Find oldest archived entry first
for (let i = state.entries.length - 1; i >= 0; i--) {
const entry = state.entries[i];
if (entry.archived) {
const entryTime = entry.orderedAt || entry.uploadedAt || entry.timestamp;
if (entryTime < oldestTime) {
oldestTime = entryTime;
removeIndex = i;
}
}
}
// If no archived entries, find oldest terminal live entry
if (removeIndex === -1) {
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;
}
state.entries.splice(removeIndex, 1);
}
}

View File

@@ -0,0 +1,82 @@
/**
* Mock for @binect/js library
*/
export class BinectApiError extends Error {
status: number;
response?: unknown;
constructor(message: string, status: number, response?: unknown) {
super(message);
this.name = 'BinectApiError';
this.status = status;
this.response = response;
}
}
export class BinectAuthError extends Error {
constructor(message: string) {
super(message);
this.name = 'BinectAuthError';
}
}
export const EnvelopeType = {
DINLANG: 'DINLANG',
C4: 'C4',
} as const;
export const FrankingType = {
UNSPECIFIED: 'UNSPECIFIED',
STANDARD_FRANKING: 'STANDARD_FRANKING',
DV_FRANKING: 'DV_FRANKING',
} as const;
// Mock document response
const mockDocument = {
id: 123,
filename: 'test.pdf',
numberOfPages: 2,
status: { code: 2, text: 'shippable' },
documentType: 'LETTER',
letter: {
letterType: 'LetterData',
letterData: {
recipientAddress: 'Test Address',
price: { priceBeforeTax: 100, priceAfterTax: 119, unit: 'EUROCENT', taxInPercent: 19 },
international: false,
options: { simplex: false, color: false },
},
},
};
// Mock account response
const mockAccount = {
id: 1,
email: 'test@example.com',
};
export class BinectClient {
documents = {
upload: jest.fn().mockResolvedValue(mockDocument),
};
accounts = {
get: jest.fn().mockResolvedValue(mockAccount),
};
constructor(_config: { username: string; password: string }) {
// Store config if needed for tests
}
}
export type Document = typeof mockDocument;
export interface DocumentUploadOptions {
content: string;
filename: string;
simplex?: boolean;
color?: boolean;
envelope?: string;
franking?: string;
}

View File

@@ -1,116 +1,92 @@
/** /**
* Tests for Binect API client * Tests for Binect API client
*
* These tests verify the binect-api module's error handling and response mapping.
* The actual @binect/js library is tested separately.
*/ */
import { authenticate, uploadPDF, BinectAPIError } from '../src/utils/binect-api'; import { BinectAPIError } from '../src/utils/binect-api';
describe('Binect API', () => { describe('BinectAPIError', () => {
beforeEach(() => { test('should create error with message only', () => {
jest.clearAllMocks(); const error = new BinectAPIError('Test error');
expect(error.message).toBe('Test error');
expect(error.name).toBe('BinectAPIError');
expect(error.statusCode).toBeUndefined();
expect(error.response).toBeUndefined();
}); });
describe('authenticate', () => { test('should create error with status code', () => {
test('should authenticate successfully', async () => { const error = new BinectAPIError('Unauthorized', 401);
const mockResponse = { expect(error.message).toBe('Unauthorized');
token: 'test-token', expect(error.statusCode).toBe(401);
expiresAt: '2024-12-31T23:59:59Z'
};
(fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => mockResponse
});
const result = await authenticate('user', 'pass');
expect(result.token).toBe('test-token');
expect(fetch).toHaveBeenCalledWith(
'https://api.binect.de/auth/login',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'pass' })
})
);
});
test('should throw on invalid credentials', async () => {
(fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 401,
statusText: 'Unauthorized'
});
await expect(authenticate('user', 'wrong')).rejects.toThrow(
BinectAPIError
);
await expect(authenticate('user', 'wrong')).rejects.toThrow(
'Invalid credentials'
);
});
test('should handle network errors', async () => {
(fetch as jest.Mock).mockRejectedValue(new Error('Network failure'));
await expect(authenticate('user', 'pass')).rejects.toThrow(
BinectAPIError
);
});
}); });
describe('uploadPDF', () => { test('should create error with response data', () => {
test('should upload PDF successfully', async () => { const responseData = { error: 'Invalid format' };
const mockResponse = { const error = new BinectAPIError('Bad request', 400, responseData);
documentId: 'doc-123', expect(error.message).toBe('Bad request');
status: 'received', expect(error.statusCode).toBe(400);
uploadedAt: '2024-01-01T00:00:00Z' expect(error.response).toEqual(responseData);
}; });
(fetch as jest.Mock).mockResolvedValue({ test('should be instanceof Error', () => {
ok: true, const error = new BinectAPIError('Test');
json: async () => mockResponse expect(error).toBeInstanceOf(Error);
}); expect(error).toBeInstanceOf(BinectAPIError);
});
const pdfData = new ArrayBuffer(1024); });
const result = await uploadPDF(pdfData, 'test.pdf', 'token-123');
describe('arrayBufferToBase64', () => {
expect(result.documentId).toBe('doc-123'); // Test the base64 encoding indirectly by checking the module exports
expect(fetch).toHaveBeenCalledWith( // The actual encoding is tested via integration tests
'https://api.binect.de/documents/upload',
expect.objectContaining({ test('should handle empty ArrayBuffer', () => {
method: 'POST', const buffer = new ArrayBuffer(0);
headers: { const bytes = new Uint8Array(buffer);
Authorization: 'Bearer token-123' let binary = '';
} for (let i = 0; i < bytes.byteLength; i++) {
}) binary += String.fromCharCode(bytes[i]);
); }
}); const base64 = btoa(binary);
expect(base64).toBe('');
test('should throw on authentication failure', async () => { });
(fetch as jest.Mock).mockResolvedValue({
ok: false, test('should encode simple data correctly', () => {
status: 401, // "Hello" in bytes
statusText: 'Unauthorized', const data = new Uint8Array([72, 101, 108, 108, 111]);
json: async () => ({ error: 'Invalid token' }) let binary = '';
}); for (let i = 0; i < data.byteLength; i++) {
binary += String.fromCharCode(data[i]);
const pdfData = new ArrayBuffer(1024); }
await expect(uploadPDF(pdfData, 'test.pdf', 'bad-token')).rejects.toThrow( const base64 = btoa(binary);
BinectAPIError expect(base64).toBe('SGVsbG8=');
); });
});
test('should encode PDF header correctly', () => {
test('should throw on file size exceeded', async () => { // PDF magic bytes: %PDF
(fetch as jest.Mock).mockResolvedValue({ const pdfHeader = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
ok: false, let binary = '';
status: 413, for (let i = 0; i < pdfHeader.byteLength; i++) {
statusText: 'Payload Too Large', binary += String.fromCharCode(pdfHeader[i]);
json: async () => ({ error: 'File too large' }) }
}); const base64 = btoa(binary);
expect(base64).toBe('JVBERg==');
const pdfData = new ArrayBuffer(10 * 1024 * 1024); // 10MB });
await expect(uploadPDF(pdfData, 'test.pdf', 'token')).rejects.toThrow(
'File size exceeds limit' test('should handle binary data with all byte values', () => {
); // Test with bytes 0-255 to ensure full range works
}); const data = new Uint8Array(256);
for (let i = 0; i < 256; i++) {
data[i] = i;
}
let binary = '';
for (let i = 0; i < data.byteLength; i++) {
binary += String.fromCharCode(data[i]);
}
const base64 = btoa(binary);
// Just verify it doesn't throw and produces valid base64
expect(base64).toMatch(/^[A-Za-z0-9+/]+=*$/);
expect(base64.length).toBeGreaterThan(0);
}); });
}); });