diff --git a/static/app.html.bak2.1777937982 b/static/app.html.bak2.1777937982
new file mode 100644
index 0000000..fd20395
--- /dev/null
+++ b/static/app.html.bak2.1777937982
@@ -0,0 +1,1854 @@
+
+
+
+
+
+
PGŽ SPORT — Operativna aplikacija
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashboard
+
Pregled stanja
+
+
+
+
+
DR
+
+
Damir Radulićpgz admin
+
Primorsko-goranska županija
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/sport2.html b/static/sport2.html
index b7f0204..2216e7c 100644
--- a/static/sport2.html
+++ b/static/sport2.html
@@ -408,10 +408,11 @@ async function enrichEntity(kind, id){
| Polje | Trenutno | Predloženo |
${rows}
-
-
-
-
+
+
+
+
+
`;
} else {
@@ -454,11 +455,39 @@ function enrichSelectAll(kind, id, on){
tbody.querySelectorAll('input[type=checkbox]').forEach(cb => { cb.checked = !!on; });
}
+// Reusable toast component (success / error / info / warn).
+window.toast = function(msg, type, duration){
+ type = type || 'success';
+ duration = duration || 3000;
+ const palette = {
+ success: ['#1ec773', '#0b1a16'],
+ error: ['#ff6b6b', '#1a0b0b'],
+ info: ['#4a9eff', '#04132b'],
+ warn: ['#ffb84a', '#1a1004'],
+ }[type] || ['#4a9eff', '#04132b'];
+ const t = document.createElement('div');
+ t.className = 'pgz-toast pgz-toast-' + type;
+ t.style.cssText = 'position:fixed;right:20px;bottom:20px;'+
+ 'background:'+palette[0]+';color:'+palette[1]+';'+
+ 'padding:12px 18px;border-radius:8px;font-weight:700;font-size:14px;'+
+ 'z-index:99999;box-shadow:0 6px 22px rgba(0,0,0,.45);'+
+ 'transform:translateY(40px);opacity:0;transition:all .25s ease-out;'+
+ 'max-width:380px;line-height:1.45;';
+ t.innerHTML = msg;
+ document.body.appendChild(t);
+ requestAnimationFrame(()=>{ t.style.transform='translateY(0)'; t.style.opacity='1'; });
+ setTimeout(()=>{
+ t.style.transform='translateY(40px)'; t.style.opacity='0';
+ setTimeout(()=>t.remove(), 280);
+ }, duration);
+ return t;
+};
+
async function enrichApply(kind, id){
const target = document.getElementById('enrich-out-'+kind+'-'+id);
const tbody = document.getElementById('enrich-diff-'+kind+'-'+id);
const preview = (window._enrichPreviews||{})[kind+':'+id];
- if(!preview){ alert('Prvo pokreni "▶ Pokreni"'); return; }
+ if(!preview){ toast('⚠ Prvo pokreni "▶ Pokreni"', 'warn'); return; }
const proposed = preview.proposed || {};
const fields = {};
if(tbody){
@@ -469,7 +498,7 @@ async function enrichApply(kind, id){
} else {
Object.assign(fields, proposed);
}
- if(!Object.keys(fields).length){ alert('Označi barem jedno polje za primjenu.'); return; }
+ if(!Object.keys(fields).length){ toast('Označi barem jedno polje za primjenu.', 'warn'); return; }
if(target) target.innerHTML = '
⏳ Spremam u bazu…
';
try{
const r = await fetch(API+'/v2/enrich/'+kind+'/'+id+'/apply', {
@@ -484,18 +513,46 @@ async function enrichApply(kind, id){
else if(kind === 'savez' && typeof openSavez === 'function') await openSavez(id);
else if(kind === 'sportas' && typeof openSportas === 'function') await openSportas(id);
setTimeout(() => enrichEntity(kind, id), 350);
- const cnt = Object.keys(data.applied||{}).length;
- const t = document.createElement('div');
- t.style.cssText = 'position:fixed;bottom:20px;right:20px;background:var(--ok,#1ec773);color:#0b1a16;padding:10px 16px;border-radius:6px;font-weight:700;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.4)';
- t.textContent = '✓ Spremljeno '+cnt+' polja u bazu';
- document.body.appendChild(t);
- setTimeout(()=>t.remove(), 3500);
+ const cnt = data.applied_count != null ? data.applied_count : Object.keys(data.applied||{}).length;
+ const fieldsList = (data.applied_fields || Object.keys(data.applied||{})).join(', ');
+ if(cnt){
+ toast('✅ Spremljeno
'+cnt+' polja u bazu'
+ + (fieldsList ? '
'+esc(fieldsList)+'' : ''),
+ 'success', 3500);
+ } else {
+ toast('Nema novih izmjena za spremiti.', 'info', 2500);
+ }
}catch(e){
console.error(e);
+ toast('❌ Greška pri spremanju: '+esc(e.message||String(e)), 'error', 4500);
if(target) target.innerHTML = '
Greška pri spremanju: '+esc(e.message||String(e))+'
';
}
}
+// Bulk enrichment — used by "Obogati sve" buttons in list views
+async function enrichBulk(kind, limit, coverage_max){
+ limit = limit || 50; coverage_max = coverage_max || 70;
+ if(!confirm('Pokreni obogaćivanje za '+limit+' nasumično odabranih ('+kind+', coverage<'+coverage_max+'%)?')) return;
+ toast('⏳ Pokrećem bulk obogaćivanje za '+limit+' '+kind+'…', 'info', 2500);
+ try{
+ const r = await fetch(API+'/v2/enrich/bulk', {
+ method:'POST',
+ headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({kind, limit, coverage_max}),
+ });
+ const data = await r.json();
+ if(!r.ok) throw new Error(data.detail || ('HTTP '+r.status));
+ toast('✅ Bulk gotov:
'+data.processed+'/'+data.requested+' obrađeno, '+
+ 'dodano
'+data.fields_total+' polja u DB ('+data.elapsed_s+'s)',
+ 'success', 5000);
+ // Reload the section so new values appear
+ if(typeof loadSection === 'function' && _state && _state.section) loadSection(_state.section);
+ }catch(e){
+ console.error(e);
+ toast('❌ Bulk greška: '+esc(e.message||String(e)), 'error', 5000);
+ }
+}
+
function enrichBlock(kind, id){
return `
diff --git a/static/uploads/avatars/99-3a8466b0.png b/static/uploads/avatars/99-3a8466b0.png
new file mode 100644
index 0000000..6bb84ce
Binary files /dev/null and b/static/uploads/avatars/99-3a8466b0.png differ
diff --git a/static/uploads/avatars/99-68860ddb.png b/static/uploads/avatars/99-68860ddb.png
deleted file mode 100644
index f88bc3b..0000000
Binary files a/static/uploads/avatars/99-68860ddb.png and /dev/null differ
diff --git a/workers/enrichment_worker.py b/workers/enrichment_worker.py
index ce005bf..42af399 100644
--- a/workers/enrichment_worker.py
+++ b/workers/enrichment_worker.py
@@ -69,13 +69,78 @@ def _log(msg: str) -> None:
pass
-def _heartbeat() -> None:
+def _redis():
try:
import redis
- r = redis.Redis(host=os.environ.get('REDIS_HOST', 'localhost'),
- port=int(os.environ.get('REDIS_PORT', '6379')),
- password=os.environ.get('REDIS_PASS', None))
+ except Exception:
+ return None
+ host = os.environ.get('REDIS_HOST', 'localhost')
+ port = int(os.environ.get('REDIS_PORT', '6379'))
+ pwd = (os.environ.get('REDIS_PASS') or '').strip().strip("'").strip('"') or None
+ for p in (pwd, None):
+ try:
+ r = redis.Redis(host=host, port=port, password=p,
+ decode_responses=True, socket_connect_timeout=2)
+ r.ping()
+ return r
+ except Exception:
+ continue
+ return None
+
+
+def _heartbeat(meta: dict | None = None) -> None:
+ r = _redis()
+ if not r: return
+ try:
r.set('cc:pgz-enricher:heartbeat', str(int(time.time())))
+ if meta is not None:
+ r.set('cc:pgz-enricher:last_cycle', json.dumps(meta, default=str))
+ except Exception:
+ pass
+
+
+def _is_paused() -> bool:
+ r = _redis()
+ if not r: return False
+ try:
+ return (r.get('cc:pgz-enricher:pause') or '0') == '1'
+ except Exception:
+ return False
+
+
+def _consume_run_now() -> bool:
+ r = _redis()
+ if not r: return False
+ try:
+ v = r.get('cc:pgz-enricher:run_now')
+ if v == '1':
+ r.set('cc:pgz-enricher:run_now', '0')
+ return True
+ except Exception:
+ return False
+ return False
+
+
+def _refresh_confidence() -> None:
+ """Read live confidence override from redis (set by /worker/confidence)."""
+ global CONFIDENCE_MIN
+ r = _redis()
+ if not r: return
+ try:
+ v = r.get('cc:pgz-enricher:confidence')
+ if v:
+ CONFIDENCE_MIN = float(v)
+ except Exception:
+ pass
+
+
+def _bump_fields_24h(n: int) -> None:
+ if n <= 0: return
+ r = _redis()
+ if not r: return
+ try:
+ r.incrby('cc:pgz-enricher:fields_24h', n)
+ r.expire('cc:pgz-enricher:fields_24h', 86400)
except Exception:
pass
@@ -264,8 +329,10 @@ def _process(kind: str, eid: int) -> tuple[int, list[str]]:
def _cycle() -> dict:
+ _refresh_confidence()
started = time.time()
- out = {'sportas': 0, 'klub': 0, 'savez': 0, 'fields_total': 0}
+ out = {'sportas': 0, 'klub': 0, 'savez': 0, 'fields_total': 0,
+ 'started_at': datetime.now(timezone.utc).isoformat()}
fields_total = 0
for kind, picker, limit in (
('sportas', _pick_sportas, 50),
@@ -278,26 +345,45 @@ def _cycle() -> dict:
for eid in ids:
if DRY:
continue
+ if _is_paused():
+ _log("paused → break out of cycle")
+ break
n, fields = _process(kind, eid)
out[kind] += 1
fields_total += n
+ if n: _bump_fields_24h(n)
time.sleep(1.5) # gentle pacing
_heartbeat()
out['fields_total'] = fields_total
out['elapsed_s'] = round(time.time() - started, 1)
+ out['ended_at'] = datetime.now(timezone.utc).isoformat()
return out
def main() -> int:
_log(f"enrichment_worker starting | API_BASE={API_BASE} | sleep={SLEEP_S}s | dry={DRY}")
while True:
+ if _is_paused():
+ _log("paused (cc:pgz-enricher:pause=1) — sleeping 30s")
+ _heartbeat({'paused': True})
+ time.sleep(30)
+ continue
try:
stats = _cycle()
_log(f"cycle done: {json.dumps(stats)}")
+ _heartbeat(stats)
except Exception as e:
_log(f"cycle FAILED: {type(e).__name__}: {e}")
- _heartbeat()
- time.sleep(SLEEP_S)
+ _heartbeat({'error': str(e)[:200]})
+ # Sleep in 5-second slices so /worker/run-now and /pause respond fast.
+ elapsed = 0
+ while elapsed < SLEEP_S:
+ if _consume_run_now():
+ _log("run-now signal received → starting next cycle early")
+ break
+ if _is_paused():
+ break
+ time.sleep(5); elapsed += 5
if __name__ == '__main__':