Plan 16 Phase 4 5 - SekiroKenjii/Quizzivy GitHub Wiki
Historical phase record. On 2026-09-22 the user approved the independent
student UX review. Spec v0.5 and 92-student-ux.md supersede the ordinary-page
right panels, mockup-only acceptance and phone width constraints below. Section
payloads, shuffle rules, auth and integrity contracts remain applicable.
Deliverable: every /app/* screen designed and built for 1024px and up,
and the take-test engine brought to parity with the deck at every width
(S-05, S-06, S-08, S-12, and the new S-15), including the section dimension
the engine has been missing since Phase 3.
Exit criteria: each student route at 1280 and 1440 matches S-13–S-17
board by board; the engine matches S-08 at 1024 and S-12 at 360; #87, #88
and the take-test half of #91 close; every CI step green, E2E chromium and
live green; released to production as v0.4.0-phase-4.5.
Why a half phase. Phase 3 built the student side from a sheet drawn at 390 and stretched it: on a laptop the home was a 768px column of full-width buttons, the intro kept its phone back-arrow chrome, the result page stacked its score above a single column, and the engine kept the sticky footer and the separate save strip that S-08 removes above 1024. Thuong measured this after the Phase 4 release (2026-09-06) and asked for it to be fixed before Phase 5 rather than folded into it: "giao diện của học viên đang focus hết vào mobile … giao diện trên desktop rất tệ và kể cả phần giao diện làm bài thi cũng không giống với mockup ở nhiều điểm."
Design first, because the deck is the contract (docs/design/mockups): the
student sheet gains section 6 with five boards, and nothing is built that a
board does not draw. Then the contract change the engine needs, then the
engine, then the shell and the four pages, then release.
Decisions taken in this file, worth a second look.
-
Sections reach the student through the session payload.
AttemptSessiongainssections[](id,title,instructions) andStudentQuestiongainssectionId. Nothing revealing is added — instructions are written for the student — and E2E 9's key list is unchanged. -
Shuffle stays inside a section.
DealManager.Presentshuffles the questions of each section with the section id as salt and keeps section order, so S-08's rail can group by section and still count 1…n in presentation order. A cross-section shuffle would put "Phần 1" at questions 1, 4, 7. An attempt in flight across the deploy keeps every answer (they are keyed by question id) and sees its questions reordered once; the release note says so. -
caseSensitivejoinsStudentBlank. The deck's fill-blank line ("Không phân biệt hoa thường. Viết đúng chính tả.") is a promise about grading, and a promise the student cannot check is worse than none. The flag is a rule, not a key; it is the one field of the blank that is safe to show. - Section instructions are shown on the first question of the section in presentation order, not on every question. S-05 draws them on Q11 and Q16, which are firsts; a ten-question section repeating its instruction ten times is noise. The navigator's section label carries the title on every question.
- Below 1024 nothing changes visually except the column cap: the S-03 layouts keep their phone shape, with the content column capped at 40rem rather than the current 48rem (S-13's note).
-
One student layout, not two.
StudentDetailLayout(back arrow + title) becomes a mode ofStudentLayoutselected by a route handle, because S-13/S-14 keep the nav bar on detail pages from 1024 and only the phone wants the arrow. The take-test route keepsFocusLayout. -
The student's right column is F-11's panel, through
PageAside. Not a grid column: the sameSideColumnthe admin screens use, full height beside the scrolling middle, with F-13's handle and the remembered panel width.StudentLayouttherefore exposes the samePageAsideSlotthe admin layout does, and each page renders its panel content into it from 1024; below that the same content returns to the flow where S-03/S-10 draw it (Thuong, 2026-09-07: "nhớ áp dụng quy ước aside", and the narrow-column classes and settings pages "bị bóp lại … khá đơn điệu"). -
#94 ("Hồ sơ" card) was decided on 2026-09-07: build it. Thuong chose the
deck-faithful answer over dropping the card, so
PATCH /auth/metakes afullNameand nothing else, and both boards now draw the same card. The email is the login and stays read-only; onlyPATCH /admin/students/{id}moves it. The card's hint names where the address came from, which is the answer to "why can I not edit this?".
Depends on: —
Touches: docs/design/mockups/sheets/10-student.html, docs/design/mockups/index.html
Size: M
Done when:
- Section 6 with S-13 (shell + home), S-14 (intro), S-15 (review at 1024), S-16 (result), S-17 (classes + settings), each with a board note stating the rule it adds and callouts for the states it does not draw
- Every rule is derived from an existing board (F-04 densities, F-11 panel width, S-08's 720px column) — no new tokens, no new components
-
node docs/design/mockups/check.mjsclean; index counts updated - Thuong has looked at the five boards in the deck and said go (2026-09-07, after the redraw to F-11/F-13 and the full-width S-17)
Depends on: —
Touches: api/openapi.yaml, server/gen/openapi/, server/internal/modules/attempts/{domain,repositories,http}/, web/src/lib/api/schema.d.ts, web/src/features/take-test/
Size: M
Done when:
-
AttemptSession.sections: StudentSection[](id,title,instructions: string | null, in test order) andStudentQuestion.sectionId;StudentBlank.caseSensitive - The session query projects
test_version_sections(already joined for ordering) into the payload; the domainQuestioncarriesSectionID, the paper carriesSections -
DealManager.Presentshuffles within a section (salt = section id) and keeps section order;deal_testpins that a two-section version never interleaves - The contract test over
openapi.yamland E2E 9 still assert none ofisCorrect,sampleAnswer,acceptedAnswers,transcriptunder/app/*;make gen-checkandpnpm gen:checkclean -
make test-apigreen (needs Docker)
Depends on: T-4.5.2
Touches: web/src/features/take-test/, web/src/lib/i18n/locales/{vi,en}.json
Size: L
Done when:
- From
lg: one header row — Thoát · test title · save badge ("Đã lưu 09:41" / "Đang lưu…" / unsaved) · "Còn n lần rời trang" · timer — over the 4px progress bar; the save strip and the sticky footer render only belowlg - From
lg: "Câu trước" (outline) and "Câu sau" (primary) inline under the answer at their own width, the last question's "Xem lại & nộp" in the same slot, and the S-08 shortcut hint to their right - From
lg: the meta line above the stem reads "Phần 1 · Ngữ pháp — Câu 3 / 24 · 1 điểm"; belowlgthe header keeps "Câu 3/24" and the points stay under the answer (S-05) - Section instructions render above the body of the section's first
question: an
alert-mutedwith a headphones icon when the question has audio, otherwise the muted line S-05 draws on the fill-blank frame - Navigator rail and sheet group the dots under "Phần n · Title" (S-06,
S-08); the review screen at
lgis S-15 — header row unchanged, first control "Quay lại bài", rail without its button, the two actions inline - Fill-blank closes with the matching rule (case-sensitive or not) and
short-answer's meta is one
justify-betweenrow (#91) - Unit tests: section grouping, first-of-section detection, the
lgbranch of the header (render at 1280 and 360 with a matchMedia stub) - E2E chromium (1280): the smoke spec asserts the S-08 header row and the
absence of the footer; a new
mobile-chromiumproject (Pixel 5) runs the same spec and asserts the S-12 footer
Depends on: T-4.5.1 sign-off
Touches: web/src/layouts/StudentLayout.tsx, web/src/layouts/StudentDetailLayout.tsx (removed), web/src/app/router.tsx, web/src/components/shared/
Size: M
Done when:
- Top bar: logo, "Bài của tôi", "Lớp", and from
lga user menu (avatar initial + given name + chevron → Cài đặt, Đăng xuất); belowlgthe S-03 bar with its icon button, unchanged - From
lgthe content area is the admin's two columns: a scrolling middle withp-8and thePageAsideSlot, so a page'sPageAsidelands beside the scroll at F-11's width with F-13's handle; belowlgthe phone padding and a 40rem cap - Detail routes (
assignments/:id,attempts/:id/result,settings) declarehandle.detail; belowlgthe layout shows the back arrow + title, fromlgthe nav bar with the page drawing its own "← Bài của tôi" link (S-14, S-16) -
/app/settingslights no nav item;/app/classeslights "Lớp" - Unit test: the layout renders the arrow at 360 and the bar at 1280 for a detail route
Depends on: T-4.5.4
Touches: web/src/features/assignments/pages/StudentHomePage.tsx
Size: M
Done when:
- From
lg: the middle fills its column; due and resume cards lay out sideways with the button at its own width on the right; completed rows add the class name before "Nộp dd/mm" - Panel (
PageAside): "Sắp tới · n" then "Lớp của tôi" (names, teacher, "Tham gia lớp") under F-11's uppercase labels; not rendered when both are empty; empty states stay in the middle - Below
lg: S-03 exactly as today
Depends on: T-4.5.4
Touches: web/src/features/assignments/pages/AssignmentIntroPage.tsx
Size: S
Done when:
- From
lg: back link, provenance and title over "Khi làm bài" and "Sau khi nộp" side by side (3fr / 2fr); the panel holds "Tóm tắt" as label/value rows, the start button and its note; blocked states and resume take the button's place - Below
lg: S-04 exactly as today
Depends on: T-4.5.4
Touches: web/src/features/results/pages/ResultPage.tsx
Size: M
Done when:
- From
lg: back link, title, and the "Nộp lúc … · Lượt n/m" line over the filters and the paper at 720px in the middle; the panel holds "Kết quả": the tile, then Đúng / Sai / Chờ chấm / Nộp lúc / Lượt as label/value rows, then the withheld-block sentence - The S-09b subtractions hold in both columns; a withheld score leaves the right column empty except the one muted sentence
- Below
lg: S-09 exactly as today
Depends on: T-4.5.4
Touches: web/src/features/classes/pages/StudentClassesPage.tsx, web/src/features/auth/pages/StudentSettingsPage.tsx, web/src/features/auth/pages/LoginPage.tsx, web/src/layouts/PublicLayout.tsx
Size: S
Done when:
- Classes: a two-column grid of class cards (name, teacher · joined,
description, open / submitted counts derived from the home's lists)
with "Lớp" lit, and the panel holding S-01's join-code field that goes
to
/join/:code/confirm; belowlgthe "Tham gia lớp" button as S-10 - Settings: a two-column grid of Hồ sơ / Mật khẩu / Tài khoản Google /
Ngôn ngữ, nothing lit, and the panel holding the account (avatar, name,
email, role, sign-in methods, since, classes) with "Đăng xuất"; below
lgS-10's list with sign-out at the bottom - Login shows S-02's brand panel from
lg; join is S-01's single card at every width — both measured in the browser, fixed if off
Depends on: T-4.5.3 … T-4.5.8 Touches: — Size: M Done when:
- Every
/app/*route measured at 1280, 1024, 768 and 360 as the seed student, with the engine on a three-part paper (the dev seed gained one:seed/05-dev-sections.sql), compared board by board with the deck open beside it (2026-09-07) - Deviations fixed in the same PR: the phone's flag beside the paper, the worth said once at 1024, the nav's "Lớp", full dates in the panels
Depends on: T-4.5.9
Touches: docs/plan/, AGENTS.md, server/README.md if the contract note moves
Size: S
Done when:
-
release/phase-4.5offdevelop, merged--no-fftomainwith Thuong's go, taggedv0.4.0-phase-4.5; Fly and Cloudflare Pages green;/healthzand a student login checked on production - #87, #88 closed; #91 updated to leave only the papers back button
- AGENTS.md records the student layout rule (one layout, detail mode by handle, 1024 breakpoint) and the within-section shuffle
Generated from docs/plan/16-phase-4.5.md. Edit that file in the repository; changes made on this page are overwritten.