aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-billing.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-06 11:45:21 +0100
committerAgando <roduccomm@gmail.com>2026-11-06 11:45:21 +0100
commit03143a6e88832d06c7b08cdbf8de96ba79bda7d6 (patch)
treea82db0d323e36aec94587332b263e2be9fd062c3 /modules/core/module-billing.js
parent15b42d6379484bb3f439eb5d7f77f12877f74497 (diff)
release(v4.6.150): commit the module work that has been live but uncommitted
Operator commit-approve, 2026-11-06. These changes have been served since 2026-11-02/03 while git HEAD still stamped v4.6.146, so the mirror published a tree four builds behind the running instance and the release tag named the wrong build. module-orchestrator.js — orchestrate-parallel reported success it could not prove. Results are now recorded by sub-task id through a resultSink handed to delegateTask (harvest matched by currentTask/lastTask strings, which a later delegation on the same agent overwrote, losing finished work as "agent not found"); the tally counts summaries with status ok instead of Promise.allSettled resolutions (a spawn refusal fulfils with undefined and used to print green); and a busy agent is no longer reused at pool cap, where two siblings erased each other's lease. Guard: modules/test/test-orchestrator-parallel-harvest.js (16 cases, reproduces the one-agent three-sub-task cap case that printed 3/3 succeeded). module-billing.js — the balance said "not signed in — newOS4 sends no session token", which was already false: the browser sends the HttpOnly cookie and parse_bearer_token() falls back to it. `cost` is on by default (the flag was opt-in and nobody knew the command existed; `cost off` opts out) and the command now prints today's platform spend from GET /api/billing/usage?days=1 alongside the session estimate. modules/ext/webdav.js — the inline WebView detector missed "Version/4.0" Android WebViews, including the operator's own phone, so the login offered a Telegram widget that cannot complete there. It now delegates to modules/wx11-tg-webview.js when the page loaded it and keeps a matching fallback (wv token, Version/4.0-Android, Telegram, FBAN/Instagram/Line). modules/ext/media.js — text analysis model renamed deepseek-v4-flash -> deepseek-v4.1-flash in both the field and the printed label. main.html + module-loader.js — v4.6.146 -> v4.6.150 in LATEST_VER and the three asset queries; core-bundle.js rebuilt (rebuild-bundles.py reports it already in sync, 1,250,569 bytes). Verified: node --check on all six scripts and the new test; the 16-case orchestrator guard passes against the deployed bundle (ORCH_SRC=...); bundle rebuild is a no-op; core-bundle.js?v=v4.6.150 answers 200.
Diffstat (limited to 'modules/core/module-billing.js')
-rw-r--r--modules/core/module-billing.js33
1 files changed, 28 insertions, 5 deletions
diff --git a/modules/core/module-billing.js b/modules/core/module-billing.js
index 87f8dc3..4ccf196 100644
--- a/modules/core/module-billing.js
+++ b/modules/core/module-billing.js
@@ -1,8 +1,10 @@
// Billing / pricing surface for newOS4 (2026-10-28)
//
// Why: the terminal could show a model's name but never what it costs. Prices and the anonymous
-// message allowance are public data, so they need no login — the account BALANCE does, and newOS4
-// has no session layer, so that is reported honestly as "not signed in" rather than guessed.
+// message allowance are public data, so they need no login — the account BALANCE does.
+// 2026-11-03: newOS4 DOES have a session — the browser sends the HttpOnly cookie and
+// parse_bearer_token() reads it — so the balance below is real when logged in. The old comment
+// claimed otherwise and made the 401 look expected.
//
// Commands: cost [on|off], usage
class BillingModule {
@@ -56,7 +58,9 @@ class BillingModule {
}
async _loadBalance() {
- // Only answers when a session exists; newOS4 currently has none, so this is expected to 401.
+ // Cookie auth is what makes this work: the browser sends webx11_session_token and
+ // parse_bearer_token() falls back to it. If this 401s, the user is logged out — say so,
+ // do not assume newOS4 cannot authenticate (that comment was wrong until 2026-11-03).
try {
const r = await fetch('/api/billing/subscription', { credentials: 'include' });
if (!r.ok) return null;
@@ -67,6 +71,16 @@ class BillingModule {
}
}
+ async _loadUsage(days) {
+ try {
+ const r = await fetch('/api/billing/usage?days=' + (days || 1), { credentials: 'include' });
+ if (!r.ok) return null;
+ return await r.json();
+ } catch (e) {
+ return null;
+ }
+ }
+
// --- session spend, measured where the tokens actually are ----------------
_hookUsage() {
const om = this.terminal && this.terminal.ollamaManager;
@@ -109,8 +123,10 @@ class BillingModule {
}
}
+ // On by default since 2026-11-03: "the terminal shows tokens but not cost" was the default,
+ // not a bug — the flag was opt-in and nobody knew the command existed. `cost off` opts out.
_costLineOn() {
- try { return localStorage.getItem('webx11_cost_line') === '1'; } catch (e) { return false; }
+ try { return localStorage.getItem('webx11_cost_line') !== '0'; } catch (e) { return true; }
}
// --- the command ----------------------------------------------------------
@@ -138,13 +154,20 @@ class BillingModule {
p('📊 Free allowance: unavailable (billing API did not answer)', 'warning');
}
- p(`💰 Balance: ${bal && bal.balance_usd != null ? '$' + Number(bal.balance_usd).toFixed(2) : 'not signed in — newOS4 sends no session token, so a balance cannot be shown here yet'}`, 'info');
+ p(`💰 Balance: ${bal && bal.balance_usd != null ? '$' + Number(bal.balance_usd).toFixed(2) : 'not signed in — log in at /spaces/dashboard.html to see your balance'}`, 'info');
if (bal && bal.plan_allowance) {
const pa = bal.plan_allowance;
p(`🎁 Included usage (${pa.plan}): $${Number(pa.used_usd).toFixed(2)} of $${Number(pa.limit_usd).toFixed(2)} ${pa.period === 'day' ? 'today' : 'this month'} — $${Number(pa.remaining_usd).toFixed(2)} left, then your balance pays`, 'info');
}
+ const u = await this._loadUsage(1);
+ if (u && u.totals) {
+ p(`📅 Today (all newOS4 sessions): ${u.totals.requests} call(s), ` +
+ `${((u.totals.prompt_tokens + u.totals.completion_tokens) / 1000).toFixed(1)}k tokens, ` +
+ `≈ $${Number(u.totals.retail_usd).toFixed(2)} retail`, 'info');
+ }
+
if (s && s.msgs) {
p(`🧮 This session: ${s.msgs} call(s), ${(s.prompt / 1000).toFixed(1)}k in / ${(s.completion / 1000).toFixed(1)}k out ≈ $${s.cost.toFixed(4)} retail`, 'info');
Object.entries(s.byModel)