diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index aeb9266..4bece27 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -36,6 +36,20 @@ export async function adminRoutes(app: FastifyInstance) { return { total: pending.length, indexed: done, no_text_found: failed }; }); + /** + * GET /api/admin/stats + * Aggregate share and view counts across all memes. + */ + app.get('/api/admin/stats', { preHandler: requireAuth }, async () => { + const row = db + .prepare('SELECT SUM(share_count) as total_shares, SUM(view_count) as total_views FROM memes') + .get() as { total_shares: number | null; total_views: number | null }; + return { + total_shares: row.total_shares ?? 0, + total_views: row.total_views ?? 0, + }; + }); + /** * GET /api/admin/reindex/status * Returns how many memes still need OCR indexing. diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 126185f..6d7bb42 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -155,6 +155,10 @@ export const api = { reindex(): Promise<{ total: number; indexed: number; no_text_found: number }> { return apiFetch('/api/admin/reindex', { method: 'POST' }); }, + + stats(): Promise<{ total_shares: number; total_views: number }> { + return apiFetch('/api/admin/stats'); + }, }, imageUrl(filePath: string): string { diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 4df23a8..adca9c5 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1,5 +1,6 @@ -import { X, ScanText, Database, RefreshCw, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; -import { useReindexStatus, useReindex, useCollections, useTags, useMemes } from '../hooks/useMemes'; +import React from 'react'; +import { X, ScanText, Database, RefreshCw, CheckCircle2, AlertCircle, Loader2, Share2, MousePointerClick } from 'lucide-react'; +import { useReindexStatus, useReindex, useCollections, useTags, useMemes, useAdminStats } from '../hooks/useMemes'; interface Props { onClose: () => void; @@ -11,6 +12,7 @@ export function SettingsModal({ onClose }: Props) { const { data: collections } = useCollections(); const { data: tags } = useTags(); const { data: allMemes } = useMemes({ parent_only: false, limit: 1 }); + const { data: adminStats } = useAdminStats(); async function handleReindex() { await reindex.mutateAsync(); @@ -45,6 +47,18 @@ export function SettingsModal({ onClose }: Props) { +
+ } + /> + } + /> +
@@ -130,9 +144,10 @@ export function SettingsModal({ onClose }: Props) { ); } -function StatCard({ label, value }: { label: string; value: number | string }) { +function StatCard({ label, value, icon }: { label: string; value: number | string; icon?: React.ReactNode }) { return (
+ {icon &&
{icon}
}
{value}
{label}
diff --git a/frontend/src/hooks/useMemes.ts b/frontend/src/hooks/useMemes.ts index cb69e78..153c36b 100644 --- a/frontend/src/hooks/useMemes.ts +++ b/frontend/src/hooks/useMemes.ts @@ -106,6 +106,14 @@ export function useDeleteMeme() { }); } +export function useAdminStats() { + return useQuery({ + queryKey: ['admin', 'stats'], + queryFn: () => api.admin.stats(), + staleTime: 30_000, + }); +} + export function useReindexStatus() { return useQuery({ queryKey: ['admin', 'reindex-status'],