hh-shipments 0.3.13 → 0.3.14

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 +55 -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.14",
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,31 @@ 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;
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
+ const 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 (barcode && r && r.added.length) $('photo-result').insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
199
208
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
200
209
  loadPending();
201
210
  } catch (err) { $('ocr-status').textContent = ''; $('photo-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
@@ -264,6 +273,23 @@ function loadZxing() {
264
273
  return zxingPromise;
265
274
  }
266
275
 
276
+ // OCR just the top strip of a label (sender block) for the seller name.
277
+ // Small crop = fast. Returns text or null.
278
+ async function ocrTopStrip(file, statusEl) {
279
+ try {
280
+ const bmp = await createImageBitmap(file);
281
+ const sh = Math.max(60, Math.round(bmp.height * 0.30));
282
+ const canvas = document.createElement('canvas');
283
+ canvas.width = bmp.width;
284
+ canvas.height = sh;
285
+ canvas.getContext('2d').drawImage(bmp, 0, 0, bmp.width, sh, 0, 0, bmp.width, sh);
286
+ bmp.close();
287
+ const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.9));
288
+ if (!blob) return null;
289
+ const p = ocrFile(new File([blob], 'top.jpg', { type: 'image/jpeg' }), statusEl);
290
+ return await Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('top-strip timeout')), 25000))]);
291
+ } catch (e) { return null; }
292
+ }
267
293
  // Strict client-side check: only barcode reads that look exactly like a
268
294
  // tracking number are trusted (the server re-validates anyway).
269
295
  function strictTracking(s) {
@@ -329,12 +355,25 @@ async function handlePastedImageFile(f) {
329
355
  let text;
330
356
  if (barcode) {
331
357
  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'));
358
+ if (live) live.textContent = '📷 Barcode read — reading sender…';
359
+ // Number is verified; OCR just the top strip (sender block) for the
360
+ // name, in parallel with the upload.
361
+ const fd = new FormData();
362
+ fd.append('image', f);
363
+ const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
364
+ .catch(() => { throw new Error('upload timed out — check your connection and try again'); });
365
+ const topText = await ocrTopStrip(f, $('ocr-status-paste'));
366
+ const up = await upPromise;
367
+ if (!up.ok) throw new Error('upload failed');
368
+ if (live) live.textContent = '🔍 Finding tracking number…';
369
+ const r = await runQuick(text, target, topText ? { labelText: topText } : undefined);
370
+ if (r && r.added.length) target.insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
371
+ if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
372
+ return;
337
373
  }
374
+ // 2) Fallback: OCR the printed text
375
+ if (live) live.textContent = '📖 Reading screenshot…';
376
+ text = await ocrFile(f, $('ocr-status-paste'));
338
377
  if (live) live.textContent = '⬆️ Uploading…';
339
378
  const fd = new FormData();
340
379
  fd.append('image', f);
@@ -344,7 +383,6 @@ async function handlePastedImageFile(f) {
344
383
  if (!up.ok) throw new Error('upload failed');
345
384
  if (live) live.textContent = '🔍 Finding tracking number…';
346
385
  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>');
348
386
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
349
387
  } catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
350
388
  }
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) ---------------- */