hh-shipments 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +66 -17
  3. package/server.js +6 -4
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "test": "node test/run-tests.js && node test/run-carriers-tests.js"
19
19
  },
20
20
  "type": "commonjs",
21
- "version": "0.3.13",
21
+ "version": "0.3.15",
22
22
  "bin": {
23
23
  "hh-shipments": "server.js"
24
24
  },
package/public/app.js CHANGED
@@ -113,7 +113,7 @@ async function runQuick(text, targetEl, preset) {
113
113
  targetEl.innerHTML = '<p class="muted">Extracting…</p>';
114
114
  try {
115
115
  const r = await Promise.race([
116
- api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device }),
116
+ api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device, label_text: preset && preset.labelText }),
117
117
  new Promise((_, rej) => setTimeout(() => rej(new Error('extract timed out — check your connection and try again')), 45000)),
118
118
  ]);
119
119
  let html = '';
@@ -180,22 +180,32 @@ async function handlePhotoFile(f) {
180
180
  target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted" id="ocr-live-photo">📷 Scanning barcode…</p></div>`;
181
181
  const live = $('ocr-live-photo');
182
182
  const barcode = await decodeBarcode(f);
183
- let text;
183
+ let text, preset, topText = null;
184
184
  if (barcode) {
185
185
  text = barcode;
186
- if (live) live.textContent = '📷 Barcode read — verifying…';
186
+ if (live) live.textContent = '📷 Barcode read — reading sender…';
187
+ const fd = new FormData();
188
+ fd.append('image', f);
189
+ const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
190
+ .catch(() => { throw new Error('upload timed out — check your connection and try again'); });
191
+ topText = await ocrTopStrip(f, $('ocr-status'));
192
+ const up = await upPromise;
193
+ if (!up.ok) throw new Error('upload failed');
194
+ preset = topText ? { labelText: topText } : undefined;
187
195
  } else {
188
196
  if (live) live.textContent = '📖 Reading screenshot…';
189
197
  text = await ocrFile(f, $('ocr-status'));
198
+ // Upload original for reference, then run the quick flow on OCR text
199
+ const fd = new FormData();
200
+ fd.append('image', f);
201
+ let up;
202
+ try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
203
+ catch (e) { throw new Error('upload timed out — check your connection and try again'); }
204
+ if (!up.ok) throw new Error('upload failed');
190
205
  }
191
- // Upload original for reference, then run the quick flow on OCR text
192
- const fd = new FormData();
193
- fd.append('image', f);
194
- let up;
195
- try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
196
- catch (e) { throw new Error('upload timed out — check your connection and try again'); }
197
- if (!up.ok) throw new Error('upload failed');
198
- const r = await runQuick(text, $('photo-result'));
206
+ const r = await runQuick(text, $('photo-result'), preset);
207
+ if (looksLikeAppScreenshot(topText || text)) $('photo-result').insertAdjacentHTML('afterbegin', '<p class="warn">⚠️ This looks like a screenshot of the app itself — choose the label photo directly for best results.</p>');
208
+ if (barcode && r && r.added.length) $('photo-result').insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
199
209
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
200
210
  loadPending();
201
211
  } catch (err) { $('ocr-status').textContent = ''; $('photo-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
@@ -264,6 +274,23 @@ function loadZxing() {
264
274
  return zxingPromise;
265
275
  }
266
276
 
277
+ // OCR just the top strip of a label (sender block) for the seller name.
278
+ // Small crop = fast. Returns text or null.
279
+ async function ocrTopStrip(file, statusEl) {
280
+ try {
281
+ const bmp = await createImageBitmap(file);
282
+ const sh = Math.max(60, Math.round(bmp.height * 0.30));
283
+ const canvas = document.createElement('canvas');
284
+ canvas.width = bmp.width;
285
+ canvas.height = sh;
286
+ canvas.getContext('2d').drawImage(bmp, 0, 0, bmp.width, sh, 0, 0, bmp.width, sh);
287
+ bmp.close();
288
+ const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.9));
289
+ if (!blob) return null;
290
+ const p = ocrFile(new File([blob], 'top.jpg', { type: 'image/jpeg' }), statusEl);
291
+ return await Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('top-strip timeout')), 25000))]);
292
+ } catch (e) { return null; }
293
+ }
267
294
  // Strict client-side check: only barcode reads that look exactly like a
268
295
  // tracking number are trusted (the server re-validates anyway).
269
296
  function strictTracking(s) {
@@ -316,6 +343,14 @@ async function decodeBarcode(file) {
316
343
  } finally { bmp.close(); }
317
344
  return null;
318
345
  }
