mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 15:18:02 -04:00
Show returning users count in admin dashboard (#720)
This commit is contained in:
committed by
Leendert de Borst
parent
179bb62604
commit
05edda8b48
@@ -1,11 +1,6 @@
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Total active users</h3>
|
||||
<button
|
||||
@onclick="ToggleUserNames"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
@(ShowUserNames ? "Hide names" : "Show names")
|
||||
</button>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
@@ -16,63 +11,31 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last24Hours</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last24HourUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last24Hours</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users">(@UserStats.ReturningLast24Hours)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 3 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last3Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last3DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last3Days</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users">(@UserStats.ReturningLast3Days)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last7Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last7DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last7Days</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users">(@UserStats.ReturningLast7Days)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last14Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last14DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last30Days</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users (activity 24h after registration)">(@UserStats.ReturningLast30Days)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -81,7 +44,6 @@
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private UserStatistics UserStats { get; set; } = new();
|
||||
private bool ShowUserNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
@@ -95,50 +57,56 @@
|
||||
var last24Hours = now.AddHours(-24);
|
||||
var last3Days = now.AddDays(-3);
|
||||
var last7Days = now.AddDays(-7);
|
||||
var last14Days = now.AddDays(-14);
|
||||
var last30Days = now.AddDays(-30);
|
||||
|
||||
// Get user statistics
|
||||
var (count24h, users24h) = await GetActiveUserCount(last24Hours);
|
||||
var (count3d, users3d) = await GetActiveUserCount(last3Days);
|
||||
var (count7d, users7d) = await GetActiveUserCount(last7Days);
|
||||
var (count14d, users14d) = await GetActiveUserCount(last14Days);
|
||||
var (count24h, returning24h) = await GetActiveUserCount(last24Hours);
|
||||
var (count3d, returning3d) = await GetActiveUserCount(last3Days);
|
||||
var (count7d, returning7d) = await GetActiveUserCount(last7Days);
|
||||
var (count30d, returning30d) = await GetActiveUserCount(last30Days);
|
||||
|
||||
UserStats = new UserStatistics
|
||||
{
|
||||
Last24Hours = count24h,
|
||||
Last3Days = count3d,
|
||||
Last7Days = count7d,
|
||||
Last14Days = count14d,
|
||||
Last24HourUsers = users24h,
|
||||
Last3DayUsers = users3d,
|
||||
Last7DayUsers = users7d,
|
||||
Last14DayUsers = users14d
|
||||
Last30Days = count30d,
|
||||
ReturningLast24Hours = returning24h,
|
||||
ReturningLast3Days = returning3d,
|
||||
ReturningLast7Days = returning7d,
|
||||
ReturningLast30Days = returning30d,
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<(int count, List<string> users)> GetActiveUserCount(DateTime since)
|
||||
private async Task<(int totalCount, int returningCount)> GetActiveUserCount(DateTime since)
|
||||
{
|
||||
// Get unique users who:
|
||||
// 1. Have successful auth logs
|
||||
// 2. Are not the admin user
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Get all active users for the period
|
||||
var activeUsers = await dbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= since && l.IsSuccess && l.Username != "admin")
|
||||
.Select(l => l.Username)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
return (activeUsers.Count, activeUsers);
|
||||
}
|
||||
// Get returning users (those who have activity at least 24h after registration
|
||||
var returningUsers = await dbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= since && l.IsSuccess && l.Username != "admin")
|
||||
.Join(
|
||||
dbContext.AliasVaultUsers,
|
||||
log => log.Username,
|
||||
user => user.UserName,
|
||||
(log, user) => new { log, user }
|
||||
)
|
||||
.Where(x => x.log.Timestamp >= x.user.CreatedAt.AddHours(24))
|
||||
.Select(x => x.log.Username)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
private void ToggleUserNames()
|
||||
{
|
||||
ShowUserNames = !ShowUserNames;
|
||||
StateHasChanged();
|
||||
return (activeUsers.Count, returningUsers.Count);
|
||||
}
|
||||
|
||||
private sealed class UserStatistics
|
||||
@@ -146,10 +114,10 @@
|
||||
public int Last24Hours { get; set; }
|
||||
public int Last3Days { get; set; }
|
||||
public int Last7Days { get; set; }
|
||||
public int Last14Days { get; set; }
|
||||
public List<string> Last24HourUsers { get; set; } = new();
|
||||
public List<string> Last3DayUsers { get; set; } = new();
|
||||
public List<string> Last7DayUsers { get; set; } = new();
|
||||
public List<string> Last14DayUsers { get; set; } = new();
|
||||
public int Last30Days { get; set; }
|
||||
public int ReturningLast24Hours { get; set; }
|
||||
public int ReturningLast3Days { get; set; }
|
||||
public int ReturningLast7Days { get; set; }
|
||||
public int ReturningLast30Days { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days7.ToString("N0")</h4>
|
||||
</div>
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days14.ToString("N0")</h4>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days30.ToString("N0")</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -95,7 +95,7 @@
|
||||
var hours24 = now.AddHours(-24);
|
||||
var days3 = now.AddDays(-3);
|
||||
var days7 = now.AddDays(-7);
|
||||
var days14 = now.AddDays(-14);
|
||||
var days30 = now.AddDays(-30);
|
||||
|
||||
// Get email claims statistics
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
@@ -105,7 +105,7 @@
|
||||
Hours24 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= hours24),
|
||||
Days3 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days3),
|
||||
Days7 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days7),
|
||||
Days14 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days14)
|
||||
Days30 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days30)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
claimCount => claimCount.Date,
|
||||
(date, claimCounts) => claimCounts.FirstOrDefault() ?? new DailyEmailClaimCount { Date = date, Count = 0 }
|
||||
)
|
||||
.OrderByDescending(e => e.Date)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,7 @@
|
||||
public int Hours24 { get; set; }
|
||||
public int Days3 { get; set; }
|
||||
public int Days7 { get; set; }
|
||||
public int Days14 { get; set; }
|
||||
public int Days30 { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DailyEmailClaimCount
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days7.ToString("N0")</h4>
|
||||
</div>
|
||||
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days14.ToString("N0")</h4>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days30.ToString("N0")</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -95,7 +95,7 @@
|
||||
var hours24 = now.AddHours(-24);
|
||||
var days3 = now.AddDays(-3);
|
||||
var days7 = now.AddDays(-7);
|
||||
var days14 = now.AddDays(-14);
|
||||
var days30 = now.AddDays(-30);
|
||||
|
||||
// Get email statistics
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
@@ -105,7 +105,7 @@
|
||||
Hours24 = await emailQuery.CountAsync(e => e.DateSystem >= hours24),
|
||||
Days3 = await emailQuery.CountAsync(e => e.DateSystem >= days3),
|
||||
Days7 = await emailQuery.CountAsync(e => e.DateSystem >= days7),
|
||||
Days14 = await emailQuery.CountAsync(e => e.DateSystem >= days14)
|
||||
Days30 = await emailQuery.CountAsync(e => e.DateSystem >= days30)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
emailCount => emailCount.Date,
|
||||
(date, emailCounts) => emailCounts.FirstOrDefault() ?? new DailyEmailCount { Date = date, Count = 0 }
|
||||
)
|
||||
.OrderByDescending(e => e.Date)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,7 @@
|
||||
public int Hours24 { get; set; }
|
||||
public int Days3 { get; set; }
|
||||
public int Days7 { get; set; }
|
||||
public int Days14 { get; set; }
|
||||
public int Days30 { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DailyEmailCount
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days7.ToString("N0")</h4>
|
||||
</div>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days14.ToString("N0")</h4>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days30.ToString("N0")</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
var hours24 = now.AddHours(-24);
|
||||
var days3 = now.AddDays(-3);
|
||||
var days7 = now.AddDays(-7);
|
||||
var days14 = now.AddDays(-14);
|
||||
var days30 = now.AddDays(-30);
|
||||
|
||||
// Get registration statistics
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
@@ -55,7 +55,7 @@
|
||||
Hours24 = await registrationQuery.CountAsync(u => u.CreatedAt >= hours24),
|
||||
Days3 = await registrationQuery.CountAsync(u => u.CreatedAt >= days3),
|
||||
Days7 = await registrationQuery.CountAsync(u => u.CreatedAt >= days7),
|
||||
Days14 = await registrationQuery.CountAsync(u => u.CreatedAt >= days14)
|
||||
Days30 = await registrationQuery.CountAsync(u => u.CreatedAt >= days30)
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
@@ -67,6 +67,6 @@
|
||||
public int Hours24 { get; set; }
|
||||
public int Days3 { get; set; }
|
||||
public int Days7 { get; set; }
|
||||
public int Days14 { get; set; }
|
||||
public int Days30 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,10 +723,6 @@ video {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.ms-2 {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
@@ -980,6 +976,10 @@ video {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -996,6 +996,10 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1004,10 +1008,6 @@ video {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
||||
|
||||
Reference in New Issue
Block a user