Skip to content

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_TOKEN chư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
Scriptscripts/d1-invariants.mjs, Node 22, không phụ thuộc, không npm ci
Danh mụcscripts/lib/d1-invariants-catalog.mjs (id, tiêu đề, loại, SQL, sự cố gốc)
Mốcscripts/d1-invariants-baseline.json (chỉ được hạ, mọi lần nâng có lý do trong history)
Lỗi đi đâuerror-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):

bash
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ạng

Bốn loại ​

LoạiĐỏ khiDùng cho
zerokhác 0 (hoặc vượt mốc nợ, nếu đang nợ)thứ không bao giờ đúng khi khác 0
ratchetvượt mốcnợ có sẵn mà bản thân nó không sai (lesson chưa có ngân hàng quiz chốt)
livenesssố giờ từ nhịp gần nhất vượt ngưỡng, hoặc chưa từng có nhịpcron hằng giờ, máy sinh, đường gửi thư
warnkhông bao giờ; chỉ in noticetí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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
migrations-not-appliedFile migration trong git chưa có trong sổ schema_migrationszero0known-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-rowsDòng trong sổ schema_migrations không còn file trong gitwarnghi 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-revivedBảng đã RENAME mọc lại dưới tên cũzero0known-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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
cron-event-remindersCron hằng giờ (nhắc sự kiện) còn đậplivenessngưỡ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-reportNgày lỡ báo cáo vận hành (3 ngày qua)zero0Cron 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-emailNgày lỡ vòng một-thư-mỗi-ngày (3 ngày qua)zero0Ngà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-unfinishedLượt gửi hằng ngày bắt đầu mà không ghi xongzeronợ 1daily.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-indexVòng nạp chỉ mục Ran còn chạylivenessngưỡ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-stuckLượt nạp chỉ mục Ran treo ở running quá 2 giờzero0Workflow chết mà sổ vẫn running: màn hình trạng thái báo "đang nạp" mãi.
ran-run-orphanDòng ran_knowledge_runs mồ côi do cron nổ hai lần trong một giờzeronợ 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-failedLượt nạp chỉ mục Ran thất bại trong 24 giờzero0511 lượt failed tới 26.09.2026 mà không ai được báo.
cron-content-opsMáy sinh nội dung còn chạylivenessngưỡ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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
email-daily-twiceMột người nhận hai thư gắn kết trong một ngày (7 ngày qua)zero0known-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-duplicateCùng loại thư, cùng bản ghi, gửi hai lần cho một người (7 ngày qua)zero0reminders.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-spacingThư nhắc hồ sơ gửi sát nhau hơn khoảng cách tối thiểu (30 ngày qua)zero0known-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-maxMột người nhận quá ba thư nhắc hồ sơzero0Danh mục email hứa "nhắc tối đa ba lần".
email-failed-allCó thư thất bại mà không thư nào đi được trong 24 giờzero0known-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-failedThư thất bại trong 24 giờwarnghi 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-silenceHệ còn gửi được thưlivenessngưỡ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-unsubscribeThư gắn kết gửi cho người đã huỷ nhậnzero0known-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-inactiveThư gắn kết gửi cho tài khoản không hoạt động (7 ngày qua)zero0daily.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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
user-without-profileTài khoản hoạt động không có hồ sơ nghiệp vụ ở legacy_userszeronợ 2known-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ízero0Cù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ờzeronợ 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-noneKhông còn gói thành viên nào đang bánzero0known-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ó Prozero0skill 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-expiryThành viên Pro không có hạn dùngzero0skill 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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
course-retire-halfHai họ bảng khoá học bất đồng khoá còn sống hay đã nghỉzeronợ 17known-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-collisionHai khoá đang sống chung một slug chuẩnzero0known-risks "Một khoá, hai slug, hai trang" (08.09.2026): slug chuẩn là COALESCE(l.slug, c.slug).
course-slug-shadowSlug phụ của một khoá trùng slug chuẩn của khoá kháczero0Mọ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-unitsKhoá hiện ở danh mục mà không có unit ở họ bảng nàozero0known-risks "Trang trả 200, khung đầy đủ, ruột rỗng" (08.09.2026).
course-active-no-unitsKhoá active ở course-intelligence mà không có unitzeronợ 5known-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-thinUnit có ít hơn ba lesson đang hoạt độngzeronợ 18skill course-pack: mỗi unit tối thiểu ba lesson.
lesson-emptyLesson đang hoạt động không có chữ nào (không định nghĩa, bài viết, tài liệu)zeronợ 39known-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-duplicateHai lesson cùng tên trong một khoázeronợ 6known-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-capNgười học ghi danh active quá hai khoá đang mởzero0Quyết định học thích ứng: tối đa hai khoá cùng lúc, gác ở API.
course-missing-catalog-rowKhoá ở họ chương trình mà thiếu dòng ở họ danh mụczeronợ 2Chuyể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-danglingKhoá trỏ tới cộng đồng không tồn tạizero0Chuyể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-danglingSách trỏ tới cộng đồng không tồn tạizero0Chuyển từ cổng deploy, cùng lý do với khoá.

