mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 20:39:41 +02:00
feat(admin): replace mock dashboard stats with real /admin/stats endpoint
The /admin route in the unified Mana web app was rendering hardcoded
mock data (42 users, 156 successful logins, 3 failed) for every
admin who opened it. The previous code had a TODO comment to wire
up a real endpoint and the backend half had been waiting for the
frontend half ever since the consolidation landed.
Backend (mana-auth):
Add GET /api/v1/admin/stats — admin-only, returns the seven counts
the dashboard needs in a single response. Each count is its own
Drizzle query against auth.users / auth.sessions / auth.login_
attempts; they run in parallel via Promise.all so total latency is
dominated by the round-trip to Postgres, not the per-query work.
Stats:
- totalUsers → users where deleted_at IS NULL
- newUsers7d → users created in the last 7 days
- newUsers30d → users created in the last 30 days
- activeSessions → sessions where expires_at > now() AND not revoked
- uniqueUsers24h → distinct user_id from sessions with last_activity
in the last 24h (and not revoked)
- loginSuccess7d → login_attempts where successful=true, last 7d
- loginFailed7d → login_attempts where successful=false, last 7d
Plus a generatedAt ISO timestamp so the client can show staleness
if it ever caches the response.
Frontend (apps/mana/apps/web):
- Add adminService.getStats() in the existing admin API service
(sits next to getUsers / getUserData / deleteUserData; uses the
same authenticated base-client and ApiResult envelope).
- Replace the onMount mock-data block in admin/+page.svelte with
a single adminService.getStats() call. Drop the local Stats
interface in favor of the AdminStats type exported from the
service.
- Guard the Success Rate calculation against division by zero on
fresh deployments — when there have been no login attempts in
the last 7 days, render '—%' instead of NaN%.
Verification:
- mana-auth type-check unchanged (baseline errors only)
- mana-auth runtime tests still 19/19 passing
- svelte-check on the two changed web files: zero errors
Closes item #12 in docs/REFACTORING_AUDIT_2026_04.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7b25f3abfb
commit
fbb71f9366
3 changed files with 109 additions and 36 deletions
|
|
@ -146,10 +146,34 @@ export interface DeleteUserDataResponse {
|
|||
totalDeleted: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin dashboard stats — aggregate counts from auth.users,
|
||||
* auth.sessions, auth.login_attempts. Backed by GET /admin/stats.
|
||||
*/
|
||||
export interface AdminStats {
|
||||
totalUsers: number;
|
||||
newUsers7d: number;
|
||||
newUsers30d: number;
|
||||
activeSessions: number;
|
||||
uniqueUsers24h: number;
|
||||
loginSuccess7d: number;
|
||||
loginFailed7d: number;
|
||||
/** ISO timestamp of when the snapshot was generated server-side */
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin service for user data management
|
||||
*/
|
||||
export const adminService = {
|
||||
/**
|
||||
* Aggregate dashboard stats. Backed by a single mana-auth endpoint
|
||||
* that runs seven counts against the auth schema in parallel.
|
||||
*/
|
||||
async getStats(): Promise<ApiResult<AdminStats>> {
|
||||
return getClient().get<AdminStats>('/admin/stats');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of users with pagination and search
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,18 +2,9 @@
|
|||
import { onMount } from 'svelte';
|
||||
import StatCard from '$lib/components/admin/StatCard.svelte';
|
||||
import QuickLinks from '$lib/components/admin/QuickLinks.svelte';
|
||||
import { adminService, type AdminStats } from '$lib/api/services/admin';
|
||||
|
||||
interface Stats {
|
||||
totalUsers: number;
|
||||
newUsers7d: number;
|
||||
newUsers30d: number;
|
||||
activeSessions: number;
|
||||
uniqueUsers24h: number;
|
||||
loginSuccess7d: number;
|
||||
loginFailed7d: number;
|
||||
}
|
||||
|
||||
let stats = $state<Stats | null>(null);
|
||||
let stats = $state<AdminStats | null>(null);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
|
|
@ -45,27 +36,13 @@
|
|||
];
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
// TODO: Replace with actual API call to fetch admin stats
|
||||
// const response = await fetch('/api/admin/stats');
|
||||
// stats = await response.json();
|
||||
|
||||
// Mock data for now
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
stats = {
|
||||
totalUsers: 42,
|
||||
newUsers7d: 8,
|
||||
newUsers30d: 23,
|
||||
activeSessions: 15,
|
||||
uniqueUsers24h: 12,
|
||||
loginSuccess7d: 156,
|
||||
loginFailed7d: 3,
|
||||
};
|
||||
} catch (e) {
|
||||
error = 'Failed to load stats';
|
||||
} finally {
|
||||
loading = false;
|
||||
const result = await adminService.getStats();
|
||||
if (result.error) {
|
||||
error = result.error;
|
||||
} else {
|
||||
stats = result.data;
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
|
||||
let userGrowthPercent = $derived(
|
||||
|
|
@ -131,9 +108,11 @@
|
|||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Success Rate</span>
|
||||
<span class="font-medium text-green-600">
|
||||
{Math.round(
|
||||
(stats.loginSuccess7d / (stats.loginSuccess7d + stats.loginFailed7d)) * 100
|
||||
)}%
|
||||
{stats.loginSuccess7d + stats.loginFailed7d > 0
|
||||
? Math.round(
|
||||
(stats.loginSuccess7d / (stats.loginSuccess7d + stats.loginFailed7d)) * 100
|
||||
)
|
||||
: '—'}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue