hh-shipments 0.3.17 → 0.3.19

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.17",
21
+ "version": "0.3.19",
22
22
  "bin": {
23
23
  "hh-shipments": "server.js"
24
24
  },
package/public/app.js CHANGED
@@ -56,7 +56,10 @@ function showApp() {
56
56
  $('tabbar').hidden = false;
57
57
  showView('view-dash');
58
58
  loadDash();
59
- api.get('/api/version').then((v) => { if (v && v.version) $('app-version').textContent = v.version; }).catch(() => {});
59
+ api.get('/api/version').then((v) => {
60
+ const cv = (document.querySelector('meta[name="app-version"]') || {}).content || '?';
61
+ if (v && v.version) $('app-version').textContent = 'app v' + cv + ' · server v' + v.version;
62
+ }).catch(() => {});
60
63
  }
61
64
 
62
65
  /* ---------- auth ---------- */
@@ -119,6 +122,7 @@ async function runQuick(text, targetEl, preset) {
119
122
  let html = '';
120
123
  r.added.forEach((a) => { html += `<div class="review">✅ Added <b>${esc(a.number)}</b> (${esc(a.carrier.toUpperCase())})${r.simulated ? ' <span class="sim-badge">SIMULATED</span>' : ''}</div>`; });
121
124
  r.duplicates.forEach((n) => { html += `<div class="review">⛔ <b>${esc(n)}</b> is already tracked — skipped.</div>`; });
125
+ (r.updated || []).forEach((u) => { html += `<div class="review">🔄 Updated <b>${esc(u.number)}</b> — seller: <b>${esc(u.seller_name || '—')}</b>${u.device ? ` · ${esc(u.device)}` : ''}</div>`; });
122
126
  r.needs_review.forEach((c, i) => {
123
127
  html += `<div class="review" id="rev-${i}"><span class="warn">Needs review</span> — <b>${esc(c.number)}</b> (${esc(c.carrier.toUpperCase())}, ${esc(c.confidence)} confidence)${c.error ? ' — ' + esc(c.error) : ''}
124
128
  <div class="row"><button class="secondary" data-add="${i}">Add anyway</button></div></div>`;
@@ -126,7 +130,7 @@ async function runQuick(text, targetEl, preset) {
126
130
  if (r.context && (r.context.seller_name || r.context.device)) {
127
131
  html = `<div class="review">📝 Detected: ${esc(r.context.seller_name || '—')} · ${esc(r.context.device || '—')}</div>` + html;
128
132
  }
129
- if (!r.added.length && !r.needs_review.length && !r.duplicates.length) html = '<div class="review">No tracking numbers found in that text.</div>';
133
+ if (!r.added.length && !r.needs_review.length && !r.duplicates.length && !(r.updated || []).length) html = '<div class="review">No tracking numbers found in that text.</div>';
130
134
  targetEl.innerHTML = html;
131
135
  targetEl.querySelectorAll('[data-add]').forEach((btn) => {
132
136
  btn.onclick = async () => {
package/public/index.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
6
  <meta name="theme-color" content="#0a84ff">
7
+ <meta name="app-version" content="0.3.19">
7
8
  <title>HH Shipments</title>
8
9
  <link rel="manifest" href="/manifest.webmanifest">
9
10
  <link rel="apple-touch-icon" href="/icon-192.png">
package/server.js CHANGED
@@ -225,9 +225,25 @@ function quickAddText(text, opts = {}) {
225
225
  // Typed text (paste box) gets the lenient name rule; OCR never does.
226
226
  const autoCtx = extract.extractContext(label_text || text, { lenient: !!typed && !label_text });
227
227
  const existing = new Set(db.prepare('SELECT tracking_number FROM shipments').all().map((r) => r.tracking_number));
228
- const added = [], needs_review = [], duplicates = [];
228
+ const added = [], needs_review = [], duplicates = [], updated = [];
229
229
  for (const c of candidates) {
230
- if (existing.has(c.number)) { duplicates.push(c.number); continue; }
230
+ if (existing.has(c.number)) {
231
+ // Already tracked — but a re-paste carrying a name (Hunter's
232
+ // "number + name" workflow) fills a blank seller/device on it.
233
+ const newSeller = seller_name || autoCtx.seller_name;
234
+ const newDevice = device || autoCtx.device;
235
+ if (newSeller || newDevice) {
236
+ const cur = db.prepare('SELECT id, seller_name, device FROM shipments WHERE tracking_number = ?').get(c.number);
237
+ if (cur && ((!cur.seller_name && newSeller) || (!cur.device && newDevice))) {
238
+ db.prepare('UPDATE shipments SET seller_name = COALESCE(seller_name, ?), device = COALESCE(device, ?) WHERE id = ?')
239
+ .run(newSeller || null, newDevice || null, cur.id);
240
+ updated.push({ id: cur.id, number: c.number, seller_name: newSeller || cur.seller_name, device: newDevice || cur.device });
241
+ continue;
242
+ }
243
+ }
244
+ duplicates.push(c.number);
245
+ continue;
246
+ }
231
247
  if (c.confidence === 'high') {
232
248
  const r = addShipment({ number: c.number, carrier: c.carrier, seller_name: seller_name || autoCtx.seller_name, device: device || autoCtx.device, notes, source });
233
249
  if (r.error) needs_review.push({ ...c, error: r.error });
@@ -237,7 +253,7 @@ function quickAddText(text, opts = {}) {
237
253
  needs_review.push(c);
238
254
  }
239
255
  }
240
- return { added, needs_review, duplicates, context: autoCtx, simulated: provider.simulated };
256
+ return { added, needs_review, duplicates, updated, context: autoCtx, simulated: provider.simulated };
241
257
  }
242
258
 
243
259
  app.post('/api/intake/quick', requireAuth, (req, res) => {
package/src/extract.js CHANGED
@@ -89,6 +89,29 @@
89
89
  if (looksLikeLabelName(cleaned)) { out.seller_name = cleaned; break; }
90
90
  }
91
91
  }
92
+ // Chat screenshots (Messenger): the other person's name is the header at
93
+ // the very top ("Abdullah Sohail"), above the message bubbles. Take the
94
+ // first title-case name in the top lines — never Hunter's own name /
95
+ // address bubble, never app or chat UI words.
96
+ if (!out.seller_name) {
97
+ var cheadBlock = /\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|HUNTER|HILLS|THANK|THANKS|YOU|DELIVERED|SEEN|TYPING|AFTERMARKET|DISPLAY)\b/i;
98
+ var clines = String(text).split(/\r?\n/);
99
+ var checked = 0;
100
+ for (var ci = 0; ci < clines.length && checked < 6; ci++) {
101
+ var ch = clines[ci].trim();
102
+ if (!ch || ch.length > 40) continue;
103
+ checked++;
104
+ if (/\d/.test(ch)) continue;
105
+ var cw = ch.split(/\s+/);
106
+ if (cw.length < 2 || cw.length > 3) continue;
107
+ var cok = true;
108
+ for (var cj = 0; cj < cw.length; cj++) if (!/^[A-Z][a-z]{2,}$/.test(cw[cj])) { cok = false; break; }
109
+ if (!cok) continue;
110
+ if (cheadBlock.test(ch)) continue;
111
+ out.seller_name = ch;
112
+ break;
113
+ }
114
+ }
92
115
  // Hunter's typed format: tracking number on one line, seller name on
93
116
  // another ("1Z...\nAbdullah S"). Typed text is trusted — take the first
94
117
  // plausible name line that isn't the number itself.