open-vtop 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,9 +14,9 @@ An open-source VTOP client with automatic session management.
14
14
  9) qol like automatic browser open, better logging, save usn password for faster logins, logout
15
15
  ## Features
16
16
 
17
- - 🚀 **Automatic Session Initialization**: VTOP session cookies and CSRF tokens are automatically established when the server starts
18
- - 🔄 **Background Processing**: Session setup happens in the background, no need to hit any endpoints first
19
- - 📊 **Session Monitoring**: Check session status in real-time via the web interface
17
+ - **Automatic Session Initialization**: VTOP session cookies and CSRF tokens are automatically established when the server starts
18
+ - **Background Processing**: Session setup happens in the background, no need to hit any endpoints first
19
+ - **Session Monitoring**: Check session status in real-time via the web interface
20
20
 
21
21
  ## Getting Started
22
22
 
package/dist/index.js CHANGED
@@ -38,6 +38,26 @@ app.post("/api/login/form", async (c) => {
38
38
  return c.html(_jsxs("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: ["Server error: ", String(error)] }));
39
39
  }
40
40
  });
41
+ // Course Details HTML Endpoint
42
+ app.get("/api/courses/html", async (c) => {
43
+ if (!sessionManager.isLoggedIn()) {
44
+ return c.html(_jsx("div", { class: "text-red-500", children: "Session expired." }));
45
+ }
46
+ try {
47
+ const courses = await sessionManager.fetchCourseDetails();
48
+ if (courses.length === 0) {
49
+ return c.html(_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted", children: "No course details found." }));
50
+ }
51
+ return c.html(_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 gap-4", children: courses.map((course, i) => (_jsxs("div", { class: "p-4 bg-surface border border-border rounded-lg flex flex-col justify-between", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-2", children: [_jsx("span", { class: "text-xs font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.65rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-sm mb-3 leading-snug", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-3 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.65rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-lg font-bold ${course.attendanceColor === "danger"
52
+ ? "text-red-500"
53
+ : course.attendanceColor === "warning"
54
+ ? "text-yellow-500"
55
+ : "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-xs px-2 py-1 rounded bg-${course.attendanceColor === "danger" ? "red" : "green"}-500/10 text-${course.attendanceColor === "danger" ? "red" : "green"}-500`, children: course.remarks }))] })] }, i))) }));
56
+ }
57
+ catch (error) {
58
+ return c.html(_jsxs("div", { class: "p-4 border border-red-500 rounded-md text-red-500", children: ["Failed to load courses: ", String(error)] }));
59
+ }
60
+ });
41
61
  app.get("/api/assignments/html", async (c) => {
42
62
  if (!sessionManager.isLoggedIn()) {
43
63
  // If session expired, redirect/render login
@@ -48,7 +68,6 @@ app.get("/api/assignments/html", async (c) => {
48
68
  if (assignments.length === 0) {
49
69
  return c.html(_jsxs("div", { class: "p-12 text-center bg-surface border border-border rounded-lg", children: [_jsx("h3", { class: "text-lg font-medium mb-2", children: "No Upcoming Assignments" }), _jsx("p", { class: "text-muted", children: "You are all caught up!" })] }));
50
70
  }
51
- //ass
52
71
  return c.html(_jsx("div", { class: "flex flex-col gap-3", children: assignments.map((ass, i) => (_jsxs("div", { class: "flex items-center justify-between p-4 bg-surface border border-border rounded-lg transition-colors hover:border-muted group", children: [_jsxs("div", { class: "flex-1 min-w-0 pr-4", children: [_jsxs("div", { class: "flex items-baseline gap-3 overflow-hidden whitespace-nowrap text-ellipsis", children: [_jsx("span", { class: "text-xs font-bold text-muted uppercase tracking-wider min-w-fit", children: ass.courseCode }), _jsx("span", { class: "font-semibold text-sm truncate", children: ass.assignmentTitle }), _jsxs("span", { class: "text-xs text-muted", children: ["\u2014 ", ass.courseName] })] }), _jsxs("div", { class: "flex gap-4 mt-1 text-xs text-muted", children: [_jsxs("span", { children: ["Due:", " ", _jsx("span", { class: "text-foreground", children: ass.dueDate || "N/A" })] }), _jsxs("span", { children: ["Max:", " ", _jsx("span", { class: "text-foreground", children: ass.maxMarks || "N/A" })] })] })] }), _jsx("span", { class: `text-xs px-2.5 py-1 rounded-full font-medium whitespace-nowrap ${ass.status?.toLowerCase().includes("pending")
53
72
  ? "bg-red-500/10 text-red-500 border border-red-500/20"
54
73
  : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] }, i))) }));
@@ -1,7 +1,4 @@
1
- /**
2
- * Session Manager for VTOP
3
- * Automatically initializes and maintains VTOP session cookies and CSRF tokens
4
- */
1
+ //seperations of concerns my arse
5
2
  import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
6
3
  import * as path from "path";
7
4
  const BASE = "https://vtop.vit.ac.in";
@@ -16,6 +13,7 @@ const VTOP_OPEN = `${BASE}/vtop/open`;
16
13
  const CONTENT = `${BASE}/vtop/content`;
17
14
  const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
18
15
  const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
16
+ const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
19
17
  class VTOPSessionManager {
20
18
  state = {
21
19
  cookies: new Map(),
@@ -28,7 +26,6 @@ class VTOPSessionManager {
28
26
  };
29
27
  //cheerio at home
30
28
  extractCsrf(html) {
31
- // Look for _csrf token in various forms
32
29
  const patterns = [
33
30
  /name="_csrf"\s+value="([^"]+)"/,
34
31
  /name='_csrf'\s+value='([^']+)'/,
@@ -58,16 +55,12 @@ class VTOPSessionManager {
58
55
  .map(([name, value]) => `${name}=${value}`)
59
56
  .join("; ");
60
57
  }
61
- /**
62
- * Make a fetch request with cookie handling
63
- */
64
58
  async fetchWithCookies(url, options = {}) {
65
59
  const cookieHeader = this.getCookieHeader();
66
60
  const headers = new Headers(options.headers);
67
61
  if (cookieHeader) {
68
62
  headers.set("Cookie", cookieHeader);
69
63
  }
70
- // Add common browser headers
71
64
  headers.set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
72
65
  headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
73
66
  headers.set("Accept-Language", "en-US,en;q=0.9");
@@ -79,16 +72,13 @@ class VTOPSessionManager {
79
72
  headers,
80
73
  redirect: "manual", // Handle redirects manually to capture cookies
81
74
  });
82
- // Store any cookies from the response
83
75
  this.storeCookies(response);
84
- // Follow redirects manually if needed
85
76
  if (response.status >= 300 && response.status < 400) {
86
77
  const location = response.headers.get("Location");
87
78
  if (location) {
88
79
  const redirectUrl = location.startsWith("http")
89
80
  ? location
90
81
  : new URL(location, url).toString();
91
- // Create new options without body for GET redirect
92
82
  const redirectOptions = {
93
83
  headers: options.headers,
94
84
  redirect: options.redirect,
@@ -98,12 +88,9 @@ class VTOPSessionManager {
98
88
  }
99
89
  return response;
100
90
  }
101
- /**
102
- * Initialize VTOP session
103
- */
104
91
  async initialize() {
105
92
  try {
106
- console.log("🔄 Initializing VTOP session...");
93
+ console.log("Initializing VTOP session...");
107
94
  // Step 1: GET / to get SERVERID cookie
108
95
  console.log("Step 1: GET / to get SERVERID...");
109
96
  await this.fetchWithCookies(BASE);
@@ -119,16 +106,12 @@ class VTOPSessionManager {
119
106
  console.log(" ✓ OK");
120
107
  const csrf = this.extractCsrf(openPageHtml);
121
108
  if (!csrf) {
122
- console.warn("⚠️ Warning: _csrf not found on /vtop/openPage. Continuing anyway...");
109
+ console.warn("Warning: _csrf not found on /vtop/openPage. Continuing anyway...");
123
110
  }
124
111
  else {
125
112
  this.state.csrf = csrf;
126
113
  console.log(` ✓ Found _csrf: ${csrf.substring(0, 20)}...`);
127
114
  }
128
- // Optional: Visit alternate open page (used as Referer in browser)
129
- await this.fetchWithCookies(OPEN_PAGE_ALT).catch(() => {
130
- // Ignore errors on this optional step
131
- });
132
115
  // Step 4: POST /vtop/prelogin/setup
133
116
  console.log("Step 4: POST /vtop/prelogin/setup (flag=VTOP)...");
134
117
  const formData = new URLSearchParams();
@@ -145,51 +128,35 @@ class VTOPSessionManager {
145
128
  },
146
129
  body: formData.toString(),
147
130
  });
148
- console.log(` Prelogin status: ${preloginRes.status}`);
149
- // Mark as initialized
131
+ console.log(` Prelogin status: ${preloginRes.status}`);
150
132
  this.state.initialized = true;
151
133
  this.state.lastInitialized = new Date();
152
- console.log("VTOP session initialized successfully!");
134
+ console.log("VTOP session initialized successfully!");
153
135
  console.log(` Cookies stored: ${this.state.cookies.size}`);
154
136
  console.log(` Cookie names: ${Array.from(this.state.cookies.keys()).join(", ")}`);
155
137
  }
156
138
  catch (error) {
157
- console.error("Failed to initialize VTOP session:", error);
139
+ console.error("Failed to initialize VTOP session:", error);
158
140
  throw error;
159
141
  }
160
142
  }
161
- /**
162
- * Get current session state
163
- */
164
143
  getState() {
165
144
  return {
166
145
  ...this.state,
167
146
  cookies: new Map(this.state.cookies),
168
147
  };
169
148
  }
170
- /**
171
- * Get CSRF token
172
- */
173
149
  getCsrf() {
174
150
  return this.state.csrf;
175
151
  }
176
- /**
177
- * Get cookies as a header string
178
- */
179
152
  getCookies() {
180
153
  return this.getCookieHeader();
181
154
  }
182
- /**
183
- * Check if session is initialized
184
- */
185
155
  isInitialized() {
186
156
  return this.state.initialized;
187
157
  }
188
- /**
189
- * Re-initialize session (useful for periodic refresh)
190
- */
191
158
  async refresh() {
192
- console.log("🔄 Refreshing VTOP session...");
159
+ console.log("Refreshing VTOP session...");
193
160
  this.state.cookies.clear();
194
161
  this.state.csrf = null;
195
162
  this.state.initialized = false;
@@ -197,9 +164,6 @@ class VTOPSessionManager {
197
164
  this.state.username = null;
198
165
  await this.initialize();
199
166
  }
200
- /**
201
- * Detect captcha type from login page HTML using regex (no cheerio)
202
- */
203
167
  detectCaptcha(html) {
204
168
  // reCAPTCHA signals
205
169
  const recaptchaDom = html.includes('id="recaptcha"') ||
@@ -207,11 +171,7 @@ class VTOPSessionManager {
207
171
  html.includes('class="g-recaptcha"');
208
172
  const recaptchaJs = html.includes("var captchaType=2");
209
173
  const isRecaptcha = recaptchaDom || recaptchaJs;
210
- // Text CAPTCHA signals - check for captchaStr input
211
174
  const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
212
- // Look for data URI image in the HTML
213
- // Support both single and double quotes
214
- // VTOP sometimes sends "null" in base64, so we need to find the first valid one
215
175
  const imgDataUriMatches = [
216
176
  ...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
217
177
  ];
@@ -222,35 +182,27 @@ class VTOPSessionManager {
222
182
  break;
223
183
  }
224
184
  }
225
- // specific check for the warning if we found matches but none were valid
226
185
  if (!imgDataUri && imgDataUriMatches.length > 0) {
227
- console.warn("⚠️ Detected invalid captcha image (base64 is null)");
186
+ console.warn("Detected invalid captcha image (base64 is null)");
228
187
  }
229
188
  const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
230
- // Extract CSRF token
231
189
  const csrf = this.extractCsrf(html);
232
190
  return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
233
191
  }
234
- /**
235
- * Login to VTOP with username, password, and registration number
236
- * Polls the login page until a text CAPTCHA appears, solves it, and submits
237
- */
238
192
  async login(username, password, regNo, maxAttempts = 10) {
239
193
  if (!this.state.initialized) {
240
194
  console.log("Session not initialized, initializing first...");
241
195
  await this.initialize();
242
196
  }
243
- console.log("🔐 Starting VTOP login process...");
197
+ console.log("Starting VTOP login process...");
244
198
  console.log("Polling /vtop/login until text CAPTCHA appears...");
245
199
  let attempt = 0;
246
200
  while (attempt < maxAttempts) {
247
201
  attempt++;
248
202
  try {
249
- // Fetch login page
250
203
  const res = await this.fetchWithCookies(LOGIN_PAGE);
251
204
  const body = await res.text();
252
205
  const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = this.detectCaptcha(body);
253
- // Get current cookies for logging
254
206
  const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
255
207
  const curServerID = this.state.cookies.get("SERVERID") || "(?)";
256
208
  console.log(`Attempt ${attempt}: status ${res.status} | text-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`);
@@ -259,29 +211,25 @@ class VTOPSessionManager {
259
211
  if (imgDataUri) {
260
212
  const parts = extractDataUriParts(imgDataUri);
261
213
  try {
262
- // Trim whitespace just in case
263
214
  const cleanDataUri = imgDataUri.trim();
264
215
  if (cleanDataUri !== imgDataUri) {
265
- console.log("⚠️ Trimmed whitespace from captcha data URI");
216
+ console.log("Trimmed whitespace from captcha data URI");
266
217
  }
267
218
  solvedCaptcha = await solve(cleanDataUri);
268
- console.log("Solved CAPTCHA:", solvedCaptcha);
269
- // Save captcha image for debugging
219
+ console.log("Solved CAPTCHA:", solvedCaptcha);
270
220
  if (parts?.base64) {
271
221
  const out = path.resolve(process.cwd(), "captcha.jpg");
272
222
  await saveCaptchaImage(parts.base64, out);
273
- // console.log(`📷 Saved CAPTCHA image to ${out}`);
274
223
  }
275
224
  }
276
225
  catch (e) {
277
- console.warn("⚠️ Failed to solve captcha:", e);
226
+ console.warn("Failed to solve captcha:", e);
278
227
  solvedCaptcha = "";
279
- // Save the failed data URI for inspection
280
228
  try {
281
229
  const fs = await import("fs/promises");
282
230
  const failPath = path.resolve(process.cwd(), "failed_captcha_data.txt");
283
231
  await fs.writeFile(failPath, imgDataUri);
284
- console.log(`📝 Saved failed captcha data URI to ${failPath}`);
232
+ console.log(`Saved failed captcha data URI to ${failPath}`);
285
233
  }
286
234
  catch (writeErr) {
287
235
  console.error("Failed to save failed captcha data:", writeErr);
@@ -291,8 +239,7 @@ class VTOPSessionManager {
291
239
  else {
292
240
  console.log("Text CAPTCHA detected but no data URI image found.");
293
241
  }
294
- // Build and submit login form
295
- console.log("\n📤 Submitting POST /vtop/login ...");
242
+ console.log("\nSubmitting POST /vtop/login ...");
296
243
  const loginForm = new URLSearchParams();
297
244
  if (csrf)
298
245
  loginForm.set("_csrf", csrf);
@@ -324,25 +271,20 @@ class VTOPSessionManager {
324
271
  redirect: "manual",
325
272
  });
326
273
  console.log(` -> Login POST status: ${postRes.status}`);
327
- // Store any new cookies
328
274
  this.storeCookies(postRes);
329
275
  const setCookie = postRes.headers.getSetCookie?.() || [];
330
276
  if (setCookie.length > 0) {
331
277
  console.log(" -> New cookies received");
332
278
  }
333
- // Check for redirect
334
279
  const location = postRes.headers.get("Location");
335
280
  if (location) {
336
281
  console.log(` -> Redirect to: ${location}`);
337
- // Follow redirect to complete the login flow
338
282
  const redirectUrl = location.startsWith("http")
339
283
  ? location
340
284
  : new URL(location, LOGIN_PAGE).toString();
341
285
  await this.fetchWithCookies(redirectUrl);
342
286
  }
343
- // After successfully submitting the captcha, assume login worked
344
- // The actual success will be determined when we try to fetch data
345
- console.log("🎉 Login POST submitted successfully!");
287
+ console.log("Login POST submitted successfully!");
346
288
  this.state.loggedIn = true;
347
289
  this.state.username = username;
348
290
  this.state.regNo = regNo;
@@ -352,14 +294,11 @@ class VTOPSessionManager {
352
294
  console.error("Login POST failed:", e);
353
295
  }
354
296
  }
355
- // If reCAPTCHA, we cannot solve it automatically
356
297
  if (isRecaptcha) {
357
- console.log("⚠️ reCAPTCHA detected - cannot solve automatically");
358
- // Wait a bit and retry, hoping for text captcha
298
+ console.log("reCAPTCHA detected - cannot solve automatically");
359
299
  await new Promise((r) => setTimeout(r, 2000));
360
300
  continue;
361
301
  }
362
- // Wait before next poll
363
302
  await new Promise((r) => setTimeout(r, 500));
364
303
  }
365
304
  catch (e) {
@@ -367,28 +306,18 @@ class VTOPSessionManager {
367
306
  await new Promise((r) => setTimeout(r, 1000));
368
307
  }
369
308
  }
370
- console.log(`❌ Login failed after ${maxAttempts} attempts`);
309
+ console.log(`Login failed after ${maxAttempts} attempts`);
371
310
  return false;
372
311
  }
373
- /**
374
- * Check if logged in
375
- */
376
312
  isLoggedIn() {
377
313
  return this.state.loggedIn;
378
314
  }
379
- /**
380
- * Get current username
381
- */
382
315
  getUsername() {
383
316
  return this.state.username;
384
317
  }
385
- /**
386
- * Navigate through post-login pages to establish authenticated session
387
- * This mimics what the browser does after successful login
388
- */
389
318
  async navigatePostLogin() {
390
319
  if (!this.state.loggedIn) {
391
- console.error(" Cannot navigate post-login pages: not logged in");
320
+ console.error(" Cannot navigate post-login pages: not logged in");
392
321
  return false;
393
322
  }
394
323
  const cookies = this.getCookieHeader();
@@ -404,8 +333,7 @@ class VTOPSessionManager {
404
333
  Cookie: cookies,
405
334
  };
406
335
  try {
407
- // Navigate through required pages
408
- console.log("📄 Navigating post-login pages...");
336
+ console.log("Navigating post-login pages...");
409
337
  // 1. /vtop/init/page
410
338
  const initRes = await this.fetchWithCookies(INIT_PAGE, { headers });
411
339
  console.log(` -> /vtop/init/page: ${initRes.status}`);
@@ -422,24 +350,19 @@ class VTOPSessionManager {
422
350
  const newCsrf = this.extractCsrf(contentHtml);
423
351
  if (newCsrf) {
424
352
  this.state.csrf = newCsrf;
425
- console.log(`Updated CSRF token from content page`);
353
+ console.log(`Updated CSRF token from content page`);
426
354
  }
427
- console.log("Post-login navigation complete");
355
+ console.log("Post-login navigation complete");
428
356
  return true;
429
357
  }
430
358
  catch (e) {
431
- console.error("Post-login navigation failed:", e);
359
+ console.error("Post-login navigation failed:", e);
432
360
  return false;
433
361
  }
434
362
  }
435
- /**
436
- * Parse upcoming assignments from VTOP HTML response
437
- * Uses regex instead of cheerio
438
- */
363
+ //slop
439
364
  parseAssignmentsHtml(html) {
440
365
  const assignments = [];
441
- // VTOP returns assignments in a table or as JSON-like structure
442
- // Try to parse as JSON first (some endpoints return JSON)
443
366
  try {
444
367
  const jsonMatch = html.match(/\[[\s\S]*?\]/);
445
368
  if (jsonMatch) {
@@ -497,24 +420,68 @@ class VTOPSessionManager {
497
420
  }
498
421
  return assignments;
499
422
  }
500
- /**
501
- * Fetch upcoming assignments from VTOP
502
- */
503
- async fetchUpcomingAssignments() {
504
- if (!this.state.loggedIn || !this.state.regNo) {
505
- console.error("❌ Cannot fetch assignments: not logged in or no regNo");
506
- return [];
423
+ parseCourseDetailsHtml(html) {
424
+ const courses = [];
425
+ // Regex to match table rows
426
+ const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
427
+ const matches = [...html.matchAll(rowRegex)];
428
+ // Skip header row
429
+ for (let i = 1; i < matches.length; i++) {
430
+ const rowContent = matches[i][1];
431
+ // Extract Code and Name: <span class="mx-2 text-dark fw-bold">BCSE204L</span>-<span class="mx-2 text-dark">Design and Analysis of Algorithms</span>
432
+ const codeMatch = rowContent.match(/<span[^>]*text-dark fw-bold[^>]*>([^<]+)<\/span>/);
433
+ const nameMatch = rowContent.match(/-[\s\n]*<span[^>]*text-dark[^>]*>([^<]+)<\/span>/);
434
+ // Extract Type: <td class="fst-italic text-primary fw-bold mx-1">TH</td>
435
+ const typeMatch = rowContent.match(/<td[^>]*fst-italic[^>]*>([^<]+)<\/td>/);
436
+ // Extract Attendance: <span class="text-danger fw-bold">65.0</span>
437
+ // Capture color class as well to determine status
438
+ const attendanceMatch = rowContent.match(/<span class="text-([a-z]+)[^>]*fw-bold">([\d.]+)<\/span>/);
439
+ // Extract Remarks: <span class="text-danger fw-bold">Critical - must improve</span>
440
+ // This usually comes after attendance in the last column
441
+ const remarksMatch = rowContent.match(/<span class="text-[^>]*>([^<]+)<\/span>[\s\n]*<\/td>[\s\n]*<\/tr>$/) ||
442
+ rowContent.match(/<td[^>]*text-nowrap text-start[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/);
443
+ if (codeMatch && nameMatch) {
444
+ courses.push({
445
+ code: codeMatch[1].trim(),
446
+ name: nameMatch[1].trim(),
447
+ type: typeMatch ? typeMatch[1].trim() : "N/A",
448
+ attendance: attendanceMatch ? attendanceMatch[2].trim() : "N/A",
449
+ attendanceColor: attendanceMatch
450
+ ? attendanceMatch[1].trim()
451
+ : "secondary",
452
+ remarks: remarksMatch ? remarksMatch[1].trim() : "",
453
+ });
454
+ }
507
455
  }
508
- // Ensure we've navigated post-login pages
509
- await this.navigatePostLogin();
510
- const cookies = this.getCookieHeader();
456
+ return courses;
457
+ }
458
+ async performAcademicsCheck(headers) {
511
459
  const now = new Date();
512
- // First, do the academics default check
513
460
  const accParams = new URLSearchParams();
514
461
  accParams.set("authorizedID", this.state.regNo);
515
462
  if (this.state.csrf)
516
463
  accParams.set("_csrf", this.state.csrf);
517
464
  accParams.set("x", now.toUTCString());
465
+ try {
466
+ console.log("Performing AcademicsDefaultCheck...");
467
+ const accRes = await fetch(ACADEMICS_CHECK, {
468
+ method: "POST",
469
+ headers,
470
+ body: accParams.toString(),
471
+ });
472
+ console.log(` -> AcademicsDefaultCheck: ${accRes.status}`);
473
+ }
474
+ catch (e) {
475
+ console.warn("AcademicsDefaultCheck failed:", e);
476
+ }
477
+ }
478
+ async fetchCourseDetails() {
479
+ if (!this.state.loggedIn || !this.state.regNo) {
480
+ console.error("Cannot fetch course details: not logged in");
481
+ return [];
482
+ }
483
+ const cookies = this.getCookieHeader();
484
+ const now = new Date();
518
485
  const apiHeaders = {
519
486
  Accept: "*/*",
520
487
  "Accept-Encoding": "gzip, deflate, br, zstd",
@@ -529,27 +496,65 @@ class VTOPSessionManager {
529
496
  "Sec-Fetch-Site": "same-origin",
530
497
  "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
531
498
  };
499
+ await this.performAcademicsCheck(apiHeaders);
500
+ const assParams = new URLSearchParams();
501
+ assParams.set("authorizedID", this.state.regNo);
502
+ if (this.state.csrf)
503
+ assParams.set("_csrf", this.state.csrf);
504
+ assParams.set("x", now.toUTCString());
532
505
  try {
533
- // AcademicsDefaultCheck
534
- console.log("📚 Checking academics context...");
535
- const accRes = await fetch(ACADEMICS_CHECK, {
506
+ console.log("Fetching course details...");
507
+ const res = await fetch(COURSE_DETAILS, {
536
508
  method: "POST",
537
509
  headers: apiHeaders,
538
- body: accParams.toString(),
510
+ body: assParams,
539
511
  });
540
- console.log(` -> AcademicsDefaultCheck: ${accRes.status}`);
512
+ const html = await res.text();
513
+ console.log("Course details HTML:", html);
514
+ const courses = this.parseCourseDetailsHtml(html);
515
+ console.log(`Parsed ${courses.length} courses`);
516
+ return courses;
541
517
  }
542
- catch (e) {
543
- console.warn("⚠️ AcademicsDefaultCheck failed:", e);
518
+ catch (error) {
519
+ console.error("Failed to fetch course details:", error);
520
+ return [];
521
+ }
522
+ }
523
+ async fetchUpcomingAssignments() {
524
+ if (!this.state.loggedIn || !this.state.regNo) {
525
+ console.error("Cannot fetch assignments: not logged in or no regNo");
526
+ return [];
544
527
  }
545
- // Now fetch the assignments
528
+ await this.navigatePostLogin();
529
+ const cookies = this.getCookieHeader();
530
+ const now = new Date();
531
+ const accParams = new URLSearchParams();
532
+ accParams.set("authorizedID", this.state.regNo);
533
+ if (this.state.csrf)
534
+ accParams.set("_csrf", this.state.csrf);
535
+ accParams.set("x", now.toUTCString());
536
+ const apiHeaders = {
537
+ Accept: "*/*",
538
+ "Accept-Encoding": "gzip, deflate, br, zstd",
539
+ "Accept-Language": "en-US,en;q=0.7",
540
+ "Content-Type": "application/x-www-form-urlencoded",
541
+ Cookie: cookies,
542
+ Origin: BASE,
543
+ Priority: "u=1, i",
544
+ Referer: CONTENT,
545
+ "Sec-Fetch-Dest": "empty",
546
+ "Sec-Fetch-Mode": "cors",
547
+ "Sec-Fetch-Site": "same-origin",
548
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
549
+ };
550
+ await this.performAcademicsCheck(apiHeaders);
546
551
  const assParams = new URLSearchParams();
547
552
  assParams.set("authorizedID", this.state.regNo);
548
553
  if (this.state.csrf)
549
554
  assParams.set("_csrf", this.state.csrf);
550
555
  assParams.set("x", now.toUTCString());
551
556
  try {
552
- console.log("📝 Fetching upcoming assignments...");
557
+ console.log(" Fetching upcoming assignments...");
553
558
  const assRes = await fetch(UPCOMING_ASSIGNMENTS, {
554
559
  method: "POST",
555
560
  headers: apiHeaders,
@@ -561,19 +566,17 @@ class VTOPSessionManager {
561
566
  console.log(` -> Raw response:\n${assBody}`);
562
567
  if (assBody) {
563
568
  const assignments = this.parseAssignmentsHtml(assBody);
564
- console.log(`✅ Parsed ${assignments.length} assignments`);
569
+ console.log(` Parsed ${assignments.length} assignments`);
565
570
  return assignments;
566
571
  }
567
572
  }
568
573
  catch (e) {
569
- console.error(" Failed to fetch assignments:", e);
574
+ console.error(" Failed to fetch assignments:", e);
570
575
  }
571
576
  return [];
572
577
  }
573
578
  }
574
- // Export singleton instance
575
579
  export const sessionManager = new VTOPSessionManager();
576
- // Auto-initialize on module load
577
580
  sessionManager.initialize().catch((error) => {
578
581
  console.error("Failed to auto-initialize session:", error);
579
582
  });
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
2
  import { BaseLayout } from "./layouts/Base.js";
3
3
  export const Dashboard = ({ username }) => {
4
- return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-8", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-sm", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsx("div", { "hx-get": "/api/assignments/html", "hx-trigger": "load", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-16 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-6 w-6 border-2 border-muted border-t-white mb-4" }), _jsx("p", { class: "text-sm", children: "Syncing assignments..." })] }) }) })] }));
4
+ return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-8", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-sm", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsxs("div", { class: "space-y-8", children: [_jsxs("section", { children: [_jsxs("div", { class: "flex items-center justify-between mb-4", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Course Details" }), _jsx("span", { class: "text-xs text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), _jsx("div", { "hx-get": "/api/courses/html", "hx-trigger": "htmx:afterOnLoad from:#assignments-loader", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 gap-4 animate-pulse", children: [1, 2, 3, 4].map((i) => (_jsx("div", { class: "h-32 bg-surface/50 border border-border rounded-lg" }, i))) }) })] }), _jsxs("section", { children: [_jsx("h2", { class: "text-lg font-semibold mb-4", children: "Assignments" }), _jsx("div", { id: "assignments-loader", "hx-get": "/api/assignments/html", "hx-trigger": "load", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-16 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-6 w-6 border-2 border-muted border-t-white mb-4" }), _jsx("p", { class: "text-sm", children: "Syncing assignments..." })] }) }) })] })] })] }));
5
5
  };
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  export const SuccessMessage = ({ message }) => {
3
- return _jsxs("p", { class: "success", children: ["\u2713 ", message] });
3
+ return _jsx("p", { class: "success", children: message });
4
4
  };
package/package.json CHANGED
@@ -27,5 +27,5 @@
27
27
  "tsx": "^4.7.1",
28
28
  "typescript": "^5.8.3"
29
29
  },
30
- "version": "1.0.5"
30
+ "version": "1.0.6"
31
31
  }