@xuda.io/account_module 1.2.2285 → 1.2.2286

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
@@ -1957,6 +1957,177 @@ export const ops_send_notification = async function (req = {}) {
1957
1957
  } catch (err) { return { code: -1, data: err.message }; }
1958
1958
  };
1959
1959
 
1960
+ // ---------------------------------------------------------------------------
1961
+ // Fleet server admin (superuser) — list the platform's own servers, queue a
1962
+ // soft/hard code deploy, and poll deploy status/progress. These are thin, SAFE
1963
+ // wrappers over the existing, battle-tested publish engine in misc_module
1964
+ // (_publish_rollout / _reinstall_server_detached): a deploy here only sets
1965
+ // stat:1 + publish_mode on the target's xuda_servers doc — the dev
1966
+ // orchestrator's 1-min tick (controller_module -> misc.publish_to_servers)
1967
+ // picks it up and runs the drain -> reinstall -> undrain rollout, writing
1968
+ // progress back onto the SAME doc (stat 1->2->3/4, stat_ts, publish_error). So
1969
+ // the blast radius of THIS module is a single CouchDB field write; the vetted
1970
+ // engine does the actual work. Deploy targets are region app-servers only
1971
+ // (docType 'regional_server' + application_server); farm/proxmox nodes and the
1972
+ // dev orchestrator itself are never deployable here.
1973
+ // ---------------------------------------------------------------------------
1974
+
1975
+ // stat state-machine on an xuda_servers doc -> friendly deploy state.
1976
+ const _SRV_DEPLOY_STATE = { 1: 'queued', 2: 'deploying', 3: 'idle', 4: 'failed' };
1977
+ const _srv_deploy_state = (doc) => _SRV_DEPLOY_STATE[doc && doc.stat] || 'unknown';
1978
+ const _srv_busy = (doc) => !!doc && (doc.stat === 1 || doc.stat === 2);
1979
+ // A deploy target is a region control-plane app-server (runs the /var/xuda
1980
+ // stack). NOT farm/proxmox nodes (no stack) and NOT dev (the orchestrator
1981
+ // itself — it deploys via rsync, and can't reinstall itself mid-rollout).
1982
+ const _srv_is_deployable = (doc) => !!(doc && doc.docType === 'regional_server' && doc.application_server === true && doc._id !== 'dev');
1983
+ const _srv_role = (doc) => {
1984
+ if (!doc) return 'other';
1985
+ if (doc.docType === 'regional_server' && doc.application_server) return 'region';
1986
+ if (doc.provider === 'proxmox' && doc.vmid) return 'farm_vm';
1987
+ if (doc.provider === 'proxmox' && (doc.node || doc.api_url)) return 'farm_node';
1988
+ return 'other';
1989
+ };
1990
+
1991
+ // Load every xuda_servers doc via _all_docs (nano .list) rather than a
1992
+ // match-all Mango selector — avoids the "no matching index" warning that would
1993
+ // otherwise spam the log on every (frequently-polled) status call.
1994
+ const _load_all_server_docs = async () => {
1995
+ const couch = await db_module.get_con('xuda_servers');
1996
+ const res = await couch.list({ include_docs: true });
1997
+ return (res.rows || []).map((r) => r.doc).filter((d) => d && !String(d._id).startsWith('_design'));
1998
+ };
1999
+
2000
+ // Normalize one xuda_servers doc into the admin list view.
2001
+ const _srv_view = (doc) => {
2002
+ const info = doc.server_info && typeof doc.server_info === 'object' ? doc.server_info : {};
2003
+ return {
2004
+ id: doc._id,
2005
+ role: _srv_role(doc),
2006
+ deployable: _srv_is_deployable(doc),
2007
+ provider: doc.provider || (doc.droplet_id ? 'digitalocean' : doc.node || doc.api_url ? 'proxmox' : 'unknown'),
2008
+ hostname: doc.hostname || info.public_hostname || null,
2009
+ public_ip: doc.public || info.public_ip || null,
2010
+ private_ip: doc.private || info.private_ip || null, // WireGuard mesh IP (10.8.0.x)
2011
+ server_status: doc.server_status || info.status || 'unknown', // active | off
2012
+ server_is_full: !!doc.server_is_full,
2013
+ capacity: doc.server_capacity || null,
2014
+ countries: doc.countries || null,
2015
+ deploy: {
2016
+ state: _srv_deploy_state(doc),
2017
+ busy: _srv_busy(doc),
2018
+ publish_mode: doc.publish_mode || null,
2019
+ stat: typeof doc.stat === 'number' ? doc.stat : null,
2020
+ stat_ts: doc.stat_ts || null,
2021
+ error: doc.publish_error || null,
2022
+ requested_by: doc.deploy_requested_by || null,
2023
+ requested_ts: doc.deploy_requested_ts || null,
2024
+ },
2025
+ };
2026
+ };
2027
+
2028
+ // List every fleet server (regions + farm nodes) with live status + deploy
2029
+ // state. Read-only. Live status is maintained by controller_module.check_live_servers.
2030
+ export const ops_list_servers = async function (req = {}) {
2031
+ try {
2032
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
2033
+ const docs = await _load_all_server_docs();
2034
+ const rank = { region: 0, farm_node: 1, farm_vm: 2, other: 3 };
2035
+ const servers = docs
2036
+ .map(_srv_view)
2037
+ .sort((a, b) => rank[a.role] - rank[b.role] || (a.id === 'master' ? -1 : b.id === 'master' ? 1 : String(a.id).localeCompare(String(b.id))));
2038
+ return {
2039
+ code: 1,
2040
+ data: {
2041
+ servers,
2042
+ deploy_targets: servers.filter((s) => s.deployable).map((s) => s.id),
2043
+ orchestrator: 'dev.xuda.ai',
2044
+ note: 'Deploy runs from the dev orchestrator; region app-servers only. Soft = zero-downtime drain + reload; hard = teardown + clean install.',
2045
+ },
2046
+ };
2047
+ } catch (err) { return { code: -1, data: err.message }; }
2048
+ };
2049
+
2050
+ // Queue a code deploy to ONE region app-server. Enqueue-only + superuser-gated:
2051
+ // sets stat:1 + publish_mode on the target doc; the dev orchestrator rolls it
2052
+ // out. mode: 'soft' (default, zero-downtime) | 'hard' (teardown + clean
2053
+ // install). Guards: deployable target only; not the dev orchestrator; not
2054
+ // already busy; confirm required (hard requires the server id typed back).
2055
+ // dry_run returns the plan WITHOUT writing (safe preview).
2056
+ export const ops_deploy_server = async function (req = {}) {
2057
+ try {
2058
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
2059
+ const server_id = String(req.server_id || '').trim();
2060
+ const mode = req.mode === 'hard' ? 'hard' : 'soft';
2061
+ if (!server_id) return { code: -1, data: 'server_id is required' };
2062
+ if (server_id === 'dev') return { code: -1, data: 'the dev orchestrator deploys via rsync, not this rollout; pick a region (master/us/eu)' };
2063
+
2064
+ // FULL doc (no field projection) so it is safe to save back.
2065
+ const sr = await db_module.get_couch_doc('xuda_servers', server_id);
2066
+ if (sr.code < 0 || !sr.data) return { code: -1, data: `server "${server_id}" not found` };
2067
+ const doc = sr.data;
2068
+ 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)` };
2069
+
2070
+ // Confirmation gate — proportional to blast radius.
2071
+ if (mode === 'hard') {
2072
+ 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.` };
2073
+ } else {
2074
+ const ok = req.confirm === true || req.confirm === 'true' || req.confirm === server_id;
2075
+ if (!ok) return { code: -1, data: `confirm required: re-send with confirm:true to deploy "${server_id}" (soft, zero-downtime).` };
2076
+ }
2077
+
2078
+ // Already-in-flight guard.
2079
+ 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).` };
2080
+
2081
+ const plan = { server_id, mode, current_state: _srv_deploy_state(doc), private_ip: doc.private || null };
2082
+ if (req.dry_run) return { code: 1, data: { dry_run: true, would_deploy: plan } };
2083
+
2084
+ // Enqueue: the dev orchestrator's 1-min publish tick picks stat:1 up.
2085
+ doc.stat = 1;
2086
+ doc.stat_ts = Date.now();
2087
+ doc.publish_mode = mode;
2088
+ doc.deploy_requested_by = req.uid;
2089
+ doc.deploy_requested_ts = Date.now();
2090
+ delete doc.publish_error;
2091
+ const wr = await db_module.save_couch_doc('xuda_servers', doc);
2092
+ if (wr.code < 0) return { code: -1, data: wr.data };
2093
+
2094
+ _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);
2095
+ 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}` });
2096
+
2097
+ return { code: 1, data: { enqueued: true, server_id, mode, state: 'queued', orchestrator: 'dev.xuda.ai', poll: 'ops_server_deploy_status' } };
2098
+ } catch (err) { return { code: -1, data: err.message }; }
2099
+ };
2100
+
2101
+ // Poll deploy status/progress for one server (or all region app-servers). Reads
2102
+ // the stat state-machine the rollout writes onto each xuda_servers doc.
2103
+ export const ops_server_deploy_status = async function (req = {}) {
2104
+ try {
2105
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
2106
+ const now = Date.now();
2107
+ const to_status = (doc) => ({
2108
+ id: doc._id,
2109
+ state: _srv_deploy_state(doc),
2110
+ busy: _srv_busy(doc),
2111
+ publish_mode: doc.publish_mode || null,
2112
+ stat: typeof doc.stat === 'number' ? doc.stat : null,
2113
+ stat_ts: doc.stat_ts || null,
2114
+ elapsed_ms: doc.stat_ts && _srv_busy(doc) ? now - doc.stat_ts : null,
2115
+ error: doc.publish_error || null,
2116
+ requested_by: doc.deploy_requested_by || null,
2117
+ requested_ts: doc.deploy_requested_ts || null,
2118
+ });
2119
+
2120
+ if (req.server_id) {
2121
+ const sr = await db_module.get_couch_doc('xuda_servers', String(req.server_id).trim());
2122
+ if (sr.code < 0 || !sr.data) return { code: -1, data: `server "${req.server_id}" not found` };
2123
+ return { code: 1, data: to_status(sr.data) };
2124
+ }
2125
+ const docs = await _load_all_server_docs();
2126
+ const rows = docs.filter(_srv_is_deployable).map(to_status);
2127
+ return { code: 1, data: { servers: rows, any_busy: rows.some((r) => r.busy) } };
2128
+ } catch (err) { return { code: -1, data: err.message }; }
2129
+ };
2130
+
1960
2131
  const TIPS_FILE_PATH = path.join(process.env.XUDA_HOME, 'dist', 'tips', 'tips.json');
