feat: add batch GBP audit button to admin dashboard
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
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
Run audit for all active companies with background thread, live progress panel, and optional Google Places API data refresh. Pattern mirrors existing social audit batch implementation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
700382b55d
commit
ef125baf57
@ -5,10 +5,15 @@ Admin Audit Routes
|
||||
SEO and GBP audit dashboards for admin panel.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
from flask import abort, render_template, request, redirect, url_for, flash
|
||||
from flask import abort, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from . import bp
|
||||
@ -22,6 +27,98 @@ from utils.decorators import role_required, is_audit_owner
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# GBP BATCH AUDIT STATE (shared file for multi-worker safety)
|
||||
# ============================================================
|
||||
|
||||
_GBP_BATCH_STATE_FILE = os.path.join(tempfile.gettempdir(), 'nordabiz_gbp_batch_state.json')
|
||||
|
||||
_GBP_BATCH_DEFAULT = {
|
||||
'running': False,
|
||||
'progress': 0,
|
||||
'total': 0,
|
||||
'completed': 0,
|
||||
'errors': 0,
|
||||
'results': [],
|
||||
}
|
||||
|
||||
|
||||
def _read_gbp_batch_state():
|
||||
try:
|
||||
with open(_GBP_BATCH_STATE_FILE, 'r') as f:
|
||||
fcntl.flock(f, fcntl.LOCK_SH)
|
||||
data = json.load(f)
|
||||
fcntl.flock(f, fcntl.LOCK_UN)
|
||||
return data
|
||||
except (FileNotFoundError, json.JSONDecodeError, IOError):
|
||||
return dict(_GBP_BATCH_DEFAULT)
|
||||
|
||||
|
||||
def _write_gbp_batch_state(state):
|
||||
try:
|
||||
tmp_path = _GBP_BATCH_STATE_FILE + '.tmp'
|
||||
with open(tmp_path, 'w') as f:
|
||||
fcntl.flock(f, fcntl.LOCK_EX)
|
||||
json.dump(state, f, default=str)
|
||||
fcntl.flock(f, fcntl.LOCK_UN)
|
||||
os.replace(tmp_path, _GBP_BATCH_STATE_FILE)
|
||||
except IOError as e:
|
||||
logger.error(f"Failed to write GBP batch state: {e}")
|
||||
|
||||
|
||||
def _run_gbp_batch_background(company_ids, fetch_google):
|
||||
"""Background thread: audit all companies one by one."""
|
||||
from gbp_audit_service import GBPAuditService, fetch_google_business_data
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
service = GBPAuditService(db)
|
||||
state = _read_gbp_batch_state()
|
||||
|
||||
for i, company_id in enumerate(company_ids):
|
||||
company_name = '?'
|
||||
try:
|
||||
company = db.get(Company, company_id)
|
||||
company_name = company.name if company else f'ID {company_id}'
|
||||
|
||||
if fetch_google:
|
||||
fetch_google_business_data(db, company_id, force_refresh=True)
|
||||
|
||||
result = service.audit_company(company_id)
|
||||
service.save_audit(result, source='automated')
|
||||
|
||||
state['results'].append({
|
||||
'company_id': company_id,
|
||||
'company_name': company_name,
|
||||
'score': result.completeness_score,
|
||||
'status': 'ok',
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"GBP batch audit failed for company {company_id}: {e}")
|
||||
state['errors'] += 1
|
||||
state['results'].append({
|
||||
'company_id': company_id,
|
||||
'company_name': company_name,
|
||||
'score': None,
|
||||
'status': 'error',
|
||||
'error': str(e)[:100],
|
||||
})
|
||||
|
||||
state['completed'] = i + 1
|
||||
state['progress'] = round((i + 1) / state['total'] * 100)
|
||||
_write_gbp_batch_state(state)
|
||||
|
||||
state['running'] = False
|
||||
_write_gbp_batch_state(state)
|
||||
except Exception as e:
|
||||
logger.error(f"GBP batch audit thread crashed: {e}")
|
||||
state = _read_gbp_batch_state()
|
||||
state['running'] = False
|
||||
_write_gbp_batch_state(state)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SEO ADMIN DASHBOARD
|
||||
# ============================================================
|
||||
@ -612,6 +709,79 @@ def admin_gbp_audit():
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/gbp-audit/run-batch', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
def admin_gbp_audit_run_batch():
|
||||
"""Start batch GBP audit for all active companies."""
|
||||
if not is_audit_owner():
|
||||
return jsonify({'error': 'Brak uprawnień'}), 403
|
||||
|
||||
state = _read_gbp_batch_state()
|
||||
if state.get('running'):
|
||||
return jsonify({
|
||||
'error': 'Audyt już działa',
|
||||
'progress': state.get('progress', 0),
|
||||
'completed': state.get('completed', 0),
|
||||
'total': state.get('total', 0),
|
||||
}), 409
|
||||
|
||||
fetch_google = request.form.get('fetch_google', '0') == '1'
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company_ids = [c.id for c in db.query(Company.id).filter(Company.status == 'active').all()]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if not company_ids:
|
||||
return jsonify({'error': 'Brak aktywnych firm'}), 400
|
||||
|
||||
_write_gbp_batch_state({
|
||||
'running': True,
|
||||
'progress': 0,
|
||||
'total': len(company_ids),
|
||||
'completed': 0,
|
||||
'errors': 0,
|
||||
'results': [],
|
||||
})
|
||||
|
||||
thread = threading.Thread(
|
||||
target=_run_gbp_batch_background,
|
||||
args=(company_ids, fetch_google),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'total': len(company_ids),
|
||||
'message': f'Rozpoczęto audyt GBP dla {len(company_ids)} firm.',
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/gbp-audit/batch-status')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_gbp_audit_batch_status():
|
||||
"""Get current batch GBP audit status with live results feed."""
|
||||
state = _read_gbp_batch_state()
|
||||
results = state.get('results', [])
|
||||
|
||||
since = request.args.get('since', 0, type=int)
|
||||
new_results = results[since:]
|
||||
|
||||
return jsonify({
|
||||
'running': state.get('running', False),
|
||||
'progress': state.get('progress', 0),
|
||||
'completed': state.get('completed', 0),
|
||||
'total': state.get('total', 0),
|
||||
'errors': state.get('errors', 0),
|
||||
'results': new_results,
|
||||
'results_total': len(results),
|
||||
})
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DIGITAL MATURITY DASHBOARD
|
||||
# ============================================================
|
||||
|
||||
@ -432,6 +432,8 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -448,6 +450,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="gbpBatchBtn" class="btn btn-primary btn-sm" onclick="startGbpBatch()" title="Uruchom audyt GBP dla wszystkich firm">
|
||||
<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>
|
||||
<a href="{{ url_for('api.api_gbp_audit_health') }}" class="btn btn-outline btn-sm" target="_blank">
|
||||
<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"/>
|
||||
@ -457,6 +465,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<div id="gbpBatchConfirm" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div style="background:white; border-radius:var(--radius-lg); padding:var(--spacing-xl); max-width:480px; width:90%; box-shadow:var(--shadow-lg);">
|
||||
<h3 style="margin-bottom:var(--spacing-md);">Uruchom audyt GBP</h3>
|
||||
<p style="color:var(--text-secondary); margin-bottom:var(--spacing-md);">
|
||||
Audyt sprawdzi kompletnosc profili Google Business Profile dla wszystkich aktywnych firm.
|
||||
Wyniki zostana zapisane automatycznie.
|
||||
</p>
|
||||
<div style="margin-bottom:var(--spacing-lg);">
|
||||
<label style="display:flex; align-items:center; gap:var(--spacing-sm); cursor:pointer;">
|
||||
<input type="checkbox" id="gbpFetchGoogle" checked>
|
||||
<span style="font-size:var(--font-size-sm);">Pobierz aktualne dane z Google Places API (wolniejsze, ale dokladniejsze)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex; gap:var(--spacing-sm); justify-content:flex-end;">
|
||||
<button class="btn btn-outline btn-sm" onclick="document.getElementById('gbpBatchConfirm').style.display='none'">Anuluj</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('gbpBatchConfirm').style.display='none'; doStartGbpBatch();">
|
||||
<svg width="14" height="14" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live batch audit panel -->
|
||||
<div id="gbpBatchPanel" 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="gbpBatchSpinner" 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="gbpBatchTitle">Audyt GBP w toku...</div>
|
||||
<div style="font-size:var(--font-size-xs); color:#3b82f6;" id="gbpBatchSubtitle">Sprawdzanie profili Google Business Profile</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:var(--font-size-2xl); font-weight:700; color:#1e40af;" id="gbpBatchCounter">0 / 0</div>
|
||||
<div style="font-size:var(--font-size-xs); color:#3b82f6;" id="gbpBatchErrors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:6px; background:#e0e7ff;">
|
||||
<div id="gbpBatchProgressBar" style="height:100%; background:#2563eb; transition:width 0.3s; width:0%;"></div>
|
||||
</div>
|
||||
<div id="gbpBatchFeed" style="max-height:300px; overflow-y:auto; padding:var(--spacing-sm) var(--spacing-lg); font-size:var(--font-size-sm); font-family:monospace;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
@ -775,4 +826,121 @@ function resetFilters() {
|
||||
document.getElementById('filterSearch').value = '';
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GBP Batch Audit
|
||||
// ============================================================
|
||||
var _gbpBatchSince = 0;
|
||||
|
||||
function startGbpBatch() {
|
||||
document.getElementById('gbpBatchConfirm').style.display = 'flex';
|
||||
}
|
||||
|
||||
function doStartGbpBatch() {
|
||||
var btn = document.getElementById('gbpBatchBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Uruchamianie...';
|
||||
_gbpBatchSince = 0;
|
||||
|
||||
var fetchGoogle = document.getElementById('gbpFetchGoogle').checked ? '1' : '0';
|
||||
|
||||
fetch('{{ url_for("admin.admin_gbp_audit_run_batch") }}', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||||
body: 'fetch_google=' + fetchGoogle
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.status === 'started') {
|
||||
document.getElementById('gbpBatchPanel').style.display = 'block';
|
||||
document.getElementById('gbpBatchCounter').textContent = '0 / ' + data.total;
|
||||
document.getElementById('gbpBatchFeed').innerHTML = '';
|
||||
pollGbpBatch();
|
||||
} else {
|
||||
document.getElementById('gbpBatchPanel').style.display = 'block';
|
||||
document.getElementById('gbpBatchTitle').textContent = data.error || 'Blad uruchamiania';
|
||||
document.getElementById('gbpBatchTitle').style.color = '#dc2626';
|
||||
document.getElementById('gbpBatchSpinner').style.display = 'none';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Uruchom audyt';
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
document.getElementById('gbpBatchPanel').style.display = 'block';
|
||||
document.getElementById('gbpBatchTitle').textContent = 'Blad polaczenia: ' + e.message;
|
||||
document.getElementById('gbpBatchTitle').style.color = '#dc2626';
|
||||
document.getElementById('gbpBatchSpinner').style.display = 'none';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Uruchom audyt';
|
||||
});
|
||||
}
|
||||
|
||||
function pollGbpBatch() {
|
||||
fetch('{{ url_for("admin.admin_gbp_audit_batch_status") }}?since=' + _gbpBatchSince)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
document.getElementById('gbpBatchCounter').textContent = data.completed + ' / ' + data.total;
|
||||
document.getElementById('gbpBatchProgressBar').style.width = data.progress + '%';
|
||||
|
||||
if (data.errors > 0) {
|
||||
document.getElementById('gbpBatchErrors').textContent = data.errors + ' bledow';
|
||||
}
|
||||
|
||||
// Append new feed entries
|
||||
var feed = document.getElementById('gbpBatchFeed');
|
||||
data.results.forEach(function(r) {
|
||||
var line = document.createElement('div');
|
||||
line.style.padding = '3px 0';
|
||||
line.style.borderBottom = '1px solid #f3f4f6';
|
||||
if (r.status === 'ok') {
|
||||
var scoreColor = r.score >= 90 ? '#166534' : (r.score >= 70 ? '#92400e' : '#991b1b');
|
||||
var scoreBg = r.score >= 90 ? '#dcfce7' : (r.score >= 70 ? '#fef3c7' : '#fee2e2');
|
||||
line.innerHTML = '<span style="color:#22c55e;">✓</span> ' + r.company_name +
|
||||
' <span style="background:' + scoreBg + '; color:' + scoreColor + '; padding:1px 6px; border-radius:3px; font-size:11px;">' + r.score + '%</span>';
|
||||
} else {
|
||||
line.innerHTML = '<span style="color:#dc2626;">✗</span> ' + r.company_name +
|
||||
' <span style="color:#dc2626; font-size:11px;">' + (r.error || 'blad') + '</span>';
|
||||
}
|
||||
feed.appendChild(line);
|
||||
});
|
||||
feed.scrollTop = feed.scrollHeight;
|
||||
_gbpBatchSince += data.results.length;
|
||||
|
||||
if (data.running) {
|
||||
setTimeout(pollGbpBatch, 2000);
|
||||
} else {
|
||||
// Completed
|
||||
document.getElementById('gbpBatchSpinner').style.animation = 'none';
|
||||
document.getElementById('gbpBatchSpinner').style.borderColor = '#22c55e';
|
||||
document.getElementById('gbpBatchTitle').textContent = 'Audyt zakonczony — ' + data.completed + ' firm sprawdzonych';
|
||||
document.getElementById('gbpBatchTitle').style.color = '#166534';
|
||||
document.getElementById('gbpBatchSubtitle').textContent = data.errors > 0 ? (data.errors + ' bledow') : 'Wszystkie firmy zbadane pomyslnie';
|
||||
|
||||
var btn = document.getElementById('gbpBatchBtn');
|
||||
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';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
setTimeout(pollGbpBatch, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if batch is already running on page load
|
||||
(function() {
|
||||
fetch('{{ url_for("admin.admin_gbp_audit_batch_status") }}?since=0')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.running) {
|
||||
document.getElementById('gbpBatchPanel').style.display = 'block';
|
||||
document.getElementById('gbpBatchCounter').textContent = data.completed + ' / ' + data.total;
|
||||
document.getElementById('gbpBatchProgressBar').style.width = data.progress + '%';
|
||||
document.getElementById('gbpBatchBtn').disabled = true;
|
||||
document.getElementById('gbpBatchBtn').textContent = 'Audyt w toku...';
|
||||
_gbpBatchSince = data.results_total;
|
||||
pollGbpBatch();
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user