@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.
- package/index.mjs +85 -17
- 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 =
|
|
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
|
-
//
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2094
|
-
|
|
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
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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
|
|
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
|
|
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
|
|
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);
|