Reading Journey: quiz answer review never shows correct answers/explanations (no backend endpoint ships the answer key) #663

Closed
opened 2026-07-17 16:19:01 +00:00 by spikerj · 5 comments
Owner

Found while enabling strictTemplates (#662). The reading-journey quiz review UI reads correctAnswerIndex/explanation from QuestionForTakingDto, but that DTO intentionally excludes answers ("excluded for security" per backend comment) and no controller endpoint serves the existing QuizForReviewDto. Result: the correct-answer highlight and explanations have always rendered as undefined — the review feature is visually non-functional.

Frontend is now honestly typed (type-guard helpers correctAnswerIndexOf()/explanationOf() in reading-journey.component.ts), so wiring this up needs:

  1. Backend: a post-completion review endpoint returning QuizForReviewDto (DTO already exists, unused)
  2. Frontend: fetch review data after submission and pass it to the review template (the guards will pick it up)
Found while enabling strictTemplates (#662). The reading-journey quiz review UI reads `correctAnswerIndex`/`explanation` from `QuestionForTakingDto`, but that DTO intentionally excludes answers ("excluded for security" per backend comment) and no controller endpoint serves the existing `QuizForReviewDto`. Result: the correct-answer highlight and explanations have always rendered as undefined — the review feature is visually non-functional. Frontend is now honestly typed (type-guard helpers `correctAnswerIndexOf()`/`explanationOf()` in reading-journey.component.ts), so wiring this up needs: 1. Backend: a post-completion review endpoint returning `QuizForReviewDto` (DTO already exists, unused) 2. Frontend: fetch review data after submission and pass it to the review template (the guards will pick it up)
Author
Owner

Backend half up as PR #419 (mergeable). Claim verified: QuizForReviewDto/ToReviewDto() existed with the full answer key but had zero controller callers.

New GET api/quiz/{id}/review: ownership via the same GetQuizQuery-by-username pattern as GetQuiz; 403 until the caller has a completed attempt (new IQuizAttemptService.HasCompletedAttempt) so the taking-DTO's answer exclusion can't be bypassed mid-quiz; 200 → ToReviewDto(). 4 controller tests incl. an explicit no-answer-leak assertion on the 403 path.

Remaining (this ticket stays open): the spikersoft-angular half — fetch /review after submission and feed the existing correctAnswerIndexOf()/explanationOf() guards in reading-journey.component.ts.

**Backend half up as PR [#419](https://git.spikersoft.com/spikerj/spikersoft-backend/pulls/419)** (mergeable). Claim verified: `QuizForReviewDto`/`ToReviewDto()` existed with the full answer key but had zero controller callers. New `GET api/quiz/{id}/review`: ownership via the same `GetQuizQuery`-by-username pattern as `GetQuiz`; **403 until the caller has a completed attempt** (new `IQuizAttemptService.HasCompletedAttempt`) so the taking-DTO's answer exclusion can't be bypassed mid-quiz; 200 → `ToReviewDto()`. 4 controller tests incl. an explicit no-answer-leak assertion on the 403 path. **Remaining (this ticket stays open):** the spikersoft-angular half — fetch `/review` after submission and feed the existing `correctAnswerIndexOf()`/`explanationOf()` guards in `reading-journey.component.ts`.
Author
Owner

Sweep note: backend PR #419 merged the post-completion review endpoint with the answer key, but I could not find frontend code consuming it in reading-journey — the user-facing 'never shows correct answers' symptom may persist. Leaving open pending the UI half (or evidence it exists).

Sweep note: backend PR #419 merged the post-completion review endpoint with the answer key, but I could not find frontend code consuming it in reading-journey — the user-facing 'never shows correct answers' symptom may persist. Leaving open pending the UI half (or evidence it exists).
Author
Owner

Audited against origin/master in both repos — the backend half shipped, the Angular half never did. Staying open. The ticket title's premise ("no backend endpoint ships the answer key") is now stale, so updating the notes:

Backend: DONE. spikersoft-backend PR #419 (d06150b6, merge 9f335874).

  • QuizController.cs:85 — [HttpGet("{id}/review")] GetQuizReview, returning QuizForReviewDto.
  • Correctly gated: :110 checks _quizAttemptService.HasCompletedAttempt(id, username) and returns 403 otherwise, so the taking DTO's answer-exclusion can't be bypassed mid-quiz. Ownership still resolved through GetQuizQuery (404 on miss).
  • DTOs split as intended: QuizDto.cs:118 keeps CorrectAnswerIndex/Explanation excluded from the taking shape; :145-146 adds them to the review shape.

Angular: NOT DONE — nothing calls the endpoint. git grep -nE "/review|getQuizReview|QuizForReview" origin/master -- '*.ts' over spikersoft-angular returns no quiz hits at all (only an art-studio review link and an unrelated geography.service.ts:316 facts-review POST).

The consequence is a UI that silently renders the empty state forever:

  • reading-journey.component.ts:724-726 — correctAnswerIndexOf() is return "correctAnswerIndex" in question ? question.correctAnswerIndex : null;, and :728-730 explanationOf() is the same shape. Since the client only ever fetches the taking DTO, that in check is always false and both helpers always return null.
  • So reading-journey.component.html:428 [class.correct-answer]="$index === correctAnswerIndexOf(question)" never matches any option, and :277 keeps rendering readingQuiz.explanationPending — "Explanation will be available after quiz completion" (en.json:1752) — which is now a false promise: completion happens, the explanation still never arrives.

Remaining work: add a getQuizReview(id) call to the quiz service, fetch the review DTO once the attempt is complete, and feed it to the review pane so correctAnswerIndexOf/explanationOf receive a QuizQuestion rather than a QuestionForTakingDto. Then drop or re-word explanationPending.

Note the identical dead-helper pattern exists at aptituderator.component.ts:699 — worth fixing in the same pass, or filing separately if aptituderator has no review endpoint of its own.

Audited against `origin/master` in both repos — **the backend half shipped, the Angular half never did.** Staying open. The ticket title's premise ("no backend endpoint ships the answer key") is now stale, so updating the notes: **Backend: DONE.** spikersoft-backend PR #419 (`d06150b6`, merge `9f335874`). - `QuizController.cs:85` — `[HttpGet("{id}/review")] GetQuizReview`, returning `QuizForReviewDto`. - Correctly gated: `:110` checks `_quizAttemptService.HasCompletedAttempt(id, username)` and returns **403** otherwise, so the taking DTO's answer-exclusion can't be bypassed mid-quiz. Ownership still resolved through `GetQuizQuery` (404 on miss). - DTOs split as intended: `QuizDto.cs:118` keeps `CorrectAnswerIndex`/`Explanation` excluded from the taking shape; `:145-146` adds them to the review shape. **Angular: NOT DONE — nothing calls the endpoint.** `git grep -nE "/review|getQuizReview|QuizForReview" origin/master -- '*.ts'` over spikersoft-angular returns **no quiz hits at all** (only an art-studio review *link* and an unrelated `geography.service.ts:316` facts-review POST). The consequence is a UI that silently renders the empty state forever: - `reading-journey.component.ts:724-726` — `correctAnswerIndexOf()` is `return "correctAnswerIndex" in question ? question.correctAnswerIndex : null;`, and `:728-730` `explanationOf()` is the same shape. Since the client only ever fetches the *taking* DTO, that `in` check is always false and both helpers always return `null`. - So `reading-journey.component.html:428` `[class.correct-answer]="$index === correctAnswerIndexOf(question)"` never matches any option, and `:277` keeps rendering `readingQuiz.explanationPending` — *"Explanation will be available after quiz completion"* (`en.json:1752`) — which is now a false promise: completion happens, the explanation still never arrives. **Remaining work:** add a `getQuizReview(id)` call to the quiz service, fetch the review DTO once the attempt is complete, and feed it to the review pane so `correctAnswerIndexOf`/`explanationOf` receive a `QuizQuestion` rather than a `QuestionForTakingDto`. Then drop or re-word `explanationPending`. Note the identical dead-helper pattern exists at `aptituderator.component.ts:699` — worth fixing in the same pass, or filing separately if aptituderator has no review endpoint of its own.
Author
Owner

Backend half is merged; the frontend half is all that's left.

GET api/quiz/{id}/review exists on master (SpikerSoft.Api/Domain/Quiz/QuizController.cs:86, spikersoft-backend#419) — ownership-scoped, 403 unless the caller has ≥1 completed attempt so the taking DTO's answer-exclusion can't be bypassed mid-quiz, 200 → quiz.ToReviewDto().

Nothing calls it. reading-journey.component.ts still has only the honest type guards (correctAnswerIndexOf() at :724, explanationOf() at :728) and no /review fetch anywhere in the component or the services. The review UI therefore still renders undefined highlights in production.

Remaining: fetch the review payload after submission and feed it to the existing guards.

— 2026-08-06 tracker sweep, Opus 5 Agent. Staying open.

**Backend half is merged; the frontend half is all that's left.** `GET api/quiz/{id}/review` exists on master (`SpikerSoft.Api/Domain/Quiz/QuizController.cs:86`, spikersoft-backend#419) — ownership-scoped, 403 unless the caller has ≥1 completed attempt so the taking DTO's answer-exclusion can't be bypassed mid-quiz, 200 → `quiz.ToReviewDto()`. Nothing calls it. `reading-journey.component.ts` still has only the honest type guards (`correctAnswerIndexOf()` at :724, `explanationOf()` at :728) and no `/review` fetch anywhere in the component or the services. The review UI therefore still renders undefined highlights in production. Remaining: fetch the review payload after submission and feed it to the existing guards. — 2026-08-06 tracker sweep, Opus 5 Agent. Staying open.
Author
Owner

Migrated to spikerj/spikersoft-angular#654 as part of the umbrella-tracker breakup.

Verified 2026-08-07. backend@98102023 QuizController.cs:85 review endpoint live (401 anon); angular@8e5a4048 has zero getQuizReview/QuizForReview hits, guards still fed the taking DTO

Status: Add getQuizReview(id) to the quiz service, fetch the review DTO once the attempt is complete, and pass a QuizQuestion (not QuestionForTakingDto) into the existing guards; then drop or re-word explanationPending. The identical dead-guard pattern at aptituderator.component.ts:699,703 (aptituderator.component.html:389,540,556) should be fixed in the same pass or split out if aptituderator has no review endpoint of its own. The title's original premise ("no backend endpoint ships the answer key") is stale — only the UI half remains.

Closing here. Work now lives in the repo that holds the fix, so fixes #654 in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.

— Opus 5 Agent

Migrated to **spikerj/spikersoft-angular#654** as part of the umbrella-tracker breakup. Verified 2026-08-07. backend@98102023 QuizController.cs:85 review endpoint live (401 anon); angular@8e5a4048 has zero getQuizReview/QuizForReview hits, guards still fed the taking DTO Status: Add `getQuizReview(id)` to the quiz service, fetch the review DTO once the attempt is complete, and pass a `QuizQuestion` (not `QuestionForTakingDto`) into the existing guards; then drop or re-word `explanationPending`. The identical dead-guard pattern at `aptituderator.component.ts:699,703` (`aptituderator.component.html:389,540,556`) should be fixed in the same pass or split out if aptituderator has no review endpoint of its own. The title's original premise ("no backend endpoint ships the answer key") is stale — only the UI half remains. Closing here. Work now lives in the repo that holds the fix, so `fixes #654` in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only. — Opus 5 Agent
Sign in to join this conversation.