Files
steinhelge 5393e85a74
Build & Deploy / build-and-deploy (push) Successful in 2m18s
Add race and station management
- races table: name, date, description, is_active
- stations table: ordered checkpoints with GPS per race
- New /api/races and /api/races/{id}/stations endpoints
- Upload now requires race + station selection; uses station GPS
  so images without GPS EXIF are accepted
- passages filtered by active race throughout
- RacePage: create races, manage stations (add/edit/delete checkpoints)
- Navbar shows active race name
- Start and finish stations created automatically per race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:45 +01:00

127 lines
3.9 KiB
Python

"""
Beregn split-tider og totalresultat fra passeringsloggen.
"""
from datetime import datetime
from typing import Optional
import aiosqlite
async def get_results(db: aiosqlite.Connection, race_id: Optional[str] = None) -> list[dict]:
"""
Hent totalresultat for alle utøvere som har passert start og mål.
Returnerer sortert liste med split-tider.
"""
race_filter = "AND p.race_id = ?" if race_id else ""
params = [race_id] if race_id else []
async with db.execute(f"""
SELECT p.profile_id, p.bib_number, a.name, a.club,
p.station, p.timestamp_utc
FROM passages p
LEFT JOIN athletes a ON a.profile_id = p.profile_id
WHERE p.needs_review = 0 AND p.profile_id IS NOT NULL {race_filter}
ORDER BY p.profile_id, p.timestamp_utc
""", params) as cur:
rows = await cur.fetchall()
# Grupper per utøver
athletes: dict[str, dict] = {}
for row in rows:
pid = row["profile_id"]
if pid not in athletes:
athletes[pid] = {
"profile_id": pid,
"bib_number": row["bib_number"],
"name": row["name"],
"club": row["club"],
"passages": [],
}
athletes[pid]["passages"].append({
"station": row["station"],
"timestamp_utc": row["timestamp_utc"],
})
results = []
for pid, data in athletes.items():
passages = data["passages"]
if not passages:
continue
# Finn start og mål
start_p = next((p for p in passages if p["station"] == "start"), None)
finish_p = next((p for p in passages if p["station"] == "finish"), None)
start_time = _parse_ts(start_p["timestamp_utc"]) if start_p else None
finish_time = _parse_ts(finish_p["timestamp_utc"]) if finish_p else None
total_seconds = None
if start_time and finish_time:
total_seconds = (finish_time - start_time).total_seconds()
# Split-tider mellom alle stasjoner
splits = []
prev_ts = start_time
prev_station = "start"
for p in passages:
if p["station"] == "start":
continue
ts = _parse_ts(p["timestamp_utc"])
split_s = (ts - prev_ts).total_seconds() if prev_ts else None
splits.append({
"from": prev_station,
"to": p["station"],
"split_seconds": split_s,
"split_formatted": _fmt_seconds(split_s),
"timestamp_utc": p["timestamp_utc"],
})
prev_ts = ts
prev_station = p["station"]
results.append({
"profile_id": pid,
"bib_number": data["bib_number"],
"name": data["name"] or f"Ukjent ({data['bib_number']})",
"club": data["club"],
"start_time": start_p["timestamp_utc"] if start_p else None,
"finish_time": finish_p["timestamp_utc"] if finish_p else None,
"total_seconds": total_seconds,
"total_formatted": _fmt_seconds(total_seconds),
"splits": splits,
"dnf": finish_time is None and start_time is not None,
"dns": start_time is None,
})
# Sorter: fullført (etter tid), DNF, DNS
results.sort(key=_sort_key)
return results
def _parse_ts(ts_str: str) -> Optional[datetime]:
if not ts_str:
return None
try:
return datetime.fromisoformat(ts_str)
except ValueError:
return None
def _fmt_seconds(seconds: Optional[float]) -> Optional[str]:
if seconds is None:
return None
seconds = int(seconds)
h = seconds // 3600
m = (seconds % 3600) // 60
s = seconds % 60
if h:
return f"{h}:{m:02d}:{s:02d}"
return f"{m}:{s:02d}"
def _sort_key(r: dict):
if r["dns"]:
return (3, 0)
if r["dnf"]:
return (2, 0)
return (1, r["total_seconds"] or float("inf"))