346
+ // Detect when the pasted image is a screenshot OF THE APP itself (its own
347
+ // header/buttons/tab bar get OCR'd) — the label inside is too small to read
348
+ // well, and UI words pollute the results.
349
+ function looksLikeAppScreenshot(text) {
350
+ const t = String(text || '').toLowerCase();
351
+ return /paste a screenshot|extract from text|finding tracking number|reading screenshot|mark received|open carrier tracking|hh shipments/.test(t);
352
+ }
353
+
319
354
  async function handlePastedImageFile(f) {
320
355
  const target = $('quick-result');
321
356
  try {
@@ -329,12 +364,26 @@ async function handlePastedImageFile(f) {
329
364
  let text;
330
365
  if (barcode) {
331
366
  text = barcode;
332
- if (live) live.textContent = '📷 Barcode read — verifying…';
333
- } else {
334
- // 2) Fallback: OCR the printed text
335
- if (live) live.textContent = '📖 Reading screenshot…';
336
- text = await ocrFile(f, $('ocr-status-paste'));
367
+ if (live) live.textContent = '📷 Barcode read — reading sender…';
368
+ // Number is verified; OCR just the top strip (sender block) for the
369
+ // name, in parallel with the upload.
370
+ const fd = new FormData();
371
+ fd.append('image', f);
372
+ const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
373
+ .catch(() => { throw new Error('upload timed out — check your connection and try again'); });
374
+ const topText = await ocrTopStrip(f, $('ocr-status-paste'));
375
+ const up = await upPromise;
376
+ if (!up.ok) throw new Error('upload failed');
377
+ if (live) live.textContent = '🔍 Finding tracking number…';
378
+ const r = await runQuick(text, target, topText ? { labelText: topText } : undefined);
379
+ if (looksLikeAppScreenshot(topText)) target.insertAdjacentHTML('afterbegin', '<p class="warn">⚠️ This looks like a screenshot of the app itself — paste the label photo directly for best results.</p>');
380
+ if (r && r.added.length) target.insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
381
+ if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
382
+ return;
337
383
  }
384
+ // 2) Fallback: OCR the printed text
385
+ if (live) live.textContent = '📖 Reading screenshot…';
386
+ text = await ocrFile(f, $('ocr-status-paste'));
338
387
  if (live) live.textContent = '⬆️ Uploading…';
339
388
  const fd = new FormData();
340
389
  fd.append('image', f);
@@ -344,7 +393,7 @@ async function handlePastedImageFile(f) {
344
393
  if (!up.ok) throw new Error('upload failed');
345
394
  if (live) live.textContent = '🔍 Finding tracking number…';
346
395
  const r = await runQuick(text, target);
347
- if (barcode && r && r.added.length) target.insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
396
+ if (looksLikeAppScreenshot(text)) target.insertAdjacentHTML('afterbegin', '<p class="warn">⚠️ This looks like a screenshot of the app itself — paste the label photo directly for best results.</p>');
348
397
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
349
398
  } catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
350
399
  }
package/server.js CHANGED
@@ -218,9 +218,11 @@ app.post('/api/intake/text', requireAuth, (req, res) => {
218
218
  // high-confidence ones, return the rest for review. Used by paste intake
219
219
  // and the WhatsApp webhook.
220
220
  function quickAddText(text, opts = {}) {
221
- const { seller_name, device, notes, source = 'paste' } = opts;
221
+ const { seller_name, device, notes, source = 'paste', label_text } = opts;
222
222
  const candidates = extract.extractTrackingNumbers(text);
223
- const autoCtx = extract.extractContext(text);
223
+ // When the number came from a barcode scan, text is just the number —
224
+ // use the label's top-strip OCR text (if provided) for seller context.
225
+ const autoCtx = extract.extractContext(label_text || text);
224
226
  const existing = new Set(db.prepare('SELECT tracking_number FROM shipments').all().map((r) => r.tracking_number));
225
227
  const added = [], needs_review = [], duplicates = [];
226
228
  for (const c of candidates) {
@@ -238,9 +240,9 @@ function quickAddText(text, opts = {}) {
238
240
  }
239
241
 
240
242
  app.post('/api/intake/quick', requireAuth, (req, res) => {
241
- const { text, seller_name, device, notes } = req.body || {};
243
+ const { text, seller_name, device, notes, label_text } = req.body || {};
242
244
  if (!text || !text.trim()) return res.status(400).json({ error: 'empty text' });
243
- res.json(quickAddText(text, { seller_name, device, notes, source: 'paste' }));
245
+ res.json(quickAddText(text, { seller_name, device, notes, source: 'paste', label_text }));
244
246
  });
245
247
 
246
248
  /* ---------------- intake: images (in-app upload + iOS Shortcut) ---------------- */