hh-shipments 0.3.6 → 0.3.8

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.6",
21
+ "version": "0.3.8",
22
22
  "bin": {
23
23
  "hh-shipments": "server.js"
24
24
  },
package/public/app.js CHANGED
@@ -111,7 +111,10 @@ async function loadDash() {
111
111
  async function runQuick(text, targetEl, preset) {
112
112
  targetEl.innerHTML = '<p class="muted">Extracting…</p>';
113
113
  try {
114
- const r = await api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device });
114
+ const r = await Promise.race([
115
+ api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device }),
116
+ new Promise((_, rej) => setTimeout(() => rej(new Error('extract timed out — check your connection and try again')), 45000)),
117
+ ]);
115
118
  let html = '';
116
119
  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>`; });
117
120
  r.duplicates.forEach((n) => { html += `<div class="review">⛔ <b>${esc(n)}</b> is already tracked — skipped.</div>`; });
@@ -169,12 +172,18 @@ async function ocrFile(file, statusEl) {
169
172
  }
170
173
 
171
174
  async function handlePhotoFile(f) {
175
+ const target = $('photo-result');
172
176
  try {
177
+ f = await shrinkImage(f);
178
+ const url = URL.createObjectURL(f);
179
+ target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted">📖 Reading screenshot…</p></div>`;
173
180
  const text = await ocrFile(f, $('ocr-status'));
174
181
  // Upload original for reference, then run the quick flow on OCR text
175
182
  const fd = new FormData();
176
183
  fd.append('image', f);
177
- const up = await fetch('/api/intake/image', { method: 'POST', body: fd });
184
+ let up;
185
+ try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
186
+ catch (e) { throw new Error('upload timed out — check your connection and try again'); }
178
187
  if (!up.ok) throw new Error('upload failed');
179
188
  const r = await runQuick(text, $('photo-result'));
180
189
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
@@ -203,25 +212,56 @@ function pastedImageFiles(e) {
203
212
  return files;
204
213
  }
205
214
 
206
- // Paste-text tab: pasting a screenshot runs OCR and extracts tracking numbers;
207
- // pasting plain text behaves normally
215
+ // Shrink huge phone photos before OCR/upload: much faster to read and send,
216
+ // and less exposed to a stalled connection. Falls back to the original file.
217
+ async function shrinkImage(file, maxDim = 1600) {
218
+ try {
219
+ const bmp = await createImageBitmap(file);
220
+ const scale = Math.min(1, maxDim / Math.max(bmp.width, bmp.height));
221
+ if (scale >= 1) { bmp.close(); return file; }
222
+ const canvas = document.createElement('canvas');
223
+ canvas.width = Math.round(bmp.width * scale);
224
+ canvas.height = Math.round(bmp.height * scale);
225
+ canvas.getContext('2d').drawImage(bmp, 0, 0, canvas.width, canvas.height);
226
+ bmp.close();
227
+ const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.85));
228
+ return blob ? new File([blob], 'label.jpg', { type: 'image/jpeg' }) : file;
229
+ } catch (e) { return file; }
230
+ }
231
+
232
+ async function fetchTimeout(url, opts = {}, ms = 60000) {
233
+ const c = new AbortController();
234
+ const t = setTimeout(() => c.abort(), ms);
235
+ try { return await fetch(url, { ...opts, signal: c.signal }); }
236
+ finally { clearTimeout(t); }
237
+ }
208
238
  async function handlePastedImageFile(f) {
239
+ const target = $('quick-result');
209
240
  try {
241
+ // Instant feedback: show a thumbnail + progress the moment the image lands
242
+ f = await shrinkImage(f);
243
+ const url = URL.createObjectURL(f);
244
+ target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted" id="ocr-live">📖 Reading screenshot…</p></div>`;
210
245
  const text = await ocrFile(f, $('ocr-status-paste'));
246
+ const live = $('ocr-live');
247
+ if (live) live.textContent = '⬆️ Uploading…';
211
248
  const fd = new FormData();
212
249
  fd.append('image', f);
213
- const up = await fetch('/api/intake/image', { method: 'POST', body: fd });
250
+ let up;
251
+ try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
252
+ catch (e) { throw new Error('upload timed out — check your connection and try again'); }
214
253
  if (!up.ok) throw new Error('upload failed');
215
- const r = await runQuick(text, $('quick-result'));
216
- if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('quick-result'), text);
217
- } catch (err) { $('ocr-status-paste').textContent = ''; $('quick-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
254
+ if (live) live.textContent = '🔍 Finding tracking number…';
255
+ const r = await runQuick(text, target);
256
+ if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
257
+ } catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
218
258
  }
219
259
 
220
260
  $('tab-paste').addEventListener('paste', async (e) => {
221
261
  const files = pastedImageFiles(e);
222
262
  if (!files.length) return; // text paste — let it land in the textarea
223
263
  e.preventDefault();
224
- $('paste-box').innerHTML = '📋 <span class="muted">or tap here, then paste a screenshot</span>';
264
+ $('paste-box').innerHTML = '📋 <span class="muted">or tap here and paste a screenshot</span>';
225
265
  for (const f of files) await handlePastedImageFile(f);
226
266
  });
227
267
 
@@ -383,7 +423,11 @@ let searchT;
383
423
  $('search').addEventListener('input', () => { clearTimeout(searchT); searchT = setTimeout(loadDash, 400); });
384
424
  let searchAT;
385
425
  $('search-arch').addEventListener('input', () => { clearTimeout(searchAT); searchAT = setTimeout(loadArchive, 400); });
386
- $('btn-quick').onclick = () => runQuick($('paste-text').value, $('quick-result'));
426
+ $('btn-quick').onclick = () => {
427
+ const t = $('paste-text').value.trim();
428
+ 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; }
429
+ runQuick(t, $('quick-result'));
430
+ };
387
431
  $('btn-manual').onclick = async () => {
388
432
  $('manual-err').textContent = '';
389
433
  try {
package/public/index.html CHANGED
@@ -61,11 +61,11 @@
61
61
  <button class="tab" data-tab="manual">Manual</button>
62
62
  </div>
63
63
  <div id="tab-paste" class="tabpanel">
64
- <textarea id="paste-text" rows="6" placeholder="Paste the seller's message here…"></textarea>
65
- <div id="paste-box" class="pastebox" contenteditable="true" spellcheck="false">📋 <span class="muted">or tap here, then paste a screenshot</span></div>
66
- <div class="row" style="margin-top:8px"><button id="btn-clipboard" class="secondary">📋 Grab screenshot from clipboard</button></div>
64
+ <textarea id="paste-text" rows="4" placeholder="Paste the seller's message here…"></textarea>
65
+ <div class="row" style="margin:8px 0"><button id="btn-clipboard" class="primary">📋 Paste screenshot — extracts automatically</button></div>
66
+ <div id="paste-box" class="pastebox" contenteditable="true" spellcheck="false">📋 <span class="muted">or tap here and paste a screenshot</span></div>
67
67
  <div id="ocr-status-paste" class="muted"></div>
68
- <button id="btn-quick" class="primary">Extract &amp; add</button>
68
+ <button id="btn-quick" class="secondary">Extract from text above</button>
69
69
  <div id="quick-result"></div>
70
70
  </div>
71
71
  <div id="tab-photo" class="tabpanel" hidden>