Ngân hàng câu hỏi ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
quiz-index-out-of-rangeCâu quiz có correct_index ngoài mảng phương ánzero0Chuyể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-optionsCâu quiz không đủ bốn phương ánzero0Chuyển từ cổng deploy.
quiz-cluster-formativeNgân hàng tự kiểm có đáp án dồn một vị trízeronợ 135known-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-ideaQuiz Big Idea của unit có đáp án dồn một vị trízeronợ 30Cù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-missingLesson đang hoạt động không có ngân hàng quiz chốtratchet219Chuyể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-shortNgân hàng quiz chốt ít câu hơn một lượt quizzeronợ 5Mộ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-retiredLesson từng có ngân hàng tự kiểm mà nay không còn câu active nàozero0known-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-cjkCâu quiz đang dùng có chữ Hán, kana hoặc Hangul lẫn vàozeronợ 12known-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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
text-emdashDấu gạch dài trong cột nội dung chínhzero0CLAUDE.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-asciiSlug ngoài a-z0-9-zero0Sổ 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-cjkBài học có chữ Hán, kana hoặc Hangul lẫn vàozeronợ 1Cù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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
lesson-doc-emptyTài liệu đọc đã xuất bản mà không có chữ nàozero0skill 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-imageTài liệu đọc nhúng ảnh base64zero0rich-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-nodeTài liệu đọc có loại khối mà trình hiển thị không biếtzero0known-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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
book-en-missingSách khai bản /en trong sitemap mà thiếu tên hoặc mô tả tiếng Anhzero0www 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-missingChương khai bản /en mà thiếu tên, câu hỏi hoặc Big Idea tiếng Anhzero0www sitemap-book-chapters.ts khai /en cho mọi chương của sách active (PR #357).
book-no-coverSách đã xuất bản không có bìazero0known-risks "Bìa đã vẽ mà không vào D1" (28.09.2026).
book-section-emptySách đã xuất bản có section chỉ có tiêu đềzero0known-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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
library-preview-brokenTài liệu thư viện cho xem thử mà không mở đượczero0skill 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-assetTài liệu thư viện dạng tệp/liên kết đang hiện mà không có tệpzero0skill communities Luật 10: người vừa trả tiền bấm vào nhận 404.
holding-community-lockedCộng đồng chỗ chứa tạm không còn mởzero0skill 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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
mentors-none-publishedKhông còn mentor nào xuất bảnzero0known-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-familyBảng không tiền tố seo_/news_ (site không đọc) đã quay lạizero0skill 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-compiledMục What's New trong git chưa có trong D1zero0known-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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
ran-index-emptyChỉ mục conan-knowledge-vi không có mẩu nàozero0known-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-coveragePhần trăm nội dung khoá học chưa vào chỉ mục Ranratchet65%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 ​

idCanh gìLoạiMốc 14.10.2026Sự cố gốc
content-run-stuckLượt sinh nội dung treo ở running quá 2 giờzero0known-risks "Lượt gọi model treo vô hạn".
content-burst-deadChế độ dồn lực bật mà nhịp tim đã tắtzero0known-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-quarantineArtefact bị cách ly (quarantine) đang chặnratchet60Mỗ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-openFinding mức blocker đang mởratchet25known-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-rateHơn 20% lượt sinh nội dung thất bại trong 24 giờzero0known-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-unparseableMốc created_at mà julianday() không đọc được, ở ba bảng hàng audit sozero0 (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-overdueKhoá có nội dung mới hơn audit quá 24 giờzero0 (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 active vào khoá archived (known-risks). enrollment-over-cap chỉ đếm khoá đang mở.
  • Sách mức 1 chưa publish. book-section-empty chỉ đếm sách đã publish.
  • Tài liệu thư viện asset_url NULL khi is_preview=0 (1.094 tài liệu khung). library-preview-broken chỉ 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-retired chỉ đế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', mang user_id thậ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 qua REAL_SEND; mục nào cố ý đếm cả thư thử phải khai testSends: '<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/start xá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ụcSốNghĩa là gì
ran-index-coverage65%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-half1716 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-orphan16Cron 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-waiting1212 đơn kích hoạt needs_review, cũ nhất 70 ngày.
quiz-cjk1212 câu quiz đang dùng có chữ Hán (7 khoá, sinh 27-31.08.2026).
quiz-cluster-formative135135/252 ngân hàng tự kiểm dồn đáp án, 41 bài quá nửa ở một vị trí.
user-without-profile2resolveLocalIdentity (src/shared/d1.ts) tạo dòng users mà không gọi ensureLegacyUserProfile.
daily-email-unfinished1Lượ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-email0 (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 ​

  1. Thêm một mục vào CATALOG trong scripts/lib/d1-invariants-catalog.mjs: id chữ thường nối gạch không chữ số, title tiếng Việt, kind, sql trả cột n (thêm cột note nếu cần giải thích một giá trị đặc biệt), why trỏ về sự cố thật.
  2. SQL: một câu SELECT/WITH; thời gian luôn qua julianday(); cửa sổ tính từ 'now'; đọc hằng số hay danh sách từ mã nguồn qua ctx (xem ctx.constant, ctx.engagementKinds) thay vì chép số. Đọc thêm file repo nào thì thêm vào REPO_FILES và sparse checkout của workflow.
  3. node scripts/d1-invariants.mjs --self-test, rồi đo thật: --wrangler --only <id>. Ghi lại meta.duration; câu quét chữ trên bảng lớn thì đo trước khi thêm.
  4. 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ố>". ratchet mới thì --update-baseline tự khởi tạo ở giá trị hiện tại.
  5. 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 scheduled vố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:

  1. Cloudflare dashboard → My Profile → API Tokens → Create Token → Create Custom Token.
  2. Permissions: Account → D1 → Read. Không thêm quyền nào khác. Account Resources: tài khoản Conan.
  3. GitHub → Settings → Secrets and variables → Actions → New repository secret, tên CLOUDFLARE_D1_READ_TOKEN.
  4. 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.mjs lấy mọi annotation có title d1:* (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-applied bỏ qua lượt chạy (có in ra) khi deploy-api đang chạy, vì lúc đó sổ chưa kịp ghi. Không đọc được trạng thái deploy-api thì 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. ci chỉ 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ùng MAX(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-rows là 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/test ghi email_deliveries với user_id của admin, ref_id='admin-test', audit_of NULL. Bản đầu không lọc nó: ngày 07.09.2026 admin gửi thử membership_activated hai lần, và email-ref-duplicate sẽ đỏ suốt bảy ngày; xem trước profile_incomplete bốn lần là email-nudge-max (không có cửa sổ thời gian) đỏ vĩnh viễn. Đã sửa bằng REAL_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-ascii bản đầu soi news_articles, bảng không tiền tố mà không route nào phục vụ; /news đọc legacy_news_articles qua getD1Client. 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 (skill api-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-raise mỗi lần. Thay bằng quiz-formative-retired.

Chưa làm ​

  • Mẩu chỉ mục Ran trỏ tới nội dung đã nghỉ (530 dòng indexed khô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.

Tài liệu nội bộ nền tảng Conan School.