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:
@@ -21,15 +21,31 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_TESSERACT = False
|
HAS_TESSERACT = False
|
||||||
|
|
||||||
|
# --- Load .env file automatically ---
|
||||||
|
_dotenv = Path(__file__).parent.parent.parent / ".hermes" / ".env"
|
||||||
|
if not _dotenv.exists():
|
||||||
|
_dotenv = Path.home() / ".hermes" / ".env"
|
||||||
|
if _dotenv.exists():
|
||||||
|
for _line in _dotenv.read_text().splitlines():
|
||||||
|
_line = _line.strip()
|
||||||
|
if not _line or _line.startswith("#") or "=" not in _line:
|
||||||
|
continue
|
||||||
|
_key, _val = _line.split("=", 1)
|
||||||
|
_key = _key.strip()
|
||||||
|
_val = _val.strip()
|
||||||
|
# Only set if not already in environment
|
||||||
|
if _key not in os.environ:
|
||||||
|
os.environ[_key] = _val
|
||||||
|
|
||||||
# === LLM OCR via OpenCode Go ===
|
# === LLM OCR via OpenCode Go ===
|
||||||
OPENCODE_GO_KEY = os.environ.get("OPENCODE_GO_API_KEY", "")
|
OPENCODE_GO_KEY = (os.environ.get("OPENCODE_GO_API_KEY") or "").strip()
|
||||||
OPENCODE_GO_BASE = os.environ.get("OPENCODE_GO_BASE_URL", "https://opencode.ai/zen/go/v1")
|
OPENCODE_GO_BASE = (os.environ.get("OPENCODE_GO_BASE_URL") or "https://opencode.ai/zen/go/v1").strip()
|
||||||
LLM_OCR_MODEL = os.environ.get("LLM_OCR_MODEL", "mimo-v2-omni")
|
LLM_OCR_MODEL = (os.environ.get("LLM_OCR_MODEL") or "mimo-v2-omni").strip()
|
||||||
|
|
||||||
# === Google Gemini OCR (free tier) ===
|
# === Google Gemini OCR (free tier) ===
|
||||||
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
|
GOOGLE_API_KEY = (os.environ.get("GOOGLE_API_KEY") or "").strip()
|
||||||
GOOGLE_API_BASE = os.environ.get("GOOGLE_API_BASE_URL", "https://generativelanguage.googleapis.com/v1beta")
|
GOOGLE_API_BASE = (os.environ.get("GOOGLE_API_BASE_URL") or "https://generativelanguage.googleapis.com/v1beta").strip()
|
||||||
GOOGLE_OCR_MODEL = os.environ.get("GOOGLE_OCR_MODEL", "gemini-2.5-flash")
|
GOOGLE_OCR_MODEL = (os.environ.get("GOOGLE_OCR_MODEL") or "gemini-2.5-flash").strip()
|
||||||
|
|
||||||
UPLOADS = Path(__file__).parent / "uploads"
|
UPLOADS = Path(__file__).parent / "uploads"
|
||||||
UPLOADS.mkdir(exist_ok=True)
|
UPLOADS.mkdir(exist_ok=True)
|
||||||
@@ -133,8 +149,8 @@ def _save_photo_to_db(
|
|||||||
"""INSERT OR IGNORE INTO photos
|
"""INSERT OR IGNORE INTO photos
|
||||||
(orig_filename, file_hash, saved_as, file_size, exif_json,
|
(orig_filename, file_hash, saved_as, file_size, exif_json,
|
||||||
gps_lat, gps_lng, ocr_engine, ocr_model, ocr_raw_text,
|
gps_lat, gps_lng, ocr_engine, ocr_model, ocr_raw_text,
|
||||||
ocr_match_5dash6, ocr_match_5plus, machine_id)
|
ocr_match_5dash6, ocr_match_5plus, machine_id, sticker_color, has_barcode)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
(
|
(
|
||||||
orig_filename,
|
orig_filename,
|
||||||
file_hash,
|
file_hash,
|
||||||
@@ -149,6 +165,8 @@ def _save_photo_to_db(
|
|||||||
ocr_result.get("match_5dash6") if ocr_result else None,
|
ocr_result.get("match_5dash6") if ocr_result else None,
|
||||||
ocr_result.get("match_5plus") if ocr_result else None,
|
ocr_result.get("match_5plus") if ocr_result else None,
|
||||||
machine_id,
|
machine_id,
|
||||||
|
ocr_result.get("sticker_color") if ocr_result else None,
|
||||||
|
1 if ocr_result and ocr_result.get("has_barcode") else 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -552,6 +570,28 @@ def run_ocr_google(image_bytes: bytes, model: str | None = None, sticker_mode: b
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Process one photo (shared between analyze + bulk)
|
# Process one photo (shared between analyze + bulk)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
def _extract_machine_id(ocr_result: dict) -> str | None:
|
||||||
|
"""Extract machine ID from OCR result with priority logic.
|
||||||
|
|
||||||
|
1. match_5dash6 (5dash6 format: 12345-678901) → first 5 digits
|
||||||
|
2. match_5plus (any 5+ digit run) → first 5 digits
|
||||||
|
3. Otherwise → None
|
||||||
|
"""
|
||||||
|
match_5dash6 = ocr_result.get("match_5dash6")
|
||||||
|
match_5plus = ocr_result.get("match_5plus")
|
||||||
|
|
||||||
|
if match_5dash6:
|
||||||
|
# "12345-678901" → strip non-digits → "12345678901" → first 5
|
||||||
|
clean = re.sub(r"\D", "", match_5dash6)
|
||||||
|
if len(clean) >= 5:
|
||||||
|
return clean[:5]
|
||||||
|
if match_5plus:
|
||||||
|
clean = re.sub(r"\D", "", match_5plus)
|
||||||
|
if len(clean) >= 5:
|
||||||
|
return clean[:5]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _process_one(orig_filename: str, contents: bytes, ocr_engine: str,
|
def _process_one(orig_filename: str, contents: bytes, ocr_engine: str,
|
||||||
ocr_model: str | None, sticker_mode: bool) -> dict:
|
ocr_model: str | None, sticker_mode: bool) -> dict:
|
||||||
"""Run EXIF + OCR on a single photo. Returns result dict."""
|
"""Run EXIF + OCR on a single photo. Returns result dict."""
|
||||||
@@ -596,8 +636,9 @@ def _process_one(orig_filename: str, contents: bytes, ocr_engine: str,
|
|||||||
|
|
||||||
machine_id = None
|
machine_id = None
|
||||||
asset = None
|
asset = None
|
||||||
if ocr_result.get("match_5dash6"):
|
mid = _extract_machine_id(ocr_result)
|
||||||
machine_id = re.sub(r"\D", "", ocr_result["match_5dash6"])[-5:]
|
if mid:
|
||||||
|
machine_id = mid
|
||||||
asset = lookup_machine_id(machine_id)
|
asset = lookup_machine_id(machine_id)
|
||||||
|
|
||||||
# Save to DB if not a duplicate
|
# Save to DB if not a duplicate
|
||||||
@@ -737,8 +778,9 @@ async def bulk_process(
|
|||||||
asset = None
|
asset = None
|
||||||
needs_gps = False
|
needs_gps = False
|
||||||
|
|
||||||
if ocr_result.get("match_5dash6"):
|
mid = _extract_machine_id(ocr_result)
|
||||||
machine_id = re.sub(r"\D", "", ocr_result["match_5dash6"])[-5:]
|
if mid:
|
||||||
|
machine_id = mid
|
||||||
asset = lookup_machine_id(machine_id)
|
asset = lookup_machine_id(machine_id)
|
||||||
if asset:
|
if asset:
|
||||||
summary["matched"] += 1
|
summary["matched"] += 1
|
||||||
@@ -857,8 +899,9 @@ async def reprocess_photo(
|
|||||||
|
|
||||||
machine_id = None
|
machine_id = None
|
||||||
asset = None
|
asset = None
|
||||||
if ocr_result.get("match_5dash6"):
|
mid = _extract_machine_id(ocr_result)
|
||||||
machine_id = re.sub(r"\D", "", ocr_result["match_5dash6"])[-5:]
|
if mid:
|
||||||
|
machine_id = mid
|
||||||
asset = lookup_machine_id(machine_id)
|
asset = lookup_machine_id(machine_id)
|
||||||
|
|
||||||
# Update DB record with new OCR results
|
# Update DB record with new OCR results
|
||||||
@@ -866,7 +909,8 @@ async def reprocess_photo(
|
|||||||
try:
|
try:
|
||||||
db2.execute(
|
db2.execute(
|
||||||
"""UPDATE photos SET ocr_engine=?, ocr_model=?, ocr_raw_text=?,
|
"""UPDATE photos SET ocr_engine=?, ocr_model=?, ocr_raw_text=?,
|
||||||
ocr_match_5dash6=?, ocr_match_5plus=?, machine_id=?
|
ocr_match_5dash6=?, ocr_match_5plus=?, machine_id=?,
|
||||||
|
sticker_color=?
|
||||||
WHERE id=?""",
|
WHERE id=?""",
|
||||||
(
|
(
|
||||||
ocr_result.get("engine"),
|
ocr_result.get("engine"),
|
||||||
@@ -875,6 +919,7 @@ async def reprocess_photo(
|
|||||||
ocr_result.get("match_5dash6"),
|
ocr_result.get("match_5dash6"),
|
||||||
ocr_result.get("match_5plus"),
|
ocr_result.get("match_5plus"),
|
||||||
machine_id,
|
machine_id,
|
||||||
|
ocr_result.get("sticker_color"),
|
||||||
photo_id,
|
photo_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -892,6 +937,54 @@ async def reprocess_photo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/photos/{photo_id}/reset")
|
||||||
|
async def reset_photo(photo_id: int):
|
||||||
|
"""Delete a photo record and its file, allowing re-upload as new."""
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT saved_as FROM photos WHERE id = ?", (photo_id,)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Photo not found")
|
||||||
|
|
||||||
|
# Delete the file from disk
|
||||||
|
filepath = UPLOADS / row["saved_as"]
|
||||||
|
if filepath.exists():
|
||||||
|
filepath.unlink()
|
||||||
|
|
||||||
|
conn.execute("DELETE FROM photos WHERE id = ?", (photo_id,))
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"ok": True, "reset": photo_id, "message": "Photo entry deleted. Re-upload to process as new."}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/photos/{photo_id}")
|
||||||
|
async def delete_photo(photo_id: int):
|
||||||
|
"""Delete a photo record and its file from disk."""
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT saved_as FROM photos WHERE id = ?", (photo_id,)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Photo not found")
|
||||||
|
|
||||||
|
# Delete the file from disk
|
||||||
|
filepath = UPLOADS / row["saved_as"]
|
||||||
|
if filepath.exists():
|
||||||
|
filepath.unlink()
|
||||||
|
|
||||||
|
conn.execute("DELETE FROM photos WHERE id = ?", (photo_id,))
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"ok": True, "deleted": photo_id}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/lookup")
|
@app.get("/api/lookup")
|
||||||
async def lookup_asset(machine_id: str = ""):
|
async def lookup_asset(machine_id: str = ""):
|
||||||
"""Look up an asset by machine_id in the canteen assets database."""
|
"""Look up an asset by machine_id in the canteen assets database."""
|
||||||
|
|||||||
+109
-6
@@ -374,20 +374,78 @@ function loadPreviousPhotos() {
|
|||||||
section.style.display = 'block';
|
section.style.display = 'block';
|
||||||
document.getElementById('prevCount').textContent = data.photos.length;
|
document.getElementById('prevCount').textContent = data.photos.length;
|
||||||
div.innerHTML = data.photos.map(p => renderPrevCard(p)).join('');
|
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(() => {});
|
.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) {
|
function renderPrevCard(p) {
|
||||||
const hasMatch = p.ocr_match_5dash6 ? 'matched' : 'no-match';
|
const hasMatch = p.machine_id ? 'matched' : 'no-match';
|
||||||
const matchText = p.ocr_match_5dash6 || (p.ocr_match_5plus ? 'digits found' : 'no match');
|
const matchText = p.machine_id || (p.ocr_match_5plus ? 'digits found' : 'no match');
|
||||||
const engine = p.ocr_engine || 'tesseract';
|
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 time = p.created_at ? new Date(p.created_at + 'Z').toLocaleString() : '';
|
||||||
const colorHtml = p.sticker_color
|
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>`
|
? `<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">
|
<div class="prev-header">
|
||||||
<span class="prev-filename">${esc(p.orig_filename)}</span>
|
<span class="prev-filename">${esc(p.orig_filename)}</span>
|
||||||
<span class="prev-time">${time}</span>
|
<span class="prev-time">${time}</span>
|
||||||
@@ -397,6 +455,7 @@ function renderPrevCard(p) {
|
|||||||
<span class="match-badge ${hasMatch}">${esc(matchText)}</span>
|
<span class="match-badge ${hasMatch}">${esc(matchText)}</span>
|
||||||
<span class="engine-badge ${engCls}">${esc(engine)}${p.ocr_model ? ' ' + esc(p.ocr_model) : ''}</span>
|
<span class="engine-badge ${engCls}">${esc(engine)}${p.ocr_model ? ' ' + esc(p.ocr_model) : ''}</span>
|
||||||
${colorHtml}
|
${colorHtml}
|
||||||
|
${hasCoords ? ` <span class="match-badge has-gps">📍 ${Number(p.gps_lat).toFixed(4)}, ${Number(p.gps_lng).toFixed(4)}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="prev-actions">
|
<div class="prev-actions">
|
||||||
<select id="reEng${p.id}">
|
<select id="reEng${p.id}">
|
||||||
@@ -414,10 +473,42 @@ function renderPrevCard(p) {
|
|||||||
</select>
|
</select>
|
||||||
<label><input type="checkbox" id="reSticker${p.id}"> 🏷️</label>
|
<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-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>
|
</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>`;
|
</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) {
|
async function reprocessPhoto(photoId) {
|
||||||
const eng = document.getElementById('reEng' + photoId).value;
|
const eng = document.getElementById('reEng' + photoId).value;
|
||||||
const model = document.getElementById('reModel' + 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 ---
|
// --- File handling ---
|
||||||
|
|
||||||
document.getElementById('fileInput').addEventListener('change', async function(e) {
|
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 resp = await fetch('/api/analyze' + getOcrParams(), { method: 'POST', body: fd });
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
if (data.duplicate) {
|
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">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>';
|
html += '<div class="exif-row"><span class="exif-key">File size</span><span class="exif-val">' + data.file_size_kb + ' KB</span></div>';
|
||||||
|
|||||||
Reference in New Issue
Block a user