Files
steinhelge 45f7a77171
Build & Deploy / build-and-deploy (push) Successful in 46s
Støtte for flere bibs per bilde, EXIF-metadata og zoom i gjennomgang
- 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>
2026-03-22 09:01:51 +01:00

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