fix(ui): billing-shape-aware quota chip and modal #43
@@ -670,49 +670,22 @@ function renderQuotaModal(quota) {
|
|||||||
const displayPct = (quota.metered_fraction_of_plan * 100).toFixed(1);
|
const displayPct = (quota.metered_fraction_of_plan * 100).toFixed(1);
|
||||||
const calls = Number(quota.metered_calls_30d || 0).toLocaleString();
|
const calls = Number(quota.metered_calls_30d || 0).toLocaleString();
|
||||||
|
|
||||||
// ── per-provider balance rows (reusing .bar-row / .bars-scroll) ─────
|
// ── provider accounts (labeled by billing shape) ────────────────────
|
||||||
const byProvider = quota.by_provider || {};
|
const byProvider = quota.by_provider || {};
|
||||||
const providers = Object.keys(byProvider).sort();
|
const providers = Object.keys(byProvider).sort();
|
||||||
let providerRowsHtml = '';
|
let providerRowsHtml = '';
|
||||||
let totalBalance = quota.total_balance_usd;
|
|
||||||
if (providers.length) {
|
if (providers.length) {
|
||||||
providerRowsHtml = providers.map(name => {
|
providerRowsHtml = providers.map(name => {
|
||||||
const p = byProvider[name] || {};
|
const p = byProvider[name] || {};
|
||||||
const balance = p.balance_usd;
|
|
||||||
const burn = p.burn_rate_usd_per_hour ?? null;
|
|
||||||
const proj = p.projected_hours_remaining ?? null;
|
|
||||||
const low = !!p.runway_low_warning;
|
const low = !!p.runway_low_warning;
|
||||||
const balanceAt = p.balance_at || null;
|
const accent = low ? 'var(--tblr-danger)' : 'var(--tblr-secondary)';
|
||||||
const source = p.balance_source || '';
|
return `<div class="bar-row" title="${formatProviderBalance(name, p)}">
|
||||||
const lineParts = [];
|
|
||||||
if (balance != null && typeof balance === 'number') {
|
|
||||||
lineParts.push('$' + balance.toFixed(2));
|
|
||||||
} else {
|
|
||||||
lineParts.push('balance unknown');
|
|
||||||
}
|
|
||||||
if (burn != null && typeof burn === 'number') {
|
|
||||||
lineParts.push('~$' + burn.toFixed(2) + '/hr');
|
|
||||||
}
|
|
||||||
if (proj != null && typeof proj === 'number') {
|
|
||||||
lineParts.push('~' + Math.round(proj) + 'h');
|
|
||||||
}
|
|
||||||
const suffix = formatBalanceStaleness(balanceAt, source);
|
|
||||||
const accent = low ? 'var(--tblr-danger)' : (balance == null || burn == null) ? 'var(--tblr-warning)' : 'var(--tblr-success)';
|
|
||||||
return `<div class="bar-row" title="${escapeHtml(name)}: ${lineParts.join(' · ')}${suffix ? ' · ' + suffix : ''}">
|
|
||||||
<span class="bar-label" style="color:${accent};font-weight:600">${escapeHtml(name)}</span>
|
<span class="bar-label" style="color:${accent};font-weight:600">${escapeHtml(name)}</span>
|
||||||
<span class="bar-val">${lineParts.join(' · ')}${suffix ? ' <span style="color:var(--tblr-secondary);font-size:0.68rem">· ' + escapeHtml(suffix) + '</span>' : ''}</span>
|
<span class="bar-val">${formatProviderBalance(name, p)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalRow = '';
|
|
||||||
if (totalBalance != null && typeof totalBalance === 'number') {
|
|
||||||
totalRow = `<div class="bar-row" style="border-top:1px solid rgba(255,255,255,.09);margin-top:4px;padding-top:8px">
|
|
||||||
<span class="bar-label" style="font-weight:700">Total</span>
|
|
||||||
<span class="bar-val" style="font-weight:700">$${totalBalance.toFixed(2)}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div style="font-size:1.8rem;font-weight:700;color:${heroColor}">${displayPct}%</div>
|
<div style="font-size:1.8rem;font-weight:700;color:${heroColor}">${displayPct}%</div>
|
||||||
@@ -725,15 +698,14 @@ function renderQuotaModal(quota) {
|
|||||||
<span class="quota-label">Reset</span><span class="quota-value">${quota.next_reset_date || 'not configured'}</span>
|
<span class="quota-label">Reset</span><span class="quota-value">${quota.next_reset_date || 'not configured'}</span>
|
||||||
</div>
|
</div>
|
||||||
${providers.length ? `
|
${providers.length ? `
|
||||||
<h6 class="card-title mb-2" style="font-size:.78rem;text-transform:uppercase;letter-spacing:.06em;color:var(--tblr-secondary)">Per-provider balance & runway</h6>
|
<h6 class="card-title mb-2" style="font-size:.78rem;text-transform:uppercase;letter-spacing:.06em;color:var(--tblr-secondary)">Provider accounts</h6>
|
||||||
<div id="quota-provider-rows" class="bars-scroll" style="max-height:260px">
|
<div id="quota-provider-rows" class="bars-scroll" style="max-height:260px">
|
||||||
${providerRowsHtml}
|
${providerRowsHtml}
|
||||||
${totalRow}
|
</div>` : '<div class="text-muted small">No provider accounts configured</div>'}
|
||||||
</div>` : ''}
|
|
||||||
<div class="quota-note">${escapeHtml(quota.note || '')}</div>`;
|
<div class="quota-note">${escapeHtml(quota.note || '')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBalanceStaleness(balanceAt, source) {
|
function formatBalanceStaleness(balanceAt) {
|
||||||
if (!balanceAt) return '';
|
if (!balanceAt) return '';
|
||||||
try {
|
try {
|
||||||
const then = new Date(balanceAt);
|
const then = new Date(balanceAt);
|
||||||
@@ -741,17 +713,57 @@ function formatBalanceStaleness(balanceAt, source) {
|
|||||||
if (Number.isNaN(ms) || ms < 0) return '';
|
if (Number.isNaN(ms) || ms < 0) return '';
|
||||||
const minutes = Math.floor(ms / 60000);
|
const minutes = Math.floor(ms / 60000);
|
||||||
if (minutes < 1) return 'just now';
|
if (minutes < 1) return 'just now';
|
||||||
if (minutes < 60) return `sampled ${minutes}m ago`;
|
if (minutes < 60) return `${minutes}m old`;
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
if (hours < 24) return `sampled ${hours}h ago`;
|
if (hours < 24) return `${hours}h old`;
|
||||||
const days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24);
|
||||||
return `sampled ${days}d ago`;
|
return `${days}d old`;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _quotaChipAccent(low, hasKnownBurn) {
|
/* Format a provider entry's balance according to its billing shape.
|
||||||
|
- Negative tiny balances render as "~$0.00 (slight overage)" so we never
|
||||||
|
show "-$0.00".
|
||||||
|
- Positive tiny balances render as "<$0.01".
|
||||||
|
- A genuinely zero balance renders as "$0.00". */
|
||||||
|
function formatProviderBalance(provider, entry) {
|
||||||
|
const source = entry && entry.balance_source ? entry.balance_source : '';
|
||||||
|
const balance = entry && typeof entry.balance_usd === 'number' ? entry.balance_usd : null;
|
||||||
|
const balanceAt = entry && entry.balance_at ? entry.balance_at : null;
|
||||||
|
const suffix = formatBalanceStaleness(balanceAt);
|
||||||
|
const suffixText = suffix ? ` (${suffix})` : '';
|
||||||
|
|
||||||
|
if (balance === null) {
|
||||||
|
return `${escapeHtml(provider)}: account balance not configured`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amountText;
|
||||||
|
if (balance === 0) {
|
||||||
|
amountText = '$0.00';
|
||||||
|
} else if (Math.abs(balance) < 0.005) {
|
||||||
|
amountText = balance < 0 ? '~$0.00 (slight overage)' : '<$0.01';
|
||||||
|
} else {
|
||||||
|
amountText = '$' + balance.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceText = source === 'polled'
|
||||||
|
? 'credits'
|
||||||
|
: source === 'telemetry'
|
||||||
|
? 'overage allowance'
|
||||||
|
: source === 'unconfigured'
|
||||||
|
? 'account balance not configured'
|
||||||
|
: 'account';
|
||||||
|
|
||||||
|
if (source === 'unconfigured') {
|
||||||
|
return `${escapeHtml(provider)}: ${amountText} · account balance not configured`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${escapeHtml(provider)}: ${amountText} ${sourceText}${suffixText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _quotaChipAccent(low) {
|
||||||
if (low) {
|
if (low) {
|
||||||
return {
|
return {
|
||||||
'--chip-accent': 'var(--tblr-danger)',
|
'--chip-accent': 'var(--tblr-danger)',
|
||||||
@@ -759,13 +771,6 @@ function _quotaChipAccent(low, hasKnownBurn) {
|
|||||||
borderColor: 'rgba(239,68,68,.35)',
|
borderColor: 'rgba(239,68,68,.35)',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!hasKnownBurn) {
|
|
||||||
return {
|
|
||||||
'--chip-accent': 'var(--tblr-warning)',
|
|
||||||
background: 'linear-gradient(135deg, rgba(245,158,11,.12), rgba(59,130,246,.08))',
|
|
||||||
borderColor: 'rgba(245,158,11,.38)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
'--chip-accent': 'var(--tblr-success)',
|
'--chip-accent': 'var(--tblr-success)',
|
||||||
};
|
};
|
||||||
@@ -781,58 +786,22 @@ function renderQuotaChip(quota) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chip.hidden = false;
|
chip.hidden = false;
|
||||||
// ── per-provider aggregate display -----------------------------------
|
|
||||||
const byProvider = quota.by_provider || {};
|
|
||||||
const providers = Object.keys(byProvider);
|
|
||||||
const entries = providers.map(name => byProvider[name]).filter(Boolean);
|
|
||||||
const knownBalances = entries.map(p => p.balance_usd).filter(v => v != null && typeof v === 'number');
|
|
||||||
const totalBalance = knownBalances.length ? knownBalances.reduce((a, b) => a + b, 0) : null;
|
|
||||||
|
|
||||||
if (totalBalance != null) {
|
// The chip always tells the kWh-subscription story; per-provider balances
|
||||||
const low = entries.some(p => !!p.runway_low_warning);
|
// live in the modal where they can be labeled by billing shape.
|
||||||
const hasKnownBurn = entries.some(p => p.burn_rate_usd_per_hour != null);
|
const pct = Math.min(quota.metered_fraction_of_plan * 100, 100);
|
||||||
const worstProjEntry = entries
|
const displayPct = (quota.metered_fraction_of_plan * 100).toFixed(1);
|
||||||
.filter(p => p.projected_hours_remaining != null)
|
fill.style.width = `${pct}%`;
|
||||||
.sort((a, b) => Number(a.projected_hours_remaining) - Number(b.projected_hours_remaining))[0];
|
text.textContent = `${displayPct}% · ${quota.plan_kwh} kWh plan`;
|
||||||
const hours = worstProjEntry ? Math.round(Number(worstProjEntry.projected_hours_remaining)) : null;
|
text.setAttribute('title', 'Click for provider account detail');
|
||||||
const chipStyle = _quotaChipAccent(low, hasKnownBurn);
|
|
||||||
const balanceStr = '$' + totalBalance.toFixed(2);
|
|
||||||
|
|
||||||
text.textContent = hours != null ? balanceStr + ' · ~' + hours + 'h' : balanceStr;
|
|
||||||
|
|
||||||
|
const chipStyle = _quotaChipAccent(pct > 90);
|
||||||
for (const [k, v] of Object.entries(chipStyle)) {
|
for (const [k, v] of Object.entries(chipStyle)) {
|
||||||
chip.style.setProperty(k, v);
|
chip.style.setProperty(k, v);
|
||||||
}
|
}
|
||||||
if (chipStyle['--chip-accent']) {
|
if (chipStyle['--chip-accent']) {
|
||||||
text.style.color = chipStyle['--chip-accent'];
|
text.style.color = chipStyle['--chip-accent'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip lists every provider plus its staleness
|
|
||||||
const tooltipLines = providers.map(name => {
|
|
||||||
const p = byProvider[name] || {};
|
|
||||||
const bal = p.balance_usd;
|
|
||||||
const burn = p.burn_rate_usd_per_hour;
|
|
||||||
const parts = [escapeHtml(name)];
|
|
||||||
if (bal != null && typeof bal === 'number') {
|
|
||||||
parts.push('$' + bal.toFixed(2));
|
|
||||||
} else {
|
|
||||||
parts.push('balance unknown');
|
|
||||||
}
|
|
||||||
if (burn != null && typeof burn === 'number') parts.push('~$' + burn.toFixed(2) + '/hr');
|
|
||||||
const proj = p.projected_hours_remaining;
|
|
||||||
if (proj != null && typeof proj === 'number') parts.push('~' + Math.round(proj) + 'h');
|
|
||||||
const suffix = formatBalanceStaleness(p.balance_at, p.balance_source);
|
|
||||||
if (suffix) parts.push(suffix);
|
|
||||||
return parts.join(' · ');
|
|
||||||
});
|
|
||||||
text.setAttribute('title', tooltipLines.join('\n'));
|
|
||||||
fill.style.width = `${Math.min(quota.metered_fraction_of_plan * 100, 100)}%`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ── fallback: percentage only ----------------------------------------
|
|
||||||
const pct = Math.min(quota.metered_fraction_of_plan * 100, 100);
|
|
||||||
fill.style.width = `${pct}%`;
|
|
||||||
text.textContent = `${(quota.metered_fraction_of_plan * 100).toFixed(1)}%`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDecisions(decisions) {
|
function renderDecisions(decisions) {
|
||||||
|
|||||||
147
src/tui.py
147
src/tui.py
@@ -65,79 +65,50 @@ def _fmt_runway(hours: float) -> str:
|
|||||||
return f"~{hours / 24:.1f}d"
|
return f"~{hours / 24:.1f}d"
|
||||||
|
|
||||||
|
|
||||||
def _quota_lead_values(rows: dict) -> tuple[Optional[float], Optional[float], Optional[float], bool, Optional[str]]:
|
def _format_small_balance(balance: Optional[float]) -> str:
|
||||||
providers = sorted(
|
if balance is None:
|
||||||
|
return "n/a"
|
||||||
|
if balance == 0:
|
||||||
|
return "$0.00"
|
||||||
|
if abs(balance) < 0.005:
|
||||||
|
return "~$0.00 (slight overage)" if balance < 0 else "<$0.01"
|
||||||
|
return f"${balance:.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_provider_quota_row(provider: str, rows: dict) -> str:
|
||||||
|
balance = rows.get(f"{provider} balance_usd")
|
||||||
|
source = rows.get(f"{provider} balance_source") or ""
|
||||||
|
balance_at = rows.get(f"{provider} balance_at")
|
||||||
|
burn = rows.get(f"{provider} burn_rate_usd_per_hour")
|
||||||
|
runway = rows.get(f"{provider} projected_hours_remaining")
|
||||||
|
|
||||||
|
amount = _format_small_balance(balance) if isinstance(balance, (int, float)) else "n/a"
|
||||||
|
if source == "polled":
|
||||||
|
label = "credits"
|
||||||
|
elif source == "telemetry":
|
||||||
|
label = "overage allowance"
|
||||||
|
elif source == "unconfigured":
|
||||||
|
label = "account balance not configured"
|
||||||
|
else:
|
||||||
|
label = "account"
|
||||||
|
|
||||||
|
parts = [f"{provider}: {amount} {label}"]
|
||||||
|
if burn is not None and isinstance(burn, (int, float)):
|
||||||
|
parts.append(f"burn ${burn:.2f}/h")
|
||||||
|
if runway is not None and isinstance(runway, (int, float)):
|
||||||
|
parts.append(f"runway {_fmt_runway(float(runway))}")
|
||||||
|
if balance_at:
|
||||||
|
parts.append(f"@{balance_at}")
|
||||||
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _provider_quota_rows(rows: dict) -> list[str]:
|
||||||
|
return [
|
||||||
|
_format_provider_quota_row(provider, rows)
|
||||||
|
for provider in sorted(
|
||||||
{label[: -len(" balance_usd")] for label in rows if label.endswith(" balance_usd")}
|
{label[: -len(" balance_usd")] for label in rows if label.endswith(" balance_usd")}
|
||||||
)
|
)
|
||||||
eligible = [
|
|
||||||
p for p in providers if rows.get(f"{p} balance_source") == "polled"
|
|
||||||
]
|
]
|
||||||
candidates = []
|
|
||||||
for provider in eligible:
|
|
||||||
balance_value = rows.get(f"{provider} balance_usd")
|
|
||||||
balance = _numeric_suffix_value(balance_value)
|
|
||||||
burn_value = rows.get(f"{provider} burn_rate_usd_per_hour")
|
|
||||||
burn = _numeric_suffix_value(burn_value)
|
|
||||||
runway_value = rows.get(f"{provider} projected_hours_remaining")
|
|
||||||
runway = _numeric_suffix_value(runway_value)
|
|
||||||
low = bool(rows.get(f"{provider} runway_low_warning"))
|
|
||||||
note = rows.get(f"{provider} runway_note")
|
|
||||||
candidates.append((provider, balance, burn, runway, low, note))
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
return (None, None, None, False, None)
|
|
||||||
|
|
||||||
def _key(item):
|
|
||||||
_provider, balance, _burn, runway, _low, _note = item
|
|
||||||
runway_sort = runway if isinstance(runway, (int, float)) else float("inf")
|
|
||||||
balance_sort = balance if isinstance(balance, (int, float)) else float("inf")
|
|
||||||
return (runway_sort, balance_sort, item[0])
|
|
||||||
|
|
||||||
_, balance, burn, runway, low, note = min(candidates, key=_key)
|
|
||||||
return (balance, burn, runway, low, note)
|
|
||||||
|
|
||||||
|
|
||||||
def _numeric_suffix_value(value: object) -> Optional[float]:
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(value, (int, float)):
|
|
||||||
return float(value)
|
|
||||||
if isinstance(value, str):
|
|
||||||
try:
|
|
||||||
return float(value.split()[0])
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _format_quota_lead(
|
|
||||||
balance: Optional[float],
|
|
||||||
burn: Optional[float],
|
|
||||||
runway_hours: Optional[float],
|
|
||||||
low_warning: bool,
|
|
||||||
) -> str:
|
|
||||||
"""The quota panel's headline: balance, burn rate, projected runway.
|
|
||||||
|
|
||||||
Deliberately 2dp rather than ``_fmt_usd``'s 6 — this is money an operator
|
|
||||||
acts on, not per-request micro-money.
|
|
||||||
|
|
||||||
``burn`` and ``runway_hours`` are legitimately ``None`` when the sample
|
|
||||||
segment is too short to extrapolate (PR #30's guard against a wild rate
|
|
||||||
right after a top-up). That is a real answer, not missing data, so it
|
|
||||||
renders "burn n/a" and the caller shows ``runway_note`` alongside. The
|
|
||||||
literal "None" must never reach the screen.
|
|
||||||
"""
|
|
||||||
if balance is None:
|
|
||||||
return "balance n/a"
|
|
||||||
parts = [f"balance ${balance:.2f} left"]
|
|
||||||
parts.append(f"burn ${burn:.2f}/h" if burn is not None else "burn n/a")
|
|
||||||
if runway_hours is not None:
|
|
||||||
runway = f"runway {_fmt_runway(float(runway_hours))}"
|
|
||||||
# Same markup idiom the legend already uses for `resets`.
|
|
||||||
parts.append(
|
|
||||||
f"[rgb(200,80,80)]{runway}[/rgb(200,80,80)]" if low_warning else runway
|
|
||||||
)
|
|
||||||
return " · ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
# Compact bottom keys legend. The 11 BINDINGS are collapsed to these short
|
# Compact bottom keys legend. The 11 BINDINGS are collapsed to these short
|
||||||
@@ -603,16 +574,6 @@ class DashboardApp(App):
|
|||||||
flex_default=None,
|
flex_default=None,
|
||||||
window_start_30d=None,
|
window_start_30d=None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Build the quota legend string, suppressing ``None`` values.
|
|
||||||
|
|
||||||
``metered``/``frac``/``calls`` come from ``rows.get(...)`` and may be
|
|
||||||
``None`` (unmetered plan); render those as ``n/a`` rather than the
|
|
||||||
Python literal so the legend never contains the substring ``"None"``.
|
|
||||||
|
|
||||||
``reset_date`` is the billing-period start (shown as "period since …").
|
|
||||||
``window_start_30d`` is the rolling 30-day window start (shown as
|
|
||||||
"window start …").
|
|
||||||
"""
|
|
||||||
metered_fmt = "n/a" if metered is None else metered
|
metered_fmt = "n/a" if metered is None else metered
|
||||||
frac_fmt = "n/a" if frac is None else frac
|
frac_fmt = "n/a" if frac is None else frac
|
||||||
calls_fmt = "n/a" if calls is None else calls
|
calls_fmt = "n/a" if calls is None else calls
|
||||||
@@ -625,13 +586,11 @@ class DashboardApp(App):
|
|||||||
legend += f" · [rgb(128,128,128)]flex default {flex_default}[/rgb(128,128,128)]"
|
legend += f" · [rgb(128,128,128)]flex default {flex_default}[/rgb(128,128,128)]"
|
||||||
if reset_date:
|
if reset_date:
|
||||||
legend += (
|
legend += (
|
||||||
f" · [rgb(128,128,128)]period since {reset_date}"
|
f" · [rgb(128,128,128)]period since {reset_date}[/rgb(128,128,128)]"
|
||||||
f"[/rgb(128,128,128)]"
|
|
||||||
)
|
)
|
||||||
if window_start_30d:
|
if window_start_30d:
|
||||||
legend += (
|
legend += (
|
||||||
f" · [rgb(128,128,128)]window start {window_start_30d}"
|
f" · [rgb(128,128,128)]window start {window_start_30d}[/rgb(128,128,128)]"
|
||||||
f"[/rgb(128,128,128)]"
|
|
||||||
)
|
)
|
||||||
if next_reset_date:
|
if next_reset_date:
|
||||||
legend += (
|
legend += (
|
||||||
@@ -639,6 +598,10 @@ class DashboardApp(App):
|
|||||||
)
|
)
|
||||||
return legend
|
return legend
|
||||||
|
|
||||||
|
def _format_quota_kwh_lead(self, plan, metered, frac) -> str:
|
||||||
|
percent = round(float(frac) * 100) if frac is not None else 0
|
||||||
|
return f"{percent}% of {plan} kWh plan ({metered or 0} kWh metered)"
|
||||||
|
|
||||||
def _render(self, model: dict) -> None:
|
def _render(self, model: dict) -> None:
|
||||||
# Quota panel — guard against being called before compose() mounts
|
# Quota panel — guard against being called before compose() mounts
|
||||||
# these widgets (set_interval in on_mount can fire _on_interval too
|
# these widgets (set_interval in on_mount can fire _on_interval too
|
||||||
@@ -655,18 +618,10 @@ class DashboardApp(App):
|
|||||||
frac = rows.get("fraction")
|
frac = rows.get("fraction")
|
||||||
calls = rows.get("calls")
|
calls = rows.get("calls")
|
||||||
|
|
||||||
# Balance/runway lead. Wired OUTSIDE the plan gate on purpose: the
|
|
||||||
# credit balance is meaningful whether or not a kWh plan is set, and
|
|
||||||
# it is the figure that actually stops traffic.
|
|
||||||
lead = self.query_one("#quota-lead", Static)
|
lead = self.query_one("#quota-lead", Static)
|
||||||
note = self.query_one("#quota-note", Static)
|
note = self.query_one("#quota-note", Static)
|
||||||
if rows:
|
if rows:
|
||||||
balance, burn, runway, low, note_text = _quota_lead_values(rows)
|
lead.update(self._format_quota_kwh_lead(plan, metered, frac))
|
||||||
lead.update(_format_quota_lead(balance, burn, runway, low))
|
|
||||||
if balance is not None and burn is None:
|
|
||||||
note.update(str(note_text or "burn estimate unavailable"))
|
|
||||||
note.display = True
|
|
||||||
else:
|
|
||||||
note.update("")
|
note.update("")
|
||||||
note.display = False
|
note.display = False
|
||||||
else:
|
else:
|
||||||
@@ -682,12 +637,14 @@ class DashboardApp(App):
|
|||||||
window_start = rows.get("window_start_30d")
|
window_start = rows.get("window_start_30d")
|
||||||
flex_default = model.get("flex_default")
|
flex_default = model.get("flex_default")
|
||||||
next_reset_date = rows.get("next_reset_date")
|
next_reset_date = rows.get("next_reset_date")
|
||||||
legend.update(
|
legend_lines = [
|
||||||
self._format_quota_legend(
|
self._format_quota_legend(
|
||||||
plan, metered, frac, calls, reset_date,
|
plan, metered, frac, calls, reset_date,
|
||||||
next_reset_date, flex_default, window_start,
|
next_reset_date, flex_default, window_start,
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
|
legend_lines.extend(_provider_quota_rows(rows))
|
||||||
|
legend.update("\n".join(legend_lines))
|
||||||
else:
|
else:
|
||||||
bar.display = False
|
bar.display = False
|
||||||
legend.update("quota not configured")
|
legend.update("quota not configured")
|
||||||
|
|||||||
@@ -56,21 +56,19 @@ def build_model(data: dict) -> dict:
|
|||||||
{"label": "reset_date", "value": quota.get("reset_date")},
|
{"label": "reset_date", "value": quota.get("reset_date")},
|
||||||
{"label": "next_reset_date", "value": quota.get("next_reset_date")},
|
{"label": "next_reset_date", "value": quota.get("next_reset_date")},
|
||||||
{"label": "window_start_30d", "value": quota.get("window_start_30d")},
|
{"label": "window_start_30d", "value": quota.get("window_start_30d")},
|
||||||
{"label": "total_balance_usd", "value": quota.get("total_balance_usd")},
|
|
||||||
]
|
]
|
||||||
for provider, entry in sorted((quota.get("by_provider") or {}).items()):
|
for provider, entry in sorted((quota.get("by_provider") or {}).items()):
|
||||||
balance = entry.get("balance_usd")
|
balance = entry.get("balance_usd")
|
||||||
|
source = entry.get("balance_source") or ""
|
||||||
balance_at = entry.get("balance_at")
|
balance_at = entry.get("balance_at")
|
||||||
balance_label = f"{provider} balance_usd"
|
|
||||||
if balance is None:
|
|
||||||
balance_value = None
|
|
||||||
elif balance_at:
|
|
||||||
balance_value = f"{balance} @ {balance_at}"
|
|
||||||
else:
|
|
||||||
balance_value = str(balance)
|
|
||||||
quota_rows.append({"label": balance_label, "value": balance_value})
|
|
||||||
quota_rows.append(
|
quota_rows.append(
|
||||||
{"label": f"{provider} balance_source", "value": entry.get("balance_source")}
|
{"label": f"{provider} balance_usd", "value": balance}
|
||||||
|
)
|
||||||
|
quota_rows.append(
|
||||||
|
{"label": f"{provider} balance_source", "value": source}
|
||||||
|
)
|
||||||
|
quota_rows.append(
|
||||||
|
{"label": f"{provider} balance_at", "value": balance_at}
|
||||||
)
|
)
|
||||||
quota_rows.append(
|
quota_rows.append(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -175,13 +175,18 @@ def test_quota_modal_has_not_configured_fallback():
|
|||||||
assert "not configured" in html
|
assert "not configured" in html
|
||||||
|
|
||||||
|
|
||||||
def test_admin_quota_chip_references_balance_and_runway():
|
def test_admin_quota_chip_references_kwh_plan_and_modal_lists_provider_accounts():
|
||||||
"""The index page references per-provider balance / runway quota fields and the provider rows container."""
|
"""The chipshows kWh-plan fraction and the modal has labeled Provider accounts rows."""
|
||||||
index_path = ROOT / "admin" / "frontend" / "index.html"
|
index_path = ROOT / "admin" / "frontend" / "index.html"
|
||||||
html = index_path.read_text()
|
html = index_path.read_text()
|
||||||
assert "balance_usd" in html and "runway_low_warning" in html
|
assert "metered_fraction_of_plan" in html and "plan_kwh" in html
|
||||||
assert 'id="quota-provider-rows"' in html
|
assert 'id="quota-provider-rows"' in html
|
||||||
assert "balance_at" in html
|
assert "Provider accounts" in html
|
||||||
|
assert "formatProviderBalance" in html
|
||||||
|
assert "overage allowance" in html
|
||||||
|
assert "credits" in html
|
||||||
|
assert "~$0.00 (slight overage)" in html
|
||||||
|
assert "<$0.01" in html
|
||||||
|
|
||||||
|
|
||||||
def test_builtin_profile_cards_offer_duplicate(admin_client):
|
def test_builtin_profile_cards_offer_duplicate(admin_client):
|
||||||
|
|||||||
@@ -239,14 +239,15 @@ def test_build_model_quota_panel_includes_reset_date():
|
|||||||
assert by_label["window_start_30d"] == "2026-07-24"
|
assert by_label["window_start_30d"] == "2026-07-24"
|
||||||
|
|
||||||
|
|
||||||
def test_build_model_quota_panel_includes_total_balance_and_provider_rows():
|
def test_build_model_quota_panel_includes_provider_rows():
|
||||||
"""The new per-provider quota shape must reach the TUI data model."""
|
"""The per-provider quota shape reaches the TUI data model."""
|
||||||
m = build_model(_fixture())
|
m = build_model(_fixture())
|
||||||
rows = m["quota"]
|
rows = m["quota"]
|
||||||
by_label = {r["label"]: r["value"] for r in rows}
|
by_label = {r["label"]: r["value"] for r in rows}
|
||||||
assert by_label["total_balance_usd"] == 8.50
|
assert "total_balance_usd" not in by_label
|
||||||
assert by_label["neuralwatt balance_usd"] == "8.5 @ 2026-08-23T09:58:00+00:00"
|
assert by_label["neuralwatt balance_usd"] == 8.5
|
||||||
assert by_label["neuralwatt balance_source"] == "telemetry"
|
assert by_label["neuralwatt balance_source"] == "telemetry"
|
||||||
|
assert by_label["neuralwatt balance_at"] == "2026-08-23T09:58:00+00:00"
|
||||||
assert by_label["neuralwatt burn_rate_usd_per_hour"] == 1.0
|
assert by_label["neuralwatt burn_rate_usd_per_hour"] == 1.0
|
||||||
assert by_label["neuralwatt projected_hours_remaining"] == 8.5
|
assert by_label["neuralwatt projected_hours_remaining"] == 8.5
|
||||||
assert by_label["neuralwatt runway_low_warning"] is False
|
assert by_label["neuralwatt runway_low_warning"] is False
|
||||||
@@ -1717,195 +1718,159 @@ def test_placeholder_row_matches_column_arity():
|
|||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# T4 — quota panel leads with balance and runway.
|
# T4 — quota panel: kWh-only lead, labeled per-provider account rows.
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_format_quota_lead_states():
|
def test_format_small_balance_renders_tiny_and_normal_values():
|
||||||
"""All four lead states, including the one this deployment is actually in.
|
assert tui._format_small_balance(None) == "n/a"
|
||||||
|
assert tui._format_small_balance(0.0) == "$0.00"
|
||||||
burn/runway are legitimately None when the sample segment is too short
|
assert tui._format_small_balance(0.0001) == "<$0.01"
|
||||||
(PR #30). That is deliberate, not missing data, so the None path must
|
assert tui._format_small_balance(-0.004) == "~$0.00 (slight overage)"
|
||||||
still say something an operator can read.
|
assert tui._format_small_balance(8.50) == "$8.50"
|
||||||
"""
|
assert tui._format_small_balance(-1.0) == "$-1.00"
|
||||||
full = tui._format_quota_lead(8.50, 1.0, 8.5, False)
|
|
||||||
assert "balance $8.50 left" in full
|
|
||||||
assert "burn $1.00/h" in full
|
|
||||||
assert "runway ~8.5h" in full
|
|
||||||
|
|
||||||
none_burn = tui._format_quota_lead(8.50, None, None, False)
|
|
||||||
assert "burn n/a" in none_burn
|
|
||||||
assert "None" not in none_burn
|
|
||||||
|
|
||||||
assert tui._format_quota_lead(None, 1.0, 8.5, False) == "balance n/a"
|
|
||||||
assert "runway ~20.8d" in tui._format_quota_lead(100.0, 1.0, 500.0, False)
|
|
||||||
|
|
||||||
low = tui._format_quota_lead(8.50, 4.0, 1.5, True)
|
|
||||||
assert "[rgb(200,80,80)]runway ~1.5h" in low
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_lead_values_binds_only_to_polled_providers():
|
def test_format_provider_quota_row_labels_billing_shape():
|
||||||
rows = {
|
polled = {
|
||||||
"openrouter balance_usd": "50.0 @ 2026-08-23T10:00:00+00:00",
|
"openrouter balance_usd": 4.999,
|
||||||
"openrouter balance_source": "polled",
|
"openrouter balance_source": "polled",
|
||||||
"openrouter burn_rate_usd_per_hour": 5.0,
|
"openrouter balance_at": "2026-08-23T10:00:00+00:00",
|
||||||
|
"openrouter burn_rate_usd_per_hour": 0.5,
|
||||||
"openrouter projected_hours_remaining": 10.0,
|
"openrouter projected_hours_remaining": 10.0,
|
||||||
"openrouter runway_low_warning": False,
|
}
|
||||||
"openrouter runway_note": None,
|
row = tui._format_provider_quota_row("openrouter", polled)
|
||||||
"neuralwatt balance_usd": "-0.004 @ 2026-08-23T10:00:00+00:00",
|
assert "openrouter: $5.00 credits" in row
|
||||||
|
assert "burn $0.50/h" in row
|
||||||
|
assert "runway ~10.0h" in row
|
||||||
|
|
||||||
|
telemetry = {
|
||||||
|
"neuralwatt balance_usd": -0.004,
|
||||||
"neuralwatt balance_source": "telemetry",
|
"neuralwatt balance_source": "telemetry",
|
||||||
"neuralwatt burn_rate_usd_per_hour": 1.0,
|
"neuralwatt balance_at": "2026-08-23T09:58:00+00:00",
|
||||||
"neuralwatt projected_hours_remaining": -0.004,
|
|
||||||
"neuralwatt runway_low_warning": True,
|
|
||||||
"neuralwatt runway_note": "allowance reads negative",
|
|
||||||
}
|
}
|
||||||
balance, burn, runway, low, note = tui._quota_lead_values(rows)
|
row = tui._format_provider_quota_row("neuralwatt", telemetry)
|
||||||
assert balance == 50.0
|
assert "neuralwatt: ~$0.00 (slight overage) overage allowance" in row
|
||||||
assert burn == 5.0
|
|
||||||
assert runway == 10.0
|
|
||||||
assert low is False
|
|
||||||
assert note is None
|
|
||||||
|
|
||||||
|
unconfigured = {
|
||||||
def test_quota_lead_values_uses_lowest_runway_among_polled():
|
"provider-a balance_usd": None,
|
||||||
rows = {
|
"provider-a balance_source": "unconfigured",
|
||||||
"openrouter balance_usd": 50.0,
|
|
||||||
"openrouter balance_source": "polled",
|
|
||||||
"openrouter burn_rate_usd_per_hour": 5.0,
|
|
||||||
"openrouter projected_hours_remaining": 10.0,
|
|
||||||
"openrouter runway_low_warning": False,
|
|
||||||
"openrouter runway_note": None,
|
|
||||||
"provider-b balance_usd": 20.0,
|
|
||||||
"provider-b balance_source": "polled",
|
|
||||||
"provider-b burn_rate_usd_per_hour": 5.0,
|
|
||||||
"provider-b projected_hours_remaining": 3.0,
|
|
||||||
"provider-b runway_low_warning": True,
|
|
||||||
"provider-b runway_note": "low",
|
|
||||||
}
|
}
|
||||||
balance, burn, runway, low, note = tui._quota_lead_values(rows)
|
row = tui._format_provider_quota_row("provider-a", unconfigured)
|
||||||
assert balance == 20.0
|
assert "provider-a: n/a account balance not configured" in row
|
||||||
assert runway == 3.0
|
|
||||||
assert low is True
|
|
||||||
assert note == "low"
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_lead_values_falls_back_to_lowest_balance_when_no_runway():
|
def test_provider_quota_rows_returns_one_row_per_provider_sorted():
|
||||||
rows = {
|
rows = {
|
||||||
"openrouter balance_usd": 50.0,
|
"neuralwatt balance_usd": 0.0001,
|
||||||
|
"neuralwatt balance_source": "telemetry",
|
||||||
|
"neuralwatt balance_at": None,
|
||||||
|
"neuralwatt burn_rate_usd_per_hour": None,
|
||||||
|
"neuralwatt projected_hours_remaining": None,
|
||||||
|
"neuralwatt runway_low_warning": False,
|
||||||
|
"neuralwatt runway_note": None,
|
||||||
|
"openrouter balance_usd": 4.999,
|
||||||
"openrouter balance_source": "polled",
|
"openrouter balance_source": "polled",
|
||||||
|
"openrouter balance_at": None,
|
||||||
"openrouter burn_rate_usd_per_hour": None,
|
"openrouter burn_rate_usd_per_hour": None,
|
||||||
"openrouter projected_hours_remaining": None,
|
"openrouter projected_hours_remaining": None,
|
||||||
"openrouter runway_low_warning": False,
|
"openrouter runway_low_warning": False,
|
||||||
"openrouter runway_note": "recent top-up",
|
|
||||||
"provider-b balance_usd": 20.0,
|
|
||||||
"provider-b balance_source": "polled",
|
|
||||||
"provider-b burn_rate_usd_per_hour": None,
|
|
||||||
"provider-b projected_hours_remaining": None,
|
|
||||||
"provider-b runway_low_warning": False,
|
|
||||||
"provider-b runway_note": None,
|
|
||||||
}
|
|
||||||
balance, burn, runway, low, note = tui._quota_lead_values(rows)
|
|
||||||
assert balance == 20.0
|
|
||||||
assert burn is None
|
|
||||||
assert runway is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_lead_values_returns_none_for_all_telemetry_providers():
|
|
||||||
rows = {
|
|
||||||
"neuralwatt balance_usd": -0.004,
|
|
||||||
"neuralwatt balance_source": "telemetry",
|
|
||||||
"neuralwatt burn_rate_usd_per_hour": 1.0,
|
|
||||||
"neuralwatt projected_hours_remaining": -0.004,
|
|
||||||
"neuralwatt runway_low_warning": True,
|
|
||||||
"neuralwatt runway_note": "negative allowance",
|
|
||||||
}
|
|
||||||
assert tui._quota_lead_values(rows) == (None, None, None, False, None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_lead_values_returns_none_for_old_flat_payload():
|
|
||||||
rows = {
|
|
||||||
"balance_usd": 8.5,
|
|
||||||
"burn_rate_usd_per_hour": 1.0,
|
|
||||||
"projected_hours_remaining": 8.5,
|
|
||||||
"runway_low_warning": False,
|
|
||||||
"runway_note": None,
|
|
||||||
}
|
|
||||||
assert tui._quota_lead_values(rows) == (None, None, None, False, None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_lead_values_sorted_name_tiebreak():
|
|
||||||
rows = {
|
|
||||||
"openrouter balance_usd": 50.0,
|
|
||||||
"openrouter balance_source": "polled",
|
|
||||||
"openrouter burn_rate_usd_per_hour": 1.0,
|
|
||||||
"openrouter projected_hours_remaining": 5.0,
|
|
||||||
"openrouter runway_low_warning": False,
|
|
||||||
"openrouter runway_note": None,
|
"openrouter runway_note": None,
|
||||||
"provider-a balance_usd": 50.0,
|
|
||||||
"provider-a balance_source": "polled",
|
|
||||||
"provider-a burn_rate_usd_per_hour": 1.0,
|
|
||||||
"provider-a projected_hours_remaining": 5.0,
|
|
||||||
"provider-a runway_low_warning": False,
|
|
||||||
"provider-a runway_note": None,
|
|
||||||
}
|
}
|
||||||
balance, burn, runway, low, note = tui._quota_lead_values(rows)
|
out = tui._provider_quota_rows(rows)
|
||||||
assert balance == 50.0
|
assert len(out) == 2
|
||||||
assert runway == 5.0
|
assert out[0].startswith("neuralwatt:")
|
||||||
|
assert "overage allowance" in out[0]
|
||||||
|
assert out[1].startswith("openrouter:")
|
||||||
|
assert "credits" in out[1]
|
||||||
|
|
||||||
|
|
||||||
def test_quota_panel_burn_unavailable_renders_note_verbatim():
|
def test_quota_panel_lead_is_kwh_only_and_note_empty():
|
||||||
"""When burn is None the note renders VERBATIM — never blank, 0, or None.
|
"""The quota lead is the kWh-plan fraction; the note line stays hidden."""
|
||||||
|
|
||||||
This is the state the live deployment is in right now, so it is the first
|
|
||||||
thing an operator sees, not an edge case.
|
|
||||||
"""
|
|
||||||
stub = _StubFetcher()
|
stub = _StubFetcher()
|
||||||
data = _fixture()
|
data = _fixture()
|
||||||
note = "burn estimate unavailable: segment after last balance increase spans only 9 minutes"
|
|
||||||
for block in (data["quota"], data["coverage"]["quota"]):
|
|
||||||
block["by_provider"]["neuralwatt"]["balance_source"] = "polled"
|
|
||||||
block["by_provider"]["neuralwatt"]["balance_usd"] = 7.5
|
|
||||||
block["by_provider"]["neuralwatt"]["burn_rate_usd_per_hour"] = None
|
|
||||||
block["by_provider"]["neuralwatt"]["projected_hours_remaining"] = None
|
|
||||||
block["by_provider"]["neuralwatt"]["runway_low_warning"] = False
|
|
||||||
block["by_provider"]["neuralwatt"]["runway_note"] = note
|
|
||||||
stub.payload = data
|
stub.payload = data
|
||||||
app = tui.DashboardApp(fetcher=stub)
|
app = tui.DashboardApp(fetcher=stub)
|
||||||
|
|
||||||
def _assert(a):
|
def _assert(a):
|
||||||
lead_text = str(a.query_one("#quota-lead", Static).content)
|
lead_text = str(a.query_one("#quota-lead", Static).content)
|
||||||
note_text = str(a.query_one("#quota-note", Static).content)
|
assert "20% of 6.25 kWh plan" in lead_text
|
||||||
assert "balance $7.50 left" in lead_text
|
|
||||||
assert "burn n/a" in lead_text
|
|
||||||
assert "None" not in lead_text
|
assert "None" not in lead_text
|
||||||
# The note is passed through unchanged, not summarised.
|
|
||||||
assert note_text == note
|
|
||||||
assert note_text.strip() != ""
|
|
||||||
assert note_text.strip() != "0"
|
|
||||||
assert note_text.strip() != "None"
|
|
||||||
|
|
||||||
_run_app(app, _assert)
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_panel_note_hidden_when_burn_is_available():
|
|
||||||
"""With a real burn rate there is nothing to explain, so no note line."""
|
|
||||||
stub = _StubFetcher()
|
|
||||||
data = _fixture()
|
|
||||||
for block in (data["quota"], data["coverage"]["quota"]):
|
|
||||||
block["by_provider"]["neuralwatt"]["balance_source"] = "polled"
|
|
||||||
block["by_provider"]["neuralwatt"]["balance_usd"] = 12.0
|
|
||||||
block["by_provider"]["neuralwatt"]["burn_rate_usd_per_hour"] = 0.5
|
|
||||||
block["by_provider"]["neuralwatt"]["projected_hours_remaining"] = 24.0
|
|
||||||
block["by_provider"]["neuralwatt"]["runway_low_warning"] = False
|
|
||||||
block["by_provider"]["neuralwatt"]["runway_note"] = None
|
|
||||||
stub.payload = data
|
|
||||||
app = tui.DashboardApp(fetcher=stub)
|
|
||||||
|
|
||||||
def _assert(a):
|
|
||||||
lead_text = str(a.query_one("#quota-lead", Static).content)
|
|
||||||
assert "balance $12.00 left" in lead_text
|
|
||||||
assert "burn $0.50/h" in lead_text
|
|
||||||
assert "runway ~24.0h" in lead_text
|
|
||||||
assert a.query_one("#quota-note", Static).display is False
|
assert a.query_one("#quota-note", Static).display is False
|
||||||
|
|
||||||
_run_app(app, _assert)
|
_run_app(app, _assert)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quota_legend_includes_provider_account_rows():
|
||||||
|
"""The TUI quota legend lists one labeled row per provider after the kWh
|
||||||
|
summary line."""
|
||||||
|
stub = _StubFetcher()
|
||||||
|
data = _fixture()
|
||||||
|
stub.payload = data
|
||||||
|
app = tui.DashboardApp(fetcher=stub)
|
||||||
|
|
||||||
|
def _assert(a):
|
||||||
|
legend = str(a.query_one("#quota-legend", Static).content)
|
||||||
|
assert "neuralwatt:" in legend
|
||||||
|
assert "overage allowance" in legend
|
||||||
|
assert "None" not in legend
|
||||||
|
|
||||||
|
_run_app(app, _assert)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quota_legend_lists_polled_provider_with_credits_label():
|
||||||
|
"""A polled provider row shows the credits label, distinct from a
|
||||||
|
telemetry provider's overage-allowance label."""
|
||||||
|
stub = _StubFetcher()
|
||||||
|
data = _fixture()
|
||||||
|
for block in (data["quota"], data["coverage"]["quota"]):
|
||||||
|
block["by_provider"]["openrouter"] = {
|
||||||
|
"balance_usd": 4.999,
|
||||||
|
"balance_at": "2026-08-23T10:00:00+00:00",
|
||||||
|
"balance_source": "polled",
|
||||||
|
"burn_rate_usd_per_hour": None,
|
||||||
|
"projected_hours_remaining": None,
|
||||||
|
"runway_low_warning": False,
|
||||||
|
"runway_note": None,
|
||||||
|
}
|
||||||
|
stub.payload = data
|
||||||
|
app = tui.DashboardApp(fetcher=stub)
|
||||||
|
|
||||||
|
def _assert(a):
|
||||||
|
legend = str(a.query_one("#quota-legend", Static).content)
|
||||||
|
assert "openrouter:" in legend
|
||||||
|
assert "credits" in legend
|
||||||
|
|
||||||
|
_run_app(app, _assert)
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_small_balance_handles_boundary_and_zero():
|
||||||
|
"""Exactly ±0.005 renders conventionally (not tiny); zero renders $0.00."""
|
||||||
|
assert tui._format_small_balance(0.005) == "$0.01"
|
||||||
|
assert tui._format_small_balance(-0.005) == "$-0.01"
|
||||||
|
assert tui._format_small_balance(0.0) == "$0.00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_provider_quota_row_includes_balance_at_when_present():
|
||||||
|
rows = {
|
||||||
|
"openrouter balance_usd": 4.999,
|
||||||
|
"openrouter balance_source": "polled",
|
||||||
|
"openrouter balance_at": "2026-08-23T10:00:00+00:00",
|
||||||
|
"openrouter burn_rate_usd_per_hour": None,
|
||||||
|
"openrouter projected_hours_remaining": None,
|
||||||
|
}
|
||||||
|
row = tui._format_provider_quota_row("openrouter", rows)
|
||||||
|
assert "openrouter: $5.00 credits" in row
|
||||||
|
assert "@2026-08-23T10:00:00+00:00" in row
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_provider_quota_row_unknown_source_prefixes_account():
|
||||||
|
rows = {
|
||||||
|
"provider-x balance_usd": 3.25,
|
||||||
|
"provider-x balance_source": "",
|
||||||
|
"provider-x balance_at": None,
|
||||||
|
"provider-x burn_rate_usd_per_hour": None,
|
||||||
|
"provider-x projected_hours_remaining": None,
|
||||||
|
}
|
||||||
|
row = tui._format_provider_quota_row("provider-x", rows)
|
||||||
|
assert "provider-x: $3.25 account" in row
|
||||||
|
|||||||
Reference in New Issue
Block a user