Asia Business Builders — Merchant Banking Phụ lục 1 · Kiến trúc Bản tin Teams Đội kỹ thuật · cập nhật 25/09/2026

Phụ lục 1 của Kiến trúc SIXDO Data Hub · 23/09/2026 · cập nhật 25/09/2026

Bản tin Teams từ bên trong

Một bên tiêu thụ chỉ-đọc của kho: chạy thế nào, quyết định gửi ra sao, và khi hỏng thì kênh nhìn thấy gì

Tài liệu kiến trúc chính vẽ bản tin là một ô trong sơ đồ. Phụ lục này mở ô đó ra cho người sẽ sửa code, trực vận hành, hoặc phải trả lời câu "sao sáng nay không có bản tin?". Các con số chính ở đây đều kèm nguồn — file và dòng, hoặc phép đo và ngày đo. Code ghim ở main 2ed7140 (25/09): bản tin v1 như ở 109ca30, bản tin v2 thêm ở 6dd143a (Pha 3) và b92aedf (script đăng ký). Từ khoảng 13:48 ngày 25/09 (file .env sửa lúc 13:47:45) công tắc TEAMS_DIGEST_PHIEN_BAN=2 đã bật — lượt tự động đầu tiên của v2 là 07:00 ngày 26/09. Chỗ nào chỉ đúng cho v1 được ghi rõ.

10quan hệ được gọi (9 mart + 1 dq) — v1 là 6
2lượt mỗi sáng · 07:00 và 09:30
4kênh nhận thẻ · Tổng hợp, POS, Online và Vận hành
0cột dữ liệu cá nhân rời câu SQL

Phần 01

Bản tin là gì — và không là gì

Trong sơ đồ ở Phần 02 của Kiến trúc SIXDO Data Hub, bản tin là một trong ba bên tiêu thụ đọc từ kho dùng chung. Nó thừa hưởng các luật của kho — chỉ đọc, một định nghĩa doanh thu, không đưa dữ liệu cá nhân ra ngoài — và thêm một luật của riêng nó: không bao giờ im lặng.


Từ 25/09: bản tin v2. Từ khoảng 13:48 ngày 25/09, .env có TEAMS_DIGEST_PHIEN_BAN=2. Mỗi sáng tác vụ gửi ba thẻ, mỗi thẻ vào một kênh: Tổng hợp (khoá cũ TEAMS_DIGEST_WEBHOOK), Tại cửa hàng (POS) và Online (mọi mã kênh khác POS). Thẻ lỗi vẫn chỉ đi kênh Vận hành/IT. Bố cục khai trong notify/bo_cuc.json. Lượt tự động đầu tiên của v2 là 07:00 ngày 26/09; tới lúc đó mới có lượt gửi tay 13:38 (3 × HTTP 202) và một lượt tác vụ bấm tay 13:54 ([bỏ qua]). Quay về v1: xoá dòng công tắc hoặc đặt 1 — state v1 được ghi song song nên không gửi trùng thẻ Tổng hợp. Công tắc nằm trong .env, nên mọi lệnh teams_digest.py, kể cả chạy tay, đều vào notify/bantin_v2.py (teams_digest.py:651-659).

Năm điều cần nhớ nếu chỉ đọc một mục: (1) Bản tin chạy 07:00 và 09:30 từ thư mục làm việc của checkout chính — đổi nhánh ở đó là đổi code đang chạy thật, và điều đó đã xảy ra chiều 24/09. (2) Chạy tay thì luôn kèm --dry-run: mọi lệnh không có cờ này đều có thể đăng thẻ thật — kể cả --out, cờ chỉ ghi thêm ra file chứ không chặn gửi — trừ --kiem, --kiem-cau-hinh, --explain và --khung --out; --kenh-thu chỉ gửi vào kênh thử. Nhưng --dry-run cũng chỉ là "không chạm mạng": nó vẫn nối kho và vẫn ghi vào log thật. Với v2 (đang bật), --dry-run không ghi ALERT_teams.txt và không gửi thẻ lỗi; với v1, kho lỗi thì vẫn ghi đè cờ cảnh báo thật. (3) Sáng không thấy thẻ thì đọc logs/teams_digest_<ngày>.log, logs/ALERT_teams.txt, logs/teams_digest_state_v2.json, logs/teams_digest_cho_<thẻ>.json (nếu có) và kênh Vận hành. LastTaskResult đúng mã thoát của Python từ khi tác vụ được đăng ký lại lúc 18:13 ngày 23/09; trước đó mọi lỗi hiện thành 0x1. (4) Sửa một chỉ tiêu là sửa ba tới bốn nơi: model dbt lớp bantin, digest_v2.sql, digest_daily.sql (v1 phải bằng v2 — kiểm bằng doi_chieu_v1_v2.py) và measure DAX; riêng Q22 là q22_ky.sql cùng hai model dbt dq_q22_tomtat và dq_summary. (5) Thẻ an toàn về dữ liệu cá nhân nhờ câu SQL, không hoàn toàn nhờ phân quyền — đừng thêm cột nào khi chưa đọc Phần 09.

Khía cạnhBản tin làBản tin không là
Sản phẩmBa thẻ tóm tắt doanh số ngày hôm qua và lũy kế tháng — Tổng hợp, Tại cửa hàng (POS), Online (ngoài POS) — mỗi thẻ vào một kênh Teams, mỗi sáng (v1: một thẻ, một kênh)Một bản thay dashboard — không lọc, không đi sâu, không tương tác
Nguồn sốLớp mart của kho — đúng lớp Power BI đọc — qua lớp bantin (các bảng bt_*) dbt dựng mỗi đêm. Hợp đồng số v2 ở đầu digest_v2.sql; v2 bằng v1 ở 38 khoá. Tám chỉ tiêu của v1 có đối ứng ở đầu digest_daily.sql — bảy với measure DAX, riêng Q22 với hai model dbt; ba chỉ số mới của v2 chỉ có phía SQL (DAX-BANTIN-V2)Một con số "tự nghĩ ra". Nhưng cũng không phải đọc chung một công thức với dashboard — sự thống nhất được giữ bằng hợp đồng viết tay (Phần 06)
QuyềnGọi 10 quan hệ (9 mart + dq.dq_bantin_doichieu; v1 là 6) bằng tài khoản bi_reader — không đọc được thông tin liên lạc của kháchKhông ghi vào kho, không gọi FAST ERP, không đọc file Power BI
Khi hỏngNói thành lời, mỗi kiểu một thẻ. Kênh kinh doanh nhận "dữ liệu chưa về" — kể cả khi kho đã có số nhưng lớp bantin chưa dựng lại hoặc cổng B01–B03 trượt (v2; khi đó kênh Vận hành nhận "Lớp số liệu bản tin chưa sẵn sàng" kèm lý do); các thẻ lỗi kỹ thuật — "đang dựng lại", "không đọc được kho", "không đủ quyền", "không lập được bản tin" — chỉ đi kênh Vận hành/IT (từ 18:11 ngày 23/09)Một kênh Vận hành trống khi một lượt cần đọc kho mà không đọc được. (Kho chết sau khi bản tin đầy đủ đã gửi thì không lượt nào báo — bản tin không phải bộ canh kho.) Nhưng kênh kinh doanh nay im lặng trong những sáng mất kho — đánh đổi có chủ đích, xem Phần 12. Ba kẽ hở khác (lỗi Python, luồng Power Automate hỏng sau khi đã nhận, máy không chạy tác vụ) ghi ở Phần 11
Người đọcNgười nghiệp vụ ở ba kênh kinh doanh; người trực ở kênh Vận hành/ITĐường cảnh báo của đồng bộ đêm — cái đó đi SYNC_ALERT_WEBHOOK, một khoá khác, và hiện chưa được đặt. Đổ lỗi kỹ thuật vào kênh lãnh đạo là hỏng kênh

Năm nguyên tắc — mỗi cái đổi bằng một bài học đã trả giá

Cổng độ tươi chạy trước. Không bao giờ gửi số cũ như số mới. Ngày dữ liệu lấy từ chính lớp mart, không từ cờ trạng thái của lần đồng bộ — vì cờ đó được ghi trước khi lớp mart dựng xong (sự cố HEALTH-FALSE-OK). Chi tiết ở Phần 04.
Im lặng là chế độ hỏng tệ nhất. Đồng bộ chết thì vẫn gửi — gửi thẻ ngắn nói dữ liệu chưa về. Một kênh trống bị đọc thành "chắc hôm nay chưa ai gửi", và không ai đi tìm nguyên nhân. Từ P2.9b, nguyên tắc này được thực hiện theo từng kênh: lỗi kỹ thuật nói với kênh Vận hành, không nói với kênh kinh doanh.
HTTP 200 chưa phải "đã đăng". Lớp gửi luôn đọc nội dung phản hồi, vì có cơ chế trả 200 mà giấu lỗi bên trong. Chi tiết ở Phần 08.
Không dữ liệu cá nhân (luật R8). Câu SQL không chọn tên, số điện thoại, email, địa chỉ hay mã khách. Trên thẻ chỉ có số tổng hợp cùng mã và tên của cửa hàng, kênh — không phải của người.
Một tác vụ riêng, không phải bước thứ năm của đồng bộ đêm. Nếu bản tin là bước cuối của script đồng bộ thì đồng bộ chết là bản tin không bao giờ chạy — đúng sự im lặng cần tránh. Tách riêng còn giữ được một điều: lỗi của bản tin không làm đỏ kết quả của tác vụ đêm, nên không sinh báo động giả ở đó.

Phần 02

Các thành phần và ranh giới giữa chúng

Mười bảy file code và cấu hình, năm loại file sinh ra lúc chạy, bốn luồng Power Automate — ba cho các kênh kinh doanh, một cho kênh Vận hành/IT. Ranh giới quan trọng nhất là đường cắt giữa phần biết về doanh số (teams_digest.py, bantin_v2.py) và phần biết về mạng (teams_client.py). v2 thêm một đường cắt thứ hai: khoi.py biết nội dung, ve_teams.py biết Teams.


Thành phần của Bản tin Teams Bộ lịch Windows gọi teams_digest.py hai lần mỗi sáng. Chương trình đọc cấu hình từ .env, bỏ qua sớm nếu kỳ đã có bản tin đầy đủ, rồi (bản v2 đang bật, qua bantin_v2.py) đọc mười quan hệ của lớp mart và dq bằng tài khoản chỉ-đọc bi_reader, ghi log và file trạng thái, và gửi thẻ qua teams_client.py tới bốn luồng Power Automate: ba thẻ bản tin tới ba kênh kinh doanh, thẻ lỗi tới kênh Vận hành. Bộ lịch Windows SIXDO-DataHub-TeamsDigest 07:00 · 09:30 WakeToRun · cửa sổ ẩn tối đa 15 phút / lượt .env (không vào git) TEAMS_DIGEST_PHIEN_BAN = 2 TEAMS_DIGEST_WEBHOOK TEAMS_DIGEST_WEBHOOK_POS TEAMS_DIGEST_WEBHOOK_ONLINE TEAMS_DIGEST_WEBHOOK_VANHANH BI_READER_PASSWORD TEAMS_DIGEST_MODE URL = thông tin đăng nhập notify/teams_digest.py công tắc = 2 → giao cho bantin_v2.py 1 · mo_log() mở log sau tham số + .env 2 · nen_chay() kỳ đã đủ → [bỏ qua], không nối kho 3 · ket_noi() chỉ đọc · khoá 5 s · câu lệnh 90 s 4 · gate_do_tuoi() độ tươi · (v2) + cổng B01–B03 5 · lay_so_lieu() v1: 1 câu · v2: bt_* + 4 câu 6 · dựng thẻ 2 loại × 3 thẻ · 6 loại thẻ lỗi 7 · nen_gui() · nen_gui_loi() chống trùng theo từng thẻ 8 · gui() | gui_loi() → state (v2) gửi hỏng → lưu payload chờ mã thoát 0 · 1 · 2 · 3 · 4 Kho · datahub-pg loopback · vai bi_reader mart.fact_salesview mart.bt_kpi_ngayv2 mart.bt_khach_ngayv2 mart.bt_kenh_nhomv2 mart.dim_date mart.dim_storechỉ lấy tên mart.dim_channel mart.fact_payment mart.fact_order_biview dq.dq_bantin_doichieuv2 bị từ chối: raw.* · bảng khách gốc không ghi gì vào kho 10 tên gọi · 0 cột PII rời câu SQL 1 dòng logs/ (không vào git) teams_digest_<ngày>.log mỗi lượt nối thêm · UTF-8 teams_digest_state_v2.json trí nhớ theo từng thẻ (v2) teams_digest_state.json dạng v1 · bản sao thẻ Tổng hợp teams_digest_cho_<thẻ>.json thẻ gửi hỏng, chờ gửi lại (v2) ALERT_teams.txt teams_client.py nơi DUY NHẤT chạm mạng tối đa 3 lần · 20 s / lần đọc body trước mã trạng thái không cần Postgres khối JSON · Adaptive Card 1.2 HTTPS POST Power Automate 4 luồng Workflows *.powerplatform.com thành công = 2xx 3 kênh kinh doanh Tổng hợp, POS, Online + "chưa về" Kênh Vận hành chỉ thẻ lỗi từ 23/09 nghiệm thu = mắt người Không có mũi tên nào tới FAST ERP hay Power BI. Lượt theo lịch: thẻ lỗi không bao giờ tới kênh kinh doanh. Bản tin không import mã nạp dữ liệu và không đọc file Power BI. Thẻ lỗi chỉ mang dòng đầu của thông báo lỗi, không mang stack trace.

Mũi tên liền là luồng dữ liệu của một lượt chạy; mũi tên đứt là cấu hình đọc vào lúc khởi động. Đỏ là chỗ chạm mạng ra ngoài máy. Bước 2 và kênh Vận hành có từ commit 9a063c4 (18:11 ngày 23/09); các bảng bt_*, ba kênh kinh doanh và file trạng thái v2 có từ 6dd143a / b92aedf, bật ngày 25/09. Tám ô của cột giữa là thứ tự của v1; v2 giữ cùng thứ tự và thêm hai bước (gửi lại payload chờ, cổng sẵn sàng của lớp bantin).

FileDòngViệcChạm vào
notify/teams_digest.py867Nghiệp vụ v1: mở log, bỏ qua sớm, cổng độ tươi, gọi SQL, dựng 6 loại thẻ, quyết định gửi, chọn kênh, ghi trạng thái. Kèm công tắc (dòng 651-659): TEAMS_DIGEST_PHIEN_BAN=2 thì giao cho bantin_v2.pyKho (đọc) · logs/ (ghi)
notify/bantin_v2.py836Điều phối v2: kiểm bố cục, bỏ qua sớm, gửi lại payload chờ, cổng sẵn sàng, dựng và quyết định theo state riêng từng thẻ; --kiem, --khung, --probeKho (đọc) · logs/ (ghi)
notify/khoi.py · ve_teams.py511 · 7412 khối nội dung trung tính (preset v1 trùng byte thẻ v1) · khối → Adaptive Card 1.2—
notify/bo_cuc.json24Ba thẻ, tên khoá .env của từng thẻ, kênh lỗi, kênh thử — không chứa URL—
notify/teams_client.py207Vận chuyển: POST, đọc "thành công" theo từng loại webhook, thử lại, che URL khi inMạng — và chỉ mạng
notify/sql/freshness.sql13Một câu, một số: ngày dữ liệu mới nhấtmart.fact_sales
notify/sql/digest_daily.sql210v1: một câu, một dòng, một cột jsonb chứa toàn bộ số của thẻ6 bảng mart
5 câu SQL v220 · 258 · 109 · 23 · 12bantin_san_sang (cổng sẵn sàng) · digest_v2 (số, hợp đồng ở đầu file) · digest_v2_khach (khách mới / quay lại) · q22_ky · q22_toan_cuc (chỉ cho --kiem)các bảng bt_* · dq
notify/tests/test_quyet_dinh_gui.py30043 phép kiểm của v1, không cần kho, không chạm mạng; 10 phép dựa trên 8 lần chạy nguyên main() với log, .env, kết nối kho, cổng độ tươi và lớp gửi đều giả — chưa phép nào đi qua nhánh bản tin đầy đủ hay nhánh lỗi quyền—
notify/tests/test_bo_cuc.py · doi_chieu_v1_v2.py577 · 198120 phép của v2 (chạy nguyên main() với kho, mạng, file giả) · so v1 với v2 trên hub thật (cần hub)— · kho (đọc)
infra/register_task_digest.ps1105Đăng ký hoặc gỡ tác vụ Windows. Từ b92aedf: trước khi đăng ký gọi teams_digest.py --kiem-cau-hinh — kiểm bo_cuc.json và khoá của mọi thẻ đang bật có mặt, không in giá trị; không đạt thì cảnh báo nhưng vẫn đăng ký. Thiếu khoá kênh Vận hành chỉ ra một dòng [thông tin] — vẫn không cảnh báoBộ lịch Windows
logs/teams_digest_<ngày>.log—Mỗi lượt chạy nối thêm một khối. File mở ngay sau khi đọc tham số và .env (teams_digest.py:648) — lỗi xảy ra trước đó, ví dụ .env không đọc được bằng UTF-8, không để lại dòng log nàosinh lúc chạy · không vào git
logs/teams_digest_state_v2.json—Trí nhớ của v2: mỗi thẻ một mục (kỳ, loại, mã băm, giờ gửi), bộ đếm "chưa về" dùng chung, thẻ lỗi gần nhất. Ghi nguyên tử sau từng thẻsinh lúc chạy · không vào git
logs/teams_digest_state.json—Dạng v1: kỳ nào, loại thẻ nào, đã gửi lúc nào. Từ v2 là bản sao mục tong_hop của state v2 — để quay về v1 không gửi trùngsinh lúc chạy · không vào git
logs/teams_digest_cho_<thẻ>.json—(v2) Thẻ gửi hỏng được lưu nguyên payload; lượt sau cùng kỳ gửi lại đúng payload đó, không cần khosinh lúc chạy · không vào git
logs/ALERT_teams.txt—Cờ sự cố: có mặt nghĩa là từ lần gửi bản tin thành công gần nhất đã có một lượt gửi hỏng hoặc không đọc được kho — hoặc (v2) lớp bantin chưa sẵn sàng, thiếu khoá của một thẻ, bo_cuc.json hỏng. v2 nối thêm dòng, không ghi đè; chỉ xoá khi một lượt có ít nhất 1 thẻ gửi được, 0 thẻ hỏng, 0 sự cốsinh lúc chạy · không vào git

Số dòng đo bằng wc -l tại 2ed7140, 25/09/2026. File trong logs/ bị loại khỏi git bởi .gitignore dòng 8, 9 và 11 — dòng 11 che cả state_v2 và cho_*, nhưng không che file tạm ….json.tmp lúc ghi nguyên tử (backlog STATE-TMP-GITIGNORE).

Vì sao cắt đôi đúng chỗ đó: teams_client.py là nơi duy nhất chạm mạng, nên là nơi duy nhất phải hiểu bẫy "200 chưa phải đã đăng". Nó không phụ thuộc Postgres, nên lệnh --probe kiểm được đường gửi ngay cả khi Docker đang tắt — đúng lúc người trực cần tách "hỏng kho" khỏi "hỏng đường gửi". Tương tự, teams_digest.py có bộ đọc .env riêng và cố ý không import ingest/db.py, vì file đó kéo theo client của FAST — bản tin không được phụ thuộc vào hệ nguồn.

Phần 03

Một ngày của ba tác vụ nền

Bản tin không gọi đồng bộ đêm và đồng bộ đêm không gọi bản tin. Điểm hẹn chính là ngày lớn nhất trong mart.fact_sales: đồng bộ đẩy nó lên, bản tin đọc nó. v2 còn một điểm hẹn nữa: ngày lớn nhất của bt_kpi_ngay / bt_khach_ngay và ba cổng B01–B03 (Phần 04). Điểm chạm thứ hai là lúc dbt đang dựng lại mart — khi đó bản tin gặp bảng biến mất hoặc khoá quá 5 giây, và lùi ra bằng thẻ "đang dựng lại".


Một ngày của ba tác vụ nền Trục 24 giờ. Đồng bộ đêm bắt đầu 01:00. Bản tin chạy 07:00 và lượt bù 09:30; lượt bù bỏ qua nếu 07:00 đã đăng thẻ đầy đủ. Kiểm sức khoẻ chạy 09:00, 15:00 và 21:00. 00:0004:0008:00 12:0016:0020:0024:00 Đồng bộ đêm · NightlySync 01:00 nạp → dbt run → dbt test → verify cửa sổ nặng 00:30–03:00 Bản tin Teams · TeamsDigest 07:00 lượt chính 09:30 lượt bù 07:00 đã đăng thẻ đầy đủ → [bỏ qua] không nối kho, thoát 0 Kiểm sức khoẻ · Health 09:0015:0021:00

Giờ theo đồng hồ máy (+07). Vạch đứt quanh 01:00 là khung 00:30–03:00 mà dự án tránh chạy truy vấn nặng — quy ước, không có code nào chặn.

Đồng bộ đêmBản tin TeamsKiểm sức khoẻ
Tên tác vụSIXDO-DataHub-NightlySyncSIXDO-DataHub-TeamsDigestSIXDO-DataHub-NightlySync-Health
Giờ01:0007:00 · 09:3009:00 · 15:00 · 21:00
Đánh thức máy (WakeToRun)cócókhông
Cửa sổhiệnẩnhiện
Giới hạn thời gian4 giờ15 phút10 phút
Kiểu đăng nhậpcả ba đều Interactive — chỉ chạy khi có người đang đăng nhập
Khi máy chạy pinkhông khởi động; bị dừng nếu rút điện giữa chừng (mặc định Windows)chạy được — sửa và đăng ký lại lúc 18:13:38 ngày 23/09không khởi động; bị dừng nếu rút điện giữa chừng
Lần chạy gần nhất · đo 25/09 ~16:0025/09 02:04:04 · 0x0 — trễ ba giờ, máy ngủ qua 01:0025/09 13:54:18 · 0x0 — bấm tay, v2 [bỏ qua]. Lượt lịch 07:00:03 gửi v1; 09:30:04 [bỏ qua]. Lần tới: 26/09 07:00, 0 lượt lỡ25/09 15:00:01 · 0x0

Tác vụ chạy code của bất kỳ nhánh nào đang được checkout đã xảy ra 24/09

Tác vụ trỏ tới sixdo-datahub\.venv\Scripts\python.exe và …\notify\teams_digest.py ở checkout chính — đường dẫn tuyệt đối được đóng cứng lúc đăng ký. Nó không biết git là gì: nó chạy file đang nằm trên đĩa. Quy ước từ 23/09: checkout chính ở nhánh p2/phase2-bi-gates-c-d, fast-forward theo từng commit của nhánh làm việc. Đo 25/09 chiều: nhánh đó ở 7749d0e, đi trước main một commit tài liệu. main cục bộ và trên GitHub nay là 2ed7140, đã có đủ notify/ v2. Người chưa chốt chuyển checkout chính sang main hay giữ nhánh vận hành.

Kịch bản hỏng (25/09 trở đi): ai đó checkout một nhánh cũ hay một commit trước 6dd143a ở checkout chính. .env vẫn có TEAMS_DIGEST_PHIEN_BAN=2 nhưng code đó không biết công tắc, nên tác vụ âm thầm chạy v1: kênh POS và Online im lặng, còn kênh Tổng hợp vẫn có thẻ nên không ai thấy lạ. Không có gì báo rằng code production vừa lùi về.

Đã xảy ra thật: 15:38:35 ngày 24/09 checkout chính bị chuyển sang main cũ, không xác định được ai. Phát hiện lúc 19:20, kéo lại lúc 19:24 — kịp trước đồng bộ 01:00 và bản tin 07:00. Suốt bốn giờ đó git status vẫn báo "sạch".

Ngày 23/09 thật sự diễn ra thế nào

Lượt tự động đầu tiên trong đời của tác vụ là 07:00 ngày 23/09, và nó đạt. Nhưng cùng ngày đó phơi ra ba điểm yếu một lúc: hai điểm được vá trọn trước khi hết ngày, còn điểm pin chỉ được vá cho tác vụ bản tin. Đáng đọc như một ca mẫu, vì phần thiết kế ở Phần 05 và 07 là câu trả lời cho chính buổi sáng này.

GiờChuyện gì xảy raBằng chứng
01:00:03 → 01:05:35Đồng bộ đêm chạy xong, ngày dữ liệu tiến lên 22/09logs/sync_2026-09-23.log
07:00:02 → 07:00:25Lượt 07:00 chạy trong lúc máy đang ở chế độ ngủ hiện đại, cắm điện. Quyết định gửi — kỳ mới, thẻ đầy đủ cho ngày 22/09, HTTP 202 ngay lần đầu. Trước đó tác vụ mang mã 0x41303 — "chưa từng chạy"log dòng 1464; file trạng thái gui_luc = 07:00:25; mã băm c49502602622e69d trùng bản chạy thử 06:59
07:34:21Rút điện — máy chạy pinsự kiện Kernel-Power 105
08:59:41Container datahub-pg dừng (lúc đó chính sách khởi động lại còn là no), khoảng 100 giây sau khi máy ra khỏi ngủ đông (07:34:45 → 08:58:01). Mới là tương quan thời gian — nguyên nhân chưa đo đượcbacklog DOCKER-NO-RESTART
09:30Lượt bù không chạy. Máy đang ngủ và chạy pin; tác vụ không khởi động khi chạy pin. Nguyên nhân là pin vẫn là suy luận (nhật ký Task Scheduler đang tắt), nhưng là suy luận mạnh: máy đã thức từ 09:44:31 mà tác vụ vẫn không chạy cho tới khi cắm điện lúc 10:15:24LastRunTime không có mốc 09:30
10:15:24 → 10:15:25Cắm điện lại; một giây sau tác vụ chạy bù (nhờ StartWhenAvailable)sự kiện 105; LastRunTime
10:15:27 → ~10:15:38Kết nối kho hết giờ sau đúng 10 giây → ghi cờ ALERT_teams.txt → đăng thẻ "Không đọc được kho dữ liệu" vào cùng kênh, ngay sau thẻ đúng lúc 07:00, HTTP 202. Python trả mã 3; tác vụ ghi 0x1log dòng 1470–1474; ALERT_teams.txt
~10:42 · 17:50Container được bật lại bằng tay; chiều cùng ngày được tạo lại với restart: unless-stopped và bộ nhớ chia sẻ 1 GBcommit 8f9b21e; docker inspect
18:11 → 18:17Phiên P2.9b vá cả ba điểm yếu: bỏ qua lượt bù khi kỳ đã đủ, thẻ lỗi chỉ sang kênh Vận hành, giữ mã thoát, cho chạy khi dùng pin. Tác vụ được đăng ký lại lúc 18:13:38. Một lượt chạy thật có kiểm soát lúc 18:13:54 (cổng kho giả, --asof 02/09) đăng thẻ "mất kho" vào kênh Vận hành, HTTP 202; file trạng thái và cờ cảnh báo thật được khôi phục sau đó. Người dùng xác nhận lúc 18:22: thẻ có ở kênh Vận hành, không có ở kênh kinh doanhcommit 9a063c4, 109ca30, de90a68; log dòng 1476–1481

Ba điểm yếu, một buổi sáng: (1) chính sách pin mặc định của Windows làm WakeToRun vô tác dụng khi rút điện; (2) thẻ lỗi không đi qua bộ chống lặp, nên một thẻ báo hỏng được đăng sau một thẻ đúng cho cùng một kỳ — người đọc kênh thấy hai thông điệp mâu thuẫn; (3) mã thoát 3 bị tầng PowerShell gộp thành 0x1, trùng với mã "lỗi Python ngoài dự kiến". Cả ba được ghi thành BANTIN-THE-LOI-SAU-THE-DUNG và TASK-PIN-MA-THOAT trong findings-backlog.md, rồi được vá ở Pha 1 của P2.9b — trọn vẹn với điểm (2) và (3); điểm (1) chỉ cho tác vụ bản tin, còn NightlySync và Health vẫn giữ chính sách pin mặc định (TASK-PIN-MA-THOAT, chưa có mốc sửa). Lượt thật có kiểm soát lúc 18:13:54 chạy tay trên 9a063c4 — trước bản 109ca30 vốn chỉ đổi dòng log — và không qua bộ lịch. Bằng chứng qua bộ lịch đến vào ngày 24/09, ở bảng dưới.

Ngày 24/09 — bản vá chạy thật

GiờChuyện gì xảy raBằng chứng
23/09 18:36:35 → 24/09 05:46:52Máy vào ngủ bằng một lệnh chủ động (sự kiện 187 SetSuspendState; dậy theo đường khởi động — dấu hiệu Hibernate), đang chạy pin từ 18:32. Đồng bộ 01:00 rơi; chạy bù 05:55 → 06:02:50, tất cả đạtbacklog MAY-NGU-LENH-SLEEP
07:00:03 → 07:00:18Bản tin đầy đủ kỳ 23/09 → kênh=TEAMS_DIGEST_WEBHOOK, HTTP 202 lần đầu. Lượt tác vụ đầu tiên trên dạng lệnh mới, và nó chạy trong lúc máy đang Modern Standby (06:10 → 07:20) — hẹn giờ trong standby lúc chạy lúc không. Người xác nhận nhìn thấy thẻ trong kênhlog 24/09 dòng 2–6; file trạng thái gui_luc = 07:00:18
07:00:18Cờ ALERT_teams.txt của sự cố 10:15 ngày 23/09 được xoá — theo quy tắc, chỉ một bản tin gửi được mới xoá cờ. Cờ nằm đó gần 21 giờ mà không ai được báoteams_digest.py:709; cờ vẫn không có lúc ~16:00 ngày 25/09
09:30Lượt bù không chạy đúng giờ — máy ngủ 07:21 → 09:54TRẠNG THÁI 24/09 trong CLAUDE.md
09:56:18Container tự lên lại sau khi máy dậy (unless-stopped, RestartCount=0) — bằng chứng thật đầu tiên cho chính sách khởi động lạiTRẠNG THÁI 24/09
10:27Pha 1 được nghiệm thu theo quyết định của người ("Chốt bằng cách b"), khi nen_chay() chưa chặn một lượt thật nàocommit b1c3adc
10:35:43 → 10:35:44Tác vụ chạy (chạy bù hay có người bấm chạy: không xác định được) → [bỏ qua] đã gửi bản tin đầy đủ cho kỳ 2026-09-23 lúc 07:00:18, không nối kho. Bộ lịch ghi 0x0. Lần đầu nen_chay() chặn một lượt thậtlog 24/09 dòng 8–10; Get-ScheduledTaskInfo
15:38:35 → 19:24Checkout chính bị chuyển sang main cũ; kéo lại và fast-forward tới a189055commit aa16f9c
24/09 21:55 → 25/09 02:03:55Máy ở Modern Standby; đồng bộ chạy 02:04 thay vì 01:00, xong 02:11:45, dbt run PASS=46 gồm 5 model bantin mớicommit a0eaa51

