---
url: https://docs.nemo12.com/architecture/sdd-038/practice-ui.md
description: >-
  Màn làm bài và kết bài, nút Report, nút hướng dẫn, video, Grammar nền tảng,
  khu làm đề đầy đủ 50 đề
---

# SDD-038 · Màn làm bài, Grammar và khu đề

> Một phần của [SDD-038 Cổng IELTS](index.md). 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.

## 29. Giọng của learner: lưu lại, nghe lại được, và có hạn xoá (SRC-884, 2026-09-20)

### a. Mạch lưu-phiên âm-chấm đã chạy từ SRC-801, nhưng chỉ ở một chỗ

Đường `/ielts/speaking/levels/{1,2,3}` đã làm đủ: ghi âm trên trình duyệt, đẩy tệp thô vào R2
(`CONTENT.put(audioKey(...))`), ghi `audio_key` + `audio_seconds` vào `ielts_productions`, phiên
âm bằng `@cf/openai/whisper-large-v3-turbo`, rồi chấm theo rubric của bậc. Nói cách khác, phần
"AI bóc tách transcript rồi chấm điểm" **không phải việc làm sau — nó đang chạy production**.

Chỗ thiếu nằm ở **sáu năng lực nhỏ của Speaking**. Chúng luyện bằng câu hỏi bốn lựa chọn, y hệt
Reading: learner đọc bốn phương án rồi bấm một cái, và cả buổi "luyện nói" trôi qua mà không ai mở
miệng. Đó chính là màn hình chủ dự án đang nhìn.

### b. Bốn câu trắc nghiệm ở lại, và mọc thêm một bước cuối

Dễ nhất là kết luận rằng luyện nói bằng trắc nghiệm là sai rồi thay sạch bằng ghi âm. Nhưng bốn
câu ấy làm một việc mà ghi âm không làm được: chúng bày ra **bốn cách xử lý cùng một tình huống**
rồi hỏi cách nào tốt hơn. Learner chưa từng thấy ba cách còn lại thì không có gì để so, và một
đoạn ghi âm không dạy nó rằng im lặng tìm từ tệ hơn nói vòng.

Nhận ra là bước trước; làm được là bước sau. Khoảng cách giữa hai bước ấy chỉ đóng lại bằng cách
nói. Nên bộ bài giữ nguyên và có thêm một bước **ở cuối**, sau hàng nút "Làm lại / Chọn bài khác":
đề nói đứng ngay sau bốn lời giải vừa đọc, và learner mang chúng vào đoạn nói của mình.

Đặt ở đầu thì thành mời learner làm đúng cái nó chưa học; chen lên trước hàng nút thì thành một
cái cửa quay chắn ngang đường ra của người đã làm xong bài.

### c. Đề nói nằm ở CSDL, khác với đề của thang ba bậc

Đề của thang ba bậc nằm trong mã nguồn (`modules/ielts/ladder.ts`) vì chúng là kiến thức về một kỳ
thi ổn định nhiều năm. Đề ở đây thì buộc vào **một năng lực nhỏ cụ thể**, và chính các năng lực ấy
đã sống trong CSDL và sửa được ở màn admin từ SRC-805. Để đề nói trong mã nguồn nghĩa là người
soạn sửa được tên năng lực, sửa được bốn bài tập của nó, nhưng phải mở một pull request để sửa đúng
một câu hỏi quan trọng nhất.

Migration `0256` thêm `speak_prompt_en` + `speak_prompt_vi` vào `ielts_micro_skills` và seed sáu
đề. NULL nghĩa là năng lực ấy không có bước nói, và màn kết đơn giản không hiện khối ấy — Reading
và Listening luôn rơi vào nhánh này.

Bài nộp vào `ielts_productions` với `track='micro'` (bên cạnh `'diagnostic'` của SRC-792 và
`'practice'` của SRC-801) và `prompt_id` dạng `micro:speaking.<code>`, nên truy ngược từ một bài
nộp về năng lực nó thuộc về là một phép cắt chuỗi chứ không phải một cột thứ hai.

Rubric là `RUBRICS[2]` cho mọi lượt nói ở đây, và **cố ý không có band**: lượt này dài ba bốn chục
giây và soi vào đúng một thao tác. Bậc 3 sẽ gắn một con số band vào bốn mươi giây, và một band
dựng từ bốn mươi giây là con số nghe như phép đo mà không phải. Ai muốn band thì đã có thang ba
bậc, nơi đề dài hơn và rubric nói thẳng rằng nó đang chấm band.

Sàn 15 giây, trần 90 giây — hẹp hơn hẳn 20-240 của thang bậc, vì đây là một việc ba mươi giây chứ
không phải một bài thi, và vì trần giây cũng là trần hoá đơn Whisper.

### d. Learner nghe lại được chính mình

Đây là chỗ thiếu **lớn hơn** cả bước nói, và nó đã tồn tại lặng lẽ từ SRC-792: tệp vào R2 rồi chỉ
**mentor** mở lại được (`modules/mentor/routes.ts`). Learner nộp xong nhận về một bản phiên âm và
mấy câu nhận xét; giọng của chính nó nằm trong một cái bucket nó không có đường tới.

Bản phiên âm không thay được bản ghi, vì nó đã lọc mất đúng những thứ đang luyện. Bốn trong sáu
năng lực nhỏ của Speaking là `pace`, `self-correct`, `paraphrase`, `long-turn` — cả bốn sống ở chỗ
chữ viết không giữ lại. Một learner đọc "I think, er, the main reason is" thì thấy một câu bình
thường; nghe lại chính mình thì nghe thấy ba giây im lặng.

Trang `/ielts/recordings` gộp **cả ba** `track` vào một danh sách theo thời gian. Learner không
biết tới ba chữ ấy và cũng không cần: với nó, đây chỉ là "những lần con đã nói". Lọc theo `track`
sẽ giấu mất chính bài chẩn đoán, tức là bài nói đầu tiên và thường là bài đáng nghe lại nhất.

Phát **qua worker**, không ký URL R2 — cùng lý do đã ghi ở đường của mentor: một URL đã ký thì sống
tiếp sau khi phiên hết hạn, sau khi gia đình rời đi, sau khi ai đó dán nó vào một tin nhắn.
`learner_id` nằm trong mệnh đề `WHERE` chứ không phải một phép kiểm sau khi đã đọc xong dòng, nên
không có nhánh nào để quên.

### e. Luật xoá sinh ra cùng lúc với luật lưu

Một tính năng lưu giọng trẻ em mà không có đường xoá là một rủi ro vĩnh viễn đổi lấy một công dụng
không ai kể ra được. Lượt dọn hằng đêm (`audioRetention.ts`, gắn vào cron `0 21 * * *`) xoá tệp
quá **180 ngày**, trần 500 tệp một lượt.

Cái bị xoá là **tệp**, không phải bài nộp: bản phiên âm, nhận xét, band mentor chốt và ngày nộp ở
lại nguyên vẹn; chỉ `audio_key` về NULL. Learner mất khả năng *nghe lại* một bài rất cũ, chứ không
mất chính bài ấy — đánh đổi đúng chiều, vì thứ bị bỏ là thứ ít người dùng nhất và rủi ro nhất.

Thứ tự **R2 trước, D1 sau** là cố ý. Hỏng giữa chừng theo thứ tự này để lại một khoá trỏ vào chỗ
trống, và lượt sau dọn nốt. Thứ tự ngược lại để lại một **tệp mồ côi**: không dòng nào trỏ tới nó
nữa, nên không lượt dọn nào tìm ra nó nữa.

Con số 180 ngày được **nói thẳng ra trên chính trang nghe lại**, không giấu trong trang điều khoản:
một learner quay lại sau một năm và không thấy bài cũ sẽ nghĩ hệ làm mất bài của nó.

### e-bis. Nghe được nhưng không tua được (sửa 20.09.2026)

Nguyên nhân: route trả thẳng `obj.body`, không có `Accept-Ranges`, và một request
`Range: bytes=1000-2000` được trả lời bằng 200 kèm trọn 429.200 byte thay vì 206. Trình duyệt đọc
điều đó là "máy chủ không cắt khúc được" rồi bỏ hẳn thanh tua.

Nay cả hai đường (learner và mentor) dùng chung `shared/audioRange.ts`. Đường của mentor mang y
nguyên lỗi này từ SRC-792: thầy cô nghe một đoạn ba phút cũng không tua được, và chép cách sửa sang
hai chỗ là chờ ngày chúng lệch nhau.

Bản giả R2 trong testkit cũng phải biết cắt khúc — một bản giả không cắt khúc sẽ làm test xanh
trong khi production trả nguyên tệp, tức là dựng lại đúng cái lỗi vừa sửa.

### f. Còn mở

* `/ielts/recordings` **lọc bỏ** dòng đã hết hạn tệp (`audio_key IS NOT NULL`), nên bài cũ biến
  khỏi danh sách thay vì hiện ra kèm câu "tệp đã xoá, bản chữ còn đây". Bản chữ vẫn nằm trong CSDL
  và mentor vẫn đọc được; chỉ learner là mất đường tới. Đó là hành vi hiện tại, đã ghi trong test,
  chứ không phải điều đáng mong muốn.
* Phụ huynh **không** có đường nghe bài của con (chốt 2026-09-20: learner + mentor). Mở thêm vai
  này cần một phép kiểm quan hệ phụ huynh-learner ở route, chưa làm.
* Sáu đề nói hiện do migration seed. Màn admin đã sửa được `ielts_micro_skills` nhưng **chưa có ô
  nhập** cho hai cột mới, nên sửa đề vẫn phải qua SQL.

## 89. Bảng rộng làm cả trang cuộn ngang (SRC-987, 22.09.2026)

**Nguyên nhân là `<fieldset>`, không phải cái bảng.** UA stylesheet của trình duyệt đặt
`min-inline-size: min-content` cho `fieldset`, nên nó KHÔNG chịu hẹp hơn nội dung - kể cả khi nội
dung ấy nằm trong một khung `overflow-x-auto` lẽ ra phải tự cuộn. `EditGate` bọc mọi form trong
một `fieldset disabled` (đó là cách chế độ đọc khoá mọi ô mà không phải nhớ ô nào), nên mọi trang
dùng `EditGate` đều mang sẵn cái bẫy này. Sửa bằng `min-w-0` trên chính fieldset.

Ca e2e đo `document.scrollWidth` sau khi nới thêm năm tháng, ở cả 1280px lẫn 375px, và đo ở cả
chế độ sửa lẫn chế độ đọc - fieldset đổi lớp giữa hai chế độ.

## 91. Lối ra khỏi bài đang làm, và nhãn nút bằng tiếng Anh (SRC-993, 23.09.2026)

Chỉ đạo: nút dùng **icon và tiếng Anh**; **bỏ** nút "Chọn bài khác"; **thêm một icon để dừng việc
làm bài rồi quay lại màn chọn bài**.

**Vì sao cái dấu X đáng giá nhất trong ba việc trên.** Trước bản này, learner đang làm dở một bài
mà muốn đổi sang bài khác thì chỉ còn đường bấm vệt đường dẫn ở đầu trang - mà không ai coi vệt
đường dẫn là một cái nút. Trên điện thoại, việc họ thật sự làm là bấm nút Back của trình duyệt, và
phần đã làm mất theo. Một dấu X ngay cạnh thanh tiến độ là chỗ mắt tìm tới khi muốn dừng, và nó có
mặt SUỐT cả bài chứ không chỉ ở màn cuối.

Chính vì thế nút "Chọn bài khác" ở màn cuối bỏ được: hai đường tới cùng một chỗ trên một màn là
một đường thừa, và đường còn lại thì rộng hơn hẳn.

**Nhãn nút nói tiếng Anh** (`Try again`, `Next exercise`) theo luật sân luyện SRC-834. Câu ghi nhận
ngay phía trên vẫn tiếng Việt, và đó không phải mâu thuẫn: SRC-953 vạch đúng ranh giới ấy - chữ
Nemo NÓI VỚI learner thì tiếng Việt, còn nhãn của một nút là một phần của giao diện.

Cả hai nút icon đều chỉ có hình, nên cả hai mang `aria-label` hoặc chữ đi kèm: một nút chỉ có hình
là một nút câm với người không nhìn thấy nó. Ca e2e gác đúng chỗ ấy - nó tìm nút dừng bằng chính
cái nhãn đọc-thành-lời.

## 96. Màn làm bài: bớt cam, và chữ giao diện nói tiếng Anh (SRC-1008, 23.09.2026)

**Bớt cam.** Trước bản này, một câu vừa chấm đúng có tới bốn mảng cam cùng lúc: viền ô đáp án, nền
ô đáp án, dấu tích, và chữ "Đúng rồi!" to ngay bên dưới - cộng thêm thanh tiến độ cam ở đầu trang.
Một màn hình mà màu nhấn chiếm cả nửa thì màu nhấn không còn nhấn được gì. Nay ô đúng và thanh
tiến độ dùng màu biển; cam chỉ còn ở ĐÚNG MỘT chỗ - dấu tích cạnh đáp án đúng - nên mắt vẫn tìm ra
nó ngay. Chữ "Correct!" chuyển sang màu chữ chính; những tia sáng nhỏ quanh nó vẫn giữ cam vì chúng
chỉ tồn tại nửa giây.

**Tiếng Anh cho mọi chữ giao diện của màn làm bài**: `Check`, `Correct!`, `Not quite.`, `Next →`,
`Finish`, câu tóm tắt cuối bài, nhãn thanh tiến độ, câu nhắc bản chữ, các chữ quanh bước ghi âm.

Đây là một vạch HẸP LẠI của SRC-953, và cần nói rõ để không ai "sửa lại cho đúng luật": SRC-953
cho chữ HƯỚNG DẪN trong sân luyện nói tiếng Việt, vì một learner band 5.0 đọc lời giải thích tiếng
Anh sẽ hiểu lỗ chỗ. Trên màn LÀM BÀI thì chủ dự án chọn tiếng Anh cho cả những câu ngắn của Nemo -
chúng đủ ngắn để không cần dịch, và một nút tiếng Việt giữa một đề tiếng Anh là đúng cái lớp dịch
mà SRC-834 gỡ đi.

**Thứ KHÔNG đổi:** nội dung câu hỏi. Bậc 1 và 2 của phần Nói, phần Viết, và bậc 1 của phần Nghe
vẫn tiếng Việt theo thang bốn chặng (SRC-821, SRC-976) - đó là chữ trong BÀI, không phải chữ trên
GIAO DIỆN. Lời giải thích (`why_vi`) đi theo ngôn ngữ của chính câu hỏi.

## 97. Màn kết bài: "Try again" chỉ còn icon, thêm "Take a break" (SRC-1009, 23.09.2026)

**"Try again" chỉ còn icon.** Cạnh nút chữ `Next exercise`, một mũi tên vòng tròn đã đủ nói "làm
lại", và bỏ chữ đi làm nút chính nổi hẳn lên. `aria-label` và `title` giữ lời cho trình đọc màn
hình và cho người rê chuột; ca e2e tìm nút bằng chính cái tên ấy và đồng thời đòi nút không có chữ
nào hiện ra.

**"Take a break"** quay về danh sách bài của năng lực đang luyện. Dấu X cạnh thanh tiến độ vẫn có
suốt cả bài (SRC-993), nhưng ở màn cuối thì mắt learner nằm ở hàng nút, không ở góc trên - và câu
hỏi của họ lúc ấy chính là "giờ đi đâu". Nhãn nói ra VIỆC ("tạm nghỉ") chứ không nói đích đến, vì
đó là điều learner đang chọn.

## 101. Bài luyện nhỏ vào sổ kèm thời gian làm (SRC-1013, 23.09.2026)

Chủ dự án: *"Tôi vừa luyện tập rồi, nhưng ko thấy số liệu được điều chỉnh."* Sổ production cho thấy
mọi dòng `exercise:*` đều có `seconds = 0`: `MicroExercise` ghi số câu và số câu đúng nhưng không đo
giờ. Dashboard hub (Am I showing up, This month, mốc tháng) cộng GIÂY, nên bài làm xong không nhích
được con số nào.

* Màn bài tập tự đo thời gian nhìn (dừng khi tab ẩn, trần 90 phút) và gửi kèm CHÍNH dòng "làm xong".
  Không tách dòng riêng: `ielts-micro-attempts` và `mastery` đếm lượt làm theo tiền tố `exercise:`,
  một dòng thời gian mang tiền tố ấy sẽ thành lượt làm ma.
* Giới hạn còn lại: thời gian của một bài bỏ dở không được ghi (chỉ ghi khi làm xong), và các dòng
  cũ vẫn 0 giây. Không bù số cho dòng cũ vì mọi con số bù đều là đoán.

## 112. Mỗi bài luyện có số phút cố định (SRC-1034, 25.09.2026)

Chủ dự án: *"Mỗi bài luyện tập của các sub-skills, đều cần tương ứng với một số phút. Để khi learner
học xong mỗi lần thì hệ thống lại cộng thêm thời gian đã học."*

* **Cột `minutes`** (migration 0292) = số câu một lượt x phút mỗi câu, làm tròn lên, tối thiểu 3
  (Reading 1 / 1.5 / 2 theo bậc, Listening 2, Writing 1.5, Speaking 2). Sửa được từng bài.
* **Làm xong thì server cộng đúng `minutes x 60` giây**, thay cho số giây đồng hồ trên máy đo
  (SRC-1013). Lượt khác (đọc bài, bài chưa định phút) giữ số client gửi.
* **Hiện ra**: thẻ bài in "· N min"; màn kết bài in "+N min added to your study time".
* Cái giá: learner bấm qua thật nhanh vẫn được cộng đủ phút. Chấp nhận theo chỉ đạo; nếu cần, thêm
  một mức sàn thời gian tối thiểu sau.

## 114. Video "What is a Learning Plan?" chỉ cho tài khoản xem thử (SRC-1043, 25.09.2026)

Chủ dự án: *"Trong trang IELTS Learning Plan, tạo ra một video có nhạc, có lời bằng tiếng Anh, để
chia sẻ và giải thích cho learners... lưu vào đâu đó trong Cloudflare. Về công nghệ, sử dụng
remotion. Hãy làm cho riêng tài khoản của tôi, là dac2205@gmail.com."*

