hh-shipments 0.3.16 → 0.3.17

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 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.16",
21
+ "version": "0.3.17",
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, label_text: preset && preset.labelText }),
116
+ api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device, label_text: preset && preset.labelText, typed: preset && preset.typed }),
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 = '';
@@ -573,7 +573,7 @@ $('search-arch').addEventListener('input', () => { clearTimeout(searchAT); searc
573
573
  $('btn-quick').onclick = () => {
574
574
  const t = $('paste-text').value.trim();
575
575
  if (!t) { $('quick-result').innerHTML = '<p class="muted">Paste the seller\'s text above first — or tap <b>📋 Paste screenshot</b>, screenshots extract on their own.</p>'; return; }
576
- runQuick(t, $('quick-result'));
576
+ runQuick(t, $('quick-result'), { typed: true });
577
577
  };
578
578
  $('btn-manual').onclick = async () => {
579
579
  $('manual-err').textContent = '';
package/server.js CHANGED
@@ -218,11 +218,12 @@ 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', label_text } = opts;
221
+ const { seller_name, device, notes, source = 'paste', label_text, typed } = opts;
222
222
  const candidates = extract.extractTrackingNumbers(text);
223
223
  // When the number came from a barcode scan, text is just the number —
224
224
  // use the label's top-strip OCR text (if provided) for seller context.
225
- const autoCtx = extract.extractContext(label_text || text);
225
+ // Typed text (paste box) gets the lenient name rule; OCR never does.
226
+ const autoCtx = extract.extractContext(label_text || text, { lenient: !!typed && !label_text });
226
227
  const existing = new Set(db.prepare('SELECT tracking_number FROM shipments').all().map((r) => r.tracking_number));
227
228
  const added = [], needs_review = [], duplicates = [];
228
229
  for (const c of candidates) {
@@ -240,9 +241,9 @@ function quickAddText(text, opts = {}) {
240
241
  }
241
242
 
242
243
  app.post('/api/intake/quick', requireAuth, (req, res) => {
243
- const { text, seller_name, device, notes, label_text } = req.body || {};
244
+ const { text, seller_name, device, notes, label_text, typed } = req.body || {};
244
245
  if (!text || !text.trim()) return res.status(400).json({ error: 'empty text' });
245
- res.json(quickAddText(text, { seller_name, device, notes, source: 'paste', label_text }));
246
+ res.json(quickAddText(text, { seller_name, device, notes, source: 'paste', label_text, typed: !!typed }));
246
247
  });
247
248
 
248
249
  /* ---------------- intake: images (in-app upload + iOS Shortcut) ---------------- */
package/src/extract.js CHANGED
@@ -38,8 +38,11 @@
38
38
  }
39
39
 
40
40
  // Light heuristics for seller name + device details from message text.
41
- function extractContext(text) {
41
+ // opts.lenient: the text was typed by Hunter himself (paste-text box) —
42
+ // trust a name-looking line far more than OCR output.
43
+ function extractContext(text, opts) {
42
44
  var out = { seller_name: null, device: null };
45
+ opts = opts || {};
43
46
  if (!text) return out;
44
47
  // A plausible person name: no digits (addresses have them), not ALL CAPS
45
48
  // (shipping-label blocks are), proper name shape. Shipping labels like
@@ -86,6 +89,25 @@
86
89
  if (looksLikeLabelName(cleaned)) { out.seller_name = cleaned; break; }
87
90
  }
88
91
  }
92
+ // Hunter's typed format: tracking number on one line, seller name on
93
+ // another ("1Z...\nAbdullah S"). Typed text is trusted — take the first
94
+ // plausible name line that isn't the number itself.
95
+ if (!out.seller_name && opts.lenient) {
96
+ var lblock = /\b(UPS|FEDEX|USPS|DHL|GROUND|EXPRESS|PRIORITY|BILLING|TRACKING|POSTAGE|LABEL|SHIPMENT|SHIP\b|SCREENSHOT|READING|FINDING|UPLOADING|EXTRACT|PASTE|DETAILS|SAVE|TAP|CLIPBOARD|NUMBER|BACK|HOME|ARCHIVE|SETTINGS)\b/i;
97
+ var lns = String(text).split(/\r?\n/);
98
+ for (var k = 0; k < lns.length; k++) {
99
+ var ln = lns[k].trim();
100
+ if (!ln || ln.length > 40) continue;
101
+ if (/\d{6,}/.test(ln)) continue; // tracking-number line
102
+ if (/\d/.test(ln)) continue; // addresses/order lines have digits
103
+ var w = ln.split(/\s+/);
104
+ if (w.length > 4) continue;
105
+ if (lblock.test(ln)) continue;
106
+ if (!/^[A-Za-z][A-Za-z .,'-]*$/.test(ln)) continue;
107
+ out.seller_name = ln;
108
+ break;
109
+ }
110
+ }
89
111
  // Device: iPhone 15 Pro Max 256GB, iPad Pro 11, etc.
90
112
  var dm = text.match(/\b(iPhone\s+\d{1,2}\s*(?:Pro\s*Max|Pro|Plus|Mini|e)?|iPad\s*(?:Pro|Air|Mini)?\s*\d*(?:\.\d)?)\b/i);
91
113
  var sm = text.match(/\b(64|128|256|512)\s?GB\b/i) || text.match(/\b1\s?TB\b/i);