address greptile review: per-provider deficit notch, startedAt filter, weekRange refresh, deduplicate providerDisplayName
This commit is contained in:
@@ -172,8 +172,8 @@ export function costService(db: Db) {
|
|||||||
.orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`));
|
.orderBy(desc(sql`coalesce(sum(${costEvents.costCents}), 0)::int`));
|
||||||
|
|
||||||
const runConditions: ReturnType<typeof eq>[] = [eq(heartbeatRuns.companyId, companyId)];
|
const runConditions: ReturnType<typeof eq>[] = [eq(heartbeatRuns.companyId, companyId)];
|
||||||
if (range?.from) runConditions.push(gte(heartbeatRuns.finishedAt, range.from));
|
if (range?.from) runConditions.push(gte(heartbeatRuns.startedAt, range.from));
|
||||||
if (range?.to) runConditions.push(lte(heartbeatRuns.finishedAt, range.to));
|
if (range?.to) runConditions.push(lte(heartbeatRuns.startedAt, range.to));
|
||||||
|
|
||||||
const runRows = await db
|
const runRows = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { CostByProviderModel, CostWindowSpendRow } from "@paperclipai/shared";
|
import type { CostByProviderModel, CostWindowSpendRow } from "@paperclipai/shared";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { QuotaBar } from "./QuotaBar";
|
import { QuotaBar } from "./QuotaBar";
|
||||||
import { formatCents, formatTokens } from "@/lib/utils";
|
import { formatCents, formatTokens, providerDisplayName } from "@/lib/utils";
|
||||||
|
|
||||||
interface ProviderQuotaCardProps {
|
interface ProviderQuotaCardProps {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -17,17 +17,6 @@ interface ProviderQuotaCardProps {
|
|||||||
showDeficitNotch: boolean;
|
showDeficitNotch: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerLabel(provider: string): string {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
anthropic: "Anthropic",
|
|
||||||
openai: "OpenAI",
|
|
||||||
google: "Google",
|
|
||||||
cursor: "Cursor",
|
|
||||||
jetbrains: "JetBrains AI",
|
|
||||||
};
|
|
||||||
return map[provider.toLowerCase()] ?? provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProviderQuotaCard({
|
export function ProviderQuotaCard({
|
||||||
provider,
|
provider,
|
||||||
rows,
|
rows,
|
||||||
@@ -76,7 +65,7 @@ export function ProviderQuotaCard({
|
|||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<CardTitle className="text-sm font-semibold">
|
<CardTitle className="text-sm font-semibold">
|
||||||
{providerLabel(provider)}
|
{providerDisplayName(provider)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs mt-0.5">
|
<CardDescription className="text-xs mt-0.5">
|
||||||
<span className="font-mono">{formatTokens(totalInputTokens)}</span> in
|
<span className="font-mono">{formatTokens(totalInputTokens)}</span> in
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ export function formatTokens(n: number): string {
|
|||||||
return String(n);
|
return String(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Map a raw provider slug to a display-friendly name. */
|
||||||
|
export function providerDisplayName(provider: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
anthropic: "Anthropic",
|
||||||
|
openai: "OpenAI",
|
||||||
|
google: "Google",
|
||||||
|
cursor: "Cursor",
|
||||||
|
jetbrains: "JetBrains AI",
|
||||||
|
};
|
||||||
|
return map[provider.toLowerCase()] ?? provider;
|
||||||
|
}
|
||||||
|
|
||||||
/** Build an issue URL using the human-readable identifier when available. */
|
/** Build an issue URL using the human-readable identifier when available. */
|
||||||
export function issueUrl(issue: { id: string; identifier?: string | null }): string {
|
export function issueUrl(issue: { id: string; identifier?: string | null }): string {
|
||||||
return `/issues/${issue.identifier ?? issue.id}`;
|
return `/issues/${issue.identifier ?? issue.id}`;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { EmptyState } from "../components/EmptyState";
|
|||||||
import { PageSkeleton } from "../components/PageSkeleton";
|
import { PageSkeleton } from "../components/PageSkeleton";
|
||||||
import { ProviderQuotaCard } from "../components/ProviderQuotaCard";
|
import { ProviderQuotaCard } from "../components/ProviderQuotaCard";
|
||||||
import { PageTabBar } from "../components/PageTabBar";
|
import { PageTabBar } from "../components/PageTabBar";
|
||||||
import { formatCents, formatTokens } from "../lib/utils";
|
import { formatCents, formatTokens, providerDisplayName } from "../lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||||
import { Gauge } from "lucide-react";
|
import { Gauge } from "lucide-react";
|
||||||
@@ -52,17 +52,6 @@ function computeRange(preset: DatePreset): { from: string; to: string } {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerDisplayName(provider: string): string {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
anthropic: "Anthropic",
|
|
||||||
openai: "OpenAI",
|
|
||||||
google: "Google",
|
|
||||||
cursor: "Cursor",
|
|
||||||
jetbrains: "JetBrains AI",
|
|
||||||
};
|
|
||||||
return map[provider.toLowerCase()] ?? provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** current week mon-sun boundaries as iso strings */
|
/** current week mon-sun boundaries as iso strings */
|
||||||
function currentWeekRange(): { from: string; to: string } {
|
function currentWeekRange(): { from: string; to: string } {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -121,7 +110,9 @@ export function Usage() {
|
|||||||
return range;
|
return range;
|
||||||
}, [preset, customFrom, customTo]);
|
}, [preset, customFrom, customTo]);
|
||||||
|
|
||||||
const weekRange = useMemo(() => currentWeekRange(), []);
|
// key to today's date string so the range auto-refreshes after midnight on the next 30s refetch
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
const weekRange = useMemo(() => currentWeekRange(), [today]);
|
||||||
|
|
||||||
// for custom preset, only fetch once both dates are selected
|
// for custom preset, only fetch once both dates are selected
|
||||||
const customReady = preset !== "custom" || (!!customFrom && !!customTo);
|
const customReady = preset !== "custom" || (!!customFrom && !!customTo);
|
||||||
@@ -190,21 +181,23 @@ export function Usage() {
|
|||||||
return map;
|
return map;
|
||||||
}, [windowData]);
|
}, [windowData]);
|
||||||
|
|
||||||
// deficit notch: projected spend exceeds remaining budget — only meaningful for mtd preset
|
// compute deficit notch per provider: only meaningful for mtd — projects spend to month end
|
||||||
// (other presets use a different date range than the monthly budget, so the projection is nonsensical)
|
// and flags when that projection exceeds the provider's pro-rata budget share.
|
||||||
const showDeficitNotch = useMemo(() => {
|
function providerDeficitNotch(providerKey: string): boolean {
|
||||||
if (preset !== "mtd") return false;
|
if (preset !== "mtd") return false;
|
||||||
const budget = summary?.budgetCents ?? 0;
|
const budget = summary?.budgetCents ?? 0;
|
||||||
if (budget <= 0) return false;
|
if (budget <= 0) return false;
|
||||||
const spend = summary?.spendCents ?? 0;
|
const totalSpend = summary?.spendCents ?? 0;
|
||||||
const today = new Date();
|
const providerCostCents = (byProvider.get(providerKey) ?? []).reduce((s, r) => s + r.costCents, 0);
|
||||||
const daysElapsed = today.getDate();
|
const providerShare = totalSpend > 0 ? providerCostCents / totalSpend : 0;
|
||||||
const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
const providerBudget = budget * providerShare;
|
||||||
const daysRemaining = daysInMonth - daysElapsed;
|
if (providerBudget <= 0) return false;
|
||||||
const burnRatePerDay = spend / Math.max(daysElapsed, 1);
|
const now = new Date();
|
||||||
const projected = spend + burnRatePerDay * daysRemaining;
|
const daysElapsed = now.getDate();
|
||||||
return projected > budget;
|
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
||||||
}, [summary, preset]);
|
const burnRate = providerCostCents / Math.max(daysElapsed, 1);
|
||||||
|
return providerCostCents + burnRate * (daysInMonth - daysElapsed) > providerBudget;
|
||||||
|
}
|
||||||
|
|
||||||
const providers = Array.from(byProvider.keys());
|
const providers = Array.from(byProvider.keys());
|
||||||
|
|
||||||
@@ -298,7 +291,7 @@ export function Usage() {
|
|||||||
totalCompanySpendCents={summary?.spendCents ?? 0}
|
totalCompanySpendCents={summary?.spendCents ?? 0}
|
||||||
weekSpendCents={weekSpendByProvider.get(p) ?? 0}
|
weekSpendCents={weekSpendByProvider.get(p) ?? 0}
|
||||||
windowRows={windowSpendByProvider.get(p) ?? []}
|
windowRows={windowSpendByProvider.get(p) ?? []}
|
||||||
showDeficitNotch={showDeficitNotch}
|
showDeficitNotch={providerDeficitNotch(p)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +307,7 @@ export function Usage() {
|
|||||||
totalCompanySpendCents={summary?.spendCents ?? 0}
|
totalCompanySpendCents={summary?.spendCents ?? 0}
|
||||||
weekSpendCents={weekSpendByProvider.get(p) ?? 0}
|
weekSpendCents={weekSpendByProvider.get(p) ?? 0}
|
||||||
windowRows={windowSpendByProvider.get(p) ?? []}
|
windowRows={windowSpendByProvider.get(p) ?? []}
|
||||||
showDeficitNotch={showDeficitNotch}
|
showDeficitNotch={providerDeficitNotch(p)}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user