* **Dựng**: `tools/videos` (Remotion 4.0.528), NGOÀI workspaces vì đây là công cụ render chạy trong
  CI, không vào bundle của site nào, nên không đụng luật bộ ba UI (SRC-650). Tám cảnh theo tám đoạn
  lời (`scripts-content/learning-plan.json`): nỗi lo, Learning Plan, điểm hiện tại → mục tiêu, từng
  tháng, phút cộng dồn, lời mời.
* **Tiếng và nhạc**: lời đọc sinh bằng Workers AI `aura-2-en` (cùng model TTS với Listening), giọng
  asteria; độ dài từng cảnh lấy theo độ dài lời thật. Nhạc nền tự tổng hợp bằng code (không tải từ
  đâu về, không vướng bản quyền).
* **Lưu và phát**: workflow chạy tay `render-videos.yml` render rồi đưa lên R2
  `nemo12-content/videos/learning-plan.mp4` (đúng luật mọi thứ chạm production đi qua GitHub). Phát
  qua `GET /v1/owner/videos/learning-plan`: có phiên và email trong danh sách xem thử mới ra, mọi
  người khác nhận 404. Thẻ "Watch first" trên trang Learning Plan chỉ hiện cho email ấy.
* Remotion miễn phí cho cá nhân và công ty tối đa 3 người; quá mức đó cần giấy phép công ty.

## 115. Nút hướng dẫn ở góc phải mọi trang (SRC-1048, 25.09.2026)

Chủ dự án: *"Trong tất cả các trang, luôn có một icon gì đó ở góc trên phải, click vào đó thì nó sẽ
mở ra Popover để hướng dẫn. Các hướng dẫn cần chia theo cụm, ngắn gọn... bằng tiếng Việt là chính."*
Nút "?" đứng cạnh ảnh đại diện ở cả hai khung header của sân IELTS (`ielts/Shell.tsx` và cổng
`/ielts` trong `App.tsx`), mở popover shadcn. Nội dung ở `ielts/helpContent.ts`, chọn theo đường
dẫn (trang chính, trang kỹ năng, sub-skill, bài luyện, How to do this, task type, Learning Plan,
Learner Profile, Membership, Referral, Setup, Leaderboard, Live, đề luyện; còn lại dùng bản chung).
Mỗi trang 2-4 cụm, mỗi cụm 1-3 dòng.

## 118. Năm video "một thông điệp" trong sân luyện (SRC-1052, 25.09.2026)

Gắn bằng `ielts/IeltsClip.tsx`: một dòng "▶ Video 1 phút", bấm mới mở, phụ đề Việt bật sẵn. Reading
(skills-first), Listening (listen-twice), trang chính dưới dashboard (small-steps), Learner Profile
(know-you), Membership (three-months). Kịch bản và luật dựng: docs/product/videos. Menu tài khoản có
thêm mục "What's new" trỏ sang trang công khai (SDD-029).

## 122. Khu ngữ pháp nền tảng (SRC-1065, 26.09.2026)

Chủ dự án: *"cần có một khu vực chuyên để học ngữ pháp... 6 mảng, mỗi mảng 6 chủ đề ngữ pháp nền
tảng nhất... video giới thiệu... một đoạn giới thiệu, 3 ví dụ, và 3 ví dụ dễ gây hiểu lầm... 5
Practice Tasks... từ level 1 dễ nhất tới level 5 khó nhất."*

* URL: `/ielts/grammar` (6 mảng: Verb tenses, Building sentences, Nouns and determiners,
  Adjectives and adverbs, Modals and verb patterns, Linking ideas) và `/ielts/grammar/<chủ đề>`.
  Lối vào: thẻ "Build your base" ở trang `/ielts`.
* Nội dung là 36 file JSON trong bundle (`apps/learn/src/ielts/grammar/content/`), như tầng luyện
  đọc: mở được khi API hỏng, không cần migration. Soạn bằng một workflow 12 agent (một người soạn
  và một người duyệt cho mỗi mảng); lượt duyệt sửa 11 chỗ, phần lớn là câu có hơn một đáp án đúng.
* Bài tập: level 1-3 chọn đáp án, level 4-5 điền từ (có danh sách đáp án chấp nhận), level 5 mang
  giọng câu học thuật IELTS.
* Ngôn ngữ theo SRC-953: tên trang, tên chủ đề, đề bài tiếng Anh; giới thiệu, ghi chú, lý do lỗi,
  giải thích đáp án tiếng Việt.
* Video: `grammar-<chủ đề>`, cùng khuôn "một thông điệp" (luật, một ví dụ, một lỗi hay gặp, lời
  mời luyện), phụ đề Việt/Anh; render bằng workflow render-videos. Video chưa render thì dòng video
  tự ẩn.
* Tiến độ và phút học: xem §123.

## 123. Grammar vào sổ học tập, cộng phút học (SRC-1066, 26.09.2026)

