channel-worker 2.5.81 → 2.5.89

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.81",
3
+ "version": "2.5.89",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -130,31 +130,52 @@ async function scrapeTiles(page) {
130
130
  // LAST_90D chạy thật. THIS_YEAR và LIFETIME thì trang LẶNG LẼ rơi về 28 ngày —
131
131
  // không báo lỗi, nên phải đối chiếu nhãn khoảng thời gian trả về.
132
132
 
133
+ const vnYesterdayIso = () => new Date(Date.now() + 7 * 3600e3 - 864e5).toISOString().slice(0, 10);
134
+
133
135
  const INSIGHTS_BASE = 'https://www.facebook.com/professional_dashboard/profile_insights';
134
136
 
135
137
  // Số trên Insights là số đầy đủ, dấu chấm là phân cách nghìn ("1.333.852").
136
138
  // KHÔNG dùng parseViews ở đây — hàm đó dịch "1.2" thành 12 vì phải lo hậu tố K/N.
139
+ // Số trên Insights luôn là số NGUYÊN, chỉ khác dấu ngăn nghìn theo ngôn ngữ
140
+ // của profile: tiếng Việt dùng dấu chấm ("1.333.852"), tiếng Anh dùng dấu phẩy
141
+ // ("312,314"). Bỏ CẢ HAI. Chỉ strip dấu chấm là profile tiếng Anh đọc "312,314"
142
+ // thành 312 — sai 1000 lần mà không có dấu hiệu gì (gặp thật 2026-09-06, 4 kênh).
137
143
  function parseExact(txt) {
138
- const m = /(\d[\d.]*)/.exec(String(txt || '').replace(/\s+/g, ' '));
144
+ const m = /(\d[\d.,]*)/.exec(String(txt || '').replace(/\s+/g, ' '));
139
145
  if (!m) return null;
140
- const n = parseInt(m[1].replace(/\./g, ''), 10);
146
+ const n = parseInt(m[1].replace(/[.,]/g, ''), 10);
141
147
  return Number.isFinite(n) ? n : null;
142
148
  }
143
149
 
144
150
  // "31 Tháng 8 11:41" → ISO. Facebook không ghi năm; tháng lớn hơn tháng hiện
145
151
  // tại nghĩa là năm ngoái (bài cuối tháng 12 xem vào tháng 1).
152
+ const EN_MONTH = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 };
153
+
146
154
  function parseVnPostTime(txt, now = new Date()) {
147
- const m = /(\d{1,2})\s*Tháng\s*(\d{1,2})\s+(\d{1,2}):(\d{2})/i.exec(String(txt || ''));
148
- if (!m) return null;
149
- const [, d, mo, h, mi] = m.map(Number);
155
+ const t = String(txt || '');
156
+ let d; let mo; let h; let mi;
157
+ const vn = /(\d{1,2})\s*Tháng\s*(\d{1,2})\s+(\d{1,2}):(\d{2})/i.exec(t);
158
+ if (vn) {
159
+ [, d, mo, h, mi] = vn.map(Number);
160
+ } else {
161
+ // Profile tiếng Anh: "Fri Sep 4, 2:33pm" (12 giờ, có am/pm).
162
+ const en = /(?:[A-Za-z]{3},?\s+)?([A-Za-z]{3})\s+(\d{1,2}),?\s+(\d{1,2}):(\d{2})\s*(am|pm)/i.exec(t);
163
+ if (!en) return null;
164
+ mo = EN_MONTH[en[1].toLowerCase()];
165
+ if (!mo) return null;
166
+ d = Number(en[2]); h = Number(en[3]); mi = Number(en[4]);
167
+ const pm = en[5].toLowerCase() === 'pm';
168
+ if (pm && h < 12) h += 12;
169
+ if (!pm && h === 12) h = 0;
170
+ }
150
171
  let year = now.getUTCFullYear();
151
172
  // Mốc so sánh theo giờ VN vì Facebook hiện giờ địa phương của tài khoản.
152
173
  const vnNow = new Date(now.getTime() + 7 * 3600e3);
153
174
  if (mo > vnNow.getUTCMonth() + 1) year -= 1;
154
175
  const iso = `${year}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`
155
176
  + `T${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:00+07:00`;
156
- const t = new Date(iso);
157
- return Number.isNaN(t.getTime()) ? null : t.toISOString();
177
+ const parsed = new Date(iso);
178
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
158
179
  }
