Skip to content

SDD-038 · Bài tập năm bậc và trạng thái ​

Một phần của SDD-038 Cổng IELTS. Phần đầu là đặc tả đang áp dụng, mỗi mục giữ số § cũ; diễn biến và lý do các vòng quyết định nằm ở mục Lịch sử quyết định cuối trang.

5i. Năm bậc bài tập, mỗi bài một URL (SRC-850, 2026-09-19) ​

Vì sao cần một tầng nữa, chứ không chỉ thêm cột level vào câu hỏi ​

Trước bản này (§5f), một năng lực là MỘT thang dài: mở /ielts/skills/reading/main-idea ra là đi từ câu đầu tới câu cuối, không có chỗ dừng và không có gì để gửi cho nhau. Đặt level lên từng câu vẫn không giải được hai việc chỉ đạo vừa nêu:

  • "Mỗi bài tập đều có URL riêng" - URL phải trỏ vào một thứ CÓ RANH GIỚI. Một câu hỏi thì quá nhỏ (URL cho từng câu là hàng nghìn URL không ai gửi cho ai), còn cả một bậc thì quá to.
  • "Learners có thể filter để chọn ra bài tập" - lọc chỉ có nghĩa khi có nhiều VẬT THỂ để lọc. Lọc ra vài trăm câu rời thì learner vẫn đứng trước cùng một danh sách không biết bắt đầu từ đâu.

Nên ielts_micro_exercises đứng giữa năng lực và câu hỏi (migration 0250). Một bài tập là một buổi làm việc trọn vẹn: 5-10 đoạn, làm hết trong chừng mười phút, có điểm kết.

Thành phần từng bậc nằm trong dữ liệu, không nằm trong đầu người soạn ​

BậcThành phầnGhi chú
15 đoạn, đều tiếng ViệtThao tác là việc của tư duy, không phải của vốn từ
25 đoạn Việt + 5 đoạn Anh, bản Việt DỊCH TỪ bản AnhNội dung đã hiểu rồi, phần mới duy nhất là chữ
310 đoạn ngắn, đều tiếng AnhKhông còn bản dịch đi kèm
4Như bậc 2, đoạn dài hơnÝ trải qua nhiều câu
5Gần như đề thi thậtHỏi theo cách đề thật hỏi (Matching Headings...)

Bậc 2 và 4 là chỗ DUY NHẤT trong cả hệ có quan hệ giữa hai câu hỏi. Quan hệ ấy nằm ở cột pair, không nằm trong quy ước "seq n và seq n+5 thì là một cặp": quy ước ấy đúng cho tới bài đầu tiên có sáu đoạn Việt, và lúc sai thì nó sai im lặng - trang ghép nhầm hai đoạn không liên quan rồi nói với learner rằng đây là bản dịch của nhau. Đường đọc xếp theo COALESCE(pair, seq) rồi mới tới ngôn ngữ, nên bản Việt luôn đứng trước bản Anh của cùng cặp dù trang có xáo thứ tự.

level và stage là hai khái niệm, không phải một ​

§5f đã có stage 1-4 với ranh giới chặng 1-2 tiếng Việt, chặng 3-4 tiếng Anh. Năm bậc KHÔNG thay thế nó: level nói bài tập này khó tới đâu, stage nói câu này bằng thứ tiếng nào và ở độ dài nào. Lấy stage = level sẽ sinh ra câu tiếng Anh mang chặng 2 ở bậc 2 song ngữ - phá đúng ranh giới §5f dựng lên, và phá im lặng vì trên màn hình không có gì khác thường. Ánh xạ nằm ở stageOf() trong scripts/micro-seed.mjs.

Tỉ lệ Việt/Anh đổi, và đó là do chính quy cách này ​

§5f đặt luật "chặng tiếng Việt là chặng dài nhất" (chỉ đạo 2026-09-18: "luyện tập tiếp rất nhiều kỹ thuật trên tài liệu tiếng Việt"). Năm bậc cộng lại ra 15 câu Việt và 25 câu Anh mỗi năng lực, nên luật ấy không còn đúng được. Tiếng Việt KHÔNG bị cắt - nó tăng từ 10 lên 15; tiếng Anh mới là phần được thêm vào. Cổng kiểm tương ứng vì thế thu hẹp về đúng phần thang cũ thay vì bị nới ra hoặc bị xoá.

Nội dung soạn ở content/, không soạn thẳng vào migration ​

Lý thuyết nằm trong D1 vì nó cần sửa được từ màn admin (§5e). Bài tập thì ngược lại: một bậc 2 là năm đoạn Việt cộng đúng năm đoạn ấy bằng tiếng Anh, và quan hệ dịch giữa chúng phải đọc được bằng mắt lúc soạn - trong một màn admin từng ô một, không ai kiểm được rằng đoạn Việt số 3 thật sự là bản dịch của đoạn Anh số 3.

Nên nội dung sống ở content/ielts-micro/<skill>/<code>.json, scripts/micro-seed.mjs sinh migration, và scripts/check-micro-content.mjs gác khuôn: số cặp, ngôn ngữ theo bậc, độ dài đoạn, đáp án trỏ vào lựa chọn có thật. Ngưỡng độ dài lấy từ dải THẬT của bộ nội dung mẫu chứ không bốc số tròn, và giữa bậc 2 và bậc 4 cố ý để một khoảng hở - hai bậc chỉ khác nhau ở độ dài, nên hai dải chạm nhau là hai bậc chạm nhau.

