Skip to content

Commit 080bc69

Browse files
theo-learnerclaude
andcommitted
feat: redesign Monthly Cost card with burn rate, session savings counter, and scenario label
- Add IIFE pattern to compute derived cost values inline - Show hourly burn rate alongside monthly cost figure - Add scenario label badge (Optimized/Moderate/High Load/Emergency) based on vCPU tier - Add session savings counter that accumulates saved/overspent since page load using 1s ticker - Session savings goes negative (red) when running above fixed baseline - Sync currentCostRef on each metrics fetch so ticker always uses the latest rate - Fix extra closing </div> that caused JSX fragment mismatch (tsc error) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 62e14f3 commit 080bc69

File tree

1 file changed

+88
-30
lines changed

1 file changed

+88
-30
lines changed

src/app/page.tsx

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,11 @@ export default function Dashboard() {
518518
const [costAnalysisData, setCostAnalysisData] = useState<CostReport | null>(null);
519519
const [costAnalysisLoading, setCostAnalysisLoading] = useState(false);
520520

521+
// --- Session Savings Counter ---
522+
const sessionSavingsRef = useRef<number>(0);
523+
const [sessionSavingsDisplay, setSessionSavingsDisplay] = useState<number>(0);
524+
const currentCostRef = useRef<{ monthlyCost: number; fixedCost: number }>({ monthlyCost: 42, fixedCost: 166 });
525+
521526
// --- Public Status / Showcase Banner State ---
522527
const [publicStatus, setPublicStatus] = useState<PublicStatus | null>(null);
523528
const [toastDismissed, setToastDismissed] = useState(true);
@@ -978,6 +983,11 @@ export default function Dashboard() {
978983
const data = await res.json();
979984

980985
setCurrent(data);
986+
// Keep cost ref in sync so the 1s savings counter uses the latest rate
987+
currentCostRef.current = {
988+
monthlyCost: data.cost?.opGethMonthlyCost ?? data.cost?.monthlyEstimated ?? 42,
989+
fixedCost: data.cost?.fixedCost ?? 166,
990+
};
981991

982992
const point = {
983993
name: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
@@ -1117,6 +1127,18 @@ export default function Dashboard() {
11171127
return () => clearInterval(interval);
11181128
}, []);
11191129

1130+
// --- Session Savings Counter: tick every 1s ---
1131+
useEffect(() => {
1132+
const ticker = setInterval(() => {
1133+
const { monthlyCost, fixedCost } = currentCostRef.current;
1134+
// hourly savings = (fixedCost - monthlyCost) / 730; per second = / 3600
1135+
const savingsPerSecond = (fixedCost - monthlyCost) / 730 / 3600;
1136+
sessionSavingsRef.current += savingsPerSecond;
1137+
setSessionSavingsDisplay(sessionSavingsRef.current);
1138+
}, 1000);
1139+
return () => clearInterval(ticker);
1140+
}, []);
1141+
11201142
if (isLoading) return (
11211143
<div className="flex h-screen w-full items-center justify-center bg-gray-50 text-blue-600">
11221144
<div className="flex flex-col items-center gap-4">
@@ -1462,35 +1484,72 @@ export default function Dashboard() {
14621484

14631485
{/* Card 1: Monthly Cost */}
14641486
<div className="bg-white rounded-2xl p-5 shadow-sm border border-gray-200/60">
1465-
<div data-testid="monthly-cost">
1466-
<span className="text-[11px] text-gray-400 font-semibold uppercase tracking-wider">Monthly Cost</span>
1467-
<div className="flex items-baseline gap-2 mt-1">
1468-
<span className="text-4xl font-black text-gray-900">
1469-
${(current?.cost.opGethMonthlyCost || current?.cost.monthlyEstimated || 42).toFixed(0)}
1470-
</span>
1471-
<span className="text-base font-bold text-gray-400">/mo</span>
1472-
</div>
1473-
<p className="text-gray-400 text-[10px] mt-1">
1474-
{current?.metrics.gethVcpu || 1} vCPU · {current?.metrics.gethMemGiB || 2} GiB
1475-
</p>
1476-
</div>
1477-
<div className="mt-3 pt-3 border-t border-gray-100">
1478-
<div className="flex items-center justify-between">
1479-
<span className="text-[10px] text-gray-400">vs Fixed 4 vCPU (${current?.cost.fixedCost?.toFixed(0) || '166'}/mo)</span>
1480-
<span className={`text-xs font-bold ${current?.cost.isPeakMode ? 'text-red-500' : 'text-green-600'}`}>
1481-
{current?.cost.isPeakMode ? '+' : ''}{((current?.cost.monthlySaving || 0) / (current?.cost.fixedCost || 166) * -100).toFixed(0)}%
1482-
</span>
1483-
</div>
1484-
<div className="mt-2 h-2 bg-gray-100 rounded-full overflow-hidden">
1485-
<div
1486-
className={`h-full rounded-full transition-all duration-500 ${current?.cost.isPeakMode ? 'bg-red-400' : 'bg-green-400'}`}
1487-
style={{ width: `${Math.min(Math.max(((current?.cost.opGethMonthlyCost || 42) / (current?.cost.fixedCost || 166)) * 100, 5), 100)}%` }}
1488-
/>
1489-
</div>
1490-
<div className="flex justify-between mt-1">
1491-
<span className="text-[9px] text-gray-400">$0</span>
1492-
<span className="text-[9px] text-gray-400">${current?.cost.fixedCost?.toFixed(0) || '166'}</span>
1493-
</div>
1487+
{(() => {
1488+
const monthlyCost = current?.cost.opGethMonthlyCost ?? current?.cost.monthlyEstimated ?? 42;
1489+
const fixedCost = current?.cost.fixedCost ?? 166;
1490+
const vcpu = current?.metrics.gethVcpu ?? 1;
1491+
const memGiB = current?.metrics.gethMemGiB ?? 2;
1492+
const isPeak = current?.cost.isPeakMode ?? false;
1493+
const hourlyRate = monthlyCost / 730;
1494+
const baselineHourly = fixedCost / 730;
1495+
const savingsPct = ((fixedCost - monthlyCost) / fixedCost * 100);
1496+
const barPct = Math.min(Math.max((monthlyCost / fixedCost) * 100, 5), 100);
1497+
const scenarioLabel = vcpu >= 8 ? { text: 'Emergency', color: 'text-red-500 bg-red-50' }
1498+
: vcpu >= 4 ? { text: 'High Load', color: 'text-orange-500 bg-orange-50' }
1499+
: vcpu >= 2 ? { text: 'Moderate', color: 'text-amber-500 bg-amber-50' }
1500+
: { text: 'Optimized', color: 'text-emerald-600 bg-emerald-50' };
1501+
const sessionAbs = Math.abs(sessionSavingsDisplay);
1502+
const sessionIsOverspend = sessionSavingsDisplay < -0.001;
1503+
return (
1504+
<>
1505+
<div data-testid="monthly-cost">
1506+
<div className="flex items-start justify-between">
1507+
<span className="text-[11px] text-gray-400 font-semibold uppercase tracking-wider">Monthly Cost</span>
1508+
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded-full ${scenarioLabel.color}`}>
1509+
{scenarioLabel.text}
1510+
</span>
1511+
</div>
1512+
<div className="flex items-baseline gap-2 mt-1">
1513+
<span className="text-4xl font-black text-gray-900">
1514+
${monthlyCost.toFixed(0)}
1515+
</span>
1516+
<span className="text-base font-bold text-gray-400">/mo</span>
1517+
<span className={`ml-auto text-[11px] font-semibold ${isPeak ? 'text-red-500' : 'text-gray-500'}`}>
1518+
${hourlyRate.toFixed(3)}/hr
1519+
</span>
1520+
</div>
1521+
<p className="text-gray-400 text-[10px] mt-0.5">
1522+
{vcpu} vCPU · {memGiB} GiB · baseline ${baselineHourly.toFixed(3)}/hr
1523+
</p>
1524+
</div>
1525+
<div className="mt-3 pt-3 border-t border-gray-100">
1526+
<div className="flex items-center justify-between">
1527+
<span className="text-[10px] text-gray-400">vs Fixed 4 vCPU (${fixedCost.toFixed(0)}/mo)</span>
1528+
<span className={`text-xs font-bold ${isPeak ? 'text-red-500' : 'text-green-600'}`}>
1529+
{isPeak ? '+' : '-'}{Math.abs(savingsPct).toFixed(0)}%
1530+
</span>
1531+
</div>
1532+
<div className="mt-2 h-2 bg-gray-100 rounded-full overflow-hidden">
1533+
<div
1534+
className={`h-full rounded-full transition-all duration-700 ${isPeak ? 'bg-red-400' : 'bg-emerald-400'}`}
1535+
style={{ width: `${barPct}%` }}
1536+
/>
1537+
</div>
1538+
<div className="flex justify-between mt-1">
1539+
<span className="text-[9px] text-gray-400">$0</span>
1540+
<span className="text-[9px] text-gray-400">${fixedCost.toFixed(0)}</span>
1541+
</div>
1542+
{/* Session Savings Counter */}
1543+
<div className={`mt-2.5 flex items-center justify-between px-2.5 py-1.5 rounded-lg ${sessionIsOverspend ? 'bg-red-50' : 'bg-emerald-50'}`}>
1544+
<span className="text-[10px] text-gray-500">Session {sessionIsOverspend ? 'overspend' : 'savings'}</span>
1545+
<span className={`text-[11px] font-bold tabular-nums ${sessionIsOverspend ? 'text-red-600' : 'text-emerald-600'}`}>
1546+
{sessionIsOverspend ? '+' : '-'}${sessionAbs.toFixed(4)}
1547+
</span>
1548+
</div>
1549+
</div>
1550+
</>
1551+
);
1552+
})()}
14941553
<button
14951554
onClick={() => {
14961555
if (costAnalysisExpanded) {
@@ -1571,7 +1630,6 @@ export default function Dashboard() {
15711630
)}
15721631
</div>
15731632
)}
1574-
</div>
15751633
</div>
15761634
</div>
15771635

0 commit comments

Comments
 (0)