159
180
 
160
181
  // Nhãn khoảng thời gian PHẢI khớp cái đã xin. Trang dựng khung trước rồi mới
@@ -162,32 +183,107 @@ function parseVnPostTime(txt, now = new Date()) {
162
183
  // hiệu gì. Đo thật 2026-09-06: 5/43 lượt xin LAST_7D nhận về "Hôm nay", và một
163
184
  // kênh ghi được daily_views 279.946 trong khi views_7d chỉ 278.662 — tức là số
164
185
  // 28 ngày bị nhét vào ô "hôm nay". Phải ĐỢI nhãn khớp, không thì thà hỏng to.
186
+ // Profile để tiếng Anh thì Facebook trả nhãn tiếng Anh ("Last 7 days: Aug 30 -
187
+ // Sep 5"). Đo 2026-09-06: 4/52 kênh như vậy, parser thuần tiếng Việt coi trang
188
+ // đó là trống hoàn toàn.
165
189
  const RANGE_LABEL = {
166
- TODAY: /hôm nay/i,
167
- LAST_7D: /7 ngày qua/i,
168
- LAST_14D: /14 ngày qua/i,
169
- LAST_28D: /28 ngày qua/i,
170
- LAST_60D: /60 ngày qua/i,
171
- LAST_90D: /90 ngày qua/i,
190
+ TODAY: /hôm nay|^today/i,
191
+ LAST_7D: /7 ngày qua|last 7 days/i,
192
+ LAST_14D: /14 ngày qua|last 14 days/i,
193
+ LAST_28D: /28 ngày qua|last 28 days/i,
194
+ LAST_60D: /60 ngày qua|last 60 days/i,
195
+ LAST_90D: /90 ngày qua|last 90 days/i,
172
196
  };
173
197
 
198
+ // Đọc CHIP khoảng thời gian ("7 ngày qua: 30 Tháng 8 - 5 Tháng 9", "Hôm nay",
199
+ // "Tùy chỉnh: …"). KHÔNG đọc dòng "Tổng số lượt xem": Facebook dựng HAI kiểu
200
+ // giao diện cho cùng trang này — kiểu A có "… Tổng số lượt xem trong 7 ngày
201
+ // qua", kiểu B là "Số liệu chia nhỏ về lượt xem" rồi "26.313 -91% Lượt xem"
202
+ // (không có chữ "Tổng số"). Bám vào dòng đó là ~10/44 kênh kiểu B bị coi như
203
+ // trang trống — đúng ca "Má hồng phai" 2026-09-06.
204
+ async function currentRangeLabel(page) {
205
+ return page.evaluate(() => {
206
+ const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
207
+ for (const e of document.querySelectorAll('div[role="button"],div,span')) {
208
+ const t = clean(e.innerText);
209
+ if (t.length < 70 && /^(Hôm nay$|Today$|Tùy chỉnh:|Custom:|Năm nay:|This year:|\d+ ngày qua:|Last \d+ days:)/i.test(t)) return t;
210
+ }
211
+ return '';
212
+ }).catch(() => '');
213
+ }
214
+
215
+ // Tổng lượt xem của khoảng đang hiện, đọc được CẢ HAI kiểu giao diện.
216
+ // Phải neo cuối chuỗi ở "Lượt xem": trang còn có "Lượt xem trong tối thiểu 3
217
+ // giây" và "… 1 phút", khớp lỏng là vớ nhầm mấy số đó.
218
+ // Giữa CON SỐ và NHÃN, Facebook nhét đủ thứ không phải chữ: phần trăm, và có
219
+ // khi cả MŨI TÊN. Ảnh chụp 2026-09-06 kênh "lặn biển 01":
220
+ // "105,474 ↓ -46% Total views from previous 28 days"
221
+ // Regex cũ chỉ cho khoảng trắng ở đó nên trượt sạch — trong khi số nằm ngay
222
+ // đấy. Bản tiếng Việt và khoảng 7 ngày không kèm mũi tên nên vẫn chạy, chỉ
223
+ // khoảng tuỳ chỉnh (có % giảm) mới lộ. `[^\p{L}]*` nuốt mọi thứ không phải chữ.
224
+ const TOTAL_LABELS = [
225
+ /^([\d.,]+)([^\p{L}]*)(?:Tổng số lượt xem|Total views)/iu,
226
+ /^([\d.,]+)([^\p{L}]*)(?:Lượt xem|Views)$/iu,
227
+ ];
228
+
229
+ function pickTotal(chunks) {
230
+ for (const re of TOTAL_LABELS) {
231
+ for (const c of chunks) {
232
+ const m = re.exec(c);
233
+ if (!m) continue;
234
+ const pm = /(-?[\d.,]+)%/.exec(m[2] || '');
235
+ return { views: parseExact(m[1]), pct: pm ? parseFloat(pm[1].replace(/,/g, '')) : null };
236
+ }
237
+ }
238
+ return { views: null, pct: null };
239
+ }
240
+
174
241
  async function readViewsPage(page, range, log) {
175
242
  const url = `${INSIGHTS_BASE}/views/?tab=reels&date_range=${range}`;
176
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
177
243
  const want = RANGE_LABEL[range];
178
244
  let label = '';
179
- // Tối đa ~24s: khung trang hiện trong ~3s, số đổ về chậm hơn nhiều.
180
- for (let i = 0; i < 12; i++) {
181
- await page.waitForTimeout(2000);
182
- label = await page.evaluate(() => {
183
- const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
184
- for (const e of document.querySelectorAll('div,span')) {
185
- const t = clean(e.innerText);
186
- if (t.length < 60 && /Tổng số lượt xem/i.test(t)) return t;
245
+ // LẦN ĐIỀU HƯỚNG ĐẦU TIÊN THƯỜNG BỊ BỎ QUA: trang giữ mặc định 28 ngày và
246
+ // lờ tham số date_range. Đo thật 2026-09-06 lúc 12h trưa: 8/47 kênh xin
247
+ // TODAY mà trang trả về "… trong 28 ngày qua" — không phải hết dữ liệu, mà
248
+ // tham số không ăn. Nạp lại là lần sau nó nhận, nên thử tới 3 lượt.
249
+ for (let attempt = 0; attempt < 3; attempt++) {
250
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
251
+ for (let i = 0; i < 8; i++) {
252
+ await page.waitForTimeout(2000);
253
+ label = await currentRangeLabel(page);
254
+ if (label && (!want || want.test(label))) break;
255
+ }
256
+ if (!want || !label || want.test(label)) break;
257
+ log('info', `[insights] ${range}: lần ${attempt + 1} trang vẫn hiện "${label}", nạp lại`);
258
+ // Facebook NHỚ khoảng đã chọn lần trước theo từng profile, và tham số trên
259
+ // URL không phải lúc nào cũng đè được cái nhớ đó — nạp lại mấy lần vẫn y
260
+ // nguyên (đo 2026-09-06: 4 kênh xin LAST_7D mà cứ hiện "Hôm nay"). Đường
261
+ // chắc chắn là BẤM vào mục trong bộ chọn: chúng là thẻ <a> nên bấm là điều
262
+ // hướng client-side, luôn áp dụng.
263
+ if (attempt === 1) {
264
+ // CHUỘT THẬT của Playwright, không phải element.click() trong evaluate:
265
+ // click tổng hợp trong DOM không phải lúc nào cũng kích được handler của
266
+ // React (đo 2026-09-06: evaluate trả false, log không hề có dòng "đổi
267
+ // khoảng"). page.click gửi đủ chuỗi sự kiện chuột.
268
+ const ok = await (async () => {
269
+ try {
270
+ await page.locator('div[role="button"]').filter({ hasText: /^(Hôm nay|Today|Tùy chỉnh:|Custom:|Năm nay:|This year:|\d+ ngày qua:|Last \d+ days:)/ })
271
+ .first().click({ timeout: 8000 });
272
+ await page.waitForTimeout(2000);
273
+ await page.locator(`a[role="menuitemradio"][href*="date_range=${range}"]`).first().click({ timeout: 8000 });
274
+ return true;
275
+ } catch { return false; }
276
+ })();
277
+ if (ok) {
278
+ log('info', `[insights] ${range}: đổi khoảng bằng cách bấm bộ chọn`);
279
+ for (let i = 0; i < 8; i++) {
280
+ await page.waitForTimeout(2000);
281
+ label = await currentRangeLabel(page);
282
+ if (label && want.test(label)) break;
283
+ }
284
+ if (label && want.test(label)) break;
187
285
  }
188
- return '';
189
- }).catch(() => '');
190
- if (label && (!want || want.test(label))) break;
286
+ }
191
287
  }
192
288
  // Hai ca KHÁC HẲN nhau, đừng gộp:
193
289
  // nhãn RỖNG = khoảng này chưa có số nào. Chuyện thường, không phải lỗi —
@@ -208,25 +304,23 @@ async function readViewsPage(page, range, log) {
208
304
  for (const e of document.querySelectorAll('div,span')) {
209
305
  const t = clean(e.innerText);
210
306
  if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
211
- if (/Lượt xem|lượt xem/.test(t)) { seen.add(t); chunks.push(t); }
307
+ if (/[Ll]ượt xem|[Vv]iews/.test(t)) { seen.add(t); chunks.push(t); }
212
308
  if (chunks.length > 120) break;
213
309
  }
214
- const label = [...document.querySelectorAll('div,span,button')]
215
- .map((e) => clean(e.innerText))
216
- .find((t) => t.length < 60 && /ngày qua:|hôm nay/i.test(t)) || '';
217
- return { chunks, label };
218
- }).catch(() => ({ chunks: [], label: '' }));
310
+ return { chunks };
311
+ }).catch(() => ({ chunks: [] }));
219
312
 
220
- // Tổng: "216.353 -35% Tổng số lượt xem trong 7 ngày qua" / "… hôm nay"
221
- let total = null; let pct = null;
222
- for (const c of raw.chunks) {
223
- const m = /^([\d.]+)\s*(?:(-?[\d.,]+)%)?\s*Tổng số lượt xem/i.exec(c);
224
- if (m) { total = parseExact(m[1]); pct = m[2] ? parseFloat(String(m[2]).replace(',', '.')) : null; break; }
225
- }
313
+ const t0 = pickTotal(raw.chunks);
314
+ const total = t0.views; const pct = t0.pct;
226
315
  // Dòng bài: "31 Tháng 8 11:41 56.243 Lượt xem"
227
316
  const posts = [];
317
+ const POST_LINE = [
318
+ /(\d{1,2}\s*Tháng\s*\d{1,2}\s+\d{1,2}:\d{2})\s+([\d.,]+)\s*Lượt xem/i,
319
+ /((?:[A-Za-z]{3},?\s+)?[A-Za-z]{3}\s+\d{1,2},?\s+\d{1,2}:\d{2}\s*(?:am|pm))\s+([\d.,]+)\s*Views/i,
320
+ ];
228
321
  for (const c of raw.chunks) {
229
- const m = /(\d{1,2}\s*Tháng\s*\d{1,2}\s+\d{1,2}:\d{2})\s+([\d.]+)\s*Lượt xem/i.exec(c);
322
+ let m = null;
323
+ for (const re of POST_LINE) { m = re.exec(c); if (m) break; }
230
324
  if (!m) continue;
231
325
  const at = parseVnPostTime(m[1]);
232
326
  const views = parseExact(m[2]);
@@ -236,6 +330,70 @@ async function readViewsPage(page, range, log) {
236
330
  return { range, label, total, pct, posts };
237
331
  }
238
332
 
333
+ // Đọc ĐÚNG MỘT NGÀY qua khoảng tuỳ chỉnh.
334
+ //
335
+ // Vì sao không dùng date_range=TODAY: Facebook KHÔNG có số của hôm nay. Đo
336
+ // 2026-09-06 lúc 12h trưa trên kênh 1,5 triệu view/7 ngày, trang trả về đúng
337
+ // chữ "--" kèm "Chưa đủ dữ liệu để hiển thị". Chip khoảng thời gian của chính
338
+ // nó cũng kết thúc ở HÔM QUA ("28 ngày qua: 9 Tháng 8 - 5 Tháng 9" trong khi
339
+ // đã là ngày 6) — cửa sổ dữ liệu của Facebook đóng sổ ở hôm qua.
340
+ //
341
+ // Không có preset "Hôm qua" (đã thử YESTERDAY/LAST_1D/PREVIOUS_DAY, cả ba rơi
342
+ // về 28 ngày). Định dạng dưới đây lấy được bằng cách bấm thật vào lịch trong
343
+ // mục "Tùy chỉnh" rồi đọc URL nó sinh ra.
344
+ //
345
+ // BẪY: dòng "… Tổng số lượt xem trong 28 ngày qua" ở khoảng tuỳ chỉnh là NHÃN
346
+ // CŨ chưa kịp đổi, nhưng CON SỐ thì đã đúng của ngày đã chọn. Nên phải xác
347
+ // minh bằng CHIP ("Tùy chỉnh: 5 Tháng 9 – 5 Tháng 9"), không phải bằng nhãn đó.
348
+ async function readOneDay(page, iso, log) {
349
+ const url = `${INSIGHTS_BASE}/views/?tab=reels&date_range=CUSTOM&start_date=${iso}&end_date=${iso}`;
350
+ let chip = '';
351
+ for (let attempt = 0; attempt < 3; attempt++) {
352
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
353
+ for (let i = 0; i < 8; i++) {
354
+ await page.waitForTimeout(2000);
355
+ chip = await page.evaluate(() => {
356
+ const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
357
+ for (const e of document.querySelectorAll('div[role="button"],span,div')) {
358
+ const t = clean(e.innerText);
359
+ if (t.length < 70 && /^(Tùy chỉnh:|Custom:)/i.test(t)) return t;
360
+ }
361
+ return '';
362
+ }).catch(() => '');
363
+ if (chip) break;
364
+ }
365
+ if (chip) break;
366
+ log('info', `[insights] ngày ${iso}: lần ${attempt + 1} chưa thấy chip tuỳ chỉnh, nạp lại`);
367
+ }
368
+ if (!chip) return { iso, chip: '', views: null };
369
+ // Chip đổi TRƯỚC, con số đổ về SAU — chờ cố định 3s là hên xui: đo 2026-09-06
370
+ // có kênh chip đúng "Custom: Sep 5 - Sep 5" mà số vẫn null. Chờ tới khi thấy
371
+ // số, tối đa ~14s.
372
+ let views = null;
373
+ for (let i = 0; i < 7; i++) {
374
+ await page.waitForTimeout(2000);
375
+ const cs = await readTotalChunks(page);
376
+ views = pickTotal(cs).views;
377
+ if (views != null) break;
378
+ }
379
+ log('info', `[insights] ngày ${iso}: ${views} lượt xem — chip "${chip}"`);
380
+ return { iso, chip, views };
381
+ }
382
+
383
+ async function readTotalChunks(page) {
384
+ return page.evaluate(() => {
385
+ const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
386
+ const seen = new Set(); const out = [];
387
+ for (const e of document.querySelectorAll('div,span')) {
388
+ const t = clean(e.innerText);
389
+ if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
390
+ if (/[Ll]ượt xem|[Vv]iews/.test(t)) { seen.add(t); out.push(t); }
391
+ if (out.length > 120) break;
392
+ }
393
+ return out;
394
+ }).catch(() => []);
395
+ }
396
+
239
397
  async function readAudiencePage(page, log) {
240
398
  await page.goto(`${INSIGHTS_BASE}/audience/`, { waitUntil: 'domcontentloaded', timeout: 45_000 });
241
399
  await page.waitForTimeout(7000);
@@ -245,16 +403,19 @@ async function readAudiencePage(page, log) {
245
403
  for (const e of document.querySelectorAll('div,span')) {
246
404
  const t = clean(e.innerText);
247
405
  if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
248
- if (/người theo dõi|bỏ theo dõi/i.test(t)) { seen.add(t); out.push(t); }
406
+ if (/người theo dõi|bỏ theo dõi|followers|follows|unfollows/i.test(t)) { seen.add(t); out.push(t); }
249
407
  if (out.length > 60) break;
250
408
  }
251
409
  return out;
252
410
  }).catch(() => []);
253
- const pick = (re) => { for (const c of chunks) { const m = re.exec(c); if (m) return parseExact(m[1]); } return null; };
411
+ const pick = (...res) => {
412
+ for (const re of res) for (const c of chunks) { const m = re.exec(c); if (m) return parseExact(m[1]); }
413
+ return null;
414
+ };
254
415
  const a = {
255
- followers: pick(/^([\d.]+)\s*(?:[-+][\d.,]+%\s*)?Tổng số người theo dõi/i),
256
- net_followers: pick(/^([\d.]+)\s*(?:[-+][\d.,]+%\s*)?Số người theo dõi thực/i),
257
- unfollows: pick(/([\d.]+)\s*Lượt bỏ theo dõi/i),
416
+ followers: pick(/^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Tổng số người theo dõi/i, /^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Total followers/i),
417
+ net_followers: pick(/^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Số người theo dõi thực/i, /^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Net follows/i),
418
+ unfollows: pick(/([\d.,]+)\s*Lượt bỏ theo dõi/i, /([\d.,]+)\s*Unfollows/i),
258
419
  };
259
420
  log('info', `[insights] đối tượng: ${a.followers} follower, thực ${a.net_followers}, bỏ ${a.unfollows}`);
260
421
  return a;
@@ -264,24 +425,27 @@ async function readAudiencePage(page, log) {
264
425
  // Việc khớp bài với idea và tính hiệu để lòi ra "bài cũ đang nổ" làm ở API —
265
426
  // worker không giữ trạng thái ngày hôm qua.
266
427
  async function runInsights({ page, payload, log }) {
267
- const today = await readViewsPage(page, 'TODAY', log);
428
+ // 7 ngày trước để làm ấm trang (lần điều hướng ĐẦU hay bị bỏ qua tham số),
429
+ // rồi mới tới ngày hôm qua.
268
430
  const week = await readViewsPage(page, 'LAST_7D', log);
431
+ const yIso = vnYesterdayIso();
432
+ const yest = await readOneDay(page, yIso, log);
269
433
  const audience = await readAudiencePage(page, log);
270
- if (today.total == null && week.total == null) {
434
+ if (yest.views == null && week.total == null) {
271
435
  throw new Error('không đọc được tổng lượt xem trên trang Insights — FB đổi giao diện?');
272
436
  }
273
437
  if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
274
438
  const d = await log.apiRequest('POST', '/analytics/fb-insights', {
275
439
  channel_id: payload.channel_id,
276
440
  at: new Date().toISOString(),
277
- today: { views: today.total, pct: today.pct, posts: today.posts },
441
+ yesterday: { date: yest.iso, views: yest.views, chip: yest.chip },
278
442
  week: { views: week.total, posts: week.posts },
279
443
  audience,
280
444
  });
281
- log('info', `[insights] đã ghi: ngày ${d?.daily_views}, follower ${d?.followers}, bài khớp ${d?.matched}/${d?.posts}, view bài cũ ${d?.unattributed}`);
445
+ log('info', `[insights] đã ghi: hôm qua ${d?.prev_day_views}, follower ${d?.followers}, bài khớp ${d?.matched}/${d?.posts}, view bài cũ ${d?.unattributed}`);
282
446
  return {
283
447
  ok: true, mode: 'insights',
284
- views_today: today.total, views_7d: week.total,
448
+ views_yesterday: yest.views, views_7d: week.total,
285
449
  posts: week.posts.length, matched: d?.matched ?? null,
286
450
  followers: audience.followers, unattributed: d?.unattributed ?? null,
287
451
  };