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:
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)
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`.
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).
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:
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-730explanationOf() 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.
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.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while enabling strictTemplates (#662). The reading-journey quiz review UI reads
correctAnswerIndex/explanationfromQuestionForTakingDto, but that DTO intentionally excludes answers ("excluded for security" per backend comment) and no controller endpoint serves the existingQuizForReviewDto. 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:QuizForReviewDto(DTO already exists, unused)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 sameGetQuizQuery-by-username pattern asGetQuiz; 403 until the caller has a completed attempt (newIQuizAttemptService.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
/reviewafter submission and feed the existingcorrectAnswerIndexOf()/explanationOf()guards inreading-journey.component.ts.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).
Audited against
origin/masterin 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, merge9f335874).QuizController.cs:85—[HttpGet("{id}/review")] GetQuizReview, returningQuizForReviewDto.:110checks_quizAttemptService.HasCompletedAttempt(id, username)and returns 403 otherwise, so the taking DTO's answer-exclusion can't be bypassed mid-quiz. Ownership still resolved throughGetQuizQuery(404 on miss).QuizDto.cs:118keepsCorrectAnswerIndex/Explanationexcluded from the taking shape;:145-146adds 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 unrelatedgeography.service.ts:316facts-review POST).The consequence is a UI that silently renders the empty state forever:
reading-journey.component.ts:724-726—correctAnswerIndexOf()isreturn "correctAnswerIndex" in question ? question.correctAnswerIndex : null;, and:728-730explanationOf()is the same shape. Since the client only ever fetches the taking DTO, thatincheck is always false and both helpers always returnnull.reading-journey.component.html:428[class.correct-answer]="$index === correctAnswerIndexOf(question)"never matches any option, and:277keeps renderingreadingQuiz.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 socorrectAnswerIndexOf/explanationOfreceive aQuizQuestionrather than aQuestionForTakingDto. Then drop or re-wordexplanationPending.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.Backend half is merged; the frontend half is all that's left.
GET api/quiz/{id}/reviewexists 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.tsstill has only the honest type guards (correctAnswerIndexOf()at :724,explanationOf()at :728) and no/reviewfetch 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.
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 aQuizQuestion(notQuestionForTakingDto) into the existing guards; then drop or re-wordexplanationPending. The identical dead-guard pattern ataptituderator.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 #654in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.— Opus 5 Agent