feat: split problems into active vs resolved with collapsible resolved section
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
Users who had auth problems (failed logins, password resets, security alerts) but have since logged in successfully are now shown in a collapsed "Rozwiązane problemy" section. Active problems remain prominently displayed at the top. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ebd1d3fcce
commit
774ce6fca6
@ -163,9 +163,31 @@ def _tab_problems(db, start_date, days):
|
||||
PageView.load_time_ms > 3000
|
||||
).group_by(PageView.user_id).all())
|
||||
|
||||
# Problem users — dict lookups instead of per-user queries
|
||||
# Max timestamps for resolution detection
|
||||
last_failed_map = dict(db.query(
|
||||
AuditLog.user_email, func.max(AuditLog.created_at)
|
||||
).filter(
|
||||
AuditLog.action == 'login_failed',
|
||||
AuditLog.created_at >= start_dt
|
||||
).group_by(AuditLog.user_email).all())
|
||||
|
||||
last_reset_map = dict(db.query(
|
||||
EmailLog.recipient_email, func.max(EmailLog.created_at)
|
||||
).filter(
|
||||
EmailLog.email_type == 'password_reset',
|
||||
EmailLog.created_at >= start_30d
|
||||
).group_by(EmailLog.recipient_email).all())
|
||||
|
||||
last_sec_alert_map = dict(db.query(
|
||||
SecurityAlert.user_email, func.max(SecurityAlert.created_at)
|
||||
).filter(
|
||||
SecurityAlert.created_at >= start_dt
|
||||
).group_by(SecurityAlert.user_email).all())
|
||||
|
||||
# Problem users — split into active vs resolved
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
problem_users = []
|
||||
active_problems = []
|
||||
resolved_problems = []
|
||||
|
||||
for user in users:
|
||||
fl = failed_logins_map.get(user.email, 0)
|
||||
@ -185,7 +207,26 @@ def _tab_problems(db, start_date, days):
|
||||
)
|
||||
|
||||
if score > 0:
|
||||
problem_users.append({
|
||||
# Check if auth problems resolved by successful login after last incident
|
||||
auth_times = []
|
||||
if fl > 0 and user.email in last_failed_map:
|
||||
auth_times.append(last_failed_map[user.email])
|
||||
if pr_30d > 0 and user.email in last_reset_map:
|
||||
auth_times.append(last_reset_map[user.email])
|
||||
if sa_7d > 0 and user.email in last_sec_alert_map:
|
||||
auth_times.append(last_sec_alert_map[user.email])
|
||||
|
||||
has_auth = fl > 0 or pr_30d > 0 or sa_7d > 0 or is_locked
|
||||
last_auth_problem = max(auth_times) if auth_times else None
|
||||
|
||||
is_resolved = False
|
||||
resolved_at = None
|
||||
if has_auth and not is_locked and user.last_login and last_auth_problem:
|
||||
if user.last_login > last_auth_problem:
|
||||
is_resolved = True
|
||||
resolved_at = user.last_login
|
||||
|
||||
problem_data = {
|
||||
'user': user,
|
||||
'score': score,
|
||||
'failed_logins': fl,
|
||||
@ -195,9 +236,16 @@ def _tab_problems(db, start_date, days):
|
||||
'security_alerts': sa_7d,
|
||||
'is_locked': is_locked,
|
||||
'last_login': user.last_login,
|
||||
})
|
||||
'resolved_at': resolved_at,
|
||||
}
|
||||
|
||||
problem_users.sort(key=lambda x: x['score'], reverse=True)
|
||||
if is_resolved:
|
||||
resolved_problems.append(problem_data)
|
||||
else:
|
||||
active_problems.append(problem_data)
|
||||
|
||||
active_problems.sort(key=lambda x: x['score'], reverse=True)
|
||||
resolved_problems.sort(key=lambda x: x['score'], reverse=True)
|
||||
|
||||
# Proactive alerts
|
||||
alerts = []
|
||||
@ -414,7 +462,8 @@ def _tab_problems(db, start_date, days):
|
||||
'js_errors': js_errors_total,
|
||||
'security_alerts': security_alerts_total,
|
||||
'never_logged_in': never_logged_count,
|
||||
'problem_users': problem_users[:50],
|
||||
'active_problems': active_problems[:50],
|
||||
'resolved_problems': resolved_problems[:50],
|
||||
'alerts': alerts,
|
||||
'remediation': remediation,
|
||||
}
|
||||
@ -1491,12 +1540,14 @@ def user_insights_export():
|
||||
if export_type == 'problems':
|
||||
data = _tab_problems(db, start_date, days)
|
||||
writer.writerow(['Użytkownik', 'Email', 'Problem Score', 'Nieudane logowania',
|
||||
'Resety hasła', 'Błędy JS', 'Wolne strony', 'Ostatni login'])
|
||||
for p in data['problem_users']:
|
||||
'Resety hasła', 'Błędy JS', 'Wolne strony', 'Ostatni login', 'Status'])
|
||||
all_problems = data['active_problems'] + data['resolved_problems']
|
||||
for p in all_problems:
|
||||
writer.writerow([
|
||||
p['user'].name, p['user'].email, p['score'],
|
||||
p['failed_logins'], p['password_resets'], p['js_errors'],
|
||||
p['slow_pages'], p['last_login'] or 'Nigdy'
|
||||
p['slow_pages'], p['last_login'] or 'Nigdy',
|
||||
'Rozwiązany' if p['resolved_at'] else 'Aktywny'
|
||||
])
|
||||
|
||||
elif export_type == 'engagement':
|
||||
|
||||
@ -135,6 +135,10 @@
|
||||
.alert-action a { padding: 4px 12px; background: white; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--font-size-xs); color: var(--primary); text-decoration: none; }
|
||||
.alert-action a:hover { background: var(--background); }
|
||||
|
||||
/* Resolved section toggle */
|
||||
details[open] summary span:first-child { transform: rotate(90deg); }
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.insights-header { flex-direction: column; align-items: flex-start; }
|
||||
@ -306,14 +310,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Active problems -->
|
||||
<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 %}">
|
||||
{{ data.problem_users|length }}
|
||||
<h2>Aktywne problemy
|
||||
<span class="badge {% if data.active_problems|length > 10 %}badge-critical{% elif data.active_problems|length > 0 %}badge-high{% else %}badge-ok{% endif %}">
|
||||
{{ data.active_problems|length }}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{% if data.problem_users %}
|
||||
{% if data.active_problems %}
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
@ -330,7 +335,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in data.problem_users %}
|
||||
{% for p in data.active_problems %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-cell">
|
||||
@ -357,11 +362,56 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">
|
||||
<p>Brak użytkowników z problemami w wybranym okresie.</p>
|
||||
<p>Brak aktywnych problemów w wybranym okresie.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Resolved problems (collapsible) -->
|
||||
{% if data.resolved_problems %}
|
||||
<div class="section-card" style="opacity: 0.8;">
|
||||
<details>
|
||||
<summary style="cursor: pointer; display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-lg); font-weight: 600; padding: var(--spacing-xs) 0; list-style: none;">
|
||||
<span style="transition: transform 0.2s; display: inline-block;">▶</span>
|
||||
Rozwiązane problemy
|
||||
<span class="badge badge-ok">{{ data.resolved_problems|length }}</span>
|
||||
<span style="font-size: var(--font-size-xs); font-weight: 400; color: var(--text-muted);">— zalogowali się po problemach</span>
|
||||
</summary>
|
||||
<div class="table-scroll" style="margin-top: var(--spacing-md);">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Użytkownik</th>
|
||||
<th>Było problemów</th>
|
||||
<th>Nieudane logowania</th>
|
||||
<th>Resety hasła</th>
|
||||
<th>Alerty</th>
|
||||
<th>Rozwiązano</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in data.resolved_problems %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-cell">
|
||||
<div class="user-avatar" style="background: var(--success);">{{ p.user.name[0] if p.user.name else '?' }}</div>
|
||||
<a href="{{ url_for('admin.user_insights_profile', user_id=p.user.id, ref_tab=tab, ref_period=period) }}">{{ p.user.name or p.user.email }}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge badge-ok">{{ p.score }}</span></td>
|
||||
<td>{{ p.failed_logins }}</td>
|
||||
<td>{{ p.password_resets }}</td>
|
||||
<td>{{ p.security_alerts }}</td>
|
||||
<td><span class="badge badge-active">Zalogowano {{ p.resolved_at.strftime('%d.%m %H:%M') }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TAB: ENGAGEMENT -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user