Skip to content

Commit 16adca4

Browse files
committed
fix: enable real-time candidate activity tracking in admin interface
Database improvements: - Update get_all_candidate_invitations to include target_user_id via JOIN - Eliminate complex user lookup by providing direct user_id reference Admin interface fixes: - Replace placeholder activity display with real candidate activity data - Pass user_id directly to showActivity function instead of invitation_id - Add comprehensive activity display with icons, timing, and query details - Handle edge cases: no account created yet, no activities, API errors Activity display enhancements: - Show activity summary stats (total activities, query attempts) - Display recent activity in chronological table with details - Include query text preview and error messages - Add activity type icons and color coding for different events - Highlight failed queries and impersonation activities Error handling: - Clear messaging when candidate hasn't created account yet - Graceful fallback when API calls fail - Detailed error information for debugging This resolves the issue where candidate activities (query execution, page navigation, etc.) were not visible in the admin interface despite being properly logged in the database.
1 parent ce083de commit 16adca4

File tree

2 files changed

+131
-16
lines changed

2 files changed

+131
-16
lines changed

β€Žmodels/candidates.pyβ€Ž

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,10 @@ def get_all_candidate_invitations():
193193
conn = get_user_db_connection()
194194
try:
195195
invitations = conn.execute('''
196-
SELECT ci.*, u.username as created_by_name
196+
SELECT ci.*, u_creator.username as created_by_name, u_target.id as target_user_id
197197
FROM candidate_invitations ci
198-
JOIN users u ON ci.created_by = u.id
198+
JOIN users u_creator ON ci.created_by = u_creator.id
199+
LEFT JOIN users u_target ON ci.email = u_target.email
199200
ORDER BY ci.created_at DESC
200201
''').fetchall()
201202

β€Žtemplates/admin/candidate_invitations.htmlβ€Ž

