---
url: https://docs.nemo12.com/architecture/sdd-038/home-dashboard.md
description: >-
  Hub /ielts, dashboard công sức, Recent wins, thẻ 200 giờ, các vòng feedback
  giao diện 19 và 20.09.2026
---

# SDD-038 · Hub và dashboard /ielts

> 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. Các mục §56, §74, §94, §100, §107, §125 nằm ở [Hub /ielts: thứ tự vào cửa và các thẻ](home-dashboard-hub-cards.md).

## 7. Thẻ bằng nhau, và thanh trên của điện thoại (SRC-820)

Hai nguyên nhân độc lập, cùng gây ra một triệu chứng:

**Chuỗi `h-full` bị đứt.** `HubTile` đã khai `h-full` trên Card, nhưng giữa ô lưới và Card còn hai
lớp bọc là `<m.div>` (`StaggerItem`, `Liftable`) cao theo nội dung. `height: 100%` tính theo cha
gần nhất, nên chỉ cần một mắt xích cao auto là con số ấy rơi về auto và thẻ co lại đúng bằng chữ
bên trong. Nay `h-full` đi liền một mạch từ ô lưới xuống Card. Đây là cái bẫy dễ tái phát nhất khi
thêm một lớp bọc animation mới: thêm lớp thì phải nối `h-full` qua nó.

**Mỗi hàng lưới cao tự do.** Mặc định của CSS grid là mỗi hàng cao theo thẻ dài nhất của riêng
hàng ấy, nên bốn thẻ xếp 3+1 thì thẻ đứng một mình ở hàng dưới thấp hẳn. `auto-rows-fr` bắt mọi
hàng bằng nhau. Áp cho các lưới thẻ của `/ielts/listening`, `/ielts/reading`, hai lưới của
`Production.tsx` và lưới năng lực của `MicroSkill.tsx`. Đo sau khi sửa: bốn thẻ `/ielts/listening`
đều 235×246 ở 1440px và 343×185 ở 375px.

**Thanh trên trên điện thoại.** Vệt đường dẫn, ba chip và bộ đổi ngôn ngữ cùng chen một dòng 375px:
`flex-1` bóp vệt đường dẫn còn vài chục pixel, nhãn vắt hai dòng và đè lên chip cạnh nó. Nay vệt
đường dẫn chiếm trọn một dòng dưới `sm` (`basis-full`), các chip xuống dòng dưới; từ `sm` trở lên
mọi thứ về lại một dòng. Dấu `/` phân cách cũng ẩn theo đúng nhánh đứng trước nó - trước đó nhánh
trên đã ẩn trên màn hẹp mà dấu phân cách vẫn hiện, ra một dòng mở đầu bằng "/ Luyện nghe".

## 11. Recent wins và nhịp 12 tuần trên `/ielts` (SRC-853, 2026-09-19)

Cả hai đọc cùng một endpoint `ielts-effort`, không thêm lượt gọi nào: cửa sổ của nó mở từ 30 ngày
lên **84 ngày** (`EFFORT_WINDOW_DAYS`), còn hình cột 30 ngày vẫn cắt ra từ chính tập dòng ấy.

### 10a. Nhịp 12 tuần

Bốn thẻ có từ SRC-835 đều nói về một khoảnh — hôm nay, 7 ngày, 30 ngày, cả đời học — nên không
thẻ nào trả lời được câu quyết định chuyện 200 giờ có tới hay không: *tôi có đang giữ nhịp
không*. Mười hai cột cạnh nhau trả lời nó trong một cái liếc.

* **Tuần = bảy ngày lùi từ hôm nay**, không phải tuần lịch: cột cuối luôn bằng đúng con số trong
  thẻ Last 7 days ngay bên cạnh (một test gác đúng đẳng thức này). Cắt theo thứ Hai thì cột cuối
  là một tuần dở dang và hai chỗ trên cùng màn hình nói hai con số khác nhau về cùng một việc.
* **Đường kẻ ngang là trung bình của chính learner**, chia cho đủ 12 tuần kể cả tuần chưa học:
  nhịp tính trên thời gian TRÔI QUA, không trên thời gian đã ngồi học. Nó không phải chỉ tiêu —
  tuần dưới đường là một tuần bận, không phải một tuần trượt (cùng luật không-phán-xét với §8b).