Ngày 25/09 — v2 lên sóng

GiờChuyện gì xảy raBằng chứng
02:04:20 → 02:11:45Đồng bộ chạy bù sau Modern Standby; dbt run PASS 46/46; verify.py P1.6 17/17logs/sync_2026-09-25.log
07:00:03Bản tin v1 đầy đủ kỳ 24/09, HTTP 202 lần 1log 25/09
09:30:04[bỏ qua] — lượt 09:30 đầu tiên chạy đúng giờ và dừng trước khi nối kholog 25/09
10:26:38Một URL webhook thật bị dán vào .env.example (file git theo dõi) — đã hoàn tác trước khi stagebacklog ENV-EXAMPLE-URL-THAT
~10:43Code v2 (Pha 3, 6dd143a) vào checkout chính; công tắc còn tắtTRẠNG THÁI 25/09
13:24Thử đường gửi kênh POS và Online, HTTP 202log 25/09
13:38:31Gửi thật v2 với --force cho kỳ 24/09, 3 × HTTP 202 lần 1. Kênh Tổng hợp có hai thẻ cho 24/09 (người đã biết trước khi chọn)log 25/09; teams_digest_state_v2.json
~13:48Công tắc TEAMS_DIGEST_PHIEN_BAN=2 vào .env (file sửa lúc 13:47:45)log 25/09 khối 13:48:01
13:52 → 14:40Nghiệm thu Pha 5 theo quyết định người (Q-g) và Pha 6 (tài liệu)bi/reports/3_report/P2.9_v2.md
13:54:18Trong lúc nghiệm thu: Start-ScheduledTask → v2 [bỏ qua], 0x0 — lượt tác vụ thật đầu tiên đi vào v2Get-ScheduledTaskInfo

Phần 04

Cổng độ tươi và kỳ báo cáo

Hai ngày quyết định mọi thứ phía sau: kỳ báo cáo (asof) — bản tin đang nói về ngày nào — và ngày dữ liệu — kho thật sự có dữ liệu tới ngày nào.


TênTính thế nàoNguồn
Kỳ báo cáo asofHôm qua theo đồng hồ của máy (date.today() − 1 ngày), hoặc giá trị của --asof. Lượt 07:00 và 09:30 cùng một ngày có cùng asof — đó là thứ làm chúng thành "cùng kỳ"teams_digest.py:661 (v1) · bantin_v2.py:660 (v2)
Ngày dữ liệuselect max(order_date) from mart.fact_sales — một câu, chạy trước mọi thứ khác. Không rẻ như tên gọi: nó quét toàn bộ view, vì mart không có index (đo 24/09: 2,6–4,6 giây; EXPLAIN 25/09: 1,98 s và 2,69 s, mỗi lần ghi khoảng 1 GiB temp, chiếm 50–70% thời gian cả lượt v2 — backlog FRESHNESS-TEMP-1GB)freshness.sql:13

Ba khả năng, ba cách xử lý — và một khả năng thứ tư ở v2

Ngày dữ liệu so với kỳ báo cáoThẻVì sao
Rỗng, hoặc nhỏ hơn"Dữ liệu chưa về" — nêu ngày dữ liệu mới nhất và trễ bao nhiêu ngàyCâu SQL doanh số không được chạy. Không có đường nào để một con số cũ lọt lên thẻ
BằngThẻ đầy đủTrường hợp bình thường
Lớn hơnThẻ đầy đủ, chèn thêm dòng "⚠ Hệ thống có đơn ngày … — sau kỳ báo cáo. Bất thường ở nguồn, cần kiểm tra."Với lượt tự động, đồng bộ đêm chỉ nạp tới hôm qua, nên đơn của hôm nay trong kho là điều lạ phải nói ra. Chạy tay --asof cho một ngày đã qua thì trường hợp này luôn xảy ra, và dòng cảnh báo khi đó là báo động giả
(v2) Bằng hoặc lớn hơn, nhưng lớp bantin chưa sẵn sàngBa kênh kinh doanh nhận "dữ liệu chưa về"; kênh Vận hành nhận "Lớp số liệu bản tin chưa sẵn sàng" kèm lý do; cờ cảnh báo được ghiKho đã có số nhưng bảng tổng hợp còn của hôm qua — gửi thì ra số sai. Không có --force nào vượt được

Cổng thứ hai của v2: lớp số liệu có sẵn sàng không

v2 không tính số từ fact_sales mà đọc các bảng mart.bt_* — bảng vật lý dbt dựng lại mỗi đêm. Nếu một đêm dbt run chết giữa chừng, view fact_sales đã tiến lên nhưng bt_* vẫn là số của hôm qua. Vì vậy, sau cổng độ tươi, bantin_san_sang.sql kiểm hai điều: ngày lớn nhất của bt_kpi_ngay và bt_khach_ngay không nhỏ hơn ngày dữ liệu, và ba cổng B01–B03 của dq.dq_bantin_doichieu đều true. Trượt thì thành loại bang_cu (bảng trên). Nguồn: bantin_san_sang.sql:1-11; bantin_v2.py:300-313, 366-382, 748-772.

Vì sao không dùng cờ trạng thái của lần đồng bộ

Bảng raw.sync_run có cột status, và đọc nó trông tự nhiên hơn. Nhưng cờ ok được ghi trước khi dbt run dựng lại lớp mart — nên ok không chứng minh số trên lớp mart đã mới (sự cố HEALTH-FALSE-OK, đã tái hiện đêm 22/09: cờ đúng lúc 01:03:31 trong khi dbt mới tới model 20/40). Thêm nữa, tài khoản bi_reader bị thu hồi mọi quyền trên schema raw. mart.fact_sales là một view trên fact_order_line nối fact_order (kèm dim_product), nên ngày lớn nhất của nó chỉ tiến lên khi dbt đã dựng lại các bảng đó và chính view — đêm 23/09 là model 32/41 lúc 01:03:54, khoảng một phút trước khi dbt run xong; mọi bảng câu SQL bản tin đọc đều đã xong trước mốc đó. Đó là tín hiệu đúng cho v1. Với v2 thì chưa đủ — các bảng bt_* là bảng vật lý dựng riêng — nên có cổng thứ hai ở trên.

Vì sao cố ý không lọc is_excluded ở cổng này

Để khớp từng byte với measure [Ngày dữ liệu] của Power BI, vốn cũng không lọc. Nếu bản tin lọc mà dashboard không, hai nơi sẽ khai hai "ngày dữ liệu" khác nhau cho cùng một kho. Các chỉ tiêu doanh thu thì có lọc — ở câu SQL thứ hai, không phải ở đây.

Ba điều cổng này không nói

Điều dễ hiểu nhầmThực tế
"Có dữ liệu tới ngày D" nghĩa là ngày D đã đủKhông. Nó chỉ nói kho có ít nhất một dòng hàng ngày D — bất kỳ dòng nào, kể cả dòng trả, bao bì 0đ hay đơn bị loại. Đồng bộ đêm cố ý nạp chồng hai ngày để bắt chứng từ đóng muộn, nên số của ngày D còn có thể nhích lên ở đêm sau. Thẻ 07:00 là số tại lần đồng bộ 01:00; phần về muộn sẽ nằm trong lũy kế tháng của thẻ hôm sau (trừ khi D là ngày cuối tháng — thẻ hôm sau mở tháng mới, nên phần đó không hiện trên thẻ nào), và thẻ ngày D không được đăng lại (Phần 05). Độ lớn của phần về muộn chưa được đo
Mọi đơn đều được tính vào ngày dữ liệuView mart.fact_sales không chứa đơn không có dòng hàng (freshness.sql:12). Một ngày chỉ có đơn rỗng sẽ không đẩy ngày dữ liệu lên
Kỳ nào rồi cũng có thẻ đầy đủKhông. Ít nhất năm đường làm một kỳ mất thẻ vĩnh viễn, vì chỉ có hai lượt và sáng hôm sau asof đã sang ngày khác: (1) 07:00 báo "chưa về", nhưng đồng bộ chỉ xong sau 09:30; (2) máy ngủ qua cả hai lượt và chỉ chạy bù sau nửa đêm; (3) kho không đọc được ở cả hai lượt — thẻ lỗi chỉ đi kênh Vận hành và không ghi trạng thái bản tin; (4) gửi thất bại ở cả hai lượt — với v2, lượt 07:00 hỏng thì payload được lưu và lượt 09:30 gửi lại đúng payload đó, không cần kho; (5) (v2) lớp bantin chưa sẵn sàng ở cả hai lượt. Kỳ báo cáo cũng không có khái niệm ngày nghỉ: thứ Bảy, Chủ nhật, ngày lễ là kỳ như mọi kỳ

Phần 05

Quyết định có gửi hay không

Hai lượt mỗi sáng nghĩa là mỗi kỳ có thể được dựng thẻ hai lần. Ba hàm quyết định. nen_chay() luôn chạy trước — có nối kho không. Sau đó mỗi lượt đi đúng một trong hai nhánh: nen_gui() nếu đọc được kho — bản tin có được đăng không; hoặc nen_gui_loi() nếu nối hay đọc kho lỗi — thẻ lỗi có được đăng không. Cả ba đọc cùng một file trạng thái; riêng nen_gui() còn so mã băm của thẻ. v2 giữ đúng ba luật đó nhưng áp theo từng thẻ: nen_chay_v2(), nen_gui() của v1 chạy trên mục riêng của thẻ, nen_gui_loi_v2(); tất cả đọc teams_digest_state_v2.json (bantin_v2.py:262-279, 795-797).


File trạng thái v1 — nay là bản sao của thẻ Tổng hợp

logs/teams_digest_state.json
{
 "asof": "‹YYYY-MM-DD›",          kỳ của lượt gần nhất đã ghi
 "loai": "day_du" | "chua_ve",   chỉ hai loại này được ghi — thẻ lỗi không bao giờ ghi
 "hash": "‹16 ký tự hex›",        sha256 (v1: khối JSON gửi đi · v2: dữ liệu + mục bố cục của thẻ)
 "gui_luc": "‹thời điểm đăng›",   giữ nguyên nếu lượt này quyết định KHÔNG gửi
 "chua_ve_lien_tiep": ‹n›,        số kỳ CÓ LƯỢT CHẠY liên tiếp chưa về — kỳ máy tắt cả ngày không được đếm
 "loi": {"asof", "loai", "gui_luc"} thẻ lỗi gần nhất đã gửi — chỉ có sau lần gửi thẻ lỗi đầu tiên
}

Ở v1, mã băm phủ toàn bộ thẻ. Hai kỳ khác nhau luôn ra hai mã khác nhau, vì ngày của kỳ nằm trong thân thẻ; hai lượt trong cùng một buổi sáng với cùng dữ liệu ra cùng một mã. Mã băm chỉ được so trong cùng một kỳ.

File trạng thái v2 — một mục cho mỗi thẻ

logs/teams_digest_state_v2.json
{
 "tong_hop" | "pos" | "online": {"asof", "loai", "hash", "gui_luc"}   mỗi thẻ một mục
 "chua_ve_lien_tiep": ‹n›,        bộ đếm dùng chung, đếm theo kỳ
 "loi": {"asof", "loai", "gui_luc"} thẻ lỗi gần nhất
}

File được ghi nguyên tử (ghi file tạm rồi os.replace) sau từng thẻ, và chép mục tong_hop sang file dạng v1. Mã băm tính trên dữ liệu + mục bố cục, không tính trên thẻ đã dựng, vì dòng đếm ngược chứng chỉ TLS trên thẻ đổi mỗi ngày. Thẻ gửi hỏng được lưu nguyên ở teams_digest_cho_<thẻ>.json; lượt sau cùng kỳ gửi lại đúng payload đó, không cần kho; payload của kỳ khác bị bỏ. Giới hạn đã biết: quay về v1 giữa một kỳ đang "chưa về" có thể nhắc thêm một lần. Nguồn: bantin_v2.py:25-32, 219-254, 484-536.

Trước khi nối kho — nen_chay()

Nếu file trạng thái nói kỳ này đã có bản tin đầy đủ, lượt chạy in [bỏ qua] và thoát 0 trước khi nối kho. Một buổi sáng bình thường, lượt 09:30 dừng ở đây. Lý do phải dừng trước khi nối: lượt 10:15 ngày 23/09 nối kho đúng lúc container tắt, rồi rơi vào nhánh lỗi — không nối thì không có nhánh lỗi nào để rơi vào. Được miễn: --force, --dry-run, --out, --explain; --probe thì không đi qua hàm này (teams_digest.py:572-596, gọi ở :748). v2 (bantin_v2.py:262-270, gọi ở :717) miễn thêm --kiem, --kenh-thu, --url; chỉ [bỏ qua] khi mọi thẻ đang chọn đã đủ — thiếu thẻ nào thì chỉ dựng thẻ đó.

Bản tin — nen_gui(), theo đúng thứ tự nó xét

#Điều kiệnGửi?Lý do thiết kế
1Có --forcegửiCửa thoát cho người. Tạo thẻ thứ hai trong kênh — hầu như không bao giờ là việc đúng
2Cùng kỳ · lượt trước là chưa về · lượt này là đầy đủgửiLý do tồn tại của lượt 09:30. Đêm đồng bộ muộn thì 07:00 báo chưa về, 09:30 đăng bản thật. Chặn chỗ này thì ngày đồng bộ muộn không bao giờ có bản tin
3aLượt này chưa về · cùng kỳ · cùng mã băm với lượt trướckhôngĐã báo rồi — hoặc lượt 07:00 đã cố ý im theo 3b; khi đó dòng log "đã báo … lúc" in giờ của kỳ nhắc trước, không phải của kỳ này
3bLượt này chưa về · đã là kỳ thứ n liên tiếp, n > 3 và n không chia hết cho 3khôngGiảm nhiễu — xem bảng lịch nhắc bên dưới
3cLượt này chưa về, các trường hợp còn lạigửiTới lượt nhắc
4Cùng kỳ · cùng loại · cùng mã bămkhôngLớp phòng thủ thứ hai. Từ 18:11 ngày 23/09, lượt 09:30 của một buổi sáng bình thường đã dừng ở nen_chay(); hàng này (và hàng 6) chỉ còn chạm được khi chạy tay với --out hoặc --dry-run
5Kỳ mớigửiLượt 07:00 của một buổi sáng bình thường dừng ở đây
6Cùng kỳ · cùng loại đầy đủ · nội dung đổikhôngChặn đăng trùng (vá ngày 23/09). Hai thẻ khác số trong một buổi sáng làm người đọc mất tin cả hai, trong khi phần chênh — vài đơn về muộn — gần như không đổi quyết định nào

