@xuda.io/account_module 1.2.2287 → 1.2.2289

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.
Files changed (2) hide show
  1. package/index.mjs +85 -17
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1990,11 +1990,29 @@ const _srv_role = (doc) => {
1990
1990
  return 'other';
1991
1991
  };
1992
1992
 
1993
+ // xuda_servers is authoritative on the dev orchestrator and replicates dev->master
1994
+ // ONE-WAY (db_module.get_live_servers_couch): nothing replicates back INTO dev, so
1995
+ // a stat:1 enqueue written on any other node's local couch strands and the dev
1996
+ // rollout tick never sees it. So the ops server methods (list / status / deploy)
1997
+ // must read+write the ORCHESTRATOR's couch, not the local region replica. On dev
1998
+ // itself this is the local connection (unchanged); from any other node we reach
1999
+ // dev's couch over the WG mesh, reusing THIS node's couch admin credential (shared
2000
+ // fleet-wide). Endpoint comes from _conf.deploy_orchestrator; absent -> fall back
2001
+ // to local (no worse than before, and never throws on nodes not yet reconfigured).
2002
+ const _orchestrator_servers_couch = () => {
2003
+ const orch = _conf.deploy_orchestrator || {};
2004
+ const host = process.env.XUDA_HOSTNAME;
2005
+ if (!orch.couch_location || host === orch.hostname) return db_module.get_con('xuda_servers');
2006
+ const local = (_conf.couchdb_control_database && _conf.couchdb_control_database[host]) || {};
2007
+ const url = `${local.db_server_http || 'http'}://${local.db_server_username || 'admin'}:${local.db_server_password || ''}@${orch.couch_location}/`;
2008
+ return db_module.get_con('xuda_servers', url);
2009
+ };
2010
+
1993
2011
  // Load every xuda_servers doc via _all_docs (nano .list) rather than a
1994
2012
  // match-all Mango selector — avoids the "no matching index" warning that would
1995
2013
  // otherwise spam the log on every (frequently-polled) status call.
1996
2014
  const _load_all_server_docs = async () => {
1997
- const couch = await db_module.get_con('xuda_servers');
2015
+ const couch = _orchestrator_servers_couch();
1998
2016
  const res = await couch.list({ include_docs: true });
1999
2017
  return (res.rows || []).map((r) => r.doc).filter((d) => d && !String(d._id).startsWith('_design'));
2000
2018
  };