* **Trục Y từ 0.** Kéo gốc trục lên biến một tuần 11 giờ so với 12 giờ thành một vực sâu.

### 10b. Recent wins

`workers/api/src/modules/ieltsSkills/wins.ts` — hàm thuần, và **mọi câu do server viết**, client
chỉ bày (cùng luật với `encouragement.ts`: hai nơi cùng suy luận là hai nơi sẽ lệch, mà lệch ở
đây nghĩa là hệ khen một đứa trẻ vì một việc nó chưa làm).

| Win | Điều kiện | Nguồn |
| --- | --- | --- |
| `result` | năng lực nhỏ có kết quả đẹp nhất tuần, gộp các lượt | `learning_events.ref` dạng `exercise:{skill}.{code}...` |
| `first` | bài đọc/nghe trọn vẹn ĐẦU TIÊN rơi đúng vào tuần này | `MIN(day)` không cắt cửa sổ |
| `vocabulary` | số từ mới gặp trong tuần | `vocabulary()` + `glossaryFor` |
| `improvement` | một năng lực nhỏ tiến bộ tuần này so tuần trước | hai cửa sổ 7 ngày |
| `streak` | chuỗi ngày không bỏ, qua MỌI kỹ năng | `streakDays` |

Bốn ràng buộc, mỗi cái chặn một kiểu nói dối:

* **Không có win dự phòng.** Chẳng có việc gì thì mảng rỗng và thẻ biến mất. Một lời khen không
  có việc thật đứng sau sẽ dạy learner rằng mọi lời khen ở đây đều không đáng tin, kể cả lời thật.
* **Ngưỡng.** Kết quả cần tối thiểu 4 câu và 70% đúng; tiến bộ cần nhảy ít nhất 10 điểm phần trăm
  và đủ 4 câu ở CẢ HAI tuần. "Đúng 1/1" và "từ 50% lên 51%" đều đúng về số học mà rỗng về nội dung.
* **"Lần đầu tiên" chỉ nói một lần**, nên nó phải rơi đúng vào tuần này mới được nhắc.
* **Chữ là "met", không phải "learned"** với vốn từ: phép đếm biết learner đã GẶP từ ấy trong một
  bài, nó không biết learner đã nhớ. Chỗ biết chuyện nhớ là `modules/retention/engine.ts`.

Giới hạn đã biết: phép trừ vốn từ chỉ nhìn trong 84 ngày, nên một từ gặp lần đầu từ bốn tháng
trước có thể được đếm lại là "mới" — chấp nhận có chủ ý, vì mở cửa sổ ra cả đời học nghĩa là đọc
toàn bộ sổ mỗi lần mở trang chỉ để một dòng khen chính xác hơn vài từ.

Năng lực nhỏ chưa có `name_en` trong `ielts_micro_skills` thì **bỏ câu ấy**, không in mã máy ra
cho learner đọc. Chữ viết tiếng Anh vì thẻ đứng trong sân IELTS (§8a).

Mã: `wins.ts`, `effort.ts`, route `ielts-effort` trong `routes.ts`; `WeeklyHoursCard` và
`RecentWinsCard` trong `apps/learn/src/IeltsPathway.tsx`.
Cổng kiểm: `wins.test.ts` (11 ca), `effort.test.ts` (4 ca 12 tuần), `routes.test.ts` (2 ca qua HTTP).

## 13. Sân luyện sau vòng feedback 2026-09-19 (SRC-862)

### 13.1 Trang Reading không còn đọc thành tiếng

Nút "Read the passage", ô chọn giọng và nút loa từng đoạn (SRC-787) đã gỡ khỏi
`PassageText.tsx`. Lý do: nghe là kỹ năng của sân Listening, và một bài Reading có sẵn giọng đọc
thì learner sẽ nghe thay vì đọc — đúng kỹ năng đang luyện bị thay bằng kỹ năng khác. Route
`/v1/public/ielts/reading/{cluster}/{seq}/audio` giữ nguyên: Listening vẫn dùng chung tầng TTS.

### 13.2 Thẻ Mastery của các năng lực nhỏ

