---
url: https://docs.nemo12.com/architecture/sdd-038/learning-plan.md
description: >-
  Learning Plan là một lời hẹn, Plan Board, monthly plan, lưu CSDL, chia sẻ công
  khai, số tháng mặc định
---

# SDD-038 · Learning Plan

> 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.

## 39. Learning plan: một lời hẹn, không phải một biểu mẫu (SRC-916, 2026-09-20)

### a. Thứ bị gỡ, và vì sao gỡ là đúng

Trang có hai khối, cả hai đều chạy đúng: bốn ô chọn band hiện tại, rồi một nút gọi model chia các
mốc trung gian. Nhưng chúng trả lời sai câu hỏi mà learner mang tới đây. Nó mở **"Learning plan"**
để XEM kế hoạch của mình, và gặp một biểu mẫu bắt nó **tự khai điểm** rồi **tự bấm nút** để máy
đoán hộ.

Chỗ hỏng không nằm ở kỹ thuật. Một kế hoạch máy chia từ bốn con số learner tự đoán trông **y hệt**
một kế hoạch thật: cùng bảng, cùng ngày tháng, cùng con số band. Nó mời một đứa trẻ tin vào thứ
chưa có ai nhìn. Kế hoạch thật ra đời ở buổi gặp 1-1, nơi thầy cô đã đọc bài viết và nghe đoạn nói
của chính đứa trẻ ấy.

### b. Không mất đường nào

Khai điểm hiện tại vẫn còn ở ba chỗ (`IeltsSettings`, `IeltsLearningSetup`, `IeltsGoalCard`) — và
ba chỗ ấy mới đúng là nơi của nó: chúng là ngăn **khai báo**, không phải trang **xem kế hoạch**.
Gỡ ở đây không lấy đi khả năng nào, nó chỉ thôi hỏi một câu ở sai chỗ.

Dòng "Learning plan" ở lại trong `IELTS_TASKS`: nó là bước cuối của năm chặng, và bỏ hẳn thì
learner mất luôn chỗ để biết bước ấy tồn tại và đang chờ điều gì. Trang nay nói **vì sao** phải
chờ chứ không chỉ nói "hãy chờ", và có một đường sang đặt buổi gặp để nó không thành ngõ cụt.

### c. Tiếng Anh, và mấy chuỗi sót lại

`learn.nemo12.com/ielts/**` nói tiếng Anh (SRC-834/835), và trang này đã đổi từ SRC-865. Ba chuỗi
tiếng Việt vừa gỡ cùng biểu mẫu (`"Con đang ở đâu?"`, `"Lưu"`, `"Chia mốc"`) là **sót lại** từ
trước lần đổi ấy, không phải một ngoại lệ có chủ ý.

### d. Gác bằng e2e, vì typecheck không thấy một biểu mẫu mọc lại

Thứ phải **vắng mặt** ở đây là một biểu mẫu. Nó mọc lại thì không có gì đỏ. Nên phép kiểm gác cả
hai vế: câu thông báo có mặt, và `main select` cùng nút `Lưu` đếm bằng 0.

## 60. Plan Board: bốn kỹ năng × các tháng, và số giờ phải bỏ ra (SRC-952, 22.09.2026)

### 60a. Nó khác `/ielts/setup/plan` ở chỗ nào

Trang mốc (`plan`) nói learner phải TỚI ĐÂU ở vài thời điểm. Plan Board nói phải BỎ RA BAO NHIÊU
để đi từ mốc nọ sang mốc kia. Một learner đọc mốc thì biết đích; đọc bảng này mới biết tuần sau
mình phải ngồi mấy giờ. Hai câu hỏi khác nhau nên hai trang khác nhau, và gộp lại thì bảng mốc
(vốn ngắn, đọc một liếc) sẽ bị một bảng mười hai cột nuốt mất.

### 60b. Ba con số là luật

| | Giá trị | Ở đâu |
| --- | --- | --- |
| Mốc | ngày 1 hoặc 15 hàng tháng, cách nhau 30-31 ngày | `planMilestones` |
| Mặc định | mỗi tháng 2 kỹ năng, mỗi kỹ năng +0.5 | `defaultPlan` |
| Quy đổi | 0.1 điểm ≈ 3 giờ | `HOURS_PER_TENTH` |

Hiện chúng là hằng số trong bundle. **Khi nối back-end chúng phải thành cấu hình đọc từ server**:
con số 3 giờ là một giả định về việc học, và một giả định sửa được bằng tay ở production thì không
được ghim trong mã trình duyệt.

### 60c. Learner chỉnh từng ô, bước 0.1