@@ -2063,12 +2081,26 @@ export const ops_deploy_server = async function (req = {}) {
2063
2081
  if (!server_id) return { code: -1, data: 'server_id is required' };
2064
2082
  if (server_id === 'dev') return { code: -1, data: 'the dev orchestrator deploys via rsync, not this rollout; pick a region (master/us/eu)' };
2065
2083
 
2066
- // FULL doc (no field projection) so it is safe to save back.
2067
- const sr = await db_module.get_couch_doc('xuda_servers', server_id);
2068
- if (sr.code < 0 || !sr.data) return { code: -1, data: `server "${server_id}" not found` };
2069
- const doc = sr.data;
2084
+ // Read + write the ORCHESTRATOR's authoritative couch (dev), NOT the local
2085
+ // region replica: a stat:1 written to a region's copy never reaches the dev
2086
+ // rollout tick (nothing replicates back into dev), so it would strand and the
2087
+ // deploy would silently no-op. Raw nano .get/.insert (full doc, safe to save
2088
+ // back) mirrors _publish_rollout's own access pattern.
2089
+ const couch = _orchestrator_servers_couch();
2090
+ let doc;
2091
+ try {
2092
+ doc = await couch.get(server_id);
2093
+ } catch (e) {
2094
+ return { code: -1, data: e.statusCode === 404 ? `server "${server_id}" not found` : `orchestrator couch read failed: ${e.message}` };
2095
+ }
2070
2096
  if (!_srv_is_deployable(doc)) return { code: -1, data: `server "${server_id}" is not a deployable region app-server (needs docType 'regional_server' + application_server)` };
2071
2097
 
2098
+ // Preview (dry_run) writes nothing and needs no confirmation, so it must run
2099
+ // BEFORE the confirmation and busy gates: "Preview plan" sends dry_run:true
2100
+ // with no confirm, and should also work while a deploy is already in flight.
2101
+ const plan = { server_id, mode, current_state: _srv_deploy_state(doc), busy: _srv_busy(doc), private_ip: doc.private || null };
2102
+ if (req.dry_run) return { code: 1, data: { dry_run: true, would_deploy: plan } };
2103
+
2072
2104
  // Confirmation gate — proportional to blast radius.
2073
2105
  if (mode === 'hard') {
2074
2106
  if (req.confirm !== server_id) return { code: -1, data: `hard deploy tears down and reinstalls "${server_id}" (brief downtime). Re-send with confirm:"${server_id}" to proceed.` };
@@ -2080,18 +2112,18 @@ export const ops_deploy_server = async function (req = {}) {
2080
2112
  // Already-in-flight guard.
2081
2113
  if (_srv_busy(doc)) return { code: -1, data: `a deploy is already ${_srv_deploy_state(doc)} for "${server_id}"; wait for it to finish (poll ops_server_deploy_status).` };
2082
2114
 
2083
- const plan = { server_id, mode, current_state: _srv_deploy_state(doc), private_ip: doc.private || null };
2084
- if (req.dry_run) return { code: 1, data: { dry_run: true, would_deploy: plan } };
2085
-
2086
- // Enqueue: the dev orchestrator's 1-min publish tick picks stat:1 up.
2115
+ // Enqueue on the orchestrator's couch: the dev 1-min publish tick picks stat:1 up.
2087
2116
  doc.stat = 1;
2088
2117
  doc.stat_ts = Date.now();
2089
2118
  doc.publish_mode = mode;
2090
2119
  doc.deploy_requested_by = req.uid;
2091
2120
  doc.deploy_requested_ts = Date.now();
2092
2121
  delete doc.publish_error;
2093
- const wr = await db_module.save_couch_doc('xuda_servers', doc);
2094
- if (wr.code < 0) return { code: -1, data: wr.data };
2122
+ try {
2123
+ await couch.insert(doc);
2124
+ } catch (e) {
2125
+ return { code: -1, data: `enqueue write to orchestrator couch failed: ${e.message}` };
2126
+ }
2095
2127
 
2096
2128
  _ops_notify_ops(`Server deploy queued (${mode}): ${server_id}`, [['Server', server_id], ['Mode', mode], ['By', req.uid], ['State', 'queued'], ['Orchestrator', 'dev.xuda.ai']], 'OPS-DEPLOY-' + server_id);
2097
2129
  logs_msa.add_account_log({ uid: req.uid, method: 'ops_deploy_server', source: 'ops dashboard', response_status_code: 200, response_data: `queued ${mode} deploy for ${server_id} by ${req.uid}` });
@@ -2120,9 +2152,14 @@ export const ops_server_deploy_status = async function (req = {}) {
2120
2152
  });
2121
2153
 
2122
2154
  if (req.server_id) {
2123
- const sr = await db_module.get_couch_doc('xuda_servers', String(req.server_id).trim());
2124
- if (sr.code < 0 || !sr.data) return { code: -1, data: `server "${req.server_id}" not found` };
2125
- return { code: 1, data: to_status(sr.data) };
2155
+ // Read the orchestrator's authoritative copy so progress reflects the dev
2156
+ // rollout's stat writes (the region replica can lag / never see enqueues).
2157
+ try {
2158
+ const doc = await _orchestrator_servers_couch().get(String(req.server_id).trim());
2159
+ return { code: 1, data: to_status(doc) };
2160
+ } catch (e) {
2161
+ return { code: -1, data: e.statusCode === 404 ? `server "${req.server_id}" not found` : `orchestrator couch read failed: ${e.message}` };
2162
+ }
2126
2163
  }
2127
2164
  const docs = await _load_all_server_docs();
2128
2165
  const rows = docs.filter(_srv_is_deployable).map(to_status);
@@ -6382,6 +6419,37 @@ const _nl_ensure_storage = async () => {
6382
6419
  if (save.code < 0) console.error('[newsletter] replicator ensure failed:', save.data);
6383
6420
  };
6384
6421
 
6422
+ // The admin panel is served from whatever region CF routes the superuser to, but
6423
+ // xuda_newsletter lives on master + master2 only. So the read-side methods must
6424
+ // resolve MASTER's couch when running on a region (reachable over the WG hub at
6425
+ // 10.8.0.1), and read the local couch on master itself and on dev (standalone,
6426
+ // is_debug). Returns a DSN for the _custom read helpers, or null = local couch.
6427
+ const _nl_read_dsn = () => {
6428
+ if (_conf.is_debug) return null;
6429
+ if (process.env.XUDA_HOSTNAME === 'master.xuda.ai') return null;
6430
+ const m = _conf.couchdb_control_database?.['master.xuda.ai'];
6431
+ if (!m?.db_server_location) return null;
6432
+ return `${m.db_server_http}://${m.db_server_username}:${m.db_server_password}@${m.db_server_location}`;
6433
+ };
6434
+
6435
+ // Mango query against xuda_newsletter wherever it actually lives (master from a
6436
+ // region, local on master/dev). Same {docs} shape either way.
6437
+ const _nl_find = async (selector, extra = {}) => {
6438
+ const dsn = _nl_read_dsn();
6439
+ if (dsn) return await db_module.find_couch_query_custom(NEWSLETTER_DB, dsn, { selector, ...extra });
6440
+ return await db_module.find_couch_query(NEWSLETTER_DB, { selector, ...extra }, true);
6441
+ };
6442
+
6443
+ // Fetch one issue by _id from the same source. Returns {code, data} like
6444
+ // get_couch_doc so callers stay unchanged.
6445
+ const _nl_get = async (id) => {
6446
+ const dsn = _nl_read_dsn();
6447
+ if (!dsn) return await db_module.get_couch_doc(NEWSLETTER_DB, id);
6448
+ const ret = await db_module.find_couch_query_custom(NEWSLETTER_DB, dsn, { selector: { _id: id }, limit: 1 });
6449
+ const doc = (ret.docs || [])[0];
6450
+ return doc ? { code: 1, data: doc } : { code: -1, data: 'issue not found' };
6451
+ };
6452
+
6385
6453
  // Next issue number = max(existing) + 1. Read-only projection (never PUT these).
6386
6454
  const _nl_next_number = async () => {
6387
6455
  try {
@@ -6811,7 +6879,7 @@ export const newsletter_generate = async function (req = {}) {
6811
6879
  export const newsletter_list_issues = async function (req = {}) {
6812
6880
  try {
6813
6881
  if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
6814
- const ret = await db_module.find_couch_query(NEWSLETTER_DB, { selector: { docType: 'newsletter_issue' }, limit: 500 }, true);
6882
+ const ret = await _nl_find({ docType: 'newsletter_issue' }, { limit: 500 });
6815
6883
  const rows = (ret.docs || [])
6816
6884
  .sort((a, b) => (b.issue_number || 0) - (a.issue_number || 0))
6817
6885
  .map((d) => ({ id: d._id, issue_number: d.issue_number, status: d.status, subject: d.subject, created_ts: d.created_ts, published_ts: d.published_ts || null, coupon: d.content && d.content.coupon ? d.content.coupon.promo_code : null, sent: d.sent || { count: 0, failed: 0 } }));
@@ -6825,7 +6893,7 @@ export const newsletter_preview = async function (req = {}) {
6825
6893
  try {
6826
6894
  if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
6827
6895
  const id = req.id || `nl_issue_${req.issue_number}`;
6828
- const ret = await db_module.get_couch_doc(NEWSLETTER_DB, id);
6896
+ const ret = await _nl_get(id);
6829
6897
  if (ret.code < 0 || !ret.data) return { code: -1, data: 'issue not found' };
6830
6898
  const host = _ops_host();
6831
6899
  const [fpool, ppool] = await Promise.all([_nl_fashion_pool(), _nl_partner_pool()]);
@@ -6844,7 +6912,7 @@ export const newsletter_send_sample = async function (req = {}) {
6844
6912
  try {
6845
6913
  if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
6846
6914
  const id = req.id || `nl_issue_${req.issue_number}`;
6847
- const ret = await db_module.get_couch_doc(NEWSLETTER_DB, id);
6915
+ const ret = await _nl_get(id);
6848
6916
  if (ret.code < 0 || !ret.data) return { code: -1, data: 'issue not found' };
6849
6917
  const to = req.email || 'info@xuda.ai';
6850
6918
  const sr = await _newsletter_send_sample_internal(ret.data, to, req.theme);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2287",
3
+ "version": "1.2.2289",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {