feat: live enrichment feed with per-company progress rows
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

- Live panel with spinner, progress bar, company counter
- Each company appears as a row with platform badges showing status
  (changes/no_changes/skipped/error/no_data)
- Incremental polling (since= param) for efficient updates
- After completion: link to review page if changes found
- Blue highlighted rows for companies with new data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-12 08:50:17 +01:00
parent d39acc303e
commit ce5a259749
2 changed files with 149 additions and 28 deletions

View File

@ -979,8 +979,47 @@ def admin_social_audit_run_enrichment():
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_social_audit_enrichment_status():
"""Get current enrichment job status with pending changes summary."""
"""Get current enrichment job status with live results feed."""
pending = _enrichment_status.get('pending_changes', [])
results = _enrichment_status.get('results', [])
# Return last N results for live feed (since_index param for incremental updates)
since = request.args.get('since', 0, type=int)
new_results = results[since:]
# Build compact live feed entries
feed = []
for r in new_results:
profiles_summary = []
for p in r.get('profiles', []):
status = p.get('status', 'unknown')
icon = {'changes': '+', 'no_changes': '=', 'skipped': '~', 'error': '!', 'no_data': '-'}.get(status, '?')
platform = p.get('platform', '?')
change_count = len(p.get('changes', []))
desc = ''
if status == 'changes':
desc = f'{change_count} zmian'
elif status == 'skipped':
desc = 'API'
elif status == 'error':
desc = p.get('reason', 'błąd')[:40]
elif status == 'no_data':
desc = 'brak danych'
elif status == 'no_changes':
desc = 'aktualne'
profiles_summary.append({
'platform': platform,
'icon': icon,
'status': status,
'desc': desc,
})
feed.append({
'company_name': r.get('company_name', '?'),
'company_id': r.get('company_id'),
'has_changes': r.get('has_changes', False),
'profiles': profiles_summary,
})
return jsonify({
'running': _enrichment_status['running'],
'progress': _enrichment_status['progress'],
@ -990,6 +1029,8 @@ def admin_social_audit_enrichment_status():
'last_run': _enrichment_status['last_run'].strftime('%d.%m.%Y %H:%M') if _enrichment_status['last_run'] else None,
'pending_count': len(pending),
'approved': _enrichment_status.get('approved', False),
'feed': feed,
'results_count': len(results),
})

View File