Hai nút `−` và `+` thay cho kiểu bấm-để-xoay-vòng: xoay vòng thì muốn 0.3 phải bấm ba lần và đi
qua hai giá trị sai, còn muốn lùi thì phải đi hết vòng. Nút `+` khoá khi ô chạm trần — khoảng còn
thiếu tới mục tiêu TẠI thời điểm ấy — vì cộng quá đích là bày ra một kế hoạch tiêu giờ vào phần
điểm không tồn tại.

Cộng dồn bằng **phần mười nguyên** (`Math.round(x * 10)`), không cộng thẳng số thực: `0.1 + 0.2`
trong dấu phẩy động ra `0.30000000000000004`.

### 60d. Chưa lưu, và trang nói thẳng

Thiếu mục tiêu hoặc thiếu điểm hiện tại thì **không dựng bảng**, chỉ nói thiếu gì: một bảng vẽ từ
điểm bịa ra trông y hệt một bảng thật.

### 60e. Đường vào

`/ielts/plan-board`, đứng ngang `setup` · `settings` · `profile` — không phải một `IeltsTaskKey`,
vì nó không có điểm kết thúc. Ở hub `/ielts` nó là ô ĐẦU trong hàng ba lối đi (trước Live class và
Leaderboard) và mang màu accent: hai cái kia là chỗ xem cho vui, còn đây là thứ duy nhất trong
hàng đổi được việc learner làm hôm nay.

**Chỗ dễ sai:** nhánh legacy `/ielts/<task>` trong `pathToRoute` nhận mọi khoá lạ rồi đổ sang mạch
luyện, nên `plan-board` phải đứng TRƯỚC nó — cùng bẫy đã sập ở SRC-950 với `skills`. Có test chặn
trong `ieltsRoutes.test.ts`.

### 60f. Tooltip cột tháng nói tiếng Anh

Bấm vào đầu cột hiện *"In November 2026 · milestone 01.11.2026"*. Bản prototype viết câu này bằng
tiếng Việt; chủ dự án chốt tiếng Anh ngày 22.09.2026, đúng luật sân luyện `/ielts/**` (SRC-834/835)
— một câu tiếng Việt ở đây sẽ là ngoại lệ duy nhất trong cả màn hình.

## 66. Learning Plan: đọc trước, sửa sau, và một cái thước giờ (SRC-958, 22.09.2026)

* **Tên trang là Learning Plan.** "Plan Board" là tên của một thứ đồ nghề; learner không đi tìm một
  cái bảng, họ đi tìm kế hoạch học của mình.
* **Mặc định đọc.** Tám mươi cái nút tròn (bốn hàng × mười cột × hai nút) đập vào mắt trước cả con
  số. Chúng chỉ hiện khi bấm Edit, cùng luật với `BandStepper` ở §62a.
* **Bỏ ngày dưới tên tháng** ở đầu cột: nó lặp lại đúng thông tin của dòng trên bằng chữ nhỏ hơn.
  Ngày đầy đủ vẫn đọc được trong tooltip khi bấm vào tháng.
* **Hai thẻ điểm ở trên cùng**, chỉ đọc. Sửa chúng là việc của trang Goal; hai chỗ cùng sửa một giá
  trị là hai chỗ sẽ lệch nhau.
* **Cụm "Study time per month"** ở cuối trang, learner sửa được. Nó không phải một con số của kế
  hoạch mà là cái THƯỚC để đọc kế hoạch, nên nó đứng sau thứ nó đo. Hàng tổng giờ được tô theo nó:
  vượt mức là coral, nhẹ hơn 60% mức là xanh. Coral chứ không đỏ — REQ-UX-03, và một tháng nặng là
  việc cần cân lại, không phải một lỗi sai.
* Hai dòng hướng dẫn cũ đã bỏ: cách chỉnh một ô thì hai cái nút tự nói ra khi learner bấm Edit.

## 67. Nút xếp hộ: nhịp khởi động bốn kỹ năng (SRC-959, 22.09.2026)

### 67a. Hai luật, và cả hai nói về việc HỌC

1. **Thứ tự vào cuộc.** Reading và Listening trước vì chúng là kỹ năng TIẾP NHẬN: learner tiến được
   mà chưa cần tự tạo ra ngôn ngữ. Writing và Speaking là kỹ năng TẠO RA, và chúng ăn vào chính vốn
   từ với cấu trúc mà hai kỹ năng đầu vừa dựng. Đẩy Speaking vào tháng đầu là bắt learner nói bằng
   thứ họ chưa đọc đủ để có.