* Migration 0299 nới CHECK của `learning_events.skill` thêm `'grammar'` (dựng lại bảng như 0249).
  Một sổ duy nhất nên phút học, chuỗi ngày, bảng xếp hạng tuần, thưởng học tập và dự đoán 200 giờ
  tự cộng bài ngữ pháp mà không sửa chỗ nào.
* `POST /v1/learners/{id}/ielts-grammar {topic, level}`: bài làm ĐÚNG lần đầu ghi một dòng, cộng cố
  định 120 giây. Làm lại bài đã đúng không cộng nữa (`INSERT ... WHERE NOT EXISTS`), nếu không bấm
  Kiểm tra nhiều lần là nhiều lần hai phút. `GET` trả danh sách bài đã đúng và tổng phút.
* Dòng ngữ pháp ghi `items = 0`: là công sức, không phải bằng chứng về bốn kỹ năng, nên không vào
  tỉ lệ đúng, lời khen "bài đọc trọn vẹn đầu tiên", hay learner_evidence.
* Dashboard: thẻ "Build your base" ở `/ielts` hiện `n/180`; trang Grammar đọc tiến độ từ máy chủ
  (gộp với bản localStorage khi chưa đăng nhập).

## 124. Grammar hiện dần (SRC-1067, 26.09.2026)

Chủ dự án: *"Mặc định khi mới vào thì đừng hiển thị quá nhiều thông tin... hiển thị cực kỳ ít
thông tin (kiểu như vài thông tin tổng quan), và chỗ để learners bắt đầu. Learner cần gì thì mới
click vào chỗ đó để hiển thị dần ra."* Ảnh kèm là trang chủ đề, nơi video chiếm trọn màn đầu.

* Trang chủ đề: mặc định chỉ tên, một dòng công thức, tiến độ `x/5` và MỘT nút bắt đầu. Video,
  "Chủ đề này là gì", Examples, Common mistakes là bốn dòng gập, bấm mới mở. Bài tập hiện TỪNG bài,
  đúng rồi mới sang bài sau; xong 5/5 thì mời sang chủ đề kế.
* Trang Grammar: tổng `n/180`, một nút "Bắt đầu/Học tiếp" trỏ chủ đề chưa xong đầu tiên, và sáu mảng
  gập lại, bấm mới hiện sáu chủ đề.
* SRC-1068 (26.09.2026): *"Phần Practice, mỗi thời điểm chỉ hiển thị đúng 1 câu. Và khi đã practice
  thì các phần khác sẽ được ẩn bớt đi."* Bấm bắt đầu thì công thức và bốn cụm gập ẩn hẳn, màn chỉ còn
  tên chủ đề và MỘT câu; một dòng "Dừng luyện, xem lại lý thuyết" đưa chúng trở lại.

## 127. Dấu "?" giải thích thuật ngữ ở tiêu đề sub-skill và dạng đề (SRC-1082, 27.09.2026)

Chủ dự án: cạnh tiêu đề (ví dụ "Inference") là dấu hỏi, bấm vào ra popover giải thích và cho ví
dụ về chính thuật ngữ ấy bằng tiếng Việt, cho mọi trang; bỏ câu "Luyện cho vững sub-skill này
trước; dạng bài thi (task type) sẽ tới sau." (SRC-1050), thay bằng một dòng tiếng Anh ngắn bổ nghĩa
cho tiêu đề.

* `TermTitle` (apps/learn/src/ielts/TermTitle.tsx) dùng cho 51 trang sub-skill (`/ielts/skills/...`)
  và 12 trang dạng đề Reading (`/ielts/tasks/reading/...`).
* Nội dung ở `apps/learn/src/ielts/data/termHints/*.json`, khoá `<micro|task>:<kỹ năng>:<mã>`:
  `tagline_en` (3-8 từ), `what_vi`, `example_vi`. Soạn bằng workflow 10 agent (soạn + duyệt theo
  nhóm kỹ năng); lượt duyệt sửa 6 chỗ, gồm hai câu nói sai tiêu chí chấm Speaking và một câu hứa
  quá tay về band 6 ở Writing.
* Mã mới chưa có nội dung thì trang chỉ hiện tiêu đề, không vỡ.

## 151. Khu làm đề đầy đủ: 50 đề mở khoá tuần tự, phiếu kết quả, nhận xét AI (SRC-1146, 30.09.2026)

Chủ dự án 30.09.2026: `learn.nemo12.com/ielts/tests` là khu làm đề đầy đủ bốn kỹ năng, 50 đề. Learner bắt
đầu từ đề 1; đề k+1 chỉ mở khi đề k có một lượt ĐÃ NỘP và HỢP LỆ (làm nghiêm túc). Làm xong có phiếu kết
quả theo kỹ năng lớn và năng lực nhỏ, 3 tới 5 năng lực nên ưu tiên, và vài câu nhận xét chung do AI viết.

