feat: Claude subscription usage widget with real OAuth data
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m11s

This commit is contained in:
Cosmo
2026-04-16 15:42:24 +00:00
parent 9ec0b3a88f
commit dca85e1137
2 changed files with 17 additions and 43 deletions

View File

@@ -1,10 +1,10 @@
export const dynamic = "force-dynamic";
import { WeatherWidget } from "@/components/widgets/WeatherWidget";
import { CalendarWidget } from "@/components/widgets/CalendarWidget";
import { DashboardHeader } from "@/components/widgets/DashboardHeader";
import { SavingsWidget } from "@/components/widgets/SavingsWidget";
import { GitActivityWidget } from "@/components/widgets/GitActivityWidget";
import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget";
export default function DashboardPage() {
return (
@@ -18,6 +18,7 @@ export default function DashboardPage() {
<div className="space-y-5">
<SavingsWidget />
<GitActivityWidget />
<ClaudeUsageWidget />
</div>
</div>
</div>

View File

@@ -1,61 +1,34 @@
export const dynamic = "force-dynamic";
import { NextResponse } from "next/server";
// Bridge HTTP API (cosmo-studio bridge в той же сети coolify)
const BRIDGE_URL = "http://172.18.0.5:3402/api/usage";
const PROXY_URL = "http://192.168.31.103:3301/claude-oauth-usage";
export async function GET() {
try {
const res = await fetch(BRIDGE_URL, {
signal: AbortSignal.timeout(5000),
const res = await fetch(PROXY_URL, {
signal: AbortSignal.timeout(8000),
cache: "no-store",
});
if (!res.ok) throw new Error(`Bridge HTTP ${res.status}`);
if (!res.ok) throw new Error(`Proxy HTTP ${res.status}`);
const data = await res.json();
const usage = data.usage;
if (!usage) {
return NextResponse.json({ ok: false, error: "No usage data yet" });
}
// Найти данные Anthropic
const anthropicProvider = usage.planLimits?.providers?.find(
(p: any) => p.provider === "anthropic"
);
const windows = anthropicProvider?.windows || [];
const window5h = windows.find((w: any) => w.label?.includes("5") || w.label === "5h");
const windowWeek = windows.find((w: any) => w.label?.toLowerCase().includes("week"));
const windowSonnet = windows.find((w: any) => w.label?.toLowerCase().includes("sonnet"));
// Стоимость сегодня
const todayEntry = usage.cost?.daily?.find((d: any) => {
const today = new Date().toISOString().split("T")[0];
return d.date === today;
});
const toWindow = (obj: any) => obj ? {
label: obj.label || "",
usedPercent: obj.utilization ?? 0,
resetAt: obj.resets_at ? new Date(obj.resets_at).getTime() : undefined,
} : null;
return NextResponse.json({
ok: true,
anthropic: {
window5h: window5h || null,
windowWeek: windowWeek || null,
windowSonnet: windowSonnet || null,
error: anthropicProvider?.error || null,
plan: anthropicProvider?.plan || null,
window5h: toWindow(data.five_hour),
windowWeek: toWindow(data.seven_day),
windowSonnet: toWindow(data.seven_day_sonnet),
error: null,
},
cost: {
today: todayEntry?.totalCost || 0,
todayTokens: todayEntry?.totalTokens || 0,
total: usage.cost?.totals?.totalCost || 0,
},
updatedAt: usage.updatedAt,
});
} catch (e) {
return NextResponse.json({
ok: false,
error: String(e),
anthropic: null,
cost: null,
});
} catch (e) {
return NextResponse.json({ ok: false, error: String(e), anthropic: null, cost: null });
}
}