Lines changed: 128 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ <h5 class="text-muted">No Invitations Created</h5>
236236
<button class="btn btn-outline-info" onclick="viewInvitationUrl('${invitation.invitation_token}', '${invitation.candidate_name}', '${invitation.email}')" title="View URL">
237237
<i class="fas fa-link"></i>
238238
</button>
239-
<button class="btn btn-outline-primary" onclick="showActivity(${invitation.id}, '${invitation.candidate_name}')" title="View Activity">
239+
<button class="btn btn-outline-primary" onclick="showActivity(${invitation.target_user_id}, '${invitation.candidate_name}')" title="View Activity">
240240
<i class="fas fa-chart-line"></i>
241241
</button>
242242
${invitation.is_active ? `
@@ -368,7 +368,7 @@ <h5 class="text-muted">No Invitations Created</h5>
368368
});
369369
}
370370

371-
function showActivity(invitationId, candidateName) {
371+
function showActivity(userId, candidateName) {
372372
document.getElementById('activityCandidateName').textContent = candidateName;
373373

374374
const modal = new bootstrap.Modal(document.getElementById('activityModal'));
@@ -384,22 +384,136 @@ <h5 class="text-muted">No Invitations Created</h5>
384384
</div>
385385
`;
386386

387-
// Note: This would need the user_id, not invitation_id
388-
// For now, show placeholder
389-
setTimeout(() => {
387+
// Check if userId is null (no user account created yet)
388+
if (!userId || userId === 'null') {
390389
document.getElementById('activityContent').innerHTML = `
391390
<div class="alert alert-info">
392-
<h6>Activity Tracking</h6>
393-
<p class="mb-0">Detailed candidate activity tracking will be displayed here, including:</p>
394-
<ul class="mb-0">
395-
<li>Query attempts and results</li>
396-
<li>Time spent on assessment</li>
397-
<li>Pages visited and navigation patterns</li>
398-
<li>Challenge completion status</li>
399-
</ul>
391+
<h6><i class="fas fa-info-circle me-2"></i>No Account Created Yet</h6>
392+
<p class="mb-0">This candidate hasn't used their invitation link yet, so no user account exists.</p>
393+
<p class="mb-0 mt-2"><small>Activity will appear here once they access their invitation URL and perform actions.</small></p>
394+
</div>
395+
`;
396+
return;
397+
}
398+
399+
// Fetch the actual activity data directly with the user_id
400+
fetch(`/api/admin/candidates/${userId}/activity`)
401+
.then(response => response.json())
402+
.then(data => {
403+
if (data.success) {
404+
displayCandidateActivity(data.activities, data.summary);
405+
} else {
406+
throw new Error(data.error || 'Failed to load activity');
407+
}
408+
})
409+
.catch(error => {
410+
console.error('Error loading candidate activity:', error);
411+
document.getElementById('activityContent').innerHTML = `
412+
<div class="alert alert-danger">
413+
<h6>Error Loading Activity</h6>
414+
<p class="mb-0">Unable to load candidate activity: ${error.message}</p>
415+
<p class="mb-0 mt-2"><small>This might be because the candidate hasn't performed any activities yet, or there was a technical issue.</small></p>
416+
</div>
417+
`;
418+
});
419+
}
420+
421+
function displayCandidateActivity(activities, summary) {
422+
const container = document.getElementById('activityContent');
423+
424+
if (!activities || activities.length === 0) {
425+
container.innerHTML = `
426+
<div class="alert alert-info">
427+
<h6><i class="fas fa-info-circle me-2"></i>No Activity Yet</h6>
428+
<p class="mb-0">This candidate hasn't performed any tracked activities yet.</p>
429+
<p class="mb-0 mt-2"><small>Activities include: query execution, page navigation, login/logout events, and impersonation sessions.</small></p>
430+
</div>
431+
`;
432+
return;
433+
}
434+
435+
let html = `
436+
<div class="row mb-3">
437+
<div class="col-md-6">
438+
<div class="card border-0 bg-light">
439+
<div class="card-body text-center py-2">
440+
<h6 class="mb-1">${summary?.activity_count || activities.length}</h6>
441+
<small class="text-muted">Total Activities</small>
442+
</div>
443+
</div>
400444
</div>
445+
<div class="col-md-6">
446+
<div class="card border-0 bg-light">
447+
<div class="card-body text-center py-2">
448+
<h6 class="mb-1">${summary?.query_attempts || 0}</h6>
449+
<small class="text-muted">Query Attempts</small>
450+
</div>
451+
</div>
452+
</div>
453+
</div>
454+
455+
<h6 class="mb-3">Recent Activity</h6>
456+
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
457+
<table class="table table-sm table-hover">
458+
<thead class="table-light sticky-top">
459+
<tr>
460+
<th>Time</th>
461+
<th>Activity</th>
462+
<th>Details</th>
463+
<th>Duration</th>
464+
</tr>
465+
</thead>
466+
<tbody>
467+
`;
468+
469+
activities.forEach(activity => {
470+
const timestamp = new Date(activity.timestamp).toLocaleString();
471+
const activityIcon = getActivityIcon(activity.activity_type);
472+
const activityClass = getActivityClass(activity.activity_type, activity.success);
473+
const duration = activity.execution_time_ms ? `${activity.execution_time_ms}ms` : '-';
474+
475+
html += `
476+
<tr class="${activityClass}">
477+
<td><small>${timestamp}</small></td>
478+
<td>
479+
<span class="badge bg-secondary me-1">${activityIcon}</span>
480+
<small>${activity.activity_type.replace('_', ' ')}</small>
481+
</td>
482+
<td>
483+
<small>${activity.details || '-'}</small>
484+
${activity.query_text ? `<br><code style="font-size: 0.7em;">${activity.query_text.substring(0, 100)}${activity.query_text.length > 100 ? '...' : ''}</code>` : ''}
485+
${activity.error_message ? `<br><small class="text-danger">Error: ${activity.error_message}</small>` : ''}
486+
</td>
487+
<td><small>${duration}</small></td>
488+
</tr>
401489
`;
402-
}, 1000);
490+
});
491+
492+
html += '</tbody></table></div>';
493+
container.innerHTML = html;
494+
}
495+
496+
function getActivityIcon(activityType) {
497+
const icons = {
498+
'query_executed': 'πŸ”',
499+
'page_access': 'πŸ“„',
500+
'candidate_login': 'πŸ”‘',
501+
'candidate_logout': 'πŸšͺ',
502+
'impersonation_started': 'πŸ‘€',
503+
'impersonation_ended': '❌',
504+
'impersonated_page_access': 'πŸ”πŸ‘€'
505+
};
506+
return icons[activityType] || 'πŸ“';
507+
}
508+
509+
function getActivityClass(activityType, success) {
510+
if (activityType === 'query_executed' && success === false) {
511+
return 'table-warning';
512+
}
513+
if (activityType.includes('impersonation')) {
514+
return 'table-info';
515+
}
516+
return '';
403517
}
404518

405519
function refreshInvitations() {

0 commit comments

Comments
Β (0)