Nội dung đã soạn (cập nhật 19.09.2026) ​

Toàn bộ 10 năng lực của phần ĐỌC đã có đủ nội dung năm bậc: main-idea, detail, evidence, inference, vocabulary, reference, purpose, tone, organization, summary. Mỗi năng lực 10 bài tập và 80 câu, cộng lại 100 bài tập và 800 câu, nạp bằng migrations/0252_micro_content.sql.

Cộng với 506 câu của thang bốn chặng (0234-0246) thì bảng ielts_micro_skill_items có 1.306 câu. Con số ấy được gác bằng một phép đếm tay trong routes.test.ts: một migration nạp thiếu hoặc nạp hai lần sẽ lộ ra ở đó, và mỗi lần soạn thêm nội dung thì con số phải được sửa - đó là chỗ người review nhìn thấy đã thêm bao nhiêu.

Ba ràng buộc được kiểm bằng SQL sau khi nạp, ngoài cổng check-micro-content chạy trên JSON:

  • không câu nào có stage lệch với lang (ranh giới của migration 0238 còn nguyên);
  • mọi số pair đều ứng với đúng hai câu, một Việt một Anh;
  • mọi answer đều trỏ vào một lựa chọn có thật.

Nghe, Nói và Viết (25 năng lực còn lại) chưa soạn.

42. Ba trạng thái của một bài: chưa làm · đang dở · đã xong (SRC-919, 20.09.2026) ​

Vế đã xong đã có từ trước: GET /v1/learners/{id}/ielts-micro-attempts đếm các dòng action='practice' trong learning_events theo tiền tố exercise:<skill>.<code>.l.

Vế đang dở thì không đọc được từ sổ ấy, và đó là hệ quả của một quyết định cũ còn đúng: một bài bỏ giữa chừng cố ý không ghi dòng nào, vì ghi lúc mở trang thì mở ra rồi đóng lại cũng thành "đã học" và chuỗi ngày không bỏ buổi (SRC-801) mất hết nghĩa.

Nên trạng thái dở có chỗ riêng: bảng ielts_micro_progress (migration 0267), một dòng cho mỗi cặp (learner, bài), giữ answered / total và picks - các lựa chọn đã chấm.

Vì sao giữ cả picks, không chỉ một con số. Một thẻ báo "In progress · 3/5" mà bấm vào lại phải làm từ câu 1 thì cái nhãn ấy chỉ tố cáo learner chứ không giúp gì. Có picks thì mở lại bài là về đúng câu chưa chấm đầu tiên, các câu cũ còn nguyên lời giải đã đọc.

Làm xong thì XOÁ dòng, không đánh dấu done=1. Trạng thái xong đã có một nguồn sự thật khác; một cột thứ hai nói cùng điều ấy là hai bản sao sẽ có ngày lệch nhau. Hệ quả phụ dễ chịu: bảng luôn nhỏ bằng số bài đang dở.

Ghi sau MỖI câu được chấm, không đợi lúc rời trang: learner rời bài bằng cách đóng tab hoặc tắt máy nhiều hơn là bằng một nút Thoát, và beforeunload vừa không chạy trên di động vừa không kịp gửi.

Ba trạng thái, ba hình trên lưới chọn bài, xếp theo mức đòi sự chú ý: đang dở (viền accent đặc) nổi hơn đã xong (nền accent nhạt), vì bài xong rồi thì việc của nó là lùi lại. Bài vừa dở vừa từng xong thì hiện cả hai nhãn - chúng nói hai điều khác nhau.

Một bẫy đã gài cổng: chưa dựng lại xong thì chưa được lưu. Thiếu cờ resumed thì lượt lưu đầu tiên ghi đè bản trên máy chủ bằng một bài trống, đúng vào lúc learner vừa mở lại bài đang dở.

44. Bộ lọc bậc gọn lại, và mô tả chỉ hiện khi được chọn (SRC-921, 20.09.2026) ​

44a. Năm thẻ hai dòng, mô tả xuống dưới ​

SRC-850 đặt mô tả vào trong thẻ với lý do: learner đọc mô tả để CHỌN bậc, nên mô tả phải có mặt trước lúc chọn. Lý do ấy đúng, nhưng cái giá của nó thì chỉ nhìn thấy khi trang đã đủ nội dung: năm đoạn văn ba dòng xếp thành lưới 3 cột chiếm trọn màn hình đầu, và learner phải cuộn qua toàn bộ phần giải thích mới thấy được bài tập đầu tiên. Thứ learner tới trang này để làm là làm bài, không phải đọc về năm bậc.

Bản này giữ cả hai: thẻ còn hai dòng (Level N + thành phần, ví dụ "5 đoạn Việt + 5 đoạn Anh") nên năm thẻ đứng vừa một hàng và đọc được trong một cái nhìn; đoạn why chuyển xuống dưới hàng thẻ và chỉ in cho bậc đang chọn. Dòng composition ở lại vì nó chính là thứ phân biệt bậc 2 với bậc 4 khi chưa bấm vào đâu cả.

Mô tả đứng ngoài thẻ chứ không nở ra bên trong thẻ: nở trong thẻ thì thẻ đang chọn cao gấp ba bốn thẻ kia và cả hàng lệch nhau mỗi lần learner đổi bậc.

