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.
- package/package.json +1 -1
- package/public/app.js +66 -17
- package/server.js +6 -4
package/package.json
CHANGED
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 —
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 —
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
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 (
|
|
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
|
-
|
|
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) ---------------- */
|