2. **Nhịp tăng dần.** Tháng đầu nhẹ (0.2 mỗi kỹ năng), tháng hai vừa (0.4), từ tháng ba là nhịp đầy
   (0.5). Một kế hoạch đòi nhịp đầy ngay tuần đầu là kế hoạch vỡ ở tuần thứ ba.

`RAMP_START_MONTH` và `RAMP_STEPS` trong `IeltsPlanBoard.tsx`; hàm thuần `rampPlan`, có test riêng
ở `planBoard.test.ts` (8 ca).

### 67b. Ba chỗ dễ sai, đã có ca gác

* **Vượt đích.** Cột cuối của một kỹ năng phải là phần lẻ còn lại, không phải một nhịp đầy tràn qua
  mục tiêu: vượt đích nghĩa là bảng xếp giờ học cho phần điểm không tồn tại.
* **Kỹ năng đã ở đích** không được nhận cột nào khác 0.
* **Cả bốn đã ở đích** thì vẫn trả một cột rỗng, để bảng có chỗ đứng thay vì biến mất.

### 67c. Nút chỉ hiện khi đang sửa

Nó GHI ĐÈ cả bảng, nên nó không được đứng trong một màn learner tưởng là chỉ để đọc. Câu dưới nút
nói trước nó sắp làm gì — một nút ghi đè không báo trước là một nút người ta chỉ dám bấm một lần
rồi thôi.

## 68. Learning Plan lưu xuống CSDL (SRC-960, 22.09.2026)

### 68a. Ba chỗ bảng chưa đúng, tìm ra khi rà trước lúc lưu

1. **Mốc trôi.** Các mốc tính từ ngày hôm nay. Lưu "cột thứ n tăng bao nhiêu" thì learner mở lại
   sau hai tháng sẽ thấy đúng những con số cũ nằm dưới những cái tháng MỚI — cả kế hoạch đổi nghĩa
   mà không ai đụng vào. **Mỗi ô nay mang theo ngày mốc của chính nó.**
2. **Bảng thiếu cột.** Learner hạ mức tăng của vài tháng là đích lùi ra ngoài cột cuối, và dòng
   tổng kết nói "not reached yet" mà không có chỗ nào sửa cho tới nơi — một bảng tự mâu thuẫn. Nay
   thiếu thì tự nối thêm cột rỗng, tới trần 24.
3. **Ngày trong chuỗi ISO.** `new Date("2026-10-01")` được đọc là UTC; ở giờ Việt Nam nó ra 01.10
   lúc 07:00 — đúng ngày, nhưng chỉ là may. Cả hai chiều nay đi qua `isoDate`/`spreadSavedPlan`,
   dựng `Date` từ ba con số theo giờ địa phương.

### 68b. Hai bảng, khoá theo ngày

`learner_ielts_plan` (một dòng mỗi learner: `monthly_hours`) và `learner_ielts_plan_cells` (một
dòng mỗi ô: `milestone_date` · `skill` · `gain`). Migration 0270, idempotent.

