45f7a77171
Build & Deploy / build-and-deploy (push) Successful in 46s
- OCR: ny read_all_bibs() returnerer alle unike startnumre (≥2 sifre) per bilde
- Ingest: oppretter én passering per bib (ikke bare beste), ingen bib → needs_review
- image_tagger.py: skriv/les bib-metadata som JSON i EXIF UserComment (piexif)
- Ingest + resolve: tagger bildefilen med bibs automatisk og ved manuell bekreftelse
- API: POST /api/passages/{id}/reanalyze — re-kjør OCR på eksisterende bilde
- API: POST /api/passages/{id}/resolve oppdaterer nå EXIF med bekreftet bib
- races: ny kolonne bib_filter_enabled (med automatisk migrering) + per-løp toggle
- ReviewPage: Re-analyser-knapp og klikk-for-zoom med scroll/drag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
214 lines
7.5 KiB
Python
214 lines
7.5 KiB
Python
"""
|
|
CRUD for løp (races) og stasjoner (stations).
|
|
"""
|
|
|
|
import uuid
|
|
from typing import Optional
|
|
|
|
import aiosqlite
|
|
|
|
|
|
async def init_race_tables(db: aiosqlite.Connection) -> None:
|
|
await db.executescript("""
|
|
CREATE TABLE IF NOT EXISTS races (
|
|
race_id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
date TEXT,
|
|
description TEXT,
|
|
is_active INTEGER NOT NULL DEFAULT 0,
|
|
bib_filter_enabled INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS stations (
|
|
station_id TEXT PRIMARY KEY,
|
|
race_id TEXT NOT NULL REFERENCES races(race_id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
station_order INTEGER NOT NULL DEFAULT 0,
|
|
gps_lat REAL,
|
|
gps_lon REAL,
|
|
gps_alt REAL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(race_id, name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_stations_race ON stations(race_id, station_order);
|
|
""")
|
|
# Migrering: legg til kolonne for eksisterende databaser
|
|
try:
|
|
await db.execute(
|
|
"ALTER TABLE races ADD COLUMN bib_filter_enabled INTEGER NOT NULL DEFAULT 0"
|
|
)
|
|
await db.commit()
|
|
except Exception:
|
|
pass # Kolonnen finnes allerede
|
|
await db.commit()
|
|
|
|
|
|
# ── Løp ──────────────────────────────────────────────────────────────────────
|
|
|
|
async def create_race(
|
|
db: aiosqlite.Connection,
|
|
name: str,
|
|
date: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
bib_filter_enabled: bool = False,
|
|
) -> dict:
|
|
race_id = str(uuid.uuid4())
|
|
await db.execute(
|
|
"INSERT INTO races (race_id, name, date, description, bib_filter_enabled) VALUES (?, ?, ?, ?, ?)",
|
|
(race_id, name, date, description, int(bib_filter_enabled)),
|
|
)
|
|
# Sett som aktivt hvis det er første løpet
|
|
async with db.execute("SELECT COUNT(*) FROM races") as cur:
|
|
count = (await cur.fetchone())[0]
|
|
if count == 1:
|
|
await db.execute("UPDATE races SET is_active = 1 WHERE race_id = ?", (race_id,))
|
|
|
|
# Opprett standard start- og målstasjon
|
|
for order, (sname, display) in enumerate([("start", "Start"), ("finish", "Mål")]):
|
|
await db.execute(
|
|
"""INSERT INTO stations (station_id, race_id, name, display_name, station_order)
|
|
VALUES (?, ?, ?, ?, ?)""",
|
|
(str(uuid.uuid4()), race_id, sname, display, order * 1000),
|
|
)
|
|
|
|
await db.commit()
|
|
return await get_race(db, race_id)
|
|
|
|
|
|
async def get_race(db: aiosqlite.Connection, race_id: str) -> Optional[dict]:
|
|
async with db.execute("SELECT * FROM races WHERE race_id = ?", (race_id,)) as cur:
|
|
row = await cur.fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
async def list_races(db: aiosqlite.Connection) -> list[dict]:
|
|
async with db.execute("SELECT * FROM races ORDER BY date DESC, created_at DESC") as cur:
|
|
rows = await cur.fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
async def update_race(
|
|
db: aiosqlite.Connection,
|
|
race_id: str,
|
|
name: str,
|
|
date: Optional[str],
|
|
description: Optional[str],
|
|
bib_filter_enabled: bool = False,
|
|
) -> bool:
|
|
cur = await db.execute(
|
|
"UPDATE races SET name = ?, date = ?, description = ?, bib_filter_enabled = ? WHERE race_id = ?",
|
|
(name, date, description, int(bib_filter_enabled), race_id),
|
|
)
|
|
await db.commit()
|
|
return cur.rowcount > 0
|
|
|
|
|
|
async def delete_race(db: aiosqlite.Connection, race_id: str) -> bool:
|
|
cur = await db.execute("DELETE FROM races WHERE race_id = ?", (race_id,))
|
|
await db.commit()
|
|
return cur.rowcount > 0
|
|
|
|
|
|
async def set_active_race(db: aiosqlite.Connection, race_id: str) -> bool:
|
|
await db.execute("UPDATE races SET is_active = 0")
|
|
cur = await db.execute(
|
|
"UPDATE races SET is_active = 1 WHERE race_id = ?", (race_id,)
|
|
)
|
|
await db.commit()
|
|
return cur.rowcount > 0
|
|
|
|
|
|
async def get_active_race(db: aiosqlite.Connection) -> Optional[dict]:
|
|
async with db.execute("SELECT * FROM races WHERE is_active = 1 LIMIT 1") as cur:
|
|
row = await cur.fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
# ── Stasjoner ─────────────────────────────────────────────────────────────────
|
|
|
|
async def list_stations(db: aiosqlite.Connection, race_id: str) -> list[dict]:
|
|
async with db.execute(
|
|
"SELECT * FROM stations WHERE race_id = ? ORDER BY station_order",
|
|
(race_id,),
|
|
) as cur:
|
|
rows = await cur.fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
async def get_station(db: aiosqlite.Connection, station_id: str) -> Optional[dict]:
|
|
async with db.execute(
|
|
"SELECT * FROM stations WHERE station_id = ?", (station_id,)
|
|
) as cur:
|
|
row = await cur.fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
async def create_station(
|
|
db: aiosqlite.Connection,
|
|
race_id: str,
|
|
name: str,
|
|
display_name: str,
|
|
station_order: int,
|
|
gps_lat: Optional[float] = None,
|
|
gps_lon: Optional[float] = None,
|
|
gps_alt: Optional[float] = None,
|
|
) -> dict:
|
|
station_id = str(uuid.uuid4())
|
|
await db.execute(
|
|
"""INSERT INTO stations
|
|
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt),
|
|
)
|
|
await db.commit()
|
|
return dict((await db.execute(
|
|
"SELECT * FROM stations WHERE station_id = ?", (station_id,)
|
|
)).get_cursor() or {})
|
|
|
|
|
|
async def upsert_station(
|
|
db: aiosqlite.Connection,
|
|
race_id: str,
|
|
name: str,
|
|
display_name: str,
|
|
station_order: int,
|
|
gps_lat: Optional[float] = None,
|
|
gps_lon: Optional[float] = None,
|
|
gps_alt: Optional[float] = None,
|
|
station_id: Optional[str] = None,
|
|
) -> dict:
|
|
if station_id:
|
|
await db.execute(
|
|
"""UPDATE stations SET name=?, display_name=?, station_order=?,
|
|
gps_lat=?, gps_lon=?, gps_alt=? WHERE station_id=?""",
|
|
(name, display_name, station_order, gps_lat, gps_lon, gps_alt, station_id),
|
|
)
|
|
else:
|
|
station_id = str(uuid.uuid4())
|
|
await db.execute(
|
|
"""INSERT INTO stations
|
|
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt),
|
|
)
|
|
await db.commit()
|
|
async with db.execute("SELECT * FROM stations WHERE station_id = ?", (station_id,)) as cur:
|
|
row = await cur.fetchone()
|
|
return dict(row)
|
|
|
|
|
|
async def delete_station(db: aiosqlite.Connection, station_id: str) -> bool:
|
|
# Ikke tillat sletting av start/finish
|
|
async with db.execute(
|
|
"SELECT name FROM stations WHERE station_id = ?", (station_id,)
|
|
) as cur:
|
|
row = await cur.fetchone()
|
|
if row and row["name"] in ("start", "finish"):
|
|
return False
|
|
cur = await db.execute("DELETE FROM stations WHERE station_id = ?", (station_id,))
|
|
await db.commit()
|
|
return cur.rowcount > 0
|