@xuda.io/account_module 1.2.2294 → 1.2.2296
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 +73 -19
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1774,14 +1774,17 @@ const _ops_services_power = async (uid, on) => {
|
|
|
1774
1774
|
return { total: servers.length, ok, err };
|
|
1775
1775
|
};
|
|
1776
1776
|
|
|
1777
|
-
// OPS
|
|
1778
|
-
//
|
|
1777
|
+
// OPS alerts go to the superuser accounts (info@xuda.ai is the superuser). One
|
|
1778
|
+
// system notification per superuser, delivered as a dashboard banner AND an email,
|
|
1779
|
+
// so each superuser gets exactly one [OPS] copy. A separate hardcoded email to
|
|
1780
|
+
// info@ used to run alongside this and double-delivered to info@ (info@ is itself
|
|
1781
|
+
// the superuser, so the notification already emailed it), so it was removed.
|
|
1782
|
+
// `ref` is shown in a small footer.
|
|
1779
1783
|
const _ops_notify_ops = (subject, rows, ref) => {
|
|
1780
1784
|
try {
|
|
1781
1785
|
const esc = (v) => String(v == null ? '' : v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1782
1786
|
const footer = ref ? `<p style="margin:16px 0 0 0;font-size:11px;color:#999">Ref: ${esc(ref)}</p>` : '';
|
|
1783
1787
|
const body = `<h2>${esc(subject)}</h2><table cellpadding="6" style="border-collapse:collapse;font-size:14px">${rows.map(([k, v]) => `<tr><td><strong>${esc(k)}</strong></td><td>${esc(v)}</td></tr>`).join('')}</table>${footer}`;
|
|
1784
|
-
email_msa.send_email({ email: 'info@xuda.ai', subject: `[OPS] ${subject}`, body });
|
|
1785
1788
|
const supers = _conf.superuser_account_ids || [];
|
|
1786
1789
|
if (supers.length) notification_msa.submit_notification({ type: 'system', uid_arr: supers, system: true, subject: `[OPS] ${subject}`, body, delivery_method: ['banner', 'email'], display_type: 'warning' });
|
|
1787
1790
|
} catch (e) { console.error('[ops notify]', e.message); }
|
|
@@ -2286,6 +2289,11 @@ export const ops_seed_do_datacenters = async function (req = {}) {
|
|
|
2286
2289
|
docType: 'do_datacenter',
|
|
2287
2290
|
provider: 'digitalocean',
|
|
2288
2291
|
is_do_datacenter: true,
|
|
2292
|
+
// Inventory doc, NOT a live controller: keep the server-health check from
|
|
2293
|
+
// probing https://do_<slug>.xuda.ai/cpi/... (there is no such endpoint - DNS
|
|
2294
|
+
// fails). The health loop skips any doc with application_server === false
|
|
2295
|
+
// (controller_module codex_check_xuda_servers_health), same as farm nodes.
|
|
2296
|
+
application_server: false,
|
|
2289
2297
|
region_slug: dc.slug, // DO createDroplet region
|
|
2290
2298
|
location: dc.slug, // strips (\d+$) to the metro for macro scoping, like a farm node's hel1
|
|
2291
2299
|
metro: dc.metro,
|
|
@@ -7515,13 +7523,43 @@ const _nl_one = async (db, docType) => {
|
|
|
7515
7523
|
}
|
|
7516
7524
|
};
|
|
7517
7525
|
|
|
7526
|
+
// The newsletter/content list + random-pick queries above all select on a single
|
|
7527
|
+
// `docType` field. Without an index Couch full-scans each content DB and logs
|
|
7528
|
+
// "No matching index found". These DBs live locally on dev + master (regions read
|
|
7529
|
+
// newsletter from master via DSN), so ensure a [docType] index on each at module
|
|
7530
|
+
// load, idempotent, only on the content-home nodes.
|
|
7531
|
+
(async () => {
|
|
7532
|
+
const host = process.env.XUDA_HOSTNAME;
|
|
7533
|
+
if (host !== 'dev.xuda.ai' && host !== 'master.xuda.ai') return;
|
|
7534
|
+
const content_dbs = ['xuda_newsletter', 'xuda_dev_articles', 'xuda_news_articles', 'xuda_did_you_know'];
|
|
7535
|
+
for (const db of content_dbs) {
|
|
7536
|
+
try {
|
|
7537
|
+
const ret = await db_module.create_couch_index(db, {
|
|
7538
|
+
index: { fields: ['docType'] },
|
|
7539
|
+
name: 'idx_content_docType',
|
|
7540
|
+
ddoc: 'idx_content_docType',
|
|
7541
|
+
type: 'json',
|
|
7542
|
+
});
|
|
7543
|
+
if (ret?.error) console.error(`[newsletter] ${db} docType index create failed:`, ret.error);
|
|
7544
|
+
} catch (err) {
|
|
7545
|
+
console.error(`[newsletter] ${db} docType index init failed:`, err?.message || err);
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
})();
|
|
7549
|
+
|
|
7518
7550
|
const _nl_fashion_url = (p) => {
|
|
7519
7551
|
const rel = p.custom_url || (p.slug ? '/' + p.slug : '');
|
|
7520
7552
|
return `https://xuda.fashion${rel}`;
|
|
7521
7553
|
};
|
|
7522
7554
|
const _nl_fashion_img = (p) => {
|
|
7523
7555
|
const first = p.images && (p.images.front || Object.values(p.images)[0]);
|
|
7524
|
-
|
|
7556
|
+
if (!first) return '';
|
|
7557
|
+
// Request a light, email-safe variant: the fashion server resizes on the fly
|
|
7558
|
+
// (JPEG out) when the image URL carries ?w. Legacy product images can be ~3MB
|
|
7559
|
+
// PNGs that mail clients silently refuse to render, which is what left the tee
|
|
7560
|
+
// broken in the weekly issue; ~560px keeps it visible and lightweight. Servers
|
|
7561
|
+
// without the resize route just ignore the query and serve the original.
|
|
7562
|
+
return `https://xuda.fashion${first}?w=560&q=78`;
|
|
7525
7563
|
};
|
|
7526
7564
|
// A fashion product prioritized to the recipient's CITY, then COUNTRY, then a
|
|
7527
7565
|
// featured active tee, then the classic tee (mirrors the city-merch placeholder
|
|
@@ -7818,24 +7856,40 @@ const _nl_render_email = (issue, { host, first_name, fashion, partners, unsub_ur
|
|
|
7818
7856
|
</div>`;
|
|
7819
7857
|
};
|
|
7820
7858
|
|
|
7821
|
-
// Demo
|
|
7822
|
-
// city/country-targeted sections
|
|
7823
|
-
// instead of falling back, and
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
city: 'Berlin',
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
}
|
|
7859
|
+
// Demo personas for previews/samples. Each is a real region with its own tee so
|
|
7860
|
+
// the city/country-targeted sections (fashion + the local ambassador/mentor)
|
|
7861
|
+
// render instead of falling back, and the first name makes personalization
|
|
7862
|
+
// visible. The [SAMPLE] rotates through them by issue number so consecutive
|
|
7863
|
+
// previews vary instead of always showing Berlin. Real subscribers always get
|
|
7864
|
+
// their OWN city (the blast passes each account's country/city), so this only
|
|
7865
|
+
// affects the review copy that goes to ops.
|
|
7866
|
+
const _NL_SAMPLE_PERSONAS = [
|
|
7867
|
+
{ first_name: 'Alex', cc: 'de', country: 'Germany', city: 'Berlin' },
|
|
7868
|
+
{ first_name: 'Sofia', cc: 'pt', country: 'Portugal', city: 'Lisbon' },
|
|
7869
|
+
{ first_name: 'Min-jun', cc: 'kr', country: 'South Korea', city: 'Seoul' },
|
|
7870
|
+
{ first_name: 'Mateo', cc: 'mx', country: 'Mexico', city: 'Monterrey' },
|
|
7871
|
+
{ first_name: 'Amara', cc: 'eg', country: 'Egypt', city: 'Cairo' },
|
|
7872
|
+
{ first_name: 'Noa', cc: 'nl', country: 'Netherlands', city: 'Utrecht' },
|
|
7873
|
+
];
|
|
7874
|
+
const _nl_sample_recipient = (host, rotate = 0) => {
|
|
7875
|
+
const n = _NL_SAMPLE_PERSONAS.length;
|
|
7876
|
+
const p = _NL_SAMPLE_PERSONAS[(((rotate | 0) % n) + n) % n];
|
|
7877
|
+
return {
|
|
7878
|
+
host,
|
|
7879
|
+
first_name: p.first_name,
|
|
7880
|
+
cc: p.cc,
|
|
7881
|
+
country: p.country,
|
|
7882
|
+
city: p.city,
|
|
7883
|
+
fashion: { title: 'XUDA.AI Classic Tee', url: 'https://xuda.fashion', img: '' },
|
|
7884
|
+
partners: { ambassador: null, mentor: null },
|
|
7885
|
+
unsub_url: `https://${host}/newsletter/unsubscribe?u=SAMPLE&t=SAMPLE`,
|
|
7886
|
+
};
|
|
7887
|
+
};
|
|
7834
7888
|
|
|
7835
7889
|
const _newsletter_send_sample_internal = async (issue, to_email, theme) => {
|
|
7836
7890
|
const host = _ops_host();
|
|
7837
7891
|
const [fpool, ppool] = await Promise.all([_nl_fashion_pool(), _nl_partner_pool()]);
|
|
7838
|
-
const rcpt = _nl_sample_recipient(host);
|
|
7892
|
+
const rcpt = _nl_sample_recipient(host, issue && issue.issue_number);
|
|
7839
7893
|
rcpt.fashion = _nl_fashion_for(fpool, rcpt.country, rcpt.city, issue.issue_number);
|
|
7840
7894
|
rcpt.partners = _nl_partners_for(ppool, rcpt.cc, rcpt.country, rcpt.city);
|
|
7841
7895
|
rcpt.theme = theme;
|
|
@@ -7941,7 +7995,7 @@ export const newsletter_preview = async function (req = {}) {
|
|
|
7941
7995
|
if (ret.code < 0 || !ret.data) return { code: -1, data: 'issue not found' };
|
|
7942
7996
|
const host = _ops_host();
|
|
7943
7997
|
const [fpool, ppool] = await Promise.all([_nl_fashion_pool(), _nl_partner_pool()]);
|
|
7944
|
-
const rcpt = _nl_sample_recipient(host);
|
|
7998
|
+
const rcpt = _nl_sample_recipient(host, ret.data.issue_number);
|
|
7945
7999
|
rcpt.fashion = _nl_fashion_for(fpool, rcpt.country, rcpt.city, ret.data.issue_number);
|
|
7946
8000
|
rcpt.partners = _nl_partners_for(ppool, rcpt.cc, rcpt.country, rcpt.city);
|
|
7947
8001
|
rcpt.theme = req.theme;
|