feat: Claude subscription usage widget with real OAuth data
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m11s
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m11s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user