Trước bản này Learner Model chỉ biết BỐN kỹ năng: `layer.ts` trả một `SkillView` cho mỗi kỹ năng,
không có tầng nào nhỏ hơn. Mastery theo năng lực nhỏ vì thế là phần thêm mới, dựng trên hai nguồn
bằng chứng đã có sẵn nhưng trước đây một nguồn bị vứt đi ngay tại trình duyệt:

| Nguồn | Bảng | Ghi từ |
| --- | --- | --- |
| Câu hỏi trong bài đọc/bài nghe, tách theo thẻ năng lực | `ielts_subskill_results` (migration 0254) | `POST /v1/learners/{id}/ielts-practice-attempts`, trường `breakdown` |
| Bài tập của chính năng lực ấy | `learning_events`, `ref = 'exercise:{skill}.{code}...'` | đã có từ SRC-850 |

Mã năng lực trong `content/` trùng đúng mã trong `ielts_micro_skills`, nên hai nguồn cộng thẳng
vào nhau mà không cần bảng quy đổi — và cố ý không có bảng ấy: một bảng quy đổi là chỗ hai danh
sách lặng lẽ trôi ra khỏi nhau.

Thang bậc (`workers/api/src/modules/ieltsSkills/mastery.ts`): dưới 8 câu thì KHÔNG gán bậc, trên
đó là Building (<50%) · Getting there (50–70%) · Solid (70–85%) · Strong (≥85%). Không có bậc nào
tên "Weak" và không có màu đỏ — cùng luật với ba thẻ số liệu ở §8.

Thẻ đứng trên cả bốn trang kỹ năng (`MasteryCard` trong `Hub.tsx`), mỗi dòng bấm được để đi thẳng
vào năng lực ấy: một bảng nói "Inference 44%" mà không có đường đi tới chỗ luyện Inference là bảo
learner tự xoay xở với một tin xấu.

### 13.3 Trang cụm bài rút một nửa, và hết tiếng Việt

`SkillPanel` bỏ ba khối: câu ghi nhận (`notes`), bảy ô vuông, và dòng lý do dưới mỗi nút. Cả ba do
server viết bằng tiếng Việt, mà sân luyện nói tiếng Anh (§8, SRC-834). Chúng KHÔNG được dịch máy
sang tiếng Anh: chúng là câu động viên, và một câu động viên dịch máy thì thà không có. Số liệu
tương đương vẫn còn trên trang kỹ năng dưới dạng ba thẻ `SkillStatusCard`.

Thẻ cụm còn tên và một dòng số; nhãn lĩnh vực và nhãn Unit bỏ đi vì lĩnh vực lặp lại đúng thứ dòng
lọc ngay trên vừa nói.

### 13.4 Khu luyện đề đứng riêng dưới cùng

`/ielts/{skill}/tests` và `/ielts/{skill}/tests/{id}`. Một đề = đúng số phần của đề thật, đúng thứ
tự, đúng thời gian: Reading 3 passage/60 phút, Listening 4 part/30 phút, Writing Task 1+2/60 phút
(20 + 40, theo trọng số thật), Speaking Part 1-2-3/14 phút. Nội dung ghép từ chính thư viện luyện
tập (`data/examTests.ts`) chứ không soạn kho thứ hai — kho thứ hai luôn là kho cũ hơn.

CHƯA CÓ, và trang nói thẳng điều đó: một đồng hồ chạy suốt cả đề, và một phiếu điểm cho cả bài.
Hai thứ ấy là một trang làm bài khác hẳn; dựng nửa vời thì một đồng hồ sai hoặc một điểm tổng nhầm
sẽ bị learner bắt được ngay lần thứ hai, rồi họ thôi tin cả phần còn lại.

## 20. Vòng feedback giao diện 2026-09-19, chiều tối (SRC-872, SRC-876, SRC-878)

Ba đợt chỉ đạo liên tiếp trong một buổi, cùng một hướng: **bớt chữ, và cho mỗi con số một việc rõ
ràng**. Ghi chung một mục vì chúng sửa cùng một bề mặt và tách ra thì ba mục nói lại cùng một lý do.

### 20a. Màn sub-skill còn một nửa số element (SRC-872)