Lưới là grid-cols-2 sm:grid-cols-3 lg:grid-cols-5. "Một hàng" là hình dạng trên màn rộng; ép năm cột xuống màn điện thoại thì mỗi thẻ rộng chừng 70px và nhãn gãy chữ.

44b. Hàng tag dạng câu hỏi rời khỏi trang năng lực ​

Hàng tag ấy là phần giao diện của SRC-895 §35 trên trang năng lực. Gỡ nó đi không gỡ mô hình hai chiều: /ielts/tasks/reading/<dạng> vẫn là lối vào đầy đủ của chiều dạng câu hỏi, thẻ bài ở trang năng lực vẫn mang nhãn dạng (L2 · Matching Headings), và kho bài vẫn là một kho hai nhãn. Thứ mất đi là một bộ lọc thứ hai đứng chen giữa bộ lọc bậc và kết quả, trên một trang mà việc chính là chọn bậc rồi bấm vào bài.

Lọc theo ?task=<mã> trên URL vẫn chạy dù không còn nút bấm nào đặt nó: một link đã lưu hoặc một đường dẫn sinh ra từ nơi khác vẫn cắt đúng kho như trước. Giữ nhánh lọc ấy rẻ hơn hẳn việc bỏ rồi dựng lại khi chiều dạng câu hỏi cần quay lại trang này.

44c. Một component, bốn kỹ năng ​

MicroSkill.tsx phục vụ cả Reading, Listening, Writing và Speaking, nên yêu cầu "làm cả với 3 trang tương tự của 3 kỹ năng còn lại" đã được đáp ứng bởi chính một chỗ sửa. Không có bản sao nào của bộ lọc này cần sửa theo.

46. Mở khoá bài tập theo thứ tự (SRC-923, 20.09.2026) ​

46a. Hai cửa, không phải một ​

Bậc: 1-3 mở sẵn; bậc 4 mở khi làm xong hết bậc 3, bậc 5 khi xong hết bậc 4. Bài trong bậc: chỉ bài đầu mở sẵn, bài sau mở khi bài liền trước đã xong.

Bậc 1-3 mở sẵn vì chúng là cùng một thao tác tư duy ở ba lớp ngôn ngữ (Việt · Việt rồi Anh · Anh, xem §5f): một learner đọc tiếng Anh trôi chảy không có lý do gì phải đi qua bậc 1 mới chạm được bậc 3. Bậc 4 và 5 đổi độ dài chứ không đổi ngôn ngữ, và vào thẳng đó khi chưa giữ nổi thao tác trên đoạn ngắn là học sai thứ.

"Đã xong" đọc từ số lần làm (ielts-micro-attempts), không từ tiến độ dở: nếu tính cả bài mở dở thì mở đủ mười bài là mở khoá cả bậc mà chưa làm câu nào.

46b. Luật ở một chỗ, thi hành ở hai chỗ ​

apps/learn/src/ielts/unlock.ts là hàm thuần, không React và không mạng. Lưới chọn bài khoá thẻ (thẻ khoá không bọc trong Link, nên Cmd+click cũng không lách được); trang làm bài hỏi lại cùng hàm ấy để chặn người gõ thẳng URL - bài đã làm một lần là đã nằm trong lịch sử trình duyệt.

Đọc không được danh sách bài thì mở: đây là một luật sư phạm, không phải một cổng an ninh, và để lỗi mạng chặn learner khỏi bài của chính họ là đổi sai loại rủi ro.

Bậc rỗng không chặn bậc sau - nếu không, một lỗ nội dung sẽ khoá cứng bậc 5 vĩnh viễn.

52. Dấu trạng thái trên mỗi thẻ việc (SRC-932, 21.09.2026) ​

52a. Trạng thái đã có từ §43; thứ thiếu là một chỗ cho MẮT ​

§43 (SRC-919) đã dựng đủ ba trạng thái và đủ đường dữ liệu: ielts-micro-attempts cho "đã xong", ielts_micro_progress cho "đang dở". Thứ chỉ đạo này nói tới không phải một dữ liệu còn thiếu, mà là cách nói: trạng thái ấy đang được nói bằng một dòng chữ nhỏ ở đáy thẻ ("In progress · 3/5", "✓ Done once") và bằng một sắc viền nhạt. Cả hai kênh đều đòi learner đọc kỹ TỪNG thẻ, trong khi việc họ đang làm là quét một lưới sáu tới mười thẻ để tìm chỗ bỏ dở. Chủ dự án nhìn đúng màn hình ấy và không thấy trạng thái nào - đó là phép đo.

SetMark (apps/learn/src/ielts/SetMark.tsx) là một ô 28px đứng cạnh tên việc, mang bốn hình:

Trạng tháiHìnhMàu
Khoáđĩa mang ổ khoátrung tính
Chưa làmvòng tròn nét đứt--muted-foreground 50%
Đang dởvòng tròn tô một phần đúng bằng phần việc đã chấmcoral (--accent)
Đã xongdấu tích nét trầncoral (--accent)

Chữ không mất đi, nó lui về đúng phần hình không nói được: mấy lần ("Done 3×") và còn bao nhiêu ("3/5 answered", "2/3 parts"). Chữ "In progress" và dấu ✓ trong nhãn thì bỏ, vì cái dấu vừa nói xong đúng hai điều ấy và nói to hơn.

