From 6c54912cfca26acf25365eca1341a27a7bc223c9 Mon Sep 17 00:00:00 2001 From: Shawn Date: Mon, 25 May 2026 20:55:07 -0400 Subject: [PATCH] feat: inline machine ID editing in Previous Photos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tap any machine ID in the Previous Photos list to edit it inline. - Replaces static text with editable input on click/tap - Enter/blur saves via /api/assign-machine-id - Esc restores original value - Shows ✏️ indicator on hover - Saves new ID to DB, refreshes asset info and badge instantly --- static/index.html | 103 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index dd6dc5f..4a0d5d7 100644 --- a/static/index.html +++ b/static/index.html @@ -278,6 +278,21 @@ if ('serviceWorker' in navigator) { border-radius: var(--radius-sm); margin-bottom: 8px; cursor: zoom-in; display: block; background: var(--card2); } + /* Inline machine ID editing in Previous Photos */ + .prev-mid-editable { + cursor: text; border-bottom: 1px dashed var(--text3); + transition: border-color 0.15s, color 0.15s; + padding: 0 2px; + } + .prev-mid-editable:hover { border-color: var(--accent); color: var(--text); } + .prev-mid-editable::after { content: ' ✏️'; font-size: 9px; opacity: 0.5; } + .prev-mid-editable:hover::after { opacity: 1; } + .prev-mid-input { + background: var(--card2); color: var(--text); border: 1px solid var(--accent); + border-radius: 6px; padding: 2px 6px; font-size: 11px; font-weight: 700; + width: 100px; outline: none; + } + .prev-mid-input:focus { border-color: var(--accent2); box-shadow: 0 0 0 2px rgba(59,130,246,0.3); } .export-modal-backdrop { position: fixed; inset: 0; z-index: 999; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; } .export-modal { background: var(--card); border-radius: var(--radius); padding: 20px; max-width: 320px; width: 90%; text-align: center; } @@ -568,7 +583,7 @@ function renderPrevCard(p) { ${time}
- Machine: ${p.machine_id ? esc(p.machine_id) : '—'} + Machine: ${esc(p.machine_id || '—')} ${esc(matchText)} ${esc(engine)}${p.ocr_model ? ' ' + esc(p.ocr_model) : ''} ${colorHtml} @@ -603,6 +618,92 @@ function renderPrevCard(p) {
`; } +/* ── Inline machine ID editing in Previous Photos ── */ +function editMachineIdInline(photoId) { + const span = document.getElementById('midSpan' + photoId); + if (!span) return; + const currentVal = span.dataset.mid || ''; + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'prev-mid-input'; + input.value = currentVal; + input.placeholder = 'Enter machine ID'; + input.dataset.photoId = photoId; + input.dataset.original = currentVal; + span.replaceWith(input); + input.focus(); + input.select(); + input.addEventListener('blur', () => saveMachineIdInline(input, photoId)); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') input.blur(); + if (e.key === 'Escape') { saveMachineIdInline(input, photoId, true); } + }); +} + +function saveMachineIdInline(input, photoId, cancel) { + const orig = input.dataset.original || ''; + const val = cancel ? orig : input.value.trim(); + // Restore span first so UI is consistent + function restoreSpan(mid, label) { + const span = document.createElement('span'); + span.id = 'midSpan' + photoId; + span.className = 'prev-mid-editable'; + span.dataset.mid = mid || ''; + span.textContent = label || mid || '—'; + span.onclick = () => editMachineIdInline(photoId); + input.replaceWith(span); + return span; + } + if (cancel || val === orig) { + restoreSpan(orig, orig || '—'); + return; + } + fetch('/api/assign-machine-id', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ photo_id: photoId, machine_id: val }) + }) + .then(r => r.json()) + .then(data => { + restoreSpan(val, val); + // Update the badge + const card = document.getElementById('midSpan' + photoId)?.closest('.prev-photo-card'); + if (card) { + const badge = card.querySelector('.match-badge'); + if (badge) { + badge.textContent = val; + badge.className = 'match-badge matched'; + } + } + // Update asset info + const assetEl = document.getElementById('prevAsset' + photoId); + const infoEl = document.getElementById('prevMachineInfo' + photoId); + if (data.asset) { + const a = data.asset; + if (assetEl) { + assetEl.innerHTML = '🆔 ' + esc(a.machine_id) + ' · ' + esc(a.name) + '' + + (a.category ? ' · 📦 ' + esc(a.category) : '') + + (a.status === 'active' ? ' 🟢 Active' : ''); + } + if (infoEl) { + const parts = [a.location_name, a.building, a.floor, a.zone].filter(Boolean); + infoEl.innerHTML = parts.join(' · ') || '(No location details)'; + } + } else { + if (assetEl) assetEl.innerHTML = '⚠️ No asset match for ' + esc(val) + ''; + } + }) + .catch(() => { + restoreSpan(orig, orig || '—'); + // Show error toast + const toast = document.createElement('div'); + toast.style.cssText = 'position:fixed;bottom:60px;left:50%;transform:translateX(-50%);background:var(--red);color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;z-index:9999;animation:fadeIn 0.2s;'; + toast.textContent = 'Failed to save machine ID'; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); + }); +} + async function resetPhoto(photoId) { if (!confirm('Reset this entry and re-process from scratch?')) return;