Thẻ bài tập trước đây mang bảy thứ: huy hiệu bậc, mũi tên, tên tiếng Anh, tên tiếng Việt, một đoạn
mô tả, và ba con số. Nay còn hai: **tên tiếng Anh** (thứ learner chọn theo) và **một dòng số**
(`L2 · 5 items · 5 VI + 5 EN`). Trang danh sách sub-skill cũng vậy — số thứ tự và tên tiếng Anh,
hết. Lý do không phải thẩm mỹ: learner ở màn này đang **chọn** bài chứ chưa làm bài, và mô tả dài
thuộc về trang bài. Mũi tên bỏ vì cả thẻ đã là một link — nó lặp lại điều đó mười lần một màn.

Tên tiếng Việt bỏ theo luật sân luyện nói tiếng Anh (SRC-834/835), không phải để tiết kiệm chỗ.

### 20b. Vệt đường dẫn có một nguồn duy nhất (SRC-872)

Trước đó mỗi trang tự thêm nhánh gốc, và ba biến thể cùng sống: đúng (`kind: "pathway"` →
`/ielts`), **sai** (`kind: "home"` → `/courses`, nhãn vẫn đề "IELTS" nên bấm vào là rơi khỏi mạch),
và thiếu hẳn (hai trang lỗi mở đầu bằng "Listening"). Nay `Shell` tự dựng nhánh gốc và lọc bỏ nhánh
gốc trang truyền vào; một trang mới lỡ thêm lại cũng không ra hai chữ "IELTS" liền nhau. Mọi nhãn
trên vệt viết tiếng Anh, gồm cả tên cụm bài (`name_en`) và nhãn bậc (`L1 · VI`… `L5 · Exam`).

Quy tắc "mọi trang phải nhớ làm X" mà có hai chục chỗ phải nhớ thì sai sót là chuyện thời gian.

### 20c. Learning Setup và Settings vào menu avatar (SRC-872)

Hai dòng chữ nhỏ ở cuối trang hub là chỗ ít người cuộn tới nhất; avatar là chỗ người ta đã quen tìm
những thứ thuộc về "tài khoản của tôi". `AvatarMenu` nhận thêm `items` (mục riêng của từng cổng,
đứng trước các mục chung) và `english` — sân IELTS đã gỡ công tắc VI/EN nên `t()` ở đây sẽ trả về
tiếng Việt và menu thành nửa Việt nửa Anh.

### 20d. Thẻ 200 giờ tính từ NGÀY BẮT ĐẦU HỌC, và chỉnh được nhịp (SRC-876)

**Vì sao cửa sổ cố định sai.** Ba nhịp cũ (7 ngày, 30 ngày, 12 tuần) đều chia cho một mẫu số cố
định. Với người bắt đầu hôm qua, hai giờ học chia cho ba mươi ngày ra "4 phút một ngày", và ngày đủ
200 giờ rơi vào năm 2029 — đúng về số học, vô nghĩa với người vừa bắt đầu. `since_start` chia tổng
số phút cho **số ngày kể từ buổi học đầu tiên**, tính cả hai đầu (mẫu số nhỏ nhất là 1, không phải
0: mẫu số 0 cho nhịp vô cực và ngày đích là hôm nay, một lời hứa sai).

**Hai nút.** Cộng ở trên, trừ ở dưới, mỗi lần một giờ mỗi tuần; đây là chỗ learner hỏi "nếu học
thêm một giờ một tuần thì sớm hơn bao nhiêu", và câu trả lời phải hiện ra trong cùng thẻ chứ không
sau một vòng mạng. Vì vậy phép tính chạy ở client (`apps/learn/src/ielts/plan.ts`) và là **bản phản
chiếu** của `dayReaching` trong `workers/api/src/modules/ieltsSkills/effort.ts`. Hai bộ test dùng
đúng cùng những con số (200 giờ ở 10 giờ/tuần là 140 ngày; nhịp 0 im lặng; quá mười năm im lặng),
nên lệch một bên là một bộ test đỏ chứ không phải một màn hình nói hai ngày khác nhau.

