HNS sprint: 3-tab drill-down + parallel deep scraper dispatch

HNS-1 verify: smoke test 93409 OK, gap 854 uncovered, throughput ~60/min
HNS-2 dispatch: scripts/hns_dispatch.sh + 5 parallel workers shard'd po roster ID; coverage 265→1098 distinct_seasons (93.7% of 1172 roster), 125→971 distinct_matches; total seasons 3170→13371, matches 23515→150071
HNS-3 UI: 6-tab panel collapsed na 3 (🏆 Karijera / 📅 Utakmice / 👤 Profil); novi /api/v2/clan/{id}/hns-matches?limit=30 + /clan/{id}/hns-profile (bio + summary + HNS deep link); prof-grid 3-col card s gold jersey badge; OIB RBAC-mask. Test Josip Zec 449: 72 sez/30 utak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Damir Radulić
2026-05-05 14:24:05 +02:00
parent 448273945c
commit 1e611d59f1
4 changed files with 227 additions and 46 deletions
+84
View File
@@ -5856,3 +5856,87 @@ def v2_clanovi_priority_sort(only: bool = False, limit: int = 500):
""", (limit,))
return {"count": len(rows), "rows": rows}
# ===================================================================
# HNS-3 (2026-05-05) — explicit drill-down endpoints for sport2.html
# Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one)
# Description: 3-tab drill-down (HNS Karijera / Utakmice / Profil)
# ===================================================================
@router.get("/clan/{clan_id}/hns-matches")
def v2_clan_hns_matches(clan_id: int, limit: int = 30):
"""Posljednje N utakmica iz hns_player_matches za sportaša (sortirano DESC po datumu)."""
if limit < 1: limit = 1
if limit > 500: limit = 500
rows = db_query("""
SELECT id, hns_igrac_id, clan_id, datum, natjecanje,
domacin, gost, rezultat, pozicija, startna,
minute_od, minute_do,
CASE WHEN minute_od IS NOT NULL AND minute_do IS NOT NULL
THEN (minute_do - minute_od) ELSE NULL END AS minute,
golovi, asistencije, zuti, crveni, source_url, scraped_at
FROM pgz_sport.hns_player_matches
WHERE clan_id = %s
ORDER BY datum DESC NULLS LAST, id DESC
LIMIT %s
""", (clan_id, limit))
return {"clan_id": clan_id, "limit": limit, "count": len(rows), "rows": rows}
@router.get("/clan/{clan_id}/hns-profile")
def v2_clan_hns_profile(clan_id: int):
"""Bio + HNS profil block za drill-down 'Profil' tab.
Vraća sve relevantne kolone iz pgz_sport.clanovi + HNS deep link + agregirane HNS statistike.
"""
p = db_one("""
SELECT c.id, c.ime, c.prezime, c.oib,
c.datum_rodenja, c.datum_rodjenja,
c.mjesto_rodenja, c.mjesto_rodjenja,
c.spol, c.sport, c.pozicija,
c.dominantna_noga, c.visina_cm, c.tezina_kg,
c.broj_dresa, c.broj_dres,
c.kategorija, c.podkategorija, c.kategorije,
c.reprezentativac, c.kategoriziran, c.stipendiran,
c.aktivan, c.aktivni_status,
c.email, c.telefon, c.adresa, c.grad,
c.biografija,
c.slug, c.slika_url,
c.hns_igrac_id, c.profile_url, c.source_url,
c.klub_id, c.klub_naziv_godisnjak,
k.naziv AS klub_naziv,
c.dob_age,
EXTRACT(YEAR FROM age(COALESCE(c.datum_rodjenja, c.datum_rodenja)))::int AS dob_calc
FROM pgz_sport.clanovi c
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
WHERE c.id = %s
""", (clan_id,))
if not p:
raise HTTPException(404, "Sportaš nije pronađen")
# Aggregate HNS career stats (cheap, single row)
summary = db_one("""
SELECT count(DISTINCT sezona) AS sezona_broj,
COALESCE(sum(nastupi),0) AS ukupno_nastupa,
COALESCE(sum(golovi),0) AS ukupno_golova,
COALESCE(sum(asistencije),0) AS ukupno_asistencija,
COALESCE(sum(zuti),0) AS ukupno_zutih,
COALESCE(sum(crveni),0) AS ukupno_crvenih,
COALESCE(sum(minute),0) AS ukupno_minuta,
min(sezona) AS prva_sezona,
max(sezona) AS zadnja_sezona
FROM pgz_sport.hns_player_seasons WHERE clan_id = %s
""", (clan_id,)) or {}
# Build HNS deep link
hns_url = p.get("profile_url")
if not hns_url and p.get("hns_igrac_id"):
slug = p.get("slug") or f"{(p.get('ime') or '').lower()}-{(p.get('prezime') or '').lower()}".replace(" ", "-")
hns_url = f"https://semafor.hns.family/igraci/{p['hns_igrac_id']}/{slug}/"
return {
"clan_id": clan_id,
"profile": p,
"hns_summary": summary,
"hns_url": hns_url,
}