fix: decode HTML entities in social audit, replace browser dialogs with modals
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

- Fix checkmarks showing as ✓ by using Unicode ✓/✗ directly
- Decode HTML entities (' &) from og:meta in enricher results
- Replace native confirm()/alert() with styled modal dialogs and toasts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-12 12:31:02 +01:00
parent 23b8815e10
commit ce0a6863d2
3 changed files with 117 additions and 13 deletions

View File

@ -21,6 +21,7 @@ Author: Claude Code
Date: 2025-12-29
"""
import html as html_module
import os
import sys
import json
@ -1096,7 +1097,12 @@ class SocialProfileEnricher:
enricher = enrichers.get(platform)
if enricher:
try:
return enricher(url)
result = enricher(url)
# Decode HTML entities in all string values (og:meta often contains &amp; &#39; etc.)
for key, val in result.items():
if isinstance(val, str):
result[key] = html_module.unescape(val)
return result
except Exception as e:
logger.warning(f"Failed to enrich {platform} profile {url}: {e}")
return {}

View File

@ -694,7 +694,7 @@
<div class="profile-checklist">
{% if p.has_profile_photo is not none %}
<span class="check-item {{ 'ok' if p.has_profile_photo else 'missing' }}">
{{ '&#10003;' if p.has_profile_photo else '&#10007;' }} Zdjęcie profilowe
{{ '✓' if p.has_profile_photo else '✗' }} Zdjęcie profilowe
</span>
{% else %}
<span class="check-item unknown">? Zdjęcie profilowe</span>
@ -702,7 +702,7 @@
{% if p.has_cover_photo is not none %}
<span class="check-item {{ 'ok' if p.has_cover_photo else 'missing' }}">
{{ '&#10003;' if p.has_cover_photo else '&#10007;' }} Zdjęcie w tle
{{ '✓' if p.has_cover_photo else '✗' }} Zdjęcie w tle
</span>
{% else %}
<span class="check-item unknown">? Zdjęcie w tle</span>
@ -710,7 +710,7 @@
{% if p.has_bio is not none %}
<span class="check-item {{ 'ok' if p.has_bio else 'missing' }}">
{{ '&#10003;' if p.has_bio else '&#10007;' }} Opis / bio
{{ '✓' if p.has_bio else '✗' }} Opis / bio
</span>
{% else %}
<span class="check-item unknown">? Opis / bio</span>

View File

@ -323,6 +323,41 @@
</div>
{% endif %}
</div>
<!-- Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal-box">
<div class="modal-icon" id="modalIcon">
<svg width="28" height="28" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
</div>
<h3 class="modal-title" id="modalTitle"></h3>
<p class="modal-message" id="modalMessage"></p>
<p class="modal-detail" id="modalDetail"></p>
<div class="modal-actions" id="modalActions"></div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 3000; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 2000; justify-content: center; align-items: center; animation: modalFadeIn 0.2s ease; }
.modal-overlay.active { display: flex; }
.modal-box { background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl); max-width: 440px; width: 90%; text-align: center; box-shadow: 0 20px 40px rgba(0,0,0,0.2); animation: modalSlideUp 0.3s ease; }
@keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalSlideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.modal-icon { width: 56px; height: 56px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto var(--spacing-md); }
.modal-icon.warn { background: #fef3c7; color: #f59e0b; }
.modal-icon.danger { background: #fee2e2; color: #ef4444; }
.modal-icon.success { background: #dcfce7; color: #22c55e; }
.modal-title { font-size: var(--font-size-lg); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-sm); }
.modal-message { color: var(--text-secondary); margin-bottom: var(--spacing-xs); line-height: 1.5; }
.modal-detail { font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-lg); }
.modal-actions { display: flex; gap: var(--spacing-sm); justify-content: center; }
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; max-width: 400px; }
.toast.success { border-left-color: #22c55e; }
.toast.error { border-left-color: #ef4444; }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_js %}
@ -333,9 +368,60 @@ function toggleCompany(header) {
arrow.classList.toggle('open');
}
function approveChanges() {
if (!confirm('Czy na pewno chcesz zatwierdzić i zapisać zebrane dane do bazy?\n\nTa operacja zaktualizuje {{ summary.profiles_with_changes }} profili w {{ summary.companies_with_changes }} firmach.')) return;
// Toast
function showToast(message, type, duration) {
type = type || 'info'; duration = duration || 4000;
var icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
var container = document.getElementById('toastContainer');
var toast = document.createElement('div');
toast.className = 'toast ' + type;
toast.innerHTML = '<span style="font-size:1.2em">' + (icons[type]||'') + '</span><span>' + message + '</span>';
container.appendChild(toast);
setTimeout(function() { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(function() { toast.remove(); }, 300); }, duration);
}
// Modal
function showModal(opts) {
var icon = document.getElementById('modalIcon');
icon.className = 'modal-icon ' + (opts.iconType || 'warn');
icon.innerHTML = opts.iconSvg || '<svg width="28" height="28" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>';
document.getElementById('modalTitle').textContent = opts.title;
document.getElementById('modalMessage').innerHTML = opts.message;
document.getElementById('modalDetail').textContent = opts.detail || '';
var actions = document.getElementById('modalActions');
actions.innerHTML = '';
(opts.buttons || []).forEach(function(b) {
var btn = document.createElement('button');
btn.className = 'btn ' + (b.cls || 'btn-outline');
btn.textContent = b.label;
btn.onclick = function() { closeModal(); if (b.action) b.action(); };
actions.appendChild(btn);
});
document.getElementById('confirmModal').classList.add('active');
}
function closeModal() {
document.getElementById('confirmModal').classList.remove('active');
}
document.getElementById('confirmModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
function approveChanges() {
showModal({
title: 'Zatwierdź zmiany',
iconType: 'warn',
message: 'Czy na pewno chcesz zatwierdzić i zapisać zebrane dane do bazy?',
detail: 'Ta operacja zaktualizuje {{ summary.profiles_with_changes }} profili w {{ summary.companies_with_changes }} firmach.',
buttons: [
{ label: 'Anuluj', cls: 'btn-outline' },
{ label: 'Zatwierdź i zapisz', cls: 'btn-primary', action: doApprove }
]
});
}
function doApprove() {
var approveBtn = document.getElementById('approveBtn');
var discardBtn = document.getElementById('discardBtn');
if (approveBtn) { approveBtn.disabled = true; approveBtn.textContent = 'Zapisywanie...'; }
@ -348,24 +434,36 @@ function approveChanges() {
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'approved') {
alert('Zatwierdzone! Zaktualizowano ' + data.applied + ' profili.' + (data.errors > 0 ? ' (' + data.errors + ' błędów)' : ''));
location.reload();
showToast('Zaktualizowano ' + data.applied + ' profili' + (data.errors > 0 ? ' (' + data.errors + ' błędów)' : ''), 'success', 5000);
setTimeout(function() { location.reload(); }, 1500);
} else {
alert('Błąd: ' + (data.error || 'Nieznany błąd'));
showToast('Błąd: ' + (data.error || 'Nieznany błąd'), 'error');
if (approveBtn) { approveBtn.disabled = false; approveBtn.textContent = 'Zatwierdź'; }
if (discardBtn) discardBtn.disabled = false;
}
})
.catch(function(e) {
alert('Błąd: ' + e.message);
showToast('Błąd połączenia: ' + e.message, 'error');
if (approveBtn) { approveBtn.disabled = false; approveBtn.textContent = 'Zatwierdź'; }
if (discardBtn) discardBtn.disabled = false;
});
}
function discardChanges() {
if (!confirm('Czy na pewno chcesz ODRZUCIĆ wszystkie zebrane dane?\n\nBaza danych nie zostanie zmieniona.')) return;
showModal({
title: 'Odrzuć zmiany',
iconType: 'danger',
iconSvg: '<svg width="28" height="28" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>',
message: 'Czy na pewno chcesz odrzucić wszystkie zebrane dane?',
detail: 'Baza danych nie zostanie zmieniona.',
buttons: [
{ label: 'Anuluj', cls: 'btn-outline' },
{ label: 'Odrzuć', cls: 'btn-danger', action: doDiscard }
]
});
}
function doDiscard() {
fetch('{{ url_for("admin.admin_social_audit_enrichment_discard") }}', {
method: 'POST',
headers: {'X-CSRFToken': '{{ csrf_token() }}'},
@ -373,8 +471,8 @@ function discardChanges() {
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'discarded') {
alert('Odrzucono ' + data.count + ' zmian. Baza danych nie została zmieniona.');
window.location.href = '{{ url_for("admin.admin_social_audit") }}';
showToast('Odrzucono ' + data.count + ' zmian. Baza bez zmian.', 'success');
setTimeout(function() { window.location.href = '{{ url_for("admin.admin_social_audit") }}'; }, 1500);
}
});
}