feat: add remediation effectiveness tracking to Problems tab
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
Shows whether password resets and welcome emails led to successful logins: - Summary cards: success rate, resolved/pending/failed counts, avg time to login - Detailed table: each action with user, type, date sent, and outcome - Resolved = user logged in after email, Pending = <48h, Failed = no login Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2aefbbf331
commit
01833aa567
@ -311,6 +311,102 @@ def _tab_problems(db, start_date, days):
|
||||
|
||||
never_logged_count = len(never_logged)
|
||||
|
||||
# ============================================================
|
||||
# REMEDIATION EFFECTIVENESS (30d)
|
||||
# ============================================================
|
||||
# All password resets and welcome emails sent in last 30d
|
||||
remediation_emails = db.query(EmailLog).filter(
|
||||
EmailLog.email_type.in_(['password_reset', 'welcome']),
|
||||
EmailLog.created_at >= start_30d,
|
||||
EmailLog.status == 'sent'
|
||||
).order_by(desc(EmailLog.created_at)).all()
|
||||
|
||||
# Batch: first login AFTER each email's sent time, per user
|
||||
rem_emails_list = [e.recipient_email for e in remediation_emails]
|
||||
rem_users_map = {}
|
||||
rem_logins_map = {} # email -> list of (action, timestamp)
|
||||
if rem_emails_list:
|
||||
rem_users_map = {u.email: u for u in db.query(User).filter(
|
||||
User.email.in_(set(rem_emails_list)), User.is_active == True
|
||||
).all()}
|
||||
# All successful logins for these users in last 30d
|
||||
login_rows = db.query(
|
||||
AuditLog.user_email, AuditLog.created_at
|
||||
).filter(
|
||||
AuditLog.user_email.in_(set(rem_emails_list)),
|
||||
AuditLog.action == 'login',
|
||||
AuditLog.created_at >= start_30d
|
||||
).order_by(AuditLog.created_at).all()
|
||||
for row in login_rows:
|
||||
rem_logins_map.setdefault(row.user_email, []).append(row.created_at)
|
||||
|
||||
remediation_items = []
|
||||
resolved_count = 0
|
||||
pending_count = 0
|
||||
failed_count = 0
|
||||
resolution_hours = []
|
||||
|
||||
for email_log in remediation_emails:
|
||||
u = rem_users_map.get(email_log.recipient_email)
|
||||
if not u:
|
||||
continue
|
||||
|
||||
# Find first login AFTER this email was sent
|
||||
user_logins = rem_logins_map.get(email_log.recipient_email, [])
|
||||
first_login_after = None
|
||||
for login_time in user_logins:
|
||||
if login_time > email_log.created_at:
|
||||
first_login_after = login_time
|
||||
break
|
||||
|
||||
if first_login_after:
|
||||
delta = first_login_after - email_log.created_at
|
||||
hours = delta.total_seconds() / 3600
|
||||
resolution_hours.append(hours)
|
||||
if hours < 1:
|
||||
time_label = f'{int(delta.total_seconds() / 60)} min'
|
||||
elif hours < 24:
|
||||
time_label = f'{hours:.1f} godz.'
|
||||
else:
|
||||
time_label = f'{delta.days} dni'
|
||||
result = 'resolved'
|
||||
result_label = f'Zalogował się po {time_label}'
|
||||
resolved_count += 1
|
||||
elif (now - email_log.created_at).total_seconds() < 48 * 3600:
|
||||
result = 'pending'
|
||||
result_label = 'Oczekuje'
|
||||
pending_count += 1
|
||||
else:
|
||||
result = 'failed'
|
||||
result_label = 'Brak loginu'
|
||||
failed_count += 1
|
||||
|
||||
action_labels = {
|
||||
'password_reset': 'Reset hasła',
|
||||
'welcome': 'Email powitalny',
|
||||
}
|
||||
|
||||
remediation_items.append({
|
||||
'user': u,
|
||||
'action': action_labels.get(email_log.email_type, email_log.email_type),
|
||||
'sent_at': email_log.created_at,
|
||||
'result': result,
|
||||
'result_label': result_label,
|
||||
})
|
||||
|
||||
total_rem = resolved_count + pending_count + failed_count
|
||||
avg_resolution_h = round(sum(resolution_hours) / len(resolution_hours), 1) if resolution_hours else None
|
||||
|
||||
remediation = {
|
||||
'items': remediation_items[:30],
|
||||
'total': total_rem,
|
||||
'resolved': resolved_count,
|
||||
'pending': pending_count,
|
||||
'failed': failed_count,
|
||||
'success_rate': round(resolved_count / total_rem * 100) if total_rem > 0 else 0,
|
||||
'avg_resolution_hours': avg_resolution_h,
|
||||
}
|
||||
|
||||
return {
|
||||
'locked_accounts': locked_accounts,
|
||||
'failed_logins': failed_logins_total,
|
||||
@ -320,6 +416,7 @@ def _tab_problems(db, start_date, days):
|
||||
'never_logged_in': never_logged_count,
|
||||
'problem_users': problem_users[:50],
|
||||
'alerts': alerts,
|
||||
'remediation': remediation,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -230,6 +230,82 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Remediation effectiveness -->
|
||||
{% if data.remediation and data.remediation.total > 0 %}
|
||||
<div class="section-card">
|
||||
<h2>Skuteczność działań <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(30 dni)</small></h2>
|
||||
|
||||
<div class="stats-grid" style="margin-bottom: var(--spacing-lg);">
|
||||
<div class="stat-card success">
|
||||
<div class="stat-value">{{ data.remediation.success_rate }}%</div>
|
||||
<div class="stat-label">Skuteczność</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-value">{{ data.remediation.resolved }}</div>
|
||||
<div class="stat-label">Zalogowali się</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-value">{{ data.remediation.pending }}</div>
|
||||
<div class="stat-label">Oczekują (< 48h)</div>
|
||||
</div>
|
||||
<div class="stat-card error">
|
||||
<div class="stat-value">{{ data.remediation.failed }}</div>
|
||||
<div class="stat-label">Brak reakcji</div>
|
||||
</div>
|
||||
{% if data.remediation.avg_resolution_hours is not none %}
|
||||
<div class="stat-card info">
|
||||
<div class="stat-value">
|
||||
{% if data.remediation.avg_resolution_hours < 1 %}
|
||||
{{ (data.remediation.avg_resolution_hours * 60)|int }} min
|
||||
{% elif data.remediation.avg_resolution_hours < 24 %}
|
||||
{{ data.remediation.avg_resolution_hours }} godz.
|
||||
{% else %}
|
||||
{{ (data.remediation.avg_resolution_hours / 24)|round(1) }} dni
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="stat-label">Śr. czas do loginu</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="table-scroll" style="max-height: 350px;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Użytkownik</th>
|
||||
<th>Akcja</th>
|
||||
<th>Wysłano</th>
|
||||
<th>Wynik</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in data.remediation.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-cell">
|
||||
<div class="user-avatar">{{ r.user.name[0] if r.user.name else '?' }}</div>
|
||||
<a href="{{ url_for('admin.user_insights_profile', user_id=r.user.id, ref_tab=tab, ref_period=period) }}">{{ r.user.name or r.user.email }}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ r.action }}</td>
|
||||
<td>{{ r.sent_at.strftime('%d.%m %H:%M') }}</td>
|
||||
<td>
|
||||
{% if r.result == 'resolved' %}
|
||||
<span class="badge badge-active">{{ r.result_label }}</span>
|
||||
{% elif r.result == 'pending' %}
|
||||
<span class="badge badge-medium">{{ r.result_label }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-high">{{ r.result_label }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-card">
|
||||
<h2>Użytkownicy z problemami
|
||||
<span class="badge {% if data.problem_users|length > 10 %}badge-high{% elif data.problem_users|length > 0 %}badge-medium{% else %}badge-ok{% endif %}">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user