@xuda.io/account_module 1.2.2296 → 1.2.2297

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -1999,10 +1999,10 @@ export const ops_account_snapshot = async function (req = {}) {
1999
1999
  const a = ar.data;
2000
2000
  let credits = { max: 0, used: 0, remaining: 0 };
2001
2001
  try {
2002
- const cr = await get_account_ai_usage({ uid: account_uid });
2002
+ const cr = await get_account_ai_usage({ uid: account_uid, ..._LIFETIME_WINDOW });
2003
2003
  if (cr && cr.code === 55) {
2004
2004
  const max = Math.round((cr.data?.credits?.total || 0) * 100) / 100;
2005
- const used = Math.round((cr.data?.usage?.total || 0) * 100) / 100;
2005
+ const used = Math.round(_usage_total(cr.data?.usage) * 100) / 100;
2006
2006
  credits = { max, used, remaining: Math.round((max - used) * 100) / 100, by_source: cr.data?.credits || {} };
2007
2007
  }
2008
2008
  } catch (e) { console.error('[ops snapshot credits]', e.message); }
@@ -6471,11 +6471,13 @@ export const save_cache_hit = async function (_id) {
6471
6471
  ///////////////////////////////
6472
6472
 
6473
6473
  export const get_account_ai_usage = async function (req, job_id, headers) {
6474
- const d = new Date();
6475
- const curr_month = Number(String(d.getMonth() + 1).padStart(2, '0')); // months are 0-based
6476
- const curr_year = d.getFullYear();
6477
-
6478
- const { uid, year_from = curr_year, month_from = curr_month, day_from = 0, year_to = curr_year, month_to = curr_month, day_to = 99 } = req;
6474
+ // Default to the LIFETIME window (all-time spend) — the credit model is a
6475
+ // remaining-pool ledger (see _LIFETIME_WINDOW + the Credit Management page).
6476
+ // Callers that want a specific range (the usage chart in WorkspaceUsage.vue)
6477
+ // pass explicit year/month/day. The nav meter + ring call with no window, so
6478
+ // this makes them read lifetime "used" and match the Credit Management page
6479
+ // instead of a current-month slice against a lifetime pool.
6480
+ const { uid, year_from = 2000, month_from = 1, day_from = 1, year_to = 2999, month_to = 12, day_to = 31 } = req;
6479
6481
  try {
6480
6482
  const app_id = await get_account_default_project_id(uid);
6481
6483
 
@@ -6499,8 +6501,13 @@ export const get_account_ai_usage = async function (req, job_id, headers) {
6499
6501
 
6500
6502
  // console.log('ai_usage', ai_usage.rows);
6501
6503
 
6502
- let total_usage = response.total_usage;
6503
6504
  let profile = response.profiles;
6505
+ // The ai_usage2 list function emits per-profile spend but sometimes a null
6506
+ // rolled-up total. Roll it up HERE at the source so every consumer gets a real
6507
+ // usage.total — including the dashboard nav (shared-store fetchCredits reads
6508
+ // data.usage.total directly). Without this, Number(null) -> 0 hid all spend in
6509
+ // the meter/ring and disabled the account-wide credit cap.
6510
+ let total_usage = _usage_total({ total: response.total_usage, profile });
6504
6511
 
6505
6512
  const ai_credits = await db_module.get_couch_view('xuda_billing', 'ai_credits', {
6506
6513
  startkey: [uid, ''],
@@ -6610,6 +6617,23 @@ const _fold_usage = function (profile_map) {
6610
6617
  return { by_profile, by_user };
6611
6618
  };
6612
6619
 
6620
+ // Single source of truth for an account's rolled-up AI-credit usage. The usage
6621
+ // view reliably emits per-profile spend but sometimes returns a null rolled-up
6622
+ // total (usage.total); Number(null)||0 then collapsed "used" to 0 EVERYWHERE —
6623
+ // hiding spend in the nav/meter AND making the account-wide credit cap never
6624
+ // fire. Prefer the view's own total when finite, otherwise sum the per-profile
6625
+ // fold (same source, same credit unit).
6626
+ const _usage_total = function (usage_data) {
6627
+ // NOTE: Number(null) === 0 (and IS finite), which is exactly what collapsed
6628
+ // "used" to 0 — so guard on the RAW type, not the coerced number. Only trust
6629
+ // the view's own total when it is a real finite number; otherwise roll up the
6630
+ // per-profile fold.
6631
+ const t = usage_data?.total;
6632
+ if (typeof t === 'number' && Number.isFinite(t)) return t;
6633
+ const { by_profile } = _fold_usage(usage_data?.profile);
6634
+ return Object.values(by_profile).reduce((a, b) => a + (Number(b) || 0), 0);
6635
+ };
6636
+
6613
6637
  // Wide window so "used" reads as all-time spend (the lifetime / remaining-pool model).
6614
6638
  const _LIFETIME_WINDOW = { year_from: 2000, month_from: 1, day_from: 1, year_to: 2999, month_to: 12, day_to: 31 };
6615
6639
 
@@ -6625,8 +6649,8 @@ const _get_scoped_usage = async function (owner_uid, need_raw) {
6625
6649
  if (cached && now - cached.ts < _SCOPED_USAGE_TTL && (!need_raw || cached.has_raw)) return cached.data;
6626
6650
 
6627
6651
  const usage_ret = await get_account_ai_usage({ uid: owner_uid, ..._LIFETIME_WINDOW });
6628
- const total = Number(usage_ret?.data?.usage?.total) || 0;
6629
6652
  const { by_profile, by_user } = _fold_usage(usage_ret?.data?.usage?.profile);
6653
+ const total = _usage_total(usage_ret?.data?.usage);
6630
6654
 
6631
6655
  let by_model = {};
6632
6656
  let by_source = {};
@@ -6757,8 +6781,8 @@ export const evaluate_credit_gate = async function (req) {
6757
6781
 
6758
6782
  // Back-compat: no rules or disabled => legacy single ledger cap, byte-for-byte.
6759
6783
  if (!rules || rules.enabled === false) {
6760
- const usage = await get_account_ai_usage({ uid: owner });
6761
- const over = (Number(usage?.data?.credits?.total) || 0) - (Number(usage?.data?.usage?.total) || 0) < -0.5;
6784
+ const usage = await get_account_ai_usage({ uid: owner, ..._LIFETIME_WINDOW });
6785
+ const over = (Number(usage?.data?.credits?.total) || 0) - _usage_total(usage?.data?.usage) < -0.5;
6762
6786
  return { block: over, scope: over ? 'pool' : null, reason: over ? _SCOPE_REASON.ledger : '', hard_breached: over ? [{ scope: 'pool', key: 'ledger' }] : [], soft_breached: [], account_id: owner };
6763
6787
  }
6764
6788
 
@@ -7105,10 +7129,13 @@ export const broadcast_credits = function (uid) {
7105
7129
  _credits_broadcast_timers[uid] = setTimeout(async () => {
7106
7130
  delete _credits_broadcast_timers[uid];
7107
7131
  try {
7108
- const ret = await get_account_ai_usage({ uid });
7132
+ // Lifetime window (matches the Credit Management meter) + the same null-total
7133
+ // rollup as _get_scoped_usage — get_account_ai_usage can return a null
7134
+ // rolled-up usage.total, which otherwise broadcasts used:0 to the live nav.
7135
+ const ret = await get_account_ai_usage({ uid, ..._LIFETIME_WINDOW });
7109
7136
  if (!ret || ret.code !== 55) return;
7110
7137
  const max = Math.round((ret.data?.credits?.total || 0) * 100) / 100;
7111
- const used = Math.round((ret.data?.usage?.total || 0) * 100) / 100;
7138
+ const used = Math.round(_usage_total(ret.data?.usage) * 100) / 100;
7112
7139
  ws_dashboard_msa.emit_message_to_dashboard({
7113
7140
  service: 'credits_update',
7114
7141
  to: [uid],
@@ -7907,7 +7934,12 @@ export const newsletter_generate = async function (req = {}) {
7907
7934
  if (!req.system && !_ops_is_super(req)) return { code: -403, data: 'superuser only' };
7908
7935
  // Master-only (dev exempt for testing): the issue DB must never be created
7909
7936
  // on a regional couch, and the weekly coupon/sample flow belongs to master.
7910
- if (!_conf.is_debug && process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_generate runs on master only' };
7937
+ // MASTER ONLY, dev included. Dev is is_debug but still sends real [SAMPLE]/
7938
+ // [OPS] mail and keeps its own issue counter, so a "Generate now" on dev
7939
+ // blasts live info@ inboxes and creates dev-only drafts that never appear in
7940
+ // the master Newsletter Manager (dev does not replicate to master). Preview /
7941
+ // Test still work on dev for reviewing render; only issuing is locked here.
7942
+ if (process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_generate runs on master only' };
7911
7943
  await _nl_ensure_storage();
7912
7944
 
7913
7945
  // Two DISTINCT tips: one for "Did you know?", one for "Tip of the week".
@@ -8068,7 +8100,9 @@ export const newsletter_publish = async function (req = {}) {
8068
8100
  if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
8069
8101
  // Same master-only gate as generate: the blast must run where xuda_newsletter
8070
8102
  // and the authoritative xuda_accounts live (dev exempt for testing).
8071
- if (!_conf.is_debug && process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_publish runs on master only' };
8103
+ // MASTER ONLY, dev included: a publish on dev would blast dev's opted-in
8104
+ // accounts with real mail. Locked so only the real master issues a newsletter.
8105
+ if (process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_publish runs on master only' };
8072
8106
  const id = req.id || `nl_issue_${req.issue_number}`;
8073
8107
  const gret = await db_module.get_couch_doc(NEWSLETTER_DB, id);
8074
8108
  if (gret.code < 0 || !gret.data) return { code: -1, data: 'issue not found' };