@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.
Files changed (2) hide show
  1. package/index.mjs +73 -19
  2. 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 = the superuser accounts + info@xuda.ai. Sends both an email to info@ and
1778
- // a system notification to every superuser. `ref` is shown in a small footer.
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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
- return first ? `https://xuda.fashion${first}` : '';
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 recipient for previews/samples. Uses a real region (Berlin/DE) so the
7822
- // city/country-targeted sections fashion + the local ambassador/mentor — render
7823
- // instead of falling back, and a demo first name so personalization is visible.
7824
- const _nl_sample_recipient = (host) => ({
7825
- host,
7826
- first_name: 'Alex',
7827
- cc: 'de',
7828
- country: 'Germany',
7829
- city: 'Berlin',
7830
- fashion: { title: 'XUDA.AI Classic Tee', url: 'https://xuda.fashion', img: '' },
7831
- partners: { ambassador: null, mentor: null },
7832
- unsub_url: `https://${host}/newsletter/unsubscribe?u=SAMPLE&t=SAMPLE`,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2294",
3
+ "version": "1.2.2296",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {