hh-shipments 0.3.7 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +37 -6
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.7",
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>`; });
@@ -171,13 +174,16 @@ async function ocrFile(file, statusEl) {
171
174
  async function handlePhotoFile(f) {
172
175
  const target = $('photo-result');
173
176
  try {
177
+ f = await shrinkImage(f);
174
178
  const url = URL.createObjectURL(f);
175
179
  target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted">📖 Reading screenshot…</p></div>`;
176
180
  const text = await ocrFile(f, $('ocr-status'));
177
181
  // Upload original for reference, then run the quick flow on OCR text
178
182
  const fd = new FormData();
179
183
  fd.append('image', f);
180
- 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'); }
181
187
  if (!up.ok) throw new Error('upload failed');
182
188
  const r = await runQuick(text, $('photo-result'));
183
189
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
@@ -206,21 +212,46 @@ function pastedImageFiles(e) {
206
212
  return files;
207
213
  }
208
214
 
209
- // Paste-text tab: pasting a screenshot runs OCR and extracts automatically —
210
- // no "Extract" tap needed. Plain text paste still lands in the textarea.
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
+ }
211
238
  async function handlePastedImageFile(f) {
212
239
  const target = $('quick-result');
213
240
  try {
214
241
  // Instant feedback: show a thumbnail + progress the moment the image lands
242
+ f = await shrinkImage(f);
215
243
  const url = URL.createObjectURL(f);
216
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>`;
217
245
  const text = await ocrFile(f, $('ocr-status-paste'));
218
246
  const live = $('ocr-live');
219
- if (live) live.textContent = '🔍 Finding tracking number…';
247
+ if (live) live.textContent = '⬆️ Uploading…';
220
248
  const fd = new FormData();
221
249
  fd.append('image', f);
222
- 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'); }
223
253
  if (!up.ok) throw new Error('upload failed');
254
+ if (live) live.textContent = '🔍 Finding tracking number…';
224
255
  const r = await runQuick(text, target);
225
256
  if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
226
257
  } catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }