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:
2026-04-02 12:47:50 +08:00
commit b719b358f8
41 changed files with 3430 additions and 0 deletions

View 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);
}

View 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
View 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
View 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
View 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
View 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);
}