52b. Ô dấu có mặt kể cả khi chưa làm ​

Một dấu lúc có lúc không thì mắt phải dừng lại ở từng thẻ để phân biệt "chưa làm" với "màn hình chưa kịp vẽ" - và lúc ấy cái dấu lấy mất đúng thứ nó sinh ra để cho. Vòng nét đứt là một chỗ trống CÓ CHỦ Ý, khác hẳn một khoảng trắng.

Vòng ấy vẽ bằng --muted-foreground chứ không bằng --border: trong thế giới tối (n12-deep, nơi cả sân luyện này sống) --border là trắng 16%, trên mặt thẻ gần như không nhìn thấy - mà một cái dấu không nhìn thấy thì không phải là một trạng thái.

Thẻ đề NIÊM PHONG (§50) không mang dấu: "mock" là một LOẠI thẻ chứ không phải một trạng thái của learner, và nhãn "Mock - sit it once" của nó nói đúng điều cần nói.

52c. Bốn hình, một màu: phân biệt bằng HÌNH chứ không bằng sắc độ (SRC-934, 21.09.2026) ​

--success / --state-solid là màu của mastery - "đã vững ở năng lực này", thang 5 bậc. "Tôi đã làm bài này rồi" không phải "tôi đã vững": một learner làm xong một bài và sai sạch vẫn là đã làm xong. Mượn màu mastery cho một phép đếm lượt là dạy mắt learner một nghĩa sai, rồi nghĩa ấy va vào chính thang mastery ở thẻ "Where am I strong?" của cùng sân luyện này.

Coral thì đúng vai: nó là điểm màu duy nhất của hệ, nghĩa là "learner có việc ở đây".

Vậy "xong" và "đang dở" cùng coral thì phân biệt bằng gì? Bằng hình, không bằng sắc độ. Một vòng tròn tô dở khác hẳn một dấu tích, và khác cả ở cỡ nhỏ lẫn từ khoảng cách của cả lưới. Đó đúng là thứ bản SRC-919 thiếu: nó phân biệt hai trạng thái bằng bg-accent/10 với bg-accent/5, mà mắt không đọc được "đậm hơn một chút" khi hai thẻ cách nhau nửa màn hình. Sửa đúng chỗ hỏng là đổi KÊNH, không phải đổi màu.

Dấu tích vẽ nét trần, không đĩa không viền: một đĩa đặc ở góc mỗi thẻ đã xong là một mảng màu lặp lại sáu tới mười lần một màn, đòi sự chú ý đúng bằng thẻ đang dở - trong khi việc của một bài đã xong là lùi lại. Ô 28px bao quanh giữ nguyên cho cả bốn trạng thái, để các dấu thẳng hàng nhau dù hình bên trong to nhỏ khác nhau.

Không hình nào dùng đỏ: REQ-UX-03.

§49c đã ghi thành luật rằng khu luyện đề và lưới bài tập phải nói cùng một ngôn ngữ hình. Nên ExamTests.tsx đổi CÙNG lượt, dùng CÙNG component - không phải một bản chép. Đó là lý do SetMark đứng riêng một file thay vì nằm trong MicroSkill.tsx: một bản chép là cách cái luật ở §49c hỏng lặng lẽ vào lần sửa màu tiếp theo.

52c-bis. Đĩa ĐẶC cho hai trạng thái đáng nhìn, và một tooltip (SRC-991, 22.09.2026) ​

Đây là một lượt đảo lại hai quyết định của §52c, và cần ghi rõ vì sao chứ không chỉ ghi rằng đã đảo.

§52c chọn dấu tích nét trần với lý do "một đĩa đặc lặp lại sáu tới mười lần một màn đòi sự chú ý đúng bằng thẻ đang dở, trong khi việc của một bài đã xong là lùi lại". Lo ấy có thật, nhưng nó trả lời sai câu hỏi. Thứ learner quét tìm trên lưới này không phải "chỗ nào yên tĩnh" mà là "chỗ nào tôi vào được BÂY GIỜ" - và bản cũ trả lời câu ấy bằng một vòng nét đứt mờ, tức là đặt hình yếu nhất trong cả bộ lên đúng trạng thái đáng mời gọi nhất. Ảnh chụp của chủ dự án cho thấy chính điều đó: ba thẻ đã xong và một thẻ mở sẵn, mà thẻ mở sẵn là thẻ khó nhìn ra nhất.

Bảng mới:

Trạng tháiHìnhMàu
Khoáđĩa viền rỗng mang ổ khoátrung tính
Sẵn sàng làmđĩa đặc mang mũi tên bắt đầunắng (--n12-sun-400)
Đang dởvòng tròn tô một phần đúng bằng phần việc đã chấmcoral (--accent)
Đã xongđĩa đặc mang dấu tíchcoral (--accent)

Luật đứng sau bảng: hai trạng thái đáng nhìn đều đặc, và chúng khác nhau ở MÀU chứ không ở độ đậm. Nắng = "vào đây được rồi", coral = "đã qua". Trạng thái không đáng nhìn (khoá) là trạng thái duy nhất còn viền rỗng. Cách phân biệt bằng hình của §52c vẫn giữ giữa "xong" và "đang dở" - một đĩa đặc khác hẳn một vòng tô dở.

