diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md
index 1cbd119..a26be2e 100644
--- a/.planning/REQUIREMENTS.md
+++ b/.planning/REQUIREMENTS.md
@@ -74,7 +74,7 @@ interview-ready after each short practice loop.
- [x] **WEB-02**: User can create a diagnostic interview session from the web
app.
- [x] **WEB-03**: User can answer a diagnostic question and see rubric feedback.
-- [ ] **WEB-04**: User can see learner memory, readiness, and next challenge
+- [x] **WEB-04**: User can see learner memory, readiness, and next challenge
after answering.
- [ ] **WEB-05**: Operator can ingest source material from the web app.
- [ ] **WEB-06**: Operator can inspect ontology candidate concepts, edges, and
@@ -117,7 +117,7 @@ interview-ready after each short practice loop.
| ONTO-01..ONTO-04 | Phase 5 | Complete |
| ASSET-01..ASSET-03 | Phase 6 | Complete |
| WEB-01..WEB-03 | Phase 7 | Complete |
-| WEB-04 | Phase 8 | Pending |
+| WEB-04 | Phase 8 | Complete |
| WEB-05..WEB-08 | Phase 9 | Pending |
**Coverage:**
@@ -128,4 +128,4 @@ interview-ready after each short practice loop.
---
*Requirements defined: 2026-04-26*
-*Last updated: 2026-04-26 after Phase 7 execution.*
+*Last updated: 2026-04-26 after Phase 8 execution.*
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 3e2a0d7..b6acb3b 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -7,7 +7,7 @@ See: `.planning/PROJECT.md` (updated 2026-04-26)
**Core value:** The user should feel and prove that they are becoming more
interview-ready after each short practice loop.
-**Current focus:** Phase 8 planning: Learning Progress View.
+**Current focus:** Phase 9 planning: Material and Asset Workspace.
## Current Decisions
@@ -38,10 +38,11 @@ interview-ready after each short practice loop.
- v2 Frontend MVP milestone selected to turn the backend learning loop into a
usable web service.
- Phase 7 web app shell and diagnostic start UI is implemented and verified.
+- Phase 8 learning progress view is implemented and verified.
## Next Actions
-1. Plan and execute Phase 8: Learning Progress View.
+1. Plan and execute Phase 9: Material and Asset Workspace.
2. Verify the production OpenAI image model identifier before real image
generation calls.
3. Add standardized SUMMARY frontmatter or Nyquist validation files if future
@@ -80,6 +81,9 @@ interview-ready after each short practice loop.
- 2026-04-26: Phase 7 implementation verified with `go test ./...`, OpenSpec
validation, root/asset HTTP smoke, and diagnostic API smoke through the
server used by the web app.
+- 2026-04-26: Phase 8 implementation verified with `go test ./...`, OpenSpec
+ validation, app script smoke, and learner memory/readiness/next-challenge API
+ smoke after an answer.
---
*State initialized: 2026-04-26.*
diff --git a/.planning/phases/008-learning-progress-view/008-CONTEXT.md b/.planning/phases/008-learning-progress-view/008-CONTEXT.md
new file mode 100644
index 0000000..d5ccd9e
--- /dev/null
+++ b/.planning/phases/008-learning-progress-view/008-CONTEXT.md
@@ -0,0 +1,34 @@
+# Phase 8 Context: Learning Progress View
+
+**Status:** Ready for execution
+**Started:** 2026-04-26
+
+## Goal
+
+Show evidence-backed learning progress in the web app after diagnostic
+practice.
+
+## Requirements
+
+- WEB-04: User can see learner memory, readiness, and next challenge after
+ answering.
+
+## Inputs
+
+- Phase 7 web app shell.
+- Existing backend endpoints:
+ - `GET /api/v1/learners/{userID}/memory`
+ - `GET /api/v1/learners/{userID}/readiness-map`
+ - `GET /api/v1/learners/{userID}/next-challenge`
+
+## UX Direction
+
+Keep progress in the right-side context column so the answer workspace remains
+centered. The user should see the loop close immediately: answer, feedback,
+memory, readiness, next challenge.
+
+## Out of Scope
+
+- Full graph visualization.
+- Historical readiness timeline.
+- Editing learner memory.
diff --git a/.planning/phases/008-learning-progress-view/008-PLAN.md b/.planning/phases/008-learning-progress-view/008-PLAN.md
new file mode 100644
index 0000000..41e66d8
--- /dev/null
+++ b/.planning/phases/008-learning-progress-view/008-PLAN.md
@@ -0,0 +1,26 @@
+# Phase 8 Plan: Learning Progress View
+
+**Status:** Ready for execution
+**Phase Goal:** Close the practice loop with visible learning progress.
+
+## Tasks
+
+### 1. Add progress UI region
+
+- Add learner progress section to the right context pane.
+- Include manual refresh affordance.
+
+### 2. Fetch progress after answer
+
+- Fetch learner memory, readiness map, and next challenge after grading.
+- Render empty/error states when progress is unavailable.
+
+### 3. Verify
+
+- Add/update web app tests for progress asset content.
+- Run Go tests, OpenSpec validation, line-count check, and live smoke.
+
+## Out of Scope
+
+- Charts.
+- Historical progress storage.
diff --git a/.planning/phases/008-learning-progress-view/008-RESEARCH.md b/.planning/phases/008-learning-progress-view/008-RESEARCH.md
new file mode 100644
index 0000000..22cf93d
--- /dev/null
+++ b/.planning/phases/008-learning-progress-view/008-RESEARCH.md
@@ -0,0 +1,20 @@
+# Phase 8 Research: Learning Progress View
+
+## Findings
+
+The backend already provides progress projection after answer submission. The
+frontend only needs to fetch and summarize the three endpoints after grading.
+
+Useful MVP display:
+
+- profile target role and stack
+- top concept mastery states
+- readiness percentage
+- next challenge concept, ladder level, and question
+
+## Recommendation
+
+- Refresh progress automatically after successful answer submission.
+- Add a manual refresh button for recovery.
+- Use empty state before the first answer.
+- Keep evidence labels compact so they do not overwhelm the practice surface.
diff --git a/.planning/phases/008-learning-progress-view/008-SUMMARY.md b/.planning/phases/008-learning-progress-view/008-SUMMARY.md
new file mode 100644
index 0000000..7b770bc
--- /dev/null
+++ b/.planning/phases/008-learning-progress-view/008-SUMMARY.md
@@ -0,0 +1,33 @@
+# Phase 8 Summary
+
+**Status:** Complete
+**Completed:** 2026-04-26
+
+## Delivered
+
+- Added learning progress region to the web app right context pane.
+- Added manual progress refresh action.
+- After answer submission, the app fetches learner memory, readiness map, and
+ next challenge.
+- Rendered readiness percentage, concept mastery states, and recommended next
+ challenge.
+- Added frontend asset test coverage for progress API wiring.
+
+## Verification
+
+```powershell
+gofmt -w cmd internal
+go test ./...
+openspec validate frontend-mvp --strict
+```
+
+Additional smoke check:
+
+- Submitted a diagnostic answer, then verified learner memory, readiness, and
+ next challenge APIs returned progress consumed by the app script.
+
+## Deferred
+
+- Browser screenshot audit.
+- Charts and historical progress.
+- Editable learner memory.
diff --git a/.planning/phases/008-learning-progress-view/008-VERIFICATION.md b/.planning/phases/008-learning-progress-view/008-VERIFICATION.md
new file mode 100644
index 0000000..f3aba28
--- /dev/null
+++ b/.planning/phases/008-learning-progress-view/008-VERIFICATION.md
@@ -0,0 +1,24 @@
+# Phase 8 Verification
+
+## Verdict
+
+PASS
+
+## Requirement Coverage
+
+- WEB-04: PASS. The web app can fetch and render learner memory, readiness, and
+ next challenge after an answer.
+
+## Evidence
+
+- `go test ./...` passed.
+- `openspec validate frontend-mvp --strict` passed.
+- Static app script includes the readiness API integration.
+- Live smoke confirmed memory mastery, readiness percentage, and next challenge
+ after diagnostic answer submission.
+
+## Residual Risk
+
+The UI is still verified through code and HTTP/API smoke rather than browser
+screenshots. Phase 9 should add visual/browser validation after the workspace
+surface is complete.
diff --git a/internal/webapp/assets_test.go b/internal/webapp/assets_test.go
index f653ed5..c860041 100644
--- a/internal/webapp/assets_test.go
+++ b/internal/webapp/assets_test.go
@@ -33,4 +33,7 @@ func TestHandlerServesAsset(t *testing.T) {
if !strings.Contains(rec.Body.String(), "diagnostic-sessions") {
t.Fatal("expected app script content")
}
+ if !strings.Contains(rec.Body.String(), "readiness-map") {
+ t.Fatal("expected progress API content")
+ }
}
diff --git a/internal/webapp/static/app.js b/internal/webapp/static/app.js
index f466e8e..39ca69b 100644
--- a/internal/webapp/static/app.js
+++ b/internal/webapp/static/app.js
@@ -2,6 +2,7 @@ const state = {
session: null,
selectedQuestion: null,
lastAnswer: null,
+ progress: null,
};
const els = {
@@ -11,6 +12,8 @@ const els = {
answerButton: document.querySelector("#answer-button"),
questions: document.querySelector("#questions"),
feedback: document.querySelector("#feedback"),
+ progress: document.querySelector("#progress"),
+ refreshProgress: document.querySelector("#refresh-progress"),
status: document.querySelector("#status-line"),
error: document.querySelector("#error-line"),
title: document.querySelector("#session-title"),
@@ -45,6 +48,11 @@ els.sessionForm.addEventListener("submit", async (event) => {
}
});
+els.refreshProgress.addEventListener("click", async () => {
+ clearError();
+ await refreshProgress();
+});
+
els.answerForm.addEventListener("submit", async (event) => {
event.preventDefault();
clearError();
@@ -63,6 +71,7 @@ els.answerForm.addEventListener("submit", async (event) => {
});
state.lastAnswer = answer;
renderFeedback();
+ await refreshProgress();
setStatus(`Answer graded as ${answer.grade.overall}`);
} catch (error) {
showError(error.message);
@@ -96,6 +105,54 @@ function renderSession() {
els.answerButton.disabled = !state.selectedQuestion;
}
+async function refreshProgress() {
+ if (!state.session) return;
+ setStatus("Refreshing learning progress...");
+
+ try {
+ const userID = encodeURIComponent(state.session.user_id);
+ const [memory, readiness, challenge] = await Promise.all([
+ request(`/api/v1/learners/${userID}/memory`),
+ request(`/api/v1/learners/${userID}/readiness-map`),
+ request(`/api/v1/learners/${userID}/next-challenge`),
+ ]);
+ state.progress = { memory, readiness, challenge };
+ renderProgress();
+ setStatus("Learning progress updated");
+ } catch (error) {
+ showError(error.message);
+ renderProgress();
+ }
+}
+
+function renderProgress() {
+ els.refreshProgress.disabled = !state.session;
+ if (!state.progress) {
+ els.progress.className = "feedback empty-state";
+ els.progress.textContent = "Answer once to update learner memory and readiness.";
+ return;
+ }
+
+ const { memory, readiness, challenge } = state.progress;
+ const mastery = memory.mastery || [];
+ els.progress.className = "feedback";
+ els.progress.innerHTML = `
+ ${escapeHTML(memory.profile.target_role)} readiness ${escapeHTML(challenge.concept.label)} · ${escapeHTML(challenge.ladder_level)} ${escapeHTML(challenge.question)}Concept memory
+ Next challenge
+
Progress
+