Fix API key loading (strip whitespace, auto-load .env), add reset endpoint, improve maps + machine info display

- server.py now auto-loads .env file directly (robust key loading)
- Strip whitespace from all env var values
- Add POST /api/photos/{id}/reset endpoint to delete + allow re-upload
- Frontend: Reset button on prev-photo-cards + duplicate banners
- Improved mini-map rendering with better popups and zoom control
- Machine info panel shows building/floor/room/address/model details
- Added resetAndReupload() for in-place re-processing after reset
This commit is contained in:
2026-05-25 18:43:47 -04:00
parent f1707d9e6e
commit d617b7b9da
2 changed files with 217 additions and 21 deletions
+109 -6
View File
@@ -374,20 +374,78 @@ function loadPreviousPhotos() {
section.style.display = 'block';
document.getElementById('prevCount').textContent = data.photos.length;
div.innerHTML = data.photos.map(p => renderPrevCard(p)).join('');
// Init maps and asset lookups after rendering
data.photos.forEach(p => {
if (p.gps_lat && p.gps_lng && p.machine_id) {
initPrevMap(p);
lookupPrevAsset(p);
}
});
})
.catch(() => {});
}
function initPrevMap(p) {
const el = document.getElementById('prevMap' + p.id);
if (!el || typeof L === 'undefined') return;
// Clear any existing map
el.innerHTML = '';
el.style.background = 'var(--card2)';
try {
const map = L.map(el, { zoomControl: true }).setView([p.gps_lat, p.gps_lng], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OSM', maxZoom: 19
}).addTo(map);
const label = p.machine_id ? '<b>' + esc(p.machine_id) + '</b>' : '📸 Photo location';
L.marker([p.gps_lat, p.gps_lng])
.addTo(map)
.bindPopup(label + '<br>📍 ' + Number(p.gps_lat).toFixed(5) + ', ' + Number(p.gps_lng).toFixed(5));
setTimeout(() => { try { map.invalidateSize(); } catch(e) {} }, 400);
// Store map ref for later cleanup
if (!window._prevMaps) window._prevMaps = {};
window._prevMaps[p.id] = map;
} catch (e) { /* ignore map errors */ }
}
function lookupPrevAsset(p) {
const el = document.getElementById('prevAsset' + p.id);
const infoEl = document.getElementById('prevMachineInfo' + p.id);
if (!el) return;
fetch('/api/lookup?machine_id=' + encodeURIComponent(p.machine_id))
.then(r => r.json())
.then(data => {
if (data.found) {
const a = data.asset;
el.innerHTML = '🆔 ' + esc(a.machine_id) + ' · <strong>' + esc(a.name) + '</strong>' +
(a.category ? ' · 📦 ' + esc(a.category) : '') +
(a.status === 'active' ? ' 🟢 Active' : '');
if (infoEl) {
let info = '';
if (a.building_name) info += '🏢 ' + esc(a.building_name) + ' ';
if (a.floor) info += '📶 Floor ' + esc(a.floor) + ' ';
if (a.room) info += '🚪 ' + esc(a.room) + ' ';
if (a.address) info += '🏠 ' + esc(a.address);
if (a.make || a.model) info += '🔧 ' + esc(a.make || '') + ' ' + esc(a.model || '');
infoEl.innerHTML = info ? info : '(No location details)';
}
} else {
el.innerHTML = '⚠️ No asset match for <strong>' + esc(p.machine_id) + '</strong>';
}
})
.catch(() => { el.innerHTML = '⚠️ Lookup failed'; });
}
function renderPrevCard(p) {
const hasMatch = p.ocr_match_5dash6 ? 'matched' : 'no-match';
const matchText = p.ocr_match_5dash6 || (p.ocr_match_5plus ? 'digits found' : 'no match');
const hasMatch = p.machine_id ? 'matched' : 'no-match';
const matchText = p.machine_id || (p.ocr_match_5plus ? 'digits found' : 'no match');
const engine = p.ocr_engine || 'tesseract';
const engCls = engine === 'llm' || engine === 'llm_batch' ? 'llm' : 'tesseract';
const engCls = engine === 'llm' || engine === 'llm_batch' ? 'llm' : engine === 'google' ? 'llm' : 'tesseract';
const time = p.created_at ? new Date(p.created_at + 'Z').toLocaleString() : '';
const colorHtml = p.sticker_color
? `<span class="sticker-color" style="background:${p.sticker_color === 'green' ? 'var(--green)' : p.sticker_color === 'orange' ? 'var(--amber)' : p.sticker_color === 'yellow' ? '#eab308' : 'var(--text3)'}"></span>`
: '';
return `<div class="prev-photo-card">
const hasCoords = p.gps_lat && p.gps_lng;
return `<div class="prev-photo-card">
<div class="prev-header">
<span class="prev-filename">${esc(p.orig_filename)}</span>
<span class="prev-time">${time}</span>
@@ -397,6 +455,7 @@ function renderPrevCard(p) {
<span class="match-badge ${hasMatch}">${esc(matchText)}</span>
<span class="engine-badge ${engCls}">${esc(engine)}${p.ocr_model ? ' ' + esc(p.ocr_model) : ''}</span>
${colorHtml}
${hasCoords ? ` <span class="match-badge has-gps">📍 ${Number(p.gps_lat).toFixed(4)}, ${Number(p.gps_lng).toFixed(4)}</span>` : ''}
</div>
<div class="prev-actions">
<select id="reEng${p.id}">
@@ -414,10 +473,42 @@ function renderPrevCard(p) {
</select>
<label><input type="checkbox" id="reSticker${p.id}"> 🏷️</label>
<button class="btn btn-primary btn-xs" onclick="reprocessPhoto(${p.id})">🔄 Re-run</button>
<button class="btn btn-xs" style="background:transparent;border:1px solid var(--red);color:var(--red);" onclick="deletePhoto(${p.id})">🗑️</button>
<button class="btn btn-xs" style="background:transparent;border:1px solid var(--amber);color:var(--amber);" onclick="resetPhoto(${p.id})">🔄 Reset</button>
</div>
${hasCoords && p.machine_id ? `<div style="margin-top:6px;">
<div id="prevMap${p.id}" class="mini-map" style="height:120px;border-radius:6px;background:var(--card2);"></div>
<div id="prevAsset${p.id}" style="font-size:11px;margin-top:4px;color:var(--text2);"></div>
<div id="prevMachineInfo${p.id}" style="margin-top:2px;"></div>
</div>` : hasCoords ? `<div style="margin-top:6px;">
<div id="prevMap${p.id}" class="mini-map" style="height:100px;border-radius:6px;background:var(--card2);"></div>
</div>` : ''}
</div>`;
}
async function resetPhoto(photoId) {
if (!confirm('Reset this entry? The photo will be deleted from the database and can be re-uploaded as new.')) return;
try {
const resp = await fetch(`/api/photos/${photoId}/reset`, { method: 'POST' });
if (!resp.ok) throw new Error('Reset failed');
loadPreviousPhotos();
} catch (e) {
alert('Reset failed: ' + e.message);
}
}
async function resetAndReupload(photoId) {
if (!photoId) { alert('No photo ID available'); return; }
if (!confirm('Reset this entry and re-process as fresh?')) return;
try {
await fetch(`/api/photos/${photoId}/reset`, { method: 'POST' });
// Re-trigger the upload
uploadSelected();
} catch (e) {
alert('Reset failed: ' + e.message);
}
}
async function reprocessPhoto(photoId) {
const eng = document.getElementById('reEng' + photoId).value;
const model = document.getElementById('reModel' + photoId).value;
@@ -442,6 +533,17 @@ async function reprocessPhoto(photoId) {
}
}
async function deletePhoto(photoId) {
if (!confirm('Delete this entry and its file?')) return;
try {
const resp = await fetch(`/api/photos/${photoId}`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Delete failed');
loadPreviousPhotos();
} catch (e) {
alert('Delete failed: ' + e.message);
}
}
// --- File handling ---
document.getElementById('fileInput').addEventListener('change', async function(e) {
@@ -592,9 +694,10 @@ async function uploadSelected() {
const resp = await fetch('/api/analyze' + getOcrParams(), { method: 'POST', body: fd });
const data = await resp.json();
let html = '';
let html = '';
if (data.duplicate) {
html += '<div style="margin-bottom:8px;"><span class="match-badge dup">♻️ Already processed — skipped</span></div>';
html += '<div style="margin-bottom:8px;display:flex;align-items:center;gap:6px;"><span class="match-badge dup">♻️ Already processed — skipped</span>' +
'<button class="btn btn-xs" style="background:transparent;border:1px solid var(--amber);color:var(--amber);margin-left:auto;" onclick="resetAndReupload(' + (data.photo_id || 'null') + ')">🔄 Reset & Re-process</button></div>';
}
html += '<div class="exif-row"><span class="exif-key">Saved as</span><span class="exif-val">' + esc(data.saved_as) + '</span></div>';
html += '<div class="exif-row"><span class="exif-key">File size</span><span class="exif-val">' + data.file_size_kb + ' KB</span></div>';