Uh oh!
There was an error while loading. Please reload this page.
feat(dashboard): group courses by semester with an Archive for past terms (#140) - #368
Conversation
/api/graph/{user_id}/courses has always returned the offering's term
label; the client type never declared it, so every consumer had to cast
through any to reach it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
termRankFromLabel mirrors the sort_key formula from migration 0019 so a label-only fallback orders identically to the server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors services/academics.py::current_term — today within [start_date, end_date], else the highest sort_key — so client and server never disagree about which semester is current. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ack (#140) Fixtures are the four terms seeded by migration 0019 verbatim, so a drift between this rule and the backend's shows up here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ordering keys on sort_key when the semesters payload is available and degrades to the label-derived rank otherwise. Courses with no term go to an 'Other' bucket rather than being dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only courses that rank strictly below the current term are archived. Undatable courses — and every course when /api/semesters gives us nothing — stay in the default list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ck (#140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/api/graph/{user_id}/courses emits `term`; the landing read
`(c as any).semester`, which is always undefined. `distinct` was
therefore always empty and every signed-in user silently fell through to
the hardcoded SAMPLE_SEMESTERS demo chips.
The sample chips are now the logged-out preview only — a signed-in user
with no terms gets their own empty state instead of another student's
fake grades.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…140) Chips now sort by sort_key from /api/semesters and default to the date-derived current term instead of whichever term the courses payload happened to list first. A failed semesters fetch degrades to the label-derived order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gives the dashboard archive somewhere to land: selecting an archived class opens that semester's gradebook rather than the current one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#140) Same markup, lifted so the current-term list, the archive and the graph overlay can all render a course line without a third copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve (#140) Current-term courses show by default; earlier terms collapse behind an Archive toggle, grouped by label most recent first. Also covers the mobile 'My Courses' tab, which renders the same panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The floating course key now lists only current-term courses and offers past terms as a compact Archive that deep-links into each semester's gradebook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Headings only appear once a student has courses in more than one term. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps the top-nav legend consistent with the courses key overlay, which already lists only the current semester. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion (#140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tech (#140) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reading `term` instead of `(c as any).semester` removed the only no-explicit-any in Landing.tsx, so its suppression entry no longer matches anything. eslint exits 2 on a stale suppression even with zero errors, which fails the CI lint gate — `main` exits 0, this branch did not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Warning Review limit reached
Next review available in:54 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (11)
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend-staging | c1d6f9b | Commit Preview URL Branch Preview URL | Jul 22 2026, 04:25 AM |
Jose-Gael-Cruz-Lopez
commented
Jul 21, 2026
@coderabbitai review |
✅ Action performedReview finished.
|
Empty commit to re-run CI after the frontend-staging Workers build-command config fix (npm run cf:build restored; the broken wrangler deploy --env staging skipped the OpenNext build). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes#140.
Groups enrolled classes by semester, defaults to the current term, and puts past terms behind an Archive.
The dependency was already satisfied
#140 is filed as "depends on the backend API issue", but that's stale — nothing was blocking:
GET /api/semestersalready exists and serves{id, term, year, label, start_date, end_date, sort_key}.GET /api/graph/{user_id}/coursesalready returns atermlabel per course; only the frontendEnrolledCourseinterface hadn't declared it.So this is frontend-only.
It also fixes a live bug
screens/Gradebook/Landing.tsxread(c as any).semesteroff the courses payload — but the API sendsterm. The lookup therefore always came back empty and fell through to the hardcoded demo constant:Every signed-in user was being shown those two fake semester chips instead of their own. The
as anyis exactly what let the field-name mismatch through the type checker.SAMPLE_SEMESTERSis now strictly the logged-out preview; a signed-in user with no terms gets their own empty state (atermsReadygate keeps that from hanging on the skeleton). Covered by tests.New
src/lib/semesters.ts— one tested source of truthcurrentTerm(semesters, today)mirrorsservices/academics.py::current_termexactly: the highest-sort_keyterm whose[start_date, end_date]contains today, falling back to the highestsort_keyoverall when today lands in a gap between terms.todayis injectable. Dates compare asYYYY-MM-DDstrings (lexicographic == chronological), and aDateis read in local time —toISOString()would shift the day either side of midnight.termRankFromLabel("Fall 2025") -> 20253, mirroring migration 0019'syear * 10 + ordinal. Only consulted when no realsort_keyis available;sort_keyalways wins.groupCoursesByTerm,courseTermLabels,partitionCurrentAndArchive.Degradation — the rule is "never hide a course"
If
/api/semestersfails or returns nothing, everything collapses to one flat ungrouped list with no Archive control and nothing hidden. Courses with an empty or unparseabletermare never dropped and never archived. Future-term courses are never archived. Only a course ranking strictly below the current term is archived — showing a course in the wrong bucket is recoverable, hiding it isn't.UI
The partition is applied to the "My courses" panel (which is also the mobile
coursestab), the graph legend chips, theCoursesKeyoverlay, andManageCoursesModal's list.CourseProgressRowwas extracted rather than copying that markup a third time. Archived rows route to/gradebook?semester=<label>, which the gradebook landing reads and preselects — so selecting an archived class lands in that semester's gradebook, per the acceptance criteria.The Calendar is untouched, per the explicit product decision in the issue.
Verification
npm testnpx tsc --noEmitnpx eslint . --max-warnings=-1npm run buildOne thing worth flagging because it nearly shipped broken: fixing the
as anyremoved the onlyno-explicit-anyinLanding.tsx, which left a stale entry ineslint-suppressions.json. ESLint exits 2 on a stale suppression even with zero errors —mainexits 0, this branch did not — so the lint gate would have failed CI. Pruned in its own commit.For the reviewer
src/lib/data.test.ts,src/components/DocumentUploadModal.test.tsx), forced by makingtermrequired onEnrolledCourse.Landingreads?semester=fromwindow.location.searchin auseStateinitializer rather thanuseSearchParams(), which would require aSuspenseboundary in the route shell this component doesn't own. It never reaches the rendered tree, so there's no hydration mismatch — but adding the boundary instead is a reasonable alternative.🤖 Generated with Claude Code