1961
2132
  let _did_you_know_tips_cache = null;
1962
2133
  let _did_you_know_tips_cache_mtime = 0;
package/index_ms.mjs CHANGED
@@ -189,6 +189,18 @@ export const ops_send_notification = async function (...args) {
189
189
  return await broker.send_to_queue("ops_send_notification", ...args);
190
190
  };
191
191
 
192
+ export const ops_list_servers = async function (...args) {
193
+ return await broker.send_to_queue("ops_list_servers", ...args);
194
+ };
195
+
196
+ export const ops_deploy_server = async function (...args) {
197
+ return await broker.send_to_queue("ops_deploy_server", ...args);
198
+ };
199
+
200
+ export const ops_server_deploy_status = async function (...args) {
201
+ return await broker.send_to_queue("ops_server_deploy_status", ...args);
202
+ };
203
+
192
204
  export const did_you_know_tips = async function (...args) {
193
205
  return await broker.send_to_queue("did_you_know_tips", ...args);
194
206
  };
package/index_msa.mjs CHANGED
@@ -189,6 +189,18 @@ export const ops_send_notification = function (...args) {
189
189
  broker.send_to_queue_async("ops_send_notification", ...args);
190
190
  };
191
191
 
192
+ export const ops_list_servers = function (...args) {
193
+ broker.send_to_queue_async("ops_list_servers", ...args);
194
+ };
195
+
196
+ export const ops_deploy_server = function (...args) {
197
+ broker.send_to_queue_async("ops_deploy_server", ...args);
198
+ };
199
+
200
+ export const ops_server_deploy_status = function (...args) {
201
+ broker.send_to_queue_async("ops_server_deploy_status", ...args);
202
+ };
203
+
192
204
  export const did_you_know_tips = function (...args) {
193
205
  broker.send_to_queue_async("did_you_know_tips", ...args);
194
206
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2285",
3
+ "version": "1.2.2286",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {