3v324v23 Claude Sonnet 5.5 commited on
Commit
bdbe42d
·
1 Parent(s): cc4d7ae

Home lists every lesson behind the curriculum with two ways to catch up; archive add-behind-lessons-to-home

Browse files
docs/tasks/prd-mathly.md CHANGED
@@ -387,7 +387,7 @@ The exercise screen is where a student practices one skill.
387
 
388
  A tab bar at the bottom of the screen leads between them: home, path, submissions (STU-7), grades, collection and review (STU-8).
389
 
390
- - **STU-1 Home** shows only what is critical: the nearest test's reminder with its readiness and today's task (TST-3), a required simulation, the **new** assignments (המשימות שלי: not handed in yet), and a button that continues the path at the next skill; my AI requests and their state (AI-1).
391
  - **STU-2 Path:** see 4.4.
392
  - **STU-3 Grades:** all quiz (best grade), exercise test, simulation and assignment grades.
393
  - **STU-4 Collection:** see MST-7.
 
387
 
388
  A tab bar at the bottom of the screen leads between them: home, path, submissions (STU-7), grades, collection and review (STU-8).
389
 
390
+ - **STU-1 Home** shows only what is critical: the nearest test's reminder with its readiness and today's task (TST-3), a required simulation, **every lesson behind the curriculum** (PTH-11; earliest date first, each with its two ways to finish it: all its skills mastered, shown as "N מתוך M מיומנויות" and opening the next skill, or its quiz passed with at least 13 of 15, PTH-10, opening the quiz with the best grade so far), the **new** assignments (המשימות שלי: not handed in yet), and a button that continues the path at the next skill; my AI requests and their state (AI-1).
391
  - **STU-2 Path:** see 4.4.
392
  - **STU-3 Grades:** all quiz (best grade), exercise test, simulation and assignment grades.
393
  - **STU-4 Collection:** see MST-7.
mathly2/realtests.py CHANGED
@@ -255,8 +255,8 @@ def home(request: Request, user: dict = Depends(learner)) -> dict:
255
  the required simulation, and the next skill of the path."""
256
  here, today, uid = reads.of(request), clock.school_day(), user["id"]
257
  class_ids = [c["id"] for c in here.classes_of_student(user["email"])]
258
- from .skills import RECENT
259
- here.prefetch(("tests_of_classes", class_ids), ("recent_skills", uid, RECENT), *_student_reads(uid)) # the tests and the student's facts: one wait
260
  tests = [t for t in here.tests_of_classes(class_ids) if t["date"] >= today]
261
  cards = [card(request, user["id"], t, today) for t in tests]
262
  nearest = cards[0] if cards else None
@@ -264,7 +264,8 @@ def home(request: Request, user: dict = Depends(learner)) -> dict:
264
  next_skill_row = request.app.state.bank.skill(next_id) if next_id else None
265
  return {"tests": cards, "task": {"test_id": nearest["id"], **_with_skill_name(request, nearest["task"])} if nearest else None,
266
  "required_simulations": [{"test_id": c["id"], "date": c["date"], "days_left": c["days_left"]} for c in cards if c["simulation"]["required"]],
267
- "next_skill": {"id": next_id, "name": next_skill_row["name"]} if next_skill_row else None}
 
268
 
269
 
270
  def _student_test(request: Request, user: dict, test_id: int) -> dict:
 
255
  the required simulation, and the next skill of the path."""
256
  here, today, uid = reads.of(request), clock.school_day(), user["id"]
257
  class_ids = [c["id"] for c in here.classes_of_student(user["email"])]
258
+ from .skills import RECENT, behind_lessons
259
+ here.prefetch(("tests_of_classes", class_ids), ("recent_skills", uid, RECENT), ("attempts_of", uid, "quiz"), ("curriculum",), *_student_reads(uid)) # the tests and the student's facts: one wait
260
  tests = [t for t in here.tests_of_classes(class_ids) if t["date"] >= today]
261
  cards = [card(request, user["id"], t, today) for t in tests]
262
  nearest = cards[0] if cards else None
 
264
  next_skill_row = request.app.state.bank.skill(next_id) if next_id else None
265
  return {"tests": cards, "task": {"test_id": nearest["id"], **_with_skill_name(request, nearest["task"])} if nearest else None,
266
  "required_simulations": [{"test_id": c["id"], "date": c["date"], "days_left": c["days_left"]} for c in cards if c["simulation"]["required"]],
267
+ "next_skill": {"id": next_id, "name": next_skill_row["name"]} if next_skill_row else None,
268
+ "behind": behind_lessons(request, user)}
269
 
270
 
271
  def _student_test(request: Request, user: dict, test_id: int) -> dict:
mathly2/skills.py CHANGED
@@ -8,7 +8,7 @@ import logging
8
  from fastapi import APIRouter, Depends, HTTPException, Request
9
  from pydantic import BaseModel
10
 
11
- from . import clock, comets, quiz, reads
12
  from .access import learner
13
  from .learning import (DAILY_GOAL, MASTERED, current_round, is_faded, is_mastered, last_counted, next_in_round, next_skill, skill_state, streak,
14
  today_progress)
@@ -211,10 +211,11 @@ def _comet_due(request: Request, user_id: str, today_now: dict, state: dict | No
211
  return True
212
 
213
 
214
- @router.get("/path")
215
- def path(request: Request, user: dict = Depends(learner)) -> dict:
 
216
  store, here, uid = request.app.state.store, reads.of(request), user["id"]
217
- here.prefetch(("progress_of", uid), ("last_answered", uid), ("daily_correct", uid), ("recent_skills", uid, RECENT), ("attempts_of", uid, "quiz"), ("curriculum",))
218
  states = {p["skill_id"]: skill_state(p) for p in here.progress_of(uid)}
219
  last, today = here.last_answered(uid), clock.school_day()
220
  quizzes_facts = quiz.facts(store, request.app.state.bank, uid, clock.now(), here)
@@ -236,6 +237,29 @@ def path(request: Request, user: dict = Depends(learner)) -> dict:
236
  "completed": completed, "passed": passed, "due_date": due.isoformat() if due else None,
237
  "behind": bool(due and due < today and not completed),
238
  "quiz": {"best": fact.get("best"), "open": bool(fact.get("open")), "due_at": fact["open"]["due_at"].isoformat() if fact.get("open") else None}})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  daily = here.daily_correct(uid)
240
  return {"lessons": lessons, "behind": sum(1 for l in lessons if l["behind"]), "next": next_skill(order, states, here.recent_skills(uid, RECENT)),
241
  "today": today_progress(daily, today), "streak": streak(daily, today)}
 
8
  from fastapi import APIRouter, Depends, HTTPException, Request
9
  from pydantic import BaseModel
10
 
11
+ from . import attempts, clock, comets, quiz, reads
12
  from .access import learner
13
  from .learning import (DAILY_GOAL, MASTERED, current_round, is_faded, is_mastered, last_counted, next_in_round, next_skill, skill_state, streak,
14
  today_progress)
 
211
  return True
212
 
213
 
214
+ def lesson_rows(request: Request, user: dict) -> tuple[list[dict], list[str], dict]:
215
+ """The student's lessons with their skills' states, whether each is completed (all skills mastered or its quiz passed) and behind the curriculum,
216
+ and its quiz state. Shared by the path and the home. Returns (lessons, the skills in the bank's order, the skill states)."""
217
  store, here, uid = request.app.state.store, reads.of(request), user["id"]
218
+ here.prefetch(("progress_of", uid), ("last_answered", uid), ("attempts_of", uid, "quiz"), ("curriculum",))
219
  states = {p["skill_id"]: skill_state(p) for p in here.progress_of(uid)}
220
  last, today = here.last_answered(uid), clock.school_day()
221
  quizzes_facts = quiz.facts(store, request.app.state.bank, uid, clock.now(), here)
 
237
  "completed": completed, "passed": passed, "due_date": due.isoformat() if due else None,
238
  "behind": bool(due and due < today and not completed),
239
  "quiz": {"best": fact.get("best"), "open": bool(fact.get("open")), "due_at": fact["open"]["due_at"].isoformat() if fact.get("open") else None}})
240
+ return lessons, order, states
241
+
242
+
243
+ def behind_lessons(request: Request, user: dict) -> list[dict]:
244
+ """The lessons behind the curriculum for the home, the earliest date first: how far the skills are and the next unmastered one, and the quiz."""
245
+ lessons, _, _ = lesson_rows(request, user)
246
+ rows = []
247
+ for l in lessons:
248
+ if not l["behind"]:
249
+ continue
250
+ nxt = next((s for s in l["skills"] if s["state"] != MASTERED), None)
251
+ rows.append({"id": l["id"], "name": l["name"], "due_date": l["due_date"], "mastered": l["mastered"], "total": len(l["skills"]),
252
+ "next_skill": {"id": nxt["id"], "name": nxt["name"]} if nxt else None,
253
+ "quiz": {"best": l["quiz"]["best"], "open": l["quiz"]["open"], "pass_right": attempts.PASS_RIGHT, "size": attempts.QUIZ_SIZE}})
254
+ return sorted(rows, key=lambda r: (r["due_date"], r["id"]))
255
+
256
+
257
+ @router.get("/path")
258
+ def path(request: Request, user: dict = Depends(learner)) -> dict:
259
+ here, uid = reads.of(request), user["id"]
260
+ here.prefetch(("progress_of", uid), ("last_answered", uid), ("daily_correct", uid), ("recent_skills", uid, RECENT), ("attempts_of", uid, "quiz"), ("curriculum",))
261
+ lessons, order, states = lesson_rows(request, user)
262
+ today = clock.school_day()
263
  daily = here.daily_correct(uid)
264
  return {"lessons": lessons, "behind": sum(1 for l in lessons if l["behind"]), "next": next_skill(order, states, here.recent_skills(uid, RECENT)),
265
  "today": today_progress(daily, today), "streak": streak(daily, today)}
mathly2/static/app.css CHANGED
@@ -478,3 +478,8 @@ ul.students li [dir="ltr"] { overflow-wrap: anywhere; min-width: 0; }
478
  .review-card.done { opacity: .75; border-color: var(--sky-gold); }
479
  .review-card .ex-text { margin: 4px 0 8px; }
480
  .review-group [hidden] { display: none; } /* the heading's flex layout would otherwise show it */
 
 
 
 
 
 
478
  .review-card.done { opacity: .75; border-color: var(--sky-gold); }
479
  .review-card .ex-text { margin: 4px 0 8px; }
480
  .review-group [hidden] { display: none; } /* the heading's flex layout would otherwise show it */
481
+ /* The lessons behind the curriculum, on the home */
482
+ .behind-list li { border-top: 1px solid var(--line); padding: 8px 0; }
483
+ .behind-list li:first-child { border-top: 0; }
484
+ .behind-list .late { color: var(--sky-danger); font-weight: 600; }
485
+ .behind-list .actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0; }
mathly2/static/home.js CHANGED
@@ -66,6 +66,16 @@ function assignmentsCard(all) {
66
  }).join("")}</ul></section>`;
67
  }
68
 
 
 
 
 
 
 
 
 
 
 
69
  /** The photo help card (STU-6): only for a class with the AI on; it also lists the student's latest requests with their state. */
70
  function aiCard(ai) {
71
  if (!ai?.on) return "";
@@ -84,7 +94,7 @@ function draw(view, data, assignments, ai) {
84
  view(`${tabs("home")}${backToSubjects()}
85
  ${first ? nearest(first, data.task, data.next_skill) : `<section class="card" id="no-test"><h2>הבית</h2>
86
  <p class="muted">המורה עוד לא קבע מבחן. בינתיים אפשר להמשיך במסלול.</p></section>`}
87
- ${assignmentsCard(assignments)}${aiCard(ai)}
88
  ${required.map(t => `<a class="required" href="#/sim/${t.id}" data-required="${t.id}"><b>חובה</b> מבחן סימולציה למבחן ב־${shortDate(t.date)}<span aria-hidden="true">‹</span></a>`).join("")}
89
  <p>${data.next_skill ? `<a class="primary-link" id="continue-path" href="#/skill/${encodeURIComponent(data.next_skill.id)}">להמשיך במסלול: ${fmt(data.next_skill.name)} <span aria-hidden="true">‹</span></a>`
90
  : `<a class="primary-link" id="continue-path" href="${PATH}">למסלול: כל הכוכבים פתוחים לתרגול <span aria-hidden="true">‹</span></a>`}</p>`);
 
66
  }).join("")}</ul></section>`;
67
  }
68
 
69
+ /** Every lesson behind the curriculum, with the two ways to finish it (PTH-10): all its skills, or its quiz with at least 13 of 15. */
70
+ function behindCard(lessons) {
71
+ if (!lessons?.length) return "";
72
+ return `<section class="card" id="behind-card"><h2>שיעורים בפיגור</h2>
73
+ <ul class="plain behind-list">${lessons.map(l => `<li data-lesson="${esc(l.id)}"><p><b>${fmt(l.name)}</b><br><span class="late">⚠ היה אמור להסתיים ב־${shortDate(l.due_date)}</span></p>
74
+ <p class="actions">${l.next_skill ? `<a class="button" data-way="skills" href="#/skill/${encodeURIComponent(l.next_skill.id)}">לסיים את המיומנויות: ${l.mastered} מתוך ${l.total}</a>` : ""}
75
+ <a class="button" data-way="quiz" href="#/quiz/${encodeURIComponent(l.id)}">${l.quiz.open ? "להמשיך את הבוחן" : "בוחן"}: לפחות ${l.quiz.pass_right} מתוך ${l.quiz.size}</a></p>
76
+ <p class="muted" data-best>${l.quiz.best === null ? "עוד לא נעשה בוחן" : `הציון הטוב ביותר בבוחן: ${l.quiz.best}`}</p></li>`).join("")}</ul></section>`;
77
+ }
78
+
79
  /** The photo help card (STU-6): only for a class with the AI on; it also lists the student's latest requests with their state. */