**Mốc thi và band.** Route `ielts-effort` trả thêm `exam` (đọc `learner_ielts_goals`) và
`current_overall` (đọc `learner_ielts_baselines`), cả hai qua một helper chịu được cảnh bảng chưa
có. Thẻ nói ngày đích sớm/muộn hơn ngày thi bao nhiêu, và band sau 200 giờ = band hiện tại + 1.0,
chặn ở 9.0 vì thang IELTS dừng ở đó. Chưa khai mốc thi hay band thì thẻ **im lặng** về chúng thay
vì bịa — "sớm hơn ngày thi 40 ngày" khi learner chưa đặt ngày thi nào là một câu thật sự sai.

Cùng đợt: bỏ hai thẻ **Today** và **Last 30 days** (chúng đo lại thứ "Last 7 days" đã đo, chỉ đổi
cửa sổ; và thẻ Today chào learner bằng số 0 vào ngày chưa kịp học), đưa **Recent wins** lên hàng
đầu, và rút thẻ 12 tuần còn nửa chiều cao để nó đứng cùng hàng với thẻ tiên đoán — một cái nói nhịp
đã qua, cái kia nói nhịp ấy dẫn tới đâu.

### 20e. Band chỉnh bằng hai nút, và Overall đứng trên cùng (SRC-878)

`<select>` mười một dòng đổi thành `BandStepper`: tên kỹ năng, điểm căn giữa, hai nút nhích nửa
điểm. Chọn band là một bước **nhích** quanh chỗ mình đang đứng chứ không phải một lần chọn trong
danh sách, và trên điện thoại cái danh sách ấy phủ nửa màn để đổi đúng một nấc. Nút chạm trần (4.0
hoặc 9.0) thì **tắt chứ không biến mất** — một nút vắng mặt làm hàng nút nhảy chỗ.

Overall lên **trên cùng** và mang màu accent: nó là con số learner và phụ huynh thật sự nói về nhau
("con đang 6.5"), còn bốn band là cách nó được tạo ra. Nó vẫn không có ô nhập — vẫn là kết quả của
bốn ô dưới.

**Cancel đứng cạnh Save** (`CancelEditButton` + `CancelContext` trong `EditGate`). Trước đây Cancel
ở góc trên thẻ còn Save ở cuối form: hai nút của cùng một quyết định — giữ hay bỏ những gì vừa gõ —
mà người dùng phải tìm ở hai chỗ.

### 20f. Tiêu đề thẻ căn giữa, và một dấu hỏi giải thích ở mỗi thẻ (SRC-878)

`CardHint` nhận hai phần và **cả hai đều bắt buộc**: `what` (thẻ đo cái gì) và `next` (nhìn xong
nên làm gì). Chỉ có `what` thì popover là một chú thích từ điển — learner đọc xong vẫn đứng yên tại
chỗ. Lời giải thích không in thẳng lên thẻ vì learner cần nó đúng một lần, thường là lần đầu gặp
màn hình; in ra thì mọi lần sau nó là chữ thừa (luật tối giản SRC-048 đếm element).

Popover đi bằng Radix (`components/ui/popover.tsx` theo chuẩn shadcn) để có sẵn bàn phím, focus
trap và `aria-expanded` — DS-001 §0 chốt hành vi đi bằng Radix, không dựng tay.

Cùng đợt: **bỏ tiêu đề trang ở cả bốn trang kỹ năng** — vệt đường dẫn ngay trên đã nói
"IELTS / Reading", và một dòng chữ to lặp lại đúng chữ ấy đẩy ba thẻ số liệu xuống dưới nếp gấp.

## 36. Vòng feedback giao diện 2026-09-20, sáng (SRC-906)

### 36a. Trang chủ bỏ đi ba thứ

* **Tiêu đề "IELTS" ở đầu trang.** Logo *NEMO IELTS* trên thanh đã nói learner đang ở đâu; dòng chữ ấy chỉ đẩy dashboard xuống thêm một dòng.
* **Thẻ "200 hours".** Thẻ tiên đoán ngay dưới đã vẽ trọn 200 giờ thành 40 khối, kèm "30h done" và "200h total". Một thanh tiến độ nói lại đúng chừng ấy là một con số bắt đọc hai lần.
* **Các đường kẻ trong thẻ "Small wins"**, cộng với chữ mờ đi (`text-muted-foreground`) và dấu tích nhỏ lại. Đây là thẻ learner **liếc** qua, không phải bảng để tra từng dòng; chữ sáng bằng chữ tiêu đề thì nó giành mất chỗ của việc chính trên trang.

