SDD-038 · Đăng nhập, quyền và ngôn ngữ
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.
8. Cả sân IELTS chỉ còn tiếng Anh, và dashboard nói về công sức (SRC-834, SRC-835, 2026-09-18)
8a. Bỏ công tắc VI/EN
Mọi trang trong sân IELTS — cả /ielts lẫn /ielts/reading, /ielts/speaking/parts/... — nay chỉ hiển thị tiếng Anh. Lý do nằm ở chính môn học: thứ learner cần tập là đọc thẳng tiếng Anh, và một nút đổi sang tiếng Việt ngay trên màn hình là lời mời bỏ việc ấy — lời mời luôn được nhận.
Đây là ngoại lệ có phạm vi, không phải đổi luật ngôn ngữ của repo: UI tiếng Việt vẫn là mặc định của Nemo12 (CLAUDE.md, SRC-604); ngoại lệ chỉ dành cho sân IELTS.
Ranh giới: tên và nhãn, không phải thân bài dạy học (ghi lại khi rà SRC-931). Câu "chuyển hết thành tiếng Anh" áp cho thứ learner ĐỌC ĐỂ ĐIỀU HƯỚNG: tiêu đề, nhãn nút, tên phần thi, tên năng lực, tên bậc. Phần THÂN BÀI DẠY thì theo thang bốn chặng §5f và vẫn bắt đầu bằng tiếng Việt — note_vi / measures_vi / tell_vi của trang phần thi, why của năm bậc, và toàn bộ cổng đọc hiểu trong microCheckpoints.ts. Lý do là cùng một lý do đã dựng nên thang bốn chặng: thao tác tư duy gặp lần đầu bằng thứ tiếng learner đọc trôi chảy thì lúc sai, họ biết mình sai vì cách nghĩ chứ không phải vì thiếu một chữ. Một trang phần thi viết trọn bằng tiếng Anh sẽ dạy đúng cái nó định dạy cho người đã ở band 6.5, và không dạy được gì cho người ở band 4.5.
Ba điều đi kèm:
- Hạ tầng song ngữ ở lại (
pick,ttrongapps/learn/src/ielts/lang.tsx): dữ liệu nội dung vẫn mang hai trường*_vi/*_envà chặng dịch còn đang lấp dần bản tiếng Anh.pickchính là chỗ rơi về bản còn lại khi bản kia trống — bỏ nó đi thì mỗi ô chưa dịch xong thành một chỗ trống không hiểu vì sao trống. - Tên lĩnh vực (domain) tiếng Anh cả lúc chọn lẫn lúc hiển thị trên chip ở header.
- Thanh trên của
/ieltsmang tênNEMO IELTS, không phảiNemo12: mọi trang con của sân này đều mang chữ IELTS trên vệt đường dẫn, nên một thanh trên gọi tên khác là chỗ duy nhất trong cả luồng nói sai nơi learner đang đứng. - Nhánh gốc "IELTS" của vệt đường dẫn trỏ về
/ielts(hub bốn kỹ năng), không còn trỏ/ielts/courses(danh sách 12 khoá) — một nhánh gốc mang tên kỹ năng mà dẫn sang chỗ đọc thiết kế chương trình là một cái bẫy im lặng. - Chân trang bỏ khẩu hiệu mặc định "Học cho học sinh Việt Nam" ở cả ba bản sao
SiteFooter: chân trang đã nói tên và năm, một khẩu hiệu lặp ở đáy mọi trang chỉ thêm một dòng không ai đọc lần thứ hai. Cổng nào có câu riêng thì vẫn truyềntagline(Marlins).
8b. Trang kỹ năng: ba card, mỗi card một câu hỏi
| Card | Hình | Nguồn |
|---|---|---|
| Am I showing up? | bảy ô vuông của bảy ngày qua | progress.active_days, streak |
| Is my vocabulary growing? | đường vốn từ cộng dồn 14 ngày | progress.lexis_by_day |
| Are my answers right? | hai thanh: tuần này và tuần trước | progress.accuracy |
Dùng chung cho cả bốn kỹ năng (SkillStatusCard là chỗ duy nhất bốn trang gọi tới). Chưa có lịch sử thì không vẽ gì — ba thẻ toàn số 0 là bản kê những việc learner chưa làm, bày ra đúng lúc nó chưa kịp làm gì.
8c. /ielts: bốn mini-card ghi nhận, và ngày đủ 200 giờ
Đường dữ liệu: GET /v1/learners/{id}/ielts-effort (workers/api/src/modules/ieltsSkills/effort.ts), gộp mọi kỹ năng vì đích 200 giờ là đích chung của cả hành trình; tổng giờ đi bằng một SUM riêng chứ không cắt theo cửa sổ 30 ngày (vốn học không tự mất đi vì tuần này nghỉ).
- Tiêu chuẩn ngày: 60 phút (
DAILY_STANDARD_MINUTES). Một giờ mỗi ngày đưa learner tới 200 giờ trong khoảng hơn nửa năm — đúng độ dài một mùa luyện thi, nên tiêu chuẩn và đích đến kể cùng một câu chuyện. Đây là quyết định sản phẩm, đổi nó là đổi lời hứa với phụ huynh. - Bốn mini-card: Today (phút hôm nay so tiêu chuẩn) · Last 7 days · Last 30 days · 200 hours (tổng giờ và phần trăm).
- Một thẻ tiên đoán riêng: ngày đủ 200 giờ theo nhịp 7 ngày và theo nhịp 30 ngày, hai con số riêng chứ không trộn thành một nhịp trung bình — chính khoảng cách giữa hai ngày ấy mới nói được điều gì đó.
- Nhịp bằng 0, hoặc ngày đích xa hơn 10 năm, thì IM LẶNG (
reaches_target_on: null). Chia cho 0 ra vô cực, và "con sẽ đủ 200 giờ vào năm 2189" là một câu vừa đúng về số học vừa tàn nhẫn. - Chưa có lịch sử thì không bày dashboard nào. Bốn lối vào kỹ năng vẫn đủ để bắt đầu.
- Bốn link kỹ năng gộp vào một card để chúng đọc ra là một khối riêng, không phải phần đuôi của dashboard phía trên.
Ngày tháng in theo DD.MM.YYYY (SRC-748), kể cả trên các trang chỉ còn tiếng Anh.
26. Sân luyện đứng sau đăng nhập (SRC-883, 2026-09-19)
Mọi URL dưới /ielts/** đòi tài khoản trước khi vẽ bất cứ thứ gì.
Cổng nằm ở ĐÚNG MỘT CHỖ, apps/learn/src/ielts/SignInGate.tsx, bọc ngoài App tại điểm mount Practice.tsx. Không phải mỗi trang tự gọi: mạch này có hơn hai chục trang, và một luật "trang nào cũng phải nhớ gác" thì chỗ quên được sẽ đúng là chỗ không ai mở ra xem.
Ba trạng thái, và "đang hỏi" KHÔNG phải "chưa đăng nhập": khi /v1/me chưa trả lời, màn hình là một dòng chờ trên nền tối chứ không phải màn hình mời đăng nhập. Vẽ "Sign in" nửa giây rồi để nó biến mất trước mắt một người ĐÃ đăng nhập đọc ra như một lần bị đá khỏi phiên.
Cổng riêng, không dùng me của trang cha. Cổng của App.tsx còn kèm cổng ra mắt và màn hình Join; đi qua nó nghĩa là một learner hợp lệ có thể bị giữ lại vì một lý do chẳng liên quan gì tới việc luyện đề. Hai lượt gọi /v1/me là cái giá, cùng cookie cùng phiên nên lượt thứ hai gần như luôn trúng cache.
Chữ trên cổng viết tiếng Anh (SRC-834/835): đây là màn hình ĐẦU TIÊN của sân luyện tiếng Anh. Nút Google cũng vậy — nó chỉ chịu đổi ngôn ngữ khi script GIS được tải kèm ?hl=en, vì tuỳ chọn locale truyền lúc renderButton không kéo lại được ngôn ngữ nó đã chọn theo trình duyệt lúc tải. GoogleButton vì thế nhận thêm prop locale, mặc định vi cho mọi chỗ gọi cũ.
Hệ quả phải biết: bốn endpoint modules/ieltsContent ở §17 vẫn public ở tầng API — cổng này là cổng GIAO DIỆN. Và 140 trang luyện nay không còn được máy tìm kiếm đọc; đó là cái đánh đổi của chỉ đạo này, không phải một tác dụng phụ bị bỏ sót.
27. Khoá luôn API, không chỉ giao diện (SRC-885, 2026-09-20)
Đúng chỗ phải khoá. Một cổng chỉ ở giao diện thì curl https://api.nemo12.com/v1/public/ielts/... vẫn kéo về trọn 140 trang — tức là cái khoá chỉ khoá người dùng thật, còn ai biết gõ lệnh thì không. Cổng nào cũng vậy: nó chỉ có giá trị bằng lớp yếu nhất của nó.
Mười đường rời danh sách công khai, cả mười nay nằm dưới requireSession:
| Nhóm | Đường | Mã |
|---|---|---|
| Nội dung luyện | /v1/public/ielts/content/* (cụm, một cụm, khởi động, kho từ) | ieltsContent/routes.ts |
| Lý thuyết năng lực nhỏ | /v1/public/ielts/micro-skills, …/{skill}/{code}, …/levels/{level}/{seq} | microSkills/routes.ts |
| Giọng đọc | …/reading/{cluster}/{seq}/audio, …/listening/…, …/micro-items/{id}/audio | ieltsPractice/routes.ts |
Mỗi nhóm khoá bằng MỘT dòng use(prefix, requireSession) chứ không phải mười lần gắn lẻ: đường thứ mười một thêm vào mai sau được chặn sẵn, và quên gắn là đúng kiểu sai sót mà authCoverage.test.ts sinh ra để bắt. Mười dòng tương ứng đã xoá khỏi danh sách PUBLIC của cổng ấy — danh sách đó là nơi duy nhất nói route nào cố ý mở, nên để lại một dòng thừa là để lại một lời khai sai.
Ba cái bẫy đã gỡ, cả ba đều hỏng im lặng nếu bỏ qua:
fetchkhông tự gửi cookie qua tên miền khác. App ởlearn.nemo12.com, API ởapi.nemo12.com.data/store.tsvà ba lời gọi micro-skills trongapi.tsnay nóicredentials: "include"; thiếu nó thì mọi trang nội dung trả 401 và learner thấy "nội dung hỏng" chứ không thấy "bạn chưa đăng nhập".- Thẻ media còn không gửi cookie nữa.
<audio src>mặc định gửi request ẩn danh, và không có cách nào đặt header cho nó. Hai chỗ phát tiếng nay khaicrossOrigin="use-credentials"(CORS của worker đã trảAccess-Control-Allow-Credentials: truecho*.nemo12.com, nên không phải đổi gì ở tầng đó). Quên dòng này thì hỏng ra thành "bấm nghe không kêu". Cache-Control: publictrên một route đã đòi phiên là mâu thuẫn. Một bản nằm trong cache dùng chung là một bản phát lại được cho người chưa đăng nhập. Đổi thànhprivate, max-age=300— vẫn 300 giây, nhưng trong trình duyệt của từng người.
Tên đường giữ nguyên chữ public, và đó là cố ý: đổi đường là làm gãy mọi bundle app còn nằm trong cache trình duyệt của learner đang mở dở một bài. Chữ ấy nay đọc là "nội dung đã công bố", không phải "ai cũng gọi được".
Cổng kiểm: ba file test mới/ mở rộng gọi bằng request THẬT không mang cookie — ieltsContent/routes.test.ts, microSkills/routes.test.ts, ieltsPractice/audioAuth.test.ts. Chúng gác thêm hai điều mà một phép kiểm mã nguồn không thấy được: 401 không mang theo một mẩu nội dung nào, và địa chỉ sai cũng trả 401 chứ không trả 404 — 404 cho địa chỉ sai trong khi 401 cho địa chỉ đúng là tự kể ra cụm nào có thật, tức là dò được cả danh mục mà không cần tài khoản.
Còn mở, có chủ ý: /v1/public/ielts/catalog (danh mục 12 khoá) và /v1/public/band-pricing. Đó là thứ phụ huynh đọc trên nemo12.com trước khi tạo tài khoản, không phải thứ learner làm bài trong sân luyện — cùng ranh giới mà CLAUDE.md đã vạch giữa trang công khai và sân luyện.
Vá cùng ngày: một khoá theo TIỀN TỐ chỉ giữ được thứ mọc trong tiền tố ấy
Lời hứa ở §27 vì thế phải đọc chính xác hơn: một khoá theo tiền tố chặn sẵn đường mới nằm trong tiền tố, không phải mọi đường mới của module. Đó là một lời hứa hẹp hơn nhiều, và chỗ chênh giữa hai cách đọc chính là chỗ lỗi này chui qua.
Vá (commit 1472915f): gắn thẳng ieltsContentRouter.use("/v1/public/ielts/exam-papers", requireSession). Không nới tiền tố thành /v1/public/ielts/*: index.ts mount mọi router ở /, nên một wildcard rộng như thế khi bị làm phẳng sẽ chụp luôn /v1/public/ielts/catalog của curriculum/routes.ts — đúng cái đường mà đoạn trên vừa nói là cố ý mở cho phụ huynh. Chặn đúng thứ cần chặn, không chặn kèm thứ cố ý mở.
Không gãy gì ở phía trình duyệt: người gọi duy nhất là apps/learn/src/ielts/data/store.ts, và hàm fetch của nó đã mang credentials: "include" cho mọi đường nội dung từ SRC-885.
Ai bắt được, và ai đáng lẽ phải bắt. authCoverage.test.ts bắt được ngay lượt CI đầu tiên — cổng ấy đứng ở tầng bảng route, nên nó thấy mọi đường mới bất kể tiền tố. Còn ieltsContent/routes.test.ts thì không: bộ ca của nó cho exam-papers gọi kèm cookie ở mọi lượt, nên cụm "không có phiên" chưa từng chạm tới đường này. Nay đã có ca 401, và thêm một ca gác rằng 401 ấy không mang theo tên đề nào — cùng lý lẽ chống dò danh mục đã dùng cho bài đọc: một 401 có kèm "Writing paper 1" vẫn nói cho người chưa đăng nhập biết có bao nhiêu đề và tên chúng là gì.
28. Một cổng cho cả sân, và tên learner không còn là nút thoát (SRC-886, 2026-09-20)
Nghe như hai lỗi rời nhau. Thật ra chúng cách nhau đúng một cú bấm: bấm vào tên mình thì mất phiên, rồi bấm vào nhãn IELTS để quay lại thì đứng trước một sản phẩm khác không có đường về. Cả hai đều là chuyện của thanh trên, nên §28 sửa thanh trên chứ không sửa hai chỗ.
a. Tên learner là cái cò thoát phiên
AccountChip trong apps/learn/src/ielts/Shell.tsx là một <button> mà toàn bộ nội dung nhìn thấy được là tên người đang đăng nhập, còn onClick duy nhất của nó gọi thẳng signOut(). Tức là thứ đọc ra như một cái nhãn ("đây là bạn") lại chính là hành động phá nhiều nhất màn hình, và không có bước xác nhận nào ở giữa. Lời cảnh báo duy nhất là title="Sign out" — thứ không bao giờ hiện ra trên màn cảm ứng, đúng nơi một cái chip hình viên thuốc nằm sát chip "Choose a field" dễ bị chạm nhầm nhất.
Nay chip ấy là đúng AvatarMenu mà /ielts đang dùng, không phải một menu thứ hai giống giống: hai nửa của cùng một sân mở ra cùng một menu, và "Sign out" là một mục phải chọn chứ không phải thứ xảy ra khi chạm hụt. Nhánh "chưa đăng nhập" cũ của chip (một chữ "Đăng nhập" mở ra nút Google tại chỗ) bị xoá luôn: từ §26, SignInGate bọc ngoài cả mạch nên nhánh ấy không còn đường nào chạy tới, và một nhánh không ai kiểm được nữa là một nhánh sẽ mục.
Tên vì thế nằm trong AvatarMenu chứ không riêng sân luyện: cùng một menu ở sáu chỗ mà chỗ có tên chỗ không là đúng kiểu lệch mà §28 vừa dọn. Hai ràng buộc đi kèm, cả hai vì chỗ chật:
hidden sm:block— thanh trên của TRANG CHỦ learn mang sẵn năm nút (Lighthouse, Lịch, Khoá học, Rủ bạn, VI/EN) và không ẩn bớt cái nào ở màn hẹp. Thêm một chuỗi tên vào đó ở 375px là đẩy cả hàng xuống dòng. Tên là thứ thêm vào khi có chỗ; avatar thì ở mọi bề ngang, nên không màn nào mất đường vào menu.max-w-28 truncate— "Nguyễn Tất Đắc" thì vừa, nhưng một tên dài sẽ bóp vệt đường dẫn bên trái cho tới lúc nó vắt dòng.
b. Nhãn IELTS dẫn ra khỏi sân IELTS
Nhánh gốc của vệt đường dẫn ở mọi trang luyện trỏ về /ielts. Nhưng /ielts TRẦN không thuộc mạch luyện: pathToRoute đưa nó về { view: "ielts" } của App.tsx, tức là ra ngoàiIeltsPracticeApp và rơi vào ba cổng của trang cha. Với người chưa/hết đăng nhập, cổng thứ ba là màn Join: "Nemos · Cổng học tập", tiếng Việt, logo Nemo12, và lối ra duy nhất là một link sang nemo12.com. Cùng một sân IELTS mà hai nửa chặn bằng hai màn hình trông khác hẳn nhau — đó là cái "loạn xạ" trong ảnh 2.
Nay /ielts đi qua cùng một cổng với các trang luyện. SignInGate.tsx xuất ra hai màn dùng lại được (IeltsLoadingScreen, IeltsSignInScreen), và App.tsx gọi chúng ngay sau nhánh ieltsPractice, tức là trước cổng ra mắt và màn Join — đúng cái lý lẽ đã đưa ieltsPractice lên trước chúng ở §26. Dùng lại chứ không chép: một bản "gần giống" ở App.tsx sẽ trôi khỏi bản gốc trong vài tuần, và lúc ấy lại có hai màn đăng nhập như cũ.
Hai thứ nhỏ đi kèm, cả hai đều là "bấm xong không có gì xảy ra":
- Cổng có lối ra. Màn
Joinmà nó thay chỗ vốn có "← Về Nemo12"; cổng mới không được nghèo hơn nên có "← Back to Nemo12". Một cổng bắt buộc mà không có đường nào khác là một ngõ cụt. - Đăng nhập hỏng không còn im lặng. Chỗ gọi cũ là
void signIn(cred): lỗi bị nuốt thành một unhandled rejection, nút Google chớp một cái rồi đúng màn hình ấy ở lại — lại là "bấm vào thì lại hiện ra chính nó".IeltsSignInScreennay giữ một dòng lỗi.
c. Vệt đường dẫn trỏ về chính nó, và trên điện thoại nó là thứ duy nhất bấm được
Shell vẽ mọi nhánh, kể cả nhánh cuối, bằng <Link> — mà nhánh cuối gần như luôn là trang đang mở. Bấm vào nó thì pushState đúng địa chỉ đang đứng và màn hình cũ hiện lại. Trên màn hẹp thì tệ hơn hẳn: luật cũ ẩn mọi nhánh trừ nhánh cuối, nên thứ duy nhất bấm được trên điện thoại là cái link trỏ về chính nó, còn "NEMO IELTS" — nhánh duy nhất thật sự đi đâu đó — không có trên màn để mà bấm. Đó là câu "click vào thì lại xuất hiện chính nó", đọc theo đúng nghĩa đen.
Hai sửa:
- Nhánh trỏ đúng chỗ đang đứng thì không phải link (
aria-current="page"). So bằng đường dẫn, không bằng "nhánh cuối cùng": vài trang truyền vệt của trang CHA lúc đang tải hoặc khi nội dung hỏng (Reading.tsx), và ở đó nhánh cuối là một lối đi thật. - Màn hẹp ẩn các nhánh GIỮA, giữ lại gốc. Hai chữ "NEMO IELTS" không tốn một dòng; một header không có đường về nhà mới là thứ đắt.
Còn mở, có chủ ý
- Đã đăng nhập nhưng chưa có hồ sơ learner (
me.learner == nullhoặcstatus !== "active") thì/ieltsvẫn rơi xuống mànJoin. Ở đó Join không là màn đăng nhập mà là chỗ nhập mã mời / mở hồ sơ học thử — một việc thật sự phải làm, không phải một cái cổng đặt nhầm chỗ. Đổi nó là đổi luồng onboarding, nên để chủ dự án quyết. - ĐÃ CHỐT 2026-09-20 (SRC-909): nhãn "NEMO IELTS" ở hub cũng dẫn về
/ielts. Nay một cái tên, một cái đích, ở mọi bề mặt của sân. Khi ĐANG đứng ở/ielts(không córoute.task) nó không còn là nút mà là chữ mangaria-current="page"— cùng luật đã áp cho vệt đường dẫn ở §28c, vì một nhãn bấm vào lại ra đúng trang đang mở là một lời mời giả. Lý lẽ "một trang không có đường ra là một cái bẫy" vẫn đúng và vẫn được giữ: đường ra nay nằm ở menu avatar ("Hồ sơ của con"), không nằm ở logo. - ĐÃ ĐÓNG 2026-09-21 (SRC-937): phiên chết giữa buổi nay được nhận ra. Nay
ielts/api.tsphát sự kiện ấy khi gặp đúng một 401 "chưa có phiên", chép nguyên luật hẹp củaapps/learn/src/api.ts(Audit #013 U-5): một 401 vì THIẾU QUYỀN cho một việc không tính, vì phiên còn sống và đá learner về màn đăng nhập ở đó là mất bài đang làm dở.auth.tsxnghe và đặtme = null, nênSignInGatehiện "Sign in to practise" — URL không đổi, đăng nhập xong là về đúng trang đang dở. Gác hai nửa: luật ởielts/api.test.ts, phản ứng của màn hình ởe2e/ieltsGatedReading.spec.ts.
30. Cổng vai người đọc: quyền đúng, người đọc sai (SRC-897, 2026-09-20)
a. Lỗi ấy, và vì sao nó im lặng
/ielts-recordings trả thẳng JSON.parse(ai_notes_json). Cột ấy chứa hai hình dạng: bài của thang bậc là {praise_vi, did_vi, one_step_vi, band}, cả bốn viết cho learner; bài chẩn đoán (SRC-792) là {strengths_vi, gaps_vi, teaser_vi, ask_in_session_vi}, và chỉ teaser_vi dành cho learner — IELTS_PRODUCTION_FEEDBACK_V1 nói thẳng ask_in_session_vi là "một câu thầy cô nên hỏi học sinh trong buổi gặp".
Trên màn hình nó hiện ra thành hai tiêu đề rỗng, nên trông như lỗi CSS. Trong body thì gaps_vi và ask_in_session_vi nằm nguyên.
Đây là một LOẠI lỗi, không phải một lỗi lẻ. Lỗi phân quyền thì kêu: 401, 403, hoặc dữ liệu của người lạ mà người dùng nhận ra ngay. Lỗi sai vai thì im — dữ liệu đúng người, chỉ sai người đọc. Người review đọc notes: JSON.parse(row.ai_notes_json) cũng không thấy gì sai, vì phải biết trước rằng cột ấy trộn vai mới thấy.
b. Vì sao authCoverage không bắt được, và không nên bắt
authCoverage.test.ts (SRC-887) hỏi route này có đi qua đăng nhập không. Câu ấy đúng và đã bắt được SRC-888 để hở đường đề thi. Nhưng ở đây route đi qua đăng nhập đầy đủ, người gọi là đúng learner, đúng phiên, đúng gia đình. Hai cổng hỏi hai câu khác nhau và cả hai đều cần.
c. Bất biến được gác, và nó vốn đã đúng sẵn
scripts/check-audience.mjs viết bất biến ấy thành lời và bắt máy giữ, bốn luật:
- File đọc cột trộn vai phải khai trong
READERS, kèm vai của người cuối và tên hàm chiếu. - File phục vụ learner phải gọi hàm chiếu đã khai, và không
JSON.parsethẳng cột ấy. - File phục vụ learner không
SELECTcột chỉ-mentor (mentor_note,mentor_band). READERSkhông có dòng thừa.
Vai khai bằng tay, không suy từ đường dẫn: report.ts không mang route nào — nó dựng báo cáo cho modules/mentor/routes.ts — nên đoán vai của nó từ tên thư mục là đúng kiểu suy luận sẽ sai vào ngày có người dùng lại nó.
d. Cổng này KHÔNG làm gì
Nó không đọc được bên trong hàm chiếu. learnerNotes mà một ngày trả luôn gaps_vi thì cổng vẫn xanh, vì với nó có hàm chiếu là đủ. Phần ấy do test giữ: speakingTurn.test.ts kiểm trên chuỗi JSON trả về chứ không kiểm từng khoá, nên một trường mentor mới thêm vào cũng bị bắt.
Hai lớp bù nhau: cổng bắt người quên chiếu, test bắt hàm chiếu chiếu sót. Không lớp nào một mình đủ, và nói rõ chỗ hở còn hơn để người sau tưởng cổng lo hết.
AI ĐƯỢC XEM, và đó là một câu hỏi khác với "phạm vi mở ra cái gì" (SRC-938, từ Audit #015 D-7). Bản đầu chỉ đòi requireSession. Nhưng "đã đăng nhập" không phải là "ở trong chương trình": tài khoản thì ai cũng tự tạo được trong ba mươi giây, nên một cái cửa như thế rộng hơn mức bất kỳ ai đã chủ đích mở — và thứ nằm sau cửa là tên thật và ảnh thật của trẻ con.
Vào được: learner đang học (active/paused, đúng bằng tập người XUẤT HIỆN trên bảng — hai tập lệch nhau là một cách âm thầm để một tài khoản cũ giữ mãi quyền nhìn danh sách lớp) và người của nhà trường (mentor/staff/admin).
guardian KHÔNG nằm trong danh sách, và đó là một lựa chọn chứ không phải bỏ sót. Quyền lợi chính đáng của phụ huynh là thứ hạng con mình, mà ví và báo cáo đã phục vụ; bày danh sách con nhà khác cho họ vẫn là cùng một sự phơi bày, chỉ qua một cái cửa nhỏ hơn. Trang này cũng nằm trong sân luyện tiếng Anh dành cho learner, không phải bề mặt của phụ huynh.
Chỉ đạo SRC-901 được giữ nguyên vẹn: "Mọi cá nhân đều biết thứ tự của chính mình, và thứ tự của những người khác." "Những người khác" ở đó là bạn cùng học. Thắt lại chỗ này không lấy đi của learner một thứ gì họ đang có.
Vì sao chọn hẹp khi còn phân vân: hẹp thì đảo lại được bằng một chuỗi thêm vào mảng vai, còn rộng thì không — một cái tên đã bị nhìn thấy là đã bị nhìn thấy. Ngày chủ dự án muốn mở cho phụ huynh thì đó là một dòng sửa.
Câu từ chối viết tiếng Anh và nói rõ lý do, vì nó hiện trên màn learner trong sân luyện tiếng Anh (SRC-834/835). Trang cũng phân biệt "bị từ chối" với "hỏng tạm thời": bảo một người vĩnh viễn không có quyền rằng hãy "thử lại sau một lát" là mời họ bấm lại vô ích.
e. Còn mở
- Chỉ
ielts_productionsđược khai. Các chỗ khác có thể trộn vai (showcase, thư gửi phụ huynh) chưa rà; thêm một cột vàoMIXEDlà cách mở rộng, và chi phí là mọi chỗ đọc phải khai vai. - Cổng đọc bằng biểu thức chính quy trên mã nguồn, không phải cây cú pháp. Nó bỏ chú thích trước khi quét, nhưng một câu SQL ghép từ nhiều biến thì nó không nhìn thấy tên cột.
105. Membership nói tiếng Anh đơn giản (SRC-1021, 24.09.2026)
Cùng luật với §104. Hai đổi đáng chú ý: tên đợt giảm và điều kiện trong bảng giá là tiếng Việt (dùng chung với trang công khai), nên trang dựng câu từ con số ("Special price: save X", "Only until DD.MM.YYYY") thay vì bày chữ gốc; và "balance/tuition/transfer" đổi thành "money/price/send money".
108. Learning Plan nói tiếng Anh đơn giản (SRC-1028, 24.09.2026)
Cùng luật §104. Trang vốn đã tiếng Anh; việc làm là hạ độ khó từ ngữ: "Where you are, where you are going" thành "Your score now and your goal", "Target reached" thành "You reach your goal", "milestone" thành "Check point", ba luật lập kế hoạch viết lại bằng câu ngắn.
168. Trang bốn kỹ năng, Full tests và Grammar nói tiếng Anh 100% (SRC-1325, 10.10.2026)
Chủ dự án: "In the pages of all 4 skills, nothing may use Vietnamese. Everything must be English." Ảnh chụp kèm theo là trang Full tests với đoạn giới thiệu tiếng Việt.
- Phạm vi:
/ielts/reading,/ielts/listening,/ielts/writing,/ielts/speakingvà mọi trang dưới chúng (cụm, bài đọc, clip, phần thi, đề luyện, phòng thi, level Nói/Viết,/ielts/speaking/pair/**),/ielts/skills/**,/ielts/tasks/**,/ielts/tests/**(cả phiếu kết quả),/ielts/grammar/**. - Thay SRC-953 cho đúng những trang này: giới thiệu, dặn dò, giới hạn của phép đo, tóm tắt kết quả, nhãn nút, dòng lỗi, nội dung nút
?(helpContent.ts), aria-label đều tiếng Anh đơn giản. Phiếu kết quả Full test do API sinh (workers/api/src/modules/ieltsFullTests) cũng tiếng Anh. - Nội dung chỉ có tiếng Việt trong dữ liệu (ví dụ
note_vicủa cụm bài đọc/nghe,title_vicủa chủ đề ngữ pháp) thì không hiện trên các trang này, không dịch bừa nội dung dài. - Các trang hành trình (
/ielts,/ielts/today,/ielts/diagnostic,/ielts/progress...) vẫn theo SRC-953. Hai thành phần dùng chung (LoadFailedLine, nút?) chọn thứ tiếng theo đường dẫn quaisEnglishOnlyPage()tronghelpContent.ts. - Nhận xét AI cũng tiếng Anh:
ielts.ladder-feedback@2(nhận xét bài luyện Viết/Nói theo bậc, cả lượt nói theo năng lực ở trang micro-skill) vàielts.full-test-comment@2(nhận xét chung của phiếu Full test) viết tiếng Anh đơn giản; tên ô*_vigiữ nguyên để dữ liệu cũ không vỡ. Khuôn dự phòngtemplateCommentcũng tiếng Anh. Thang bốn chặng của SRC-821 (tiếng Việt trước) không áp ở các trang này nữa. - Cùng ngày, chủ dự án chỉnh thêm: thẻ "Where am I strong?" đổi tên thành "My sub-skills" trên cả bốn trang kỹ năng; bỏ chữ bậc (Building, Getting there, Solid, Strong) cạnh cụm vạch, bậc chỉ còn trong aria-label; chip tài khoản chỉ hiện tên gọi (chữ cuối của họ tên,
givenNametrongapps/learn/src/ielts/givenName.ts), họ tên đủ ở đầu menu; menu cấp 2 viết thường ("Reading", không "READING"); mục đầu của menu cấp 1 và cấp 2 thẳng một cột dọc ở màn rộng. - Việc nối tiếp: nội dung D1/JSON chỉ có tiếng Việt đang bị ẩn; cần soạn bản tiếng Anh rồi hiện lại.
- Cổng:
apps/learn/e2e/ieltsSkillPagesEnglish.spec.tsmở từng trang trong phạm vi với API mock và đỏ khi thấy bất kỳ chữ cái tiếng Việt nào trong nội dung trang (trừ header chung và tên learner).
Lịch sử quyết định
Từ mục 8. Cả sân IELTS chỉ còn tiếng Anh, và dashboard nói về công sức (SRC-834, SRC-835, 2026-09-18)
Chỉ đạo chủ dự án: "Bỏ VI/EN ở góc trên phải đi, cho toàn bộ các trang IELTS. Bên trong trang này, chuyển hết thành tiếng Anh."
Chỉ đạo: "Các dashboard nhỏ ở trên cùng các trang này, có thể tách thành 3 cards nhỏ hơn. Mỗi card là một chart riêng, và trả lời một câu hỏi riêng."
Chỉ đạo: "Màn hình này cần gọn 4 cái links tới 4 skill lại, đưa vào 1 card cho gọn. [...] hiển thị vài mini-cards [...] ghi nhận những gì mà Learner đã làm được trong 30, và 7 ngày qua, và tiêu chuẩn chung mỗi ngày, và trạng thái ngày hôm nay. [...] Và đưa ra Prediction, (nội suy từ dữ liệu cũ), rằng bao giờ (ngày nào) thì Learner học đủ 200 hours, nếu tiếp tục học với tiến độ của 7 ngày qua, 30 ngày qua."
Bản trước (SRC-824) là một thẻ chứa bốn con số rồi một đường biểu đồ ở dưới. Bốn con số ấy đo bốn thứ khác nhau nên đứng chung một khung chúng đọc ra như một bảng kê, và learner phải tự ghép xem con số nào trả lời câu hỏi nào của mình.
Từ mục 26. Sân luyện đứng sau đăng nhập (SRC-883, 2026-09-19)
Chỉ đạo chủ dự án, kèm ảnh chụp /ielts/reading/clusters mở ra với chip "Sign in" ở góc phải: "Nếu không signin, thì hệ thống bắt buộc phải signin."
(Tách từ đầu §26.) Đây là đảo lại SRC-792, và đảo có ý thức. SRC-792 để mạch luyện mở công khai vì 140 trang nội dung vốn sống trên ielts.nemo12.com cho người chưa có tài khoản và cho máy tìm kiếm; nay chủ dự án chọn đổi.
Từ mục 27. Khoá luôn API, không chỉ giao diện (SRC-885, 2026-09-20)
§26 dựng cổng đăng nhập ở giao diện. Chủ dự án đọc xong phần "hệ quả phải biết" và chốt ngay: "Khoá luôn API luôn đi."
(Tách từ §27, mục "Vá cùng ngày".) §27 viết rằng một dòng use(prefix, requireSession) phủ cả nhóm, nên "đường thứ mười một thêm vào mai sau được chặn sẵn". Chưa đầy một ngày sau, SRC-888 thêm GET /v1/public/ielts/exam-papers — và nó lọt. Khoá là /v1/public/ielts/content/*; đường mới mọc ra ngoài tiền tố ấy, nên 20 đề Writing và Speaking phục vụ cho bất kỳ ai gõ curl, đúng cái mà §27 vừa dựng cổng để chặn.
Từ mục 28. Một cổng cho cả sân, và tên learner không còn là nút thoát (SRC-886, 2026-09-20)
Chủ dự án gửi hai ảnh chụp learn.nemo12.com kèm ba câu: "Click vào tag 'Nguyễn Tất Đắc' ở góc trên phải, thì đang đồng nghĩa với Logout. Cần fix. Click vào icon IELTS ở ảnh 2, thì lại xuất hiện ảnh 2. Fix đi. Quá là loạn xạ!"
Vòng sau, cùng ngày — tên quay lại cạnh avatar. Bản đầu của vòng này chỉ giữ avatar (ảnh Google, hoặc chữ tắt tên), nên tên learner biến mất khỏi thanh trên; chủ dự án xem xong nói "Hiện tên bên cạnh avatar đi." Đúng: lỗi được báo là cái tên bấm vào thì thoát phiên, không phải cái tên hiện ra — chữa bằng cách bỏ nó đi là chữa nhầm vế.
(Tách từ §28, mục "Còn mở, có chủ ý", dòng SRC-909.) Mục này từng ghi là "còn mở": cùng một cái logo làm hai việc tuỳ chỗ đứng — ở trang luyện về /ielts, ở hub thì rời hẳn sân IELTS — và lý lẽ giữ nguyên là "ở hub nó là đường ra duy nhất". Chủ dự án chốt: "Click vào logo thì ko được về HOME, mà cần về trang ielts."
(Tách từ §28, mục "Còn mở, có chủ ý", dòng SRC-937.) Mục này từng ghi là còn mở: ielts/auth.tsx hỏi /v1/me một lần lúc mount và không nghe nemo12:session-expired, nên một cookie hết hạn lúc 9h05 vẫn để learner ngồi làm bài tới hết buổi — mọi lượt ghi bài làm bị từ chối trong im lặng, vì useRecordAttempt nuốt lỗi có chủ đích.
Từ mục 30. Cổng vai người đọc: quyền đúng, người đọc sai (SRC-897, 2026-09-20)
Sau khi §29 lên production, chủ dự án bảo mở trang thật lên xem. Trang chạy đúng — và để lộ một lỗi mà 1771 test API cùng 221 e2e đều không thấy.
Trước SRC-884, mọi chỗ đọc ai_notes_json đều đi qua một hàm chiếu có tên: teaserOf lấy đúng một câu cho learner, parseNotes dựng bản đầy đủ cho mentor. Không ai viết luật ấy ra, nên không có gì ngăn người tiếp theo JSON.parse thẳng — và người tiếp theo đã làm đúng như vậy.
Từ mục 105. Membership nói tiếng Anh đơn giản (SRC-1021, 24.09.2026)
(Tách từ đầu §105.) Chủ dự án: "Chuyển trang Membership sang tiếng Anh đơn giản luôn."
Từ mục 108. Learning Plan nói tiếng Anh đơn giản (SRC-1028, 24.09.2026)
(Tách từ đầu §108.) Chủ dự án: "Chuyển trang Learning Plan sang tiếng Anh đơn giản luôn."