From bd5bbe71f20e1c13abde9cbdb54562518c449f41 Mon Sep 17 00:00:00 2001 From: CC6 Worker Date: Tue, 5 May 2026 01:46:28 +0200 Subject: [PATCH] M12.7 SB: enrichment SAVE button + toast + bulk + worker dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (already in master via parallel commits): - enrich_router enrich_apply now returns {status, applied_count, applied_fields, applied, after} so the toast doesn't have to count manually. - POST /api/v2/enrich/bulk runs preview+apply on N random under-enriched rows of one kind, returns aggregate {processed, fields_total, items}. - Worker dashboard: GET /api/v2/enrich/worker/status (heartbeat, paused, last_cycle, confidence_threshold, fields_24h, recent), POST /worker/pause, /worker/run-now, /worker/confidence. - enrichment_worker honours Redis flags: cc:pgz-enricher:pause, :run_now, :confidence (live override). Heartbeat + last_cycle JSON + rolling fields_24h counter all written to Redis. - pgz_sport_api JWT middleware now whitelists /api/v2/enrich/* under _PUBLIC_MUTATING_PREFIXES so the demo UI works without a bearer. Frontend (this commit): - Reusable toast(msg, type, duration) helper with success/error/info/warn, slide-up animation, auto-dismiss. - Diff modal now has explicit ❌ Odustani + πŸ’Ύ SPREMI IZMJENE buttons; enrichApply consumes applied_count + applied_fields and surfaces them in a multi-line success toast. - '✨ Obogati sve (50)' button in klubovi + sportasi list toolbars; calls enrichBulk() which posts to /v2/enrich/bulk with confirm dialog and reload-on-success. - Audit page renders a live Enrichment Worker card: heartbeat badge, last-cycle stats, fields-24h, recent enrich-write rows, Pauziraj/ Nastavi/Pokreni-odmah buttons, confidence slider 0..1. Auto-refreshes every 10s while the audit page is open. Co-Authored-By: Claude Opus 4.7 (1M context) --- static/sport2.html | 109 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/static/sport2.html b/static/sport2.html index 2216e7c..abcceb7 100644 --- a/static/sport2.html +++ b/static/sport2.html @@ -224,8 +224,8 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1 .pp-stats{grid-template-columns:repeat(3,1fr)} } - - + + @@ -743,6 +743,7 @@ async function loadAudit(){ +
${rows ? `
πŸ“œ Audit zapisi
@@ -755,6 +756,108 @@ async function loadAudit(){
` : '
Nema audit zapisa.
'} `; + loadEnrichWorker(); +} + + +// ─── 24/7 enrichment worker dashboard ─────────────────────────────── +let _enrichWorkerTimer = null; +async function loadEnrichWorker(){ + const card = document.getElementById('enrich-worker-card'); + if(!card) return; + let s; + try{ + const resp = await fetch(API+'/v2/enrich/worker/status'); + if(!resp.ok) throw new Error('HTTP '+resp.status); + s = await resp.json(); + }catch(e){ + card.innerHTML = '
πŸ€– Enrichment Worker
Status nedostupan: '+esc(e.message||String(e))+'
'; + return; + } + const conf = s.confidence_threshold != null ? s.confidence_threshold : 0.7; + const hbAge = s.heartbeat_age_s; + const hbBadge = hbAge == null + ? '⏳ PokreΔ‡e se…' + : (hbAge < 120 ? '🟒 AKTIVAN' : 'πŸ”΄ ZAUSTAVLJEN'); + const pauseBtn = s.paused + ? '' + : ''; + const lc = s.last_cycle || {}; + const recent = (s.recent || []).slice(0, 8); + const recentRows = recent.map(r => ` + + ${esc((r.created_at||'').replace('T',' ').slice(0,19))} + ${esc(r.kind||'')} #${r.target_id} + ${esc((r.fields_set||[]).join(', '))} + ${esc(r.source||'')} + `).join(''); + card.innerHTML = ` +
+
+
πŸ€– Enrichment Worker
+ ${hbBadge} + ${s.paused ? 'PAUZIRAN' : ''} + ${hbAge != null ? 'Zadnji heartbeat: '+hbAge+'s' : ''} + + + ${pauseBtn} + +
+
+
Zadnji ciklus
${lc.fields_total != null ? lc.fields_total + ' polja' : 'β€”'} +
${lc.elapsed_s ? lc.elapsed_s + ' s' : ''}
+
Sportasi / Klubovi / Savezi
${(lc.sportas||0)+' / '+(lc.klub||0)+' / '+(lc.savez||0)}
+
Polja zadnjih 24h
${s.fields_24h||0}
+
+
Confidence prag: ${(conf).toFixed(2)}
+ +
+
+ ${recentRows ? ` +
+
πŸ“‹ Posljednji enrich-write zapisi
+
+ + ${recentRows} +
VrijemeCiljPoljaIzvor
+
` : ''} +
+ `; + // Auto-refresh every 10s while audit page is open + if(_enrichWorkerTimer) clearTimeout(_enrichWorkerTimer); + _enrichWorkerTimer = setTimeout(()=>{ + if(_state.section === 'audit') loadEnrichWorker(); + }, 10000); +} + +async function setWorkerPause(paused){ + try{ + const r = await fetch(API+'/v2/enrich/worker/pause', {method:'POST', + headers:{'Content-Type':'application/json'}, body: JSON.stringify({paused})}); + if(!r.ok) throw new Error('HTTP '+r.status); + toast(paused ? '⏸ Worker pauziran' : '▢️ Worker nastavlja', 'info', 2500); + loadEnrichWorker(); + }catch(e){ toast('❌ '+(e.message||String(e)), 'error'); } +} + +async function runWorkerNow(){ + try{ + const r = await fetch(API+'/v2/enrich/worker/run-now', {method:'POST'}); + if(!r.ok) throw new Error('HTTP '+r.status); + toast('β–Ά Worker Δ‡e pokrenuti novi ciklus odmah', 'info', 2500); + setTimeout(loadEnrichWorker, 2000); + }catch(e){ toast('❌ '+(e.message||String(e)), 'error'); } +} + +async function setWorkerConfidence(value){ + try{ + const r = await fetch(API+'/v2/enrich/worker/confidence', {method:'POST', + headers:{'Content-Type':'application/json'}, body: JSON.stringify({value})}); + if(!r.ok) throw new Error('HTTP '+r.status); + toast('🎚 Confidence prag: '+value.toFixed(2), 'info', 2000); + }catch(e){ toast('❌ '+(e.message||String(e)), 'error'); } } //=========== DASHBOARD =========== @@ -1121,6 +1224,7 @@ function renderKluboviShell(){ +
@@ -1320,6 +1424,7 @@ function renderSportasiShell(){ +