Skip to content

Commit 04ff0ef

Browse files
committed
release: v2.7.245 billing fallback history triage filters
1 parent bc544a5 commit 04ff0ef

File tree

4 files changed

+110
-3
lines changed

4 files changed

+110
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
All notable changes to this project will be documented in this file.
66

7+
## [2.7.245] — 2026-03-16
8+
9+
- changed(web/billing): `apps/web/src/app/dashboard/billing/page.tsx` adds fallback-history triage controls for **cause** and **task** filtering, including dynamic facet counts and scoped-result rendering.
10+
- changed(web/billing): **Recent Fallback Decisions** now shows a `showing X of Y` summary plus a no-results empty state for active filters, improving operator incident narrowing.
11+
- test(validation): revalidated strict TypeScript gates for core and web after billing triage filter additions (`CORE_TSC_OK`, `WEB_TSC_OK`).
12+
713
## [2.7.244] — 2026-03-16
814

915
- feat(core/billing): `packages/core/src/routers/billingRouter.ts` now exposes `billing.clearFallbackHistory` to reset the in-memory provider fallback decision ring buffer.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.7.244
1+
2.7.245

VERSION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# Borg Project Version: 2.7.244
1+
# Borg Project Version: 2.7.245

apps/web/src/app/dashboard/billing/page.tsx

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const FALLBACK_TASK_OPTIONS: BillingTaskRoutingRuleSummary['taskType'][] = ['gen
3535
export default function ProviderAuthBillingMatrix() {
3636
const [historyDays, setHistoryDays] = useState(30);
3737
const [fallbackTaskType, setFallbackTaskType] = useState<BillingTaskRoutingRuleSummary['taskType']>('general');
38+
const [fallbackHistoryCauseFilter, setFallbackHistoryCauseFilter] = useState<'all' | 'fallback_provider' | 'budget_forced_local' | 'emergency_fallback'>('all');
39+
const [fallbackHistoryTaskFilter, setFallbackHistoryTaskFilter] = useState<'all' | BillingTaskRoutingRuleSummary['taskType']>('all');
3840

3941
// Key update dialog state
4042
const [activePortalId, setActivePortalId] = useState<string | null>(null);
@@ -125,6 +127,33 @@ export default function ProviderAuthBillingMatrix() {
125127
const activeRoutingMutationTask = setTaskRoutingRuleMutation.variables && 'taskType' in setTaskRoutingRuleMutation.variables
126128
? setTaskRoutingRuleMutation.variables.taskType
127129
: undefined;
130+
const fallbackHistoryRows = (fallbackHistory ?? []) as Array<{
131+
id: number;
132+
timestamp: number;
133+
requestedProvider?: string;
134+
selectedProvider: string;
135+
selectedModelId: string;
136+
taskType: BillingTaskRoutingRuleSummary['taskType'];
137+
strategy: string;
138+
reason: string;
139+
causeCode: 'fallback_provider' | 'budget_forced_local' | 'emergency_fallback' | 'preference_honored';
140+
}>;
141+
const fallbackHistoryTaskOptions = Array.from(new Set(fallbackHistoryRows.map((event) => event.taskType))).sort();
142+
const fallbackHistoryCauseCounts = fallbackHistoryRows.reduce((accumulator, event) => {
143+
accumulator[event.causeCode] = (accumulator[event.causeCode] ?? 0) + 1;
144+
return accumulator;
145+
}, {} as Record<string, number>);
146+
const filteredFallbackHistoryRows = fallbackHistoryRows.filter((event) => {
147+
if (fallbackHistoryCauseFilter !== 'all' && event.causeCode !== fallbackHistoryCauseFilter) {
148+
return false;
149+
}
150+
151+
if (fallbackHistoryTaskFilter !== 'all' && event.taskType !== fallbackHistoryTaskFilter) {
152+
return false;
153+
}
154+
155+
return true;
156+
});
128157

129158
const handleDefaultStrategyChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
130159
setRoutingStrategyMutation.mutate({ strategy: event.target.value as BillingRoutingStrategy });
@@ -293,7 +322,74 @@ export default function ProviderAuthBillingMatrix() {
293322
</Button>
294323
</CardHeader>
295324
<CardContent className="pt-2 space-y-1.5 max-h-64 overflow-y-auto">
296-
{fallbackHistory.map((event) => {
325+
<div className="mb-2 space-y-2 rounded-lg border border-zinc-800/70 bg-black/30 p-2 text-[10px]">
326+
<div className="flex flex-wrap items-center gap-2">
327+
<span className="text-zinc-500 uppercase tracking-wider">Cause</span>
328+
{([
329+
{ value: 'all', label: 'All' },
330+
{ value: 'fallback_provider', label: 'Fallback' },
331+
{ value: 'budget_forced_local', label: 'Budget' },
332+
{ value: 'emergency_fallback', label: 'Emergency' },
333+
] as const).map((option) => {
334+
const active = fallbackHistoryCauseFilter === option.value;
335+
const count = option.value === 'all'
336+
? fallbackHistoryRows.length
337+
: (fallbackHistoryCauseCounts[option.value] ?? 0);
338+
return (
339+
<button
340+
key={`fallback-cause-filter-${option.value}`}
341+
type="button"
342+
onClick={() => setFallbackHistoryCauseFilter(option.value)}
343+
className={`rounded border px-2 py-1 transition-colors ${active
344+
? 'border-amber-500/50 bg-amber-500/15 text-amber-200'
345+
: 'border-zinc-700 bg-zinc-950/70 text-zinc-300 hover:bg-zinc-800'
346+
}`}
347+
title={`Filter fallback history by ${option.label.toLowerCase()} causes`}
348+
aria-label={`Filter fallback history by ${option.label} causes`}
349+
>
350+
{option.label} ({count})
351+
</button>
352+
);
353+
})}
354+
</div>
355+
356+
<div className="flex flex-wrap items-center gap-2">
357+
<span className="text-zinc-500 uppercase tracking-wider">Task</span>
358+
<button
359+
type="button"
360+
onClick={() => setFallbackHistoryTaskFilter('all')}
361+
className={`rounded border px-2 py-1 transition-colors ${fallbackHistoryTaskFilter === 'all'
362+
? 'border-cyan-500/50 bg-cyan-500/15 text-cyan-200'
363+
: 'border-zinc-700 bg-zinc-950/70 text-zinc-300 hover:bg-zinc-800'
364+
}`}
365+
title="Show all task types"
366+
aria-label="Show all task types in fallback history"
367+
>
368+
All ({fallbackHistoryRows.length})
369+
</button>
370+
{fallbackHistoryTaskOptions.map((taskTypeOption) => (
371+
<button
372+
key={`fallback-task-filter-${taskTypeOption}`}
373+
type="button"
374+
onClick={() => setFallbackHistoryTaskFilter(taskTypeOption)}
375+
className={`rounded border px-2 py-1 capitalize transition-colors ${fallbackHistoryTaskFilter === taskTypeOption
376+
? 'border-cyan-500/50 bg-cyan-500/15 text-cyan-200'
377+
: 'border-zinc-700 bg-zinc-950/70 text-zinc-300 hover:bg-zinc-800'
378+
}`}
379+
title={`Filter fallback history to ${formatTaskRoutingLabel(taskTypeOption).toLowerCase()} decisions`}
380+
aria-label={`Filter fallback history to ${formatTaskRoutingLabel(taskTypeOption)} decisions`}
381+
>
382+
{formatTaskRoutingLabel(taskTypeOption)}
383+
</button>
384+
))}
385+
</div>
386+
387+
<div className="text-zinc-500">
388+
showing {filteredFallbackHistoryRows.length} of {fallbackHistoryRows.length} decisions
389+
</div>
390+
</div>
391+
392+
{filteredFallbackHistoryRows.map((event) => {
297393
const causeColor =
298394
event.causeCode === 'emergency_fallback' ? 'text-red-400 border-red-800' :
299395
event.causeCode === 'budget_forced_local' ? 'text-orange-400 border-orange-800' :
@@ -327,6 +423,11 @@ export default function ProviderAuthBillingMatrix() {
327423
</div>
328424
);
329425
})}
426+
{filteredFallbackHistoryRows.length === 0 ? (
427+
<div className="rounded-lg border border-dashed border-zinc-800 p-3 text-xs text-zinc-500 text-center">
428+
No fallback decisions match the selected filters.
429+
</div>
430+
) : null}
330431
</CardContent>
331432
</Card>
332433
)}

0 commit comments

Comments
 (0)