diff --git a/static/index.html b/static/index.html
index 218f2dd..20f257f 100644
--- a/static/index.html
+++ b/static/index.html
@@ -302,6 +302,41 @@ if ('serviceWorker' in navigator) {
.walk-timeline { font-size: 10px; color: var(--text2); margin-top: 4px; max-height: 120px; overflow-y: auto; }
.walk-timeline div { padding: 1px 0; }
+ /* Lightbox โ full-screen photo viewer with zoom */
+ #lightbox {
+ display: none; position: fixed; inset: 0; z-index: 99999;
+ background: rgba(0,0,0,0.95);
+ touch-action: none; user-select: none; -webkit-user-select: none;
+ }
+ #lightbox.active { display: flex; align-items: center; justify-content: center; }
+ #lightboxClose {
+ position: fixed; top: 12px; right: 12px; z-index: 100000;
+ width: 36px; height: 36px; border-radius: 50%;
+ background: rgba(255,255,255,0.15); border: none;
+ color: #fff; font-size: 20px; cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ -webkit-tap-highlight-color: transparent;
+ }
+ #lightboxClose:active { background: rgba(255,255,255,0.3); }
+ #lightboxContainer {
+ width: 100%; height: 100%;
+ display: flex; align-items: center; justify-content: center;
+ overflow: hidden; position: relative;
+ }
+ #lightboxImage {
+ max-width: 100%; max-height: 100%;
+ object-fit: contain; cursor: zoom-in;
+ transition: none;
+ will-change: transform;
+ }
+ #lightboxImage.zoomed { cursor: grab; }
+ #lightboxImage.zoomed:active { cursor: grabbing; }
+ #lightboxZoomHint {
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+ color: rgba(255,255,255,0.4); font-size: 11px;
+ pointer-events: none; transition: opacity 0.5s;
+ }
+
@@ -359,7 +394,7 @@ if ('serviceWorker' in navigator) {
-
+
๐ Upload & Run OCR
@@ -433,6 +468,8 @@ let selectedIdx = -1;
let currentFilter = 'all';
let bulkMap = null;
let bulkData = [];
+let _lastPhotoId = null; // stored by uploadSelected for manualLookup assign
+let _lastPhotoGps = null; // {lat, lng} for manualLookup push
// On load: fetch previously processed photos
document.addEventListener('DOMContentLoaded', loadPreviousPhotos);
@@ -843,17 +880,23 @@ async function uploadSelected() {
'๐ Re-run ' +
'
';
- // Manual entry when GPS exists but OCR failed
- if ((!ocr.match_5dash6 && !ocr.match_5plus) && data.exif && data.exif.gps) {
- html += '
' +
- '
โ๏ธ GPS found but no machine ID in OCR. Enter it manually:
' +
- '
' +
- ' ' +
- '๐ Lookup ' +
- '
' +
- '
' +
- '
';
- }
+ // Store for manualLookup assign
+ _lastPhotoId = data.photo_id;
+ _lastPhotoGps = data.exif && data.exif.gps ? {lat: data.exif.gps.lat, lng: data.exif.gps.lng} : null;
+
+ // Always show manual entry field โ user can correct OCR or enter ID manually
+ html += '
' +
+ '
' +
+ (ocr.match_5dash6 || ocr.match_5plus
+ ? 'โ๏ธ Try a different ID if the match above is wrong:'
+ : 'โ๏ธ No machine ID found in OCR. Enter it manually:') +
+ '
' +
+ '
' +
+ ' ' +
+ '๐ Lookup ' +
+ '
' +
+ '
' +
+ '
';
div.innerHTML = html;
} catch (e) {
@@ -876,6 +919,12 @@ function manualLookup() {
'
' + esc(a.name) + '
' +
'
๐ ' + esc(a.machine_id) + ' ๐ฆ ' + esc(a.category) + ' ' +
(a.status === 'active' ? '๐ข Active' : 'โช ' + esc(a.status)) +
+ '
' +
+ '
' +
+ 'โ
Assign to Photo ' +
+ (_lastPhotoGps
+ ? '๐ค Push GPS '
+ : '') +
'
';
} else {
resultDiv.innerHTML = 'โ ๏ธ No asset found for ' + esc(val) + ' ';
@@ -886,6 +935,54 @@ function manualLookup() {
});
}
+// Assign a machine ID to the last analyzed photo (manual entry)
+async function assignManualId(machineId) {
+ if (!_lastPhotoId || !machineId) { alert('No photo loaded'); return; }
+ const resultDiv = document.getElementById('manualResult');
+ try {
+ const r = await fetch('/api/assign-machine-id', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({photo_id: _lastPhotoId, machine_id: machineId})
+ });
+ const d = await r.json();
+ if (d.asset) {
+ let extra = '';
+ if (d.needs_gps && _lastPhotoGps) {
+ extra = '๐ค Push GPS ';
+ }
+ resultDiv.innerHTML = 'โ
Assigned: ' + esc(d.asset.name) + ' ๐ ' + esc(d.asset.machine_id) + '
' + extra +
+ 'Saved to photo #' + _lastPhotoId + '
';
+ } else {
+ resultDiv.innerHTML = 'โ ๏ธ Could not assign: ' + esc(d.reason || 'unknown error') + ' ';
+ }
+ } catch (e) {
+ resultDiv.innerHTML = 'โ Error: ' + esc(e.message) + ' ';
+ }
+}
+
+// Push GPS from the last analyzed photo to a canteen asset
+async function pushGpsManual(assetId) {
+ if (!_lastPhotoGps || !assetId) { alert('No GPS data available'); return; }
+ const resultDiv = document.getElementById('manualResult');
+ try {
+ const r = await fetch('/api/push-gps', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({asset_id: assetId, latitude: _lastPhotoGps.lat, longitude: _lastPhotoGps.lng})
+ });
+ const d = await r.json();
+ if (d.updated) {
+ resultDiv.innerHTML = 'โ
GPS pushed successfully! ๐ ' + Number(_lastPhotoGps.lat).toFixed(5) + ', ' + Number(_lastPhotoGps.lng).toFixed(5) + '
'
+ + 'Canteen DB updated
';
+ } else {
+ resultDiv.innerHTML = 'โ ๏ธ Push failed: ' + esc(d.reason || 'unknown') + ' ';
+ }
+ } catch (e) {
+ resultDiv.innerHTML = 'โ Error: ' + esc(e.message) + ' ';
+ }
+}
+
async function reprocessCurrent() {
const eng = document.getElementById('reRunEng').value;
const model = document.getElementById('reRunModel').value;
@@ -1650,5 +1747,169 @@ if (navigator.serviceWorker) {
// Initialize banner on load
setTimeout(updateOfflineBanner, 500);
+
+
+
โ
+
+
+
+
Pinch / scroll to zoom ยท Double-tap to fit
+
+