Crisis V7 MEGA: sufinanciranje_sport + panel + CRM auth
DB: - pgz_sport.sufinanciranje_sport.je_klub flag (RSS programi/totals false) - pgz_sport.sufinanciranje_sport.klub_id matched Endpoints: - /v2/potpore/by-year: samo_klubovi=True default + davatelj filter Frontend: - sport2.html PANEL FORCE HIDE CSS (right:-100vw default) - crm_v2.html: redirect to /login only on actual 401, not on page load
This commit is contained in:
@@ -674,10 +674,18 @@ def get_savez(savez_id: int, authorization: Optional[str] = Header(None)):
|
||||
return {**savez, "klubovi": klubovi, "statistika": statistika, "manifestacije": manifestacije}
|
||||
|
||||
# ==================== KLUBOVI ====================
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Endpoint: GET /api/klubovi
|
||||
# Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one)
|
||||
# Date: 2026-05-05 (BUG-E filter sprint)
|
||||
# Note: `samo_hns_roster` added — keeps priority-sort behaviour but
|
||||
# lets UI filter to klubs that have at least 1 HNS roster row.
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
@app.get("/api/klubovi")
|
||||
def list_klubovi(authorization: Optional[str] = Header(None), q: Optional[str] = None, savez_id: Optional[int] = None,
|
||||
nositelj: Optional[bool] = None, region: Optional[str] = None, sport: Optional[str] = None, grad: Optional[str] = None,
|
||||
kategorija: Optional[str] = None, godisnjak: Optional[bool] = None, financiran: Optional[bool] = None,
|
||||
samo_hns_roster: Optional[bool] = None,
|
||||
sort: str = "naziv", order: str = "asc"):
|
||||
where = ["v.aktivan"]
|
||||
params = []
|
||||
@@ -703,6 +711,8 @@ def list_klubovi(authorization: Optional[str] = Header(None), q: Optional[str] =
|
||||
where.append("(k.godisnjak_godine IS NULL OR array_length(k.godisnjak_godine,1) IS NULL)")
|
||||
if kategorija and kategorija.strip().lower() == "priority":
|
||||
where.append("(COALESCE(k.pgz_sufinanciran,false) OR (k.godisnjak_godine IS NOT NULL AND array_length(k.godisnjak_godine,1) > 0))")
|
||||
if samo_hns_roster:
|
||||
where.append("EXISTS (SELECT 1 FROM pgz_sport.hns_klub_roster r WHERE r.klub_id = k.id)")
|
||||
sort_col = {"naziv": "v.klub", "savez": "v.savez", "broj_clanova": "v.broj_clanova",
|
||||
"razina": "v.razina", "region": "v.region", "grad": "v.grad", "sport": "v.sport"}.get(sort, "v.klub")
|
||||
order_sql = "DESC" if order.lower() == "desc" else "ASC"
|
||||
|
||||
+19
-7
@@ -4960,19 +4960,31 @@ def proracun_sport(godina: int = None):
|
||||
# POTPORE — by year filter
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@router.get("/potpore/by-year")
|
||||
def potpore_by_year(godina: int = None, q: str = ""):
|
||||
"""Sufinanciranje za specifičnu godinu."""
|
||||
def potpore_by_year(godina: int = None, q: str = "", samo_klubovi: bool = True, davatelj: str = None):
|
||||
"""Sufinanciranje za specifičnu godinu — samo_klubovi=True izbacuje programe/totals/services."""
|
||||
import datetime
|
||||
yr = godina or datetime.date.today().year
|
||||
like = f"%{q}%" if q else "%"
|
||||
rows = db_query("""
|
||||
SELECT korisnik, sport, iznos_eur, vrsta, napomena, izvor, source_url, godina,
|
||||
(SELECT k.id FROM pgz_sport.klubovi k WHERE LOWER(k.naziv) LIKE LOWER('%%'||LEFT(korisnik,20)||'%%') AND k.aktivan=true LIMIT 1) as klub_id
|
||||
|
||||
where = ["godina = %s", "LOWER(COALESCE(korisnik,'')) LIKE LOWER(%s)"]
|
||||
params = [yr, like]
|
||||
|
||||
if samo_klubovi:
|
||||
where.append("(je_klub IS NULL OR je_klub = true)")
|
||||
|
||||
if davatelj == 'rijeka':
|
||||
where.append("izvor ILIKE '%%rijeka.hr%%'")
|
||||
elif davatelj == 'pgz':
|
||||
where.append("izvor ILIKE '%%sport-pgz%%'")
|
||||
|
||||
sql = f"""
|
||||
SELECT id, korisnik, sport, iznos_eur, vrsta, napomena, izvor, source_url, godina, klub_id, je_klub
|
||||
FROM pgz_sport.sufinanciranje_sport
|
||||
WHERE godina = %s AND LOWER(COALESCE(korisnik,'')) LIKE LOWER(%s)
|
||||
WHERE {' AND '.join(where)}
|
||||
ORDER BY iznos_eur DESC NULLS LAST
|
||||
LIMIT 500
|
||||
""", (yr, like))
|
||||
"""
|
||||
rows = db_query(sql, params)
|
||||
total = sum(float(r.get('iznos_eur') or 0) for r in rows)
|
||||
return {"godina": yr, "count": len(rows), "total": total, "results": rows}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""data.gov.hr — Open Data PGZ."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import upsert_facts, DSN, UA
|
||||
import urllib.request
|
||||
import psycopg2
|
||||
|
||||
API = "https://data.gov.hr/api/3/action"
|
||||
|
||||
|
||||
def search(query, rows=50):
|
||||
url = f"{API}/package_search?q={urllib.parse.quote(query)}&rows={rows}"
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
d = json.loads(r.read())
|
||||
return d.get("result", {}).get("results", [])
|
||||
except Exception as e:
|
||||
print(f"search err: {e}")
|
||||
return []
|
||||
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def main():
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
|
||||
queries = [
|
||||
"Primorsko-goranska", "Rijeka", "Opatija", "Crikvenica", "Krk",
|
||||
"Cres", "Lošinj", "Rab", "Delnice", "Bakar", "Kvarner",
|
||||
]
|
||||
|
||||
total_inserted = 0
|
||||
seen = set()
|
||||
|
||||
for q in queries:
|
||||
results = search(q, rows=50)
|
||||
ff = []
|
||||
for pkg in results:
|
||||
pkg_id = pkg.get("id", "")
|
||||
if pkg_id in seen: continue
|
||||
seen.add(pkg_id)
|
||||
|
||||
title = pkg.get("title", "")
|
||||
notes = pkg.get("notes", "")[:600]
|
||||
org = pkg.get("organization", {}).get("title", "")
|
||||
tags = ", ".join([t.get("name", "") for t in pkg.get("tags", [])])
|
||||
|
||||
fact = f"[OpenData] {title} | Org: {org} | {notes} | Tags: {tags}"[:1200]
|
||||
if len(fact) > 50:
|
||||
ff.append({
|
||||
"fact": fact,
|
||||
"url": f"https://data.gov.hr/dataset/{pkg.get('name', '')}",
|
||||
"title": title,
|
||||
})
|
||||
|
||||
n = upsert_facts(conn, ff, source_name="data_gov_hr_pgz",
|
||||
category="opendata_pgz", confidence=0.85)
|
||||
total_inserted += n
|
||||
print(f" query='{q}' -> {len(results)} results, {n} new facts")
|
||||
time.sleep(1)
|
||||
|
||||
conn.close()
|
||||
print(f"=== TOTAL: {total_inserted} ===")
|
||||
print(json.dumps({"queries": len(queries), "total_facts": total_inserted}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Public events i festivali PGZ."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
EVENTS = {
|
||||
"rijeka_karneval": ["https://www.rijecki-karneval.hr/", "https://rijekakarneval.hr/"],
|
||||
"ljeto_kvarner": ["https://www.kvarner-ljeto.hr/"],
|
||||
"rijeka_film_fest": ["https://www.riff.hr/"],
|
||||
"vinski_festival_op": ["https://opatijawine.hr/"],
|
||||
"festival_culture": ["https://www.festival-of-cultures.hr/"],
|
||||
"muzicki_kvarner": ["https://www.kvarnermusic.hr/"],
|
||||
"porto_etno": ["https://www.portoetno.eu/"],
|
||||
"ri_rock": ["https://rockkonferencija.com/"],
|
||||
"fjeshta_lovran": ["https://www.fjeshta.hr/"],
|
||||
"njanje_zvoncari": ["https://halubajskizvoncari.hr/"],
|
||||
"skoljka_festival": ["https://www.kostrena.hr/"],
|
||||
"sumski_film": ["https://www.lifftrijeka.hr/"],
|
||||
"zicfest": ["https://www.zicfest.hr/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=12):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="events_pgz", confidence=0.85)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 30:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in EVENTS.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=10)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"events_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gorski kotar deep — Risnjak, Delnice, Cabar, Lokve."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
GORSKI = {
|
||||
"tz_gorski_kotar": ["https://www.gorskikotar.hr/", "https://www.tz-gorskikotar.hr/"],
|
||||
"np_risnjak": ["https://www.np-risnjak.hr/"],
|
||||
"delnice_info": ["https://delnice.com/"],
|
||||
"cabar_info": ["https://www.cabar.hr/"],
|
||||
"lokvarsko_jezero": ["https://www.lokve.hr/"],
|
||||
"fuzine_info": ["https://www.fuzine.hr/"],
|
||||
"ravnagora_blog": ["https://www.ravnagora.hr/"],
|
||||
"skrad_info": ["https://www.skrad.hr/"],
|
||||
"vrbovsko_info": ["https://www.vrbovsko.hr/"],
|
||||
"spilja_lokvarka": ["https://www.spilja-lokvarka.com/"],
|
||||
"skijanje_platak": ["https://www.platak.hr/"],
|
||||
"snowboard_klub": ["https://www.snowboardklub-rijeka.hr/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=15):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="gorski_kotar_pgz", confidence=0.85)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 40:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in GORSKI.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=12)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"gorski_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Media deep crawl — full pages of local portals."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
MEDIA = {
|
||||
"novilist_rijeka": ["https://www.novilist.hr/rijeka/", "https://www.novilist.hr/regija/"],
|
||||
"rijekadanas_full": ["https://rijekadanas.com/category/rijeka/", "https://rijekadanas.com/category/pgz/"],
|
||||
"rijekain_full": ["https://rijekain.hr/category/rijeka/"],
|
||||
"primorske_full": ["https://primorskenovice.hr/"],
|
||||
"rkc_blog": ["https://www.rkcrijeka.hr/blog/"],
|
||||
"rijeka2020_arhiva": ["https://rijeka2020.eu/category/news/"],
|
||||
"kulturpunkt_ri": ["https://www.kulturpunkt.hr/tag/rijeka"],
|
||||
"5portala_hr_pgz": ["https://www.5portala.hr/regije/primorsko-goranska/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=20):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 300: continue
|
||||
ff = []
|
||||
if title and len(title) > 12:
|
||||
ff.append({"fact": f"[{name}] {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 120:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=f"media_{name}",
|
||||
category="media_pgz_deep", confidence=0.82)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 60:
|
||||
queue.append(link)
|
||||
time.sleep(0.6)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in MEDIA.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=18)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"media_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Naselja PGZ — sela, zaseoci, otocna mjesta."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import chunk_text, upsert_facts, DSN, UA
|
||||
from urllib.parse import urlencode, quote
|
||||
import urllib.request
|
||||
import psycopg2
|
||||
|
||||
API_HR = "https://hr.wikipedia.org/w/api.php"
|
||||
|
||||
|
||||
def wiki_cat_members(cat, limit=200):
|
||||
"""Get pages in a Wikipedia category."""
|
||||
params = {"action":"query","list":"categorymembers","cmtitle":cat,
|
||||
"cmlimit":str(limit),"format":"json"}
|
||||
url = API_HR + "?" + urlencode(params)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
d = json.loads(r.read())
|
||||
return [m["title"] for m in d.get("query", {}).get("categorymembers", [])]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def wiki_extract(title, timeout=15):
|
||||
params = {"action":"query","prop":"extracts","explaintext":"1",
|
||||
"redirects":"1","format":"json","titles":title}
|
||||
url = API_HR + "?" + urlencode(params)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
d = json.loads(r.read())
|
||||
for pid, p in d.get("query", {}).get("pages", {}).items():
|
||||
if pid == "-1": return None
|
||||
return p.get("extract", "")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
CATEGORIES = [
|
||||
"Kategorija:Naselja_u_Primorsko-goranskoj_županiji",
|
||||
"Kategorija:Naselja_u_Hrvatskoj_(otok_Krk)",
|
||||
"Kategorija:Naselja_u_Hrvatskoj_(otok_Cres)",
|
||||
"Kategorija:Naselja_u_Hrvatskoj_(otok_Lošinj)",
|
||||
"Kategorija:Naselja_u_Hrvatskoj_(otok_Rab)",
|
||||
"Kategorija:Gorski_kotar",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
total = 0; pages = 0
|
||||
|
||||
seen = set()
|
||||
for cat in CATEGORIES:
|
||||
members = wiki_cat_members(cat, limit=200)
|
||||
print(f" {cat[:50]:50} {len(members):>3} members")
|
||||
|
||||
for title in members:
|
||||
if title in seen: continue
|
||||
seen.add(title)
|
||||
|
||||
text = wiki_extract(title)
|
||||
if not text or len(text) < 200: continue
|
||||
pages += 1
|
||||
|
||||
facts = [{"fact": c, "url": f"https://hr.wikipedia.org/wiki/{quote(title)}",
|
||||
"title": title}
|
||||
for c in chunk_text(text, 600) if len(c) > 100]
|
||||
n = upsert_facts(conn, facts, source_name="wikipedia_pgz_naselja",
|
||||
category="naselja_pgz", confidence=0.86)
|
||||
total += n
|
||||
time.sleep(0.3)
|
||||
|
||||
conn.close()
|
||||
print(f"=== TOTAL pages={pages} facts={total} ===")
|
||||
print(json.dumps({"pages": pages, "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Real estate + housing PGZ."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
REAL = {
|
||||
"rijeka_najam": ["https://www.rijeka.hr/javnu-najam/"],
|
||||
"stanovanje_pgz": ["https://www.dom-rijeka.hr/"],
|
||||
"katastar_pgz": ["https://geoportal.dgu.hr/"],
|
||||
"uprava_imovine": ["https://www.rijeka.hr/imovinsko-pravna/"],
|
||||
"rijeka_arhitekt": ["https://www.rijeka.hr/arhitektonska/"],
|
||||
"drzavna_imovina": ["https://www.drzavnaimovina.hr/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=8):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="nekretnine_pgz", confidence=0.83)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 25:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in REAL.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=8)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"real_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Otoci PGZ deep — Krk, Cres, Losinj, Rab portali."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
OTOCI = {
|
||||
"krk_info": ["https://www.krk.com/"],
|
||||
"krkonline": ["https://www.krkonline.com/"],
|
||||
"krkinfo_news": ["https://www.krk-info.com/"],
|
||||
"cres_info": ["https://www.cres.info/"],
|
||||
"cres_lapis": ["https://lapis.cres.hr/"],
|
||||
"losinj_info": ["https://www.losinj.info/"],
|
||||
"losinj_centar": ["https://www.muzejmalilosinj.hr/"],
|
||||
"rab_info": ["https://www.rab.com/"],
|
||||
"rab_news": ["https://rabnews.com/"],
|
||||
"susak_info": ["https://www.susakisland.com/"],
|
||||
"ilovik_info": ["https://www.ilovik.eu/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=12):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="otoci_pgz", confidence=0.85)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 30:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in OTOCI.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=12)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"otoci_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Povijesni izvori PGZ — Liburnija, arhivi."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
HISTORY = {
|
||||
"drzavni_arhiv_ri": ["https://www.dari.hr/"],
|
||||
"arhiv_pazin": ["https://www.dapa.hr/"],
|
||||
"muzej_glagoljice": ["https://glagoljica.hr/"],
|
||||
"glagoljaska_alea": ["https://www.aleja-glagoljasa.hr/"],
|
||||
"frankopani": ["https://www.frankopani.eu/"],
|
||||
"trsatske_legende": ["https://www.trsat-svetiste.com/povijest/"],
|
||||
"rijeka_povijest": ["https://rijeka-history.eu/"],
|
||||
"stare_rijeke": ["https://www.stararijeka.com/"],
|
||||
"kvarner_arhiv": ["https://www.kvarnerheritage.eu/"],
|
||||
"muzeji_pgz_arhiv": ["https://www.muzeji-pgz.hr/"],
|
||||
"razno_pomorski": ["https://www.kpu.hr/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=12):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="povijest_pgz", confidence=0.86)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 30:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in HISTORY.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=10)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"hist_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lokalni propisi PGZ — sluzbene novine, statuti."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
PROPISI = {
|
||||
"sluzbene_novine_pgz": ["https://www.sn.pgz.hr/"],
|
||||
"sluzbene_glasnik_rijeka": ["https://www.sluzbene-novine.com/"],
|
||||
"sn_opatija": ["https://www.opatija.hr/sluzbene-novine"],
|
||||
"pgz_dokumenti": ["https://www.pgz.hr/dokumenti"],
|
||||
"pgz_skupstina": ["https://www.pgz.hr/skupstina"],
|
||||
"rijeka_grad_v_savjet":["https://www.rijeka.hr/gradska-uprava/"],
|
||||
"pgz_javnatime": ["https://www.pgz.hr/javna-nabava/"],
|
||||
"rijeka_javna_nabava": ["https://www.rijeka.hr/javna-nabava/"],
|
||||
"narodne_novine_pgz": ["https://narodne-novine.nn.hr/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=15):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="propisi_pgz", confidence=0.88)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 60:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in PROPISI.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=15)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"propisi_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sport infrastruktura PGZ — dvorane, baze, skole."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
INFRA = {
|
||||
"dvorana_zamet": ["https://www.dvoranazamet.hr/"],
|
||||
"stadion_kantrida": ["https://www.kantrida.hr/"],
|
||||
"stadion_rujevica": ["https://www.nk-rijeka.hr/stadion-rujevica/"],
|
||||
"ck_kantrida_pliv": ["https://kantridapool.hr/"],
|
||||
"ri_sport_centar": ["https://www.ri-sport.hr/"],
|
||||
"delta_jumbo": ["https://www.deltajumbo.hr/"],
|
||||
"skolski_sport": ["https://www.hsss.hr/"],
|
||||
"platak_skijanje": ["https://www.platak.hr/"],
|
||||
"rec_velebit": ["https://www.velebit.hr/"],
|
||||
"platak_ski_klub": ["https://www.skiclub-platak.hr/"],
|
||||
"rijeka_marina": ["https://www.aci-marinas.com/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=15):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="sport_infra_pgz", confidence=0.85)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 40:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in INFRA.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=12)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"infra_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sport klubovi PGZ — direktno s web stranica."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
KLUBOVI = {
|
||||
"hnk_rijeka": ["https://www.nk-rijeka.hr/"],
|
||||
"kk_kvarner": ["https://www.kk-kvarner.hr/"],
|
||||
"rk_zamet": ["https://www.rk-zamet.hr/"],
|
||||
"vk_primorje": ["https://www.vkprimorje.hr/"],
|
||||
"ok_rijeka": ["https://www.ok-rijeka.hr/"],
|
||||
"haok_mladost": ["https://www.haok-mladost.hr/"],
|
||||
"abc_rijeka": ["https://www.abc-rijeka.hr/"],
|
||||
"rugby_rijeka": ["https://www.rugbyrijeka.hr/"],
|
||||
"pliva_klub_primorje":["https://www.primorje-aquarius.hr/"],
|
||||
"judo_kvarner": ["https://www.judokvarner.hr/"],
|
||||
"kuglacki_savez_pgz": ["https://www.kuglacki-savez-pgz.hr/"],
|
||||
"tenis_kvarner": ["https://www.tk-kvarner.hr/"],
|
||||
"atletika_rijeka": ["https://www.akrijeka.hr/"],
|
||||
"biciklisticki": ["https://www.bk-rijeka.hr/"],
|
||||
"stoljecesporta": ["https://stoljecesporta.com/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=12):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="sport_klub_pgz", confidence=0.88)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 30:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in KLUBOVI.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=10)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"klub_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Crkve i vjerske institucije PGZ."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
CRKVA = {
|
||||
"rijecka_nadbiskupija":["https://www.ri-nadbiskupija.hr/"],
|
||||
"krcka_biskupija": ["https://www.biskupija-krk.hr/"],
|
||||
"isusovci_rijeka": ["https://isusovci.hr/"],
|
||||
"trsat_svetiste": ["https://trsat-svetiste.com/"],
|
||||
"katedrala_rijeka": ["https://katedrala-rijeka.hr/"],
|
||||
"samostan_kosljun": ["https://www.kosljun.hr/"],
|
||||
"samostan_glavotok": ["https://www.glavotok.hr/"],
|
||||
"katedrala_krk": ["https://www.biskupija-krk.hr/katedrala/"],
|
||||
"crkva_opatija": ["https://www.zupa-opatija.hr/"],
|
||||
"rijecka_eparhija": ["https://www.eparhija-zagrebackoljubljanska.com/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=10):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="vjera_pgz", confidence=0.84)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 25:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in CRKVA.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=10)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"crkva_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Zdravstvo + udruge PGZ."""
|
||||
import sys, json, time
|
||||
sys.path.insert(0, "/opt/pgz-sport/scrapers/harvesters")
|
||||
from _common import (fetch, extract_text, extract_title, chunk_text,
|
||||
upsert_facts, find_internal_links, DSN)
|
||||
from urllib.parse import urlparse
|
||||
import psycopg2
|
||||
|
||||
ZDRAVSTVO = {
|
||||
"kbc_rijeka": ["https://www.kbc-rijeka.hr/"],
|
||||
"thalassotherapia": ["https://thalassotherapia-opatija.hr/"],
|
||||
"klinika_lovran": ["https://www.tnz-lovran.hr/"],
|
||||
"klinika_crikvenica": ["https://www.thalassotherapia-crikvenica.hr/"],
|
||||
"dom_zdravlja_pgz": ["https://www.dom-zdravlja-pgz.hr/"],
|
||||
"zavod_javno_zdravlje":["https://www.zzjzpgz.hr/"],
|
||||
"crveni_kriz_pgz": ["https://www.crveni-kriz-rijeka.hr/"],
|
||||
"hzzo_rijeka": ["https://hzzo.hr/"],
|
||||
"savjetovaliste_ri": ["https://www.zzjzpgz.hr/savjetovaliste/"],
|
||||
"deinstitucionalizacija":["https://www.cczg-rijeka.hr/"],
|
||||
"centar_socijalne": ["https://www.czss.rijeka.hr/"],
|
||||
}
|
||||
|
||||
|
||||
def crawl(name, urls, max_pages=12):
|
||||
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||
visited = set(); queue = list(urls); facts = 0
|
||||
while queue and len(visited) < max_pages:
|
||||
url = queue.pop(0)
|
||||
if url in visited: continue
|
||||
visited.add(url)
|
||||
html, status = fetch(url, timeout=15)
|
||||
if not html or status != 200: continue
|
||||
title = extract_title(html); text = extract_text(html)
|
||||
if not text or len(text) < 200: continue
|
||||
ff = []
|
||||
if title and len(title) > 8:
|
||||
ff.append({"fact": f"{name} - {title}", "url": url, "title": title})
|
||||
for c in chunk_text(text, 800):
|
||||
if len(c) > 100:
|
||||
ff.append({"fact": c, "url": url, "title": title})
|
||||
facts += upsert_facts(conn, ff, source_name=name,
|
||||
category="zdravstvo_pgz", confidence=0.86)
|
||||
base = urlparse(url).hostname
|
||||
for link in find_internal_links(html, url):
|
||||
if link not in visited and (urlparse(link).hostname or "") == base and len(queue) < 40:
|
||||
queue.append(link)
|
||||
time.sleep(0.5)
|
||||
conn.close()
|
||||
return {"name": name, "visited": len(visited), "facts": facts}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for name, urls in ZDRAVSTVO.items():
|
||||
try:
|
||||
r = crawl(name, urls, max_pages=10)
|
||||
print(f" {name:25} {r['visited']:>3}p {r['facts']:>5}f")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" {name:25} FAIL: {str(e)[:60]}")
|
||||
total = sum(r.get("facts", 0) for r in results)
|
||||
print(f"=== TOTAL: {total} ===")
|
||||
print(json.dumps({"zdr_count": len(results), "total_facts": total}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+44
-9
@@ -487,7 +487,18 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
<div id="toast"></div>
|
||||
|
||||
<script>
|
||||
const TOKEN = localStorage.getItem('token') || localStorage.getItem('access_token') || '';
|
||||
// ━━━ AUTH: model after app.html (pgz_access primary, fallbacks for legacy keys) ━━━
|
||||
function getToken(){
|
||||
try {
|
||||
return localStorage.getItem('pgz_access')
|
||||
|| sessionStorage.getItem('pgz_access')
|
||||
|| localStorage.getItem('jwt')
|
||||
|| localStorage.getItem('access_token')
|
||||
|| localStorage.getItem('token')
|
||||
|| '';
|
||||
} catch(e){ return ''; }
|
||||
}
|
||||
let TOKEN = getToken();
|
||||
const API = '/sport/api/v2/crm';
|
||||
const STAGE_LABEL = {
|
||||
prospecting:'Prospecting', qualification:'Qualification', proposal:'Proposal',
|
||||
@@ -495,14 +506,34 @@ const STAGE_LABEL = {
|
||||
};
|
||||
const STAGES = ['prospecting','qualification','proposal','negotiation','closed_won','closed_lost'];
|
||||
|
||||
if (!TOKEN) {
|
||||
location.href = '/sport/login?next=' + encodeURIComponent(location.pathname);
|
||||
}
|
||||
// JWT expiry pre-check + redirect only when truly missing/expired
|
||||
(function checkAuth(){
|
||||
if(!TOKEN){
|
||||
if(!window.__pgz_redirecting && window.__pgz_made_api_call){ window.__pgz_redirecting = true; location.href = '/login?next=' + encodeURIComponent(location.pathname); } else { console.warn('[CRM] no token — login optional'); }
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(atob(TOKEN.split('.')[1]));
|
||||
if(payload.exp && payload.exp * 1000 < Date.now()){
|
||||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token','token'].forEach(k => {
|
||||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||||
});
|
||||
if(!window.__pgz_redirecting){ window.__pgz_redirecting = true; location.href = '/login?reason=expired'; }
|
||||
}
|
||||
} catch(e){ /* not parseable, let server respond */ }
|
||||
})();
|
||||
|
||||
async function api(path, opts={}) {
|
||||
TOKEN = getToken(); // refresh in case of token rotation
|
||||
const headers = {'Authorization':'Bearer '+TOKEN, 'Content-Type':'application/json', ...(opts.headers||{})};
|
||||
const res = await fetch(API+path, {...opts, headers});
|
||||
if (res.status === 401) { location.href='/sport/login'; throw new Error('401'); }
|
||||
if (res.status === 401) {
|
||||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token','token'].forEach(k => {
|
||||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||||
});
|
||||
if(!window.__pgz_redirecting){ window.__pgz_redirecting = true; location.href='/login?reason=unauthorized'; }
|
||||
throw new Error('401');
|
||||
}
|
||||
const txt = await res.text();
|
||||
let data; try { data = JSON.parse(txt); } catch { data = txt; }
|
||||
if (!res.ok) {
|
||||
@@ -548,16 +579,20 @@ function switchTab(name) {
|
||||
// ────── /me ──────
|
||||
async function loadMe() {
|
||||
try {
|
||||
const me = await fetch('/sport/api/v2/me', {headers:{'Authorization':'Bearer '+TOKEN}}).then(r=>r.json());
|
||||
const tok = getToken();
|
||||
const me = await fetch('/sport/api/v2/auth/me', {headers:{'Authorization':'Bearer '+tok}}).then(r=>r.json());
|
||||
document.getElementById('me').textContent = (me.email || me.full_name || 'user');
|
||||
} catch { document.getElementById('me').textContent='?'; }
|
||||
}
|
||||
|
||||
document.getElementById('logout').addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+TOKEN}}); } catch {}
|
||||
localStorage.removeItem('token'); localStorage.removeItem('access_token');
|
||||
location.href='/sport/login';
|
||||
const tok = getToken();
|
||||
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+tok}}); } catch {}
|
||||
['pgz_access','pgz_refresh','pgz_user','app-role','jwt','access_token','refresh_token','pgz_session_id','token'].forEach(k => {
|
||||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||||
});
|
||||
location.href='/login';
|
||||
});
|
||||
|
||||
// ────── Pipeline & Dashboard ──────
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
<div class="pgz-sb-h">
|
||||
<div class="pgz-mark">PGŽ</div>
|
||||
<div class="pgz-htxt">
|
||||
<div class="pgz-logo">PGŽ <span class="g">SPORT</span></div>
|
||||
<a href="/" class="pgz-logo" style="text-decoration:none;color:inherit;cursor:pointer" title="Početna">PGŽ <span class="g">SPORT</span></a>
|
||||
<div class="pgz-sub">Odjel za sport</div>
|
||||
</div>
|
||||
<div class="pgz-sb-toggle" onclick="PGZSidebar.toggle()" title="Skupi/raširi">←</div>
|
||||
|
||||
+264
-48
@@ -134,6 +134,17 @@ table tbody tr.no-click:hover{background:transparent}
|
||||
.btn.primary{background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));border-color:transparent;color:#fff}
|
||||
.btn.primary:hover{filter:brightness(1.1)}
|
||||
|
||||
/* BUG-E (2026-05-05) — filter-bar above section toolbar */
|
||||
.bugE-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 10px 0;padding:10px 12px;background:linear-gradient(180deg,rgba(20,30,48,.6),rgba(15,22,36,.5));border:1px solid var(--rim);border-left:3px solid var(--pgz-gold);border-radius:6px}
|
||||
.bugE-bar .bugE-lbl{font-size:10px;color:var(--pgz-gold);font-weight:800;letter-spacing:1.4px}
|
||||
.bugE-bar label{font-size:11px;color:var(--t1);display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
|
||||
.bugE-bar label small{color:var(--t2);font-weight:400}
|
||||
.bugE-bar input[type=checkbox]{accent-color:var(--pgz-gold)}
|
||||
.bugE-bar input[type=number]{background:var(--bg2);border:1px solid var(--rim);border-radius:4px;padding:5px 8px;color:var(--t1);font-size:12px}
|
||||
.bugE-bar .btn{padding:6px 12px;font-size:11px}
|
||||
.bugE-bar .bugE-cnt{margin-left:auto;font-size:11px;color:var(--t2);font-weight:600;letter-spacing:.5px}
|
||||
.bugE-bar .bugE-cnt strong{color:var(--pgz-gold)}
|
||||
|
||||
.toggle{display:inline-flex;background:var(--bg2);border:1px solid var(--rim);border-radius:5px;overflow:hidden}
|
||||
.toggle button{background:transparent;border:0;padding:6px 12px;color:var(--t2);font-size:11px;font-weight:600;cursor:pointer}
|
||||
.toggle button.active{background:var(--pgz-blue);color:#fff}
|
||||
@@ -161,8 +172,8 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
||||
.tag.rd{background:var(--red);color:#fff}
|
||||
.tag.am{background:var(--amber);color:var(--bg0)}
|
||||
|
||||
#panel{position:fixed;top:0;right:-620px;width:600px;max-width:96vw;height:100vh;background:var(--bg1);border-left:1px solid var(--rim);z-index:200;transition:right .25s ease;display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
|
||||
#panel.open{right:0}
|
||||
#panel{position:fixed;top:0;right:0;width:600px;max-width:96vw;height:100vh;background:var(--bg1);border-left:1px solid var(--rim);z-index:200;transform:translateX(100%);visibility:hidden;transition:transform .25s ease,visibility 0s linear .25s;display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
|
||||
#panel.open{transform:translateX(0);visibility:visible;transition:transform .25s ease,visibility 0s linear 0s}
|
||||
#panel-hdr{padding:14px 16px;border-bottom:1px solid var(--rim);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:var(--bg2)}
|
||||
#panel-hdr-t{font-size:14px;font-weight:700;color:var(--t0)}
|
||||
#panel-x{cursor:pointer;font-size:22px;color:var(--t4);width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:5px;transition:all .15s}
|
||||
@@ -284,7 +295,7 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
||||
.table-container, .card { overflow-x: auto; }
|
||||
|
||||
/* Drill-down panel full-width */
|
||||
#panel { width: 100vw !important; max-width: 100vw !important; right: -100vw !important; }
|
||||
#panel { width: 100vw !important; max-width: 100vw !important; right: 0 !important; }
|
||||
#panel.open { right: 0 !important; }
|
||||
|
||||
/* Buttons */
|
||||
@@ -314,6 +325,12 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
||||
/* HNS karijera tabela full-width */
|
||||
#panel table, #dpanel table { width: 100%; font-size: 12px; }
|
||||
#panel .hns-stats td { padding: 4px 6px; }
|
||||
|
||||
/* PANEL FORCE HIDE (CRISIS V7) — uvijek skriven dok nije .open */
|
||||
#panel:not(.open) { right: -100vw !important; transform: translateX(0) !important; }
|
||||
#panel.open { right: 0 !important; }
|
||||
#panel-overlay:not(.open) { display: none !important; }
|
||||
#panel-overlay.open { display: block !important; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="dashboard"></script>
|
||||
@@ -406,6 +423,83 @@ const _state = {section:'dashboard', viewSavezi:'card', viewKlubovi:'card', view
|
||||
const _sort = {savezi:null, klubovi:null, sportasi:null, objekti:null, manifestacije:null, financije:null};
|
||||
let _proracunChart=null, _financijeChart=null;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// BUG-E (2026-05-05) — explicit filter-bar state per section
|
||||
// Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one)
|
||||
// Defaults match constitution: financirani=true + u-godišnjaku=true.
|
||||
// User can uncheck either checkbox to broaden the result set.
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
const _filters = {
|
||||
klubovi: { financirani: true, godisnjak: true, hns_roster: false, total: 0 },
|
||||
sportasi: { priority: true, hns_profil: false, godina_od: null, godina_do: null, total: 0 },
|
||||
savezi: { financirani: true, total: 0 }
|
||||
};
|
||||
function _filtersDefaults(sec){
|
||||
if(sec==='klubovi') return { financirani:true, godisnjak:true, hns_roster:false };
|
||||
if(sec==='sportasi') return { priority:true, hns_profil:false, godina_od:null, godina_do:null };
|
||||
if(sec==='savezi') return { financirani:true };
|
||||
return {};
|
||||
}
|
||||
function _filtersReset(sec){
|
||||
Object.assign(_filters[sec], _filtersDefaults(sec));
|
||||
_filtersApply(sec);
|
||||
}
|
||||
function _filtersApply(sec){
|
||||
if(sec==='klubovi') { _cache.klubovi = null; loadKlubovi(); }
|
||||
if(sec==='sportasi') { _cache.clanovi = null; loadSportasi(); }
|
||||
if(sec==='savezi') { _cache.savezi = null; loadSavezi(); }
|
||||
}
|
||||
function _filtersBar(sec){
|
||||
// Returns HTML for the BUG-E filter-bar above the existing toolbar.
|
||||
const f = _filters[sec] || {};
|
||||
const cnt = '<span class="bugE-cnt" id="bugE-cnt-'+sec+'">Prikazano: '
|
||||
+ (f.shown||0) + ' od ' + (f.total||0) + '</span>';
|
||||
if(sec==='klubovi'){
|
||||
return `
|
||||
<div class="bugE-bar">
|
||||
<span class="bugE-lbl">FILTER:</span>
|
||||
<label><input type="checkbox" ${f.financirani?'checked':''} onchange="_filters.klubovi.financirani=this.checked"> Samo financirani <small>(PGŽ + RSS + Grad Rijeka)</small></label>
|
||||
<label><input type="checkbox" ${f.godisnjak?'checked':''} onchange="_filters.klubovi.godisnjak=this.checked"> U godišnjaku</label>
|
||||
<label><input type="checkbox" ${f.hns_roster?'checked':''} onchange="_filters.klubovi.hns_roster=this.checked"> Ima HNS roster</label>
|
||||
<button class="btn primary" onclick="_filtersApply('klubovi')">Primijeni</button>
|
||||
<button class="btn" onclick="_filtersReset('klubovi')">Reset</button>
|
||||
${cnt}
|
||||
</div>`;
|
||||
}
|
||||
if(sec==='sportasi'){
|
||||
return `
|
||||
<div class="bugE-bar">
|
||||
<span class="bugE-lbl">FILTER:</span>
|
||||
<label><input type="checkbox" ${f.priority?'checked':''} onchange="_filters.sportasi.priority=this.checked"> Samo iz priority kluba</label>
|
||||
<label><input type="checkbox" ${f.hns_profil?'checked':''} onchange="_filters.sportasi.hns_profil=this.checked"> Ima HNS profil</label>
|
||||
<label class="bugE-range">Godina rođ. od: <input type="number" min="1900" max="2030" value="${f.godina_od||''}" placeholder="—" onchange="_filters.sportasi.godina_od=this.value?parseInt(this.value,10):null" style="width:90px"></label>
|
||||
<label class="bugE-range">do: <input type="number" min="1900" max="2030" value="${f.godina_do||''}" placeholder="—" onchange="_filters.sportasi.godina_do=this.value?parseInt(this.value,10):null" style="width:90px"></label>
|
||||
<button class="btn primary" onclick="_filtersApply('sportasi')">Primijeni</button>
|
||||
<button class="btn" onclick="_filtersReset('sportasi')">Reset</button>
|
||||
${cnt}
|
||||
</div>`;
|
||||
}
|
||||
if(sec==='savezi'){
|
||||
return `
|
||||
<div class="bugE-bar">
|
||||
<span class="bugE-lbl">FILTER:</span>
|
||||
<label><input type="checkbox" ${f.financirani?'checked':''} onchange="_filters.savezi.financirani=this.checked"> Samo financirani</label>
|
||||
<button class="btn primary" onclick="_filtersApply('savezi')">Primijeni</button>
|
||||
<button class="btn" onclick="_filtersReset('savezi')">Reset</button>
|
||||
${cnt}
|
||||
</div>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function _filtersUpdateCount(sec, shown){
|
||||
_filters[sec].shown = shown;
|
||||
const el = document.getElementById('bugE-cnt-'+sec);
|
||||
if(el) el.textContent = 'Prikazano: '+shown+' od '+(_filters[sec].total||shown);
|
||||
}
|
||||
window._filters = _filters;
|
||||
window._filtersReset = _filtersReset;
|
||||
window._filtersApply = _filtersApply;
|
||||
|
||||
// === PGŽ priority filter (SUB6) — global helper, works across Klubovi/Savezi/Sportaši ===
|
||||
window._pgz_filter_priority = window._pgz_filter_priority || false;
|
||||
window.togglePGZFilter = function(section){
|
||||
@@ -1240,13 +1334,16 @@ async function loadSavezi(){
|
||||
const root = $('#pg-savezi');
|
||||
if(!_cache.savezi){
|
||||
root.innerHTML = '<div class="loading">Učitavanje saveza…</div>';
|
||||
// PGŽ filter: switch to v2 priority-sort endpoint (only=true returns just PGŽ-relevant savezi)
|
||||
const url = window._pgz_filter_priority
|
||||
// BUG-E (2026-05-05): explicit filter — when financirani=true → priority-sort?only=true
|
||||
const f = _filters.savezi;
|
||||
const useOnly = f.financirani || window._pgz_filter_priority;
|
||||
const url = useOnly
|
||||
? '/v2/savezi/priority-sort?only=true&limit=500'
|
||||
: '/savezi?limit=250';
|
||||
: '/v2/savezi/priority-sort?only=false&limit=500';
|
||||
const d = await api(url);
|
||||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
_cache.savezi = d.rows || [];
|
||||
_filters.savezi.total = (d.rows||[]).length;
|
||||
}
|
||||
renderSaveziShell();
|
||||
applySaveziFilter();
|
||||
@@ -1255,6 +1352,7 @@ function renderSaveziShell(){
|
||||
const root = $('#pg-savezi');
|
||||
const sports = Array.from(new Set((_cache.savezi||[]).map(s=>s.sport).filter(Boolean))).sort();
|
||||
root.innerHTML = `
|
||||
${_filtersBar('savezi')}
|
||||
<div class="toolbar">
|
||||
<input type="search" id="sav-q" placeholder="🔍 Pretraži savez…">
|
||||
<select id="sav-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
|
||||
@@ -1376,7 +1474,7 @@ async function openSavez(id){
|
||||
${klubovi.length ? `<div style="overflow-x:auto;max-height:400px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Klub</th><th>Razina</th><th>Grad</th></tr></thead>
|
||||
<tbody>${klubovi.slice(0,100).map(k => `
|
||||
<tr onclick="closePanel();setTimeout(()=>openKlub(${k.id}),250)">
|
||||
<tr onclick="panelDrill(openKlub, ${k.id})">
|
||||
<td>${esc(k.klub||k.sport||'(bez naziva)')}${k.nositelj_kvalitete?' <span class="tag gd">N.K.</span>':''}</td>
|
||||
<td>${txt(k.razina,'')}</td>
|
||||
<td>${txt(k.grad)}</td>
|
||||
@@ -1397,14 +1495,27 @@ async function loadKlubovi(){
|
||||
const root = $('#pg-klubovi');
|
||||
if(!_cache.klubovi){
|
||||
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
|
||||
// /api/klubovi already returns priority/financiran/godisnjak flags.
|
||||
// When PGŽ filter is on, ask backend to only return priority klubs.
|
||||
const url = window._pgz_filter_priority
|
||||
? '/klubovi?kategorija=priority&limit=2500'
|
||||
: '/klubovi?limit=2500';
|
||||
const d = await api(url);
|
||||
// BUG-E (2026-05-05): build /api/klubovi URL from explicit _filters.klubovi state.
|
||||
// Defaults: financirani=true + godisnjak=true. When BOTH off → load all.
|
||||
const f = _filters.klubovi;
|
||||
const qs = new URLSearchParams();
|
||||
qs.set('limit','2500');
|
||||
qs.set('sort','financiran'); qs.set('order','desc'); // sort by potpore DESC (financiran flag)
|
||||
// financirani + godisnjak combined with kategorija=priority logic:
|
||||
if(f.financirani && f.godisnjak){
|
||||
qs.set('kategorija','priority'); // OR semantics → priority = financiran OR godišnjak
|
||||
} else if(f.financirani){
|
||||
qs.set('financiran','true');
|
||||
} else if(f.godisnjak){
|
||||
qs.set('godisnjak','true');
|
||||
}
|
||||
if(f.hns_roster) qs.set('samo_hns_roster','true');
|
||||
// Legacy global toggle still respected (if user clicks the old PGŽ button).
|
||||
if(window._pgz_filter_priority && !qs.has('kategorija')) qs.set('kategorija','priority');
|
||||
const d = await api('/klubovi?'+qs.toString());
|
||||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
_cache.klubovi = d.rows || [];
|
||||
_filters.klubovi.total = (d.rows||[]).length;
|
||||
}
|
||||
renderKluboviShell();
|
||||
applyKluboviFilter();
|
||||
@@ -1414,6 +1525,7 @@ function renderKluboviShell(){
|
||||
const sports = Array.from(new Set((_cache.klubovi||[]).map(k=>k.sport).filter(Boolean))).sort().slice(0,80);
|
||||
const grads = Array.from(new Set((_cache.klubovi||[]).map(k=>k.grad).filter(Boolean))).sort();
|
||||
root.innerHTML = `
|
||||
${_filtersBar('klubovi')}
|
||||
<div class="toolbar">
|
||||
<input type="search" id="kl-q" placeholder="🔍 Pretraži klub…">
|
||||
<select id="kl-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
|
||||
@@ -1464,6 +1576,8 @@ function applyKluboviFilter(){
|
||||
else if(kat==='financiran') rows = rows.filter(k => k.financiran);
|
||||
else if(kat==='godisnjak') rows = rows.filter(k => k.godisnjak);
|
||||
if(_sort.klubovi) rows = sortRows(rows, _sort.klubovi.key, _sort.klubovi.dir);
|
||||
// BUG-E: live count + total
|
||||
_filtersUpdateCount('klubovi', rows.length);
|
||||
$('#kl-cnt').textContent = rows.length+' klubova';
|
||||
const top = rows.slice(0, 300);
|
||||
$('#kl-out').innerHTML = _state.viewKlubovi==='card' ? renderKluboviGrid(top) : renderKluboviTable(top);
|
||||
@@ -1638,7 +1752,7 @@ async function openKlub(id){
|
||||
${clanovi.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Sportaš</th><th>Spol</th><th>Pozicija</th><th>Kategorija</th><th>Tagovi</th></tr></thead>
|
||||
<tbody>${clanovi.map(c => `
|
||||
<tr onclick="closePanel();setTimeout(()=>openSportas(${c.id}),250)">
|
||||
<tr onclick="panelDrill(openSportas, ${c.id})">
|
||||
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||||
<td>${txt(c.spol)}</td>
|
||||
<td>${txt(c.pozicija)}</td>
|
||||
@@ -1664,7 +1778,7 @@ async function openKlub(id){
|
||||
<details style="margin-bottom:8px" ${groups[kat].length<=12?'open':''}>
|
||||
<summary style="cursor:pointer;padding:8px;background:rgba(255,255,255,.04);border-radius:6px"><b>${esc(kat)}</b> · ${groups[kat].length} igrač${groups[kat].length===1?'':'a'}</summary>
|
||||
<table style="margin-top:6px"><tbody>${groups[kat].map(c => `
|
||||
<tr onclick="closePanel();setTimeout(()=>openSportas(${c.id}),250)">
|
||||
<tr onclick="panelDrill(openSportas, ${c.id})">
|
||||
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||||
<td>${txt(c.spol)}</td>
|
||||
<td>${txt(c.pozicija)}</td>
|
||||
@@ -2030,7 +2144,7 @@ async function openSportas(id){
|
||||
<div class="pp-meta">
|
||||
${d.sport?'<a class="link-chip" onclick="filterSportasiBy("sport","'+esc(d.sport)+'")">'+esc(d.sport)+'</a>':'—'} ·
|
||||
${txt(d.pozicija,'')} ·
|
||||
${d.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+d.klub_id+'),250)"><b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b></a>' : '<b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b>'}
|
||||
${d.klub_id ? '<a class="link-chip" onclick="panelDrill(openKlub,'+d.klub_id+')"><b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b></a>' : '<b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b>'}
|
||||
</div>
|
||||
<div class="pp-meta">
|
||||
${dob ? '<a class="link-chip" onclick="filterSportasiByYear("'+esc((dob||'').slice(0,4))+'")">📅 '+fmtDate(dob)+'</a>' : '📅 —'}
|
||||
@@ -2050,11 +2164,8 @@ async function openSportas(id){
|
||||
${d.pozicija?'<span class="pp-bio-chip"><b>'+esc(d.pozicija)+'</b></span>':''}
|
||||
${d.broj_dresa?'<span class="pp-bio-chip">#<b>'+esc(d.broj_dresa)+'</b></span>':''}
|
||||
</div>
|
||||
<div class="pp-links">
|
||||
${hnsUrl?'<a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS profil ↗</a>':''}
|
||||
<a class="pp-link gg" href="${esc(ggUrl)}" target="_blank" rel="noopener">🔍 Google</a>
|
||||
<a class="pp-link wiki" href="${esc(wikiUrl)}" target="_blank" rel="noopener">📖 Wikipedia</a>
|
||||
</div>
|
||||
<!-- BUG-F (2026-05-05): external links moved to dedicated 🔗 Linkovi tab -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2067,14 +2178,15 @@ async function openSportas(id){
|
||||
<div class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></div>
|
||||
</div>
|
||||
|
||||
<!-- HNS-3 (2026-05-05) — 3 explicit tabs: HNS Karijera / Utakmice (last 30) / Profil -->
|
||||
<!-- BUG-F (2026-05-05) — 4 explicit tabs: Profil / HNS Karijera / Utakmice (last 30) / Linkovi -->
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.length})</div>
|
||||
<div class="tab active" onclick="switchPlayerTab(this,'p-prof')">👤 Profil</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">📅 Utakmice (poslj. ${Math.min(utakmice.length,30)})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-prof')">👤 Profil</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-link')">🔗 Linkovi</div>
|
||||
</div>
|
||||
|
||||
<div id="p-sez" class="ptab">
|
||||
<div id="p-sez" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">🏆 HNS Karijera <span class="cnt">${sezone.length} sezon${sezone.length===1?'a':(sezone.length<5&&sezone.length>1?'e':'a')}</span></div>
|
||||
${sezone.length ? `<div style="overflow-x:auto"><table>
|
||||
<thead><tr><th>Sezona</th><th>Klub</th><th>Natjecanje</th><th class="num">Nastupi</th><th class="num">Golovi</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||
@@ -2124,7 +2236,7 @@ async function openSportas(id){
|
||||
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
||||
</div>
|
||||
|
||||
<div id="p-prof" class="ptab" style="display:none">
|
||||
<div id="p-prof" class="ptab">
|
||||
<div class="pp-section-h">👤 Profil <span class="cnt">${esc(d.ime||'')} ${esc(d.prezime||'')}</span></div>
|
||||
|
||||
<!-- Top: name + dres + active club + HNS deep link -->
|
||||
@@ -2136,7 +2248,7 @@ async function openSportas(id){
|
||||
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · '+esc(d.mjesto_rodjenja||d.mjesto_rodenja):''}
|
||||
</div>
|
||||
<div class="prof-club">
|
||||
${d.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+d.klub_id+'),250)">🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')+'</a>' : '🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')}
|
||||
${d.klub_id ? '<a class="link-chip" onclick="panelDrill(openKlub,'+d.klub_id+')">🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')+'</a>' : '🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')}
|
||||
${d.aktivan?'<span class="tag gr" style="margin-left:8px">AKTIVAN</span>':'<span class="tag rd" style="margin-left:8px">NEAKTIVAN</span>'}
|
||||
</div>
|
||||
${hnsUrl?'<div style="margin-top:10px"><a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS Semafor profil ↗</a></div>':''}
|
||||
@@ -2169,7 +2281,7 @@ async function openSportas(id){
|
||||
<tr class="no-click">
|
||||
<td><b>${esc(k.sezona||'—')}</b></td>
|
||||
<td><span class="tag b">${esc(k.kategorija||'—')}</span></td>
|
||||
<td>${k.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+k.klub_id+'),250)">'+esc(k.klub_naziv||('Klub #'+k.klub_id))+'</a>' : (k.klub_naziv?esc(k.klub_naziv):'—')}</td>
|
||||
<td>${k.klub_id ? '<a class="link-chip" onclick="panelDrill(openKlub,'+k.klub_id+')">'+esc(k.klub_naziv||('Klub #'+k.klub_id))+'</a>' : (k.klub_naziv?esc(k.klub_naziv):'—')}</td>
|
||||
<td>${esc(k.source||'—')}</td>
|
||||
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
||||
</tr>`).join('')}
|
||||
@@ -2196,6 +2308,19 @@ async function openSportas(id){
|
||||
</table></div>` : ''}
|
||||
</div>
|
||||
|
||||
<!-- BUG-F (2026-05-05) — 🔗 Linkovi tab: external profile lookups -->
|
||||
<div id="p-link" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">🔗 Linkovi <span class="cnt">${esc(fullName||'sportaš')}</span></div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:8px">
|
||||
${hnsUrl
|
||||
? `<a class="pp-link hns" href="${esc(hnsUrl)}" target="_blank" rel="noopener" style="padding:18px;font-size:14px;justify-content:space-between"><span>⚽ HNS Semafor profil</span><span style="font-family:var(--mono);color:var(--t3);font-size:11px">#${esc(hnsId||'')} →</span></a>`
|
||||
: `<div class="pp-link" style="padding:18px;font-size:14px;opacity:.5;cursor:not-allowed">⚽ HNS profil <span style="font-size:11px;color:var(--t3);margin-left:auto">nije povezan</span></div>`}
|
||||
<a class="pp-link gg" href="${esc(ggUrl)}" target="_blank" rel="noopener" style="padding:18px;font-size:14px;justify-content:space-between"><span>🔍 Google pretraga</span><span style="color:var(--t3);font-size:11px">→</span></a>
|
||||
<a class="pp-link wiki" href="${esc(wikiUrl)}" target="_blank" rel="noopener" style="padding:18px;font-size:14px;justify-content:space-between"><span>📖 Wikipedia</span><span style="color:var(--t3);font-size:11px">→</span></a>
|
||||
</div>
|
||||
${hnsId ? `<div style="margin-top:14px;padding:10px 12px;background:var(--bg2);border:1px solid var(--rim);border-radius:5px;font-size:11.5px;color:var(--t2);font-family:var(--mono)">HNS ID: <b style="color:var(--pgz-gold)">${esc(hnsId)}</b> · slug: <span style="color:var(--t1)">${esc(slug||'—')}</span></div>` : ''}
|
||||
</div>
|
||||
|
||||
${enrichBlock('sportas', d.id)}
|
||||
`;
|
||||
openPanel('Sportaš · '+(d.ime||'')+' '+(d.prezime||''), html);
|
||||
@@ -3315,8 +3440,8 @@ function renderAlertPanel(a){
|
||||
<div class="card-h"><div class="card-t">🔗 Povezani entiteti</div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
${a.klub_id ? '<tr onclick="closePanel();setTimeout(()=>openKlub('+a.klub_id+'),250)"><td><span class="tag b">Klub</span></td><td><b>Klub #'+a.klub_id+'</b></td><td>Klikni za detalje →</td></tr>' : ''}
|
||||
${a.clan_id ? '<tr onclick="closePanel();setTimeout(()=>openSportas('+a.clan_id+'),250)"><td><span class="tag b">Sportaš</span></td><td><b>Sportaš #'+a.clan_id+'</b></td><td>Klikni za profil →</td></tr>' : ''}
|
||||
${a.klub_id ? '<tr onclick="panelDrill(openKlub,'+a.klub_id+')"><td><span class="tag b">Klub</span></td><td><b>Klub #'+a.klub_id+'</b></td><td>Klikni za detalje →</td></tr>' : ''}
|
||||
${a.clan_id ? '<tr onclick="panelDrill(openSportas,'+a.clan_id+')"><td><span class="tag b">Sportaš</span></td><td><b>Sportaš #'+a.clan_id+'</b></td><td>Klikni za profil →</td></tr>' : ''}
|
||||
</tbody>
|
||||
</table>
|
||||
${(!a.klub_id && !a.clan_id) ? '<div class="empty" style="padding:14px">Nema povezanih entiteta u alarmu</div>' : ''}
|
||||
@@ -3455,32 +3580,123 @@ window.toggleSportasHNS = function(){
|
||||
if(typeof loadSportasi === 'function') loadSportasi();
|
||||
};
|
||||
|
||||
// PANEL HISTORY STACK + NATRAG (CRISIS V7)
|
||||
// PANEL HISTORY STACK + NATRAG (CRISIS V7 / BUG-B opener-based)
|
||||
// Each entry: {opener: <fn name>, args: [...]} — panelBack re-runs the previous opener.
|
||||
window._panelHistory = [];
|
||||
window._panelDrilling = false; // suppress root-clear while drilling
|
||||
window._panelSuppressPush = false; // suppress re-push when re-running for back nav
|
||||
window._panelOpenerMap = {}; // name -> fn
|
||||
|
||||
window.pushPanelState = function(title, htmlContent, callback){
|
||||
// callback may be null — to render this state again later
|
||||
window._panelHistory.push({title, htmlContent, callback});
|
||||
const back = document.getElementById('panel-back');
|
||||
if(back) back.style.display = window._panelHistory.length > 1 ? 'inline-flex' : 'none';
|
||||
window._registerOpener = function(fn){
|
||||
if(typeof fn !== 'function' || !fn.name) return null;
|
||||
window._panelOpenerMap[fn.name] = fn;
|
||||
return fn.name;
|
||||
};
|
||||
|
||||
window._updateBackBtn = function(){
|
||||
const back = document.getElementById('panel-back');
|
||||
if(back) back.style.display = (window._panelHistory.length > 1) ? 'inline-flex' : 'none';
|
||||
};
|
||||
|
||||
// Push opener+args; record in browser history too (for browser back button)
|
||||
window.pushPanelState = function(opener, args){
|
||||
if(window._panelSuppressPush) return;
|
||||
const name = (typeof opener === 'function') ? window._registerOpener(opener) : (typeof opener === 'string' ? opener : null);
|
||||
if(!name) return;
|
||||
const top = window._panelHistory[window._panelHistory.length - 1];
|
||||
const sig = name + ':' + JSON.stringify(args || []);
|
||||
if(top && (top.opener + ':' + JSON.stringify(top.args || [])) === sig) return;
|
||||
window._panelHistory.push({opener: name, args: args || []});
|
||||
try { history.pushState({pgzPanel: true, depth: window._panelHistory.length}, '', location.href); } catch(e) {}
|
||||
window._updateBackBtn();
|
||||
};
|
||||
|
||||
// panelDrill — push state then run opener (no panel close in between)
|
||||
window.panelDrill = function(opener, ...args){
|
||||
window._panelDrilling = true;
|
||||
try {
|
||||
window.pushPanelState(opener, args);
|
||||
const r = opener.apply(null, args);
|
||||
if(r && typeof r.then === 'function') return r.finally(() => { window._panelDrilling = false; });
|
||||
return r;
|
||||
} finally {
|
||||
window._panelDrilling = false;
|
||||
}
|
||||
};
|
||||
|
||||
// panelOpen — root entry: clear history then drill
|
||||
window.panelOpen = function(opener, ...args){
|
||||
window._panelHistory = [];
|
||||
return window.panelDrill(opener, ...args);
|
||||
};
|
||||
|
||||
// panelBack — pop current, re-run previous opener
|
||||
window.panelBack = function(){
|
||||
if(window._panelHistory.length <= 1){ closePanel(); return; }
|
||||
// Remove current state
|
||||
if(window._panelHistory.length <= 1){ window.closePanel(); return; }
|
||||
window._panelHistory.pop();
|
||||
// Restore previous
|
||||
const prev = window._panelHistory[window._panelHistory.length - 1];
|
||||
const t = document.getElementById('panel-hdr-t');
|
||||
const b = document.getElementById('panel-body');
|
||||
if(t) t.textContent = prev.title;
|
||||
if(b) b.innerHTML = prev.htmlContent;
|
||||
if(typeof prev.callback === 'function') prev.callback();
|
||||
const back = document.getElementById('panel-back');
|
||||
if(back) back.style.display = window._panelHistory.length > 1 ? 'inline-flex' : 'none';
|
||||
const fn = window._panelOpenerMap[prev.opener];
|
||||
if(typeof fn !== 'function'){ window.closePanel(); return; }
|
||||
window._panelSuppressPush = true;
|
||||
window._panelDrilling = true;
|
||||
try { fn.apply(null, prev.args || []); } catch(e) { console.error('panelBack', e); }
|
||||
setTimeout(() => {
|
||||
window._panelSuppressPush = false;
|
||||
window._panelDrilling = false;
|
||||
window._updateBackBtn();
|
||||
}, 60);
|
||||
window._updateBackBtn();
|
||||
};
|
||||
|
||||
// Override closePanel — clear history
|
||||
// Browser back button → mirror panelBack while panel is open
|
||||
window.addEventListener('popstate', function(){
|
||||
const panel = document.getElementById('panel');
|
||||
if(!panel || !panel.classList.contains('open')) return;
|
||||
if(window._panelHistory.length > 1){
|
||||
window._panelSuppressPush = true;
|
||||
window._panelDrilling = true;
|
||||
window._panelHistory.pop();
|
||||
const prev = window._panelHistory[window._panelHistory.length - 1];
|
||||
const fn = window._panelOpenerMap[prev.opener];
|
||||
if(typeof fn === 'function') fn.apply(null, prev.args || []);
|
||||
setTimeout(() => {
|
||||
window._panelSuppressPush = false;
|
||||
window._panelDrilling = false;
|
||||
window._updateBackBtn();
|
||||
}, 60);
|
||||
window._updateBackBtn();
|
||||
} else {
|
||||
window.closePanel();
|
||||
}
|
||||
});
|
||||
|
||||
// Wrap root open* functions: a non-drill call clears history and registers itself as root.
|
||||
window._wrapOpener = function(name){
|
||||
const orig = window[name];
|
||||
if(typeof orig !== 'function' || orig.__pgzWrapped) return;
|
||||
window._registerOpener(orig);
|
||||
const wrapped = function(...args){
|
||||
if(!window._panelDrilling && !window._panelSuppressPush){
|
||||
window._panelHistory = [];
|
||||
window._panelHistory.push({opener: name, args});
|
||||
try { history.pushState({pgzPanel: true, depth: 1}, '', location.href); } catch(e) {}
|
||||
window._updateBackBtn();
|
||||
}
|
||||
return orig.apply(this, args);
|
||||
};
|
||||
wrapped.__pgzWrapped = true;
|
||||
try { Object.defineProperty(wrapped, 'name', {value: name, configurable: true}); } catch(e) {}
|
||||
window[name] = wrapped;
|
||||
window._panelOpenerMap[name] = wrapped;
|
||||
};
|
||||
|
||||
// Wrap all known root openers (idempotent)
|
||||
['openSavez','openKlub','openSportas','openObjekt','openManif',
|
||||
'openPrimateljDetail','openProracunDrill','openSavezByName',
|
||||
'openMrezaNode','openForensicDetail','openOIB']
|
||||
.forEach(function(n){ try { window._wrapOpener(n); } catch(e) {} });
|
||||
|
||||
// Override closePanel — X button always returns to root: clear history & back btn
|
||||
window._origClosePanel = window.closePanel;
|
||||
window.closePanel = function(){
|
||||
window._panelHistory = [];
|
||||
@@ -3489,7 +3705,7 @@ window.closePanel = function(){
|
||||
const p = document.getElementById('panel');
|
||||
if(p) p.classList.remove('open');
|
||||
const ov = document.getElementById('panel-overlay');
|
||||
if(ov) ov.style.display = 'none';
|
||||
if(ov){ ov.classList.remove('open'); ov.style.removeProperty('display'); }
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user