feat(social-audit): Add ability to run Social Media audit
- Add "Uruchom audyt" button to social_audit.html - Create POST /api/social/audit endpoint to verify profile URLs - Add loading overlay and modal for audit progress/results - Audit verifies each social media URL and updates check_status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
505800381e
commit
cadf91b481
143
app.py
143
app.py
@ -4159,6 +4159,149 @@ def social_audit_dashboard(slug):
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/social/audit', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("10 per hour")
|
||||
def api_social_audit_trigger():
|
||||
"""
|
||||
API: Trigger Social Media audit for a company.
|
||||
|
||||
This endpoint verifies social media profile URLs and updates their status.
|
||||
It checks if URLs are valid and accessible.
|
||||
|
||||
Request JSON body:
|
||||
- company_id: Company ID (integer) OR
|
||||
- slug: Company slug (string)
|
||||
|
||||
Returns:
|
||||
- Success: Updated social media audit results
|
||||
- Error: Error message with status code
|
||||
|
||||
Rate limited to 10 requests per hour per user.
|
||||
"""
|
||||
import requests as http_requests
|
||||
|
||||
# Admin or company owner check
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Brak danych w żądaniu. Podaj company_id lub slug.'
|
||||
}), 400
|
||||
|
||||
company_id = data.get('company_id')
|
||||
slug = data.get('slug')
|
||||
|
||||
if not company_id and not slug:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Podaj company_id lub slug firmy do audytu.'
|
||||
}), 400
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Find company by ID or slug
|
||||
if company_id:
|
||||
company = db.query(Company).filter_by(id=company_id, status='active').first()
|
||||
else:
|
||||
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
||||
|
||||
if not company:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Firma nie znaleziona lub nieaktywna.'
|
||||
}), 404
|
||||
|
||||
# Access control - admin can audit all, users only their company
|
||||
if not current_user.is_admin:
|
||||
if current_user.company_id != company.id:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Brak uprawnień do audytu social media tej firmy.'
|
||||
}), 403
|
||||
|
||||
logger.info(f"Social Media audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})")
|
||||
|
||||
# Get existing social media profiles
|
||||
profiles = db.query(CompanySocialMedia).filter(
|
||||
CompanySocialMedia.company_id == company.id
|
||||
).all()
|
||||
|
||||
verified_count = 0
|
||||
errors = []
|
||||
|
||||
# Verify each profile URL
|
||||
for profile in profiles:
|
||||
try:
|
||||
response = http_requests.head(
|
||||
profile.url,
|
||||
timeout=10,
|
||||
allow_redirects=True,
|
||||
headers={'User-Agent': 'Mozilla/5.0 (compatible; NordaBizBot/1.0)'}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
profile.is_valid = True
|
||||
profile.check_status = 'ok'
|
||||
verified_count += 1
|
||||
elif response.status_code in [301, 302, 303, 307, 308]:
|
||||
profile.is_valid = True
|
||||
profile.check_status = 'redirect'
|
||||
verified_count += 1
|
||||
elif response.status_code == 404:
|
||||
profile.is_valid = False
|
||||
profile.check_status = '404'
|
||||
else:
|
||||
profile.is_valid = True # Assume valid if we get any response
|
||||
profile.check_status = f'http_{response.status_code}'
|
||||
verified_count += 1
|
||||
|
||||
profile.last_checked_at = datetime.now()
|
||||
|
||||
except http_requests.exceptions.Timeout:
|
||||
profile.check_status = 'timeout'
|
||||
profile.last_checked_at = datetime.now()
|
||||
errors.append(f'{profile.platform}: timeout')
|
||||
except http_requests.exceptions.ConnectionError:
|
||||
profile.check_status = 'connection_error'
|
||||
profile.last_checked_at = datetime.now()
|
||||
errors.append(f'{profile.platform}: connection error')
|
||||
except Exception as e:
|
||||
profile.check_status = 'error'
|
||||
profile.last_checked_at = datetime.now()
|
||||
errors.append(f'{profile.platform}: {str(e)[:50]}')
|
||||
|
||||
db.commit()
|
||||
|
||||
# Calculate score
|
||||
all_platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok']
|
||||
profiles_dict = {p.platform: p for p in profiles}
|
||||
platforms_with_profiles = len([p for p in all_platforms if p in profiles_dict])
|
||||
score = int((platforms_with_profiles / len(all_platforms)) * 100)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Audyt Social Media zakończony. Zweryfikowano {verified_count} z {len(profiles)} profili.',
|
||||
'company_id': company.id,
|
||||
'company_name': company.name,
|
||||
'profiles_count': len(profiles),
|
||||
'verified_count': verified_count,
|
||||
'score': score,
|
||||
'errors': errors if errors else None
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Social Media audit error for company {slug or company_id}: {e}")
|
||||
db.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Błąd podczas audytu: {str(e)}'
|
||||
}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# GBP AUDIT USER-FACING DASHBOARD
|
||||
# ============================================================
|
||||
|
||||
@ -423,6 +423,142 @@
|
||||
.recommendation-text strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e2e8f0;
|
||||
border-top-color: #a855f7;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto var(--spacing-md);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.loading-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto var(--spacing-md);
|
||||
}
|
||||
|
||||
.modal-icon.success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.modal-icon.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
background: #a855f7;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
border-radius: var(--radius);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
background: #9333ea;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -454,6 +590,14 @@
|
||||
</svg>
|
||||
Profil firmy
|
||||
</a>
|
||||
{% if can_audit %}
|
||||
<button class="btn btn-primary btn-sm" onclick="runAudit()" id="runAuditBtn">
|
||||
<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
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -665,4 +809,103 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-title">Trwa audyt Social Media...</div>
|
||||
<div class="loading-description">Weryfikujemy profile na platformach spolecznosciowych. Moze to potrwac kilka sekund.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal-overlay" id="modalOverlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-icon" id="modalIcon">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="modal-title" id="modalTitle">Sukces</div>
|
||||
<div class="modal-description" id="modalDescription">Operacja zakonczona pomyslnie.</div>
|
||||
<button class="modal-btn" onclick="closeModal()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
const csrfToken = '{{ csrf_token() }}';
|
||||
const companySlug = '{{ company.slug }}';
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('loadingOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loadingOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
function showModal(title, description, isSuccess) {
|
||||
const modal = document.getElementById('modalOverlay');
|
||||
const icon = document.getElementById('modalIcon');
|
||||
const titleEl = document.getElementById('modalTitle');
|
||||
const descEl = document.getElementById('modalDescription');
|
||||
|
||||
titleEl.textContent = title;
|
||||
descEl.textContent = description;
|
||||
|
||||
icon.className = 'modal-icon ' + (isSuccess ? 'success' : 'error');
|
||||
icon.innerHTML = isSuccess
|
||||
? '<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>'
|
||||
: '<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>';
|
||||
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
async function runAudit() {
|
||||
const btn = document.getElementById('runAuditBtn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
}
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/social/audit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ slug: companySlug })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
hideLoading();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
showModal('Audyt zakonczony', 'Audyt Social Media zostal zakonczony pomyslnie. Strona zostanie odswiezona.', true);
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showModal('Blad', data.error || 'Wystapil nieznany blad podczas audytu.', false);
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
showModal('Blad polaczenia', 'Nie udalo sie polaczyc z serwerem: ' + error.message, false);
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal on overlay click
|
||||
document.getElementById('modalOverlay').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user