# Tesseract.js Client-Side Debugging Pattern

## The Problem

`Tesseract.recognize()` in the browser has multiple silent phases before "recognizing text":

1. `loading tesseract core` — downloads WASM from CDN
2. `initializing tesseract` — sets up the engine
3. `loading language traineddata` — downloads `eng.traineddata` from CDN
4. `initializing api` — final setup
5. `recognizing text` — actual OCR (the only phase with progress %)

If any phase fails (CSP block, CDN timeout, worker crash), the promise **never resolves or rejects**.
The UI just shows "Running OCR..." forever.

## The Fix: Promise.race + Full Phase Logger

```javascript
// Show ALL phases, not just "recognizing text"
const ocrResult = await Promise.race([
    Tesseract.recognize(preprocessed, 'eng', {
        logger: m => {
            console.log(`[OCR] ${m.status} ${m.progress ? Math.round(m.progress*100)+'%' : ''}`);
            if (m.status === 'recognizing text') {
                progressText.textContent = `OCR: ${Math.round(m.progress*100)}%`;
            } else {
                progressText.textContent = `Tesseract: ${m.status}...`;
            }
        },
        tessedit_pageseg_mode: '4'
    }),
    new Promise((_, reject) =>
        setTimeout(() => reject(new Error('OCR timed out after 90s')), 90000)
    )
]);
```

**Why this works:**
- `Promise.race` breaks the hang — after 90s, it rejects with a clear error
- Full phase logging shows EXACTLY where it's stuck ("loading language traineddata..." = CDN issue)
- The console gets timestamped `[OCR]` logs for post-mortem debugging

## Performance: Adaptive Upscaling

3× upscale on HD screenshots creates massive canvases (5760×3240). Use adaptive scaling:

```javascript
// 2x for large images, 3x for small ones
const scale = (img.width * img.height > 1000000) ? 2 : 3;
canvas.width = img.width * scale;
canvas.height = img.height * scale;
```

This cuts pixel count by ~55% on typical screenshots while keeping 3× for small crops.

## Second OCR Pass (Date Extraction)

The date OCR pass uses PSM 11 (sparse text) and runs on the same preprocessed blob. Apply the same
pattern with a shorter timeout (30s):

```javascript
const dateOcrResult = await Promise.race([
    Tesseract.recognize(preprocessed, 'eng', {
        logger: m => { /* same pattern */ },
        tessedit_pageseg_mode: '11'
    }),
    new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Date OCR timed out')), 30000)
    )
]);
```

## Ollama Cold-Model 502 Retry

The VPS proxy may return 502 when the local model is unloaded (KEEP_ALIVE=5m). The model takes
~3-6s to load. Retry once after a 5s delay:

```javascript
for (let attempt = 0; attempt < 2; attempt++) {
    const resp = await fetch(endpoint, { ... });
    if (resp.ok) break;
    if (resp.status !== 502 && resp.status !== 503) throw new Error(...);
    await new Promise(r => setTimeout(r, 5000)); // wait for model load
}
```