80
  function aiCard(ai) {
81
  if (!ai?.on) return "";
 
94
  view(`${tabs("home")}${backToSubjects()}
95
  ${first ? nearest(first, data.task, data.next_skill) : `<section class="card" id="no-test"><h2>הבית</h2>
96
  <p class="muted">המורה עוד לא קבע מבחן. בינתיים אפשר להמשיך במסלול.</p></section>`}
97
+ ${behindCard(data.behind)}${assignmentsCard(assignments)}${aiCard(ai)}
98
  ${required.map(t => `<a class="required" href="#/sim/${t.id}" data-required="${t.id}"><b>חובה</b> מבחן סימולציה למבחן ב־${shortDate(t.date)}<span aria-hidden="true">‹</span></a>`).join("")}
99
  <p>${data.next_skill ? `<a class="primary-link" id="continue-path" href="#/skill/${encodeURIComponent(data.next_skill.id)}">להמשיך במסלול: ${fmt(data.next_skill.name)} <span aria-hidden="true">‹</span></a>`
100
  : `<a class="primary-link" id="continue-path" href="${PATH}">למסלול: כל הכוכבים פתוחים לתרגול <span aria-hidden="true">‹</span></a>`}</p>`);
mathly2/tests/test_behind_home_api.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lessons behind the curriculum on the home (add-behind-lessons-to-home 1.1)."""
2
+ from datetime import timedelta
3
+
4
+ from mathly2 import clock
5
+ from mathly2.tests.test_quizzes_api import take
6
+ from mathly2.tests.test_skills_api import KID, KID_ID, MON, auth, raw, world # noqa: F401
7
+
8
+ STAMP = "2026-10-05T10:00:00+00:00"
9
+
10
+
11
+ def behind(client):
12
+ return client.get("/api/home", headers=auth()).json()["behind"]
13
+
14
+
15
+ def skills_of(bank, lesson):
16
+ return [s["id"] for s in bank.skills if s["unit"] == lesson]
17
+
18
+
19
+ def test_every_lesson_behind_is_listed_earliest_date_first_and_others_are_not(world):
20
+ client, store, bank = world
21
+ today = clock.school_day()
22
+ store.set_lesson_dates({"t": today - timedelta(days=2), "t2": today - timedelta(days=9), "k": today + timedelta(days=5), "i": today}) # t2 and t behind; k ahead; i is due today
23
+ got = behind(client)
24
+ assert [r["id"] for r in got] == ["t2", "t"] and got[0]["due_date"] == (today - timedelta(days=9)).isoformat()
25
+ assert all(r["name"] for r in got)
26
+ for s in skills_of(bank, "t2"):
27
+ store.save_progress(KID_ID, s, started_at=STAMP, mastered_at=STAMP)
28
+ assert [r["id"] for r in behind(client)] == ["t"] # completed: no longer listed
29
+
30
+
31
+ def test_a_lesson_without_a_date_is_never_listed_and_none_behind_means_an_empty_list(world):
32
+ client, store, bank = world
33
+ assert behind(client) == []
34
+ store.set_lesson_dates({"k": clock.school_day() + timedelta(days=3)})
35
+ assert behind(client) == []
36
+
37
+
38
+ def test_the_skills_option_says_how_far_and_opens_the_next_unmastered_skill(world):
39
+ client, store, bank = world
40
+ store.set_lesson_dates({"t": clock.school_day() - timedelta(days=1)})
41
+ skills = skills_of(bank, "t")
42
+ for s in skills[:3]:
43
+ store.save_progress(KID_ID, s, started_at=STAMP, mastered_at=STAMP)
44
+ row = behind(client)[0]
45
+ assert (row["mastered"], row["total"]) == (3, len(skills)) and row["next_skill"]["id"] == skills[3] and row["next_skill"]["name"]
46
+ store.save_progress(KID_ID, skills[1], mastered_at=None) # a skill not mastered in the middle comes first
47
+ assert behind(client)[0]["next_skill"]["id"] == skills[1]
48
+
49
+
50
+ def test_the_quiz_option_shows_the_best_grade_and_what_is_needed_and_a_pass_catches_the_lesson_up(world):
51
+ client, store, bank = world
52
+ store.set_lesson_dates({"t": clock.school_day() - timedelta(days=1)})
53
+ assert behind(client)[0]["quiz"] == {"best": None, "open": False, "pass_right": 13, "size": 15}
54
+ take(client, bank, "t", [True] * 12 + [False] * 3) # 80
55
+ quiz = behind(client)[0]["quiz"]
56
+ assert quiz["best"] == 80 and quiz["pass_right"] == 13 and quiz["size"] == 15
57
+ take(client, bank, "t", [True] * 13 + [False] * 2) # passed
58
+ assert behind(client) == []
59
+
60
+
61
+ def test_the_path_and_the_home_agree_on_what_is_behind(world):
62
+ client, store, bank = world
63
+ today = clock.school_day()
64
+ store.set_lesson_dates({"t": today - timedelta(days=4), "t2": today - timedelta(days=1)})
65
+ path = client.get("/api/path", headers=auth()).json()
66
+ assert {l["id"] for l in path["lessons"] if l["behind"]} == {r["id"] for r in behind(client)} == {"t", "t2"} and path["behind"] == 2
mathly2/tests/test_behind_home_browser.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The card of lessons behind the curriculum on the home (add-behind-lessons-to-home 2.1), in a real browser."""
2
+ from datetime import datetime, timedelta, timezone
3
+
4
+ import pytest
5
+
6
+ playwright = pytest.importorskip("playwright.sync_api")
7
+
8
+ from mathly2 import clock
9
+ from mathly2.tests.serve import running
10
+ from mathly2.tests.test_home_browser import KID_ID, PHONE, bank, browser, no_sideways_scroll, sign_in, to_home, world # noqa: F401
11
+
12
+ STAMP = "2026-10-05T10:00:00+00:00"
13
+
14
+
15
+ def seed(app, store, bank, lessons):
16
+ today = clock.school_day()
17
+ store.set_lesson_dates({lessons[0]: today - timedelta(days=2), lessons[1]: today - timedelta(days=9)})
18
+
19
+
20
+ def test_the_card_lists_the_lessons_behind_in_date_order_with_both_ways_to_finish(browser, bank):
21
+ app, store, klass, lessons = world(bank)
22
+ seed(app, store, bank, lessons)
23
+ skills = [s["id"] for s in bank.skills if s["unit"] == lessons[0]]
24
+ store.save_progress(KID_ID, skills[0], started_at=STAMP, mastered_at=STAMP)
25
+ with running(app) as url:
26
+ page = sign_in(browser, url)
27
+ to_home(page)
28
+ page.wait_for_selector("#behind-card")
29
+ rows = page.locator("#behind-card li")
30
+ assert rows.count() == 2 and rows.nth(0).get_attribute("data-lesson") == lessons[1] and rows.nth(1).get_attribute("data-lesson") == lessons[0] # the earlier date first
31
+ assert "היה אמור להסתיים ב־" in rows.nth(0).inner_text()
32
+ mine = page.locator(f"#behind-card li[data-lesson='{lessons[0]}']")
33
+ assert f"1 מתוך {len(skills)}" in mine.locator("[data-way=skills]").inner_text() and mine.locator("[data-way=skills]").get_attribute("href") == f"#/skill/{skills[1]}"
34
+ assert "לפחות 13 מתוך 15" in mine.locator("[data-way=quiz]").inner_text() and mine.locator("[data-way=quiz]").get_attribute("href") == f"#/quiz/{lessons[0]}"
35
+ assert "עוד לא נעשה בוחן" in mine.locator("[data-best]").inner_text()
36
+ no_sideways_scroll(page)
37
+ assert not page.errors
38
+
39
+
40
+ def test_the_best_quiz_grade_shows_and_the_links_open_the_skill_and_the_quiz(browser, bank):
41
+ app, store, klass, lessons = world(bank)
42
+ seed(app, store, bank, lessons)
43
+ q = store.create_attempt(KID_ID, "quiz", lessons[0], clock.now() - timedelta(hours=2), clock.now() - timedelta(hours=1), [])
44
+ store.save_attempt(q["id"], submitted_at=clock.now() - timedelta(hours=1), grade=80)
45
+ with running(app) as url:
46
+ page = sign_in(browser, url)
47
+ to_home(page)
48
+ page.wait_for_selector("#behind-card")
49
+ assert "הציון הטוב ביותר בבוחן: 80" in page.inner_text(f"#behind-card li[data-lesson='{lessons[0]}'] [data-best]")
50
+ page.click(f"#behind-card li[data-lesson='{lessons[1]}'] [data-way=skills]")
51
+ page.wait_for_selector(".mx-sheet")
52
+ assert page.evaluate("location.hash").startswith("#/skill/")
53
+
54
+
55
+ def test_no_card_when_no_lesson_is_behind_or_when_it_was_passed(browser, bank):
56
+ app, store, klass, lessons = world(bank)
57
+ store.set_lesson_dates({lessons[0]: clock.school_day() + timedelta(days=5)})
58
+ with running(app) as url:
59
+ page = sign_in(browser, url)
60
+ to_home(page)
61
+ page.wait_for_selector("#continue-path")
62
+ assert page.locator("#behind-card").count() == 0
63
+ app, store, klass, lessons = world(bank)
64
+ store.set_lesson_dates({lessons[0]: clock.school_day() - timedelta(days=1)})
65
+ q = store.create_attempt(KID_ID, "quiz", lessons[0], clock.now() - timedelta(hours=2), clock.now() - timedelta(hours=1), [])
66
+ store.save_attempt(q["id"], submitted_at=clock.now() - timedelta(hours=1), grade=87) # 13 of 15: passed
67
+ with running(app) as url:
68
+ page = sign_in(browser, url)
69
+ to_home(page)
70
+ page.wait_for_selector("#continue-path")
71
+ assert page.locator("#behind-card").count() == 0
openspec/changes/archive/2026-11-09-add-behind-lessons-to-home/.openspec.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ schema: spec-driven
2
+ created: 2026-11-09
openspec/changes/archive/2026-11-09-add-behind-lessons-to-home/proposal.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Proposal
2
+
3
+ ## Why
4
+
5
+ A student who is behind the curriculum sees it only as a small mark on the path. Being behind is critical, so the home should say which lessons are behind and offer the two ways to catch up.
6
+
7
+ ## What Changes
8
+
9
+ - **Home lists every lesson that is behind the curriculum** (its date has passed and it is not completed), the earliest date first, in a card "שיעורים בפיגור".
10
+ - **Each lesson offers two ways to finish it** (PTH-10): finish all its skills ("N מתוך M מיומנויות", opening the next unmastered skill), or pass its בוחן with at least 13 right out of 15 (opening the quiz, showing the best grade so far).
11
+ - A lesson that is completed, has no date or whose date is ahead is not listed, and with no lesson behind there is no card.
12
+
13
+ ## Capabilities
14
+
15
+ ### Modified Capabilities
16
+ - `student-home`: "Home shows only what is critical", and a new "Lessons behind on home".
17
+
18
+ ## Impact
19
+
20
+ - `GET /api/home` gains the behind lessons (the path's lesson facts are shared, not computed twice); `home.js`; PRD STU-1. No database change.
openspec/changes/archive/2026-11-09-add-behind-lessons-to-home/specs/student-home/spec.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## MODIFIED Requirements
2
+
3
+ ### Requirement: Home shows only what is critical
4
+ The home SHALL show only: the nearest test with its readiness and today's task, a required simulation, the lessons behind the curriculum, the new assignments (not handed in yet), and the button that continues the path. Other tests, assignments that were handed in or checked, and settings such as notifications SHALL NOT be on home; the other tests are reached from the test's card.
5
+
6
+ #### Scenario: Nothing critical
7
+ - **WHEN** a student has no upcoming test, no new assignment and no lesson behind
8
+ - **THEN** home shows only the button that continues the path
9
+
10
+ #### Scenario: Two tests
11
+ - **WHEN** a student has two upcoming tests
12
+ - **THEN** home shows the nearest one, and the other is listed on that test's card
13
+
14
+ ## ADDED Requirements
15
+
16
+ ### Requirement: Lessons behind on home
17
+ Home SHALL list every lesson behind the curriculum, the earliest date first, with its name and the date it should have been completed by. A completed lesson, a lesson with no date and a lesson whose date is ahead SHALL NOT be listed, and with none behind there SHALL be no such card.
18
+
19
+ #### Scenario: Two lessons behind
20
+ - **WHEN** two lessons have passed their dates and are not completed
21
+ - **THEN** home lists both, the one with the earlier date first
22
+
23
+ #### Scenario: Caught up
24
+ - **WHEN** a student passes the בוחן of a lesson that was behind
25
+ - **THEN** the lesson is no longer listed on home
26
+
27
+ #### Scenario: None behind
28
+ - **WHEN** no lesson is behind
29
+ - **THEN** home has no behind card
30
+
31
+ ### Requirement: Two ways to catch up
32
+ Each lesson listed as behind SHALL offer two ways to finish it: all its skills mastered, shown as "N מתוך M מיומנויות" and opening its next unmastered skill; or its בוחן passed with at least 13 right out of 15, opening the בוחן and showing the best grade so far.
33
+
34
+ #### Scenario: Skills
35
+ - **WHEN** a student has mastered 3 of 5 skills of a lesson that is behind
36
+ - **THEN** its row says "3 מתוך 5 מיומנויות" and the skills option opens the first skill not mastered
37
+
38
+ #### Scenario: The בוחן
39
+ - **WHEN** the student's best grade in that lesson's בוחן is 80
40
+ - **THEN** the row shows 80 and says that 13 right out of 15 is needed
openspec/changes/archive/2026-11-09-add-behind-lessons-to-home/tasks.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tasks
2
+
3
+ ## 1. The data
4
+
5
+ - [x] 1.1 Share the path's lesson facts with home and add the behind lessons to `GET /api/home` (name, date, mastered and total skills, the next unmastered skill, the בוחן's best grade and whether one is open), earliest date first; verify API tests for every scenario of "Lessons behind on home", and a timing test that home needs no more waits in a row than before
6
+
7
+ ## 2. The screen
8
+
9
+ - [x] 2.1 Show the card "שיעורים בפיגור" on home with the two options per lesson; verify Playwright tests: the card lists the lessons in date order with both options and the links open the next skill and the בוחן, the best grade and "13 מתוך 15" show, no card when none is behind, no sideways scrolling at 360px
10
+
11
+ ## 3. Wrap-up
12
+
13
+ - [x] 3.1 Update the PRD (STU-1); run the full suite and `openspec validate add-behind-lessons-to-home --strict`; verify both pass
openspec/specs/student-home/spec.md CHANGED
@@ -38,12 +38,38 @@ The student's screens (home, path, submissions "הגשות", grades, collection)
38
  - **THEN** the submissions screen opens
39
 
40
  ### Requirement: Home shows only what is critical
41
- The home SHALL show only: the nearest test with its readiness and today's task, a required simulation, the new assignments (not handed in yet), and the button that continues the path. Other tests, assignments that were handed in or checked, and settings such as notifications SHALL NOT be on home; the other tests are reached from the test's card.
42
 
43
  #### Scenario: Nothing critical
44
- - **WHEN** a student has no upcoming test and no new assignment
45
  - **THEN** home shows only the button that continues the path
46
 
47
  #### Scenario: Two tests
48
  - **WHEN** a student has two upcoming tests
49
  - **THEN** home shows the nearest one, and the other is listed on that test's card
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  - **THEN** the submissions screen opens
39
 
40
  ### Requirement: Home shows only what is critical
41
+ The home SHALL show only: the nearest test with its readiness and today's task, a required simulation, the lessons behind the curriculum, the new assignments (not handed in yet), and the button that continues the path. Other tests, assignments that were handed in or checked, and settings such as notifications SHALL NOT be on home; the other tests are reached from the test's card.
42
 
43
  #### Scenario: Nothing critical
44
+ - **WHEN** a student has no upcoming test, no new assignment and no lesson behind
45
  - **THEN** home shows only the button that continues the path
46
 
47
  #### Scenario: Two tests
48
  - **WHEN** a student has two upcoming tests
49
  - **THEN** home shows the nearest one, and the other is listed on that test's card
50
+
51
+ ### Requirement: Lessons behind on home
52
+ Home SHALL list every lesson behind the curriculum, the earliest date first, with its name and the date it should have been completed by. A completed lesson, a lesson with no date and a lesson whose date is ahead SHALL NOT be listed, and with none behind there SHALL be no such card.
53
+
54
+ #### Scenario: Two lessons behind
55
+ - **WHEN** two lessons have passed their dates and are not completed
56
+ - **THEN** home lists both, the one with the earlier date first
57
+
58
+ #### Scenario: Caught up
59
+ - **WHEN** a student passes the בוחן of a lesson that was behind
60
+ - **THEN** the lesson is no longer listed on home
61
+
62
+ #### Scenario: None behind
63
+ - **WHEN** no lesson is behind
64
+ - **THEN** home has no behind card
65
+
66
+ ### Requirement: Two ways to catch up
67
+ Each lesson listed as behind SHALL offer two ways to finish it: all its skills mastered, shown as "N מתוך M מיומנויות" and opening its next unmastered skill; or its בוחן passed with at least 13 right out of 15, opening the בוחן and showing the best grade so far.
68
+
69
+ #### Scenario: Skills
70
+ - **WHEN** a student has mastered 3 of 5 skills of a lesson that is behind
71
+ - **THEN** its row says "3 מתוך 5 מיומנויות" and the skills option opens the first skill not mastered
72
+
73
+ #### Scenario: The בוחן
74
+ - **WHEN** the student's best grade in that lesson's בוחן is 80
75
+ - **THEN** the row shows 80 and says that 13 right out of 15 is needed