Backend: Export CSV/KML/clipboard, Assign Machine ID, GPS Proximity, Session Management, Batch Assign
This commit is contained in:
Binary file not shown.
@@ -1,9 +1,9 @@
|
|||||||
"""EXIF + OCR test backend — validate that GPS survives upload pipeline."""
|
"""EXIF + OCR test backend — validate that GPS survives upload pipeline."""
|
||||||
import hashlib, io, json, os, re, uuid, sqlite3, urllib.request
|
import csv, datetime, hashlib, io, json, math, os, re, uuid, sqlite3, urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, File, Form, HTTPException, Query, UploadFile
|
from fastapi import FastAPI, File, Form, HTTPException, Query, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
@@ -141,6 +141,7 @@ def _file_hash(data: bytes) -> str:
|
|||||||
def _save_photo_to_db(
|
def _save_photo_to_db(
|
||||||
orig_filename: str, file_hash: str, saved_as: str, file_size: int,
|
orig_filename: str, file_hash: str, saved_as: str, file_size: int,
|
||||||
exif_result: dict, ocr_result: dict, machine_id: str | None,
|
exif_result: dict, ocr_result: dict, machine_id: str | None,
|
||||||
|
session_id: int | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Insert a photo record. Returns the row id."""
|
"""Insert a photo record. Returns the row id."""
|
||||||
conn = _get_photos_db()
|
conn = _get_photos_db()
|
||||||
@@ -149,8 +150,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, sticker_color, has_barcode)
|
ocr_match_5dash6, ocr_match_5plus, machine_id, sticker_color, has_barcode, session_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
(
|
(
|
||||||
orig_filename,
|
orig_filename,
|
||||||
file_hash,
|
file_hash,
|
||||||
@@ -167,6 +168,7 @@ def _save_photo_to_db(
|
|||||||
machine_id,
|
machine_id,
|
||||||
ocr_result.get("sticker_color") if ocr_result else None,
|
ocr_result.get("sticker_color") if ocr_result else None,
|
||||||
1 if ocr_result and ocr_result.get("has_barcode") else 0,
|
1 if ocr_result and ocr_result.get("has_barcode") else 0,
|
||||||
|
session_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -693,6 +695,7 @@ async def bulk_process(
|
|||||||
ocr_engine: str = Query(default="tesseract"),
|
ocr_engine: str = Query(default="tesseract"),
|
||||||
ocr_model: str = Query(default=""),
|
ocr_model: str = Query(default=""),
|
||||||
sticker_mode: bool = Query(default=False),
|
sticker_mode: bool = Query(default=False),
|
||||||
|
session_id: int | None = Query(default=None),
|
||||||
):
|
):
|
||||||
"""Process multiple photos: OCR each, extract EXIF GPS, look up matching assets.
|
"""Process multiple photos: OCR each, extract EXIF GPS, look up matching assets.
|
||||||
|
|
||||||
@@ -793,6 +796,7 @@ async def bulk_process(
|
|||||||
photo_id = _save_photo_to_db(
|
photo_id = _save_photo_to_db(
|
||||||
orig_fname, fhash, saved_name, file_size,
|
orig_fname, fhash, saved_name, file_size,
|
||||||
exif_result, ocr_result, machine_id,
|
exif_result, ocr_result, machine_id,
|
||||||
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_dup:
|
if is_dup:
|
||||||
@@ -1051,9 +1055,416 @@ async def list_uploads():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Mount static LAST
|
||||||
|
|
||||||
|
@app.get("/api/export")
|
||||||
|
async def export_photos(
|
||||||
|
format: str = Query(default="csv", pattern="^(csv|kml|clipboard)$"),
|
||||||
|
session_id: int | None = Query(default=None),
|
||||||
|
limit: int = Query(default=500, le=5000),
|
||||||
|
):
|
||||||
|
"""Export photos as CSV, KML, or clipboard-ready text.
|
||||||
|
|
||||||
|
Query params:
|
||||||
|
- format: 'csv' (default), 'kml', or 'clipboard'
|
||||||
|
- session_id: limit export to a single session
|
||||||
|
- limit: max rows (default 500, max 5000)
|
||||||
|
"""
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
if session_id:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM photos WHERE session_id = ? ORDER BY created_at DESC LIMIT ?",
|
||||||
|
(session_id, limit),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM photos ORDER BY created_at DESC LIMIT ?", (limit,)
|
||||||
|
).fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for r in rows:
|
||||||
|
d = _row_to_dict(r)
|
||||||
|
rec = {
|
||||||
|
"filename": d.get("orig_filename", ""),
|
||||||
|
"lat": d.get("gps_lat"),
|
||||||
|
"lng": d.get("gps_lng"),
|
||||||
|
"machine_id": d.get("machine_id", ""),
|
||||||
|
"ocr_engine": d.get("ocr_engine", ""),
|
||||||
|
"sticker_color": d.get("sticker_color", ""),
|
||||||
|
"timestamp": d.get("created_at", ""),
|
||||||
|
}
|
||||||
|
# Look up asset details
|
||||||
|
if d.get("machine_id"):
|
||||||
|
asset = lookup_machine_id(d["machine_id"])
|
||||||
|
if asset:
|
||||||
|
rec.update({
|
||||||
|
"asset_name": asset.get("name", ""),
|
||||||
|
"building": asset.get("building_name", ""),
|
||||||
|
"floor": asset.get("floor", ""),
|
||||||
|
"room": asset.get("room", ""),
|
||||||
|
"make": asset.get("make", ""),
|
||||||
|
"model": asset.get("model", ""),
|
||||||
|
})
|
||||||
|
records.append(rec)
|
||||||
|
|
||||||
|
if format == "csv":
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
header = ["filename", "lat", "lng", "machine_id", "asset_name",
|
||||||
|
"building", "floor", "room", "make", "model",
|
||||||
|
"ocr_engine", "sticker_color", "timestamp"]
|
||||||
|
bom = "\ufeff"
|
||||||
|
output.write(bom)
|
||||||
|
writer.writerow(header)
|
||||||
|
for rec in records:
|
||||||
|
writer.writerow([rec.get(h.replace("-", "_"), "") for h in header])
|
||||||
|
csv_content = output.getvalue()
|
||||||
|
return StreamingResponse(
|
||||||
|
iter([csv_content]),
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename=exif-export.csv"},
|
||||||
|
)
|
||||||
|
|
||||||
|
elif format == "kml":
|
||||||
|
kml_parts = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<kml xmlns="http://www.opengis.net/kml/2.2">',
|
||||||
|
" <Document>",
|
||||||
|
f" <name>EXIF Export ({len(records)} photos)</name>",
|
||||||
|
]
|
||||||
|
for rec in records:
|
||||||
|
if rec["lat"] is None or rec["lng"] is None:
|
||||||
|
continue
|
||||||
|
name = rec.get("asset_name") or rec.get("machine_id") or rec.get("filename", "Photo")
|
||||||
|
desc_lines = [
|
||||||
|
f"Filename: {rec['filename']}",
|
||||||
|
f"Machine ID: {rec.get('machine_id', 'N/A')}",
|
||||||
|
f"OCR Engine: {rec.get('ocr_engine', 'N/A')}",
|
||||||
|
f"Timestamp: {rec.get('timestamp', 'N/A')}",
|
||||||
|
]
|
||||||
|
kml_parts.append(" <Placemark>")
|
||||||
|
kml_parts.append(f" <name>{name}</name>")
|
||||||
|
kml_parts.append(f" <description>{chr(10).join(desc_lines)}</description>")
|
||||||
|
kml_parts.append(" <Point>")
|
||||||
|
kml_parts.append(f" <coordinates>{rec['lng']},{rec['lat']},0</coordinates>")
|
||||||
|
kml_parts.append(" </Point>")
|
||||||
|
kml_parts.append(" </Placemark>")
|
||||||
|
kml_parts.append(" </Document>")
|
||||||
|
kml_parts.append("</kml>")
|
||||||
|
kml_content = "\n".join(kml_parts)
|
||||||
|
return StreamingResponse(
|
||||||
|
iter([kml_content]),
|
||||||
|
media_type="application/vnd.google-earth.kml+xml",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename=exif-export.kml"},
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # clipboard
|
||||||
|
lines = []
|
||||||
|
for rec in records:
|
||||||
|
if rec["lat"] is not None and rec["lng"] is not None:
|
||||||
|
label = rec.get("asset_name") or rec.get("machine_id") or rec.get("filename", "?")
|
||||||
|
lines.append(f"{rec['lat']:.6f},{rec['lng']:.6f} # {label}")
|
||||||
|
return {"count": len(lines), "text": "\n".join(lines)}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
# Inline Machine ID Edit — Feature #5 #
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
@app.post("/api/assign-machine-id")
|
||||||
|
async def assign_machine_id(request: dict):
|
||||||
|
"""Manually assign a machine_id to a photo record.
|
||||||
|
|
||||||
|
Body: { "photo_id": int, "machine_id": str }
|
||||||
|
Returns: { "photo_id": int, "machine_id": str, "asset": dict|null, "needs_gps": bool }
|
||||||
|
"""
|
||||||
|
photo_id = request.get("photo_id")
|
||||||
|
machine_id = (request.get("machine_id") or "").strip()
|
||||||
|
if not photo_id or not machine_id:
|
||||||
|
raise HTTPException(400, "photo_id and machine_id are required")
|
||||||
|
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
row = conn.execute("SELECT id, gps_lat, gps_lng FROM photos WHERE id = ?", (photo_id,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, f"Photo {photo_id} not found")
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE photos SET machine_id = ? WHERE id = ?",
|
||||||
|
(machine_id, photo_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
asset = lookup_machine_id(machine_id)
|
||||||
|
has_photo_gps = row["gps_lat"] is not None and row["gps_lng"] is not None
|
||||||
|
needs_gps = bool(asset and (asset["latitude"] is None or asset["longitude"] is None) and has_photo_gps)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"photo_id": photo_id,
|
||||||
|
"machine_id": machine_id,
|
||||||
|
"asset": asset,
|
||||||
|
"needs_gps": needs_gps,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
# GPS Proximity — Feature #6 #
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
@app.get("/api/nearby")
|
||||||
|
async def nearby_assets(
|
||||||
|
lat: float = Query(...),
|
||||||
|
lng: float = Query(...),
|
||||||
|
radius: float = Query(default=10.0, ge=1, le=1000),
|
||||||
|
exclude_photo_id: int | None = Query(default=None),
|
||||||
|
):
|
||||||
|
"""Find canteen assets near a GPS point.
|
||||||
|
|
||||||
|
Uses Euclidean approximation over lat/lng (accurate at small radii <1km).
|
||||||
|
1 deg lat ~ 111,320m, 1 deg lng ~ 111,320 * cos(lat).
|
||||||
|
"""
|
||||||
|
conn = _get_canteen_db()
|
||||||
|
if not conn:
|
||||||
|
return {"nearby": [], "count": 0, "radius_m": radius}
|
||||||
|
|
||||||
|
try:
|
||||||
|
lat_rad = lat * 3.14159265 / 180.0
|
||||||
|
deg_per_m_lat = 1.0 / 111320.0
|
||||||
|
deg_per_m_lng = 1.0 / (111320.0 * math.cos(lat_rad))
|
||||||
|
rad_deg = radius * deg_per_m_lat
|
||||||
|
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT id, machine_id, name, latitude, longitude,
|
||||||
|
building_name, floor, room, category, status
|
||||||
|
FROM assets
|
||||||
|
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
|
||||||
|
AND (latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?) < ?
|
||||||
|
ORDER BY (latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?)
|
||||||
|
LIMIT 50""",
|
||||||
|
(lat, lat, lng, lng, rad_deg * rad_deg,
|
||||||
|
lat, lat, lng, lng),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
nearby = []
|
||||||
|
for row in rows:
|
||||||
|
dlat = row["latitude"] - lat
|
||||||
|
dlng = (row["longitude"] - lng) * math.cos(lat_rad)
|
||||||
|
distance_m = (dlat * dlat + dlng * dlng) ** 0.5 * 111320.0
|
||||||
|
if distance_m >= radius:
|
||||||
|
continue
|
||||||
|
nearby.append({
|
||||||
|
"asset_id": row["id"],
|
||||||
|
"machine_id": row["machine_id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"distance_m": round(distance_m, 1),
|
||||||
|
"latitude": row["latitude"],
|
||||||
|
"longitude": row["longitude"],
|
||||||
|
"has_gps": True,
|
||||||
|
"building": row.get("building_name"),
|
||||||
|
"floor": row.get("floor"),
|
||||||
|
"room": row.get("room"),
|
||||||
|
"category": row.get("category"),
|
||||||
|
"status": row.get("status"),
|
||||||
|
})
|
||||||
|
nearby.sort(key=lambda x: x["distance_m"])
|
||||||
|
return {"nearby": nearby, "count": len(nearby), "radius_m": radius}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
# Session Management — Feature #9 #
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
def _init_sessions_table():
|
||||||
|
"""Migrate photos.db: create sessions table, add session_id column."""
|
||||||
|
conn = sqlite3.connect(str(PHOTOS_DB))
|
||||||
|
try:
|
||||||
|
conn.execute("""CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL DEFAULT '',
|
||||||
|
building TEXT DEFAULT '',
|
||||||
|
floor TEXT DEFAULT '',
|
||||||
|
photo_count INTEGER DEFAULT 0,
|
||||||
|
matched_count INTEGER DEFAULT 0,
|
||||||
|
needs_gps_count INTEGER DEFAULT 0,
|
||||||
|
closed_at TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
)""")
|
||||||
|
cols = [r[1] for r in conn.execute("PRAGMA table_info(photos)").fetchall()]
|
||||||
|
if "session_id" not in cols:
|
||||||
|
conn.execute("ALTER TABLE photos ADD COLUMN session_id INTEGER REFERENCES sessions(id)")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
_init_sessions_table()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/sessions")
|
||||||
|
async def list_sessions():
|
||||||
|
"""List all sessions, newest first."""
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM sessions ORDER BY created_at DESC LIMIT 100"
|
||||||
|
).fetchall()
|
||||||
|
return {"sessions": [_row_to_dict(r) for r in rows]}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/sessions")
|
||||||
|
async def create_session(request: dict):
|
||||||
|
"""Create a new session.
|
||||||
|
|
||||||
|
Body: { name?, building?, floor? }
|
||||||
|
Returns: { id, ...session_fields }
|
||||||
|
"""
|
||||||
|
name = (request.get("name") or "").strip() or f"Session {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
||||||
|
building = (request.get("building") or "").strip()
|
||||||
|
floor = (request.get("floor") or "").strip()
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
cur = conn.execute(
|
||||||
|
"INSERT INTO sessions (name, building, floor) VALUES (?, ?, ?)",
|
||||||
|
(name, building, floor),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
session_id = cur.lastrowid
|
||||||
|
row = conn.execute("SELECT * FROM sessions WHERE id = ?", (session_id,)).fetchone()
|
||||||
|
return _row_to_dict(row)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/sessions/{session_id}")
|
||||||
|
async def get_session(session_id: int):
|
||||||
|
"""Get a session with all its photos."""
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
srow = conn.execute("SELECT * FROM sessions WHERE id = ?", (session_id,)).fetchone()
|
||||||
|
if not srow:
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
session = _row_to_dict(srow)
|
||||||
|
photos = conn.execute(
|
||||||
|
"SELECT * FROM photos WHERE session_id = ? ORDER BY created_at ASC",
|
||||||
|
(session_id,),
|
||||||
|
).fetchall()
|
||||||
|
session["photos"] = [_row_to_dict(r) for r in photos]
|
||||||
|
total = len(session["photos"])
|
||||||
|
matched = sum(1 for p in session["photos"] if p.get("machine_id"))
|
||||||
|
needs_gps = sum(1 for p in session["photos"] if p.get("machine_id") and (p.get("gps_lat") is None))
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE sessions SET photo_count=?, matched_count=?, needs_gps_count=? WHERE id=?",
|
||||||
|
(total, matched, needs_gps, session_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
session["photo_count"] = total
|
||||||
|
session["matched_count"] = matched
|
||||||
|
session["needs_gps_count"] = needs_gps
|
||||||
|
return session
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/sessions/{session_id}/close")
|
||||||
|
async def close_session(session_id: int):
|
||||||
|
"""Finalize a session — no more photos can be added."""
|
||||||
|
conn = _get_photos_db()
|
||||||
|
try:
|
||||||
|
row = conn.execute("SELECT id FROM sessions WHERE id = ?", (session_id,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE sessions SET closed_at = datetime('now') WHERE id = ?",
|
||||||
|
(session_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return {"ok": True, "session_id": session_id, "closed_at": "now"}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
# Batch Machine ID Assign — Feature #8 #
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
@app.post("/api/bulk-assign")
|
||||||
|
async def bulk_assign_machine_id(request: dict):
|
||||||
|
"""Assign the same machine_id to multiple photos at once.
|
||||||
|
|
||||||
|
Body: { "photo_ids": [int], "machine_id": str }
|
||||||
|
Returns: { "updated": N, "gps_pushed": M, "no_gps": [...filenames] }
|
||||||
|
"""
|
||||||
|
photo_ids = request.get("photo_ids", [])
|
||||||
|
machine_id = (request.get("machine_id") or "").strip()
|
||||||
|
|
||||||
|
if not photo_ids or not isinstance(photo_ids, list):
|
||||||
|
raise HTTPException(400, "photo_ids must be a non-empty list")
|
||||||
|
if not machine_id:
|
||||||
|
raise HTTPException(400, "machine_id is required")
|
||||||
|
|
||||||
|
asset = lookup_machine_id(machine_id)
|
||||||
|
conn = _get_photos_db()
|
||||||
|
no_gps_files = []
|
||||||
|
gps_pushed = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for pid in photo_ids:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id, orig_filename, gps_lat, gps_lng, machine_id FROM photos WHERE id = ?",
|
||||||
|
(pid,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE photos SET machine_id = ? WHERE id = ?",
|
||||||
|
(machine_id, pid),
|
||||||
|
)
|
||||||
|
|
||||||
|
if asset and (asset["latitude"] is None or asset["longitude"] is None):
|
||||||
|
gps_lat = row["gps_lat"]
|
||||||
|
gps_lng = row["gps_lng"]
|
||||||
|
if gps_lat is not None and gps_lng is not None:
|
||||||
|
canteen = _get_canteen_db()
|
||||||
|
if canteen:
|
||||||
|
try:
|
||||||
|
canteen.execute(
|
||||||
|
"UPDATE assets SET latitude=?, longitude=?, updated_at=datetime('now') WHERE id=?",
|
||||||
|
(float(gps_lat), float(gps_lng), asset["id"]),
|
||||||
|
)
|
||||||
|
canteen.commit()
|
||||||
|
gps_pushed += 1
|
||||||
|
finally:
|
||||||
|
canteen.close()
|
||||||
|
else:
|
||||||
|
no_gps_files.append(row.get("orig_filename", f"photo_{pid}"))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"updated": len(photo_ids),
|
||||||
|
"gps_pushed": gps_pushed,
|
||||||
|
"no_gps": no_gps_files,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Mount static LAST
|
# Mount static LAST
|
||||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
# Export (CSV / KML / Clipboard) — Feature #4 #
|
||||||
|
# ---------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8903)
|
uvicorn.run(app, host="0.0.0.0", port=8903)
|
||||||
|
|||||||
Reference in New Issue
Block a user