@ -523,10 +523,7 @@
</svg>
Uruchom audyt
</button>
<div id="enrichProgress" style="display: none; font-size: 12px; padding: 4px 12px; background: #eff6ff; border-radius: var(--radius); color: #2563eb;">
<span id="enrichText">Audyt...</span>
<span id="enrichPct">0%</span>
</div>
<span id="enrichMiniStatus" style="display: none; font-size: 12px; color: #2563eb; font-weight: 500;"></span>
<a href="{{ url_for('admin.admin_social_media') }}" class="btn btn-outline btn-sm">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
@ -536,6 +533,25 @@
</div>
</div>
<!-- Live enrichment panel -->
<div id="enrichPanel" style="display: none; margin-bottom: var(--spacing-xl); background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden;">
<div style="padding: var(--spacing-md) var(--spacing-lg); background: #eff6ff; border-bottom: 1px solid #bfdbfe; display: flex; align-items: center; gap: var(--spacing-md);">
<div id="enrichSpinner" style="width: 20px; height: 20px; border: 3px solid #bfdbfe; border-top-color: #2563eb; border-radius: 50%; animation: spin 0.8s linear infinite;"></div>
<div style="flex: 1;">
<div style="font-weight: 600; color: #1e40af;" id="enrichTitle">Skanowanie profili social media...</div>
<div style="font-size: var(--font-size-xs); color: #3b82f6;" id="enrichSubtitle">Dane nie zostaną zapisane bez Twojej zgody</div>
</div>
<span id="enrichCounter" style="font-size: var(--font-size-sm); font-weight: 600; color: #1e40af;">0 / 0</span>
</div>
<!-- Progress bar -->
<div style="height: 4px; background: #dbeafe;">
<div id="enrichBar" style="height: 100%; background: #2563eb; transition: width 0.5s; width: 0%;"></div>
</div>
<!-- Live feed -->
<div id="enrichFeed" style="max-height: 400px; overflow-y: auto; padding: var(--spacing-sm) var(--spacing-lg); font-size: var(--font-size-sm);"></div>
</div>
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
<!-- Summary Stats -->
<div class="stats-grid">
<div class="stat-card">
@ -989,14 +1005,17 @@ function resetFilters() {
}
// Enrichment
var _enrichSince = 0;
var _enrichPendingCount = 0;
function startEnrichment() {
if (!confirm('Uruchomić skanowanie social media dla wszystkich firm?\n\nProces działa w tle i może potrwać kilka minut.\nDane NIE zostaną zapisane bez Twojej zgody — po zakończeniu zobaczysz raport ze zmianami.\nProfile z danymi z API (OAuth) nie będą nadpisywane.')) return;
if (!confirm('Uruchomić skanowanie social media dla wszystkich firm?\n\nProces działa w tle. Dane NIE zostaną zapisane bez Twojej zgody.\nPo zakończeniu zobaczysz raport ze zmianami do zatwierdzenia.')) return;
var btn = document.getElementById('enrichBtn');
var progress = document.getElementById('enrichProgress');
btn.disabled = true;
btn.textContent = 'Uruchamianie...';
progress.style.display = 'inline-flex';
_enrichSince = 0;
_enrichPendingCount = 0;
fetch('{{ url_for("admin.admin_social_audit_run_enrichment") }}', {
method: 'POST',
@ -1005,48 +1024,109 @@ function startEnrichment() {
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'started') {
document.getElementById('enrichText').textContent = 'Skanowanie: 0/' + data.total;
// Show live panel
document.getElementById('enrichPanel').style.display = 'block';
document.getElementById('enrichCounter').textContent = '0 / ' + data.total;
document.getElementById('enrichFeed').innerHTML = '';
document.getElementById('enrichMiniStatus').style.display = 'inline';
document.getElementById('enrichMiniStatus').textContent = 'Skanowanie...';
pollEnrichment();
} else {
alert(data.error || 'Błąd uruchamiania');
btn.disabled = false;
btn.textContent = 'Uruchom audyt';
progress.style.display = 'none';
}
})
.catch(function(e) {
alert('Błąd: ' + e.message);
btn.disabled = false;
btn.textContent = 'Uruchom audyt';
progress.style.display = 'none';
});
}
function _statusIcon(status) {
if (status === 'changes') return '<span style="color:#2563eb;">&#9679;</span>';
if (status === 'skipped') return '<span style="color:#9ca3af;">&#9675;</span>';
if (status === 'error') return '<span style="color:#ef4444;">&#9888;</span>';
if (status === 'no_changes') return '<span style="color:#22c55e;">&#10003;</span>';
return '<span style="color:#9ca3af;">&#8212;</span>';
}
function _platformBadge(p) {
var colors = {
'changes': 'background:#dbeafe;color:#1d4ed8;',
'skipped': 'background:#f3f4f6;color:#6b7280;',
'error': 'background:#fee2e2;color:#991b1b;',
'no_changes': 'background:#f0fdf4;color:#15803d;',
'no_data': 'background:#f3f4f6;color:#9ca3af;'
};
var style = colors[p.status] || 'background:#f3f4f6;color:#6b7280;';
var name = p.platform.charAt(0).toUpperCase() + p.platform.slice(1);
return '<span style="display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:4px;font-size:11px;' + style + '" title="' + p.desc + '">' + name + ': ' + p.desc + '</span>';
}
function pollEnrichment() {
fetch('{{ url_for("admin.admin_social_audit_enrichment_status") }}')
fetch('{{ url_for("admin.admin_social_audit_enrichment_status") }}?since=' + _enrichSince)
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('enrichPct').textContent = data.progress + '%';
document.getElementById('enrichText').textContent = 'Skanowanie: ' + data.completed + '/' + data.total;
// Update progress
document.getElementById('enrichCounter').textContent = data.completed + ' / ' + data.total;
document.getElementById('enrichBar').style.width = data.progress + '%';
document.getElementById('enrichMiniStatus').textContent = data.completed + '/' + data.total;
// Append new feed entries
var feed = document.getElementById('enrichFeed');
if (data.feed && data.feed.length > 0) {
for (var i = 0; i < data.feed.length; i++) {
var r = data.feed[i];
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f3f4f6;';
var badges = r.profiles.map(_platformBadge).join(' ');
var nameStyle = r.has_changes ? 'font-weight:600;color:#1d4ed8;' : 'color:var(--text-secondary);';
row.innerHTML = '<span style="min-width:24px;text-align:center;">' + (_enrichSince + i + 1) + '.</span>' +
'<span style="min-width:200px;' + nameStyle + '">' + r.company_name + '</span>' +
'<span style="display:flex;gap:4px;flex-wrap:wrap;">' + badges + '</span>';
feed.appendChild(row);
feed.scrollTop = feed.scrollHeight;
}
_enrichSince += data.feed.length;
}
if (data.pending_count > _enrichPendingCount) {
_enrichPendingCount = data.pending_count;
document.getElementById('enrichSubtitle').textContent = _enrichPendingCount + ' profili z nowymi danymi (do zatwierdzenia)';
}
if (data.running) {
setTimeout(pollEnrichment, 3000);
setTimeout(pollEnrichment, 2000);
} else {
// Scan complete — redirect to review page
// Scan complete
document.getElementById('enrichSpinner').style.animation = 'none';
document.getElementById('enrichSpinner').style.borderTopColor = '#22c55e';
document.getElementById('enrichSpinner').style.borderColor = '#22c55e';
var btn = document.getElementById('enrichBtn');
btn.disabled = false;
btn.innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> Uruchom audyt';
if (data.pending_count > 0) {
document.getElementById('enrichText').textContent = data.pending_count + ' zmian do zatwierdzenia...';
window.location.href = '{{ url_for("admin.admin_social_audit_enrichment_review") }}';
document.getElementById('enrichTitle').textContent = 'Skanowanie zakończone — ' + data.pending_count + ' zmian do zatwierdzenia';
document.getElementById('enrichSubtitle').innerHTML = '<a href="{{ url_for("admin.admin_social_audit_enrichment_review") }}" style="color:#2563eb;font-weight:600;">Przejdź do raportu &rarr;</a>';
document.getElementById('enrichMiniStatus').innerHTML = '<a href="{{ url_for("admin.admin_social_audit_enrichment_review") }}" style="color:#2563eb;">' + data.pending_count + ' zmian &rarr;</a>';
// Add review link row
var linkRow = document.createElement('div');
linkRow.style.cssText = 'padding:12px 0;text-align:center;font-weight:600;';
linkRow.innerHTML = '<a href="{{ url_for("admin.admin_social_audit_enrichment_review") }}" style="color:#2563eb;font-size:14px;">Przejdź do raportu ze zmianami (' + data.pending_count + ' profili) &rarr;</a>';
feed.appendChild(linkRow);
feed.scrollTop = feed.scrollHeight;
} else {
var btn = document.getElementById('enrichBtn');
btn.disabled = false;
btn.innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> Uruchom audyt';
var errInfo = data.errors > 0 ? ', ' + data.errors + ' błędów' : '';
document.getElementById('enrichText').textContent = 'Brak nowych danych' + errInfo;
setTimeout(function() {
document.getElementById('enrichProgress').style.display = 'none';
}, 8000);
document.getElementById('enrichTitle').textContent = 'Skanowanie zakończone — brak nowych danych';
document.getElementById('enrichSubtitle').textContent = data.errors > 0 ? data.errors + ' błędów' : 'Wszystkie profile aktualne';
document.getElementById('enrichMiniStatus').textContent = 'Zakończono';
}
}
});