**Nội dung.** Đề k ghép từ đề thứ k của mỗi kỹ năng (cùng thứ tự trang danh sách đề từng kỹ năng, SRC-930).
Kho ngày 20.09.2026 ghép được khoảng 19-20 đề đủ bốn phần; số ô còn lại in "Coming soon", không bịa đề.
Trang tự đếm lúc chạy nên tự mở thêm ô khi kho dày lên.

**Ai quyết cái gì.** App chỉ ghép đề và gửi câu đã chọn. Server (`workers/api/src/modules/ieltsFullTests/`)
đóng dấu giờ mở và nộp từng phần bằng đồng hồ server, chấm Listening/Reading bằng đáp án trong D1, chấm
Writing/Speaking theo tiêu chí bằng `ielts.production-feedback@2` trên chính các bài đã nộp ở trang từng task
trong khoảng giờ của phần ấy, rồi quyết hợp lệ bằng hàm thuần `validity.ts`.

**Ngưỡng hợp lệ** (`THRESHOLDS` trong `validity.ts`):

| Luật | Ngưỡng | Lý do |
| --- | --- | --- |
| Đủ bốn phần | 4/4 đã nộp | một đề chỉ tính khi đủ |
| Câu khách quan đã trả lời | >= 90% mỗi phần L/R | bỏ trống nhiều thì điểm không nói gì |
| Thời gian tối thiểu | L/R >= 40%, W/S >= 30% thời gian chính thức (L 12, R 24, W 18, S ~4 phút) | nhanh hơn thì không kịp đọc/nghĩ |
| Nhịp trả lời | trung bình >= 15 giây/câu | điền cho xong |
| Đoán bừa | > 80% câu trắc nghiệm một đáp án cùng một vị trí (xét khi >= 10 câu) | kiểu chọn A-A-A |
| Writing | Task 1 >= 120 từ, Task 2 >= 200 từ (80% của 150/250); cụm bốn từ trùng đề <= 50% | ngắn hoặc chép đề |
| Speaking | đủ các phần, mỗi phần >= 20 giây, tổng >= 180 giây | không có gì để nghe |
| Rời tab | > 8 lần: chỉ cảnh báo, không bao giờ tự làm hỏng lượt | màn hình tắt, cuộc gọi đến cũng làm trang ẩn |

Lượt không hợp lệ vẫn có phiếu kết quả; đề sau vẫn khoá; phiếu liệt kê từng lý do bằng tiếng Việt và mời làm
lại. Admin mở tay được một đề cho một learner (tab "Mở đề IELTS" ở admin, `POST /v1/admin/ielts-full-tests/unlocks`),
mỗi lần ghi `audit_log` (`ielts_full_test.unlock`, kèm lý do). Trước migration 0316 không có lượt làm đề đầy đủ
nào được lưu kèm mốc giờ và câu trả lời, nên không có lượt cũ nào chứng minh được là hợp lệ: không "grandfather",
mọi learner bắt đầu từ đề 1.

**Phiếu kết quả** (`/ielts/tests/<n>/report/<mã lượt>`): band ước tính tổng (trung bình bốn kỹ năng, làm tròn
nửa bậc) và từng kỹ năng, câu nói thật bằng tiếng Việt rằng đây KHÔNG phải band IELTS chính thức; mỗi kỹ năng một
câu nhận xét dựng từ số liệu và bảng năng lực nhỏ (L/R theo mã năng lực của câu hỏi; Writing TR/CC/LR/GRA; Speaking
FC/LR/GRA, Pronunciation "Not measured") với nhãn Strong / Developing / Needs work. Ưu tiên: 3-5 năng lực xếp theo
độ yếu × trọng số (phần điểm trong kỹ năng), bỏ dòng dưới 2 câu, mỗi dòng dẫn tới trang năng lực nhỏ.

**Nhận xét chung** (`ielts.full-test-comment@1`, `@cf/meta/llama-3.3-70b-instruct-fp8-fast`): sinh MỘT lần mỗi
lượt, lưu `ielts_full_test_comments` (PK attempt_id, `INSERT OR IGNORE` rồi đọc lại), không sinh lại. Model chỉ
thấy số liệu đã tính. Mã chặn sau model: câu có con số không nằm trong đầu vào, chữ hứa hẹn điểm thi, hay dấu gạch
dài thì dùng khuôn. Chạm `AI_DAILY_USD_CAP` hoặc AI hỏng cũng dùng khuôn. Chi phí ước tính: nhận xét ~$0,0005;
chấm tiêu chí 2 task Writing + 3 phần Speaking ~$0,009; tổng ~$0,01 mỗi phiếu, cộng vào sổ ngày của trần USD.