Bảy ô vuông của "Am I showing up?" cũng nhỏ lại (h-10 → h-5). Câu hỏi là **có hay không** ngồi xuống, và bảy khối lớn chiếm nửa chiều cao thẻ để trả lời một câu hỏi nhị phân.

### 36b. Thẻ "Where you are now": điểm hiện tại và lần đo tiếp theo

Thẻ đọc bài đo đang `active` trong `learner_ielts_baselines` — endpoint `ielts-effort` nay trả cả bốn kỹ năng, `assessed_on` và `source`, không chỉ `overall`. Trường `current_overall` cũ **giữ nguyên**, vì thẻ tiên đoán đã đọc nó và một app cũ đang chạy không được hỏng giữa buổi học.

**Ngày đo tiếp theo tính từ NHỊP HỌC, không từ một cái lịch.** Mạch này đã chốt: 200 giờ đổi lấy 1.0 band (`bandAfterTarget`). Nửa band vì thế là 100 giờ, và ngày learner học đủ 100 giờ nữa chính là ngày đi đo lại **có nghĩa** — đo sớm hơn thì thước không nhích, và một bài đo không nhích dạy learner rằng học không ăn thua gì. Lịch cứng ("ba tháng một lần") thì ngày đo đứng yên bất kể learner học nhiều hay ít; ở đây học chăm thì ngày đo tự tiến lại gần, và đó là thứ duy nhất learner điều khiển được.

Bốn kỹ năng cùng +0.5 là **mặc định**, không phải tiên đoán: chưa ai biết kỹ năng nào nhích trước, nên hệ chia đều thay vì đoán. Vì thế bốn con số ấy đứng mờ hơn điểm hiện tại, và lời giải thích sau dấu hỏi nói thẳng rằng đây là mặc định. Chưa đo lần nào thì thẻ **biến mất** — cùng luật im lặng với thẻ wins.

### 36c. Mã lớp live còn bốn chữ số, và màn nhập căn giữa

Xem [SDD-040 §3](../sdd-040-live-sessions.md). Phía màn hình: cả trang căn giữa, bỏ hết chữ giải thích trừ một dòng, vì trang này mở ra đúng một lần mỗi buổi và chỉ có một việc — nhận bốn chữ số trong lúc learner đang nghe mentor đọc qua Zoom.

## 40. Ba chỗ gọn lại ở hub IELTS và trang Task types (SRC-917, 20.09.2026)

**Thẻ "When you reach 200 hours" bỏ hẳn.** Nó chiếm trọn một màn hình cuộn để nói một lời tiên
đoán, trong khi thẻ nhịp 12 tuần ngay cạnh đã nói cùng một điều bằng dữ liệu đã xảy ra thật. Giữa
một con số suy ra và một con số đo được, luật tối giản SRC-048 giữ con số đo được.

**Live class và Leaderboard đứng cùng một hàng**, mỗi cái một dòng chữ `text-sm` trong ô `py-2`.
Cả hai là LỐI ĐI tới chỗ khác chứ không phải việc luyện, nên chúng không đáng chiếm hai dải chiều
ngang ngay trên bốn kỹ năng. Dưới `sm` vẫn xếp chồng: hai nhãn ấy cạnh nhau ở 360px thì chữ vỡ.

