Bất biến dữ liệu D1, mỗi đêm
Mỗi sáng 08:47 giờ VN, một job GitHub Actions hỏi thẳng conan-platform-production khoảng 70 câu mà nếu câu trả lời sai thì người học, người mua, hay người vận hành sẽ gặp chuyện, và không màn hình nào báo. Chỉ đọc. Lỗi đi vào sổ lỗi tự động, mỗi bất biến một issue.
Trạng thái (14.10.2026)
- Đang chạy bằng token deploy, vì secret
CLOUDFLARE_D1_READ_TOKENchưa được tạo. Mỗi đêm job in một::warning::nói đúng câu đó cho tới khi có token đọc riêng (mục Token). - Lần đo đầu: 70 mục, 19 có mốc khác 0 (15 nợ, 4 mốc chặn). Không mục nào bị nới cho xanh: mục
zerođang khác 0 được ghi thành nợ ởscripts/d1-invariants-baseline.json, chỉ được hạ. Job xanh từ ngày đầu nhưng không bao giờ được xấu thêm. Danh sách nợ và nguồn gốc ở mục Lần đo đầu.
Chạy ở đâu
| Cái gì | Ở đâu |
|---|---|
| Workflow | .github/workflows/d1-invariants.yml, cron 47 1 * * * (08:47 VN) + chạy tay |
| Script | scripts/d1-invariants.mjs, Node 22, không phụ thuộc, không npm ci |
| Danh mục | scripts/lib/d1-invariants-catalog.mjs (id, tiêu đề, loại, SQL, sự cố gốc) |
| Mốc | scripts/d1-invariants-baseline.json (chỉ được hạ, mọi lần nâng có lý do trong history) |
| Lỗi đi đâu | error-ledger.yml nghe workflow_run của d1-invariants, mỗi annotation d1:<id> một issue |
Ở máy (đọc qua wrangler, cần npm ci ở api.conan.school và đã đăng nhập Cloudflare):
node scripts/d1-invariants.mjs --wrangler # toàn bộ, tuần tự, ~75 giây
node scripts/d1-invariants.mjs --wrangler --only claim-waiting,quiz-cjk
node scripts/d1-invariants.mjs --wrangler --update-baseline # hạ mốc sau khi dọn
node scripts/d1-invariants.mjs --self-test # không gọi mạngBốn loại
| Loại | Đỏ khi | Dùng cho |
|---|---|---|
zero | khác 0 (hoặc vượt mốc nợ, nếu đang nợ) | thứ không bao giờ đúng khi khác 0 |
ratchet | vượt mốc | nợ có sẵn mà bản thân nó không sai (lesson chưa có ngân hàng quiz chốt) |
liveness | số giờ từ nhịp gần nhất vượt ngưỡng, hoặc chưa từng có nhịp | cron hằng giờ, máy sinh, đường gửi thư |
warn | không bao giờ; chỉ in notice | tín hiệu cần người đọc, không cần chặn |
Nợ khác mốc chặn. Một mục zero đang khác 0 vẫn là zero trong danh mục, vì ý định của nó là 0. Con số hiện tại nằm ở file mốc như một khoản nợ: tăng là đỏ, giảm thì job in notice nhắc hạ, và khi hạ về 0 dòng nợ tự biến mất, mục trở lại cổng tuyệt đối. Đổi kiểu sang ratchet thì sẽ mất thông tin "con số đúng là 0".
Ngoài các mục của danh mục (tự kiểm in số mục hiện có: npm run check:d1-invariants) còn hai câu chạy trước tiên: câu chặn nguồn rỗng (users hoặc courses bằng 0 là đỏ, mọi mục khác bị bỏ qua) và gác SQL (mỗi câu phải là một SELECT hoặc WITH ... SELECT duy nhất, không dấu ;, không từ khoá ghi; sai là từ chối gửi).
Danh mục
Migration
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
migrations-not-applied | File migration trong git chưa có trong sổ schema_migrations | zero | 0 | known-risks "Token thiếu D1 Edit": 02-03.09.2026 CI áp migration đỏ, api production đóng băng hai ngày mà mọi trang vẫn tải. |
migrations-ghost-rows | Dòng trong sổ schema_migrations không còn file trong git | warn | ghi chú (đo: 1) | Sổ có dòng ma nghĩa là file đã bị đổi tên hay xoá sau khi áp: không ai mô tả được lược đồ đang chạy nữa. |
renamed-table-revived | Bảng đã RENAME mọc lại dưới tên cũ | zero | 0 | known-risks "Đổi tên một bảng: RENAME" (08.09.2026): hai bảng cùng sống là hai chỗ ghi, một chỗ đọc. |
Vòng chạy theo lịch
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
cron-event-reminders | Cron hằng giờ (nhắc sự kiện) còn đập | liveness | ngưỡng 3 giờ (đo: 0.0) | Cron 7 * * * * giành khoá scheduled_run_locks mỗi giờ; mất nhịp là cả nhánh nhắc sự kiện lẫn nạp chỉ mục Ran đứng. |
cron-ops-daily-report | Ngày lỡ báo cáo vận hành (3 ngày qua) | zero | 0 | Cron 0 1 * * * giành khoá ops-daily-report:<ngày VN>. Đếm theo giờ thì job đêm không thấy ngày lỡ (xem Nhịp hằng ngày). EMAIL_AUDIT_COPY.mode = 'off' (đọc từ mã) thì trả 0 kèm ghi chú. |
cron-daily-email | Ngày lỡ vòng một-thư-mỗi-ngày (3 ngày qua) | zero | 0 | Ngày 05.10.2026 không có dòng daily_email_runs, không ai nhận thư hằng ngày, và mục liveness 26 giờ cũ xanh cả hai đêm. Công tắc daily_email_enabled tắt thì trả 0; bật lại thì chỉ đếm ngày tới hạn sau lần đổi. |
daily-email-unfinished | Lượt gửi hằng ngày bắt đầu mà không ghi xong | zero | nợ 1 | daily.ts giành khoá trước khi gửi: lượt chết giữa chừng để lại khoá của cả ngày, và hôm đó không ai nhận thư mà không có gì đỏ. |
cron-ran-index | Vòng nạp chỉ mục Ran còn chạy | liveness | ngưỡng 3 giờ (đo: 0.0) | known-risks "Chỉ mục kiến thức trống" (01.09.2026): chỉ mục đứng yên mà Ran vẫn trả lời trơn tru. |
ran-run-stuck | Lượt nạp chỉ mục Ran treo ở running quá 2 giờ | zero | 0 | Workflow chết mà sổ vẫn running: màn hình trạng thái báo "đang nạp" mãi. |
ran-run-orphan | Dòng ran_knowledge_runs mồ côi do cron nổ hai lần trong một giờ | zero | nợ 16 | Đo 14.10.2026: index.ts chèn dòng sổ TRƯỚC khi tạo workflow; lượt cron thứ hai trong cùng giờ bị "already exists" nuốt, dòng của nó running vĩnh viễn. |
ran-run-failed | Lượt nạp chỉ mục Ran thất bại trong 24 giờ | zero | 0 | 511 lượt failed tới 26.09.2026 mà không ai được báo. |
cron-content-ops | Máy sinh nội dung còn chạy | liveness | ngưỡng 12 giờ (đo: 2.5) | known-risks "Sổ của mình kể chuyện cũ, còn workflow đã chết từ lâu". Cron 41 */3 chạy ~96 lượt/ngày; hết việc thật thì ghi vào đây. |
Email
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
email-daily-twice | Một người nhận hai thư gắn kết trong một ngày (7 ngày qua) | zero | 0 | known-risks "Bộ lọc đọc-rồi-ghi KHÔNG phải cơ chế chống trùng": 09.09.2026, 113 người nhận một lá thư hai lần. |
email-ref-duplicate | Cùng loại thư, cùng bản ghi, gửi hai lần cho một người (7 ngày qua) | zero | 0 | reminders.ts chống trùng bằng chính sổ gửi; cron chồng nhau hay lượt gọi tay chen giữa là gửi đôi. |
email-nudge-spacing | Thư nhắc hồ sơ gửi sát nhau hơn khoảng cách tối thiểu (30 ngày qua) | zero | 0 | known-risks "Lời hứa trong tài liệu mà mã không thi hành": 09.09.2026, 90 người nhận lần hai sau một ngày. |
email-nudge-max | Một người nhận quá ba thư nhắc hồ sơ | zero | 0 | Danh mục email hứa "nhắc tối đa ba lần". |
email-failed-all | Có thư thất bại mà không thư nào đi được trong 24 giờ | zero | 0 | known-risks "fireEmail nuốt lỗi theo thiết kế": binding sai hay miền chưa bật thì không có gì đỏ ngoài dòng failed. |
email-failed | Thư thất bại trong 24 giờ | warn | ghi chú (đo: 0) | Một vài thư hỏng vì địa chỉ xấu là bình thường; cả loạt hỏng thì mục email-failed-all đỏ. |
email-silence | Hệ còn gửi được thư | liveness | ngưỡng 48 giờ (đo: 16.5) | Báo cáo vận hành đi mỗi sáng, nên 48 giờ không một thư nào là đường gửi đã chết, không phải "hôm nay ít việc". |
email-after-unsubscribe | Thư gắn kết gửi cho người đã huỷ nhận | zero | 0 | known-risks "Gửi thư hằng loạt vào danh sách sai": báo spam kéo cả thư mã đăng nhập vào hố rác. |
email-engagement-inactive | Thư gắn kết gửi cho tài khoản không hoạt động (7 ngày qua) | zero | 0 | daily.ts chỉ gửi cho users.status=active; gửi vào legacy_users là gửi vào danh sách nhập cũ đầy hộp thư chết. |
Người dùng và tiền
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
user-without-profile | Tài khoản hoạt động không có hồ sơ nghiệp vụ ở legacy_users | zero | nợ 2 | known-risks "Tài khoản mới không có hồ sơ nghiệp vụ" (08.09.2026): kích hoạt Pro UPDATE 0 dòng mà báo thành công. |
claim-approved-without-tier | Đơn kích hoạt đã duyệt mà người dùng không có hạng trả phí | zero | 0 | Cùng sự cố trên: người đã chuyển tiền được báo "đã duyệt" mà quyền không mở. |
claim-waiting | Đơn kích hoạt chờ duyệt quá 48 giờ | zero | nợ 12 | Đường tiền: người đã trả tiền đứng chờ, và không màn hình nào đếm họ (skill membership-payments). |
membership-plans-none | Không còn gói thành viên nào đang bán | zero | 0 | known-risks "Token thiếu D1 Edit": membership_plans từng không tồn tại, trang Plans và câu trả lời học phí của Ran rỗng mà không lỗi. |
community-order-paid-not-pro | Đơn cộng đồng đã trả mà người mua không có Pro | zero | 0 | skill communities Luật 5: chỉ một đường cấp Pro; đơn paid mà không có dòng Pro là tiền đã thu, quyền chưa giao. |
community-pro-no-expiry | Thành viên Pro không có hạn dùng | zero | 0 | skill communities Luật 1: Pro = dòng + tier + hạn còn; thiếu hạn là Pro vĩnh viễn mà không ai bán. |
Khoá học
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
course-retire-half | Hai họ bảng khoá học bất đồng khoá còn sống hay đã nghỉ | zero | nợ 17 | known-risks "Migration đã áp không có nghĩa mọi câu lệnh đã ăn" (14.09.2026): retire ai-teen chỉ ăn một nửa. |
course-slug-collision | Hai khoá đang sống chung một slug chuẩn | zero | 0 | known-risks "Một khoá, hai slug, hai trang" (08.09.2026): slug chuẩn là COALESCE(l.slug, c.slug). |
course-slug-shadow | Slug phụ của một khoá trùng slug chuẩn của khoá khác | zero | 0 | Mọi lượt tra nhận cả hai slug rồi 301 về slug chuẩn; trùng chéo là một URL dẫn tới hai khoá. |
course-visible-no-units | Khoá hiện ở danh mục mà không có unit ở họ bảng nào | zero | 0 | known-risks "Trang trả 200, khung đầy đủ, ruột rỗng" (08.09.2026). |
course-active-no-units | Khoá active ở course-intelligence mà không có unit | zero | nợ 5 | known-risks "Dòng ghi danh sống lâu hơn khoá": khoá active không đường học vẫn tính vào trần hai khoá, vẫn vào chỉ mục Ran. |
unit-thin | Unit có ít hơn ba lesson đang hoạt động | zero | nợ 18 | skill course-pack: mỗi unit tối thiểu ba lesson. |
lesson-empty | Lesson đang hoạt động không có chữ nào (không định nghĩa, bài viết, tài liệu) | zero | nợ 39 | known-risks "Hai bài trùng tên" (09.09.2026): ba bài rỗng hoàn toàn nằm đó từ lúc dựng khoá, không màn hình nào đếm. |
lesson-name-duplicate | Hai lesson cùng tên trong một khoá | zero | nợ 6 | known-risks "Hai bài trùng tên làm migration chèn nội dung vào nhầm chỗ, im lặng" (09.09.2026). |
enrollment-over-cap | Người học ghi danh active quá hai khoá đang mở | zero | 0 | Quyết định học thích ứng: tối đa hai khoá cùng lúc, gác ở API. |
course-missing-catalog-row | Khoá ở họ chương trình mà thiếu dòng ở họ danh mục | zero | nợ 2 | Chuyển từ cổng deploy (verify-d1-after-migrate.mjs): khoá tồn tại trong chương trình mà không ai thấy ở danh sách. |
course-community-dangling | Khoá trỏ tới cộng đồng không tồn tại | zero | 0 | Chuyển từ cổng deploy. skill communities: khoá mất cộng đồng thì gác Pro không còn gì để gác. |
book-community-dangling | Sách trỏ tới cộng đồng không tồn tại | zero | 0 | Chuyển từ cổng deploy, cùng lý do với khoá. |
Ngân hàng câu hỏi
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
quiz-index-out-of-range | Câu quiz có correct_index ngoài mảng phương án | zero | 0 | Chuyển từ cổng deploy: người học chọn gì cũng sai, và không có lỗi nào hiện ra. |
quiz-not-four-options | Câu quiz không đủ bốn phương án | zero | 0 | Chuyển từ cổng deploy. |
quiz-cluster-formative | Ngân hàng tự kiểm có đáp án dồn một vị trí | zero | nợ 135 | known-risks "Đáp án đúng dồn vào một vị trí" (04.09.2026): quiz vẫn đủ câu, vẫn chấm, chỉ thôi dò được lỗ hổng. |
quiz-cluster-big-idea | Quiz Big Idea của unit có đáp án dồn một vị trí | zero | nợ 30 | Cùng sự cố trên. Ba câu Big Idea rút ngẫu nhiên nhưng phương án không xáo. |
quiz-summative-missing | Lesson đang hoạt động không có ngân hàng quiz chốt | ratchet | 219 | Chuyển từ cổng deploy (mốc 338 ở đó). Nợ có sẵn: cấm tăng, không cấm tồn tại. |
quiz-summative-short | Ngân hàng quiz chốt ít câu hơn một lượt quiz | zero | nợ 5 | Một lượt quiz rút LESSON_QUIZ_SIZE câu; ngân hàng thiếu thì người học nhận đề ngắn hơn mà không ai báo. |
quiz-formative-retired | Lesson từng có ngân hàng tự kiểm mà nay không còn câu active nào | zero | 0 | known-risks "Hai ngân hàng câu hỏi" (09.09.2026). Thay quiz-formative-missing (14.10.2026, xem Bẫy khi dựng): đếm dấu vết của hỏng, không đếm khoảng trống theo thiết kế. |
quiz-cjk | Câu quiz đang dùng có chữ Hán, kana hoặc Hangul lẫn vào | zero | nợ 12 | known-risks "Hai ngân hàng câu hỏi" (09.09.2026): vết model lúc sinh, bốn câu đã cho nghỉ khi đó. |
Chữ và slug
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
text-emdash | Dấu gạch dài trong cột nội dung chính | zero | 0 | CLAUDE.md LUẬT CHỮ (28.09.2026): migration 20261002a dọn D1; gác check:emdash chỉ soi repo, không soi D1. |
slug-not-ascii | Slug ngoài a-z0-9- | zero | 0 | Sổ lỗi Cloudflare C1 (13.10.2026): slug Job có chữ có dấu, trang JTBD trả 502 cho mọi lượt. Soi bảng mà SITE ĐỌC: tin tức là legacy_news_articles (qua getD1Client), không phải news_articles. |
text-cjk | Bài học có chữ Hán, kana hoặc Hangul lẫn vào | zero | nợ 1 | Cùng vết model với quiz-cjk, ở phần người học đọc dài nhất. |
Tài liệu đọc của lesson
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
lesson-doc-empty | Tài liệu đọc đã xuất bản mà không có chữ nào | zero | 0 | skill lesson-docs: doc_json chỉ đổi ở /publish; xuất bản một bản rỗng là người học mở ra trang trắng. |
lesson-doc-inline-image | Tài liệu đọc nhúng ảnh base64 | zero | 0 | rich-doc.ts chỉ nhận https; ảnh base64 đi vòng qua migration thì phình D1 và trang, và bộ lọc máy chủ bỏ ảnh đó ở lần lưu sau. |
lesson-doc-unknown-node | Tài liệu đọc có loại khối mà trình hiển thị không biết | zero | 0 | known-risks "Khối tuỳ biến của tài liệu lesson biến mất im lặng": RichDoc.tsx gặp loại lạ thì không vẽ gì. |
Sách
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
book-en-missing | Sách khai bản /en trong sitemap mà thiếu tên hoặc mô tả tiếng Anh | zero | 0 | www sitemap-books.ts khai /en cho mọi sách từ 08.10.2026 với điều kiện cột *_en đủ; thiếu là trang /en rơi về tiếng Việt. |
chapter-en-missing | Chương khai bản /en mà thiếu tên, câu hỏi hoặc Big Idea tiếng Anh | zero | 0 | www sitemap-book-chapters.ts khai /en cho mọi chương của sách active (PR #357). |
book-no-cover | Sách đã xuất bản không có bìa | zero | 0 | known-risks "Bìa đã vẽ mà không vào D1" (28.09.2026). |
book-section-empty | Sách đã xuất bản có section chỉ có tiêu đề | zero | 0 | known-risks "Section bị khoá hiện Nội dung đang được cập nhật" (27.09.2026). |
Cộng đồng và thư viện
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
library-preview-broken | Tài liệu thư viện cho xem thử mà không mở được | zero | 0 | skill communities Luật 10 (03.09.2026): bản xem thử trỏ tệp chưa có, người chưa mua không có gì để nếm. |
library-file-no-asset | Tài liệu thư viện dạng tệp/liên kết đang hiện mà không có tệp | zero | 0 | skill communities Luật 10: người vừa trả tiền bấm vào nhận 404. |
holding-community-locked | Cộng đồng chỗ chứa tạm không còn mở | zero | 0 | skill communities Luật 7: chỗ tạm mà khoá là một câu UPDATE đóng sập cả kho, im lặng. |
Xuất bản khác
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
mentors-none-published | Không còn mentor nào xuất bản | zero | 0 | known-risks "Mentor: admin sửa gì www cũng không đổi" (03.10.2026): khối mentor trang chủ rỗng mà không lỗi. |
playbook-wrong-family | Bảng không tiền tố seo_/news_ (site không đọc) đã quay lại | zero | 0 | skill playbook-pack: ghi vào bảng không tiền tố là câu lệnh hợp lệ, và site không bao giờ thấy bài mới. Từ 17.10.2026 sáu bảng đó đã gỡ (d1-cleanup), mục này bắt chúng quay lại. |
whats-new-not-compiled | Mục What's New trong git chưa có trong D1 | zero | 0 | known-risks "Hai bảng changelog, cả hai rỗng" (10.10.2026): JSON tuần là nguồn sự thật, migration mới là thứ chạy. |
Chỉ mục của Ran
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
ran-index-empty | Chỉ mục conan-knowledge-vi không có mẩu nào | zero | 0 | known-risks "Chỉ mục kiến thức trống, nhưng trợ lý vẫn trả lời trơn tru" (01.09.2026). |
ran-index-coverage | Phần trăm nội dung khoá học chưa vào chỉ mục Ran | ratchet | 65% | known-risks "Chỉ mục kiến thức trống": Ran trả lời trơn tru bằng kiến thức chung khi kho thiếu, không ai thấy. |
Máy sinh nội dung
| id | Canh gì | Loại | Mốc 14.10.2026 | Sự cố gốc |
|---|---|---|---|---|
content-run-stuck | Lượt sinh nội dung treo ở running quá 2 giờ | zero | 0 | known-risks "Lượt gọi model treo vô hạn". |
content-burst-dead | Chế độ dồn lực bật mà nhịp tim đã tắt | zero | 0 | known-risks "Cron dẫm lên nhau": cron nhường khi chuỗi còn đập; nhịp tắt mà mode vẫn burst là màn hình nói dối. |
content-quarantine | Artefact bị cách ly (quarantine) đang chặn | ratchet | 60 | Mỗi dòng là một chỗ máy sinh đã bỏ cuộc; số tăng là máy hỏng nhanh hơn người sửa. |
content-blockers-open | Finding mức blocker đang mở | ratchet | 25 | known-risks "Audit chỉ phủ thứ máy vừa sinh": finding mở mà không ai đọc là audit chạy cho có. |
content-failure-rate | Hơn 20% lượt sinh nội dung thất bại trong 24 giờ | zero | 0 | known-risks "Vỏ bao của nhà cung cấp: lỗi tưởng là model kém"; giá trị là số lượt hỏng khi vượt ngưỡng. |
content-timestamp-unparseable | Mốc created_at mà julianday() không đọc được, ở ba bảng hàng audit so | zero | 0 (thêm 14.10.2026) | known-risks "Audit chỉ phủ thứ máy vừa sinh" và content-ops, bẫy so mốc bằng chuỗi: hàng queue-stale-audits so bằng julianday(), mốc không đọc được thành NULL và khoá đó không bao giờ được audit lại. |
content-audit-overdue | Khoá có nội dung mới hơn audit quá 24 giờ | zero | 0 (thêm 14.10.2026) | known-risks "Audit chỉ phủ thứ máy vừa sinh" và content-ops, bước audit nằm sau một lệnh return: hàng rút 40 khoá mỗi lượt, 8 lượt một ngày, nên chờ quá một ngày là hàng không chạy. Lúc paused thì 0 kèm ghi chú số khoá chờ. |
Thứ BÌNH THƯỜNG, không được báo
Mỗi mục dưới đây từng trông như dữ liệu bẩn, và một gác báo nó sẽ bị tắt sau vài đêm kêu oan:
- Ghi danh
activevào khoáarchived(known-risks).enrollment-over-capchỉ đếm khoá đang mở. - Sách mức 1 chưa publish.
book-section-emptychỉ đếm sách đã publish. - Tài liệu thư viện
asset_urlNULL khiis_preview=0(1.094 tài liệu khung).library-preview-brokenchỉ soi tài liệu cho xem thử. - Lesson chưa có ngân hàng tự kiểm (pack không lấp
formative). Không đếm: nó lớn lên mỗi lần một khoá pack đúng luật lên active.quiz-formative-retiredchỉ đếm ngân hàng từng có mà nay mất. - Thư thử. Thư admin bấm "gửi thử" (
ref_id='admin-test', manguser_idthật của admin) và lượt gửi thử toàn danh mục (ref_id='smoke') không phải thư cho người dùng. Mọi mục email đếm theo người nhận lọc quaREAL_SEND; mục nào cố ý đếm cả thư thử phải khaitestSends: '<lý do>', tự kiểm bắt mục email thiếu cả hai. - Đáp án quiz chốt dồn một vị trí. Không đo:
/lesson-quiz/startxáo phương án mỗi lượt, nên vị trí lưu trong D1 không tới tay người học. Ba câu tự kiểm và quiz Big Idea thì KHÔNG xáo, nên hai ngân hàng đó có mục riêng.
Lần đo đầu (14.10.2026)
Đo bằng --wrangler, tuần tự, 70 câu, tổng thời gian SQL khoảng 1 giây (câu nặng nhất là quét chữ Hán trong 43 nghìn câu quiz, ~240 ms). Mười chín mục có mốc khác 0; những cái đáng đọc nhất:
| Mục | Số | Nghĩa là gì |
|---|---|---|
ran-index-coverage | 65% | Ran chỉ biết 1.499/5.751 bài viết, 799/2.097 khái niệm, 499/689 unit, 200/227 khoá. Bộ thu thập trong workflows/ran-knowledge-index.ts dùng LIMIT không ORDER BY, phần vượt trần không bao giờ vào chỉ mục, và Ran vẫn trả lời trơn tru. |
course-retire-half | 17 | 16 khoá đã nghỉ ở danh mục mà courses.status vẫn active: 11 trong số đó có unit nên vẫn nằm trong /sitemap/courses của api dưới slug tiếng Anh (phép nối slug chuẩn đòi l.is_active = 1), và cả 16 vẫn là nguồn hợp lệ của chỉ mục Ran. ai-teen thì ngược lại: legacy_courses.is_active=1 mà đã archived, nên /api/public/courses (chỉ lọc is_active) vẫn trả nó. |
ran-run-orphan | 16 | Cron 7 * * * * nổ hai lần trong một giờ (18.09 tới 11.10.2026); index.ts chèn dòng sổ TRƯỚC khi tạo workflow, lượt thứ hai bị "already exists" nuốt và dòng của nó running vĩnh viễn. |
claim-waiting | 12 | 12 đơn kích hoạt needs_review, cũ nhất 70 ngày. |
quiz-cjk | 12 | 12 câu quiz đang dùng có chữ Hán (7 khoá, sinh 27-31.08.2026). |
quiz-cluster-formative | 135 | 135/252 ngân hàng tự kiểm dồn đáp án, 41 bài quá nửa ở một vị trí. |
user-without-profile | 2 | resolveLocalIdentity (src/shared/d1.ts) tạo dòng users mà không gọi ensureLegacyUserProfile. |
daily-email-unfinished | 1 | Lượt gửi 20.09.2026 giành khoá rồi chết; hôm đó không ai nhận thư hằng ngày, sổ không có gì đỏ. |
cron-daily-email | 0 (ngoài cửa sổ) | Ngày 05.10.2026 lỡ hẳn: mọi cron của api ngừng 25 giờ (04.10 06:07 tới 05.10 07:07 UTC, đo trên scheduled_run_locks, ran_knowledge_runs, course_content_runs), nguyên nhân chưa rõ. Không có dòng daily_email_runs, không thư hằng ngày nào đi. |
Danh sách đầy đủ kèm ghi chú từng khoản nợ nằm ở trường note của file mốc.
Thêm một bất biến
- Thêm một mục vào
CATALOGtrongscripts/lib/d1-invariants-catalog.mjs:idchữ thường nối gạch không chữ số,titletiếng Việt,kind,sqltrả cộtn(thêm cộtnotenếu cần giải thích một giá trị đặc biệt),whytrỏ về sự cố thật. - SQL: một câu
SELECT/WITH; thời gian luôn quajulianday(); cửa sổ tính từ'now'; đọc hằng số hay danh sách từ mã nguồn quactx(xemctx.constant,ctx.engagementKinds) thay vì chép số. Đọc thêm file repo nào thì thêm vàoREPO_FILESvà sparse checkout của workflow. node scripts/d1-invariants.mjs --self-test, rồi đo thật:--wrangler --only <id>. Ghi lạimeta.duration; câu quét chữ trên bảng lớn thì đo trước khi thêm.- Khác 0 lúc thêm: sửa dữ liệu (bằng migration, luật "repo trước, D1 sau"), hoặc ghi nợ bằng
--update-baseline --allow-raise "<nguồn gốc của con số>".ratchetmới thì--update-baselinetự khởi tạo ở giá trị hiện tại. - Thêm một dòng vào bảng danh mục ở trang này.
Mốc chỉ được hạ
--update-baseline hạ mọi mốc mà giá trị đo thấp hơn, xoá nợ của mục zero về 0, và từ chối mọi lần nâng: tăng mốc ratchet, hay một mục zero đang sạch bỗng khác 0. Muốn nâng phải --allow-raise "<lý do>", và lý do đi vào history của chính file mốc, ai đọc diff cũng thấy. Lỗi truy vấn không bao giờ đụng mốc. Khi job in notice "thấp hơn mốc", hạ trong PR kế tiếp, nếu không thì khoảng trống giữa số thật và mốc thành chỗ để số xấu đi mà không ai biết.
Vì sao GitHub Actions, không phải một cron trong api
- Người canh không được chết cùng thứ nó canh. Cron của Worker chạy trong chính api; api đóng băng hay deploy hỏng (02-03.09.2026) thì vòng canh cũng mất, đúng lúc cần nó nhất.
- Binding D1 của Worker luôn đọc-ghi. Không có cách nào khoá một cron trong Worker vào chỉ đọc. Qua REST với token chỉ có D1 Read thì D1 tự từ chối ghi ở máy chủ: hàng rào không nằm trong mã của ai cả.
- Lỗi phải vào sổ lỗi. Sổ là issue GitHub, đọc được khi chính api đang hỏng; job Actions đỏ đi thẳng vào đó qua
workflow_run, không cần dựng đường ghi nào mới. - Không thêm nhánh nào vào hàm
scheduledvốn đã gánh bốn cron.
Vì sao không nằm trong job daily của sổ lỗi
Job daily của error-ledger.yml đỏ mỗi ngày vì token thiếu Zone Analytics Read. Một bất biến đỏ thật bên trong một job vốn đã đỏ thì không ai thấy: đỏ thành màu nền. Workflow riêng có trạng thái riêng, và mỗi lượt đỏ của nó là tin mới.
Vì sao không nằm trong cổng deploy
Dữ liệu trôi giữa hai lượt deploy (người soạn sửa ở admin, cron ghi) mà làm đỏ bước kiểm trước deploy thì api đóng băng, đúng hình dạng sự cố 02-03.09.2026, và người cần đẩy một bản sửa mã bị chặn bởi dữ liệu của người khác. Từ 14.10.2026 api.conan.school/scripts/verify-d1-after-migrate.mjs chỉ còn so sổ schema_migrations với file theo tên (bản cũ so số đếm: một file chưa áp cộng một dòng ma cho cùng con số). Bốn phép kiểm nội dung và hai mốc nợ của nó chuyển sang đây, cùng câu SQL.
Token
Job đọc CLOUDFLARE_D1_READ_TOKEN; chưa có thì rơi về CLOUDFLARE_API_TOKEN (token deploy, có quyền ghi) và in ::warning:: mỗi đêm. Tạo token đọc riêng:
- Cloudflare dashboard → My Profile → API Tokens → Create Token → Create Custom Token.
- Permissions: Account → D1 → Read. Không thêm quyền nào khác. Account Resources: tài khoản Conan.
- GitHub → Settings → Secrets and variables → Actions → New repository secret, tên
CLOUDFLARE_D1_READ_TOKEN. - Chạy tay workflow
d1-invariants; dòng::warning::về token phải biến mất.
Từ 02.05.2025, HTTP API của D1 đòi D1 Edit cho mọi lệnh ghi, nên token chỉ có D1 Read không ghi được gì kể cả khi gác SQL có lỗ. Khi đã có token đọc, workflow không truyền token deploy vào bước chạy nữa.
Sổ lỗi: một bất biến, một issue
- Mỗi vi phạm là một
::error title=d1:<id>::<thông điệp>.error-ledger.mjslấy mọi annotation có titled1:*(trước đây nó chỉ lấy dòng lỗi đầu tiên của job), dấu vân tay chỉ theo id: con số đổi mỗi đêm, hay đổi từ vi phạm sang lỗi truy vấn, vẫn là một issue. Mỗi đêm còn đỏ thêm một dòng bình luận có con số. - GitHub giữ tối đa 10 annotation lỗi mỗi step. Quá 10 thì 9 mục đầu có annotation riêng, phần còn lại gộp vào
d1:many; bảng đầy đủ luôn ở job summary. - Bốn title đặc biệt:
d1:empty-source(nguồn rỗng),d1:source(mọi câu cùng lỗi, một issue thay vì một issue mỗi mục),d1:setup(thiếu token, sai tham số),d1:many. migrations-not-appliedbỏ qua lượt chạy (có in ra) khideploy-apiđang chạy, vì lúc đó sổ chưa kịp ghi. Không đọc được trạng tháideploy-apithì vẫn kiểm: thà đỏ oan một lần.
Nhịp hằng ngày: đếm ngày lỡ, không đếm giờ
Bản đầu canh vòng thư hằng ngày và báo cáo vận hành bằng liveness 26 giờ. Job đêm chạy 01:47 UTC, chỉ 47 phút sau nhịp 0 1 * * *, nên một ngày lỡ đứng ở 24,8 giờ: xanh. Đêm sau nhịp đã chạy lại, 0,8 giờ: xanh. Không ngưỡng giờ nào vừa bắt được một ngày lỡ vừa không kêu oan khi cron trễ vài phút. Chạy giả lập trên số thật cho ngày 05.10.2026: mục cũ 24,78 giờ rồi 0,77 giờ, mục mới đỏ lỡ 2026-10-05 ba đêm liền (05, 06, 07.10) rồi tự hết.
Nay hai mục đó đếm ngày (giờ VN) đã tới hạn mà không có dòng sổ, trong ba ngày gần nhất, kể cả hôm nay nếu đã qua 01:30 UTC. Ba ngày đủ để một ngày lỡ không lọt kể cả khi GitHub bỏ một lượt cron, và không đủ để một ngày đã qua thành tiếng ồn cả tuần. Liveness theo giờ vẫn đúng cho cron hằng giờ.
Những chỗ hỏng mà hệ vẫn trông như đang chạy, gặp khi dựng
- Bảng tóm tắt in "0 đỏ" khi nguồn hỏng. Token sai thì câu chặn nguồn rỗng đỏ và mọi mục bị bỏ qua; bản đầu đếm "đỏ" chỉ trong các mục, nên tiêu đề job summary ghi
0 đỏtrên một bảng toàn "bỏ qua". Đã sửa, và có mẫu tự kiểm. - Một lượt mạng thoáng qua làm đỏ cả đêm. Lần chạy xác nhận cuối cùng (14.10.2026), câu chặn nguồn rỗng gặp "A request to the Cloudflare API failed" một lần, cả 70 mục bị bỏ qua và job đỏ với
d1:empty-source. Chạy lại ngay thì qua. Nay mỗi câu được thử lại đúng một lần sau 2 giây; lỗi thật thì lần hai cũng hỏng và vẫn đỏ. - Sổ lỗi gộp 20 bất biến thành một issue.
cichỉ lấy annotation lỗi đầu tiên của mỗi job; API annotation trả mặc định 30 dòng một trang. Cả hai đã sửa. - Chuẩn hoá chữ số của sổ lỗi. Hai id chỉ khác nhau ở chữ số sẽ ra cùng thông điệp chuẩn hoá; id mang chữ số bị tự kiểm từ chối (chính lần đầu chạy đã bắt
lesson-doc-base64). MAX(started_at)trên cột trộn hai dạng thời gian so theo thứ tự chữ:' 'đứng trước'T'. Mọi mục dùngMAX(julianday(...)).- Nhánh cũ báo dòng ma. Chạy ở máy trên một nhánh tách trước migration mới nhất thì sổ có dòng không thấy file. Vì vậy
migrations-ghost-rowslàwarn. - Cổng deploy đếm thay vì so tên (xem trên).
- Thư thử của admin trông y như thư thật.
POST /api/admin/emails/testghiemail_deliveriesvớiuser_idcủa admin,ref_id='admin-test',audit_ofNULL. Bản đầu không lọc nó: ngày 07.09.2026 admin gửi thửmembership_activatedhai lần, vàemail-ref-duplicatesẽ đỏ suốt bảy ngày; xem trướcprofile_incompletebốn lần làemail-nudge-max(không có cửa sổ thời gian) đỏ vĩnh viễn. Đã sửa bằngREAL_SEND, và tự kiểm đối chiếu từng nhãn với file đặt ra nó. - Soi nhầm họ bảng.
slug-not-asciibản đầu soinews_articles, bảng không tiền tố mà không route nào phục vụ;/newsđọclegacy_news_articlesquagetD1Client. Mục xanh mà không kiểm gì cho tin tức. Đúng cái bẫy "hai họ bảng" của data-model (skillapi-service). - Mốc chặn trên thứ lớn lên theo thiết kế.
quiz-formative-missing(1.956) đếm lesson chưa có ngân hàng tự kiểm, mà pack không bao giờ lấp ngân hàng đó: mỗi khoá pack lên active là đỏ, và lối thoát duy nhất là--allow-raisemỗi lần. Thay bằngquiz-formative-retired.
Chưa làm
- Mẩu chỉ mục Ran trỏ tới nội dung đã nghỉ (530 dòng
indexedkhông còn nguồn đủ điều kiện, đo 14.10.2026): chưa biết Ran có lọc chúng khi trả lời không, nên chưa thành mục. - Bất biến cho D1 của tool-plane: đi đường
provision-tool-plane.yml, ngoài phạm vi trang này.
Xem thêm: skill d1-invariants, sổ lỗi tự động, guardrails và quét chất lượng đêm, known-risks.