Nguồn: teams_digest.py:521-569. test_bo_cuc.py của v2 (120 phép, 21/21 đột biến bị bắt) kiểm bộ đếm dùng chung; có phủ nhánh 3b hay không thì không xác định được. Trong bộ v1: bảy trong tám hàng có phép kiểm (test_quyet_dinh_gui.py:70-83 và nhóm chạy nguyên main()); riêng nhánh giảm nhiễu 3b chưa có. Cả bộ đếm dem_chua_ve() lẫn việc ghi trạng thái khi không gửi cũng không có phép kiểm nào: một agent soát đã thử đổi bộ đếm sang đếm theo lượt, bỏ ghi trạng thái khi không gửi, và bỏ hẳn 3b — bộ kiểm thử vẫn 43/43. Lịch nhắc bên dưới suy từ code, chưa được kiểm tự động.

Lịch nhắc khi dữ liệu chưa về nhiều ngày

Kỳ chưa về liên tiếp123456789
Có thẻ "chưa về"?cócócó——có——có

Kịch bản mà lịch này sinh ra để đỡ: sau 10/10/2026, nếu chứng chỉ TLS không được gia hạn, dữ liệu ngừng chảy và thẻ "chưa về" sẽ lặp mỗi sáng. Sau một tuần người ta tắt thông báo kênh, và một bất thường mới sẽ không ai đọc. Nhưng lịch này không bao giờ tắt hẳn: im lặng bị đọc thành "chắc chưa ai gửi".

Hai chi tiết giữ bộ đếm đúng: trạng thái được ghi cả khi quyết định không gửi — nếu chỉ ghi lúc gửi thì bộ đếm đứng yên và lịch nhắc kẹt mãi ở một kỳ; và bộ đếm tăng theo kỳ, không theo lượt — đếm theo lượt thì hai lượt mỗi ngày làm lịch chạy nhanh gấp đôi dự định. Trạng thái không được ghi khi gửi thất bại (để lượt sau thử lại) và khi chạy --dry-run. Ở v2, thẻ gửi hỏng còn được lưu nguyên payload ở teams_digest_cho_<thẻ>.json; lượt sau cùng kỳ gửi lại đúng payload đó để ba thẻ không lệch số nhau.

Thẻ lỗi — nen_gui_loi() và gui_loi()

Điều kiệnGửi?Lý do thiết kế
Kỳ này đã có bản tin đầy đủ (v2: ở mọi thẻ đang chọn)khôngThẻ lỗi sau thẻ đúng chỉ làm người đọc không biết tin cái nào. Chỉ ghi log; cờ cảnh báo có với mất kho, thiếu quyền và lỗi máy chủ, không có với "đang dựng lại". Trong lượt tự động, hàng này gần như không chạm tới vì nen_chay() đã dừng trước khi nối kho
Cùng kỳ, cùng loại lỗi đã gửikhôngKho tắt cả buổi sáng thì 07:00 và 09:30 không nhắc hai lần cùng một chuyện
Loại lỗi khác, hoặc kỳ khácgửi"Mất kho" rồi "không đủ quyền" là chuyện mới
Thiếu khoá TEAMS_DIGEST_WEBHOOK_VANHANHkhông gửi đâu cảKhông bao giờ rơi sang kênh kinh doanh. Nối thêm một dòng vào cờ cảnh báo

Nguồn: teams_digest.py:598-616 và :713-741; v2 bantin_v2.py:273-279 và :453-481. Thẻ lỗi chỉ đi TEAMS_DIGEST_WEBHOOK_VANHANH — quyết định Q-e của người dùng ngày 23/09. Thẻ "dữ liệu chưa về" không phải thẻ lỗi, nên vẫn vào kênh kinh doanh.

Trước 18:11 ngày 23/09, thẻ lỗi đi đường tắt: năm lời gọi gửi trong bốn nhánh bắt lỗi đi thẳng vào kênh kinh doanh, không chống lặp, không ghi trạng thái. Hệ quả thật là thẻ "không đọc được kho" lúc 10:15 đăng ngay sau thẻ đúng lúc 07:00 (Phần 03). Bảng trên là bản vá cho chính sự cố đó. Cái giá của nó: một sáng mất kho, kênh kinh doanh không nhận gì cả — xem Phần 12.

Phần 06

Thẻ đầy đủ — từng dòng lấy từ đâu

Thẻ là một Adaptive Card phiên bản 1.2 — mức bảo thủ do code tự chọn (teams_digest.py:279; v2 ve_teams.py:71, cũng phiên bản 1.2 — chú thích ghi "Teams mobile chỉ render chắc tới 1.2"). Tài liệu Microsoft Learn hiện ghi Teams trên điện thoại hỗ trợ tới 1.6, nên 1.2 là lựa chọn an toàn chứ không phải giới hạn của Teams; muốn nâng thì phải thử thật trên điện thoại và qua Workflows trước. Khung dưới đây là cấu trúc của thẻ v1 (nay là preset v1 trong bo_cuc.json), không phải số thật: chỗ trong dấu ‹ › là giá trị lấy từ kho lúc chạy. Ba thẻ v2 ở ngay dưới khung.


1Đầu thẻ. "Dữ liệu đến" là ngày của cổng độ tươi (Phần 04), không phải ngày chạy. Nếu kho có đơn sau kỳ báo cáo, một dòng ⚠ được chèn ngay dưới — bất thường ở nguồn, cần kiểm tra.
2Ngày báo cáo. Hai dòng so sánh ghi rõ ngày được so. Chữ tăng / giảm luôn đi kèm màu, không bao giờ để màu nói một mình. Kỳ trước bằng 0 thì thẻ viết "không so sánh được" thay vì chia cho 0.
3Lũy kế tháng in luôn số ngày và số ngày cuối tuần của cả ba cửa sổ, để người đọc tự thấy phần trăm có đang so hai kỳ không bằng nhau hay không. Khối này không có dòng Giá trị trả.
4Cảnh báo lịch chỉ hiện khi cửa sổ tháng trước hoặc năm trước bị kẹp ngắn hơn kỳ này — ví dụ lũy kế tới 29/02/2024 là 29 ngày, cùng kỳ năm trước chỉ có 28.
5Cửa hàng: 5 cao nhất và 3 thấp nhất theo doanh thu lũy kế; cửa hàng có tổng bằng 0 bị bỏ khỏi danh sách nhưng vẫn tính vào mẫu số N.
6Kênh: các mã kênh chưa được nghiệp vụ xác nhận tên (Q30: SPE, LAZ, DSCWEB, DSCAPP, W29) hiện đúng mã thô kèm nhãn "(mã thô)", thay vì đoán tên. Riêng đơn không có mã kênh sẽ hiện "Chưa quy được kênh (mã thô)" — một nhãn mô tả bị gắn nhầm hậu tố; chưa xảy ra trên thẻ nào.
7Q22 chỉ xuất hiện khi kỳ này có đơn trả bằng thẻ trả trước, và giải thích vì sao các đơn đó hiện giá hàng ~0đ mà không làm doanh thu phía trên bị cộng trùng.
8Chân thẻ: dòng chứng chỉ TLS chỉ hiện khi còn ≤ 30 ngày (hiện đang hiện). Câu "không chuyển ra ngoài công ty" có trên mọi thẻ bản tin và thẻ lỗi; riêng thẻ kiểm tra kết nối --probe không có — thẻ đó không chứa con số nào.

Ba thẻ v2

Thẻ · kênhSo với khung v1 ở trên
Tổng hợpThêm khối Theo nhóm kênh, Giá trị đơn bán, Khách mới / quay lại
Tại cửa hàng (POS)Thêm Giá trị đơn bán, Trả / đổi hàng (ba dòng: hàng khách trả, hàng đổi ra, trả ròng), Khách mới / quay lại; không có khối Kênh
Online (ngoài POS)Thêm Giá trị đơn bán, Khách mới / quay lại; không có khối Cửa hàng và Q22

Số khách luôn hiện kèm dòng "phủ" — ở thẻ Online, số khách chỉ phủ khoảng 31% doanh thu đơn bán. Khối ghép theo bo_cuc.json; nội dung ở khoi.py, dạng Teams ở ve_teams.py. Thẻ POS lọc theo mã kênh nên gồm cả đơn kênh POS ở các kho tên ECOM - … (5,60% doanh thu POS tháng 9 — câu hỏi nghiệp vụ mở, POS-TAI-CUA-HANG-ECOM).

Hợp đồng số: SQL của bản tin ↔ measure của Power BI

Luật R11 cấm hai nơi chứa logic. Bản tin buộc phải có SQL riêng, nên thay vào đó nó ghi hợp đồng thành văn ở đầu digest_daily.sql (dòng 5–24) cho v1 và digest_v2.sql (dòng 27–57) cho v2, và bi/tools/build_model.csx:5-14 mang chú thích trỏ ngược lại cả hai. Người sửa một bên sẽ thấy bên kia.

Dòng trên thẻSQL của bản tinĐối ứng Power BIĐã đối chiếu?
Dữ liệu đếnmax(order_date) trên mart.fact_sales — cố ý không lọc is_excluded[Ngày dữ liệu] = MAX + REMOVEFILTERS, cũng không lọckhớp
Doanh thusum(amount), is_excluded = false[Doanh thu]khớp
Số đơncount(distinct order_id)[Số đơn]khớp
Số lượng bánsum(qty) chỉ line_kind = 'SALE' — bỏ bao bì (PACKAGING), quà tặng 0đ (GIFT) và dòng 0đ nhóm hàng U (DPNV)[Số lượng bán]khớp
Giá trị trả · Tỉ lệ trả-sum(amount) với doc_kind = 'HDF'; chia cho doanh thu 'HDA'[Giá trị trả] · [Tỉ lệ trả]Giá trị trả khớp · Tỉ lệ trả chưa đo
Lũy kế thángCửa sổ từ ngày 1 tới asofCùng measure, lọc cùng khoảng ngàydoanh thu, số đơn khớp · số lượng bán, tỉ lệ trả chưa đo
So cùng kỳ tháng trướcCửa sổ lịch kẹp tay trong CTE winKhông có measure nàokhông có đối chứng
So cùng kỳ năm trướcCửa sổ lịch kẹp taySAMEPERIODLASTYEARhai thuật toán
Q22 trên thẻThanh toán payment_code = 'TT', phạm vi lũy kế tháng, cố ý không lọc is_excludeddq_q22_tomtat là số toàn cụckhác phạm vi
(v2) Giá trị TB 1 bill (đơn bán) · Giá TB 1 sản phẩm · Khách mới / quay lạidigest_v2.sql, digest_v2_khach.sqlKhông có measure nàokhông có đối chứng (DAX-BANTIN-V2)

"Khớp" ở đây là phép đối chiếu 10/10 ngày 22/09 (SQL lúc 14:15, DAX lúc 14:19 trên cùng trạng thái dữ liệu, bi/reports/3_report/VIEC_0c_bang_chung.md). Ngày 25/09 doi_chieu_v1_v2.py so 38 khoá, v1 = v2 tuyệt đối ở 5 kỳ — chứng minh v2 bằng v1, không chứng minh bằng DAX. Phép đó so câu SQL với measure, không so JSON của thẻ với dashboard; thẻ làm tròn tới hai chữ số thập phân của "tỷ", nên lệch nhỏ không nhìn thấy trên thẻ; và thứ hạng cửa hàng, kênh chưa từng được đối chiếu.

Bốn điều thẻ nói ít hơn người đọc tưởng

DòngNgười đọc dễ hiểu làThực tế
So với cùng ngày tháng trước / năm trướcSo hai ngày tương đươngSo cùng ngày trong tháng, không cùng thứ. Thẻ 07:00 ngày 23/09 so Thứ Ba 22/09/2026 với Thứ Bảy 22/08/2026 và Thứ Hai 22/09/2025, rồi in "giảm 49,32%" và "tăng 44,51%". Không tách được phần nào của các con số đó là do thứ trong tuần — và thẻ không nói cho người đọc biết. Cờ lệch lịch chỉ áp cho số ngày của lũy kế tháng
Giá trị trả · Tỉ lệ trảHàng khách trả lại, luôn là số dươngLà tổng tiền của chứng từ trả HDF, ròng sau đổi hàng. Ở cửa hàng, phần lớn HDF là đổi hàng, nên số này có thể âm: đo ở POS 16–22/09 là −17.117.452đ (backlog GIA-TRI-TRA-AM). Thẻ POS v2 tách thành ba dòng: hàng khách trả (gộp), hàng đổi ra, trả ròng
Số của ngày hôm quaSố cuối cùng của ngày đóSố tại lần đồng bộ 01:00. Chứng từ đóng muộn vào kho đêm sau và không làm thẻ được đăng lại — xem Phần 04
"Trong kỳ có N đơn thanh toán bằng thẻ trả trước"Số trong ngày hôm qua"Kỳ" ở khối Q22 là lũy kế tháng, không phải ngày — khối này đứng ngay sau khối Kênh, cũng là lũy kế tháng

Vì sao Q22 cố ý không lọc is_excluded: hai model dbt tính Q22 — dq_q22_tomtat và dq_summary — đều không lọc. Thêm điều kiện ở đây sẽ sinh hai con số Q22 khác nhau trên cùng một hệ, đúng ở chỉ số nhạy cảm nhất của dự án (cổng M5.7 đang đỏ có chủ đích, luật R12). Muốn lọc thì phải sửa cả hai model dbt cùng lúc (digest_daily.sql:21-24; v2 q22_ky.sql:4-5, cũng cố ý không lọc).

Phần 07

Tám loại thẻ, năm mã thoát

Mỗi kiểu hỏng có một thẻ riêng với một câu riêng, vì thông điệp sai đẩy người trực đi sai hướng — và từ 23/09, một kênh riêng. Bảng dưới là v1, đọc thẳng từ main() ở commit 109ca30, theo đúng thứ tự Python xét các nhánh. v2 giữ đúng thứ tự nhánh (_phan_loai_loi, bantin_v2.py:385-409) và thêm các hàng đánh dấu (v2).