Màu nắng là màu cảnh báo của hệ, không phải màu mastery, nên nó không va vào thang năm bậc ở thẻ "Where am I strong?" theo cách --success đã từng va (§52c). Vẫn không hình nào dùng đỏ (REQ-UX-03).

Nét bên trong đĩa vẽ bằng --background chứ không bằng ocean-900: trong thế giới tối ocean-900 đã bị kéo về bản SÁNG, nên nó thành nét sáng trên nền nắng sáng - đo được 1,74:1, và cổng AS-09.3.1 bắt được. --background là màu nền tối của chính thế giới ấy nên luôn là nét đậm, trên cả nắng lẫn coral.

Tooltip là lớp chữ, không phải lớp nghĩa. Mỗi đĩa mang một nét bên trong - mũi tên cho "bắt đầu", dấu tích cho "đã xong" - vì người dùng chạm không hover được bao giờ. Một hình chỉ có nghĩa khi hover là một hình không có nghĩa trên điện thoại.

Hai chuỗi cho mỗi trạng thái, khác thứ tiếng và đó là chủ đích:

Thứ tiếngĐi đâu
Nhãn trạng thái (Locked, Not started, In progress, 1 of 4, Done)Tiếng Anharia-label
Câu giải thích ("Mở rồi, vào làm được ngay.")Tiếng Việttooltip

Nhãn tiếng Anh theo cùng họ với Not checked / In progress / Checked ở khắp sân luyện (SRC-834/835), và ngắn để người dùng trình đọc màn hình nghe một từ thay vì một câu dài mỗi lần lướt qua một thẻ. Câu giải thích tiếng Việt theo SRC-953: đây là chữ Nemo nói về bài, không phải nội dung đề.

Ô dấu nằm trong một thẻ đã là link, nên TooltipTrigger dùng asChild - một button lồng trong link là HTML không hợp lệ. Cả bốn nhánh đi qua cùng một hàm bọc: quên bọc một nhánh thì đúng trạng thái ấy im lặng mất tooltip và không cổng nào bắt được.

Gác bằng e2e (ieltsMicroSkill.spec.ts), vì tooltip hỏng bằng cách không xuất hiện - và một thứ không xuất hiện thì không ảnh chụp nào bắt được. Ca ấy kiểm luôn hai thứ tiếng đứng đúng chỗ, nên một lượt "sửa lại cho đúng luật" dịch nhầm một chiều sẽ làm nó đỏ.

52c-ter. Trang rủ bạn có cửa (SRC-997, 22.09.2026) ​

/referral có từ SRC-450 và đủ mọi thứ - mã riêng cho từng người, bảy mốc hoạt động, ba lá thư tự động, sáu luật chống gian lận. Cái nó thiếu là một cửa: nó chỉ nằm trong menu của learn ở tầng ngoài, nên một learner sống trong sân IELTS không bao giờ đi ngang qua nó. Một tính năng đủ mã, đủ thư, đủ mốc mà không ai vào được thì bằng không.

Mục vào menu avatar, ở cả hai nơi cùng lượt: hub /ielts (App.tsx) và mọi trang bên trong (ielts/Shell.tsx). Sửa một nơi là dựng lại đúng cái bẫy SRC-872 đã gỡ - learner đi sâu vào một bài luyện rồi mất đường, phải quay ngược ra hub mới thấy lại, và một menu đổi nội dung theo trang đang đứng thì learner không học được nó chứa gì.

Nhãn tiếng Anh như cả sân luyện (SRC-834/835), dù trang đích nói tiếng Việt: đây là một nhãn điều hướng, không phải chữ Nemo giải thích bài.

Gác bằng e2e chứ không bằng mắt: đây là một mục nằm sau một cú bấm, nên nó biến mất bằng cách không có ở đó - không ảnh chụp màn hình nào bắt được. Ca kiểm đứng ở một trang sâu chứ không ở hub, vì hub có menu riêng còn các trang sâu mới là chỗ learner đứng lâu nhất.

Đích đã đổi, và cách đổi đúng hơn (SRC-1010). Bản đầu trỏ ra /referral của cổng learn chung, dưới nhãn "Invite a friend". SRC-1010 đưa trang ấy vào trong khung sân luyện (/ielts/referral, nhãn "Referral", đứng cạnh "Membership"), nên learner không còn bị bắn ra một trang sáng màu thuộc một cổng khác giữa chừng buổi học. Mục này giữ lại vì nó ghi cái lỗ mà SRC-997 tìm ra - một tính năng đủ mã, đủ thư, đủ mốc mà không có cửa - còn hình dạng cái cửa thì đọc ở SRC-1010.

52d. Một lỗi bố cục lộ ra nhờ cái dấu ​

Thẻ đề ở ExamTests.tsx khai flex flex-wrap items-center justify-between nhưng xếp DỌC: Card của shadcn mang sẵn flex flex-col, và flex-wrap không ghi đè flex-col vì chúng là hai thuộc tính khác nhau. Khi thẻ chỉ có một dòng chữ thì không ai thấy; thêm cái dấu vào là nó rơi xuống dưới tên đề, giữa thẻ. Sửa bằng cách viết flex-row ra thay vì trông vào mặc định.

Loại lỗi này không cổng nào bắt được (typecheck xanh, build xanh, check-shadcn-tokens xanh) và cũng không e2e nào bắt được nếu chỉ hỏi "chữ có hiện không". Nó chỉ lộ ra khi có người dựng trang lên và nhìn.