**Con số bài tập rời góc thẻ Task types, vào popover.** Ở góc thẻ nó là một số trần mà người đọc
còn không đoán ra đơn vị; trong popover nó nói hết câu ("140 exercises ready. Open the card to
start."), cạnh tên Việt, mô tả và các mẹo làm dạng ấy. Dấu `?` dùng đúng khuôn `CardHint` của
SRC-878, nên cả sân luyện chỉ có MỘT cách hỏi "cái này là gì".

Bẫy phải giữ: Radix `PopoverTrigger` là một `<button>`, và một button nằm trong `<a>` vừa sai HTML
vừa khiến một cú bấm vừa mở popover vừa điều hướng. Vì thế link của thẻ là lớp phủ
`absolute inset-0` nằm dưới dấu hỏi, không phải thẻ bọc ngoài.

### 35g. Cổng đếm bài Reading chưa gán dạng (SRC-918, 2026-09-20)

Lệnh gán nhãn trong migration 0264 chạy **đúng một lần**. Nó ôm trọn mọi bài đã có lúc ấy — kể cả 40 bài mà các phiên khác thêm vào trong lúc nhánh SRC-895 đang chạy, nhờ điều kiện `WHERE task_type_id IS NULL` thay vì một danh sách id cố định. Nhưng bài Reading thêm vào **sau** đó, nếu quên khai `task_type_id`, sẽ mang NULL.

Và chỗ hỏng ấy không hiện ra trên màn hình. Trang năng lực dùng `LEFT JOIN` nên vẫn hiện đủ bài; trang dạng lọc theo `task_type_id` nên bài ấy biến mất. Một nửa sản phẩm nói "20 bài", nửa kia nói "19", và không ai đọc ra được sự chênh nhau nếu không ngồi đếm tay.

`scripts/check-reading-task-types.mjs` (trong `npm run check:code`) kiểm hai điều trên **file migration**, không hỏi D1:

1. Bài Reading nạp ở migration **sau 0264** phải khai `task_type_id` và giá trị không NULL.
2. Dạng được gán phải có thật trong bảng dạng ở 0264. SQLite không bật kiểm khoá ngoại theo mặc định, nên một mã gõ sai sẽ nạp thành công rồi nằm im — bài vẫn trong CSDL, vẫn hiện ở trang năng lực, và vĩnh viễn không xuất hiện ở bất kỳ trang dạng nào.

**Vì sao đọc file chứ không hỏi D1:** cổng này chặn deploy, và một cổng chặn deploy mà phụ thuộc mạng thì đến ngày Cloudflare nấc là mọi phiên đứng — đúng lập luận đã dùng để giữ `check:ui-latest` ngoài CI (DS-001 §0). Migration trong repo là thứ **sinh ra** trạng thái của D1, nên bắt ở đây là bắt trước khi hỏng.

**Một lỗi của chính cổng, đáng ghi lại.** Bản đầu cắt câu lệnh bằng `/[^;]*;/` và cắt cột bằng `split(",")`. Cả hai hỏng trên cùng một thứ: chữ tiếng Việt trong `blurb_vi`. Một dấu chấm phẩy giữa câu mô tả cắt đứt câu lệnh, và cổng đếm được **0 bài rồi in ra chữ PASS** — tức là cổng canh lỗi im lặng lại hỏng đúng kiểu im lặng ấy. Nó lộ ra vì mỗi ca kiểm đều so **số bài đếm được**, không chỉ so mã thoát. Nay cả ba hàm tách đều đi qua chuỗi bằng tay và hiểu `''` là dấu nháy thoát.

Bài học rộng hơn cho mọi cổng đọc SQL trong repo: **một cổng in PASS mà không nói nó đã kiểm bao nhiêu thứ thì không phân biệt được với một cổng chưa kiểm gì.** Con số trong dòng PASS là phần bắt buộc, không phải trang trí.

## 54. Vòng feedback giao diện sân luyện (SRC-898, 2026-09-20)

Ghi lại SAU khi việc đã lên `main`: đợt này làm xong mà không kịp ghi sổ lẫn ghi mục canonical,
nên Audit #015 (H-3) nhặt ra. Mục này mô tả đúng thứ đang chạy, đọc từ mã nguồn chứ không từ lời
commit.

* **Nhãn bậc viết đủ chữ**, thay vì một ký hiệu rút gọn mà learner phải đoán nghĩa.
* **Trang "cách làm" đứng riêng**: `/ielts/skills/<kỹ năng>/<mã>/how`, route `microTheory`. Lý
  thuyết tách khỏi chỗ làm bài, nên learner đang làm dở không phải cuộn qua phần giải thích.
* **Bài đã làm được đánh dấu**, dựa trên endpoint mới
  `GET /v1/learners/{id}/ielts-micro-attempts?skill=&code=` — nó trả về bài nào đã làm và làm
  mấy lần, nên màn danh sách không phải đoán từ điểm số.
* **Menu tài khoản có mặt ở mọi trang sân luyện**, không chỉ trang chủ.
* **Thẻ "Am I showing up?" lên trang chủ** (`IeltsPathway.tsx`), tức là câu hỏi về sự đều đặn
  đứng cùng chỗ với các con số khác thay vì nằm trong một trang riêng.

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

### Từ mục 7. Thẻ bằng nhau, và thanh trên của điện thoại (SRC-820)

Chỉ đạo chủ dự án 2026-09-18: **kích thước thẻ, cả ngang lẫn cao, phải bằng nhau trên nhiều trang**.

### Từ mục 11. Recent wins và nhịp 12 tuần trên `/ielts` (SRC-853, 2026-09-19)

Chỉ đạo chủ dự án 2026-09-19 (kèm bản prototype): thêm vào dashboard `/ielts` một thẻ **Recent
wins** với những dòng kiểu *"You got 8/10 Main Idea questions correct"*, *"You learned 42 new
vocabulary items this week"*, *"Your Matching Headings accuracy improved from 48% to 64%"*, và
một **biểu đồ 12 tuần** cho thấy mỗi tuần learner học mấy giờ.

### Từ mục 13. Sân luyện sau vòng feedback 2026-09-19 (SRC-862)

Bốn thay đổi trong một vòng, đều từ ảnh chụp màn hình của chủ dự án.

Chỉ đạo: *"Cần có card để hiển thị các sub-skills liên quan tới riêng Reading, và mastery level
của từng sub-skills đó."*

Chỉ đạo: *"Lượt bớt 50% elements trên trang này cho gọn, ko còn bất kỳ từ tiếng Việt nào nữa."*

Chỉ đạo: *"khu vực 'đề thi, luyện đề', luôn cần đặt riêng xuống một khu vực riêng ở dưới cùng.
Click vào đó thì ra danh sách một số đề, và các đề giống thật nhất có thể."*

### Từ mục 20. Vòng feedback giao diện 2026-09-19, chiều tối (SRC-872, SRC-876, SRC-878)

*"Giảm 50% elements trên màn hình này"*, rồi ngay sau đó *"giảm tiếp 50% nữa"*.

*"Breadcrumb Trail đang bị sai sai. Đúng ra là nó luôn có IELTS ở đầu, click vào thì về trang
/ielts."*

*"Cần tính trung bình từ ngày bắt đầu học, ví dụ từ ngày hôm qua, chứ không phải tính trung bình từ
7 hay 14 ngày trước… Hệ thống cho 2 buttons thôi: Cộng (hiển thị bên trên) và trừ (hiển thị bên
dưới), cộng 1 hoặc trừ 1 hours per week… Và nó so với mốc mà mình muốn để thi. Và nó cộng thêm vào
band hiện tại 1.0 điểm Overall."*

*"Với mỗi Skill nhỏ, cần căn giữa, có 2 button nhỏ là cộng và trừ… Điểm Overall hiển thị trên cùng,
và màu đỏ hoặc da cam."*

*"Mấy card headers, cần căn giữa… Góc trên phải của mỗi card, đều là một icon nho nhỏ, để click vào
thì hệ thống mở ra một Popover, để giải thích về tác dụng của cái card đó. Và Learner xem xong thì
biết nên làm gì tiếp."*

### Từ mục 36. Vòng feedback giao diện 2026-09-20, sáng (SRC-906)

Chỉ đạo chủ dự án gồm bốn việc trên ảnh chụp `/ielts` và một việc trên `/ielts/live`.

Chỉ đạo: *"điểm hiện tại, điểm Overall, thời điểm đo đạc"* và *"thời gian đo đạc tiếp theo, khi đó điểm overall tăng 0.5, mặc định là cả 4 skills đều tăng 0.5"*.

### Từ mục 40. Ba chỗ gọn lại ở hub IELTS và trang Task types (SRC-917, 20.09.2026)

Chỉ đạo chủ dự án, một vòng xem màn hình trên `learn.nemo12.com/ielts`:

> *"Ẩn card When you reach 200 hours. Cho 2 card Live class với Leaderboard cần ngắn lại, và cần
> nằm cùng trên 1 dòng."*
>
> *"Ở góc mỗi card có một icon, click vào icon đó thì ra Popover... Bỏ cả con số ở góc đi, và đưa
> con số đó vào trong Popover."*