Không dùng một cột `plan_json`: nó ghi nhanh hơn, nhưng mọi câu hỏi về sau ("tháng này learner nào
đang đặt quá 40 giờ") đều phải kéo cả cục JSON ra rồi tự bóc.

**Ghi là THAY CẢ BẢNG** trong một `batch`: learner bấm Save một lần cho cả lưới, và ghi từng ô thì
hai lượt lưu chồng nhau sẽ để lại một bảng lai — vài ô của lượt trước, vài ô của lượt sau, không
lượt nào đúng. Có ca e2e-style gác đúng điều này, vì bảng lai vẫn trả 200 và vẫn vẽ ra được.

**Ô bằng 0 không lưu**: nó là mặc định "tháng ấy không nâng kỹ năng này". Lưu chỉ làm bảng phình
ra bốn lần. `spreadSavedPlan` điền lại chỗ trống khi đọc.

### 68c. Lọc đầu vào ở server, không tin client

Mốc phải rơi đúng ngày 1 hoặc 15 và là ngày có thật; `gain` phải là bội của 0.1 trong khoảng 0-5;
khoá trùng là lỗi chứ không lấy dòng cuối; `monthly_hours` là số nguyên 1-200. Một `gain` bằng 3.7
ghi xuống sẽ sinh ra kế hoạch mà chính giao diện cũng không vẽ lại được.

**Một lượt PUT hỏng không được đụng tới bảng đang lưu** — learner gõ nhầm một ô rồi mất cả kế
hoạch là kiểu mất mát không ai tha thứ được. Có ca gác riêng.

## 72. Learning Plan: hai thẻ sửa được, và một ranh giới (SRC-967, 22.09.2026)

* **Bỏ dòng "← IELTS"**: thanh trên đã có logo bấm về được, nên một đường lùi thứ hai chỉ đẩy tiêu
  đề xuống thêm một dòng.
* **Hai thẻ sửa được tại chỗ.** Ở SRC-958 chúng chỉ đọc, với lý do "sửa là việc của trang Goal" —
  mà trang Goal đã bỏ ở SRC-961, và đây đúng là màn learner đang nhìn khi nhận ra con số sai. Vẫn
  MỘT nguồn: cả hai lưu thẳng vào `ielts-baseline` và `ielts-goal`, không có bản sao nào ở đây.
* **Ba nút Edit, ba phạm vi rõ ràng.** Thẻ Current, thẻ Target, và bảng — mỗi thứ một `EditGate`.
  Nút của bảng đứng ở góc trên phải của chính bảng; để nó ở đầu trang cạnh hai nút kia thì không
  ai đoán được nó sửa cái gì.
* **Một đường kẻ và một quãng trống** giữa hai thẻ và bảng: hai thẻ nói learner đang ở đâu và đi
  tới đâu, bảng nói đi bằng cách nào.
* **Cụm dưới cùng gộp thành một dải bốn ô**: ba con số kết quả (tổng giờ · số tháng · ngày chạm
  đích) và một con số learner tự đặt (giờ mỗi tháng). Chúng vốn là cùng một câu chuyện — "kế hoạch
  này tốn bao nhiêu, và bấy nhiêu có vừa sức con không" — nên tách làm hai khối thì phải đọc hai
  lần mới ghép lại được. Ô sửa được có viền accent, thay cho dòng chú thích đã bỏ.

**Ba `aria-label`** (`Current` · `Target` · `Monthly plan`) là cách duy nhất chỉ đúng một khối trên
một trang có ba nút Edit giống hệt nhau — cho trình đọc màn hình, và cho e2e.

## 73. Learning Plan: chiều cao đứng yên, và ba nguyên tắc (SRC-970, 22.09.2026)

### 73a. Nút cộng trừ GIỮ CHỖ ở chế độ đọc

SRC-954 ẩn hẳn hai nút ấy, và đó vẫn đúng: một nút mờ cạnh con số là lời mời bấm mà bấm không làm
gì. Nhưng ẩn bằng cách bỏ khỏi luồng thì ô co lại, cả thẻ đổi chiều cao, và hai thẻ cạnh nhau nhảy
lên nhảy xuống mỗi lần bấm Edit.

Nay chúng dùng `invisible` chứ không `hidden`: không vẽ ra, nhưng vẫn chiếm chỗ. Kèm `aria-hidden`
và `tabIndex={-1}` để bàn phím và trình đọc màn hình không vấp phải một nút vô hình. Có ca e2e đo
chiều cao thẻ trước và sau khi bấm Edit.

### 73b. Bốn ô band đọc theo CỘT

```
Listening │ Writing
Reading   │ Speaking
```

Cột trái là hai kỹ năng **tiếp nhận**, cột phải là hai kỹ năng **tạo ra**. Đó là cách learner nghĩ
khi quyết đẩy cái nào trước, và cũng là cách nút "Let Nemo arrange it" xếp lịch (§67). Xếp theo
hàng L-R-W-S thì hai nhóm bị trộn vào nhau, và cái lưới không nói được gì ngoài thứ tự của một quy
ước. Đổi ở `BAND_SKILLS` nên `/ielts/settings` theo cùng.

### 73c. Cả hai thẻ đều sửa được ngày

Thẻ Current mang ngày lần thi đã qua, Target mang ngày thi sắp tới. Cả hai đều là thứ learner tự
khai và tự nhớ sai được. Một hàng ngày duy nhất, `h-9` cố định cho cả hai chế độ — xem 73a.

### 73d. Thẻ "How to build a plan that works"

Ba nguyên tắc, đặt ở CUỐI trang:

1. Bắt đầu bằng một hai kỹ năng, không phải cả bốn. Reading và Listening trước.
2. Ít nhất mười giờ một tuần, khoảng bốn buổi ba giờ.
3. Nửa band một tháng là nhịp tốt.

Vì sao cần chúng: bảng phía trên cho learner sửa bất cứ ô nào, kể cả thành một kế hoạch không ai
theo nổi. Nút xếp hộ đưa ra bản đúng nhịp nhưng không NÓI RA vì sao nhịp ấy đúng — và một learner
không hiểu lý do sẽ sửa nó về đúng cái kế hoạch vừa được sửa đi.

Đặt cuối chứ không đầu: learner mở trang này để xem kế hoạch của mình, không phải để đọc luật.

**Chữ tiếng Anh, viết cho một đứa trẻ lớp năm** — câu ngắn, từ thường ngày, và chỗ nào cần một khái
niệm thì nói bằng hình ảnh ("your brain needs to take in a lot of English before it can make good
English"). Đây là ngoại lệ có chủ ý của SRC-953 (chữ hướng dẫn nói tiếng Việt): chính chủ dự án
chốt tiếng Anh cho riêng mấy dòng này.

## 76. Monthly plan: đầu bài lên trên, và mốc là cuối tháng (SRC-974, 22.09.2026)

**Vì sao hai con số phải đứng trên bảng.** Chúng không phải kết quả, chúng là ĐẦU BÀI: số cột và
mức giờ learner kham được mỗi tháng. Hàng tổng của bảng được tô màu theo đúng mức giờ ấy (coral
khi vượt), nên để con số ở cuối trang là bắt learner đọc một bảng đã tô màu rồi mới xuống tìm
xem nó đang tô theo luật gì. Dải dưới chân bảng nay chỉ còn hai con số kết quả: tổng giờ và ngày
chạm đích.

**Vì sao cộng trừ chứ không chỉ một ô gõ số.** Chỉnh mức học là việc dò vài nấc rồi dừng, không
phải việc biết trước con số. Ô gõ vẫn còn cho ai muốn nhảy thẳng từ 30 sang 12.

**Vì sao mốc là ngày cuối tháng, và vì sao tháng hiện tại phải có mặt.** Mốc cũ là ngày 1 hoặc 15
gần nhất SAU hôm nay, nên tháng đang sống không có cột: learner mở trang ngày 22.09 thì cột đầu
là tháng 10, và những giờ họ học trong tám ngày còn lại của tháng 9 không có chỗ nào để đứng.
Lấy ngày cuối tháng vì một mức tăng band là thứ đạt được SAU khi học hết tháng, không phải vào
một ngày giữa tháng. Migration 0273 dựng lại `learner_ielts_plan_cells` để `milestone_date` nhận
mọi ngày có thật (28, 29, 30, 31), và cổng phía API đổi theo: thứ còn bị chặn là ngày KHÔNG CÓ
THẬT như `2026-02-30`.

## 77. Learning Plan: hai thẻ gập lại (SRC-975, 22.09.2026)

Learner mở trang này để xem KẾ HOẠCH. Điểm hiện tại, điểm đích và ba nguyên tắc đều là thứ họ đã
biết, chỉ thỉnh thoảng mới cần. Để cả ba mở sẵn thì cái bảng - thứ họ đến để xem - bị đẩy xuống
dưới màn hình.

Thẻ gập ở đây khi gập vẫn nói ra một dòng NỘI DUNG (`6.5 → 8.5 overall · test 17.12.2027`), không
chỉ một cái tên. Một thẻ gập chỉ còn tên là thứ learner phải mở ra mới biết có đáng mở không, và
họ sẽ mở hết mọi thẻ đúng một lần rồi thôi. Trạng thái mở KHÔNG lưu: gập lại là để bảng nổi lên,
nên mỗi lần vào trang phải bắt đầu từ trạng thái ấy.

## 87. Monthly plan: sửa được số tháng, và hai dải số đọc được (SRC-984, 22.09.2026)

**Số tháng có một cái bẫy riêng.** Hiệu ứng "nối thêm cột cho đủ tới đích" (SRC-960) sẽ mọc lại
đúng cột learner vừa cắt, ngay trong lượt render kế tiếp - nút trừ trông như một cái nút không làm
gì cả. Nên nay có cờ `monthsPinned`: learner đã tự đặt số tháng thì hiệu ứng thôi can thiệp. Cắt
cột thì mức tăng của những tháng bị cắt **mất hẳn**, không giữ âm thầm ở đâu: một con số vô hình
vẫn cộng vào tổng giờ là thứ không ai gỡ ra được. Thêm thì thêm cột rỗng.

**Hai dải số.** Bản trước xếp chúng bằng `flex`, nên hai con số dồn về mép trái một dải rộng gần
hai nghìn pixel và bên phải là một khoảng trống không nói gì - đọc ra như một khối bị lệch chứ
không như hai con số ngang hàng. Nay mỗi dải là một lưới hai cột chia đôi đúng chiều rộng bảng, có
vạch dọc ngăn giữa, nhãn trên số dưới. Cùng một `Stat` cho cả bốn ô nên hai dải trên và dưới bảng
đọc như một cặp.

`Stepper` tách ra dùng chung cho cả số giờ lẫn số tháng: hai ô cạnh nhau mà một ô có nút còn ô kia
chỉ gõ được là thứ learner phải thử mới biết.

## 92. Chia sẻ kế hoạch ra một đường dẫn công khai (SRC-994, 23.09.2026)

**Cái được chia sẻ là một CÁI KHOÁ, không phải một bản sao.** Bảng `learner_ielts_plan_shares`
(migration 0282) chỉ nối một slug với một learner; trang công khai đọc kế hoạch ĐANG SỐNG. Learner
sửa bảng thì người cầm link thấy bản mới - đó đúng là điều họ muốn khi gửi cho bố mẹ hay thầy cô.
Một bản chụp đông cứng sẽ nói sai ngay lần sửa đầu tiên.

**MỘT learner MỘT slug.** Bấm Share lần thứ hai trả lại đúng đường dẫn cũ, vì link ấy có thể đã
nằm trong tin nhắn gửi cho ai đó. Ngược lại, ai đã thu hồi rồi bật lại thì nhận slug MỚI: làm sống
lại link cũ là phản bội chính hành động thu hồi.

**Trang công khai mang HỌ TÊN người chia sẻ** (chỉ đạo chủ dự án 23.09.2026, SRC-1003, đảo quyết
định "không tên" ban đầu của SRC-994). API trả `name` = `learners.display_name`, và trang hiện nó
dưới tiêu đề. Lý do: người nhận link cần biết đây là kế hoạch của ai, và learner đã tự bấm Share.
Vẫn KHÔNG trả id learner, không trả email - test route khoá đúng hai điều ấy.

**Slug ngẫu nhiên 12 ký tự**, bảng chữ bỏ các ký tự dễ đọc nhầm. Tên hay id learner trong URL là
mời người ta thử tên khác. Slug sai và slug đã thu hồi trả **cùng một 404**: phân biệt hai cảnh ấy
là nói cho người lạ biết đường dẫn này từng tồn tại.

**Đã trả nợ (SRC-1207, chỉ đạo chủ dự án 03.10.2026): có nút thu hồi, và chỉ CHÍNH learner được
chia sẻ.** Audit 03.10.2026 tìm ra rằng không chỗ nào ghi `revoked_at`, nên link lỡ lọt vào nhóm
chat bày họ tên và điểm mãi mãi; và vì POST chỉ đòi quyền xem learner, bố mẹ hay mentor cũng bật
được chia sẻ thay learner.

* `GET .../ielts-learning-plan/share` trả `{ slug, can_share }`: ai xem được kế hoạch thì đọc được
  trạng thái, `can_share` chỉ đúng khi phiên là chính learner (`selfLearnerId`).
* `POST` và `DELETE` cùng chặn mọi phiên không phải chính learner. `DELETE` ghi `revoked_at`; bật
  lại sau đó ra slug mới như luật trên.
* Màn hình đọc trạng thái khi mở trang: đã chia sẻ thì link hiện ngay kèm nút **Stop sharing**;
  bố mẹ và mentor mở trang thì không có nút nào. Chữ theo luật tiếng Anh đơn giản của trang này
  (SRC-1019).

Route đọc được khai vào `PUBLIC` của `authCoverage.test.ts` kèm lý do - cổng ấy là chỗ duy nhất
trong repo cho phép một route `/v1` không đi qua `requireSession`, và nó bắt phải viết ra vì sao.

## 93. Số tháng được lưu, và mặc định là 12 (SRC-996, 23.09.2026)

**Vì sao số tháng biến mất.** Phần lưu (SRC-960) chỉ ghi những ô có mức tăng lớn hơn 0 - đúng cho
nội dung, nhưng số cột thì không suy ra được từ chúng: một bảng 24 tháng mà nửa sau để trống trông
y hệt một bảng 12 tháng. Learner kéo bảng ra rồi mở lại thấy nó co về, không lỗi nào hiện ra, và
họ phải kéo lại mỗi lần vào. Số tháng là một QUYẾT ĐỊNH ("tôi định học trong hai năm"), nên nó
phải có chỗ để lưu: cột `months` trong `learner_ielts_plan` (migration 0283).

Cột cho phép `NULL`, và các hàng cũ giữ `NULL`: một giá trị mặc định nhét vào chúng sẽ CẮT NGẮN kế
hoạch của những learner đang có bảng dài hơn, mà một migration thì không được phép sửa nội dung kế
hoạch của ai. Khi `months` là `NULL`, màn hình dựng số cột như trước.

Khi đọc lại, **số cột đã lưu thắng số cột suy ra từ các ô**: phần trống ở đuôi bảng chính là kế
hoạch của learner, không phải chỗ thừa để cắt.

**Mặc định 12 tháng.** Trước đây số cột được TÍNH từ khoảng cách tới đích, nên hai learner cùng
mục tiêu mà lệch nhau nửa band lại mở ra hai bảng dài ngắn khác hẳn - một con số nhảy nhót mà
không ai hiểu vì đâu. Một năm là mốc người ta thật sự nghĩ bằng, và learner vẫn kéo ra 24 tháng
được.

`PUT` không gửi `months` thì server GIỮ NGUYÊN giá trị đang lưu (`COALESCE`), nên một client cũ
vẫn lưu được kế hoạch mà không xoá mất lựa chọn của learner. Gửi một giá trị sai thì báo lỗi chứ
không im lặng bỏ qua - im lặng ở đây là để learner tưởng đã lưu.

## 106. Learner mới đặt được mục tiêu ngay ở Learning Plan (SRC-1022, 24.09.2026)

Chủ dự án: *"Với một learner mới, họ vào trang Learning Plan, thì họ ko thể setup điểm hiện tại và
điểm mục tiêu được."* Hai thẻ sửa điểm chỉ nằm trong nhánh ĐÃ có mục tiêu; nhánh chưa có chỉ bày
một câu nhắc không có ô nào để điền. Nay nhánh ấy có cả hai thẻ, thẻ Target mở sẵn chế độ sửa
(`EditGate startEditing`) với 6.5 làm gợi ý; lưu xong trang sang nhánh có bảng kế hoạch.

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

### Từ mục 39. Learning plan: một lời hẹn, không phải một biểu mẫu (SRC-916, 2026-09-20)

Chủ dự án mở `/ielts/setup/plan` và viết: *"Bỏ hết các item trên màn hình này. Trên đây có thông
báo đại ý là 'Sau khi trao đổi với Mentors 1-1, lúc đó sẽ có Learning Plan chi tiết'."*

Ảnh chụp đầu tiên ra **trang cũ**: Playwright dùng lại server cổng 4173 còn sống của một phiên
khác, phục vụ một bản build khác. Cái test vừa viết cũng **xanh trên bản build ấy** — một kết quả
xanh vô nghĩa. Phải đọc thẳng bundle trên đĩa và dựng server riêng ở cổng khác mới kết luận được.

Đây đúng là cái bẫy CLAUDE.md đã ghi cho `check:docs`, chỉ khác bề mặt: **cổng chạy local nói dối
theo hướng dễ dãi** khi nhiều phiên dùng chung một máy.

### Từ mục 60. Plan Board: bốn kỹ năng × các tháng, và số giờ phải bỏ ra (SRC-952, 22.09.2026)

Chỉ đạo chủ dự án 22.09.2026:

> *"Trang đó ghi 4 skill trên một table, cột 1 là danh sách các skill, mỗi cột sau là một mốc ngày
> làm tròn, là ngày 1 hoặc 15 hàng tháng. (…) mỗi cột sẽ chứa 2 skills mà tăng 0.5 (…) Learner có
> thể tự chỉnh số điểm tăng của từng tháng. (…) Mỗi 0.1 điểm của một trong 4 skills, thì cần
> khoảng 3 giờ để học. Dòng cuối cùng là tổng thời gian mỗi tháng (…) cột cuối là tổng thời gian
> bỏ ra để tăng mỗi skill."* Và sau khi xem prototype: *"Tooltip hiển thị tiếng Anh nhé. (…) Cần
> có link từ màn hình home của ielts để tới trang Plan Board."*

Vòng này chỉ có màn hình: sửa thì bảng tính lại ngay, đóng trang là mất. Có một dòng *"Draft view
— changes are not saved yet"* ngay trên bảng. Một trang lặng lẽ không lưu là một trang nói dối, và
learner sẽ dựng lịch học theo một bản kế hoạch không tồn tại ở đâu cả.

### Từ mục 66. Learning Plan: đọc trước, sửa sau, và một cái thước giờ (SRC-958, 22.09.2026)

> *"Khi mới vào thì cần giảm tối thiểu các element, và mọi thứ đều là read-only (…) Bấm vào Edit
> thì luôn có 2 button Save và Cancel xuất hiện ngay ở chỗ button Edit (…) Dòng 1 trong table, cần
> bỏ ngày đi (…) Có một cụm settings ở dưới cùng (…) 'thời gian học mỗi tháng: 30 hours' (…) Hệ
> thống sẽ so sánh các con số ở dòng dưới cùng trong bảng với con số 'giờ học tiêu chuẩn mỗi
> tháng', để nó bôi màu (…) bổ sung luôn 2 cards là 'điểm hiện tại' và 'điểm mục tiêu'."*
> Và: *"Đổi tên trang từ 'Plan Board' thành 'Learning Plan'."*

Bảng vẫn **chưa lưu lên server** (back-end làm sau, đúng ý chủ dự án), nên nút Save hiện chỉ đóng
chế độ sửa. Chỗ nối `useRegisterSave` đã sẵn cho ngày có API.

### Từ mục 67. Nút xếp hộ: nhịp khởi động bốn kỹ năng (SRC-959, 22.09.2026)

> *"Có một button nào đó, click vào thì hệ thống sẽ tự tính toán và sắp xếp giúp. Đại ý là: Trong
> tháng đầu tiên sẽ khởi động với Reading và Listening, mỗi thứ một chút. Tháng thứ 2 gia tăng
> thêm. Từ tháng thứ 3 thì thêm Writing. Từ tháng thứ 4 thì thêm Speaking."*

### Từ mục 68. Learning Plan lưu xuống CSDL (SRC-960, 22.09.2026)

Chỉ đạo: *"Tìm cách lưu đi. Còn bảng có đúng hay không, thì tôi không rõ, bạn tự kiểm tra giúp."*

### Từ mục 72. Learning Plan: hai thẻ sửa được, và một ranh giới (SRC-967, 22.09.2026)

> *"Bỏ link 'IELTS' ở ngay trên cùng. Bổ sung 2 button Edit vào Current và Target. Cần có đường kẻ
> và khoảng cách để ngăn cách 2 card bên trên với cái Learning Plan chi tiết ở bên dưới. Chuyển
> button Edit hiện tại xuống dưới (…) ngay bên trên Learning Plan table. Bỏ label 'Months above
> this number…'. Trình bày lại thông tin của cụm dưới cùng."*

### Từ mục 73. Learning Plan: chiều cao đứng yên, và ba nguyên tắc (SRC-970, 22.09.2026)

> *"Trong 2 cards trên cùng, cho dù click vào Edit hay Read Only, thì độ cao của 2 card đều không
> được thay đổi. READING cần đổi chỗ cho WRITING. Trong card 1, cần sửa được ngày. Bỏ dòng 'Starts
> with Reading and Listening…'. Bổ sung một card ở dưới cùng, kiểu như 'nguyên tắc để tạo kế
> hoạch' (…) cần dùng tiếng Anh hết, nhưng ở dạng cực kỳ đơn giản để trẻ con lớp 5 cũng hiểu."*

### Từ mục 76. Monthly plan: đầu bài lên trên, và mốc là cuối tháng (SRC-974, 22.09.2026)

Chỉ đạo chủ dự án: đưa **Study time per month** và **Months** lên **trên** bảng, số giờ chỉnh
bằng nút cộng trừ; bảng phải chứa **cả tháng hiện tại**; **Target reached** là **ngày cuối tháng**.

### Từ mục 77. Learning Plan: hai thẻ gập lại (SRC-975, 22.09.2026)

Chỉ đạo chủ dự án: thẻ "How to build a plan that works" có header bấm vào mới mở, mặc định gập;
hai thẻ **Current** và **Target** nằm trong một thẻ bao, cũng gập sẵn, khi gập chỉ hiện vài thông
tin quan trọng nhất.

### Từ mục 87. Monthly plan: sửa được số tháng, và hai dải số đọc được (SRC-984, 22.09.2026)

Chỉ đạo: sửa được **Months**; trình bày lại hai dải số vì *"các thông tin trên đều bị lệch hẳn về
bên trái"*; và chữ **SKILL** ở cột một viết hoa.

### Từ mục 92. Chia sẻ kế hoạch ra một đường dẫn công khai (SRC-994, 23.09.2026)

Chỉ đạo: *"Tạo button để Learner có thể 'Share my plan'. Mọi người có thể vào xem từ trang nào đó
trên www.nemo12.com/ielts/learning-plans/slug"*.

`revoked_at` có mặt ngay từ migration đầu tiên dù màn hình chưa có nút tắt: một đường dẫn công
khai không có đường thu hồi là thứ không nên dựng. Nút tắt là việc còn nợ, ghi ở đây.

### Từ mục 93. Số tháng được lưu, và mặc định là 12 (SRC-996, 23.09.2026)

Chủ dự án: *"Chưa thấy hệ thống cho phép lưu lại thông tin. Mặc định là 12 tháng để đặt plan
nhé."*
