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}
|
return {**savez, "klubovi": klubovi, "statistika": statistika, "manifestacije": manifestacije}
|
||||||
|
|
||||||
# ==================== KLUBOVI ====================
|
# ==================== 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")
|
@app.get("/api/klubovi")
|
||||||
def list_klubovi(authorization: Optional[str] = Header(None), q: Optional[str] = None, savez_id: Optional[int] = None,
|
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,
|
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,
|
kategorija: Optional[str] = None, godisnjak: Optional[bool] = None, financiran: Optional[bool] = None,
|
||||||
|
samo_hns_roster: Optional[bool] = None,
|
||||||
sort: str = "naziv", order: str = "asc"):
|
sort: str = "naziv", order: str = "asc"):
|
||||||
where = ["v.aktivan"]
|
where = ["v.aktivan"]
|
||||||
params = []
|
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)")
|
where.append("(k.godisnjak_godine IS NULL OR array_length(k.godisnjak_godine,1) IS NULL)")
|
||||||
if kategorija and kategorija.strip().lower() == "priority":
|
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))")
|
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",
|
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")
|
"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"
|
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
|
# POTPORE — by year filter
|
||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
@router.get("/potpore/by-year")
|
@router.get("/potpore/by-year")
|
||||||
def potpore_by_year(godina: int = None, q: str = ""):
|
def potpore_by_year(godina: int = None, q: str = "", samo_klubovi: bool = True, davatelj: str = None):
|
||||||
"""Sufinanciranje za specifičnu godinu."""
|
"""Sufinanciranje za specifičnu godinu — samo_klubovi=True izbacuje programe/totals/services."""
|
||||||
import datetime
|
import datetime
|
||||||
yr = godina or datetime.date.today().year
|
yr = godina or datetime.date.today().year
|
||||||
like = f"%{q}%" if q else "%"
|
like = f"%{q}%" if q else "%"
|
||||||
rows = db_query("""
|
|
||||||
SELECT korisnik, sport, iznos_eur, vrsta, napomena, izvor, source_url, godina,
|
where = ["godina = %s", "LOWER(COALESCE(korisnik,'')) LIKE LOWER(%s)"]
|
||||||
(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
|
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
|
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
|
ORDER BY iznos_eur DESC NULLS LAST
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
""", (yr, like))
|
"""
|
||||||
|
rows = db_query(sql, params)
|
||||||
total = sum(float(r.get('iznos_eur') or 0) for r in rows)
|
total = sum(float(r.get('iznos_eur') or 0) for r in rows)
|
||||||
return {"godina": yr, "count": len(rows), "total": total, "results": 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>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<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 API = '/sport/api/v2/crm';
|
||||||
const STAGE_LABEL = {
|
const STAGE_LABEL = {
|
||||||
prospecting:'Prospecting', qualification:'Qualification', proposal:'Proposal',
|
prospecting:'Prospecting', qualification:'Qualification', proposal:'Proposal',
|
||||||
@@ -495,14 +506,34 @@ const STAGE_LABEL = {
|
|||||||
};
|
};
|
||||||
const STAGES = ['prospecting','qualification','proposal','negotiation','closed_won','closed_lost'];
|
const STAGES = ['prospecting','qualification','proposal','negotiation','closed_won','closed_lost'];
|
||||||
|
|
||||||
if (!TOKEN) {
|
// JWT expiry pre-check + redirect only when truly missing/expired
|
||||||
location.href = '/sport/login?next=' + encodeURIComponent(location.pathname);
|
(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={}) {
|
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 headers = {'Authorization':'Bearer '+TOKEN, 'Content-Type':'application/json', ...(opts.headers||{})};
|
||||||
const res = await fetch(API+path, {...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();
|
const txt = await res.text();
|
||||||
let data; try { data = JSON.parse(txt); } catch { data = txt; }
|
let data; try { data = JSON.parse(txt); } catch { data = txt; }
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -548,16 +579,20 @@ function switchTab(name) {
|
|||||||
// ────── /me ──────
|
// ────── /me ──────
|
||||||
async function loadMe() {
|
async function loadMe() {
|
||||||
try {
|
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');
|
document.getElementById('me').textContent = (me.email || me.full_name || 'user');
|
||||||
} catch { document.getElementById('me').textContent='?'; }
|
} catch { document.getElementById('me').textContent='?'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('logout').addEventListener('click', async (e) => {
|
document.getElementById('logout').addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+TOKEN}}); } catch {}
|
const tok = getToken();
|
||||||
localStorage.removeItem('token'); localStorage.removeItem('access_token');
|
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+tok}}); } catch {}
|
||||||
location.href='/sport/login';
|
['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 ──────
|
// ────── Pipeline & Dashboard ──────
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
<div class="pgz-sb-h">
|
<div class="pgz-sb-h">
|
||||||
<div class="pgz-mark">PGŽ</div>
|
<div class="pgz-mark">PGŽ</div>
|
||||||
<div class="pgz-htxt">
|
<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 class="pgz-sub">Odjel za sport</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pgz-sb-toggle" onclick="PGZSidebar.toggle()" title="Skupi/raširi">←</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{background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));border-color:transparent;color:#fff}
|
||||||
.btn.primary:hover{filter:brightness(1.1)}
|
.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{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{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}
|
.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.rd{background:var(--red);color:#fff}
|
||||||
.tag.am{background:var(--amber);color:var(--bg0)}
|
.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{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{right:0}
|
#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{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-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}
|
#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; }
|
.table-container, .card { overflow-x: auto; }
|
||||||
|
|
||||||
/* Drill-down panel full-width */
|
/* 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; }
|
#panel.open { right: 0 !important; }
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
@@ -314,6 +325,12 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
|||||||
/* HNS karijera tabela full-width */
|
/* HNS karijera tabela full-width */
|
||||||
#panel table, #dpanel table { width: 100%; font-size: 12px; }
|
#panel table, #dpanel table { width: 100%; font-size: 12px; }
|
||||||
#panel .hns-stats td { padding: 4px 6px; }
|
#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>
|
</style>
|
||||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||||
<script src="/static/shared/sidebar.js" defer data-active="dashboard"></script>
|
<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};
|
const _sort = {savezi:null, klubovi:null, sportasi:null, objekti:null, manifestacije:null, financije:null};
|
||||||
let _proracunChart=null, _financijeChart=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 ===
|
// === PGŽ priority filter (SUB6) — global helper, works across Klubovi/Savezi/Sportaši ===
|
||||||
window._pgz_filter_priority = window._pgz_filter_priority || false;
|
window._pgz_filter_priority = window._pgz_filter_priority || false;
|
||||||
window.togglePGZFilter = function(section){
|
window.togglePGZFilter = function(section){
|
||||||
@@ -1240,13 +1334,16 @@ async function loadSavezi(){
|
|||||||
const root = $('#pg-savezi');
|
const root = $('#pg-savezi');
|
||||||
if(!_cache.savezi){
|
if(!_cache.savezi){
|
||||||
root.innerHTML = '<div class="loading">Učitavanje saveza…</div>';
|
root.innerHTML = '<div class="loading">Učitavanje saveza…</div>';
|
||||||
// PGŽ filter: switch to v2 priority-sort endpoint (only=true returns just PGŽ-relevant savezi)
|
// BUG-E (2026-05-05): explicit filter — when financirani=true → priority-sort?only=true
|
||||||
const url = window._pgz_filter_priority
|
const f = _filters.savezi;
|
||||||
|
const useOnly = f.financirani || window._pgz_filter_priority;
|
||||||
|
const url = useOnly
|
||||||
? '/v2/savezi/priority-sort?only=true&limit=500'
|
? '/v2/savezi/priority-sort?only=true&limit=500'
|
||||||
: '/savezi?limit=250';
|
: '/v2/savezi/priority-sort?only=false&limit=500';
|
||||||
const d = await api(url);
|
const d = await api(url);
|
||||||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||||
_cache.savezi = d.rows || [];
|
_cache.savezi = d.rows || [];
|
||||||
|
_filters.savezi.total = (d.rows||[]).length;
|
||||||
}
|
}
|
||||||
renderSaveziShell();
|
renderSaveziShell();
|
||||||
applySaveziFilter();
|
applySaveziFilter();
|
||||||
@@ -1255,6 +1352,7 @@ function renderSaveziShell(){
|
|||||||
const root = $('#pg-savezi');
|
const root = $('#pg-savezi');
|
||||||
const sports = Array.from(new Set((_cache.savezi||[]).map(s=>s.sport).filter(Boolean))).sort();
|
const sports = Array.from(new Set((_cache.savezi||[]).map(s=>s.sport).filter(Boolean))).sort();
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
|
${_filtersBar('savezi')}
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<input type="search" id="sav-q" placeholder="🔍 Pretraži savez…">
|
<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>
|
<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>
|
${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>
|
<thead><tr><th>Klub</th><th>Razina</th><th>Grad</th></tr></thead>
|
||||||
<tbody>${klubovi.slice(0,100).map(k => `
|
<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>${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.razina,'')}</td>
|
||||||
<td>${txt(k.grad)}</td>
|
<td>${txt(k.grad)}</td>
|
||||||
@@ -1397,14 +1495,27 @@ async function loadKlubovi(){
|
|||||||
const root = $('#pg-klubovi');
|
const root = $('#pg-klubovi');
|
||||||
if(!_cache.klubovi){
|
if(!_cache.klubovi){
|
||||||
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
|
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
|
||||||
// /api/klubovi already returns priority/financiran/godisnjak flags.
|
// BUG-E (2026-05-05): build /api/klubovi URL from explicit _filters.klubovi state.
|
||||||
// When PGŽ filter is on, ask backend to only return priority klubs.
|
// Defaults: financirani=true + godisnjak=true. When BOTH off → load all.
|
||||||
const url = window._pgz_filter_priority
|
const f = _filters.klubovi;
|
||||||
? '/klubovi?kategorija=priority&limit=2500'
|
const qs = new URLSearchParams();
|
||||||
: '/klubovi?limit=2500';
|
qs.set('limit','2500');
|
||||||
const d = await api(url);
|
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; }
|
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||||
_cache.klubovi = d.rows || [];
|
_cache.klubovi = d.rows || [];
|
||||||
|
_filters.klubovi.total = (d.rows||[]).length;
|
||||||
}
|
}
|
||||||
renderKluboviShell();
|
renderKluboviShell();
|
||||||
applyKluboviFilter();
|
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 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();
|
const grads = Array.from(new Set((_cache.klubovi||[]).map(k=>k.grad).filter(Boolean))).sort();
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
|
${_filtersBar('klubovi')}
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<input type="search" id="kl-q" placeholder="🔍 Pretraži klub…">
|
<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>
|
<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==='financiran') rows = rows.filter(k => k.financiran);
|
||||||
else if(kat==='godisnjak') rows = rows.filter(k => k.godisnjak);
|
else if(kat==='godisnjak') rows = rows.filter(k => k.godisnjak);
|
||||||
if(_sort.klubovi) rows = sortRows(rows, _sort.klubovi.key, _sort.klubovi.dir);
|
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';
|
$('#kl-cnt').textContent = rows.length+' klubova';
|
||||||
const top = rows.slice(0, 300);
|
const top = rows.slice(0, 300);
|
||||||
$('#kl-out').innerHTML = _state.viewKlubovi==='card' ? renderKluboviGrid(top) : renderKluboviTable(top);
|
$('#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>
|
${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>
|
<thead><tr><th>Sportaš</th><th>Spol</th><th>Pozicija</th><th>Kategorija</th><th>Tagovi</th></tr></thead>
|
||||||
<tbody>${clanovi.map(c => `
|
<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><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||||||
<td>${txt(c.spol)}</td>
|
<td>${txt(c.spol)}</td>
|
||||||
<td>${txt(c.pozicija)}</td>
|
<td>${txt(c.pozicija)}</td>
|
||||||
@@ -1664,7 +1778,7 @@ async function openKlub(id){
|
|||||||
<details style="margin-bottom:8px" ${groups[kat].length<=12?'open':''}>
|
<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>
|
<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 => `
|
<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><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||||||
<td>${txt(c.spol)}</td>
|
<td>${txt(c.spol)}</td>
|
||||||
<td>${txt(c.pozicija)}</td>
|
<td>${txt(c.pozicija)}</td>
|
||||||
@@ -2030,7 +2144,7 @@ async function openSportas(id){
|
|||||||
<div class="pp-meta">
|
<div class="pp-meta">
|
||||||
${d.sport?'<a class="link-chip" onclick="filterSportasiBy("sport","'+esc(d.sport)+'")">'+esc(d.sport)+'</a>':'—'} ·
|
${d.sport?'<a class="link-chip" onclick="filterSportasiBy("sport","'+esc(d.sport)+'")">'+esc(d.sport)+'</a>':'—'} ·
|
||||||
${txt(d.pozicija,'')} ·
|
${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>
|
||||||
<div class="pp-meta">
|
<div class="pp-meta">
|
||||||
${dob ? '<a class="link-chip" onclick="filterSportasiByYear("'+esc((dob||'').slice(0,4))+'")">📅 '+fmtDate(dob)+'</a>' : '📅 —'}
|
${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.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>':''}
|
${d.broj_dresa?'<span class="pp-bio-chip">#<b>'+esc(d.broj_dresa)+'</b></span>':''}
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-links">
|
<!-- BUG-F (2026-05-05): external links moved to dedicated 🔗 Linkovi tab -->
|
||||||
${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>
|
|
||||||
</div>
|
</div>
|
||||||
</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 class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></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="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-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>
|
||||||
|
|
||||||
<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>
|
<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>
|
${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>
|
<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>'}
|
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
||||||
</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>
|
<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 -->
|
<!-- 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):''}
|
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · '+esc(d.mjesto_rodjenja||d.mjesto_rodenja):''}
|
||||||
</div>
|
</div>
|
||||||
<div class="prof-club">
|
<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>'}
|
${d.aktivan?'<span class="tag gr" style="margin-left:8px">AKTIVAN</span>':'<span class="tag rd" style="margin-left:8px">NEAKTIVAN</span>'}
|
||||||
</div>
|
</div>
|
||||||
${hnsUrl?'<div style="margin-top:10px"><a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS Semafor profil ↗</a></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">
|
<tr class="no-click">
|
||||||
<td><b>${esc(k.sezona||'—')}</b></td>
|
<td><b>${esc(k.sezona||'—')}</b></td>
|
||||||
<td><span class="tag b">${esc(k.kategorija||'—')}</span></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>${esc(k.source||'—')}</td>
|
||||||
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
@@ -2196,6 +2308,19 @@ async function openSportas(id){
|
|||||||
</table></div>` : ''}
|
</table></div>` : ''}
|
||||||
</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)}
|
${enrichBlock('sportas', d.id)}
|
||||||
`;
|
`;
|
||||||
openPanel('Sportaš · '+(d.ime||'')+' '+(d.prezime||''), html);
|
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>
|
<div class="card-h"><div class="card-t">🔗 Povezani entiteti</div></div>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<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.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="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.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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
${(!a.klub_id && !a.clan_id) ? '<div class="empty" style="padding:14px">Nema povezanih entiteta u alarmu</div>' : ''}
|
${(!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();
|
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._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){
|
window._registerOpener = function(fn){
|
||||||
// callback may be null — to render this state again later
|
if(typeof fn !== 'function' || !fn.name) return null;
|
||||||
window._panelHistory.push({title, htmlContent, callback});
|
window._panelOpenerMap[fn.name] = fn;
|
||||||
const back = document.getElementById('panel-back');
|
return fn.name;
|
||||||
if(back) back.style.display = window._panelHistory.length > 1 ? 'inline-flex' : 'none';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(){
|
window.panelBack = function(){
|
||||||
if(window._panelHistory.length <= 1){ closePanel(); return; }
|
if(window._panelHistory.length <= 1){ window.closePanel(); return; }
|
||||||
// Remove current state
|
|
||||||
window._panelHistory.pop();
|
window._panelHistory.pop();
|
||||||
// Restore previous
|
|
||||||
const prev = window._panelHistory[window._panelHistory.length - 1];
|
const prev = window._panelHistory[window._panelHistory.length - 1];
|
||||||
const t = document.getElementById('panel-hdr-t');
|
const fn = window._panelOpenerMap[prev.opener];
|
||||||
const b = document.getElementById('panel-body');
|
if(typeof fn !== 'function'){ window.closePanel(); return; }
|
||||||
if(t) t.textContent = prev.title;
|
window._panelSuppressPush = true;
|
||||||
if(b) b.innerHTML = prev.htmlContent;
|
window._panelDrilling = true;
|
||||||
if(typeof prev.callback === 'function') prev.callback();
|
try { fn.apply(null, prev.args || []); } catch(e) { console.error('panelBack', e); }
|
||||||
const back = document.getElementById('panel-back');
|
setTimeout(() => {
|
||||||
if(back) back.style.display = window._panelHistory.length > 1 ? 'inline-flex' : 'none';
|
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._origClosePanel = window.closePanel;
|
||||||
window.closePanel = function(){
|
window.closePanel = function(){
|
||||||
window._panelHistory = [];
|
window._panelHistory = [];
|
||||||
@@ -3489,7 +3705,7 @@ window.closePanel = function(){
|
|||||||
const p = document.getElementById('panel');
|
const p = document.getElementById('panel');
|
||||||
if(p) p.classList.remove('open');
|
if(p) p.classList.remove('open');
|
||||||
const ov = document.getElementById('panel-overlay');
|
const ov = document.getElementById('panel-overlay');
|
||||||
if(ov) ov.style.display = 'none';
|
if(ov){ ov.classList.remove('open'); ov.style.removeProperty('display'); }
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user