Tình huốngThẻKênhMã thoátCờ ALERT_teams.txtFile trạng thái
Kỳ này đã có bản tin đầy đủ (v2: ở mọi thẻ đang chọn)không có — in [bỏ qua], không nối kho—0không đụngkhông ghi
Kho có dữ liệu tới đúng kỳ báo cáoĐầy đủ — day_dukinh doanh0xoá khi gửi đượcghi
Kho có đơn sau kỳ báo cáoĐầy đủ + dòng ⚠ "bất thường ở nguồn"kinh doanh0xoá khi gửi đượcghi
Ngày dữ liệu < kỳ báo cáo, hoặc kho rỗng"Dữ liệu chưa về" — chua_ve, nói rõ trễ bao nhiêu ngàykinh doanh0xoá khi gửi đượcghi, bộ đếm kỳ chưa về tăng
Bảng không tồn tại · chờ khoá quá 5 s · câu lệnh quá 90 s"Kho dữ liệu đang được dựng lại" — dang_dung, hẹn lượt 09:30Vận hành0không ghi, không xoáchỉ khoá loi
SQLSTATE 42501 — bị từ chối quyền"Không đủ quyền đọc kho" — loi_quyen, kèm lệnh sửaVận hành3ghichỉ khoá loi
Lỗi lúc mở kết nối — SQLSTATE rỗng hoặc lớp 08. Gồm cả những lỗi máy chủ trả về ngay lúc bắt tay: sai hoặc thiếu mật khẩu (28P01), quá số kết nối (53300), CSDL đang khởi động (57P03) — psycopg 3.3.5 không gắn SQLSTATE cho lỗi lúc mở kết nối (đo 23 và 25/09)"Không đọc được kho dữ liệu" — mat_hub, gợi ý DockerVận hành3ghichỉ khoá loi
Máy chủ trả lỗi khi đang chạy câu lệnh — hết đĩa 53100, hết bộ nhớ 53200, mọi lỗi DB khác"Không lập được bản tin" — loi_hub, không đoán nguyên nhân, kèm nguyên văn lỗiVận hành3ghichỉ khoá loi
Lỗi kho, nhưng thiếu khoá kênh Vận hànhkhông cókhông kênh nào — không bao giờ sang kênh kinh doanh3 (0 nếu "đang dựng lại")nối thêm một dòng "thiếu khoá"không ghi
Gửi không thành công sau các lần thử— (không đăng được)—2 (3 nếu là thẻ mất kho, thiếu quyền hay lỗi máy chủ; thẻ "đang dựng lại" gửi hỏng vẫn là 2)ghi (v1, với thẻ lỗi: ghi đè mất nội dung lỗi gốc · v2: nối thêm dòng)không ghi → lượt sau thử lại (v2: lưu teams_digest_cho_<thẻ>.json, lượt sau gửi lại đúng payload đó)
Thiếu khoá TEAMS_DIGEST_WEBHOOK (bản tin hoặc probe)v1: không có — chỉ có dòng [LỖI] thiếu TEAMS_DIGEST_WEBHOOK · v2: thẻ đó không gửi, các thẻ khác vẫn gửikhông kênh nào2v1: không ghi · v2: ghi "thẻ <id> KHÔNG gửi được: thiếu <khoá>"không ghi → lượt sau thử lại
(v2) Lớp bantin chưa sẵn sàngKinh doanh nhận "chưa về"; Vận hành nhận bang_cu kèm lý docả hai0 (2 nếu gửi hỏng)ghi, không xoághi như "chưa về"
(v2) bo_cuc.json hỏng khi công tắc bậtcau_hinhVận hành4ghikhông ghi
(v2) --kiem / --kiem-cau-hinh không đạt, hoặc [từ chối] (cờ chỉ-v2 khi công tắc chưa bật)không có—4khôngkhông ghi
Lỗi Python ngoài dự kiếnthường là không có thẻ — chỉ có traceback trong log. Nếu traceback nằm sau dòng [kết quả] ok=True (ví dụ ghi file trạng thái hỏng) thì thẻ đã đăng mà trạng thái chưa ghi, nên lượt sau có thể đăng trùngkhông kênh nào, hoặc đã đăng1khôngkhông ghi

Nguồn v1: teams_digest.py:748-751 (bỏ qua), :769-825 (các nhánh bắt lỗi), :679-712 (gui, xoá cờ chỉ khi loại là day_du hoặc chua_ve), :713-741 (gui_loi), :842-851 (ghi trạng thái), :855-867 (mã 1). Nguồn v2: bantin_v2.py:40, 61, 385-420, 682-700, 748-772, 803-820. Loại probe chỉ chạy khi người gõ --probe và không xoá cờ; v2 thêm --probe --chi pos/online và --probe --kenh vanhanh. Ghi đè nội dung lỗi gốc khi gửi hỏng (backlog ALERT-GHI-DE-CHI-TIET) đã sửa ở v2, còn ở v1.

Thứ tự các nhánh là một phần của thiết kế kiểm được một nửa

Lỗi hết đĩa (DiskFull) là lớp con của OperationalError, tức là cùng họ với "mất kết nối". Nếu chỉ bắt theo lớp, một ổ đĩa đầy sẽ được báo là "khả năng Docker đang tắt". Chương trình tách hai thứ bằng SQLSTATE: rỗng hoặc bắt đầu bằng 08 là lỗi trong lúc mở kết nối — kể cả khi máy chủ đã trả lời bằng một lỗi FATAL như sai mật khẩu hay quá số kết nối; còn lại là lỗi máy chủ trả về khi đang chạy câu lệnh. Lỗi này lộ ra nhờ một phép thử tiêm lỗi thật.

Cặp nhánh nhạy thứ tự nhất là "đang dựng lại" với OperationalError: chờ khoá quá hạn (LockNotAvailable) và câu lệnh quá giờ (QueryCanceled) đều là lớp con của OperationalError. Đảo hai nhánh đó thì một đêm dbt chạy muộn sẽ ra thẻ "Không lập được bản tin" (loi_hub, SQLSTATE 55P03 hoặc 57014) với mã 3, thay cho thẻ "đang dựng lại" mã 0 — một agent soát đã đo điều này trên bản sao code ngày 25/09.

Giới hạn của lưới kiểm: test_quyet_dinh_gui.py có ba phép giữ các quan hệ lớp mà thứ tự nhánh dựa vào, và 10 phép kiểm trên 8 lần chạy nguyên main() với kho, mạng và file giả — trong đó 4 lần tiêm lỗi DB: một UndefinedTable, ba OperationalError. Nhưng thẻ "đang dựng lại" chỉ được thử bằng UndefinedTable — một lỗi không thuộc OperationalError — nên đảo đúng cặp nhạy nhất ở trên thì bộ kiểm thử vẫn xanh. test_bo_cuc.py của v2 cũng chỉ tiêm OperationalError, không có LockNotAvailable hay QueryCanceled — cặp nhánh nhạy nhất vẫn chưa được kiểm ở cả hai bản.

Năm mã thoát — tới được bộ lịch từ 18:13 ngày 23/09

Chương trình phân biệt 0 (xong, kể cả khi cố ý không gửi hay bỏ qua), 1 (lỗi ngoài dự kiến), 2 (gửi thất bại, thiếu URL, hoặc tham số dòng lệnh sai), 3 (lỗi kho), và (v2) 4 (cấu hình hoặc phép kiểm sai: --kiem-cau-hinh / --kiem không đạt, bo_cuc.json hỏng, dùng cờ chỉ-v2 khi công tắc chưa bật). 0x4 qua bộ lịch chưa đo. Tác vụ gọi Python qua powershell.exe -Command, và PowerShell 5.1 ở chế độ đó không tự chuyển tiếp mã thoát của chương trình con. Commit 9a063c4 thêm ; exit $LASTEXITCODE vào cuối lệnh.

Python thoát với01234 (v2)
Dạng lệnh cũ → LastTaskResult0x00x10x10x1—
Dạng lệnh mới, đăng ký 18:13:380x00x10x20x30x4 — chưa đo

Đo ngày 23/09/2026 bằng chính .venv\Scripts\python.exe với sys.exit(n), gọi qua đúng dạng tham số của tác vụ ở mỗi phiên bản.

Còn lại khi trực: dạng lệnh mới đã chạy thật qua bộ lịch — lượt 10:35 ngày 24/09 ghi 0x0 (lượt bỏ qua); 25/09 09:30:04 và 13:54:18 cũng 0x0, 0 lượt lỡ lúc ~16:00. Mã 0x1 của lượt 10:15 ngày 23/09 sinh ra từ dạng lệnh cũ, nên không đọc được là 1 hay 3. Các nhánh mất kho, thiếu quyền và lỗi máy chủ luôn trả 3 kể cả khi gửi thẻ lỗi cũng hỏng; nhánh "đang dựng lại" thì trả mã của lần gửi. Và trường NumberOfMissedRuns không đáng tin để giám sát: đo lúc 18:00 ngày 23/09 nó là 0 dù lượt 09:30 đã lỡ (lượt đó chạy trễ lúc 10:15); lúc 10:27 ngày 24/09 nó là 1 trong khi lượt 09:30 chưa chạy (TRẠNG THÁI 24/09), rồi về 0 sau lượt 10:35. Lịch sử Task Scheduler đang tắt nên không có bản ghi nào khác.

Phần 08

Lớp gửi: 200 chưa phải đã đăng

teams_client.py gửi cùng một khối JSON tới hai loại webhook, nhưng mỗi loại báo "thành công" một kiểu. KetQuaGui.ok = True chỉ được trả khi nội dung phản hồi khớp đúng hình dạng thành công của loại đó.


Workflows — Power AutomateConnector cổ điển — Office 365
Nhận ra bằng tên máylogic.azure.com · powerplatform.com · powerautomate.comwebhook.office.com · office365.com
Thành côngHTTP 202, nội dung rỗng (chấp nhận mọi mã 2xx)HTTP 200 và nội dung đúng chuỗi "1"
Bị chặn tốc độHTTP 429 — đúng mãHTTP 200 nhưng nội dung chứa lỗi 429
Tình trạngĐang dùng. URL thật của SIXDO là dạng mới *.environment.api.powerplatform.com, gặp ngày 22/09/2026Microsoft đã tắt 18–22/05/2026. Code giữ lại vì chi phí bằng 0 và để phát hiện nếu tenant còn sót

Thứ tự đọc một phản hồi — _doc_ket_qua()

Trước mọi nhánh mã trạng thái: nội dung có chữ throttle, rate limit hay too many requests → bị chặn tốc độ. Rồi đến số 429 — nhưng chỉ khi nó đứng đúng vị trí một mã trạng thái ("status": 429, statusCode=429, HTTP 429), không phải khi nó tình cờ nằm trong một mã định danh như 8f429c1d. Rồi đến JSON có khoá "error".
Sau đó mới xét theo nhánh: connector cần đúng 200 + "1"; workflow cần 2xx; nhánh không nhận ra được chỉ chấp nhận hai hình dạng đã biết (202, hoặc 200 với nội dung rỗng hay "1").
Gửi tối đa 3 lần (lần đầu và 2 lần thử lại), mỗi lần chờ phản hồi 20 giây, giãn cách 2 s rồi 4 s. Khi nhận HTTP 429 thì chờ theo Retry-After tính bằng giây, không có trần — chỉ giới hạn 15 phút của tác vụ chặn lại. Chỉ thử lại lỗi kết nối, hết giờ, 408, 429, 500, 502, 503, 504, cùng mọi trường hợp lý do đọc được có chứa "429". Không thử lại 401/403/404 — đó là URL sai hoặc luồng đã bị xoá, thử lại không đổi được kết quả.

URL webhook là một thông tin đăng nhập. Ai có URL là đăng được tin vào kênh, và tin đó không phân biệt được với tin thật. Vì vậy URL chỉ nằm trong .env (không vào git), và mọi chỗ in ra đều đi qua che_url(): giữ nguyên tên máy và 12 ký tự đầu của đường dẫn, giấu phần còn lại. Với URL của SIXDO, tên máy chứa luôn mã môi trường Power Platform — nên chuỗi đã che vẫn là thông tin nội bộ, không dán ra ngoài công ty. Script đăng ký tác vụ gọi teams_digest.py --kiem-cau-hinh để kiểm URL có mặt mà không đọc giá trị ra màn hình (từ b92aedf; trước đó là biểu thức chính quy trên một khoá). Hai luồng trong cùng một môi trường Power Platform cho ra cùng một chuỗi đã che (đo 23/09 18:15; 25/09: ba luồng Tổng hợp/POS/Online cũng trùng), nên từ commit 109ca30 dòng [gửi] ghi thêm kênh=<tên khoá .env> — và từ v2 thêm thẻ=<id> — cách duy nhất trong log biết thẻ đã dùng khoá nào. Khoá đó trỏ tới kênh nào thì chỉ nhìn Teams mới biết: ai dán nhầm URL kênh kinh doanh vào khoá Vận hành, log vẫn ghi kênh=TEAMS_DIGEST_WEBHOOK_VANHANH.

Ba giới hạn của lớp gửi

202 là "luồng đã nhận", chưa phải "thẻ đã hiện trong kênh"

Với nhánh Workflows, 202 do Power Automate trả khi nhận yêu cầu. Bước đăng thẻ vào kênh chạy sau đó, bên trong luồng, và chương trình không nhìn thấy kết quả của bước đó. Vì vậy ok=True ở nhánh này thực chất là "Power Automate đã nhận".

Kịch bản hỏng: kết nối Teams của người sở hữu luồng hết hạn, hoặc người đó nghỉ việc. Mỗi sáng log vẫn ghi ok=True status=202, không có cờ cảnh báo nào, và kênh trống. Đây là lý do script đăng ký tác vụ in dòng "NGHIEM THU: xem kenh sau luot 07:00" (trước commit 9a063c4 là "NGHIEM THU DUY NHAT: MO TEAMS NHIN VAO KENH 3 BUOI SANG LIEN TIEP"): phép nghiệm thu là mắt người nhìn kênh — và lý do việc chốt người đồng sở hữu cho cả bốn luồng vẫn là một việc đang chờ người.

Thử lại sau khi hết giờ chờ có thể đăng hai thẻ

Chương trình thử lại mọi lỗi của requests, kể cả ReadTimeout — tức là cả trường hợp máy chủ đã nhận bài nhưng trả lời chậm hơn 20 giây. Không có khoá chống trùng nào giữa các lần thử trong một lượt; file trạng thái chỉ chống lặp giữa các lượt. Đây là "đường đăng trùng thứ hai" trong kế hoạch P2.9 và là đường chưa vá; gửi lại hay dừng khi hết giờ là quyết định D2 đang chờ người. Từ 26/09 rủi ro này nhân ba kênh.

Trần kích thước chỉ cảnh báo, không chặn

v1 không kiểm kích thước. Thẻ đầy đủ v1 kỳ 22/09 ghi ra file --out là 6.738 byte — con số dùng khi so khớp byte trước và sau bản vá, có tính ký tự xuống dòng kiểu Windows. Thân HTTP thật gửi đi là 5.483 byte (tiếng Việt thành chuỗi \uXXXX; đo ngày 23/09 bằng cách dựng request mà không gửi). v2 có trần TRAN_KB = 24: lượt thường in [cảnh báo] rồi vẫn gửi; --kiem trả mã 4. Thẻ v2 kỳ 24/09: 8,1 / 6,9 / 5,3 KB; thẻ v1 cùng kỳ 6.743 byte (bantin_v2.py:65, 649-650, 787-789).