52e. Gác bằng aria-label, không gác bằng tên class ​

Bộ e2e (apps/learn/e2e/ieltsMicroSkill.spec.ts) dựng một lưới có đủ bốn trạng thái đứng cạnh nhau - bậc 2 có ba bài (xong · dở · khoá, vì "dở" không mở khoá bài sau) và bậc 3 có một bài chưa ai đụng - rồi đọc aria-label của dấu kề ngay sau tên bài. Đọc theo tên bài chứ không đếm số dấu toàn trang: bốn cái dấu đặt lộn thẻ vẫn cho ra đúng bốn dấu.

53. Xong là chấm hết câu, và bộ lọc bậc luôn có đúng một bậc (SRC-933, 21.09.2026) ​

a. Hai mốc bị gộp làm một, và đó là lý do bài xong không được ghi nhận ​

MicroExercise.tsx có hai thứ khác nhau mà bản trước gộp vào một biến finished:

NghĩaViệc của nó
allAnsweredmọi câu đã được chấmbài ĐÃ XONG, vào sổ
atEndlearner đang đứng ở màn kếtmột VỊ TRÍ trên màn hình

Điều kiện ghi sổ cũ là answered === total && index >= total, tức là đòi thêm một cú bấm "Tiếp →" sau câu cuối. Ai chấm xong câu cuối rồi đóng tab thì làm đủ bài mà learning_events trống: thẻ vẫn báo chưa làm, và theo luật mở khoá SRC-923 thì bài kế tiếp cũng vẫn khoá. Cú bấm ấy không thêm gì vào việc học, nên nó không được phép là điều kiện để việc học được ghi nhận.

Nay ghi sổ theo allAnswered; atEnd chỉ còn giữ màn kết. Tác dụng phụ đáng nói: learner nào đang mắc kẹt ở trạng thái cũ chỉ cần MỞ LẠI bài - phần dựng lại chỗ dở (SRC-919) khôi phục đủ các lựa chọn, allAnswered thành true ngay lúc ấy và sổ được ghi.

a2. Ghi sổ hỏng thì KHÔNG được xoá dấu vết (SRC-940, 21.09.2026) ​

Sửa ở mục a vẫn chưa đủ, và phần thiếu chỉ lộ ra khi truy tiếp cùng một lời phàn nàn: chủ dự án báo lại rằng thẻ Warm-up set vẫn chưa được đánh dấu, và trên màn hình lúc ấy nó vẽ hình chưa làm chứ không phải hình đang dở. Tức là cả hai nguồn sự thật cùng trống - learning_events không có dòng nào, mà ielts_micro_progress cũng không.

Hai mảnh ghép lại thành đúng cảnh ấy:

  1. logLearningEvent nuốt mọi lỗi và trả false. Điều đó đúng với vai của nó: learner vừa học xong, một dòng thống kê hỏng là việc của hệ thống, không phải việc của họ.
  2. clearMicroProgress chạy ngay trong cùng một nhịp, không đợi xem lượt ghi có tới nơi.

Cộng lại: một lượt POST hỏng vì mạng chập hay máy chủ trả 500 sẽ xoá nốt dấu vết cuối cùng của công làm bài. Thẻ quay về "chưa làm" như thể learner chưa từng mở nó, bài kế tiếp khoá lại theo luật SRC-923, và không có đường nào phục hồi - kể cả mở lại bài, vì phần dựng lại (SRC-919) đọc chính cái dòng vừa bị xoá.

Nay chỉ xoá khi sổ đã nhận thật (logLearningEvent trả true). Giữ dòng chỗ-đang-dừng lại thì lần mở bài sau, phần dựng lại khôi phục đủ các lựa chọn, allAnswered thành true ngay lúc ấy và nhánh ghi chạy lại - chính cái dòng ấy là cơ chế thử lại. Đó là lý do nó phải sống lâu hơn lượt ghi, không ngắn hơn.

Gác bằng apps/learn/e2e/ieltsProgressKeep.spec.ts, hai vế: ép lượt POST trả 500 thì không được có lệnh xoá nào, và đường thuận thì vẫn phải xoá. Một test chỉ gác vế hỏng sẽ xanh cả khi ai đó bỏ hẳn lệnh xoá đi.

a3. Bài đầu mỗi bậc mang seq 0, và ba tầng API đều vứt nó đi (SRC-943, 21.09.2026) ​

Sau hai bản sửa ở a và a2, chủ dự án báo lại lần thứ ba rằng thẻ Warm-up set vẫn chưa được đánh dấu. Lần này thay vì suy đoán tiếp, ta đọc thẳng D1 production (workflow seed-data với scripts/noop-readonly.sql - đường đọc không ghi gì). Sổ nói ngược hẳn với giả thuyết đang có:

exercise:reading.main-idea.l1.0    4 lượt
exercise:reading.tone.l1.0         3 lượt
exercise:reading.vocabulary.l1.0   3 lượt

Lượt làm bài có vào D1, đủ cả. Mọi ref đều kết thúc bằng .0, vì scripts/micro-seed.mjs đánh số bài trong một bậc từ 0 (let seq = 0). Và ba chỗ trong API đều coi 0 là số không hợp lệ:

