feat: API analytics dashboard with i18n and theme support
Next.js full-stack analytics dashboard for new-api. - Direct PostgreSQL readonly queries on logs table - 5 pages: Dashboard, Rankings, Aggregation, Logs, Detail - Dark/Light/System theme with CSS variables - Chinese/English i18n (default Chinese) - Recharts with dual Y-axis for input/output tokens - Lucide icons + Motion animations - Docker + docker-compose with external sinobridge network, port 8019
This commit is contained in:
12
app/api/aggregation/route.ts
Normal file
12
app/api/aggregation/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getUserRanking } from "@/lib/queries";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const startTs = sp.get("start") ? Number(sp.get("start")) : undefined;
|
||||
const endTs = sp.get("end") ? Number(sp.get("end")) : undefined;
|
||||
|
||||
// Get ALL users (no limit) for aggregation view
|
||||
const data = await getUserRanking(startTs, endTs, 500);
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
29
app/api/detail/[type]/[id]/route.ts
Normal file
29
app/api/detail/[type]/[id]/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getUserDetail, getModelDetail, getChannelDetail } from "@/lib/queries";
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ type: string; id: string }> }
|
||||
) {
|
||||
const { type, id } = await params;
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const startTs = sp.get("start") ? Number(sp.get("start")) : undefined;
|
||||
const endTs = sp.get("end") ? Number(sp.get("end")) : undefined;
|
||||
|
||||
let data;
|
||||
switch (type) {
|
||||
case "user":
|
||||
data = await getUserDetail(decodeURIComponent(id), startTs, endTs);
|
||||
break;
|
||||
case "model":
|
||||
data = await getModelDetail(decodeURIComponent(id), startTs, endTs);
|
||||
break;
|
||||
case "channel":
|
||||
data = await getChannelDetail(Number(id), startTs, endTs);
|
||||
break;
|
||||
default:
|
||||
return NextResponse.json({ error: "Invalid type" }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
19
app/api/logs/route.ts
Normal file
19
app/api/logs/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getLogs } from "@/lib/queries";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
|
||||
const data = await getLogs({
|
||||
page: sp.get("page") ? Number(sp.get("page")) : 1,
|
||||
pageSize: sp.get("page_size") ? Number(sp.get("page_size")) : 100,
|
||||
startTs: sp.get("start") ? Number(sp.get("start")) : undefined,
|
||||
endTs: sp.get("end") ? Number(sp.get("end")) : undefined,
|
||||
username: sp.get("username") || undefined,
|
||||
model: sp.get("model") || undefined,
|
||||
channelId: sp.get("channel_id") ? Number(sp.get("channel_id")) : undefined,
|
||||
tokenName: sp.get("token_name") || undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
11
app/api/overview/route.ts
Normal file
11
app/api/overview/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getOverview } from "@/lib/queries";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const startTs = sp.get("start") ? Number(sp.get("start")) : undefined;
|
||||
const endTs = sp.get("end") ? Number(sp.get("end")) : undefined;
|
||||
|
||||
const data = await getOverview(startTs, endTs);
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
24
app/api/rankings/route.ts
Normal file
24
app/api/rankings/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getUserRanking, getModelRanking, getChannelRanking } from "@/lib/queries";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const type = sp.get("type") || "user";
|
||||
const startTs = sp.get("start") ? Number(sp.get("start")) : undefined;
|
||||
const endTs = sp.get("end") ? Number(sp.get("end")) : undefined;
|
||||
const limit = sp.get("limit") ? Number(sp.get("limit")) : 50;
|
||||
|
||||
let data;
|
||||
switch (type) {
|
||||
case "model":
|
||||
data = await getModelRanking(startTs, endTs, limit);
|
||||
break;
|
||||
case "channel":
|
||||
data = await getChannelRanking(startTs, endTs, limit);
|
||||
break;
|
||||
default:
|
||||
data = await getUserRanking(startTs, endTs, limit);
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
12
app/api/trends/route.ts
Normal file
12
app/api/trends/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getTrends } from "@/lib/queries";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const granularity = (sp.get("granularity") || "day") as "day" | "week" | "month";
|
||||
const startTs = sp.get("start") ? Number(sp.get("start")) : undefined;
|
||||
const endTs = sp.get("end") ? Number(sp.get("end")) : undefined;
|
||||
|
||||
const data = await getTrends(granularity, startTs, endTs);
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
Reference in New Issue
Block a user