Bộ dò dữ liệu cá nhân (v2): --kiem chạy thêm bộ dò với sáu mẫu — chuỗi 32 hex, tên cột khach_key, dãy ≥ 9 chữ số, mã chữ + số, số chứng từ, email. Đo 25/09 cho kỳ 24/09: cả ba thẻ sạch. Bộ dò chỉ chạy khi có người gõ --kiem, không chạy trong lượt theo lịch.

Phần 09

Ranh giới an toàn — cái nào giữ bằng quyền, cái nào giữ bằng kỷ luật

Phân biệt hai loại này là việc quan trọng nhất của phần này. Một ranh giới giữ bằng quyền thì cơ sở dữ liệu từ chối trước khi ai kịp sai. Một ranh giới giữ bằng kỷ luật thì chỉ đúng chừng nào không ai sửa sai một dòng code.


LớpCơ chếGiữ bằng
Không ghi vào khoPhiên mở với read_only = True; tài khoản bi_reader không có quyền ghiquyền
Không đọc bảng khách gốcbi_reader bị từ chối trên mart.dim_customer, mart.fact_order, mart.fact_order_line, mart.dim_customer_pii và toàn bộ schema raw , cùng mart.dim_salesperson và schema staging — một agent soát đo lại ngày 25/09 bằng SET ROLE bi_reader + select … limit 0 trong giao dịch chỉ đọc, không bằng has_table_privilege()quyền
Không đưa họ tên, số điện thoại khách lên thẻĐi theo trên: tài khoản không đọc được các cột đóquyền
Không đưa mọi dữ liệu về người lên thẻbi_reader vẫn đọc được mã khách (customer_code), thuộc tính nhân khẩu học trong dim_customer_bi, họ tên nhân viên bán hàng trong dim_salesperson_bi, và địa chỉ, số điện thoại của cửa hàng. Từ 24/09 còn thêm hai bảng bt_khach_* của lớp bantin, mang khach_key = md5 của mã khách — giả danh chứ không phải ẩn danh hoá: ai có danh sách mã khách là nối ngược lại được. Đính chính 25/09: bản trước viết thêm "vì 2.256 mã khách có dạng số điện thoại, md5 dò ngược được" — đo lại thì fact_sales (nguồn của bt_khach_*) có 0 mã dạng số điện thoại trên 341.971 mã; cả 2.257 mã dạng đó chỉ nằm trong dim_customer_bi, là khách chưa có đơn bán. v1 an toàn vì câu SQL không chọn các cột đó (digest_daily.sql:26-28). v2 có đọc khach_key trong digest_v2_khach.sql để đếm khách mới / quay lại — chỉ số đếm rời câu SQL; --kiem chạy thêm bộ dò (Phần 08). Cả hai vẫn là kỷ luật, không phải quyềnkỷ luật
Hạn chế làm chậm đồng bộ đêmlock_timeout=5s, statement_timeout=90s, idle_in_transaction_session_timeout=30s — đặt trong chuỗi kết nối của bản tin; vai bi_reader không có giới hạn nào ở cấp vai. Kết nối được đóng trước khi gửi HTTP, nên không giữ khoá trong lúc chờ mạng. Chiều ngược lại chưa được đo: nếu hai bên chồng giờ, dbt vẫn có thể phải chờ câu của bản tin tới 90 giâykỷ luật
Không phụ thuộc hệ nguồnKhông import ingest/db.py; không có lời gọi nào tới FAST (luật R10)kỷ luật
URL webhook không lộChỉ ở .env (không vào git, .gitignore dòng 2); in ra qua che_url(). Kẽ hở: dòng đầu mỗi khối log ghi nguyên văn tham số dòng lệnh, nên chạy thử với --url <URL thật> là URL đầy đủ nằm trong logs/teams_digest_<ngày>.log. --url còn đè mọi khoá — thẻ lỗi cũng đi tới URL đó. Ở v2, --url và --kenh-thu gửi mọi thẻ kể cả thẻ lỗi tới kênh thử, nhưng không đọc/ghi state, không lưu payload, không đụng cờ. Sự cố thật 25/09 10:26: một URL thật bị dán vào .env.example, đã hoàn tác trước khi stage (ENV-EXAMPLE-URL-THAT)kỷ luật
Kiểm chứng chỉ khi gửirequests kiểm TLS theo mặc định; code không truyền verify= nàomặc định thư viện
Thẻ không rời công tyCâu "không chuyển ra ngoài công ty" ở chân mọi thẻ có số liệu và mọi thẻ lỗi (thẻ --probe không có câu này và cũng không chứa số). Người chịu trách nhiệm về con số nếu một thẻ bị chụp chuyển ra ngoài: anh Nam (nhận ngày 22/09)con người

"6 bảng" (v1) / "10 quan hệ" (v2) nghĩa là số tên được gọi trong SQL. Với v1: fact_sales và fact_order_bi là view, bốn cái còn lại là bảng. Qua view, câu truy vấn thực tế còn quét fact_order_line, fact_order và dim_product — đọc được nhờ quyền của chủ view, trong khi bi_reader đọc thẳng hai bảng đầu thì bị từ chối. Phát biểu chính xác là "không chiếu cột dữ liệu cá nhân nào", không phải "không chạm bảng có dữ liệu cá nhân".

Đính chính cho tài liệu chính. Kiến trúc SIXDO Data Hub viết rằng bản tin đọc bằng một tài khoản "không thấy — cơ sở dữ liệu từ chối", và rằng Power BI với bản tin "không tự tính lại". Đọc sát code thì cả hai câu rộng hơn thực tế: (1) tài khoản của bản tin không thấy thông tin liên lạc của khách, nhưng vẫn thấy mã khách và họ tên nhân viên — phần còn lại giữ bằng câu SQL; (2) bản tin có tính lại bằng SQL riêng, và sự thống nhất với dashboard được giữ bằng một hợp đồng viết tay cộng một lần đối chiếu 10/10, không phải bằng việc đọc chung một công thức. Điểm (1) đã có trong backlog (CLAUDE-RO-SQL-KHANGDINH-SAI: dim_salesperson_bi có 2.218 họ tên nhân viên, đo 23/09); điểm (2) là gốc của ba dòng chưa đối chiếu ở Phần 04 của bi/reports/1_planning/PLAN_BANTIN_TEAMS.html. Cập nhật 25/09: tài liệu chính đã sửa điểm (1) — nay ghi "không thấy tên, SĐT, email khách" — và thêm đường A của chatbot; điểm (2) vẫn ghi "không tự tính lại".

Phần 10

Vận hành hằng ngày

Luật một dòng: chạy tay thì luôn kèm --dry-run. Mọi lệnh không có cờ đó đều có thể đăng một thẻ vào kênh thật — và kênh đó có ban lãnh đạo.


Lệnh

MuốnLệnh (từ thư mục gốc repo)Đăng vào kênh?Lưu ý
Xem thẻ sẽ gửi.venv\Scripts\python.exe notify\teams_digest.py --dry-runkhôngNay chạy v2 và in ba thẻ. Cả lượt v2 đo 25/09: 2,84 s và 4,34 s (v1: câu SQL ~14 giây, cả lượt 24,1 giây). Vẫn ghi vào log thật, kể cả dòng [quyết định] … GỬI dù không gửi gì. v2 không ghi cờ, không gửi thẻ lỗi (v1: kho lỗi thì vẫn ghi đè ALERT_teams.txt). Không ghi file trạng thái. Tránh khung 00:30–03:00
Lưu thẻ ra file… --dry-run --out the.jsonkhôngNhiều thẻ thì thành the.<thẻ>.json. UTF-8 không BOM. Tệp là phong bì tin nhắn; muốn xem hình thì chỉ dán phần attachments[0].content vào Adaptive Card Designer. Bỏ --dry-run thì --out vẫn gửi thật — và còn được miễn nen_chay()
Xem một ngày khác… --dry-run --asof 2026-09-21khôngĐừng bỏ --dry-run ở lệnh này: thẻ của một kỳ cũ sẽ vào kênh, kèm dòng ⚠ "bất thường ở nguồn" vì kho có đơn sau kỳ đó. File trạng thái cũng bị ghi đè sang kỳ cũ, nên lượt tác vụ kế tiếp trong ngày sẽ đăng thêm một bản tin đầy đủ thứ hai cho kỳ hôm qua — ở v2 là ba thẻ thứ hai
Đo hiệu năng câu SQL… --explainkhôngChạy thật câu SQL dưới EXPLAIN (ANALYZE, BUFFERS). Kho đọc được thì không đăng gì. v2: kho lỗi thì thoát 3 ngay, không cờ, không thẻ. v1: kho lỗi thì chạy như lượt thật — ghi cờ, thoát 3, và có thể đăng thẻ lỗi vào kênh Vận hành
Chạy bộ kiểm thử.venv\Scripts\python.exe notify\tests\test_quyet_dinh_gui.py · …\test_bo_cuc.py · …\doi_chieu_v1_v2.py --asof <ngày>không43 phép của v1 và 120 phép của v2 (25/09: 120/120), không cần kho, không chạm mạng, không đụng file trạng thái thật; mã 0 = tất cả đạt. doi_chieu_v1_v2.py cần hub. Không dùng pytest — hàm tên kiem_*
Chạy lại sau khi sáng nay đã có bản tin đầy đủ.venv\Scripts\python.exe notify\teams_digest.pykhôngIn [bỏ qua], thoát 0, không nối kho — v2 chỉ bỏ qua khi cả ba thẻ đã đầy đủ. An toàn từ 18:11 ngày 23/09 — trước đó, lệnh này có thể đăng thẻ lỗi vào kênh kinh doanh. Đã chạy thật: 24/09 10:35, 25/09 09:30:04 (v1), 25/09 13:54:19 (v2). Nếu sáng nay chỉ có thẻ "chưa về" thì lệnh này nối kho và có thể gửi thật
Kiểm cấu hình v2… --kiem-cau-hinhkhông0 = đạt, 4 = thiếu khoá của một thẻ đang bật hoặc bố cục sai. Thiếu khoá Vận hành hay kênh thử chỉ là dòng [thông tin]
Kiểm số, cổng, kích thước, dữ liệu cá nhân… --kiemkhôngKhông gửi. Không đạt → mã 4
Xem khung thẻ không có số… --khung --dry-run · … --khung --out f.jsonkhôngÔ giữ chỗ thay số. --khung trần bị từ chối, mã 4. --chi <thẻ> (lặp được) chỉ chạy thẻ đó
Kiểm đường gửi… --probe · --probe --chi pos · --probe --kenh vanhanhCÓThẻ "kiểm tra kết nối", không có số. Chỉ chạy khi đã báo người trong kênh. --probe trần đi vào kênh Tổng hợp; --chi pos/online chọn kênh; --kenh vanhanh thử kênh Vận hành (chưa chạy lần nào). Từ commit 9a063c4, gửi được cũng không xoá cờ ALERT_teams.txt — chỉ một bản tin đầy đủ hay "chưa về" gửi được mới xoá. Cờ còn sau probe là đúng thiết kế
Gửi bất chấp chống lặp… --forceCÓGần như không bao giờ đúng: tạo thẻ thứ hai cho cùng một kỳ
Thử với URL khác… --url <URL>CÓĐè mọi khoá. URL đầy đủ sẽ vào log (Phần 09). Dùng --kenh-thu (khoá TEAMS_DIGEST_WEBHOOK_THU) để khỏi phải cầm URL — khoá này chưa có trong .env, nên hiện thoát 2
Quay về v1 · tắt một thẻXoá dòng TEAMS_DIGEST_PHIEN_BAN=2 · "bat": false trong bo_cuc.jsonkhôngState v1 được ghi song song nên quay về không gửi trùng thẻ Tổng hợp

Tác vụ

Get-ScheduledTaskInfo -TaskName SIXDO-DataHub-TeamsDigest   # LastRunTime, LastTaskResult, NextRunTime
Start-ScheduledTask   -TaskName SIXDO-DataHub-TeamsDigest   # chạy THẬT — vẫn qua bộ chống lặp
powershell -ExecutionPolicy Bypass -File infra\register_task_digest.ps1          # đăng ký lại (tự chạy --kiem-cau-hinh trước)
powershell -ExecutionPolicy Bypass -File infra\register_task_digest.ps1 -S4U     # chạy cả khi không ai đăng nhập — nhưng Docker Desktop thì không tự bật
powershell -ExecutionPolicy Bypass -File infra\register_task_digest.ps1 -Remove  # gỡ

LastTaskResult: từ khi đăng ký lại lúc 18:13 ngày 23/09 là đúng mã thoát của Python — 0x0, 0x1, 0x2, 0x3, và (v2) 0x4 — chưa đo qua bộ lịch (Phần 07); trước đó 0x1 nghĩa là bất kỳ lỗi nào. Hai ngoại lệ đã đo trên dạng lệnh mới: sai đường dẫn .venv cho ra 0x0, còn thiếu notify\teams_digest.py cho ra 0x2 — trùng mã "gửi thất bại". Luôn đối chiếu với log trước khi tin mã này. 0x41303 = chưa từng chạy. Nhật ký Operational của Task Scheduler đang tắt, nên lịch sử từng lượt chỉ còn trong log của chương trình.

Thao tác ít gặp — cái nào đã thử, cái nào chưa