ĐườngChỗ hỏngHậu quả
GET ielts-micro-attemptsfilter(x => x.level > 0 && x.seq > 0)lượt đã làm bị bỏ khỏi câu trả lời
GET ielts-micro-progresscùng bộ lọc ấynhãn "đang dở" mất, và mở lại bài không dựng lại được lựa chọn
PUT / DELETE ielts-micro-progressz.number().min(1)trả 400: chỗ-đang-dừng của bài đầu bậc không lưu nổi, mà cũng không xoá nổi

Cộng lại thành một vòng khoá kín, và đây là chỗ nó thành nghiêm trọng chứ không chỉ là một cái nhãn thiếu: theo luật mở khoá §46 (SRC-923), bài đầu mỗi bậc là bài duy nhất learner được phép làm lúc mới vào. Bài ấy làm xong mà hệ không ghi nhận thì không bài nào mở khoá được nữa. Cả sân luyện đứng yên ở bài đầu của mỗi bậc, cho mọi learner, trong nhiều ngày.

Vì sao không cổng nào thấy. Bộ lọc seq > 0 được viết với ý "bỏ những ref sai hình dạng" - và ý ấy đúng, chỉ có cách diễn đạt là sai. "Ref sai hình dạng" với "con số bằng 0" là hai mệnh đề khác nhau; chúng trùng nhau cho tới đúng ngày có người đánh số từ 0. Mọi ca kiểm cũ đều viết ref với seq từ 1 trở lên, nên chúng kiểm đúng một nửa thế giới. Nay lọc theo việc regex có khớp hay không, và có ca kiểm cho cả seq 0 lẫn ref sai hình dạng.

Vì sao khu luyện đề (§49) không dính. Nó dùng sơ đồ ref khác (<cụm>:<số bài>) và một endpoint khác (ielts-paper-parts) không có bộ lọc này - đó là lý do trên cùng một màn hình, thẻ đề hiện "1/3 parts" đúng trong khi thẻ bài tập báo "chưa làm".

Bài học để lại: khi cái nhãn trên màn hình nói "chưa làm" mà learner nói "tôi làm rồi", hãy đọc sổ TRƯỚC khi sửa chỗ hiện. Hai bản sửa a và a2 đều đúng và đáng giữ, nhưng cả hai đều được chọn bằng suy luận từ màn hình, và không cái nào chạm tới nguyên nhân thật.

b. Bộ lọc bậc: luôn đúng một, không có trạng thái "không lọc" ​

Bậc mặc định là bậc THẤP NHẤT CÓ BÀI, không phải hằng số 1: một năng lực mới soạn có thể chưa có bài bậc 1, và một mặc định cứng sẽ mở ra một lưới trống ngay lần vào đầu tiên. Mặc định này KHÔNG ghi lên URL - nó là lựa chọn của hệ, không của learner, và ghi nó thành một mục lịch sử là bắt nút Back bấm hai lần mới ra khỏi trang.

Bấm lại thẻ đang bật không còn tắt bộ lọc. Thứ mà trạng thái "không lọc" bày ra là một lưới trộn lẫn bài của cả năm bậc, xếp cạnh nhau như thể chúng thay thế được cho nhau - trong khi cả thiết kế của mạch này là learner đi từng bậc một.

c. Chữ trong thẻ bậc căn giữa ​

Căn giữa cả hai dòng, không riêng tên bậc: một tiêu đề giữa đứng trên một dòng mô tả căn trái thì thẻ đọc ra như bị lệch, và năm thẻ cạnh nhau nhân cái lệch ấy lên năm lần.

Gác: ba test trong apps/learn/e2e/ieltsMicroSkill.spec.ts - một cho mặc định và cho cú bấm thứ hai không tắt lọc, một cho việc chấm hết câu là vào sổ mà không bấm thêm, và hai test cũ đã sửa theo hình mới (bài của hai bậc khác nhau không còn cùng hiện một lúc).

54. Chiều cao thẻ là một con số, không phải một kết quả (SRC-939, 21.09.2026) ​

a. auto-rows-fr làm các hàng BẰNG NHAU, không làm chúng CỐ ĐỊNH ​

SRC-820 đặt auto-rows-fr cho cả hai lưới để hàng dưới không thấp hơn hàng trên. Nó làm đúng việc ấy, nhưng chiều cao chung vẫn do thẻ nhiều chữ nhất quyết định - tức là do DỮ LIỆU. Cùng một trang, hai năng lực nhỏ khác nhau, hai chiều cao thẻ khác nhau; và một tên bài dài thêm một chữ là cả lưới cao lên. Nay hai lưới mang số đo thật: auto-rows-[5rem] cho hàng thẻ bậc, auto-rows-[10rem] cho lưới thẻ bài. Thẻ khoá và thẻ mở dùng chung con số vì chúng đứng chung một lưới.

Đi kèm là ba cái chặn tràn, vì thẻ cao cố định thì chữ thừa không đẩy thẻ cao lên nữa mà rơi ra ngoài đáy: overflow-hidden trên thân thẻ, line-clamp-2 cho tên bài và dòng lý do khoá, line-clamp-1 cho dòng thành phần dưới tên bậc.

b. Chữ cắt ngắn ở NGUỒN, clamp chỉ là lưới an toàn ​

