diff --git a/apps/api/src/lib/persistence.ts b/apps/api/src/lib/persistence.ts index f50a0ca..3e264c0 100644 --- a/apps/api/src/lib/persistence.ts +++ b/apps/api/src/lib/persistence.ts @@ -129,6 +129,66 @@ export async function getAnalyticsSummary( return getStorageRepository().getAnalyticsSummary(options); } +export interface AnalyticsExport { + exportedAt: string; + totalQueries: number; + totalSpendUsd: number; + spendByMode: Record; + spendByProvider: Record; + queryCountByStatus: { + demo: number; + paid: number; + failed: number; + }; + paymentEvidenceCount: number; +} + +export async function getAnalyticsExport(): Promise { + const usage = await getUsageEvents(); + const payments = await getPaymentAttempts(); + + const exportedAt = new Date().toISOString(); + const totalQueries = usage.length; + + const spendByMode: Record = { search: 0, news: 0, scrape: 0 }; + const spendByProvider: Record = {}; + const queryCountByStatus = { demo: 0, paid: 0, failed: 0 }; + + for (const event of usage) { + spendByMode[event.mode] += event.priceUsd; + spendByProvider[event.providerId] = + (spendByProvider[event.providerId] ?? 0) + event.priceUsd; + + if (event.paymentStatus === "demo-paid") { + queryCountByStatus.demo++; + } else if (event.paymentStatus === "failed") { + queryCountByStatus.failed++; + } else { + queryCountByStatus.paid++; + } + } + + const totalSpendUsd = Number( + Object.values(spendByMode) + .reduce((sum, v) => sum + v, 0) + .toFixed(6) + ); + + const paymentEvidenceCount = payments.filter( + (p) => p.transactionHash || p.facilitatorResult || p.evidenceKind + ).length; + + return { + exportedAt, + totalQueries, + totalSpendUsd, + spendByMode, + spendByProvider, + queryCountByStatus, + paymentEvidenceCount + }; +} + export async function persistPaidRequest(input: PersistPaidRequestInput): Promise { const payment = buildPaymentAttempt(input); const usage = buildUsageEvent(input, { diff --git a/apps/api/src/routes/public.ts b/apps/api/src/routes/public.ts index 43e9fb4..a0e287c 100644 --- a/apps/api/src/routes/public.ts +++ b/apps/api/src/routes/public.ts @@ -74,6 +74,15 @@ publicRouter.get("/api/usage", async (req, res, next) => { } }); +publicRouter.get("/api/export", async (_req, res, next) => { + try { + const exportData = await getAnalyticsExport(); + res.json(exportData); + } catch (error) { + next(error); + } +}); + publicRouter.get("/api/analytics", async (req, res, next) => { try { const parsed = analyticsQuerySchema.safeParse(req.query); diff --git a/apps/web/src/pages/ControlDeckPage.tsx b/apps/web/src/pages/ControlDeckPage.tsx index 622c3a2..d16605b 100644 --- a/apps/web/src/pages/ControlDeckPage.tsx +++ b/apps/web/src/pages/ControlDeckPage.tsx @@ -6,6 +6,7 @@ import { CheckCircle2, CircleDollarSign, Clock4, + Download, Gauge, Home, Radar, diff --git a/package-lock.json b/package-lock.json index 23b777d..2894a9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,4 +1,5 @@ { + "name": "query402", "version": "0.1.0", "lockfileVersion": 3,