Ngôn ngữ: tên trang, tên kỹ năng, nhãn trạng thái tiếng Anh; dặn dò, lý do, tóm tắt, nhãn nút tiếng Việt (SRC-953), xưng "bạn" như phần còn lại của sân luyện (khu này có cả teen và người lớn). Mọi mã năng lực đều in thành nhãn đọc được ("Main idea"), không in slug.

Kiểm chứng: `workers/api/src/modules/ieltsFullTests/{validity,report,routes}.test.ts` (từng luật hợp lệ, thứ tự
mở khoá, chấm L/R bằng đáp án D1, phiếu, ưu tiên, nhận xét sinh một lần + khuôn khi AI hỏng hoặc bịa số, admin mở
tay + quyền + audit), `apps/learn/e2e/ieltsFullTests.spec.ts` (bốn trạng thái ô, lý do không hợp lệ, các phần của
phiếu, đường dẫn ưu tiên); `mobileAudit.spec.ts` phủ `/ielts/tests`, `/ielts/tests/1`, trang phiếu.

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

### Từ mục 29. Giọng của learner: lưu lại, nghe lại được, và có hạn xoá (SRC-884, 2026-09-20)

Chủ dự án nhìn màn hình cuối một bộ bài Speaking và viết:
*"Cần có tính năng để lưu lại audio của learner chứ. Tính năng 'AI bóc tách transcript rồi chấm
điểm' sẽ được làm sau. Nhưng dự kiến là có tính năng đó."*

Câu ấy đúng về cái thiếu và lệch về cái đã có, nên §29 phải nói rõ cả hai.

Sau khi §29 lên production, đo lại trên trang thật thì đường phát trả 200, đúng byte, `readyState`
lên 4, `duration` đúng 26,58 giây. Mọi phép kiểm xanh. Nhưng `audio.seekable.end(0)` bằng **0**:
đặt `currentTime = 20` thì con trỏ nhảy về 1,1 giây. Learner nghe được mà **không tua được**.

Đây là một LOẠI lỗi thứ ba, khác cả hai loại đã gặp hôm nay: không phải sai quyền, không phải sai
vai người đọc, mà là **đúng mọi thứ được kiểm, sai đúng thứ không ai kiểm**. Test kiểm `status` và
kiểm byte — cả hai vẫn đúng. e2e không chạm thẻ `<audio>`. Mắt nhìn màn hình cũng không thấy, vì
thanh tua VẪN VẼ RA, chỉ là kéo không ăn.

SRC-885 vừa mở `Content-Range`/`Accept-Ranges` qua CORS cho các đường giọng đọc. Việc ấy cần, nhưng
nó chỉ mở cửa cho những header mà máy chủ CÓ SINH RA — và đường này chưa sinh ra cái nào.

### Từ mục 89. Bảng rộng làm cả trang cuộn ngang (SRC-987, 22.09.2026)

Chủ dự án nới kế hoạch lên 13 tháng và cả trang trôi sang trái: thanh bên, tiêu đề và nút "Let
Nemo arrange it" bị cắt ngoài mép màn, có một thanh cuộn ngang chạy suốt chân trang.

**Vì sao nó lọt qua tới tận production.** Lỗi chỉ xuất hiện khi nội dung đủ rộng: mười cột thì
vừa khung, mười một thì không. Trước SRC-984 learner không đổi được số tháng nên bảng gần như
luôn dưới mười cột - tức là chính tính năng vừa thêm đã mở ra cái bẫy có sẵn. Đây là dạng hỏng
`DS-001 §4b` gọi tên: không có lỗi nào nổ ra, chỉ là bố cục lặng lẽ sai ở nhà người dùng.

### Từ mục 96. Màn làm bài: bớt cam, và chữ giao diện nói tiếng Anh (SRC-1008, 23.09.2026)

Chỉ đạo: *"Hơi bị nhiều màu da cam quá. Và cần xài tiếng Anh thay vì tiếng Việt, không chỉ ở chỗ
'Đúng rồi' hay 'Tiếp', mà còn ở các chỗ khác nữa."*

### Từ mục 97. Màn kết bài: "Try again" chỉ còn icon, thêm "Take a break" (SRC-1009, 23.09.2026)

Chỉ đạo: không dùng tiếng Việt nữa; cần một nút để quay về và tạm dừng việc luyện tập; nút "Try
again" chỉ còn icon.

Câu tóm tắt tiếng Việt trong ảnh chủ dự án gửi đã được thay bằng tiếng Anh ở SRC-1008 - ảnh chụp
bản chưa deploy.
