Skip to content

Commit 2092b27

Browse files
committed
feat: Track authenticated user queries and display detailed logged-in user activity in the admin dashboard.
1 parent 3027273 commit 2092b27

5 files changed

Lines changed: 282 additions & 38 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "User" ADD COLUMN "lastQueryAt" TIMESTAMP(3),
3+
ADD COLUMN "queryCount" INTEGER NOT NULL DEFAULT 0;

prisma/schema.prisma

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ model User {
1515
emailVerified DateTime?
1616
image String?
1717
githubLogin String? @unique
18+
queryCount Int @default(0)
19+
lastQueryAt DateTime?
1820
createdAt DateTime @default(now())
1921
updatedAt DateTime @updatedAt
2022
accounts Account[]

src/app/actions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from "@/lib/github";
2727
import {
2828
trackEvent,
29+
trackAuthenticatedQueryEvent,
2930
getPublicStats,
3031
trackReportConversionEvent,
3132
type ReportConversionEvent,
@@ -79,6 +80,11 @@ function getErrorMessage(error: unknown): string {
7980
* Reads headers at this function's top level (required by Next.js 15).
8081
*/
8182
async function trackQueryEvent(visitorId: string | undefined): Promise<void> {
83+
const session = await auth();
84+
if (session?.user?.id) {
85+
await trackAuthenticatedQueryEvent(session.user.id);
86+
}
87+
8288
if (process.env.NODE_ENV === "development" && process.env.TRACK_ANALYTICS_IN_DEV !== "true") {
8389
console.log("[Analytics] Skipped (dev). Set TRACK_ANALYTICS_IN_DEV=true to enable.");
8490
return;

src/app/admin/stats/StatsDashboardClient.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect, useState, useMemo } from "react";
44
import {
55
Users, Activity, Smartphone, Monitor, Globe,
66
RefreshCw, ArrowUpDown, ChevronUp, ChevronDown,
7-
UserCheck, TrendingUp, Database, Zap
7+
UserCheck, TrendingUp, Database, Zap, Mail, Search, MessageSquare
88
} from "lucide-react";
99
import { useRouter } from "next/navigation";
1010
import { motion, AnimatePresence } from "framer-motion";
@@ -26,6 +26,7 @@ type SortConfig = {
2626

2727
export default function StatsDashboardClient({ data, userAgent, country, isMobile }: StatsDashboardClientProps) {
2828
const router = useRouter();
29+
const loggedInUsers = data.loggedInUsers ?? [];
2930
const [isRefreshing, setIsRefreshing] = useState(false);
3031
const [selectedRange, setSelectedRange] = useState<HistoryRange>("24h");
3132
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'lastSeen', direction: 'desc' });
@@ -104,6 +105,9 @@ export default function StatsDashboardClient({ data, userAgent, country, isMobil
104105
const displayedVisitors = useMemo(() => {
105106
return sortedVisitors.slice(0, visibleCount);
106107
}, [sortedVisitors, visibleCount]);
108+
const displayedLoggedInUsers = useMemo(() => {
109+
return loggedInUsers.slice(0, 100);
110+
}, [loggedInUsers]);
107111

108112
const requestSort = (key: keyof VisitorData | 'id') => {
109113
let direction: 'asc' | 'desc' = 'asc';
@@ -187,7 +191,7 @@ export default function StatsDashboardClient({ data, userAgent, country, isMobil
187191
</motion.div>
188192

189193
{/* Main KPIs */}
190-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
194+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
191195
<StatsCard
192196
title="Total Visitors"
193197
value={data.totalVisitors}
@@ -214,6 +218,12 @@ export default function StatsDashboardClient({ data, userAgent, country, isMobil
214218
subValue="Last 5 minutes"
215219
icon={<div className="relative"><Globe className="w-5 h-5 text-green-400" />{activeNow > 0 && <div className="absolute -top-1 -right-1 w-2 h-2 bg-green-500 rounded-full animate-ping" />}</div>}
216220
/>
221+
<StatsCard
222+
title="Logged Accounts"
223+
value={data.totalLoggedInUsers ?? loggedInUsers.length}
224+
subValue={`${loggedInUsers.length} shown`}
225+
icon={<UserCheck className="w-5 h-5 text-cyan-400" />}
226+
/>
217227
<StatsCard
218228
title="KV Storage"
219229
value={formatSize(data.kvStats?.currentSize || 0)}
@@ -513,6 +523,87 @@ export default function StatsDashboardClient({ data, userAgent, country, isMobil
513523
</div>
514524
</div>
515525

526+
{/* Logged-In Accounts Table */}
527+
<div className="bg-zinc-900/50 border border-white/10 rounded-2xl overflow-hidden">
528+
<div className="px-6 py-5 border-b border-white/10 flex items-center justify-between">
529+
<h2 className="text-xl font-semibold">Logged-In Accounts (Postgres)</h2>
530+
<span className="text-xs text-zinc-500 font-mono">
531+
Showing {displayedLoggedInUsers.length} of {loggedInUsers.length}
532+
</span>
533+
</div>
534+
<div className="overflow-x-auto">
535+
<table className="w-full text-left text-sm whitespace-nowrap">
536+
<thead className="bg-zinc-900/80 text-zinc-400 font-medium">
537+
<tr>
538+
<th className="px-6 py-4">Account</th>
539+
<th className="px-6 py-4">Email</th>
540+
<th className="px-6 py-4 text-right">Queries</th>
541+
<th className="px-6 py-4 text-right">Scans</th>
542+
<th className="px-6 py-4 text-right">Searches</th>
543+
<th className="px-6 py-4 text-right">Chats</th>
544+
<th className="px-6 py-4">Last Activity (IST)</th>
545+
</tr>
546+
</thead>
547+
<tbody className="divide-y divide-white/5">
548+
{displayedLoggedInUsers.map((user) => (
549+
<tr key={user.id} className="hover:bg-white/[0.02] transition-colors">
550+
<td className="px-6 py-4">
551+
<div className="flex flex-col">
552+
<span className="text-zinc-200 font-medium">
553+
{user.githubLogin ? `@${user.githubLogin}` : user.id.slice(0, 10)}
554+
</span>
555+
<span className="text-[10px] text-zinc-500 font-mono uppercase">
556+
Joined {formatIST(user.createdAt)}
557+
</span>
558+
</div>
559+
</td>
560+
<td className="px-6 py-4 text-zinc-300">
561+
<span className="inline-flex items-center gap-2">
562+
<Mail className="w-3 h-3 text-zinc-500" />
563+
{user.email || "N/A"}
564+
</span>
565+
</td>
566+
<td className="px-6 py-4 text-right font-mono text-zinc-200">{user.queryCount}</td>
567+
<td className="px-6 py-4 text-right font-mono text-zinc-200">{user.scanCount}</td>
568+
<td className="px-6 py-4 text-right font-mono text-zinc-200">
569+
<span className="inline-flex items-center gap-1">
570+
<Search className="w-3 h-3 text-zinc-500" />
571+
{user.searchCount}
572+
</span>
573+
</td>
574+
<td className="px-6 py-4 text-right font-mono text-zinc-200">
575+
<span className="inline-flex items-center gap-1">
576+
<MessageSquare className="w-3 h-3 text-zinc-500" />
577+
{user.chatCount}
578+
</span>
579+
</td>
580+
<td className="px-6 py-4">
581+
{user.lastActivityAt ? (
582+
<div className="flex flex-col">
583+
<span className="text-zinc-300">{formatIST(user.lastActivityAt)}</span>
584+
<span className="text-[10px] text-zinc-500 font-mono uppercase">{getRelativeTime(user.lastActivityAt)}</span>
585+
</div>
586+
) : (
587+
<span className="text-zinc-500">No activity</span>
588+
)}
589+
</td>
590+
</tr>
591+
))}
592+
{loggedInUsers.length === 0 && (
593+
<tr>
594+
<td colSpan={7} className="px-6 py-12 text-center text-zinc-500">
595+
<div className="flex flex-col items-center gap-2">
596+
<UserCheck className="w-8 h-8 opacity-20" />
597+
<p>No logged-in account activity yet.</p>
598+
</div>
599+
</td>
600+
</tr>
601+
)}
602+
</tbody>
603+
</table>
604+
</div>
605+
</div>
606+
516607
{/* Visitors Table */}
517608
<div className="bg-zinc-900/50 border border-white/10 rounded-2xl overflow-hidden">
518609
<div className="px-6 py-5 border-b border-white/10 flex items-center justify-between">

0 commit comments

Comments
 (0)