Skip to content

Add instructor read-only "view as student" mode#779

Open
jon-bell wants to merge 14 commits into
stagingfrom
claude/instructor-student-view-DmoUl
Open

Add instructor read-only "view as student" mode#779
jon-bell wants to merge 14 commits into
stagingfrom
claude/instructor-student-view-DmoUl

Conversation

@jon-bell
Copy link
Copy Markdown
Contributor

@jon-bell jon-bell commented May 22, 2026

Summary

Lets an instructor see the course exactly as a specific student does — their dashboard, assignments, submissions, grades, discussion, and office hours — scoped to that student and read-only.

  • Effective identity via a per-course cookie (view_as_<course_id>), resolved identically on the server and client so role-branching pages and client context agree. The override only applies when the real user is an instructor for the course and the cookie names a non-disabled student in it. Auth/RLS identity is unchanged — the override is purely presentation/scoping.
    • Server: new getEffectiveCourseIdentity() in lib/ssrUtils.ts, used by app/course/[course_id]/layout.tsx (controller scoping + prefetch) and app/course/[course_id]/page.tsx (dashboard selection).
    • Client: ClassProfileProvider (hooks/useClassProfiles.tsx) overrides role/profile and exposes isViewingAsStudent, isReadOnly, realRole, enterViewAs, exitViewAs, plus a new useIsReadOnly() hook.
  • Entry point: a "View as this student" action on the enrollments roster (enrollmentsTable.tsx), instructors only.
  • Banner: a sticky read-only banner with an "Exit student view" button (components/course/view-as-banner.tsx).
  • Read-only gating on the key student write surfaces (help request widget, finalize-submission-early, submission line comments, new discussion thread, discussion reply/like/edit/answer, regrade-request dialogs). RLS is the backstop — a write attempted while authenticated as the instructor against the student's profile is rejected by authorizeforprofile.
  • Realtime works unchanged: in view-as mode the controllers run with role="student" and subscribe to the student's channels. check_realtime_authorization already authorizes an instructor on class:<id>:students (via authorizeforclass) and class:<id>:user:<studentProfileId> (via authorizeforclassgrader), so the read-only view is live. No schema/policy changes.

Test plan

  • New E2E spec tests/e2e/view-as-student.spec.ts: instructor enters view-as from the roster, sees the banner + student-scoped view, asserts a successful realtime subscription to the student's channel (console capture), and exits cleanly.
  • Manual: log in as an instructor → /course/<id>/manage/course/enrollments → "View as this student" → confirm banner, student nav, student dashboard, student submission + grades, live updates, and that write surfaces are disabled/hidden. "Exit student view" restores the instructor view; reload persists state.
  • npx tsc --noEmit clean for changed files; eslint clean; npm run build succeeds.

https://claude.ai/code/session_01BMhotQB8DyrrEgTY6uo85H


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Instructors can “View as student” (read-only) from the enrollments roster; persistent banner shows active view-as with an exit action.
  • UI/UX Improvements

    • Interactive controls (posting, replying, liking, editing, regrading, line comments, help widget, feedback editing, finalize/submission actions, survey/submit buttons) are hidden or disabled in read-only/view-as mode and button labels update accordingly.
  • Tests

    • Added e2e coverage for view-as-student flows, UI changes, and realtime subscriptions.
  • Chores

    • Server-side support and DB migration enable secure view-as student identity and student-assignment access.

Review Change Stack

Lets instructors see the course exactly as a specific student does — scoped
to that student's dashboard, assignments, submissions, grades, discussion,
and office hours — without being able to make changes.

The active target is stored in a per-course cookie resolved identically on
the server (role-branching layout/landing pages via getEffectiveCourseIdentity)
and on the client (ClassProfileProvider overrides role/profile and exposes
isViewingAsStudent / isReadOnly / enter/exit). Realtime works unchanged: an
instructor is already authorized on the student's broadcast channels. Key
student write surfaces are gated on isReadOnly, with RLS as the backstop.
Entry point is a "View as this student" action on the enrollments roster.

https://claude.ai/code/session_01BMhotQB8DyrrEgTY6uo85H
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Warning

Rate limit exceeded

@jon-bell has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minute and 48 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 654e6d0a-53c3-4077-8525-bc4e62b12aa8

📥 Commits

Reviewing files that changed from the base of the PR and between a99befc and a190cbc.

📒 Files selected for processing (29)
  • app/course/[course_id]/assignments/[assignment_id]/layout.tsx
  • app/course/[course_id]/assignments/[assignment_id]/submissions/[submissions_id]/layout.tsx
  • app/course/[course_id]/assignments/[assignment_id]/submissions/layout.tsx
  • app/course/[course_id]/assignments/page.tsx
  • app/course/[course_id]/discussion/[root_id]/page.tsx
  • app/course/[course_id]/discussion/discussion_thread.tsx
  • app/course/[course_id]/dynamicCourseNav.tsx
  • app/course/[course_id]/manage/course/lab-roster/page.tsx
  • app/course/[course_id]/manage/layout.tsx
  • app/course/[course_id]/manage/student/[student_id]/page.tsx
  • app/course/[course_id]/office-hours/page.tsx
  • app/course/[course_id]/office-hours/search/page.tsx
  • components/help-queue/floating-help-request-widget.tsx
  • components/help-queue/help-request-chat.tsx
  • components/ui/student-summary.tsx
  • hooks/useActiveHelpRequest.tsx
  • hooks/useClassProfiles.tsx
  • hooks/useCourseController.tsx
  • hooks/useDiscussionThreadRootController.tsx
  • hooks/useGlobalSearchIndex.tsx
  • hooks/useGradebook.tsx
  • hooks/useGradebookWhatIf.tsx
  • hooks/useOfficeHoursRealtime.tsx
  • hooks/useQueueData.ts
  • hooks/useViewAsStudentDataMask.ts
  • lib/viewAsStudentDataMask.ts
  • supabase/functions/_shared/SupabaseTypes.d.ts
  • tests/e2e/view-as-student.spec.ts
  • utils/supabase/SupabaseTypes.d.ts

Walkthrough

Adds per-course "view as student" (cookie + SSR effective identity), expands ClassProfileProvider with read-only state and useIsReadOnly, renders a ViewAsBanner, adds an enrollments "View as" button, migrates the assignments dashboard to a SECURITY DEFINER RPC, gates many interactive components for read-only, and adds e2e tests.

Changes

Instructor View-as-Student Mode

Layer / File(s) Summary
Cookie helpers & server identity
lib/viewAs.ts, lib/ssrUtils.ts, supabase/migrations/*
Adds per-course cookie helpers and getEffectiveCourseIdentity that resolves a real identity and optionally overrides it for instructors using a view-as cookie; updates migrations to replace the view with a security-definer RPC and adjust visibility logic.
Client ClassProfileProvider and hooks
hooks/useClassProfiles.tsx
Context expanded to expose isViewingAsStudent, isReadOnly, realRole/realPrivateProfileId, viewAsProfileName, and enterViewAs/exitViewAs; new useIsReadOnly() hook provided.
Layout, pages, and ViewAsBanner
app/course/[course_id]/layout.tsx, app/course/[course_id]/page.tsx, components/course/view-as-banner.tsx
Server-side pages/layouts use getEffectiveCourseIdentity; layout renders a sticky ViewAsBanner when viewing as a student and adjusts prefetch behavior.
Enrollments table: enter view-as action
app/course/[course_id]/manage/course/enrollments/enrollmentsTable.tsx
Adds an eye IconButton for instructors to enter view-as for student rows; click sets cookie and navigates to activate student view.
Read-only UI gating across components
components/ui/*, components/help-queue/*, components/submission-results/*, app/course/[course_id]/discussion/*, app/course/[course_id]/assignments/*, app/course/[course_id]/surveys/*
Multiple components now check isReadOnly and either disable interactive controls or return null to prevent actions while in view-as read-only mode.
Assignments dashboard: RPC and client fetch
app/course/[course_id]/assignments/page.tsx, hooks/useGradebookWhatIf.tsx, supabase/migrations/*
Replaces client-side view queries with get_assignments_for_student_dashboard SECURITY DEFINER RPC, updates consumed types, and adds client-side useEffect-based fetching with cancellation handling.
Discussion creation and thread updates
app/course/[course_id]/discussion/new/page.tsx, app/course/[course_id]/discussion/discussion_thread.tsx
Discussion new page disables form when read-only and thread reply/actions hide when read-only.
UI dialogs and forms
components/ui/request-regrade-dialog.tsx, components/ui/RequestRegradeForCheckDialog.tsx, components/ui/line-comments-form.tsx, components/submission-results/HintFeedbackForm.tsx, app/course/[course_id]/assignments/[assignment_id]/finalizeSubmissionEarly.tsx
Dialogs, comment/feedback forms, and action buttons early-return, hide, or disable when isReadOnly.
Help queue widget
components/help-queue/floating-help-request-widget.tsx
Widget suppresses rendering when in read-only student view.
End-to-end tests
tests/e2e/view-as-student.spec.ts
E2E coverage for instructor view-as flows, banner, read-only enforcement, cookie activation, and assignments visibility.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

backend, testing, database

"A teacher walks the student's view,
Cookies tuck the change in place,
Hooks keep hands off every form,
Banners show the gentle face,
Tests ensure the quiet grace."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add instructor read-only "view as student" mode' directly summarizes the main feature being introduced—enabling instructors to view courses in read-only student mode. It is concise, specific, and clearly indicates the primary change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jon-bell jon-bell changed the base branch from main to staging May 22, 2026 02:16
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/e2e/view-as-student.spec.ts (1)

2-4: ⚡ Quick win

Use root alias imports instead of relative imports.

Switch ../global-setup and ./TestingUtils to @/* aliases for consistency with project import rules.

♻️ Proposed change
 import { Course } from "`@/utils/supabase/DatabaseTypes`";
-import { test, expect } from "../global-setup";
+import { test, expect } from "`@/tests/global-setup`";
 import dotenv from "dotenv";
-import { createClass, createUsersInClass, loginAsUser, TestingUser } from "./TestingUtils";
+import { createClass, createUsersInClass, loginAsUser, TestingUser } from "`@/tests/e2e/TestingUtils`";

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Use @/* path alias to reference files from project root".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/view-as-student.spec.ts` around lines 2 - 4, Replace the relative
imports at the top of the test file with root-alias imports: change the module
specifiers for the test harness import (currently "../global-setup") and the
utilities import (currently "./TestingUtils") to use the project root alias
(@"...") so that the imports still expose the same symbols (test, expect from
global-setup; createClass, createUsersInClass, loginAsUser, TestingUser from
TestingUtils); update only the import paths to the `@/` equivalents to conform to
the project's path-alias rules.
lib/viewAs.ts (1)

24-28: 💤 Low value

Consider adding Secure flag for production environments.

The cookie is set without the Secure flag, which means it could be transmitted over unencrypted HTTP connections. While the cookie only contains a profile ID (not credentials) and the feature is gated to authenticated instructors, adding Secure is a defense-in-depth measure for production.

A common pattern is to conditionally add it based on the environment:

🔒 Proposed conditional Secure flag
 export function setViewAsCookie(courseId: number | string, profileId: string): void {
   if (typeof document === "undefined") return;
-  document.cookie = `${viewAsCookieName(courseId)}=${encodeURIComponent(profileId)}; path=/; SameSite=Lax`;
+  const secure = window.location.protocol === "https:" ? "; Secure" : "";
+  document.cookie = `${viewAsCookieName(courseId)}=${encodeURIComponent(profileId)}; path=/; SameSite=Lax${secure}`;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/viewAs.ts` around lines 24 - 28, The setViewAsCookie function sets a
session cookie without the Secure flag; update setViewAsCookie (and where it
composes the cookie string) to append "; Secure" when running in a
production/HTTPS context (e.g., check location.protocol === "https:" or a
runtime prod flag) so the cookie is only sent over TLS in production while
preserving current behavior in non-HTTPS/dev environments; reference
setViewAsCookie and viewAsCookieName when making the change.
app/course/[course_id]/discussion/new/page.tsx (1)

54-55: ⚡ Quick win

Use formDisabled in the Ctrl/Cmd+Enter submit guard for consistency.

The shortcut gate still uses formBusy, so read-only state is not part of the same decision path as the submit button/fieldset.

Suggested fix
- if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && !formBusy) {
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && !formDisabled) {

Also applies to: 129-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/course/`[course_id]/discussion/new/page.tsx around lines 54 - 55, The
submit shortcut currently gates on formBusy but the UI uses formDisabled (const
formDisabled = formBusy || isReadOnly), so update the shortcut guard(s) to use
formDisabled instead of formBusy; locate the keydown/shortcut handler(s) (e.g.
the Ctrl/Cmd+Enter submit guard in the component, any references around the
submit handler such as handleSubmit/onKeyDown or submitOnShortcut) and replace
checks like "if (formBusy) return" with "if (formDisabled) return" so read-only
state is respected consistently (also apply the same change to the other
occurrence referenced around lines 129-133).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/course/`[course_id]/manage/course/enrollments/enrollmentsTable.tsx:
- Around line 809-822: The "View as this student" icon currently appears for
dropped/disabled enrollments; update the render condition to also require the
enrollment be active by checking the enrollment status/disabled flag before
showing the control—e.g., only render when realRole === "instructor",
userRoleEntry.role === "student", studentProfileId is present, and the
enrollment is not disabled (check userRoleEntry.disabled !== true and/or
userRoleEntry.status !== "dropped") so that enterViewAs(studentProfileId) is
only callable for active students.

---

Nitpick comments:
In `@app/course/`[course_id]/discussion/new/page.tsx:
- Around line 54-55: The submit shortcut currently gates on formBusy but the UI
uses formDisabled (const formDisabled = formBusy || isReadOnly), so update the
shortcut guard(s) to use formDisabled instead of formBusy; locate the
keydown/shortcut handler(s) (e.g. the Ctrl/Cmd+Enter submit guard in the
component, any references around the submit handler such as
handleSubmit/onKeyDown or submitOnShortcut) and replace checks like "if
(formBusy) return" with "if (formDisabled) return" so read-only state is
respected consistently (also apply the same change to the other occurrence
referenced around lines 129-133).

In `@lib/viewAs.ts`:
- Around line 24-28: The setViewAsCookie function sets a session cookie without
the Secure flag; update setViewAsCookie (and where it composes the cookie
string) to append "; Secure" when running in a production/HTTPS context (e.g.,
check location.protocol === "https:" or a runtime prod flag) so the cookie is
only sent over TLS in production while preserving current behavior in
non-HTTPS/dev environments; reference setViewAsCookie and viewAsCookieName when
making the change.

In `@tests/e2e/view-as-student.spec.ts`:
- Around line 2-4: Replace the relative imports at the top of the test file with
root-alias imports: change the module specifiers for the test harness import
(currently "../global-setup") and the utilities import (currently
"./TestingUtils") to use the project root alias (@"...") so that the imports
still expose the same symbols (test, expect from global-setup; createClass,
createUsersInClass, loginAsUser, TestingUser from TestingUtils); update only the
import paths to the `@/` equivalents to conform to the project's path-alias rules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 55a2f0b3-43ce-4aa0-b26e-f63ec4d839d4

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec8ad7 and 065895e.

📒 Files selected for processing (15)
  • app/course/[course_id]/assignments/[assignment_id]/finalizeSubmissionEarly.tsx
  • app/course/[course_id]/discussion/discussion_thread.tsx
  • app/course/[course_id]/discussion/new/page.tsx
  • app/course/[course_id]/layout.tsx
  • app/course/[course_id]/manage/course/enrollments/enrollmentsTable.tsx
  • app/course/[course_id]/page.tsx
  • components/course/view-as-banner.tsx
  • components/help-queue/floating-help-request-widget.tsx
  • components/ui/RequestRegradeForCheckDialog.tsx
  • components/ui/line-comments-form.tsx
  • components/ui/request-regrade-dialog.tsx
  • hooks/useClassProfiles.tsx
  • lib/ssrUtils.ts
  • lib/viewAs.ts
  • tests/e2e/view-as-student.spec.ts

Comment thread app/course/[course_id]/manage/course/enrollments/enrollmentsTable.tsx Outdated
@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented May 22, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ⚠️ Changes detected (Review) 41 changed, 4 added May 22, 2026, 8:37 PM

claude and others added 3 commits May 22, 2026 02:42
The "View as this student" roster control is a clickable icon (svg with
aria-label), consistent with the other row actions, so it has no button role.
Select it via getByLabel instead.

https://claude.ai/code/session_01BMhotQB8DyrrEgTY6uo85H
…verage

- Make the roster "View as this student" control a real, keyboard-operable
  IconButton (it was a clickable <svg> icon). The label-based selector from the
  prior commit still matches it.
- Extend the read-only guarantee to two student-data write surfaces still
  reachable in view-as mode: survey response submission/editing and Feedbot hint
  feedback. (Office-hours help-chat replies remain a documented follow-up; the
  help-request creation widget is already gated.)
- E2E: add coverage for write surfaces being read-only in view-as mode, and for a
  non-instructor's forged view_as cookie being ignored (server + client guards).
- E2E: fix a router.refresh()/goto navigation race on exit (flaky on WebKit) by
  waiting for the instructor view to be restored before navigating.

Verified locally: full view-as-student spec passes on chromium + webkit; tsc,
ESLint, and Prettier clean for the changed files; production build succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use the repo's visualScreenshot() helper (Argos-backed, with the standard
stabilization) to capture one screenshot per new test:
- "View as student - read-only discussion form" (banner + disabled compose form)
- "View as student - forged cookie ignored for non-instructor" (no banner)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hooks/useClassProfiles.tsx (1)

179-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't treat “view-as role still loading” as “not viewing as.”

When a valid course cookie is present, this lookup is async. Until it resolves, viewAsRole stays null, so the provider renders the real instructor context for one paint and only then flips to read-only student mode. That briefly re-enables instructor UI/write gates and can recreate the controller churn this change is trying to avoid.

🩹 Proposed fix
   const [viewAsProfileId, setViewAsProfileId] = useState<string | null>(null);
   const [viewAsRole, setViewAsRole] = useState<UserRoleWithClassAndUser | null>(null);
+  const [isResolvingViewAs, setIsResolvingViewAs] = useState(false);
@@
   useEffect(() => {
     const isInstructor = realMyRole?.role === "instructor";
     if (!isInstructor || !viewAsProfileId || !course_id) {
       setViewAsRole(null);
+      setIsResolvingViewAs(false);
       return;
     }
     let cancelled = false;
     const supabase = createClient();
+    setIsResolvingViewAs(true);
     (async () => {
       const { data, error } = await supabase
         .from("user_roles")
@@
         .eq("disabled", false)
         .single();
       if (cancelled) return;
       setViewAsRole(error || !data ? null : (data as UserRoleWithClassAndUser));
+      setIsResolvingViewAs(false);
     })();
     return () => {
       cancelled = true;
     };
   }, [realMyRole?.role, viewAsProfileId, course_id]);
@@
-  if (isLoading) {
+  if (isLoading || isResolvingViewAs) {
     return <Skeleton height="100px" width="100%" />;
   }

Also applies to: 295-308

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useClassProfiles.tsx` around lines 179 - 205, The effect currently sets
viewAsRole to null immediately which treats "still loading" as "not viewing-as"
and briefly re-enables instructor UI; change the logic in the useEffect that
fetches the user_roles (the async IIFE inside useEffect, and the parallel block
around lines 295-308) to preserve the previous viewAsRole while the fetch is
pending by introducing a loading sentinel or a separate loading flag (e.g.,
viewAsRoleLoading) instead of setting setViewAsRole(null) at the start; only set
viewAsRole to null when the fetch completes with error/no data or when the
prerequisites (realMyRole?.role !== "instructor" or !viewAsProfileId or
!course_id) truly indicate no view-as, and continue to honor the cancelled flag
before applying the final setViewAsRole result.
🧹 Nitpick comments (1)
tests/e2e/view-as-student.spec.ts (1)

4-5: ⚡ Quick win

Use the repo root alias for these new test helpers.

These new relative imports diverge from the repo-wide TS import rule. Please switch them to @/* paths for consistency.

♻️ Proposed fix
-import { createClass, createUsersInClass, insertAssignment, loginAsUser, TestingUser } from "./TestingUtils";
-import { visualScreenshot } from "./VisualTestUtils";
+import {
+  createClass,
+  createUsersInClass,
+  insertAssignment,
+  loginAsUser,
+  TestingUser
+} from "`@/tests/e2e/TestingUtils`";
+import { visualScreenshot } from "`@/tests/e2e/VisualTestUtils`";

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use @/* path alias to reference files from project root

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/view-as-student.spec.ts` around lines 4 - 5, Replace the relative
test helper imports with the repository root alias: update the import statements
that bring in createClass, createUsersInClass, insertAssignment, loginAsUser,
TestingUser (from "./TestingUtils") and visualScreenshot (from
"./VisualTestUtils") to use the "`@/`..." path alias instead so they follow the
project TS import rule; ensure the alias paths point to the same modules and run
type-check/tests to verify the new imports resolve correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/migrations/20260522120000_view_as_student_dashboard_visibility.sql`:
- Around line 343-346: Change the overly-broad GRANTs on the view
assignments_for_student_dashboard so public-facing roles only get read access:
replace the two GRANT ALL lines for anon and authenticated with GRANT SELECT ON
TABLE public.assignments_for_student_dashboard TO anon; and GRANT SELECT ON
TABLE public.assignments_for_student_dashboard TO authenticated; (leave
service_role privileges as-is if it needs full access, or explicitly grant
appropriate higher privileges to service_role if intended).

---

Outside diff comments:
In `@hooks/useClassProfiles.tsx`:
- Around line 179-205: The effect currently sets viewAsRole to null immediately
which treats "still loading" as "not viewing-as" and briefly re-enables
instructor UI; change the logic in the useEffect that fetches the user_roles
(the async IIFE inside useEffect, and the parallel block around lines 295-308)
to preserve the previous viewAsRole while the fetch is pending by introducing a
loading sentinel or a separate loading flag (e.g., viewAsRoleLoading) instead of
setting setViewAsRole(null) at the start; only set viewAsRole to null when the
fetch completes with error/no data or when the prerequisites (realMyRole?.role
!== "instructor" or !viewAsProfileId or !course_id) truly indicate no view-as,
and continue to honor the cancelled flag before applying the final setViewAsRole
result.

---

Nitpick comments:
In `@tests/e2e/view-as-student.spec.ts`:
- Around line 4-5: Replace the relative test helper imports with the repository
root alias: update the import statements that bring in createClass,
createUsersInClass, insertAssignment, loginAsUser, TestingUser (from
"./TestingUtils") and visualScreenshot (from "./VisualTestUtils") to use the
"`@/`..." path alias instead so they follow the project TS import rule; ensure the
alias paths point to the same modules and run type-check/tests to verify the new
imports resolve correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 49c65b6f-76a8-45aa-b4e6-05aefb92241a

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8f06f and e1f5e85.

📒 Files selected for processing (4)
  • app/course/[course_id]/assignments/page.tsx
  • hooks/useClassProfiles.tsx
  • supabase/migrations/20260522120000_view_as_student_dashboard_visibility.sql
  • tests/e2e/view-as-student.spec.ts

Comment thread supabase/migrations/20260522120000_view_as_student_dashboard_visibility.sql Outdated
Two reported bugs from running the feature end-to-end:

1. "TableController for table 'discussion_threads' is closed. Cannot call
   getById(...). This indicates a stale reference is being used." — fired
   immediately on entering view-as. enterViewAs/exitViewAs did a soft client
   transition (setViewAsProfileId + router.push + router.refresh()) which
   flipped the client identity while the existing CourseController was still
   being torn down on its new profile_id/role props; components that still
   held a reference to the closed discussion_threads TableController crashed
   on the next getById. Fix: do a full document navigation
   (window.location.assign) so the server recomputes the effective identity
   from the cookie and every controller is rebuilt cleanly.

2. The /course/<id>/assignments view was empty when viewing as a student.
   Two layered causes:
   a) `public.assignments_for_student_dashboard` is `security_invoker=true`
      and its `ur_students` CTE sourced from `public.user_privileges` with
      `WHERE user_id = auth.uid()`. The only SELECT policy on
      `public.user_privileges` is the inlinable `(user_id = auth.uid())`, so
      an instructor saw zero student rows. Fix in migration
      20260522120000_view_as_student_dashboard_visibility.sql: retarget the
      CTE to `public.user_roles`, whose existing SELECT policy already lets
      an instructor of a class read that class's student rows (its second
      OR branch checks instructor/grader privilege via a user_privileges
      sub-SELECT that is satisfied by the inlinable own-rows policy — no
      recursion). Add an `authorizeforinstructorofstudent(user_id)` branch
      to the CTE WHERE for the view-as path. `public.user_privileges` RLS
      stays strictly the inlinable own-rows policy — no function-based or
      otherwise opaque predicate is added to that hot table.
   b) The Student assignments page filtered the view by
      `student_user_id = useAuthState().user?.id`, i.e. the *signed-in*
      user — which is the instructor in view-as. Use
      `useClassProfiles().role.user_id` (the effective identity, which
      follows view-as) instead. No change for normal students.

E2E: 4th test seeds an assignment in beforeAll and asserts it is visible on
/course/<id>/assignments while viewing as the student. All 8 tests (4 specs
× chromium + webkit) pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jon-bell jon-bell force-pushed the claude/instructor-student-view-DmoUl branch from e1f5e85 to bfe13e9 Compare May 22, 2026 14:14
Replaces `public.assignments_for_student_dashboard` (security_invoker view) with
`public.get_assignments_for_student_dashboard(p_class_id, p_student_profile_id)`,
a SECURITY DEFINER plpgsql function. Per CLAUDE.md ("Prefer Postgres RPCs for
data operations") and Jon's explicit guidance:

  - Authorization is one explicit gate at the top of the function: the caller
    must be either the requested student themselves OR an instructor/grader of
    the requested class. Unauthorized calls raise 42501.
  - `public.user_privileges` RLS is untouched and stays strictly the inlinable
    `(user_id = auth.uid())` only. The recursion trap from earlier drafts
    (function-based or inline-EXISTS policies on user_privileges) is avoided
    entirely because the RPC bypasses RLS by definer privilege.
  - The CTE chain is bounded by the (p_class_id, p_student_profile_id) args,
    so every downstream join is O(assignments) regardless of class size.
  - Defensive cleanup at the top of the migration drops any leftover policies/
    helpers that earlier exploratory iterations may have applied locally
    (DROP ... IF EXISTS — no-op on fresh environments).

Frontend callers updated:
  - app/course/[course_id]/assignments/page.tsx: replaces
    `useList({ resource: "assignments_for_student_dashboard", ... })` with
    `supabase.rpc("get_assignments_for_student_dashboard", { p_class_id, p_student_profile_id })`.
  - hooks/useGradebookWhatIf.tsx: same switch. This also fixes a latent view-as
    bug here (the prior code filtered by `courseController.userId`, which is
    always the real signed-in user, so the gradebook what-if wouldn't have
    matched a viewed-as student's row either).

Types regenerated via `npm run client-local`; the view entry is gone from
SupabaseTypes.d.ts and the new RPC appears under Functions.

All 8 view-as E2E tests (4 specs × chromium + webkit) pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hooks/useClassProfiles.tsx (1)

171-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't publish the real instructor role before view-as resolves.

With an active view_as_<course_id> cookie, this provider still renders once with viewAsProfileId === null and again with viewAsRole === null until the client lookup finishes. During that window — or permanently if the lookup errors — consumers see the real instructor role, so read-only gates can briefly re-enable instructor/write UI even though SSR already scoped the page to the student. Please keep the provider loading until the cookie target is resolved, or hydrate the effective identity from the server instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useClassProfiles.tsx` around lines 171 - 205, The provider currently
exposes the real instructor role while the view-as cookie lookup is pending
because viewAsProfileId is immediately set to null; change the flow so the
lookup phase is distinguishable (e.g., use undefined as "resolving" and null as
"no target") and keep viewAsRole in a loading/resolving state until the Supabase
lookup finishes. Concretely: in the effect that calls getViewAsCookie
(referenced by viewAsProfileId and setViewAsProfileId) do not set
viewAsProfileId to null synchronously—set it to the cookie value when available
and keep it undefined while resolving; in the effect that fetches the student
role (the async useEffect that uses createClient(), .from("user_roles") and sets
setViewAsRole) set setViewAsRole to an explicit loading sentinel (undefined)
before the async call and only set a concrete UserRoleWithClassAndUser or null
after the request completes (or error), and ensure consumers treat undefined as
"still resolving" so realMyRole (realMyRole?.role) is not published until
view-as resolution finishes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/course/`[course_id]/assignments/page.tsx:
- Around line 96-118: The useEffect that calls createClient() and
supabase.rpc("get_assignments_for_student_dashboard", ...) only clears loading
inside the .then path, so rejected RPCs can leave isLoading true; update the
call to handle rejections and always clear loading by adding a .catch(...)
and/or a finally block that calls setIsLoading(false), ensure
setAssignments(null) is set on error paths (including the catch), and keep the
cancelled guard; reference the existing symbols useEffect, createClient,
supabase.rpc, get_assignments_for_student_dashboard, setIsLoading,
setAssignments, and cancelled when making the change.

In `@hooks/useGradebookWhatIf.tsx`:
- Around line 75-89: The RPC call in initializeGradebookGrades assigns
this._assignments asynchronously but recalculateDependentColumns() and notify()
run before the RPC completes; update the promise handling for
client.rpc("get_assignments_for_student_dashboard", ...) so that in the
.then(...) after setting this._assignments = data ?? [] you immediately call
this.recalculateDependentColumns() and this.notify(), and also add a .catch(...)
to handle RPC errors (log or surface the error instead of treating it as empty
data) so failures don't silently leave stale formulas using assignments(...).

In `@supabase/migrations/20260522120000_view_as_student_dashboard_visibility.sql`:
- Around line 40-49: The OR predicate currently calls
public.authorizeforinstructorofstudent(ur.user_id) which ignores ur.class_id and
can wrongly grant access across classes; update the predicate to be class-scoped
by either calling a class-aware RPC (e.g.,
public.authorizeforinstructorofstudent(ur.user_id, ur.class_id) or a new
public.authorizeforinstructorofstudent_in_class(ur_user_id, class_id)) or
replace the RPC with an existence check that ensures the caller is an instructor
for ur.class_id (e.g., an EXISTS on class_instructors linking auth.uid() to
ur.class_id and ur.user_id); adjust or add the RPC signature and its SECURITY
DEFINER wrapper as needed so the view only returns rows when the instructor
relationship is for the same class (refer to authorizeforinstructorofstudent and
the user_roles alias ur.class_id).

---

Outside diff comments:
In `@hooks/useClassProfiles.tsx`:
- Around line 171-205: The provider currently exposes the real instructor role
while the view-as cookie lookup is pending because viewAsProfileId is
immediately set to null; change the flow so the lookup phase is distinguishable
(e.g., use undefined as "resolving" and null as "no target") and keep viewAsRole
in a loading/resolving state until the Supabase lookup finishes. Concretely: in
the effect that calls getViewAsCookie (referenced by viewAsProfileId and
setViewAsProfileId) do not set viewAsProfileId to null synchronously—set it to
the cookie value when available and keep it undefined while resolving; in the
effect that fetches the student role (the async useEffect that uses
createClient(), .from("user_roles") and sets setViewAsRole) set setViewAsRole to
an explicit loading sentinel (undefined) before the async call and only set a
concrete UserRoleWithClassAndUser or null after the request completes (or
error), and ensure consumers treat undefined as "still resolving" so realMyRole
(realMyRole?.role) is not published until view-as resolution finishes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9a36eb97-e520-458f-a13a-e2fe20169366

📥 Commits

Reviewing files that changed from the base of the PR and between e1f5e85 and a99befc.

📒 Files selected for processing (8)
  • app/course/[course_id]/assignments/page.tsx
  • hooks/useClassProfiles.tsx
  • hooks/useGradebookWhatIf.tsx
  • supabase/functions/_shared/SupabaseTypes.d.ts
  • supabase/migrations/20260522120000_view_as_student_dashboard_visibility.sql
  • supabase/migrations/20260522130000_assignments_dashboard_rpc.sql
  • tests/e2e/view-as-student.spec.ts
  • utils/supabase/SupabaseTypes.d.ts

Comment thread app/course/[course_id]/assignments/page.tsx Outdated
Comment thread hooks/useGradebookWhatIf.tsx
Comment thread supabase/migrations/20260522120000_view_as_student_dashboard_visibility.sql Outdated
- Flatten redundant migration: delete the view-rewrite migration whose
  output is immediately dropped by the subsequent SECURITY DEFINER RPC
  migration. The RPC migration already includes the same defensive
  cleanup statements.
- useClassProfiles: keep the provider in a loading state while the
  view-as cookie target is being resolved. Initialize viewAsProfileId
  synchronously from the cookie and set an isResolvingViewAs flag so the
  real instructor identity is never published during the in-flight
  lookup (which previously briefly re-enabled instructor write UI).
- enrollmentsTable: hide "View as this student" for disabled enrollments.
- assignments/page.tsx and useGradebookWhatIf: wrap the dashboard RPC in
  try/catch (or .then with explicit error branch) so RPC rejections
  don't leave loading state stuck or formulas evaluating against [].
  After the RPC resolves in the what-if controller, rerun
  recalculateDependentColumns() and notify() so formulas using
  assignments(...) refresh.
- view-as-student.spec.ts: use the `@/` alias for repo-local imports;
  refresh the comment describing how the instructor sees the student's
  assignments now that the data path is an RPC, not a view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 22, 2026
jon-bell and others added 6 commits May 22, 2026 16:53
Move the view-as entry point from a small inline eye in the enrollments
roster onto the per-student summary page as a labeled orange button, and
restyle StudentSummaryTrigger as a clickable chip that wraps the name +
icon (with a hover state) at every existing call site so instructors
actually find their way to the summary from anywhere they see a student.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tograder/platform into claude/instructor-student-view-DmoUl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants