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:
Till JS 2026-04-09 12:20:18 +02:00
parent 7b25f3abfb
commit fbb71f9366
3 changed files with 109 additions and 36 deletions

View file

@ -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
*/

View file

@ -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>