ViệcCáchĐã thử?
Biết kênh nào, ai sở hữu luồngKênh kinh doanh là "Trao Đổi Chung" (theo ảnh người dùng gửi ngày 24/09; tên nhóm Teams chưa ghi). URL kênh kinh doanh do anh Nam cung cấp ngày 22/09; luồng kênh Vận hành tạo ngày 23/09; luồng POS và Online tạo ngày 25/09. Tên kênh, thành viên bốn kênh và chủ sở hữu từng luồng chưa được ghi ở đâu, và chưa xác nhận luồng nào có đồng sở hữu — người trực mới phải hỏi anh Nam trước ca đầu tiên—
Kiểm nhánh của checkout chínhgit -C sixdo-datahub branch --show-current phải ra p2/phase2-bi-gates-c-d (hoặc main nếu người chốt chuyển; mỗi lần merge vào main phải fast-forward nhánh vận hành theo). Lệch thì git switch về nhánh đó, git merge --ff-only với nhánh làm việc hiện hành, rồi chạy lại bộ kiểm thử — làm trước 01:00. git status không báo gì trong trường hợp nàyđã làm 24/09
Đăng ký lại tác vụChỉ chạy register_task_digest.ps1 từ checkout chính. Script lấy đường dẫn từ chính vị trí của nó, nên chạy từ một worktree là tác vụ 07:00 chuyển sang code, .env và logs/ của worktree đó. Sau khi đăng ký, kiểm (Get-ScheduledTask -TaskName SIXDO-DataHub-TeamsDigest).Actions trỏ về checkout chínhđã làm 23/09
Thử đường gửi kênh Vận hànhĐường gửi: --probe --kenh vanhanh (v2; chưa chạy lần nào — backlog VANHANH-CHUA-PROBE). Muốn thử cả nhánh lỗi thì vẫn dùng cách đã làm ngày 23/09: sao lưu file trạng thái và ALERT_teams.txt, đặt biến môi trường PG_PORT=1 trong cửa sổ đó, chạy teams_digest.py --asof <một ngày cũ>, nhìn kênh Vận hành, rồi khôi phục hai file và so mã băm. Chạy được vì biến môi trường đè giá trị trong .env — một biến PG_PORT hay TEAMS_DIGEST_WEBHOOK còn sót trong môi trường người dùng sẽ đổi hành vi của tác vụ mà .env không cho thấy. Ngoài bảy khoá ở Phần 02, code còn đọc PG_HOST, PG_PORT, PG_DB, DIGEST_PG_USER, DIGEST_PG_PASSWORDđã làm 23/09
Dựng lại một luồng bị xoáTeams → kênh đích → … → Workflows → mẫu "Post to a channel when a webhook request is received" → Create → sao URL. Tự dán URL vào .env đúng khoá (TEAMS_DIGEST_WEBHOOK, _POS, _ONLINE, _VANHANH hay _THU), không gửi URL qua chat, thêm đồng sở hữu, rồi chạy --kiem-cau-hinh và --probe --chi <thẻ> (quy trình ở bi/reports/2_doing/BANGIAO.md §4b). URL mới có dạng *.environment.api.powerplatform.com — chú thích trong .env.example còn ghi logic.azure.com, đã lỗi thờiđã tạo luồng, chưa dựng lại lần nào
Xoay URL khi bị lộRepo chưa có quy trình. Cách hợp lý nhất, chưa kiểm chứng: dựng luồng mới như trên, dán URL mới vào .env, thử đường gửi, rồi tắt hoặc xoá luồng cũ và xác nhận URL cũ không còn đăng được. Nếu lộ qua --url, xoá cả các dòng chứa URL trong logs/teams_digest_<ngày>.logchưa
Xem lịch sử chạy của luồngTheo Microsoft Learn: Power Automate → My flows → chọn luồng → lịch sử 28 ngày. Chỉ chủ sở hữu hoặc đồng sở hữu mở được; quá 28 ngày là mất, nên phải tra sự cố trong vòng bốn tuầnchưa
Tắt bản tin tạm thờiTắt một thẻ: "bat": false trong bo_cuc.json. Quay về v1: xoá dòng công tắc. Đừng tắt bằng cách xoá khoá webhook: v1 thì mỗi lượt thoát 2, không thẻ, không cờ; v2 thì mỗi lượt thoát 2 và ghi cờ. Tắt hẳn cả tác vụ vẫn chưa có quy trình đã thử. -Remove xoá hẳn tác vụ. Disable-ScheduledTask / Enable-ScheduledTask giữ nguyên định nghĩa nhưng chưa thử trên máy này. Tắt thì báo cả bốn kênh trước — trong lúc tắt không có cả thẻ "chưa về"chưa

"Sáng nay không thấy bản tin" — đi theo thứ tự này

#Nhìn vàoNếu thấyNghĩa là
1LastRunTime của tác vụKhông có mốc sáng nayTác vụ không chạy: máy ngủ — đã xảy ra đêm 23/09 (lệnh ngủ chủ động), sáng 24/09 và đêm 24→25/09 (Modern Standby, nguyên nhân không xác định); -WakeToRun không đáng tin trong Modern Standby (MAY-NGU-LENH-SLEEP). Hoặc không ai đăng nhập, hoặc tác vụ bị tắt
2logs/teams_digest_<hôm nay>.log, tìm khối có tham số: (không có)Không có khối nào như vậyTác vụ chạy nhưng Python không lên — sai đường dẫn .venv, hoặc checkout chính đang ở nhánh không có notify/. Khối có tham số là lượt chạy tay. Khối (không có) có thể là của tác vụ hoặc của một lượt chạy tay không cờ — đối chiếu giờ của khối với LastRunTime
3Dòng [bỏ qua], [quyết định] hoặc [quyết định lỗi][bỏ qua] hay KHÔNG gửi — …Bộ chống lặp đã chặn; đọc lý do. Nếu lý do là "đã gửi … lúc …" mà kênh không có thẻ nào lúc đó, kiểm file trạng thái có bị ai sửa tay, hoặc có ai chạy thật từ thư mục khác không
3bDòng [mất hub], [thiếu quyền], [lỗi máy chủ], [lỗi hub], [mart đang dựng lại]Có — và dòng [gửi] kênh=TEAMS_DIGEST_WEBHOOK_VANHANHSự cố của kho. Thẻ đã đi kênh Vận hành, kênh kinh doanh không nhận gì — đúng thiết kế. Nhìn kênh Vận hành
4Dòng [kết quả] và ALERT_teams.txtok=FalseGửi hỏng: 401/403/404 → URL sai hoặc luồng bị xoá; 429 → bị chặn tốc độ; lỗi kết nối → mạng
5Như trênok=True status=202 mà kênh trốngHỏng bên trong luồng Power Automate — chủ sở hữu luồng xem lịch sử chạy (xem bảng thao tác ở trên)
5bDòng [LỖI] thiếu TEAMS_DIGEST_WEBHOOKCó.env thiếu khoá kênh kinh doanh. Mã 2, không thẻ. v1: không cờ — kẽ hở im lặng. v2: ghi cờ — kẽ hở này đã đóng ở v2
5c(v2) Dòng [chưa sẵn sàng], [LỖI cấu hình], [từ chối], [chờ]CóLớp bantin chưa dựng xong (xem kênh Vận hành) · bo_cuc.json hoặc khoá sai (mã 4) · dùng cờ chỉ-v2 khi công tắc tắt · có payload chờ gửi lại từ lượt trước
6Cuối khối logMột tracebackMã 1 — lỗi Python. Thường là chưa có thẻ nào đi; nhưng nếu traceback nằm sau dòng [kết quả] ok=True thì thẻ đã đăng mà trạng thái chưa ghi. Đây là kẽ hở im lặng thứ hai

Trước khi sửa code bản tin

Đổi một chỉ tiêu thì đổi ở ba tới bốn nơi: model dbt lớp bantin, notify/sql/digest_v2.sql, notify/sql/digest_daily.sql (riêng [Ngày dữ liệu] là notify/sql/freshness.sql) và đối ứng DAX — measure trong bi/tools/build_model.csx, riêng Q22 là hai model dbt. Rồi cập nhật bảng hợp đồng ở đầu cả hai file SQL và danh sách trỏ chéo ở đầu build_model.csx (luật R11), và chạy doi_chieu_v1_v2.py. Hằng số hết hạn TLS nằm ở teams_digest.py:135 và trong measure [Chứng chỉ TLS] — ở đó ngày xuất hiện ba lần trong DAX (build_model.csx:141-143) và một lần trong mô tả (:184); phía Power BI phải sửa qua quy trình Tabular Editor chế độ A (luật R2, R3, R11), không sửa tay TMDL.
So thẻ trước và sau bằng byte: --dry-run --asof <một ngày cố định> --out trước khi sửa và sau khi sửa, cùng một ngày, không có đồng bộ đêm chen giữa — thẻ có dòng "còn N ngày" của chứng chỉ TLS (tính theo ngày chạy) và dòng "Dữ liệu đến" (tính theo kho), nên so khác ngày thì luôn lệch. Bản vá ngày 23/09 đã giữ được thẻ khớp từng byte (6.738 byte) — đó là tiêu chuẩn cho mọi sửa đổi không nhằm đổi nội dung. Ngày 25/09: thẻ v1 kỳ 24/09 (6.743 byte) trùng byte bản trước PR #5; preset v1 của v2 trùng byte ở 6 bộ dữ liệu.
Chạy cả hai bộ kiểm thử (test_quyet_dinh_gui.py, test_bo_cuc.py), và nhớ chỗ chúng chưa phủ: sửa thứ tự các nhánh bắt lỗi thì phải thử thêm bằng tiêm lỗi thật . Ngày 23/09 đã làm với mất kết nối (PG_PORT=5999), 42501 và hết đĩa; chờ khoá và quá giờ câu lệnh thì chưa từng đo thật, phải tự dựng phép thử. Nhóm phép chạy nguyên main() dùng URL giả example.invalid, không bao giờ URL thật.
Đừng chạy thật từ một worktree. Log, trạng thái và cờ cảnh báo nằm theo thư mục code (ROOT/logs/). Ngày 23/09 đã có hai file trạng thái khác nhau — một ở checkout chính, một ở worktree work-0b-0c-pending-d1f0cf (đã gỡ 25/09; hiện còn .claude/worktrees/p29b-pha3 — cùng rủi ro). Chạy thật từ worktree thì bộ chống lặp không thấy các lượt của tác vụ.
Đổi định nghĩa tác vụ là phải đăng ký lại và quan sát một lượt chạy thật. Đổi kiểu đăng nhập sang -S4U thì phải đăng xuất khỏi máy rồi mới quan sát — đọc cấu hình không chứng minh được gì.

Phần 11

Giới hạn đã biết — mỗi cái một kịch bản hỏng

Code ghim ở main 2ed7140 (v2 từ 6dd143a); trạng thái vận hành đo tới ~16:00 ngày 25/09/2026. Cột cuối nói rõ cái nào đã có mục trong findings-backlog.md hay runbook, cái nào mới lộ ra khi viết phụ lục này và chưa được ghi ở đâu.


Đã đóng trong ngày 23/09

Đóng theo code và phép đo. findings-backlog.md đã ghi trạng thái ngày 25/09: BANTIN-THE-LOI-SAU-THE-DUNG = đã xử lý; TASK-PIN-MA-THOAT xong cho bản tin, còn mở cho NightlySync và Health. Cả hai vẫn nằm dưới tiêu đề "MỞ".

MụcĐã sửa thế nàoBằng chứng
BANTIN-THE-LOI-SAU-THE-DUNG · Caonen_chay() bỏ qua lượt bù khi kỳ đã đủ; thẻ lỗi chỉ đi kênh Vận hành, có chống lặp riêng9a063c4; lượt thử thật 18:13:54 vào đúng kênh Vận hành; nen_chay() lần đầu chặn một lượt thật lúc 10:35 ngày 24/09
TASK-PIN-MA-THOAT · Trung bìnhGiữ mã thoát bằng ; exit $LASTEXITCODE; cho chạy khi dùng pin. Chỉ tác vụ bản tin — tác vụ đồng bộ đêm và tác vụ Health vẫn không chạy khi dùng pin; riêng Health còn không đánh thức máy9a063c4; đăng ký lại 18:13:38; đo lại dạng lệnh mới giữ đúng mã 2 và 3; lượt tác vụ đầu tiên trên dạng lệnh mới là 07:00:03 ngày 24/09 (HTTP 202)
Cờ cảnh báo bị --probe hay "đang dựng lại" xoáChỉ xoá khi thẻ vừa gửi là day_du hoặc chua_ve9a063c4; phép kiểm "gửi OK thẻ probe → không xoá ALERT"
Log không cho biết thẻ đi kênh nàoDòng [gửi] ghi kênh=<tên khoá>109ca30

Đã đóng trong ngày 25/09 (v2)

MụcĐã sửa thế nàoBằng chứng
Cờ mất chi tiết gốc khi gửi thẻ lỗi hỏng (ALERT-GHI-DE-CHI-TIET)v2 nối thêm dòng, không ghi đè; v1 giữ nguyên để trùng bytebantin_v2.py:415-420
--url ghi đè file trạng tháiv2: --url / --kenh-thu không đọc/ghi statebantin_v2.py:34-38, 444-462
Thiếu khoá kinh doanh mà không có cờv2 ghi cờ, mã 2bantin_v2.py:803-810
Script đăng ký chỉ kiểm một khoáGọi --kiem-cau-hinhb92aedf
Tài liệu bàn giao không nhắc bản tinbi/reports/2_doing/BANGIAO.md §4bPha 6 P2.9b (fa84dd6)

Còn mở

