build view count total
This commit is contained in:
@@ -36,6 +36,20 @@ export async function adminRoutes(app: FastifyInstance) {
|
|||||||
return { total: pending.length, indexed: done, no_text_found: failed };
|
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
|
* GET /api/admin/reindex/status
|
||||||
* Returns how many memes still need OCR indexing.
|
* Returns how many memes still need OCR indexing.
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ export const api = {
|
|||||||
reindex(): Promise<{ total: number; indexed: number; no_text_found: number }> {
|
reindex(): Promise<{ total: number; indexed: number; no_text_found: number }> {
|
||||||
return apiFetch('/api/admin/reindex', { method: 'POST' });
|
return apiFetch('/api/admin/reindex', { method: 'POST' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stats(): Promise<{ total_shares: number; total_views: number }> {
|
||||||
|
return apiFetch('/api/admin/stats');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
imageUrl(filePath: string): string {
|
imageUrl(filePath: string): string {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { X, ScanText, Database, RefreshCw, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
|
import React from 'react';
|
||||||
import { useReindexStatus, useReindex, useCollections, useTags, useMemes } from '../hooks/useMemes';
|
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 {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -11,6 +12,7 @@ export function SettingsModal({ onClose }: Props) {
|
|||||||
const { data: collections } = useCollections();
|
const { data: collections } = useCollections();
|
||||||
const { data: tags } = useTags();
|
const { data: tags } = useTags();
|
||||||
const { data: allMemes } = useMemes({ parent_only: false, limit: 1 });
|
const { data: allMemes } = useMemes({ parent_only: false, limit: 1 });
|
||||||
|
const { data: adminStats } = useAdminStats();
|
||||||
|
|
||||||
async function handleReindex() {
|
async function handleReindex() {
|
||||||
await reindex.mutateAsync();
|
await reindex.mutateAsync();
|
||||||
@@ -45,6 +47,18 @@ export function SettingsModal({ onClose }: Props) {
|
|||||||
<StatCard label="Collections" value={collections?.length ?? '—'} />
|
<StatCard label="Collections" value={collections?.length ?? '—'} />
|
||||||
<StatCard label="Tags" value={tags?.length ?? '—'} />
|
<StatCard label="Tags" value={tags?.length ?? '—'} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||||
|
<StatCard
|
||||||
|
label="Total shares"
|
||||||
|
value={adminStats?.total_shares ?? '—'}
|
||||||
|
icon={<Share2 size={13} className="text-zinc-500" />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Link clicks"
|
||||||
|
value={adminStats?.total_views ?? '—'}
|
||||||
|
icon={<MousePointerClick size={13} className="text-zinc-500" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="border-t border-zinc-800" />
|
<div className="border-t border-zinc-800" />
|
||||||
@@ -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 (
|
return (
|
||||||
<div className="bg-zinc-800/60 rounded-lg px-3 py-3 text-center">
|
<div className="bg-zinc-800/60 rounded-lg px-3 py-3 text-center">
|
||||||
|
{icon && <div className="flex justify-center mb-1">{icon}</div>}
|
||||||
<div className="text-lg font-semibold text-zinc-200">{value}</div>
|
<div className="text-lg font-semibold text-zinc-200">{value}</div>
|
||||||
<div className="text-xs text-zinc-500 mt-0.5">{label}</div>
|
<div className="text-xs text-zinc-500 mt-0.5">{label}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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() {
|
export function useReindexStatus() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['admin', 'reindex-status'],
|
queryKey: ['admin', 'reindex-status'],
|
||||||
|
|||||||
Reference in New Issue
Block a user