@@ -140,10 +140,10 @@ function saveCredentials(data) {
140140}
141141
142142// ── Canvas API ────────────────────────────────────────────────────────────────
143- function httpGet ( url , headers ) {
143+ function httpGetWithHeaders ( url , headers ) {
144144 return new Promise ( ( resolve , reject ) => {
145- const mod = url . startsWith ( 'https' ) ? https : http ;
146- const req = mod . get ( url , { headers } , ( res ) => {
145+ const mod = url . startsWith ( 'https' ) ? https : http ;
146+ const req = mod . get ( url , { headers } , ( res ) => {
147147 let body = '' ;
148148 res . on ( 'data' , d => body += d ) ;
149149 res . on ( 'end' , ( ) => {
@@ -153,15 +153,32 @@ function httpGet(url, headers) {
153153 if ( res . statusCode >= 400 ) {
154154 return reject ( new Error ( `HTTP ${ res . statusCode } : ${ url } ` ) ) ;
155155 }
156- try { resolve ( JSON . parse ( body ) ) ; }
157- catch { reject ( new Error ( 'Invalid JSON response from server' ) ) ; }
156+ try {
157+ resolve ( { data : JSON . parse ( body ) , linkHeader : res . headers [ 'link' ] || '' } ) ;
158+ } catch {
159+ reject ( new Error ( 'Invalid JSON response from server' ) ) ;
160+ }
158161 } ) ;
159162 } ) ;
160163 req . on ( 'error' , reject ) ;
161164 req . setTimeout ( 12000 , ( ) => { req . destroy ( ) ; reject ( new Error ( 'Request timed out' ) ) ; } ) ;
162165 } ) ;
163166}
164167
168+ function httpGet ( url , headers ) {
169+ return httpGetWithHeaders ( url , headers ) . then ( r => r . data ) ;
170+ }
171+
172+ // Parse the Canvas `Link` response header to find the next-page URL.
173+ function parseLinkNext ( linkHeader ) {
174+ if ( ! linkHeader ) return null ;
175+ for ( const part of linkHeader . split ( ',' ) ) {
176+ const m = part . match ( / < ( [ ^ > ] + ) > .* r e l = " n e x t " / ) ;
177+ if ( m ) return m [ 1 ] ;
178+ }
179+ return null ;
180+ }
181+
165182const SKIP_KW = [
166183 'training' , 'pdp' , 'rmcpdp' , 'osa' , 'soct' , 'travel' ,
167184 'essentials' , 'respect' , 'consent' , 'osh' ,
@@ -172,15 +189,25 @@ async function fetchCoursesFromCanvas() {
172189 const token = fs . existsSync ( tokenFile ) ? fs . readFileSync ( tokenFile , 'utf8' ) . trim ( ) : '' ;
173190 if ( ! token ) return { error : 'Canvas token not saved — enter it in Settings → API Keys.' } ;
174191 const cfg = loadConfig ( ) ;
175- let url = ( cfg . CANVAS_URL || '' ) . trim ( ) . replace ( / \/ $ / , '' ) ;
176- if ( ! url ) return { error : 'Canvas URL not configured — enter it in Settings → Connection.' } ;
177- if ( ! url . startsWith ( 'http' ) ) url = 'https://' + url ;
192+ let baseUrl = ( cfg . CANVAS_URL || '' ) . trim ( ) . replace ( / \/ $ / , '' ) ;
193+ if ( ! baseUrl ) return { error : 'Canvas URL not configured — enter it in Settings → Connection.' } ;
194+ if ( ! baseUrl . startsWith ( 'http' ) ) baseUrl = 'https://' + baseUrl ;
178195 try {
179- const data = await httpGet (
180- `${ url } /api/v1/courses?enrollment_state=active&per_page=100` ,
181- { 'Authorization' : `Bearer ${ token } ` } ,
182- ) ;
183- const courses = data
196+ // Fetch all courses the user has any enrollment in (student, TA, auditor, cross-listed…).
197+ // Do NOT filter by enrollment_state=active — that drops TAs, design-experience tracks,
198+ // and cross-listed courses whose enrollment state differs from "active".
199+ // Follow Link headers for pagination in case the user has >100 courses.
200+ const allData = [ ] ;
201+ const authHeaders = { 'Authorization' : `Bearer ${ token } ` } ;
202+ let nextUrl = `${ baseUrl } /api/v1/courses?per_page=100` ;
203+ let pages = 0 ;
204+ while ( nextUrl && pages < 20 ) { // safety cap: 20 pages = 2000 courses max
205+ const { data, linkHeader } = await httpGetWithHeaders ( nextUrl , authHeaders ) ;
206+ allData . push ( ...( Array . isArray ( data ) ? data : [ ] ) ) ;
207+ nextUrl = parseLinkNext ( linkHeader ) ;
208+ pages ++ ;
209+ }
210+ const courses = allData
184211 . filter ( c => c . name && ! SKIP_KW . some ( k => c . name . toLowerCase ( ) . includes ( k ) ) )
185212 . map ( c => ( { id : c . id , name : c . name || c . course_code || String ( c . id ) } ) ) ;
186213 return { courses } ;
0 commit comments