mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-04-28 06:59:37 +00:00
fix(plan): scope TUI plan row to billing period, use currency-aware formatting
Address review feedback on #74: 1. TUI plan row previously used the active tab's filtered projects as plan spend, so 'Today' showed today's cost as plan spent. Switch renderDashboard and reloadData to getPlanUsageOrNull(), which uses the plan's own billing period regardless of tab. 2. Plan row rendered via a local formatUsd that hardcoded USD. Replace every call with formatCost so 'codeburn currency EUR' flows through. Removes the adjacent '$3,425.52' vs '$32.07' style mismatch. 3. renderPlanBar capped filled width at 100%, so 105% and 1700% looked identical. Past 100%, render a full bar plus chevron tail sized by order of magnitude (log10): 1.05x -> 1 chevron, 17x -> 2, 170x -> 3. 4. 'running on API overage pricing' is wrong for Claude Pro/Max (rate limited, not charged overage). Drop that claim; keep the Nx-over multiplier and match the under/near projection line structure. 5. Spell out 'equiv' as 'API-equivalent' in the plan label. Dead code cleanup: getPlanUsageOrNullForProjects is now unused; remove it. getPlanUsageFromProjects stays (unit tests still use it).
This commit is contained in:
parent
3f7470d29b
commit
cb4c3ee305
2 changed files with 15 additions and 20 deletions
|
|
@ -11,7 +11,7 @@ import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult
|
|||
import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
|
||||
import { dateKey } from './day-aggregator.js'
|
||||
import { CompareView } from './compare.js'
|
||||
import { getPlanUsageOrNullForProjects, type PlanUsage } from './plan-usage.js'
|
||||
import { getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
|
||||
import { planDisplayName } from './plans.js'
|
||||
import { join } from 'path'
|
||||
|
||||
|
|
@ -157,14 +157,15 @@ function fit(s: string, n: number): string {
|
|||
return s.length > n ? s.slice(0, n) : s.padEnd(n)
|
||||
}
|
||||
|
||||
function formatUsd(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }).format(value)
|
||||
}
|
||||
|
||||
function renderPlanBar(percentUsed: number, width: number): string {
|
||||
const capped = Math.max(0, Math.min(100, percentUsed))
|
||||
const filled = Math.round((capped / 100) * width)
|
||||
return `${'▓'.repeat(filled)}${'░'.repeat(Math.max(0, width - filled))}`
|
||||
if (percentUsed <= 100) {
|
||||
const capped = Math.max(0, percentUsed)
|
||||
const filled = Math.round((capped / 100) * width)
|
||||
return `${'▓'.repeat(filled)}${'░'.repeat(Math.max(0, width - filled))}`
|
||||
}
|
||||
const factor = percentUsed / 100
|
||||
const chevrons = Math.min(4, Math.max(1, Math.floor(Math.log10(factor)) + 1))
|
||||
return `${'▓'.repeat(width)}${'▶'.repeat(chevrons)}`
|
||||
}
|
||||
|
||||
function Overview({ projects, label, width, planUsage }: { projects: ProjectSummary[]; label: string; width: number; planUsage?: PlanUsage }) {
|
||||
|
|
@ -179,7 +180,7 @@ function Overview({ projects, label, width, planUsage }: { projects: ProjectSumm
|
|||
const allInputTokens = totalInput + totalCacheRead + totalCacheWrite
|
||||
const cacheHit = allInputTokens > 0
|
||||
? (totalCacheRead / allInputTokens) * 100 : 0
|
||||
const planLabel = planUsage ? `${planDisplayName(planUsage.plan.id)} plan: ${formatUsd(planUsage.spentApiEquivalentUsd)} equiv / ${formatUsd(planUsage.budgetUsd)} included` : ''
|
||||
const planLabel = planUsage ? `${planDisplayName(planUsage.plan.id)}: ${formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent vs ${formatCost(planUsage.budgetUsd)} plan` : ''
|
||||
const planPct = planUsage ? `${planUsage.percentUsed.toFixed(1)}%` : ''
|
||||
const planColor = planUsage
|
||||
? planUsage.status === 'over'
|
||||
|
|
@ -219,10 +220,10 @@ function Overview({ projects, label, width, planUsage }: { projects: ProjectSumm
|
|||
</Text>
|
||||
<Text dimColor wrap="truncate-end">
|
||||
{planUsage.status === 'under'
|
||||
? `Well within plan. Projected month: ${formatUsd(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
|
||||
? `Well within plan. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
|
||||
: planUsage.status === 'near'
|
||||
? `Approaching plan limit. Projected month: ${formatUsd(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
|
||||
: `You're ${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x over subscription value; running on API overage pricing.`}
|
||||
? `Approaching plan limit. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
|
||||
: `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x your subscription value. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -703,7 +704,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
|
|||
if (reloadGenerationRef.current !== generation) return
|
||||
|
||||
setProjects(filteredProjects)
|
||||
const usage = await getPlanUsageOrNullForProjects(filteredProjects)
|
||||
const usage = await getPlanUsageOrNull()
|
||||
if (reloadGenerationRef.current !== generation) return
|
||||
setPlanUsage(usage ?? undefined)
|
||||
} catch (error) {
|
||||
|
|
@ -802,7 +803,7 @@ export async function renderDashboard(period: Period = 'week', provider: string
|
|||
await loadPricing()
|
||||
const range = customRange ?? getDateRange(period)
|
||||
const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter)
|
||||
const planUsage = await getPlanUsageOrNullForProjects(filteredProjects)
|
||||
const planUsage = await getPlanUsageOrNull()
|
||||
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
||||
if (isTTY) {
|
||||
const { waitUntilExit } = render(
|
||||
|
|
|
|||
|
|
@ -143,12 +143,6 @@ export async function getPlanUsageOrNull(today = new Date()): Promise<PlanUsage
|
|||
return getPlanUsage(plan, today)
|
||||
}
|
||||
|
||||
export async function getPlanUsageOrNullForProjects(projects: ProjectSummary[], today = new Date()): Promise<PlanUsage | null> {
|
||||
const plan = await readPlan()
|
||||
if (!isActivePlan(plan)) return null
|
||||
return getPlanUsageFromProjects(plan, projects, today)
|
||||
}
|
||||
|
||||
export function isActivePlan(plan: Plan | undefined): plan is Plan {
|
||||
return Boolean(plan) && plan.id !== 'none' && Number.isFinite(plan.monthlyUsd) && plan.monthlyUsd > 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue