|
| 1 | +# SIS Deep-Link from Student Detail View — Design Spec |
| 2 | + |
| 3 | +**Date:** 2026-04-01 |
| 4 | +**Issue:** #78 |
| 5 | +**Scope:** Proof of concept / demo |
| 6 | +**Branch:** `feature/sis-deep-link` (from `main`) |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +Add a FERPA-compliant "Open in SIS" button to the student detail page that constructs a deep-link URL to the institution's Student Information System. Identity resolution happens server-side — the browser never receives the SIS student ID. This POC validates the architecture with sample data and a configurable demo URL. |
| 11 | + |
| 12 | +## Architecture |
| 13 | + |
| 14 | +``` |
| 15 | +Browser (student detail page) |
| 16 | + │ |
| 17 | + ├─ GET /api/students/[guid]/sis-link |
| 18 | + │ │ |
| 19 | + │ ├─ Role check (x-user-role header, admin/advisor/ir only) |
| 20 | + │ ├─ Query guid_sis_map table for sis_id |
| 21 | + │ ├─ Build URL: SIS_BASE_URL?SIS_ID_PARAM=<sis_id> |
| 22 | + │ ├─ Append audit log entry (GUID + role, never sis_id) |
| 23 | + │ └─ Return { url } or 404 |
| 24 | + │ |
| 25 | + └─ window.open(url, "_blank") |
| 26 | +``` |
| 27 | + |
| 28 | +The SIS ID never reaches the client. The audit log records access by GUID and role only. |
| 29 | + |
| 30 | +## 1. Database — `guid_sis_map` Table |
| 31 | + |
| 32 | +Table in the existing Postgres database: |
| 33 | + |
| 34 | +```sql |
| 35 | +CREATE TABLE guid_sis_map ( |
| 36 | + student_guid TEXT PRIMARY KEY, |
| 37 | + sis_id TEXT NOT NULL |
| 38 | +); |
| 39 | +``` |
| 40 | + |
| 41 | +A seed script picks ~20 random GUIDs from `student_level_with_predictions` and assigns fake SIS IDs (`BSC-100001` through `BSC-100020`). This demonstrates both the happy path (button works) and the fallback (no mapping → disabled button with tooltip). |
| 42 | + |
| 43 | +## 2. Environment Configuration |
| 44 | + |
| 45 | +Two server-only env vars in `.env.local` (no `NEXT_PUBLIC_` prefix): |
| 46 | + |
| 47 | +```env |
| 48 | +# SIS deep-link (leave blank to hide the button entirely) |
| 49 | +SIS_BASE_URL=https://sis-demo.example.com/students |
| 50 | +# Query param name the SIS expects (default: id) |
| 51 | +SIS_ID_PARAM=id |
| 52 | +``` |
| 53 | + |
| 54 | +When `SIS_BASE_URL` is unset, the API returns 404 and the UI hides the button — the feature is effectively disabled. |
| 55 | + |
| 56 | +## 3. API Route — `GET /api/students/[guid]/sis-link` |
| 57 | + |
| 58 | +**File:** `codebenders-dashboard/app/api/students/[guid]/sis-link/route.ts` |
| 59 | + |
| 60 | +**Behavior:** |
| 61 | + |
| 62 | +| Condition | Response | |
| 63 | +|-----------|----------| |
| 64 | +| `SIS_BASE_URL` unset | 404 `{ url: null }` | |
| 65 | +| Role not in `admin, advisor, ir` | 403 `{ error: "Forbidden" }` | |
| 66 | +| No mapping in `guid_sis_map` | 404 `{ url: null }` | |
| 67 | +| Mapping found | 200 `{ url: "https://sis-demo.example.com/students?id=BSC-100001" }` | |
| 68 | + |
| 69 | +**Role gating:** Reads `x-user-role` header injected by existing middleware. No changes to `lib/roles.ts` needed — the `/api/students` prefix is already gated to `admin`, `advisor`, `ir`. |
| 70 | + |
| 71 | +**Audit logging:** Appends to `logs/query-history.jsonl`: |
| 72 | + |
| 73 | +```json |
| 74 | +{ "event": "sis_link_accessed", "guid": "<guid>", "role": "advisor", "timestamp": "2026-04-01T12:00:00.000Z" } |
| 75 | +``` |
| 76 | + |
| 77 | +The `sis_id` is never logged. |
| 78 | + |
| 79 | +## 4. UI — "Open in SIS" Button |
| 80 | + |
| 81 | +**File:** `codebenders-dashboard/app/students/[guid]/page.tsx` |
| 82 | + |
| 83 | +**Placement:** In the student header card, alongside the existing alert/readiness badges. |
| 84 | + |
| 85 | +**Visibility logic** (determined by the API response on page load): |
| 86 | + |
| 87 | +| API result | Button state | |
| 88 | +|------------|--------------| |
| 89 | +| 200 with URL | Visible and clickable — opens URL in new tab | |
| 90 | +| 404 (no mapping) | Visible but disabled — tooltip: "No SIS record linked for this student" | |
| 91 | +| 403 or fetch error | Hidden entirely | |
| 92 | + |
| 93 | +Uses existing `Button` from shadcn/ui and `ExternalLink` icon from lucide-react. No new component file needed. |
| 94 | + |
| 95 | +## Files Changed |
| 96 | + |
| 97 | +| File | Change | |
| 98 | +|------|--------| |
| 99 | +| `operations/seed_guid_sis_map.py` | New — seed script for demo data | |
| 100 | +| `codebenders-dashboard/.env.local` | Add `SIS_BASE_URL`, `SIS_ID_PARAM` | |
| 101 | +| `codebenders-dashboard/app/api/students/[guid]/sis-link/route.ts` | New — server-side SIS URL builder | |
| 102 | +| `codebenders-dashboard/app/students/[guid]/page.tsx` | Add "Open in SIS" button with fetch logic | |
| 103 | + |
| 104 | +## Out of Scope |
| 105 | + |
| 106 | +- Row-Level Security on `guid_sis_map` (not needed for POC) |
| 107 | +- Real institution SIS integration (demo uses placeholder URL) |
| 108 | +- `.env.example` file (can be added later) |
| 109 | +- Supabase Edge Function alternative |
| 110 | + |
| 111 | +## Acceptance Criteria (from issue #78) |
| 112 | + |
| 113 | +- [x] `SIS_BASE_URL` env var controls whether the button appears (hidden when blank) |
| 114 | +- [x] Button only visible to Advisor + IR + Admin roles |
| 115 | +- [x] SIS ID is never stored in `student_level_with_predictions` or `llm_recommendations` |
| 116 | +- [x] SIS ID is never returned by any public API endpoint (only pre-built URL returned) |
| 117 | +- [x] Every deep-link access is logged (GUID + role, not SIS ID) |
| 118 | +- [x] Button shows a graceful fallback if no mapping exists for a GUID |
| 119 | +- [x] Works with any SIS that accepts a query-param student ID in a URL |
0 commit comments