diff --git a/admin/frontend/index.html b/admin/frontend/index.html index d5f6b5b..d1e6500 100644 --- a/admin/frontend/index.html +++ b/admin/frontend/index.html @@ -670,49 +670,22 @@ function renderQuotaModal(quota) { const displayPct = (quota.metered_fraction_of_plan * 100).toFixed(1); 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 providers = Object.keys(byProvider).sort(); let providerRowsHtml = ''; - let totalBalance = quota.total_balance_usd; if (providers.length) { providerRowsHtml = providers.map(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 balanceAt = p.balance_at || null; - const source = p.balance_source || ''; - 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 `
+ const accent = low ? 'var(--tblr-danger)' : 'var(--tblr-secondary)'; + return `
${escapeHtml(name)} - ${lineParts.join(' · ')}${suffix ? ' · ' + escapeHtml(suffix) + '' : ''} + ${formatProviderBalance(name, p)}
`; }).join(''); } - let totalRow = ''; - if (totalBalance != null && typeof totalBalance === 'number') { - totalRow = `
- Total - $${totalBalance.toFixed(2)} -
`; - } - el.innerHTML = `
${displayPct}%
@@ -725,15 +698,14 @@ function renderQuotaModal(quota) { Reset${quota.next_reset_date || 'not configured'}
${providers.length ? ` -
Per-provider balance & runway
+
Provider accounts
${providerRowsHtml} - ${totalRow} -
` : ''} +
` : '
No provider accounts configured
'}
${escapeHtml(quota.note || '')}
`; } -function formatBalanceStaleness(balanceAt, source) { +function formatBalanceStaleness(balanceAt) { if (!balanceAt) return ''; try { const then = new Date(balanceAt); @@ -741,17 +713,57 @@ function formatBalanceStaleness(balanceAt, source) { if (Number.isNaN(ms) || ms < 0) return ''; const minutes = Math.floor(ms / 60000); 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); - if (hours < 24) return `sampled ${hours}h ago`; + if (hours < 24) return `${hours}h old`; const days = Math.floor(hours / 24); - return `sampled ${days}d ago`; + return `${days}d old`; } catch (_) { 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) { return { '--chip-accent': 'var(--tblr-danger)', @@ -759,13 +771,6 @@ function _quotaChipAccent(low, hasKnownBurn) { 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 { '--chip-accent': 'var(--tblr-success)', }; @@ -781,58 +786,22 @@ function renderQuotaChip(quota) { return; } 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) { - const low = entries.some(p => !!p.runway_low_warning); - const hasKnownBurn = entries.some(p => p.burn_rate_usd_per_hour != null); - const worstProjEntry = entries - .filter(p => p.projected_hours_remaining != null) - .sort((a, b) => Number(a.projected_hours_remaining) - Number(b.projected_hours_remaining))[0]; - const hours = worstProjEntry ? Math.round(Number(worstProjEntry.projected_hours_remaining)) : null; - const chipStyle = _quotaChipAccent(low, hasKnownBurn); - const balanceStr = '$' + totalBalance.toFixed(2); - - text.textContent = hours != null ? balanceStr + ' · ~' + hours + 'h' : balanceStr; - - for (const [k, v] of Object.entries(chipStyle)) { - chip.style.setProperty(k, v); - } - if (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 ---------------------------------------- + // The chip always tells the kWh-subscription story; per-provider balances + // live in the modal where they can be labeled by billing shape. const pct = Math.min(quota.metered_fraction_of_plan * 100, 100); + const displayPct = (quota.metered_fraction_of_plan * 100).toFixed(1); fill.style.width = `${pct}%`; - text.textContent = `${(quota.metered_fraction_of_plan * 100).toFixed(1)}%`; + text.textContent = `${displayPct}% · ${quota.plan_kwh} kWh plan`; + text.setAttribute('title', 'Click for provider account detail'); + + const chipStyle = _quotaChipAccent(pct > 90); + for (const [k, v] of Object.entries(chipStyle)) { + chip.style.setProperty(k, v); + } + if (chipStyle['--chip-accent']) { + text.style.color = chipStyle['--chip-accent']; + } } function renderDecisions(decisions) { diff --git a/src/tui.py b/src/tui.py index 6338fdd..f906e1c 100644 --- a/src/tui.py +++ b/src/tui.py @@ -65,81 +65,52 @@ def _fmt_runway(hours: float) -> str: return f"~{hours / 24:.1f}d" -def _quota_lead_values(rows: dict) -> tuple[Optional[float], Optional[float], Optional[float], bool, Optional[str]]: - providers = sorted( - {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. - """ +def _format_small_balance(balance: Optional[float]) -> str: 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 "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")} + ) + ] + + # Compact bottom keys legend. The 11 BINDINGS are collapsed to these short # (key, label) pairs — duplicate quit bindings (q/Q/ctrl+c) show once. Rendered # as a single markup string so Textual's Static wraps rather than truncates. @@ -603,16 +574,6 @@ class DashboardApp(App): flex_default=None, window_start_30d=None, ) -> 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 frac_fmt = "n/a" if frac is None else frac 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)]" if reset_date: legend += ( - f" · [rgb(128,128,128)]period since {reset_date}" - f"[/rgb(128,128,128)]" + f" · [rgb(128,128,128)]period since {reset_date}[/rgb(128,128,128)]" ) if window_start_30d: legend += ( - f" · [rgb(128,128,128)]window start {window_start_30d}" - f"[/rgb(128,128,128)]" + f" · [rgb(128,128,128)]window start {window_start_30d}[/rgb(128,128,128)]" ) if next_reset_date: legend += ( @@ -639,6 +598,10 @@ class DashboardApp(App): ) 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: # Quota panel — guard against being called before compose() mounts # these widgets (set_interval in on_mount can fire _on_interval too @@ -655,20 +618,12 @@ class DashboardApp(App): frac = rows.get("fraction") 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) note = self.query_one("#quota-note", Static) if rows: - balance, burn, runway, low, note_text = _quota_lead_values(rows) - 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.display = False + lead.update(self._format_quota_kwh_lead(plan, metered, frac)) + note.update("") + note.display = False else: lead.update("") note.update("") @@ -682,12 +637,14 @@ class DashboardApp(App): window_start = rows.get("window_start_30d") flex_default = model.get("flex_default") next_reset_date = rows.get("next_reset_date") - legend.update( + legend_lines = [ self._format_quota_legend( plan, metered, frac, calls, reset_date, next_reset_date, flex_default, window_start, ) - ) + ] + legend_lines.extend(_provider_quota_rows(rows)) + legend.update("\n".join(legend_lines)) else: bar.display = False legend.update("quota not configured") diff --git a/src/tui_model.py b/src/tui_model.py index e682fe4..2d7a076 100644 --- a/src/tui_model.py +++ b/src/tui_model.py @@ -56,21 +56,19 @@ def build_model(data: dict) -> dict: {"label": "reset_date", "value": quota.get("reset_date")}, {"label": "next_reset_date", "value": quota.get("next_reset_date")}, {"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()): balance = entry.get("balance_usd") + source = entry.get("balance_source") or "" 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( - {"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( { diff --git a/tests/test_admin_frontend.py b/tests/test_admin_frontend.py index f78609a..cce40c8 100644 --- a/tests/test_admin_frontend.py +++ b/tests/test_admin_frontend.py @@ -175,13 +175,18 @@ def test_quota_modal_has_not_configured_fallback(): assert "not configured" in html -def test_admin_quota_chip_references_balance_and_runway(): - """The index page references per-provider balance / runway quota fields and the provider rows container.""" +def test_admin_quota_chip_references_kwh_plan_and_modal_lists_provider_accounts(): + """The chipshows kWh-plan fraction and the modal has labeled Provider accounts rows.""" index_path = ROOT / "admin" / "frontend" / "index.html" 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 "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): diff --git a/tests/test_tui.py b/tests/test_tui.py index 609a4f5..7ac6b77 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -239,14 +239,15 @@ def test_build_model_quota_panel_includes_reset_date(): assert by_label["window_start_30d"] == "2026-07-24" -def test_build_model_quota_panel_includes_total_balance_and_provider_rows(): - """The new per-provider quota shape must reach the TUI data model.""" +def test_build_model_quota_panel_includes_provider_rows(): + """The per-provider quota shape reaches the TUI data model.""" m = build_model(_fixture()) rows = m["quota"] by_label = {r["label"]: r["value"] for r in rows} - assert by_label["total_balance_usd"] == 8.50 - assert by_label["neuralwatt balance_usd"] == "8.5 @ 2026-08-23T09:58:00+00:00" + assert "total_balance_usd" not in by_label + assert by_label["neuralwatt balance_usd"] == 8.5 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 projected_hours_remaining"] == 8.5 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(): - """All four lead states, including the one this deployment is actually in. - - burn/runway are legitimately None when the sample segment is too short - (PR #30). That is deliberate, not missing data, so the None path must - still say something an operator can read. - """ - 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_format_small_balance_renders_tiny_and_normal_values(): + assert tui._format_small_balance(None) == "n/a" + assert tui._format_small_balance(0.0) == "$0.00" + assert tui._format_small_balance(0.0001) == "<$0.01" + assert tui._format_small_balance(-0.004) == "~$0.00 (slight overage)" + assert tui._format_small_balance(8.50) == "$8.50" + assert tui._format_small_balance(-1.0) == "$-1.00" -def test_quota_lead_values_binds_only_to_polled_providers(): - rows = { - "openrouter balance_usd": "50.0 @ 2026-08-23T10:00:00+00:00", +def test_format_provider_quota_row_labels_billing_shape(): + polled = { + "openrouter balance_usd": 4.999, "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 runway_low_warning": False, - "openrouter runway_note": None, - "neuralwatt balance_usd": "-0.004 @ 2026-08-23T10:00:00+00:00", + } + row = tui._format_provider_quota_row("openrouter", polled) + 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 burn_rate_usd_per_hour": 1.0, - "neuralwatt projected_hours_remaining": -0.004, - "neuralwatt runway_low_warning": True, - "neuralwatt runway_note": "allowance reads negative", + "neuralwatt balance_at": "2026-08-23T09:58:00+00:00", } - balance, burn, runway, low, note = tui._quota_lead_values(rows) - assert balance == 50.0 - assert burn == 5.0 - assert runway == 10.0 - assert low is False - assert note is None + row = tui._format_provider_quota_row("neuralwatt", telemetry) + assert "neuralwatt: ~$0.00 (slight overage) overage allowance" in row - -def test_quota_lead_values_uses_lowest_runway_among_polled(): - rows = { - "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", + unconfigured = { + "provider-a balance_usd": None, + "provider-a balance_source": "unconfigured", } - balance, burn, runway, low, note = tui._quota_lead_values(rows) - assert balance == 20.0 - assert runway == 3.0 - assert low is True - assert note == "low" + row = tui._format_provider_quota_row("provider-a", unconfigured) + assert "provider-a: n/a account balance not configured" in row -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 = { - "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_at": None, "openrouter burn_rate_usd_per_hour": None, "openrouter projected_hours_remaining": None, "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, - "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) - assert balance == 50.0 - assert runway == 5.0 + out = tui._provider_quota_rows(rows) + assert len(out) == 2 + 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(): - """When burn is None the note renders VERBATIM — never blank, 0, or None. - - This is the state the live deployment is in right now, so it is the first - thing an operator sees, not an edge case. - """ +def test_quota_panel_lead_is_kwh_only_and_note_empty(): + """The quota lead is the kWh-plan fraction; the note line stays hidden.""" stub = _StubFetcher() 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 app = tui.DashboardApp(fetcher=stub) def _assert(a): lead_text = str(a.query_one("#quota-lead", Static).content) - note_text = str(a.query_one("#quota-note", Static).content) - assert "balance $7.50 left" in lead_text - assert "burn n/a" in lead_text + assert "20% of 6.25 kWh plan" 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 _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