line-clamp cắt chữ ở chỗ hiển thị, nên nếu dựa vào nó thì nội dung mất đi lặng lẽ và không cổng nào thấy. Vì vậy composition trong apps/learn/src/ielts/levels.ts được viết lại cho gọn trong một dòng ở khổ hẹp ("5 Việt + 5 Anh · cùng bài" thay cho "5 đoạn Việt + 5 đoạn Anh (cùng nội dung)"), còn clamp chỉ đỡ những khổ màn hình hẹp hơn dự tính.

why của cả năm bậc rút xuống dưới 15 từ, không riêng bậc 2: đoạn này đứng ngay dưới hàng thẻ và đổi theo bậc đang chọn, nên năm đoạn dài ngắn khác nhau làm cả lưới bài bên dưới nhảy lên nhảy xuống mỗi lần learner đổi bậc. Một trần chung cho cả năm thì chỗ ấy đứng yên; min-h-6 giữ chỗ cho đúng một dòng. Luật độ dài này chép vào đầu levels.ts, nơi người sửa chữ sẽ đọc.

Gác: apps/learn/e2e/ieltsMicroSkill.spec.ts - test "năm bậc là năm thẻ" đã sửa theo chữ mới, 13 ca của tệp chạy xanh trên bản build thật.

Lịch sử quyết định ​

Từ mục 5i. Năm bậc bài tập, mỗi bài một URL (SRC-850, 2026-09-19) ​

Chỉ đạo chủ dự án 2026-09-19: "Click vào một trong các card (ví dụ như Main Idea hay Detail) thì cần vào một trang mới. Trong đó có nhiều bài tập, và chia theo 5 levels. Mỗi levels có khá nhiều bài tập. Learners có thể filter để chọn ra bài tập để làm bài. Mỗi bài tập đều có URL riêng biệt."

Từ mục 42. Ba trạng thái của một bài: chưa làm · đang dở · đã xong (SRC-919, 20.09.2026) ​

Chỉ đạo chủ dự án, nhìn lưới bài của một năng lực:

"Hệ thống cần đánh dấu 'bài nào đang làm dở', 'bài nào đã làm xong rồi'."

Từ mục 44. Bộ lọc bậc gọn lại, và mô tả chỉ hiện khi được chọn (SRC-921, 20.09.2026) ​

Chỉ đạo chủ dự án 2026-09-20, trên ảnh chụp /ielts/skills/reading/main-idea: "Filter bên trên, còn 5 cards, cùng trên 1 dòng, và mỗi card là 2 dòng thôi, bỏ hết đoạn text giới thiệu dài dòng đi. Khi chọn 1 trong các card đó thì đoạn text mới hiển thị. Bỏ hết link All task types. Bỏ cả 3 tags multiple choice đi."

Từ mục 46. Mở khoá bài tập theo thứ tự (SRC-923, 20.09.2026) ​

Chỉ đạo chủ dự án 2026-09-20: "Mặc định là chỉ level 1, 2, 3 là available, và chỉ available set đầu tiên ở trong level đó thôi. Còn lại là locked hết. Khi nào làm xong cái đầu tiên thì hệ thống mới cho phép Unlock set tiếp theo."

Từ mục 52. Dấu trạng thái trên mỗi thẻ việc (SRC-932, 21.09.2026) ​

Chỉ đạo chủ dự án 2026-09-21, trên ảnh chụp /ielts/skills/reading/vocabulary:

"Cần có icon 'checked' gì đó trên mỗi card, kiểu như card Warm-up Set, để chính mỗi learner biết là 'tôi đã làm bài đó rồi', hoặc 'tôi đang làm dở bài đó'."

Bản đầu của mục này vẽ "đã xong" bằng --success, với lý do "mỗi trạng thái một màu". Chủ dự án bác lại ngay trong ngày: "Card nào hoàn thành, thì cần có một dấu tick màu da cam nho nhỏ ở góc." Và cách đọc ấy đúng hơn với DS-001 §2.

Chỉ đạo chủ dự án trên ảnh chụp /ielts/skills/reading/main-idea: "Bài nào đã làm thì cần dùng icon hình tròn đặc da cam; cái nào đã sẵn sàng để làm thì hình tròn đặc màu gì đó, ví dụ màu vàng. Hover chuột vào thì tooltip hiển thị để learner hiểu."

Chỉ đạo chủ dự án: "Cần có link tới trang Referral."

Từ mục 53. Xong là chấm hết câu, và bộ lọc bậc luôn có đúng một bậc (SRC-933, 21.09.2026) ​

Chỉ đạo chủ dự án, ba việc trong một vòng xem màn hình learn.nemo12.com/ielts/skills/reading/main-idea:

"Tôi đã làm xong Warmup Set, nhưng không thấy được đánh dấu là đã làm xong."

"Căn giữa title của 5 card, gồm các từ như Level 1, Level 2..."

"Mặc định khi mới vào trang này thì hệ thống tự chọn Level 1 để filter. Và không thể không chọn filter nào. Mỗi thời điểm cần chọn duy nhất 1 filter."

Từ mục 54. Chiều cao thẻ là một con số, không phải một kết quả (SRC-939, 21.09.2026) ​

Chỉ đạo chủ dự án, một vòng xem màn hình learn.nemo12.com/ielts/skills/reading/main-idea:

"Cố định chiều cao của 5 cards levels."

"Các dòng text ngay bên dưới Level 1, Level 2... cần không được kéo dài trên 2 dòng."

"Độ cao các Set Cards ở dưới, cần cố định."

Và đoạn mô tả bậc 2 "để dài không quá 15 từ".