Uh oh!
There was an error while loading. Please reload this page.
Gradebook Feature - #241
Conversation
…n grid Replace the gradient course cards with the V1b layout from the design handoff: solid course-color band, course code top-left in JetBrains Mono, Playfair letter grade bottom-left, and an overlapping-discs watermark. Footer percentage is now color-graded green→amber→rust→rose→crimson. Also: - storage_service: recognize Supabase's HTTP 400 + statusCode:"409" body as "bucket already exists" so startup stops warning on every restart. - localData: stub /api/gradebook/summary so the landing page works under NEXT_PUBLIC_LOCAL_MODE.
googleapiclient routes through httplib2, which ignores HTTPS_PROXY and
times out (WinError 10060) on dev machines and proxy-bound deployments.
Swap to a direct httpx GET and wrap in _fail_redirect("userinfo_fetch_failed")
to match the existing oauth_exchange_failed error pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Course detail page restructure - Collapse the masthead from a ~340px hero into a single identity row (kicker + name + letter+percent pill; letter scale lives in the pill's hover tooltip instead of burning vertical space). - Replace the side-by-side DistributionStrip + GradeProjector pair with a hero-scale GradeCompositionBar. Each category gets a weight-proportional slot whose sub-segments encode earned (solid), lost (faded), and still-reachable (hatched). Letter cutoffs overlay the bar at 60/70/80/90, and a "Now" pin marks current %. - Cursor-following tooltips on every sub-segment explain the math behind that region (Earned 18.5 of 20 pts, contributing 18.50% to final, etc.); the tooltip auto-flips when near viewport edges. - Single-column page below the masthead — Masthead -> Composition -> Assignments — so the assignment list (the actual data-entry surface) sits above the fold instead of buried under stat duplication. Drop-lowest grading policy - New course_categories.drop_lowest column (migration_gradebook_drops.sql, idempotent + non-negative CHECK). - gradebook_service.category_grade drops the N lowest graded items by earned/possible before averaging; new dropped_assignment_ids / all_dropped_ids helpers expose which IDs are currently excluded per category and across the whole course (returned on /courses/:id). - projectGrade re-runs the same drop logic for the floor (ungraded -> 0) and ceiling (ungraded -> full) scenarios, so projection respects drops in every direction; droppedAssignmentIds computes the same set client-side so optimistic grade edits update immediately. - EditWeightsModal gains a Drop column next to Weight. - AssignmentList mutes dropped rows (55% opacity + strikethrough) with an inline "dropped" chip, and each category header shows a "drops X/N" progress chip when the policy is active. Cosmetic - Course-card orb watermark seeds via min-distance rejection sampling (62px between disc centers) so the three discs never land near-coincident. Run backend/db/migration_gradebook_drops.sql in Supabase before deploying — the new SELECT on course_categories includes drop_lowest and PostgREST will 500 otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ve mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eral for curve_mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ettingsModal to course page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…improvements - Fix auth cookies (SECURE_COOKIES flag, SESSION_SECRET wiring frontend+backend) - Grade predictor panel with hypothetical scores and Raw/Curved toggle - Bell curve grading: apply_curve function, per-assignment and course policy - Category color palette expanded to 10 distinct hues - Category tab navigation above assignment list with hover states - Assignment row hover highlight extends left/right with clean dividers - Dropped and Curved chips on assignment rows - Fraction score display tightened (30/34 format) - Letter scale fixed to match backend full A+/A-/B+ 12-tier default - Float precision fix: round to 1dp before letter-scale comparisons - Composition bar instant update on Raw/Curved toggle - Drop policy renamed to N Drops - Course name no longer truncated
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend | 39e2197 | Commit Preview URL Branch Preview URL | Jun 22 2026, 03:35 AM |
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds bell-curve grading and per-category drop-lowest scoring to the gradebook service, routes, and UI. Introduces a Grade Predictor panel for hypothetical scoring. Integrates Gradescope sync via password, session-cookie, and BU SSO/Duo auth modes with credential storage and course-link management. Updates auth cookie security to be configurable and replaces Google userinfo fetching with ChangesGradebook Enhancements: Drop-Lowest, Bell Curve & Grade Predictor
Gradescope Sync Integration
Auth Secure-Cookie and Storage Response Handling
CI Lint and ESLint Suppression Cleanup
Sequence Diagram(s)sequenceDiagram
participant User
participant GradescopeSyncModal
participant ConnectedAccounts
participant GradescopeRoute as /api/gradescope
participant GradescopeService
participant GradescopeAPI as Gradescope.com
User->>ConnectedAccounts: Click "Connect"
ConnectedAccounts->>GradescopeSyncModal: open modal
User->>GradescopeSyncModal: Choose BU SSO mode
GradescopeSyncModal->>GradescopeRoute: POST /credentials/bu-sso
GradescopeRoute->>GradescopeService: login_via_bu_sso (asyncio.to_thread)
GradescopeService->>GradescopeAPI: Playwright BU Shibboleth + Duo flow
GradescopeAPI-->>GradescopeService: session cookies
GradescopeService-->>GradescopeRoute: {_gradescope_session, signed_token}
GradescopeRoute-->>GradescopeSyncModal: 200 OK
User->>GradescopeSyncModal: Select course + Sync
GradescopeSyncModal->>GradescopeRoute: POST /sync/{sapling_course_id}
GradescopeRoute->>GradescopeService: list_assignments
GradescopeService->>GradescopeAPI: scrape assignments
GradescopeAPI-->>GradescopeService: assignment list
GradescopeService-->>GradescopeRoute: normalized assignments
GradescopeRoute-->>GradescopeSyncModal: SyncResult {inserted, updated, skipped, failed}
GradescopeSyncModal-->>ConnectedAccounts: onSynced()
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| import requests | ||
| from bs4 import BeautifulSoup | ||
| from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL |
There was a problem hiding this comment.
🟠 A missing dependency takes down the whole app, not just this feature.requests, bs4, and gradescopeapi are imported unconditionally here (only Playwright is guarded below), and main.py:24 imports routes.gradescope unconditionally. If any of these newly-added deps isn't installed in a deploy target, import routes.gradescope raises ImportError during router mount and uvicorn never boots — all of /api goes down. Guard these imports the way Playwright already is, or make the router mount degrade gracefully.
| "id": str(uuid.uuid4()), | ||
| "user_id": user_id, | ||
| "course_id": sapling_course_id, | ||
| "category_id": None, # user can re-categorize later |
There was a problem hiding this comment.
🟠 Synced grades never affect the computed grade. Inserted assignments get category_id=None, but current_grade() only buckets assignments whose category_id matches a known category — so freshly-synced grades contribute nothing to the percent until the user manually categorizes each one. A successful sync visibly changes the grade by zero, which reads as a broken sync.
| graded.append((float(e) / float(p), float(p), str(aid))) | ||
| if not graded: | ||
| return [] | ||
| graded.sort(key=lambda x: (x[0], -x[1], x[2])) |
There was a problem hiding this comment.
🟠 Drop-lowest is computed on RAW scores, but the curved display drops by CURVED score.dropped_assignment_ids ranks by raw earned/possible, while the frontend projectGrade/curved path drops based on curved values and the 'dropped' badge (droppedAssignmentIds(..., data.assignments)) uses raw. Under a curve that reorders scores, the server and UI exclude different assignments, and the badge can mark one assignment dropped while the math drops another.
| by_cat[cid].append(a) | ||
| for c in cats: | ||
| c["category_grade"] = gradebook_service.category_grade(by_cat[c["id"]]) | ||
| c["category_grade"] = gradebook_service.category_grade( |
There was a problem hiding this comment.
🟠 Per-category grade is always raw, even for curved courses. This call passes no curve args (defaults to curve_mode='raw'), while the overall percent just below (:155) is curved. The per-category numbers shown to the student won't reconcile with the curved headline grade.
| FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000") | ||
| SESSION_SECRET = os.getenv("SESSION_SECRET", "") | ||
| _sc_env = os.getenv("SECURE_COOKIES") | ||
| SECURE_COOKIES: bool = _sc_env.lower() == "true" if _sc_env is not None else FRONTEND_URL.startswith("https://") |
There was a problem hiding this comment.
🟠 OAuth cookie Secure flag is no longer fail-closed.origin/main hardcoded secure=True; now it's secure=SECURE_COOKIES, which defaults to FRONTEND_URL.startswith('https://') when the env var is unset — and FRONTEND_URL itself defaults to http://localhost:3000. A TLS-fronted prod deploy that forgets to set FRONTEND_URL/SECURE_COOKIES will send the sapling_oauth cookie without Secure (used in auth.py:280/308/418/447). The frontend's secure: NODE_ENV==='production' for the session cookie (session/route.ts:101) is a second, independent mechanism that can disagree.
| try: | ||
| if gs_id in by_gs_id: | ||
| table("assignments").update( | ||
| record_write, |
There was a problem hiding this comment.
🟡 Re-sync clobbers user edits.record_write unconditionally rewrites title/due_date/source on every sync, discarding any manual rename or due-date fix the student made to a linked assignment. Sync should be authoritative only for the grade columns + the idempotency key (gradescope_assignment_id).
| points_earned: hyp.earned, | ||
| points_possible: hyp.possible > 0 ? hyp.possible : a.points_possible, | ||
| // Predictor override takes priority over stored assignment class stats | ||
| curve_class_mean: hyp.curveClassMean !== null ? hyp.curveClassMean : a.curve_class_mean, |
There was a problem hiding this comment.
🟡 Predictor can't clear a per-assignment curve override.hyp.curveClassMean !== null ? ... : a.curve_class_mean — when the user blanks the class-avg field, GradePredictorPanel emits null, so this falls back to the stored mean and the curve stays applied. (The panel itself reads with !== undefined, so the two layers disagree.) The 'what-if no curve' scenario is silently ignored.
| if percent >= float(tier["min"]): | ||
| if rounded >= float(tier["min"]): | ||
| return str(tier["letter"]) | ||
| return None |
There was a problem hiding this comment.
🟡 A custom letter scale without a 0-floor tier yields no letter for failing students.set_letter_scale doesn't require an F/min:0 tier, so a scale like [{90,A},{80,B},{70,C}] makes this return None for 55% — the UI shows 55% with letter —. tierFor in GradeProjector.tsx has the same gap. Consider falling back to the lowest tier instead of None.
| @@ -0,0 +1,34 @@ | |||
| -- Gradebook bell-curve grading: per-course curve policy + per-assignment class stats. | |||
There was a problem hiding this comment.
🔵 Duplicate migration tree. This file (and migration_gradebook_drops.sql, migration_gradescope.sql) is byte-identical to db/migrations/0021_gradebook_curve.sql (resp. 0019/0020). The runner only globs migrations/*.sql, so these loose copies never run and only exist to drift out of sync. Recommend deleting them and keeping the numbered sequence as the single source of truth.
| @@ -53,6 +53,9 @@ CREATE TABLE IF NOT EXISTS user_courses ( | |||
| nickname TEXT, | |||
There was a problem hiding this comment.
🔵 Editing an already-applied baseline migration is a no-op on existing DBs. Migrations are tracked by filename, so these added curve columns never execute on staging/prod — they exist there only because idempotent 0021 also adds them. The edit also adds curve_* but not drop_lowest or gradescope_assignment_id, so anything deriving schema from the baseline alone (test fixtures, archived schema) gets tables missing those columns. Let 0019-0021 own the new columns; don't edit applied baselines.
| * Return a GradedAssignment with points_earned replaced by the curved value. | ||
| * Returns the original assignment unchanged if it has no curve data. | ||
| */ | ||
| export function applyCurveToAssignment( |
There was a problem hiding this comment.
🔵 Grade math is duplicated across four implementations (this file, GradeProjector.tsx, Course.tsx, and the Python gradebook_service.py), plus DEFAULT_SCALE/tierFor and the ?? 0.83/?? 0.10 policy are copy-pasted in ~5 spots. This duplication is the root cause of the divergences in the curve/drop findings. Only the predictor genuinely needs client-side compute; the plain 'curved' display could consume the server's percent/category_grade instead of re-deriving them.
AndresL230
commented
Jun 23, 2026
Review summary — Gradebook FeatureReviewed at high recall (correctness + cleanup/altitude). 14 inline comments posted on the diff; this comment covers the overview, one finding that doesn't map to a changed line, and what I checked that came back clean. Top asks before merge
Additional finding (no single changed line to attach to)🔴 The course-list endpoint ignores Lower-priority notes
Verified clean (so you don't re-check these)
🤖 Generated with Claude Code |
- gradescope_service: guard gradescopeapi imports with try/except to prevent boot crash when package is not installed; add _require_gradescopeapi() guard to login, login_with_cookies, list_student_courses, list_assignments - gradescope route: stop wiping points_earned/points_possible to NULL on re-sync when Gradescope returns no grade for an assignment - gradescope route: return generic 500 message from _load_creds instead of leaking raw decrypt exception text to the client - gradescope sync: auto-match new assignments to existing Sapling categories by checking if the category name appears in the assignment title - gradebook summary: fetch and pass curve_mode/curve_avg_target/curve_sd_delta to current_grade so the landing card grade matches the course detail for curved courses - gradebook course: pass curve args to category_grade for per-category grade display so it reflects the curve setting, not always raw - Course.tsx predictor: remove ?? 0.83 / ?? 0.10 curve defaults so the predictor only curves when both params are explicitly configured, matching backend behaviour
These three files were byte-identical to the canonical numbered migrations/0019, 0020, 0021. The migrate.py runner only scans migrations/, so the flat copies are dead and risk a hand-run reviewer re-applying the same DDL.
dropAndSum sorted by score only, so on tied score ratios the predictor could drop a different assignment than droppedAssignmentIds() and the server. Apply the same secondary 'points_possible desc' / id-asc tie-break so the projected number agrees with the dropped badge and the server grade in tie edge cases.
…stency Baseline already carried the curve_* columns but omitted course_categories.drop_lowest, assignments.gradescope_assignment_id, and the gradescope_credentials / gradescope_course_links tables. Fold all of them in so a fresh install's baseline matches the same feature set the curve columns implied, instead of a partial mix. Migrations 0019/0020 still run after baseline and remain idempotent (IF NOT EXISTS).
AndresL230
commented
Jun 25, 2026
Superseded by the DB modular redesign (#279): the gradebook is rebuilt enrollment-keyed ( |
AndresL230
commented
Jun 27, 2026
Reopening. This was closed as superseded by the DB modular redesign (#279), but that was over-broad: the redesign only re-keyed the gradebook schema, it did not rebuild the Gradescope import integration (BU SSO sync, |
- Revert category_grade to points-weighted (total_earned/total_possible);
update tests to match and add explicit higher-point-weight test
- Fix GradeProjector: return null when nothing is graded (was showing
Now 0.0%) and mirror points-weighted math in dropAndSum
- Fix curvedAssignments in Course.tsx: skip curve when policy fields
are NULL instead of applying hardcoded 0.83/0.10 defaults
- Fix Landing.tsx: authenticated users see empty state on fetch
failure instead of fake SAMPLE_COURSES data
- Fix test_gradescope.py: add upsert to _table_factory mock; tighten
test_bu_sso_limited_after_3 and test_limit_is_per_user assertions
- Add CHECK (auth_mode IN ('password','cookies')) to migration 0020Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Description
Major Gradebook feature release. Adds a bell curve grading system, a grade predictor panel, category tab navigation, and a large set of UI polish across the course page.
Changes Made
apply_curvebackend function, per-assignment class stats (entered via assignment modal), course-level curve policy (avg target + SD delta), Raw/Curved toggle persisted per-course, curved score chip on assignment rowsisPredictedvisual mode,computeCurrentGrademirrors backend logic exactlySECURE_COOKIESenv flag,SESSION_SECRETwired to frontend,secure=falseon localhost for OAuth cookiesRelated Issues
Closes #
Testing
Screenshots (if applicable)
Notes for Reviewers
Schema changes ship as ordered migrations (
backend/db/migrations/0019–0021) applied by the migration runner (backend/db/migrate.py --apply, run frombackend/) — no manual Supabase SQL-editor steps required. The runner is idempotent and records applied versions inschema_migrations. The migrations cover the bell-curve columns (assignments.curve_*,user_courses.curve_*),course_categories.drop_lowest, and the Gradescope tables/columns.(The previous manual
ALTER TABLEblock here was stale — it omitteddrop_lowestand the Gradescope schema — and has been removed in favor of the runner.)Summary by CodeRabbit
New Features
Bug Fixes