MứcGiới hạnKịch bản hỏng cụ thểTheo dõi ở
CaoTác vụ chạy code của nhánh đang checkoutĐã xảy ra: 15:38:35 ngày 24/09 checkout chính bị chuyển sang main cũ, không xác định được ai; phát hiện 19:20, kéo lại 19:24. Để nguyên thì đồng bộ 01:00 không dựng lớp bantin và bản tin 07:00 chạy bản pha 1: mất quyền đọc là kênh im lặng, thẻ lỗi lại vào kênh kinh doanh. git status vẫn "sạch" suốt thời gian đó. Từ 25/09 main đã có đủ v2; rủi ro còn lại là checkout một commit trước 6dd143a — v1 chạy âm thầm, kênh POS và Online im lặngTRẠNG THÁI 24/09 trong CLAUDE.md; chưa có mục backlog
CaoMáy ngủ bằng lệnh Sleep hay Modern StandbyĐêm 23/09 máy vào ngủ bằng một lệnh chủ động tới 05:46 → đồng bộ 01:00 rơi, chạy bù 05:55; đêm 24/09 ở Modern Standby tới 02:03 → đồng bộ chạy 02:04. Sáng 24/09 bản tin 07:00 vẫn đủ dù máy đang Modern Standby — hẹn giờ trong standby lúc chạy lúc không. Sáng 25/09 lượt 07:00:03 (v1) gửi đủ, HTTP 202; 09:30:04 [bỏ qua]. Kịch bản hỏng: tối bấm Sleep, sáng không ai mở máy trước 07:00 → cả bốn kênh im lặng, cả thẻ lỗi cũng không điMAY-NGU-LENH-SLEEP
CaoChỉ chạy khi có người đăng nhậpNghỉ Tết năm ngày, không ai đăng nhập → không bản tin, không cả thẻ "chưa về". Docker Desktop cũng chỉ tự bật khi đăng nhập, nên chuyển riêng bản tin sang -S4U sẽ ra thẻ "mất kho" mỗi sáng ở kênh Vận hànhhoãn có chủ đích · chờ người quyết -S4U
CaoNgoài thẻ lỗi của kho, không gì giám sát bản tinKênh Vận hành nay nhận thẻ lỗi của kho. Nhưng không chương trình nào đọc ALERT_teams.txt hay LastTaskResult; tác vụ Health không nhắc tới bản tin; .env không có SYNC_ALERT_WEBHOOK. Một lỗi Python (mã 1) hay một luồng hỏng sau khi trả 202 không tới kênh nào. Cờ cảnh báo lúc 10:15 ngày 23/09 nằm đó gần 21 giờ, tới khi bản tin 07:00 ngày 24/09 gửi được thì tự xoá — không ai được báo trong khoảng đó. v2 ghi cờ ở nhiều trường hợp hơn, nhưng vẫn không ai đọcchưa có mục backlog
Cao202 không chứng minh thẻ đã hiệnKết nối Teams của chủ luồng hết hạn → log ghi ok=True status=202 mỗi sáng, kênh trống, không cờ nào bật. Giờ có bốn luồng; chưa ghi nhận chủ và đồng sở hữu của luồng nào. Luồng Vận hành chưa được thử gửi lần nào sau 23/09 (VANHANH-CHUA-PROBE)chờ người chốt đồng sở hữu luồng
Trung bìnhBa kênh kinh doanh im lặng trong sáng mất khoKho tắt từ 06:30 tới trưa → ba kênh kinh doanh không nhận gì cả ngày, kênh Vận hành nhận đúng một thẻ. Hôm sau là kỳ mới, và kỳ bị lỡ không bao giờ có bản tinđánh đổi người dùng chọn (Q-e) · Phần 12
Trung bìnhThử lại sau ReadTimeoutPower Automate đã nhận nhưng trả lời chậm hơn 20 giây → lần thử thứ hai đăng thẻ thứ hai. Từ 26/09 07:00 bản tin ba kênh chạy theo lịch: rủi ro nhân baquyết định D2 trong runbook P2.9b
Trung bình"Đang dựng lại" che hai loại sự cố thật(1) Kho chậm dần, câu SQL vượt 90 giây → QueryCanceled; (2) một view bị xoá mà không được dựng lại — như sự cố DBT-CASCADE-VIEW ngày 22/09 làm mất mart.fact_sales → UndefinedTable. Cả hai ra thẻ "đang dựng lại", thoát 0, không ghi cờ, hứa "lượt bù 09:30" kể cả khi chính nó là lượt 09:30. v1: câu chạy ~14 giây, còn khoảng 6 lần dư địa tới 90 giây. v2: câu dài nhất là cổng độ tươi 1,98–2,69 s, còn khoảng 33 lần dư địa; câu số liệu v2 0,4–0,5 schưa có mục backlog
Trung bìnhSai mật khẩu bị báo là "Docker đang tắt"Dựng máy mới theo .env.example — file mẫu không có khoá BI_READER_PASSWORD mà code cần → lỗi xác thực có SQLSTATE rỗng → thẻ "khả năng Docker/container đang tắt". Người trực đi kiểm Docker, Docker vẫn chạybi/reports/2_doing/BANGIAO.md §4b/§5 (dòng 102, 139); chưa có mục backlog
Trung bìnhNgày không bán được đơn nàoCổng độ tươi không phân biệt "chưa về" với "hôm đó không có đơn". Đã có tiền lệ: ba ngày 12–14/02/2021 (Tết) không có dòng nào trong mart.fact_sales. Tết tới mà cũng vậy, bản tin sẽ báo "dữ liệu chưa về, trễ N ngày" dù đồng bộ vẫn chạy tốtchưa có mục backlog
Trung bìnhTrạng thái nằm theo thư mụcMáy mới và máy cũ cùng đăng ký tác vụ với cùng webhook trong tuần bàn giao → mỗi máy một file trạng thái → hai thẻ mỗi sáng (ở v2 là sáu)bi/reports/2_doing/BANGIAO.md §4b (gỡ tác vụ ở máy cũ trước); chưa có mục backlog
Trung bìnhSo sánh không cùng thứThẻ ngày 23/09 so một Thứ Ba với một Thứ Bảy và in "giảm 49,32%". Không biết bao nhiêu phần trong đó là do thứ trong tuần, và thẻ không nói điều đó cho người đọcchưa có mục backlog
Trung bìnhBa dòng so sánh chưa đối chiếuKịch bản: thẻ nói "tăng", dashboard nói "giảm" cho cùng kỳ năm trước, vì hai bên kẹp lịch bằng hai thuật toán khác nhau ở tháng 31 ngày hoặc tháng 2. Dòng "tháng trước" thì dashboard không có công thức nào để so. Cộng ba chỉ số v2 không có measure nào (DAX-BANTIN-V2)Phần 04 của bi/reports/1_planning/PLAN_BANTIN_TEAMS.html
Trung bìnhREADME.md còn ghi "Lịch: cron"bi/reports/2_doing/BANGIAO.md §4b đã có phần bản tin (Pha 6). Nhưng README.md:61 vẫn ghi "Lịch: cron" — người mới đọc README trước sẽ đi tìm crontabchưa có mục backlog
Trung bìnhChưa lượt tự động nào gửi ba thẻRủi ro riêng của 07:00 — máy ngủ, đồng bộ trễ (07:00 là ba thẻ "chưa về", 09:30 nâng cấp), cổng bang_cu lúc bt_* chưa dựng xong — chỉ lộ ra sáng 26/093_report/P2.9_v2.md §6–§7
Trung bìnhThẻ POS gồm cả kho ECOM - …Thẻ "Tại cửa hàng" lọc theo mã kênh POS nên gồm cả đơn kênh POS ở 7 kho tên ECOM - … — 5,60% doanh thu POS tháng 9, xếp đầu bảng cửa hàngPOS-TAI-CUA-HANG-ECOM
ThấpKhông lưu nội dung thẻ đã đăngLý do "không gửi" nói "nội dung mới ghi trong log" nhưng lượt chạy thật không in thẻ ra log. File trạng thái chỉ giữ mã băm của lượt gửi gần nhất. Nội dung chỉ truy lại được khi tình cờ có một lượt --dry-run cùng kỳ trong log — như 06:59:22 ngày 23/09, băm trùng lượt gửi 07:00:25. Không có lượt như vậy thì câu "sáng qua thẻ ghi bao nhiêu?" chỉ trả lời được bằng cách dựng lại. v2 chỉ lưu thẻ gửi hỏng; băm trên dữ liệu nên dựng lại rồi so băm được (Pha 5 tiêu chí 8), với điều kiện dữ liệu kỳ đó chưa đổichưa có mục backlog
ThấpNgày hết hạn TLS lệch một ngàyHằng số là 10/10; chứng chỉ hết hạn 06:59 ngày 11/10 giờ Việt Nam. Thẻ 07:00 ngày 11/10 sẽ nói "đã hết hạn, không nạp thêm được" trong khi đồng bộ 01:00 cùng ngày vẫn chạy đượcbi/reports/1_planning/REVIEW_20260923_P3P4_HANDOFF.md mục A17; chưa có mục backlog
ThấpCờ cảnh báo mất chi tiết gốc (v1)Kho lỗi → cờ ghi SQLSTATE → gửi thẻ lỗi hỏng → cờ bị ghi đè bằng lý do gửi hỏng, mất dòng SQLSTATE. Đã sửa ở v2; còn ở v1 để thẻ v1 trùng byteALERT-GHI-DE-CHI-TIET
ThấpFile tạm của state không bị gitignoreTác vụ bị giết đúng giữa lúc ghi file tạm và os.replace → ….json.tmp nằm lại → một lần git add -A đưa vào lịch sử (không có dữ liệu cá nhân)STATE-TMP-GITIGNORE
ThấpRetry-After không có trần429 kèm Retry-After: 900 → chờ quá giới hạn 15 phút của tác vụ → bị giết trước khi kịp ghi cờ cảnh báochưa có mục backlog
ThấpMáy mới, .env mớirequests không có trong requirements.txt (có mặt nhờ dbt kéo theo). Bộ đọc .env của bản tin không bỏ dấu nháy và không hiểu tiền tố export — khác bộ đọc của sync_nightly.ps1; cả hai đều không bỏ chú thích cuối dòng: viết TEAMS_DIGEST_MODE=auto # … là rơi vào nhánh "không rõ"; bọc URL trong dấu nháy là gửi hỏng ba lần rồi thoát 2chưa có mục backlog
ThấpLog trộn lượt thật với lượt thửLog ngày 23/09 có 12 khối: 9 lượt chạy thử (gồm cả lỗi tiêm có chủ đích), 2 lượt của tác vụ, 1 lượt chạy thật có kiểm soát lúc 18:13. Tìm [mất hub] ra bốn dòng, chỉ dòng 1472 là sự cố thật. Lượt của tác vụ là các khối có tham số: (không có). Log 25/09 có 18 khối, 4 khối không tham số — trong đó khối 13:48:01 là chạy tay, không phải tác vụchưa có mục backlog
ThấpBộ kiểm thử còn chỗ trống43 phép, nhưng chưa phủ nhánh giảm nhiễu, bộ đếm dem_chua_ve(), việc ghi trạng thái khi không gửi (đổi hay bỏ ba thứ này thì bộ kiểm vẫn xanh), cặp nhánh bắt lỗi nhạy thứ tự nhất, nhánh bản tin đầy đủ qua main(), định dạng tiền, dòng TLS, che_url, và việc thử lại trong gui_the. Con số "40/40" của ngày 22/09 vẫn không tái lập được. v2 có thêm 120 phép, 21/21 đột biến bị bắt; cặp nhánh nhạy thứ tự vẫn chưa phủkhoản nợ của bi/reports/1_planning/PLAN_BANTIN_TEAMS.html

Phần 12

Đi tiếp, và phản biện

P2.9 bản 1 được nghiệm thu ngày 23/09 theo quyết định của người dùng (Q-a). P2.9b xong trọn Pha 0–6 ngày 25/09: Pha 5 được nghiệm thu theo quyết định Q-g trên bằng chứng ngày 25/09, không chờ lượt 07:00 ngày 26/09 — agent vẫn đọc log lượt đó, lệch thì mở lại. Công tắc v2 bật khoảng 13:48 ngày 25/09. Biên bản: bi/reports/3_report/P2.9_v2.md; runbook: bi/reports/1_planning/PLAN_P2.9b_BANTIN_3KENH.md; vận hành: bi/reports/2_doing/BANGIAO.md §4b. Còn mở: đọc log 26/09, D2, D7, POS-TAI-CUA-HANG-ECOM, DAX-BANTIN-V2, kênh thử, -S4U trên PC Server.

Đã cóKênh Vận hành/IT nhận thẻ lỗi (người xác nhận 18:22 ngày 23/09) · nen_chay() bỏ qua lượt bù · nen_gui_loi() chống lặp thẻ lỗi · tác vụ giữ mã thoát và chạy khi dùng pin · container tự khởi động lại, bộ nhớ chia sẻ 1 GB · lớp dbt bantin đã qua hai đêm · v2 ba thẻ, công tắc bật ~13:48 ngày 25/09 · thử đường gửi POS/Online 13:24, gửi thật 3 thẻ 13:38 · cổng bang_cu · state v2 + payload chờ · --kiem, --kiem-cau-hinh, --khung · script đăng ký gọi --kiem-cau-hinh
KênhBốn kênh đã có luồng: Tổng hợp (giữ khoá cũ), POS và Online (tạo 25/09), Vận hành. Danh sách thành viên chưa ghi trong repo; kênh thử chưa tạo
dbtLớp mart/bantin/: bt_kpi_ngay, bt_kenh_nhom, bt_khach_mua_dau, bt_khach_ngay; cộng bảng đối chiếu dq.dq_bantin_doichieu. v2 đọc các bảng này (gửi tay 13:38 ngày 25/09; theo lịch từ 26/09) và kiểm B01–B03 trước khi gửi
notify/Pha 3 xong 25/09 (6dd143a): bo_cuc.json, khoi.py, ve_teams.py, bantin_v2.py, 5 câu SQL; state riêng từng thẻ, ghi nguyên tử. Tắt công tắc thì v1 chạy không đổi byte nào
QuyềnKhông thêm vai: bảng mới cấp cho bi_reader — kể cả hai bảng bt_khach_* mang khach_key (md5 của mã khách, giả danh chứ không ẩn danh; xem Phần 09). Chatbot P2.10: ngày 25/09 người dùng chọn đường A — chatbot đọc vùng bi_reader (gồm cả các bảng này), không đọc vùng bi_pii; đường E hoãn tới khi mở rộng cho phòng ban. Plan: bi/reports/1_planning/PLAN_P2.10.md · Phụ lục 2 · Kiến trúc Chatbot doanh số
Đánh đổi cần viết ra

Từ 24/09, các kênh kinh doanh im lặng trong những sáng mất kho

Quyết định Q-e đưa thẻ lỗi sang kênh Vận hành, để ban lãnh đạo không nhận thông báo kỹ thuật — và nó đã chạy từ 18:11 ngày 23/09. Nhưng một sáng không đọc được kho thì cũng không đọc được ngày dữ liệu, nên không dựng nổi cả thẻ "chưa về". Kết quả: ba kênh kinh doanh không nhận gì (ngoại lệ ở v2: lớp bantin chưa sẵn sàng thì kênh kinh doanh vẫn nhận "chưa về") — đúng sự im lặng mà nguyên tắc số hai của bản 1 sinh ra để chống; và kỳ đó không bao giờ được bù. Đây là lựa chọn người dùng đã đưa ra, và có thể đúng. Nhưng code không nói điều này ở đâu, và thẻ "đang dựng lại" vẫn hứa "lượt bù 09:30" — v2 dùng lại đúng thẻ đó. Nếu giữ, nên có câu trả lời cho: ai trong kênh Vận hành sẽ nhắn sang kênh kinh doanh, và trước mấy giờ.

Phản biện 01

SQL riêng là nơi thứ hai chứa logic doanh thu

Luật R11 sinh ra để cấm đúng điều này. Bản tin chấp nhận nó vì không có đường nào khác — Teams không đọc được DAX — và bù bằng một hợp đồng viết tay cộng một lần đối chiếu. Điểm yếu là không có gì tự động kiểm hợp đồng đó mỗi đêm: ai sửa measure mà quên SQL thì hai con số trôi xa nhau cho tới khi một giám đốc nhận ra. Bảng dq_bantin_doichieu của P2.9b là phép kiểm tự động đầu tiên đi đúng hướng này, và từ v2 nó là cổng chặn: B01–B03 trượt thì không có thẻ số. doi_chieu_v1_v2.py (chạy tay) giữ v2 = v1. Vẫn chưa có phép tự động nào so SQL với DAX mỗi đêm.

Phản biện 02

"Không bao giờ im lặng" mới đúng cho lỗi của kho

Mọi lỗi cơ sở dữ liệu đều ra thẻ ở kênh Vận hành. Nhưng một lỗi Python ngoài dự kiến (mã 1), một luồng Power Automate hỏng sau khi đã trả 202, hay một sáng máy không chạy tác vụ — cả ba đều là bốn kênh cùng trống, và không có gì báo cho ai. Nguyên tắc đúng ở lớp nó được viết ra, chưa đúng ở lớp hệ thống. Thứ còn thiếu không phải thêm một loại thẻ, mà là một bên thứ ba nhìn vào kết quả: một tác vụ giám sát đọc cờ cảnh báo và mã thoát — việc từ nay đã làm được, vì mã thoát tới được bộ lịch — hoặc ít nhất một người được giao nhìn bốn kênh trước 08:00.

Phản biện 03

Với chính phụ lục này

Nó được viết trong lúc một phiên khác đang làm P2.9b trên cùng máy: trong khoảng một giờ đo, .env có thêm khoá kênh Vận hành, thiết lập ngủ của máy đổi, hai commit vào notify/ và tác vụ được đăng ký lại. Phụ lục đã được sửa lại ba lần: theo commit 109ca30 tối 23/09, theo 44 điểm soát đối kháng cùng các sự kiện ngày 24–25/09, và chiều 25/09 theo một lượt soát khoảng 100 điểm sau khi v2 bật. Mọi dòng mang giờ đo nên được đọc như ảnh chụp lúc ~16:00 ngày 25/09, không phải hiện trạng. Một số hành vi v2 (--dry-run không ghi cờ, --explain không gửi thẻ lỗi) được suy từ code — các nhánh đó chưa chạy thật. Trước khi hành động theo một dòng nào ở Phần 03, 10 hay 11, hãy so git log -1 -- notify với commit ghim ở bìa, và đo lại dòng đó.

← Về tài liệu chính: Kiến trúc SIXDO Data Hub