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

View File

@@ -1,61 +1,34 @@
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
// Bridge HTTP API (cosmo-studio bridge в той же сети coolify) const PROXY_URL = "http://192.168.31.103:3301/claude-oauth-usage";
const BRIDGE_URL = "http://172.18.0.5:3402/api/usage";
export async function GET() { export async function GET() {
try { try {
const res = await fetch(BRIDGE_URL, { const res = await fetch(PROXY_URL, {
signal: AbortSignal.timeout(5000), signal: AbortSignal.timeout(8000),
cache: "no-store", cache: "no-store",
}); });
if (!res.ok) throw new Error(`Proxy HTTP ${res.status}`);
if (!res.ok) throw new Error(`Bridge HTTP ${res.status}`);
const data = await res.json(); const data = await res.json();
const usage = data.usage; const toWindow = (obj: any) => obj ? {
if (!usage) { label: obj.label || "",
return NextResponse.json({ ok: false, error: "No usage data yet" }); usedPercent: obj.utilization ?? 0,
} resetAt: obj.resets_at ? new Date(obj.resets_at).getTime() : undefined,
} : null;
// Найти данные 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;
});
return NextResponse.json({ return NextResponse.json({
ok: true, ok: true,
anthropic: { anthropic: {
window5h: window5h || null, window5h: toWindow(data.five_hour),
windowWeek: windowWeek || null, windowWeek: toWindow(data.seven_day),
windowSonnet: windowSonnet || null, windowSonnet: toWindow(data.seven_day_sonnet),
error: anthropicProvider?.error || null, error: null,
plan: anthropicProvider?.plan || 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, cost: null,
}); });
} catch (e) {
return NextResponse.json({ ok: false, error: String(e), anthropic: null, cost: null });
} }
} }