open-vtop 1.0.6 → 1.0.7

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
@@ -1,7 +1,13 @@
1
1
  # Open-VTOP
2
2
 
3
3
  An open-source VTOP client with automatic session management.
4
+ ```bash
5
+ npx open-vtop
6
+ bunx open-vtop
4
7
 
8
+ npx open-vtop logs
9
+ bunx open-vtop logs
10
+ ```
5
11
  ## Todo
6
12
  1) save usn and password for future use
7
13
  2) grab regno from the responses it self
@@ -10,73 +16,28 @@ An open-source VTOP client with automatic session management.
10
16
  5) cgpa
11
17
  6) course-page
12
18
  7) callendar
13
- 8) many more...
14
- 9) qol like automatic browser open, better logging, save usn password for faster logins, logout
15
- ## Features
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
19
+ 9) qol like automatic browser open, better logging
20
20
 
21
21
  ## Getting Started
22
22
 
23
23
  ### Installation
24
24
 
25
25
  ```bash
26
- npm install
26
+ bun i
27
27
  ```
28
28
 
29
29
  ### Development
30
30
 
31
31
  ```bash
32
- npm run dev
32
+ bun start
33
+ bun run logs #for logs
33
34
  ```
34
35
 
35
- Then open http://localhost:3000
36
+ Then open http://localhost:6767
36
37
 
37
38
  ### Production
38
39
 
39
40
  ```bash
40
- npm run build
41
- npm start
42
- ```
43
-
44
- ## How It Works
45
-
46
- When the server starts, it automatically:
47
-
48
- 1. **Establishes cookies** - Gets `SERVERID` and `JSESSIONID` cookies from VTOP
49
- 2. **Retrieves CSRF token** - Extracts the `_csrf` token from `/vtop/openPage`
50
- 3. **Completes prelogin setup** - POSTs to `/vtop/prelogin/setup` with flag=VTOP
51
- 4. **Maintains session state** - Stores cookies and tokens for subsequent requests
52
-
53
- All of this happens in the background using Node.js's native fetch API.
54
-
55
- ## API Endpoints
56
-
57
- - `GET /api/session/status` - View current session status (cookies, CSRF, etc.)
58
- - `POST /api/session/refresh` - Manually refresh the VTOP session
59
-
60
- ## Session Manager
61
-
62
- The session manager (`src/session-manager.ts`) is a singleton that:
63
- - Initializes automatically on server startup
64
- - Stores cookies and CSRF tokens
65
- - Can be imported and used by other modules
66
- - Supports manual refresh when needed
67
-
68
- ```typescript
69
- import { sessionManager } from "./session-manager.js";
70
-
71
- // Check if initialized
72
- const isReady = sessionManager.isInitialized();
73
-
74
- // Get CSRF token
75
- const csrf = sessionManager.getCsrf();
76
-
77
- // Get cookies as header string
78
- const cookies = sessionManager.getCookies();
79
-
80
- // Manually refresh
81
- await sessionManager.refresh();
41
+ bun run build
42
+ bun start
82
43
  ```
package/dist/cli.cjs ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require("child_process");
3
+ const { exec } = require("child_process");
4
+ const { platform } = require("os");
5
+ const path = require("path");
6
+
7
+ const args = process.argv.slice(2);
8
+ const showLogs = args.includes("logs");
9
+
10
+ const colors = {
11
+ reset: "\x1b[0m",
12
+ bold: "\x1b[1m",
13
+ dim: "\x1b[2m",
14
+ cyan: "\x1b[36m",
15
+ green: "\x1b[32m",
16
+ yellow: "\x1b[33m",
17
+ magenta: "\x1b[35m",
18
+ blue: "\x1b[34m",
19
+ };
20
+
21
+ const PORT = 6767;
22
+ const URL = `http://localhost:${PORT}`;
23
+
24
+ function openBrowser(url) {
25
+ const plat = platform();
26
+ let command;
27
+
28
+ switch (plat) {
29
+ case "darwin":
30
+ command = `open "${url}"`;
31
+ break;
32
+ case "win32":
33
+ command = `start "" "${url}"`;
34
+ break;
35
+ default:
36
+ command = `xdg-open "${url}"`;
37
+ }
38
+
39
+ exec(command, (error) => {
40
+ if (error) {
41
+ console.log(
42
+ `${colors.yellow}Could not open browser automatically. Please visit: ${url}${colors.reset}`,
43
+ );
44
+ }
45
+ });
46
+ }
47
+
48
+ function printBanner() {
49
+ console.log();
50
+ console.log(
51
+ `${colors.cyan}${colors.bold} ┌───────────────────────────────────────┐${colors.reset}`,
52
+ );
53
+ console.log(
54
+ `${colors.cyan}${colors.bold} │ ${colors.magenta}open-vtop${colors.cyan} │${colors.reset}`,
55
+ );
56
+ console.log(
57
+ `${colors.cyan}${colors.bold} └───────────────────────────────────────┘${colors.reset}`,
58
+ );
59
+ console.log();
60
+ console.log(
61
+ ` ${colors.green}✓${colors.reset} Server running at ${colors.bold}${URL}${colors.reset}`,
62
+ );
63
+ console.log();
64
+ console.log(` ${colors.dim}Keyboard shortcuts:${colors.reset}`);
65
+ console.log(` ${colors.bold}q${colors.reset} → Quit server`);
66
+ console.log(` ${colors.bold}o${colors.reset} → Open browser`);
67
+ console.log(` ${colors.bold}c${colors.reset} → Clear console`);
68
+ console.log();
69
+ }
70
+
71
+ let serverProcess = null;
72
+
73
+ function shutdown() {
74
+ console.log(`\n${colors.yellow}Shutting down server...${colors.reset}`);
75
+ if (serverProcess) {
76
+ serverProcess.kill("SIGTERM");
77
+ }
78
+ // Restore terminal to normal mode before exiting
79
+ if (process.stdin.isTTY) {
80
+ process.stdin.setRawMode(false);
81
+ }
82
+ process.exit(0);
83
+ }
84
+
85
+ function setupKeyboardShortcuts() {
86
+ if (process.stdin.isTTY) {
87
+ process.stdin.setRawMode(true);
88
+ process.stdin.resume();
89
+ process.stdin.setEncoding("utf8");
90
+
91
+ process.stdin.on("data", (key) => {
92
+ switch (key.toLowerCase()) {
93
+ case "q":
94
+ case "\u0003": // Ctrl+C
95
+ shutdown();
96
+ break;
97
+ case "o":
98
+ console.log(`${colors.dim}Opening browser...${colors.reset}`);
99
+ openBrowser(URL);
100
+ break;
101
+ case "c":
102
+ console.clear();
103
+ printBanner();
104
+ break;
105
+ }
106
+ });
107
+ }
108
+ }
109
+
110
+ process.on("SIGINT", shutdown);
111
+ process.on("SIGTERM", shutdown);
112
+
113
+ const serverPath = path.join(__dirname, "index.js");
114
+ serverProcess = spawn(process.execPath, [serverPath], {
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ env: { ...process.env, OPEN_VTOP_CLI: "true" },
117
+ });
118
+
119
+ serverProcess.stdout.on("data", (data) => {
120
+ const text = data.toString();
121
+ if (text.includes("Server is running")) {
122
+ printBanner();
123
+ setupKeyboardShortcuts();
124
+ openBrowser(URL);
125
+ } else if (showLogs) {
126
+ process.stdout.write(data);
127
+ }
128
+ });
129
+
130
+ serverProcess.stderr.on("data", (data) => {
131
+ if (showLogs) {
132
+ process.stderr.write(data);
133
+ }
134
+ });
135
+
136
+ serverProcess.on("error", (err) => {
137
+ console.error(
138
+ `${colors.yellow}Failed to start server: ${err.message}${colors.reset}`,
139
+ );
140
+ process.exit(1);
141
+ });
142
+
143
+ serverProcess.on("exit", (code) => {
144
+ if (code !== 0 && code !== null) {
145
+ console.log(
146
+ `${colors.yellow}Server exited with code ${code}${colors.reset}`,
147
+ );
148
+ }
149
+ process.exit(code || 0);
150
+ });
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ export function AssignmentCard({ assignment }) {
3
+ return (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg hover:border-muted transition-colors group flex flex-col gap-1", children: [_jsxs("div", { class: "flex justify-between items-start gap-2", children: [_jsx("span", { class: "font-bold text-xs text-foreground line-clamp-1", title: assignment.courseName, children: assignment.courseName }), _jsxs("span", { class: "text-[0.65rem] font-medium text-red-400 whitespace-nowrap shrink-0", children: ["Due: ", assignment.dueDate || "N/A"] })] }), _jsxs("div", { class: "flex justify-between items-end gap-2", children: [_jsxs("div", { class: "flex flex-col min-w-0", children: [_jsx("span", { class: "text-[0.7rem] text-muted truncate", title: assignment.assignmentTitle, children: assignment.assignmentTitle }), _jsx("span", { class: "text-[0.6rem] text-muted/60 font-mono", children: assignment.courseCode })] }), _jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap shrink-0 ${assignment.status?.toLowerCase().includes("pending")
4
+ ? "bg-red-500/10 text-red-500 border border-red-500/20"
5
+ : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: assignment.status || "Pending" })] })] }));
6
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ import { AssignmentCard } from "./AssignmentCard.js";
3
+ export function AssignmentsList({ assignments, }) {
4
+ return (_jsx("div", { class: "flex flex-col gap-2", children: assignments.map((assignment, i) => (_jsx(AssignmentCard, { assignment: assignment }, i))) }));
5
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ export function CourseCard({ course }) {
3
+ return (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg flex flex-col justify-between h-full hover:border-muted transition-colors", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-1.5", children: [_jsx("span", { class: "text-[0.65rem] font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-xs mb-2 leading-relaxed line-clamp-2", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-2 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.6rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-base font-bold ${course.attendanceColor === "danger"
4
+ ? "text-red-500"
5
+ : course.attendanceColor === "warning"
6
+ ? "text-yellow-500"
7
+ : "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded bg-${course.attendanceColor === "danger" ? "red" : "green"}-500/10 text-${course.attendanceColor === "danger" ? "red" : "green"}-500`, children: course.remarks }))] })] }));
8
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ import { CourseCard } from "./CourseCard.js";
3
+ export function CoursesList({ courses }) {
4
+ return (_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3", children: courses.map((course, i) => (_jsx(CourseCard, { course: course }, i))) }));
5
+ }
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ export function EmptyState({ message }) {
3
+ return (_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted text-sm", children: message }));
4
+ }
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ export function ErrorMessage({ message }) {
3
+ return (_jsx("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: message }));
4
+ }
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ export function SessionExpired() {
3
+ return (_jsx("div", { class: "text-red-500 text-sm", children: "Session expired. Please login again." }));
4
+ }
@@ -0,0 +1,82 @@
1
+ // VTOP API endpoints and constants
2
+ export const BASE = "https://vtop.vit.ac.in";
3
+ export const VTOP = `${BASE}/vtop/`;
4
+ export const OPEN_PAGE = `${BASE}/vtop/openPage`;
5
+ export const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
6
+ export const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
7
+ export const LOGIN_PAGE = `${BASE}/vtop/login`;
8
+ export const INIT_PAGE = `${BASE}/vtop/init/page`;
9
+ export const MAIN_PAGE = `${BASE}/vtop/main/page`;
10
+ export const VTOP_OPEN = `${BASE}/vtop/open`;
11
+ export const CONTENT = `${BASE}/vtop/content`;
12
+ export const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
13
+ export const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
14
+ export const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
15
+ // HTTP Headers
16
+ // Common browser headers
17
+ export const USER_AGENT_CHROME = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
18
+ export const USER_AGENT_CHROME_139 = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36";
19
+ export const USER_AGENT_CHROME_140 = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
20
+ export const ACCEPT_HTML = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
21
+ export const ACCEPT_HTML_EXTENDED = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8";
22
+ export const ACCEPT_ALL = "*/*";
23
+ export const ACCEPT_LANGUAGE = "en-US,en;q=0.9";
24
+ export const ACCEPT_LANGUAGE_ALT = "en-US,en;q=0.7";
25
+ export const ACCEPT_ENCODING = "gzip, deflate, br";
26
+ export const ACCEPT_ENCODING_ZSTD = "gzip, deflate, br, zstd";
27
+ // Standard headers for fetch requests
28
+ export const BROWSER_HEADERS = {
29
+ "User-Agent": USER_AGENT_CHROME,
30
+ Accept: ACCEPT_HTML,
31
+ "Accept-Language": ACCEPT_LANGUAGE,
32
+ "Accept-Encoding": ACCEPT_ENCODING,
33
+ Connection: "keep-alive",
34
+ "Upgrade-Insecure-Requests": "1",
35
+ };
36
+ // Headers for login POST request
37
+ export const LOGIN_POST_HEADERS = {
38
+ "Cache-Control": "max-age=0",
39
+ Origin: BASE,
40
+ Referer: OPEN_PAGE_ALT,
41
+ "Content-Type": "application/x-www-form-urlencoded",
42
+ Accept: ACCEPT_HTML_EXTENDED,
43
+ "Accept-Encoding": ACCEPT_ENCODING_ZSTD,
44
+ "Accept-Language": ACCEPT_LANGUAGE,
45
+ "Sec-Fetch-Dest": "document",
46
+ "Sec-Fetch-Mode": "navigate",
47
+ "Sec-Fetch-Site": "same-origin",
48
+ "Sec-Fetch-User": "?1",
49
+ "Upgrade-Insecure-Requests": "1",
50
+ Priority: "u=0, i",
51
+ };
52
+ // Headers for post-login navigation
53
+ export const POST_LOGIN_HEADERS = {
54
+ Referer: LOGIN_PAGE,
55
+ "User-Agent": USER_AGENT_CHROME_140,
56
+ Accept: ACCEPT_HTML_EXTENDED,
57
+ "Accept-Encoding": ACCEPT_ENCODING_ZSTD,
58
+ "Cache-Control": "max-age=0",
59
+ "Upgrade-Insecure-Requests": "1",
60
+ Origin: BASE,
61
+ "Content-Type": "application/x-www-form-urlencoded",
62
+ };
63
+ // Headers for API requests (assignments, courses)
64
+ export const API_REQUEST_HEADERS = {
65
+ Accept: ACCEPT_ALL,
66
+ "Accept-Encoding": ACCEPT_ENCODING_ZSTD,
67
+ "Accept-Language": ACCEPT_LANGUAGE_ALT,
68
+ "Content-Type": "application/x-www-form-urlencoded",
69
+ Origin: BASE,
70
+ Priority: "u=1, i",
71
+ Referer: CONTENT,
72
+ "Sec-Fetch-Dest": "empty",
73
+ "Sec-Fetch-Mode": "cors",
74
+ "Sec-Fetch-Site": "same-origin",
75
+ "User-Agent": USER_AGENT_CHROME_139,
76
+ };
77
+ // Headers for academics check
78
+ export const ACADEMICS_CHECK_HEADERS = {
79
+ "Content-Type": "application/x-www-form-urlencoded",
80
+ Referer: CONTENT,
81
+ "User-Agent": USER_AGENT_CHROME_139,
82
+ };
package/dist/index.js CHANGED
@@ -1,20 +1,33 @@
1
- #!/usr/bin/env node
2
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
3
2
  import { serve } from "@hono/node-server";
4
3
  import { Hono } from "hono";
4
+ import { streamSSE } from "hono/streaming";
5
5
  import { serveStatic } from "@hono/node-server/serve-static";
6
6
  import { Login } from "./views/Login.js";
7
7
  import { Dashboard } from "./views/Dashboard.js";
8
8
  import { sessionManager } from "./session-manager.js";
9
9
  import { createRequire } from "module";
10
+ import { ErrorMessage } from "./components/ErrorMessage.js";
11
+ import { CoursesList } from "./components/CoursesList.js";
12
+ import { AssignmentsList } from "./components/AssignmentsList.js";
13
+ import { EmptyState } from "./components/EmptyState.js";
14
+ import { SessionExpired } from "./components/SessionExpired.js";
10
15
  const app = new Hono();
11
16
  const require = createRequire(import.meta.url);
12
17
  const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
18
+ const ssePath = require.resolve("htmx-ext-sse/sse.js");
13
19
  app.use("/static/htmx.js", serveStatic({ path: htmxPath }));
20
+ app.use("/static/sse.js", serveStatic({ path: ssePath }));
14
21
  app.get("/", (c) => {
15
22
  if (sessionManager.isLoggedIn()) {
16
23
  return c.html(_jsx(Dashboard, { username: sessionManager.getUsername() }));
17
24
  }
25
+ return c.redirect("/login");
26
+ });
27
+ app.get("/login", (c) => {
28
+ if (sessionManager.isLoggedIn()) {
29
+ return c.redirect("/");
30
+ }
18
31
  return c.html(_jsx(Login, {}));
19
32
  });
20
33
  app.post("/api/login/form", async (c) => {
@@ -22,58 +35,89 @@ app.post("/api/login/form", async (c) => {
22
35
  const formData = await c.req.parseBody();
23
36
  const username = formData["username"];
24
37
  const password = formData["password"];
25
- const regNo = formData["regNo"];
26
- if (!username || !password || !regNo) {
27
- return c.html(_jsx("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: "Missing credentials. Please try again." }));
38
+ if (!username || !password) {
39
+ return c.html(_jsx(ErrorMessage, { message: "Missing credentials. Please try again." }));
28
40
  }
29
- const success = await sessionManager.login(username, password, regNo);
41
+ const success = await sessionManager.login(username, password);
30
42
  if (success) {
31
- return c.html(_jsx(Dashboard, { username: username }));
43
+ await sessionManager.navigatePostLogin();
44
+ await sessionManager.performAcademicsCheck();
45
+ const courses = await sessionManager.fetchCourseDetails();
46
+ const assignments = await sessionManager.fetchUpcomingAssignments();
47
+ c.header("HX-Push-Url", "/");
48
+ return c.html(_jsx(Dashboard, { username: username, courses: courses, assignments: assignments }));
32
49
  }
33
50
  else {
34
- return c.html(_jsx("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: "Login failed. Invalid credentials or captcha error." }));
51
+ return c.html(_jsx(ErrorMessage, { message: "Login failed." }));
35
52
  }
36
53
  }
37
54
  catch (error) {
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)] }));
55
+ return c.html(_jsx(ErrorMessage, { message: `Server error: ${String(error)}` }));
39
56
  }
40
57
  });
41
- // Course Details HTML Endpoint
58
+ app.get("/api/login/events", async (c) => {
59
+ return streamSSE(c, async (stream) => {
60
+ console.log("SSE connected");
61
+ let keepOpen = true;
62
+ const onLog = async (msg) => {
63
+ console.log("SSE log:", msg);
64
+ await stream.writeSSE({
65
+ data: msg,
66
+ event: "log",
67
+ });
68
+ };
69
+ const onComplete = async () => {
70
+ console.log("SSE complete");
71
+ await stream.writeSSE({
72
+ data: "done",
73
+ event: "login-complete",
74
+ });
75
+ keepOpen = false;
76
+ };
77
+ sessionManager.events.on("log", onLog);
78
+ sessionManager.events.once("login-complete", onComplete);
79
+ stream.onAbort(() => {
80
+ console.log("SSE aborted");
81
+ sessionManager.events.off("log", onLog);
82
+ sessionManager.events.off("login-complete", onComplete);
83
+ keepOpen = false;
84
+ });
85
+ while (keepOpen) {
86
+ // Keep the stream open
87
+ await new Promise((resolve) => setTimeout(resolve, 100));
88
+ }
89
+ sessionManager.events.off("log", onLog);
90
+ sessionManager.events.off("login-complete", onComplete);
91
+ });
92
+ });
42
93
  app.get("/api/courses/html", async (c) => {
43
94
  if (!sessionManager.isLoggedIn()) {
44
- return c.html(_jsx("div", { class: "text-red-500", children: "Session expired." }));
95
+ return c.html(_jsx(SessionExpired, {}));
45
96
  }
46
97
  try {
47
98
  const courses = await sessionManager.fetchCourseDetails();
48
99
  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." }));
100
+ return c.html(_jsx(EmptyState, { message: "No course details found." }));
50
101
  }
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))) }));
102
+ return c.html(_jsx(CoursesList, { courses: courses }));
56
103
  }
57
104
  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)] }));
105
+ return c.html(_jsx(ErrorMessage, { message: `Failed to load courses: ${String(error)}` }));
59
106
  }
60
107
  });
61
108
  app.get("/api/assignments/html", async (c) => {
62
109
  if (!sessionManager.isLoggedIn()) {
63
- // If session expired, redirect/render login
64
- return c.html(_jsx("div", { class: "text-red-500", children: "Session expired. Please refresh to log in again." }));
110
+ return c.html(_jsx(SessionExpired, {}));
65
111
  }
66
112
  try {
67
113
  const assignments = await sessionManager.fetchUpcomingAssignments();
68
114
  if (assignments.length === 0) {
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!" })] }));
115
+ return c.html(_jsx(EmptyState, { message: "No assignments pending." }));
70
116
  }
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")
72
- ? "bg-red-500/10 text-red-500 border border-red-500/20"
73
- : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] }, i))) }));
117
+ return c.html(_jsx(AssignmentsList, { assignments: assignments }));
74
118
  }
75
119
  catch (error) {
76
- return c.html(_jsxs("div", { class: "p-4 border border-red-500 rounded-md text-red-500", children: ["Failed to load assignments: ", String(error)] }));
120
+ return c.html(_jsx(ErrorMessage, { message: `Failed to load assignments: ${String(error)}` }));
77
121
  }
78
122
  });
79
123
  serve({
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Extracts CSRF token from HTML
3
+ */
4
+ export function extractCsrf(html) {
5
+ const patterns = [
6
+ /name="_csrf"\s+value="([^"]+)"/,
7
+ /name='_csrf'\s+value='([^']+)'/,
8
+ /<input[^>]*name="_csrf"[^>]*value="([^"]+)"/,
9
+ /<input[^>]*value="([^"]+)"[^>]*name="_csrf"/,
10
+ ];
11
+ for (const pattern of patterns) {
12
+ const match = html.match(pattern);
13
+ if (match && match[1]) {
14
+ return match[1];
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ /**
20
+ * Extracts registration number (authorizedIDX) from HTML
21
+ */
22
+ export function extractRegNo(html) {
23
+ const patterns = [
24
+ /name="authorizedIDX"\s+id="authorizedIDX"\s+value="([^"]+)"/,
25
+ /id="authorizedIDX"\s+name="authorizedIDX"\s+value="([^"]+)"/,
26
+ /<input[^>]*name="authorizedIDX"[^>]*value="([^"]+)"/,
27
+ /<input[^>]*id="authorizedIDX"[^>]*value="([^"]+)"/,
28
+ ];
29
+ for (const pattern of patterns) {
30
+ const match = html.match(pattern);
31
+ if (match && match[1]) {
32
+ return match[1];
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ export function detectCaptcha(html) {
38
+ const recaptchaDom = html.includes('id="recaptcha"') ||
39
+ html.includes('id="g-recaptcha"') ||
40
+ html.includes('class="g-recaptcha"');
41
+ const recaptchaJs = html.includes("var captchaType=2");
42
+ const isRecaptcha = recaptchaDom || recaptchaJs;
43
+ const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
44
+ const imgDataUriMatches = [
45
+ ...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
46
+ ];
47
+ let imgDataUri = null;
48
+ for (const match of imgDataUriMatches) {
49
+ if (match[1] && !match[1].includes(";base64,null")) {
50
+ imgDataUri = match[1];
51
+ break;
52
+ }
53
+ }
54
+ if (!imgDataUri && imgDataUriMatches.length > 0) {
55
+ console.warn("Detected invalid captcha image (base64 is null)");
56
+ }
57
+ const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
58
+ const csrf = extractCsrf(html);
59
+ return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
60
+ }
61
+ /**
62
+ * Parses assignments from HTML response
63
+ */
64
+ export function parseAssignmentsHtml(html) {
65
+ const assignments = [];
66
+ try {
67
+ const jsonMatch = html.match(/\[[\s\S]*?\]/);
68
+ if (jsonMatch) {
69
+ const parsed = JSON.parse(jsonMatch[0]);
70
+ if (Array.isArray(parsed)) {
71
+ return parsed.map((item) => ({
72
+ courseCode: String(item.courseCode || item.code || ""),
73
+ courseName: String(item.courseName || item.name || ""),
74
+ assignmentTitle: String(item.assignmentTitle || item.title || item.assignmentName || ""),
75
+ dueDate: String(item.dueDate || item.endDate || item.deadline || ""),
76
+ status: String(item.status || ""),
77
+ maxMarks: String(item.maxMarks || item.marks || ""),
78
+ }));
79
+ }
80
+ }
81
+ }
82
+ catch { }
83
+ const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
84
+ const cellRegex = /<?<t[hd][^>]*>([\s\S]*?)(?:<\/t[hd]>|<\/tr|<tr|$)/gi;
85
+ let rowMatch;
86
+ while ((rowMatch = rowRegex.exec(html)) !== null) {
87
+ const cells = [];
88
+ let cellMatch;
89
+ const rowContent = rowMatch[1];
90
+ cellRegex.lastIndex = 0;
91
+ while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
92
+ // Strip HTML tags from cell content
93
+ const cellContent = cellMatch[1].replace(/<[^>]+>/g, "").trim();
94
+ cells.push(cellContent);
95
+ }
96
+ if (cells.length >= 4 && cells[0] !== "#" && !isNaN(Number(cells[0]))) {
97
+ assignments.push({
98
+ courseCode: "", // Not in this table format
99
+ courseName: cells[1] || "",
100
+ assignmentTitle: cells[2] || "",
101
+ dueDate: cells[3] || "",
102
+ status: cells[4] || "Pending",
103
+ maxMarks: "", // Not in this table format
104
+ });
105
+ }
106
+ }
107
+ return assignments;
108
+ }
109
+ /**
110
+ * Parses course details from HTML response
111
+ */
112
+ export function parseCourseDetailsHtml(html) {
113
+ const courses = [];
114
+ const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
115
+ const matches = [...html.matchAll(rowRegex)];
116
+ for (let i = 1; i < matches.length; i++) {
117
+ const rowContent = matches[i][1];
118
+ const codeMatch = rowContent.match(/<span[^>]*text-dark fw-bold[^>]*>([^<]+)<\/span>/);
119
+ const nameMatch = rowContent.match(/-[\s\n]*<span[^>]*text-dark[^>]*>([^<]+)<\/span>/);
120
+ const typeMatch = rowContent.match(/<td[^>]*fst-italic[^>]*>([^<]+)<\/td>/);
121
+ const attendanceMatch = rowContent.match(/<span class="text-([a-z]+)[^>]*fw-bold">([\d.]+)<\/span>/);
122
+ const remarksMatch = rowContent.match(/<span class="text-[^>]*>([^<]+)<\/span>[\s\n]*<\/td>[\s\n]*<\/tr>$/) ||
123
+ rowContent.match(/<td[^>]*text-nowrap text-start[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/);
124
+ if (codeMatch && nameMatch) {
125
+ courses.push({
126
+ code: codeMatch[1].trim(),
127
+ name: nameMatch[1].trim(),
128
+ type: typeMatch ? typeMatch[1].trim() : "N/A",
129
+ attendance: attendanceMatch ? attendanceMatch[2].trim() : "N/A",
130
+ attendanceColor: attendanceMatch
131
+ ? attendanceMatch[1].trim()
132
+ : "secondary",
133
+ remarks: remarksMatch ? remarksMatch[1].trim() : "",
134
+ });
135
+ }
136
+ }
137
+ return courses;
138
+ }
@@ -1,19 +1,10 @@
1
1
  //seperations of concerns my arse
2
+ //nvm i did sepearations of concerns
2
3
  import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
3
4
  import * as path from "path";
4
- const BASE = "https://vtop.vit.ac.in";
5
- const VTOP = `${BASE}/vtop/`;
6
- const OPEN_PAGE = `${BASE}/vtop/openPage`;
7
- const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
8
- const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
9
- const LOGIN_PAGE = `${BASE}/vtop/login`;
10
- const INIT_PAGE = `${BASE}/vtop/init/page`;
11
- const MAIN_PAGE = `${BASE}/vtop/main/page`;
12
- const VTOP_OPEN = `${BASE}/vtop/open`;
13
- const CONTENT = `${BASE}/vtop/content`;
14
- const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
15
- const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
16
- const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
5
+ import { EventEmitter } from "events";
6
+ import { BASE, VTOP, OPEN_PAGE, OPEN_PAGE_ALT, PRELOGIN_SETUP, LOGIN_PAGE, INIT_PAGE, MAIN_PAGE, VTOP_OPEN, CONTENT, ACADEMICS_CHECK, UPCOMING_ASSIGNMENTS, COURSE_DETAILS, BROWSER_HEADERS, LOGIN_POST_HEADERS, POST_LOGIN_HEADERS, API_REQUEST_HEADERS, ACADEMICS_CHECK_HEADERS, } from "./constants.js";
7
+ import { extractCsrf, extractRegNo, detectCaptcha, parseAssignmentsHtml, parseCourseDetailsHtml, } from "./parsers.js";
17
8
  class VTOPSessionManager {
18
9
  state = {
19
10
  cookies: new Map(),
@@ -24,22 +15,7 @@ class VTOPSessionManager {
24
15
  username: null,
25
16
  regNo: null,
26
17
  };
27
- //cheerio at home
28
- extractCsrf(html) {
29
- const patterns = [
30
- /name="_csrf"\s+value="([^"]+)"/,
31
- /name='_csrf'\s+value='([^']+)'/,
32
- /<input[^>]*name="_csrf"[^>]*value="([^"]+)"/,
33
- /<input[^>]*value="([^"]+)"[^>]*name="_csrf"/,
34
- ];
35
- for (const pattern of patterns) {
36
- const match = html.match(pattern);
37
- if (match && match[1]) {
38
- return match[1];
39
- }
40
- }
41
- return null;
42
- }
18
+ events = new EventEmitter();
43
19
  storeCookies(response) {
44
20
  const setCookieHeaders = response.headers.getSetCookie?.() || [];
45
21
  for (const cookieStr of setCookieHeaders) {
@@ -53,7 +29,7 @@ class VTOPSessionManager {
53
29
  getCookieHeader() {
54
30
  return Array.from(this.state.cookies.entries())
55
31
  .map(([name, value]) => `${name}=${value}`)
56
- .join("; ");
32
+ .join("; "); //vvvimp
57
33
  }
58
34
  async fetchWithCookies(url, options = {}) {
59
35
  const cookieHeader = this.getCookieHeader();
@@ -61,16 +37,13 @@ class VTOPSessionManager {
61
37
  if (cookieHeader) {
62
38
  headers.set("Cookie", cookieHeader);
63
39
  }
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");
65
- headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
66
- headers.set("Accept-Language", "en-US,en;q=0.9");
67
- headers.set("Accept-Encoding", "gzip, deflate, br");
68
- headers.set("Connection", "keep-alive");
69
- headers.set("Upgrade-Insecure-Requests", "1");
40
+ Object.entries(BROWSER_HEADERS).forEach(([key, value]) => {
41
+ headers.set(key, value);
42
+ });
70
43
  const response = await fetch(url, {
71
44
  ...options,
72
45
  headers,
73
- redirect: "manual", // Handle redirects manually to capture cookies
46
+ redirect: "manual",
74
47
  });
75
48
  this.storeCookies(response);
76
49
  if (response.status >= 300 && response.status < 400) {
@@ -91,29 +64,29 @@ class VTOPSessionManager {
91
64
  async initialize() {
92
65
  try {
93
66
  console.log("Initializing VTOP session...");
94
- // Step 1: GET / to get SERVERID cookie
67
+ // 1: GET / to get SERVERID cookie
95
68
  console.log("Step 1: GET / to get SERVERID...");
96
69
  await this.fetchWithCookies(BASE);
97
- console.log("OK");
98
- // Step 2: GET /vtop/ to get JSESSIONID cookie
70
+ console.log("OK");
71
+ // 2: GET /vtop/ to get JSESSIONID cookie
99
72
  console.log("Step 2: GET /vtop/ to get JSESSIONID...");
100
73
  await this.fetchWithCookies(VTOP);
101
- console.log("OK");
102
- // Step 3: GET /vtop/openPage for CSRF token
74
+ console.log("OK");
75
+ // 3: GET /vtop/openPage for CSRF token
103
76
  console.log("Step 3: GET /vtop/openPage for CSRF...");
104
77
  const openPageRes = await this.fetchWithCookies(OPEN_PAGE);
105
78
  const openPageHtml = await openPageRes.text();
106
- console.log("OK");
107
- const csrf = this.extractCsrf(openPageHtml);
79
+ console.log("OK");
80
+ const csrf = extractCsrf(openPageHtml);
108
81
  if (!csrf) {
109
- console.warn("Warning: _csrf not found on /vtop/openPage. Continuing anyway...");
82
+ console.warn("Warning: _csrf not found on /vtop/openPage.");
110
83
  }
111
84
  else {
112
85
  this.state.csrf = csrf;
113
- console.log(`Found _csrf: ${csrf.substring(0, 20)}...`);
86
+ console.log(`Found _csrf: ${csrf.substring(0, 20)}...`);
114
87
  }
115
- // Step 4: POST /vtop/prelogin/setup
116
- console.log("Step 4: POST /vtop/prelogin/setup (flag=VTOP)...");
88
+ // 4: POST /vtop/prelogin/setup
89
+ console.log("Step 4: POST /vtop/prelogin/setup flag=VTOP");
117
90
  const formData = new URLSearchParams();
118
91
  if (csrf) {
119
92
  formData.set("_csrf", csrf);
@@ -164,32 +137,7 @@ class VTOPSessionManager {
164
137
  this.state.username = null;
165
138
  await this.initialize();
166
139
  }
167
- detectCaptcha(html) {
168
- // reCAPTCHA signals
169
- const recaptchaDom = html.includes('id="recaptcha"') ||
170
- html.includes('id="g-recaptcha"') ||
171
- html.includes('class="g-recaptcha"');
172
- const recaptchaJs = html.includes("var captchaType=2");
173
- const isRecaptcha = recaptchaDom || recaptchaJs;
174
- const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
175
- const imgDataUriMatches = [
176
- ...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
177
- ];
178
- let imgDataUri = null;
179
- for (const match of imgDataUriMatches) {
180
- if (match[1] && !match[1].includes(";base64,null")) {
181
- imgDataUri = match[1];
182
- break;
183
- }
184
- }
185
- if (!imgDataUri && imgDataUriMatches.length > 0) {
186
- console.warn("Detected invalid captcha image (base64 is null)");
187
- }
188
- const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
189
- const csrf = this.extractCsrf(html);
190
- return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
191
- }
192
- async login(username, password, regNo, maxAttempts = 10) {
140
+ async login(username, password, maxAttempts = 10) {
193
141
  if (!this.state.initialized) {
194
142
  console.log("Session not initialized, initializing first...");
195
143
  await this.initialize();
@@ -202,10 +150,13 @@ class VTOPSessionManager {
202
150
  try {
203
151
  const res = await this.fetchWithCookies(LOGIN_PAGE);
204
152
  const body = await res.text();
205
- const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = this.detectCaptcha(body);
153
+ const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = detectCaptcha(body);
206
154
  const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
207
155
  const curServerID = this.state.cookies.get("SERVERID") || "(?)";
208
- console.log(`Attempt ${attempt}: status ${res.status} | text-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`);
156
+ // const logMsg = `Attempt ${attempt}: status ${res.status}\ntext-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`;
157
+ const logMsg = `Attempt ${attempt} text-captcha=${isTextCaptcha ? "YES" : "no"} recaptcha=${isRecaptcha ? "YES" : "no"} `;
158
+ console.log(logMsg);
159
+ this.events.emit("log", logMsg);
209
160
  if (isTextCaptcha) {
210
161
  let solvedCaptcha = "";
211
162
  if (imgDataUri) {
@@ -248,19 +199,7 @@ class VTOPSessionManager {
248
199
  loginForm.set("captchaStr", solvedCaptcha);
249
200
  const _cookies = `JSESSIONID=${curJsession}; SERVERID=${curServerID}`;
250
201
  const postHeaders = {
251
- "Cache-Control": "max-age=0",
252
- Origin: BASE,
253
- Referer: OPEN_PAGE_ALT,
254
- "Content-Type": "application/x-www-form-urlencoded",
255
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
256
- "Accept-Encoding": "gzip, deflate, br, zstd",
257
- "Accept-Language": "en-US,en;q=0.9",
258
- "Sec-Fetch-Dest": "document",
259
- "Sec-Fetch-Mode": "navigate",
260
- "Sec-Fetch-Site": "same-origin",
261
- "Sec-Fetch-User": "?1",
262
- "Upgrade-Insecure-Requests": "1",
263
- Priority: "u=0, i",
202
+ ...LOGIN_POST_HEADERS,
264
203
  Cookie: _cookies,
265
204
  };
266
205
  try {
@@ -287,7 +226,7 @@ class VTOPSessionManager {
287
226
  console.log("Login POST submitted successfully!");
288
227
  this.state.loggedIn = true;
289
228
  this.state.username = username;
290
- this.state.regNo = regNo;
229
+ this.events.emit("login-complete");
291
230
  return true;
292
231
  }
293
232
  catch (e) {
@@ -307,6 +246,7 @@ class VTOPSessionManager {
307
246
  }
308
247
  }
309
248
  console.log(`Login failed after ${maxAttempts} attempts`);
249
+ this.events.emit("login-complete");
310
250
  return false;
311
251
  }
312
252
  isLoggedIn() {
@@ -322,14 +262,7 @@ class VTOPSessionManager {
322
262
  }
323
263
  const cookies = this.getCookieHeader();
324
264
  const headers = {
325
- Referer: LOGIN_PAGE,
326
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
327
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
328
- "Accept-Encoding": "gzip, deflate, br, zstd",
329
- "Cache-Control": "max-age=0",
330
- "Upgrade-Insecure-Requests": "1",
331
- Origin: BASE,
332
- "Content-Type": "application/x-www-form-urlencoded",
265
+ ...POST_LOGIN_HEADERS,
333
266
  Cookie: cookies,
334
267
  };
335
268
  try {
@@ -347,11 +280,19 @@ class VTOPSessionManager {
347
280
  const contentRes = await this.fetchWithCookies(CONTENT, { headers });
348
281
  const contentHtml = await contentRes.text();
349
282
  console.log(` -> /vtop/content: ${contentRes.status}`);
350
- const newCsrf = this.extractCsrf(contentHtml);
283
+ const newCsrf = extractCsrf(contentHtml);
351
284
  if (newCsrf) {
352
285
  this.state.csrf = newCsrf;
353
286
  console.log(`Updated CSRF token from content page`);
354
287
  }
288
+ const regNo = extractRegNo(contentHtml);
289
+ if (regNo) {
290
+ this.state.regNo = regNo;
291
+ console.log(`Extracted registration number: ${regNo}`);
292
+ }
293
+ else {
294
+ console.warn("Failed to extract registration number from content page");
295
+ }
355
296
  console.log("Post-login navigation complete");
356
297
  return true;
357
298
  }
@@ -360,102 +301,13 @@ class VTOPSessionManager {
360
301
  return false;
361
302
  }
362
303
  }
363
- //slop
364
- parseAssignmentsHtml(html) {
365
- const assignments = [];
366
- try {
367
- const jsonMatch = html.match(/\[[\s\S]*?\]/);
368
- if (jsonMatch) {
369
- const parsed = JSON.parse(jsonMatch[0]);
370
- if (Array.isArray(parsed)) {
371
- return parsed.map((item) => ({
372
- courseCode: String(item.courseCode || item.code || ""),
373
- courseName: String(item.courseName || item.name || ""),
374
- assignmentTitle: String(item.assignmentTitle || item.title || item.assignmentName || ""),
375
- dueDate: String(item.dueDate || item.endDate || item.deadline || ""),
376
- status: String(item.status || ""),
377
- maxMarks: String(item.maxMarks || item.marks || ""),
378
- }));
379
- }
380
- }
381
- }
382
- catch {
383
- // Not JSON, try HTML parsing
384
- }
385
- // Parse HTML table rows using regex
386
- // VTOP table structure: #(th), Course Name(td), Title(td), Last Date(td), Uploaded(td)
387
- // Note: VTOP HTML sometimes has malformed tags like <<td
388
- const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
389
- // Match both <th> and <td> cells, and handle malformed <<td
390
- const cellRegex = /<?<t[hd][^>]*>([\s\S]*?)(?:<\/t[hd]>|<\/tr|<tr|$)/gi;
391
- let rowMatch;
392
- while ((rowMatch = rowRegex.exec(html)) !== null) {
393
- const cells = [];
394
- let cellMatch;
395
- const rowContent = rowMatch[1];
396
- // Reset lastIndex for cell regex
397
- cellRegex.lastIndex = 0;
398
- while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
399
- // Strip HTML tags from cell content
400
- const cellContent = cellMatch[1].replace(/<[^>]+>/g, "").trim();
401
- cells.push(cellContent);
402
- }
403
- // Skip header rows (first cell would be "#" or similar header text)
404
- if (cells.length >= 4 && cells[0] !== "#" && !isNaN(Number(cells[0]))) {
405
- // Table structure: # (row num), Course Name, Title, Last Date, Uploaded
406
- // cells[0] = row number (skip)
407
- // cells[1] = Course Name
408
- // cells[2] = Title (assignment title)
409
- // cells[3] = Last Date (due date)
410
- // cells[4] = Uploaded (status)
411
- assignments.push({
412
- courseCode: "", // Not in this table format
413
- courseName: cells[1] || "",
414
- assignmentTitle: cells[2] || "",
415
- dueDate: cells[3] || "",
416
- status: cells[4] || "Pending",
417
- maxMarks: "", // Not in this table format
418
- });
419
- }
420
- }
421
- return assignments;
422
- }
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
- }
455
- }
456
- return courses;
457
- }
458
304
  async performAcademicsCheck(headers) {
305
+ if (!headers) {
306
+ headers = {
307
+ ...ACADEMICS_CHECK_HEADERS,
308
+ Cookie: this.getCookieHeader(),
309
+ };
310
+ }
459
311
  const now = new Date();
460
312
  const accParams = new URLSearchParams();
461
313
  accParams.set("authorizedID", this.state.regNo);
@@ -483,18 +335,8 @@ class VTOPSessionManager {
483
335
  const cookies = this.getCookieHeader();
484
336
  const now = new Date();
485
337
  const apiHeaders = {
486
- Accept: "*/*",
487
- "Accept-Encoding": "gzip, deflate, br, zstd",
488
- "Accept-Language": "en-US,en;q=0.7",
489
- "Content-Type": "application/x-www-form-urlencoded",
338
+ ...API_REQUEST_HEADERS,
490
339
  Cookie: cookies,
491
- Origin: BASE,
492
- Priority: "u=1, i",
493
- Referer: CONTENT,
494
- "Sec-Fetch-Dest": "empty",
495
- "Sec-Fetch-Mode": "cors",
496
- "Sec-Fetch-Site": "same-origin",
497
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
498
340
  };
499
341
  await this.performAcademicsCheck(apiHeaders);
500
342
  const assParams = new URLSearchParams();
@@ -511,7 +353,7 @@ class VTOPSessionManager {
511
353
  });
512
354
  const html = await res.text();
513
355
  console.log("Course details HTML:", html);
514
- const courses = this.parseCourseDetailsHtml(html);
356
+ const courses = parseCourseDetailsHtml(html);
515
357
  console.log(`Parsed ${courses.length} courses`);
516
358
  return courses;
517
359
  }
@@ -525,29 +367,14 @@ class VTOPSessionManager {
525
367
  console.error("Cannot fetch assignments: not logged in or no regNo");
526
368
  return [];
527
369
  }
528
- await this.navigatePostLogin();
370
+ // await this.navigatePostLogin();
529
371
  const cookies = this.getCookieHeader();
530
372
  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
373
  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",
374
+ ...API_REQUEST_HEADERS,
541
375
  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
376
  };
550
- await this.performAcademicsCheck(apiHeaders);
377
+ // await this.performAcademicsCheck(apiHeaders);
551
378
  const assParams = new URLSearchParams();
552
379
  assParams.set("authorizedID", this.state.regNo);
553
380
  if (this.state.csrf)
@@ -565,7 +392,7 @@ class VTOPSessionManager {
565
392
  console.log(` -> Response length: ${assBody.length} chars`);
566
393
  console.log(` -> Raw response:\n${assBody}`);
567
394
  if (assBody) {
568
- const assignments = this.parseAssignmentsHtml(assBody);
395
+ const assignments = parseAssignmentsHtml(assBody);
569
396
  console.log(` Parsed ${assignments.length} assignments`);
570
397
  return assignments;
571
398
  }
@@ -1,5 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
2
  import { BaseLayout } from "./layouts/Base.js";
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", {})] }), _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..." })] }) }) })] })] })] }));
3
+ export const Dashboard = ({ username, courses, assignments }) => {
4
+ return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-6 border-b border-border pb-4", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-xs", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsxs("div", { class: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("section", { class: "lg:col-span-8 xl:col-span-9 space-y-4", children: [_jsxs("div", { class: "flex items-center justify-between", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Course Details" }), _jsx("span", { class: "text-[0.65rem] text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), courses ? (courses.length === 0 ? (_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted text-sm", children: "No course details found." })) : (_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3", children: courses.map((course, i) => (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg flex flex-col justify-between h-full hover:border-muted transition-colors", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-1.5", children: [_jsx("span", { class: "text-[0.65rem] font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-xs mb-2 leading-relaxed line-clamp-2", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-2 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.6rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-base font-bold ${course.attendanceColor === "danger"
5
+ ? "text-red-500"
6
+ : course.attendanceColor === "warning"
7
+ ? "text-yellow-500"
8
+ : "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded bg-${course.attendanceColor === "danger"
9
+ ? "red"
10
+ : "green"}-500/10 text-${course.attendanceColor === "danger"
11
+ ? "red"
12
+ : "green"}-500`, children: course.remarks }))] })] }, i))) }))) : (_jsx("div", { id: "courses-loader", "hx-get": "/api/courses/html", "hx-trigger": "load", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 animate-pulse", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsx("div", { class: "h-28 bg-surface/50 border border-border rounded-lg" }, i))) }) }))] }), _jsxs("section", { class: "lg:col-span-4 xl:col-span-3 space-y-4", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Assignments" }), assignments ? (assignments.length === 0 ? (_jsx("div", { class: "p-6 text-center bg-surface border border-border rounded-lg", children: _jsx("p", { class: "text-sm text-muted", children: "No assignments pending." }) })) : (_jsx("div", { class: "flex flex-col gap-2", children: assignments.map((ass, i) => (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg hover:border-muted transition-colors group flex flex-col gap-1", children: [_jsxs("div", { class: "flex justify-between items-start gap-2", children: [_jsx("span", { class: "font-bold text-xs text-foreground line-clamp-1", title: ass.courseName, children: ass.courseName }), _jsxs("span", { class: "text-[0.65rem] font-medium text-red-400 whitespace-nowrap shrink-0", children: ["Due: ", ass.dueDate || "N/A"] })] }), _jsxs("div", { class: "flex justify-between items-end gap-2", children: [_jsxs("div", { class: "flex flex-col min-w-0", children: [_jsx("span", { class: "text-[0.7rem] text-muted truncate", title: ass.assignmentTitle, children: ass.assignmentTitle }), _jsx("span", { class: "text-[0.6rem] text-muted/60 font-mono", children: ass.courseCode })] }), _jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap shrink-0 ${ass.status?.toLowerCase().includes("pending")
13
+ ? "bg-red-500/10 text-red-500 border border-red-500/20"
14
+ : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] })] }, i))) }))) : (_jsx("div", { id: "assignments-loader", "hx-get": "/api/assignments/html", "hx-trigger": courses ? "load" : "htmx:afterOnLoad from:#courses-loader", "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-12 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-white mb-3" }), _jsx("p", { class: "text-xs", children: "Syncing assignments..." })] }) }) }))] })] })] }));
5
15
  };
@@ -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 Home = () => {
4
- return (_jsxs(BaseLayout, { title: "Open-VTOP", children: [_jsx("h1", { children: "Open-VTOP" }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD10 Login to VTOP" }), _jsxs("form", { id: "login-form", style: "display: flex; flex-direction: column; gap: 0.75rem; max-width: 400px;", children: [_jsxs("div", { children: [_jsx("label", { for: "username", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Username:" }), _jsx("input", { type: "text", id: "username", name: "username", placeholder: "Enter your VTOP username", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "password", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Password:" }), _jsx("input", { type: "password", id: "password", name: "password", placeholder: "Enter your password", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "regNo", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Registration Number:" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", placeholder: "e.g. 24BCI0150", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsx("button", { type: "submit", "hx-post": "/api/login/form", "hx-target": "#login-result", "hx-swap": "innerHTML", "hx-include": "#login-form", style: "padding: 0.75rem; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: bold;", children: "\uD83D\uDE80 Login" })] }), _jsx("div", { id: "login-result", style: "margin-top: 1rem;" })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCCA Login Status" }), _jsx("button", { "hx-get": "/api/login/status/html", "hx-target": "#login-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Login Status" }), _jsx("div", { id: "login-status", children: "Loading..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDA Upcoming Assignments" }), _jsx("button", { "hx-get": "/api/assignments/html", "hx-target": "#assignments-list", "hx-swap": "innerHTML", "hx-indicator": "#assignments-loading", style: "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: bold; cursor: pointer;", children: "\uD83D\uDD04 Load Assignments" }), _jsx("span", { id: "assignments-loading", class: "htmx-indicator", style: "margin-left: 0.5rem; color: #aaa;", children: "Loading..." }), _jsx("div", { id: "assignments-list", style: "margin-top: 1rem;", children: _jsx("div", { style: "color: #888; padding: 1rem; background: #f5f5f5; border-radius: 8px;", children: "Click \"Load Assignments\" to fetch your upcoming assignments from VTOP." }) })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD27 Session Status" }), _jsx("button", { "hx-get": "/api/session/status", "hx-target": "#session-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Session Status" }), _jsx("div", { id: "session-status", children: "Loading session status..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDD Debug Logs" }), _jsx("button", { "hx-get": "/api/debug/logs", "hx-target": "#debug-logs", "hx-swap": "innerHTML", "hx-trigger": "click", children: "Load Debug Logs" }), _jsx("button", { "hx-post": "/api/debug/logs/clear", "hx-target": "#debug-logs", "hx-swap": "innerHTML", style: "margin-left: 0.5rem; background: #ff6b6b;", children: "Clear Logs" }), _jsx("div", { id: "debug-logs", style: "margin-top: 1rem; max-height: 400px; overflow-y: auto; background: #1e1e1e; color: #0f0; font-family: monospace; padding: 1rem; border-radius: 4px; font-size: 0.85rem;", children: "Click \"Load Debug Logs\" to see login attempts..." })] })] }));
4
+ return (_jsxs(BaseLayout, { title: "Open-VTOP", children: [_jsx("h1", { children: "Open-VTOP" }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "Login to VTOP" }), _jsxs("form", { id: "login-form", style: "display: flex; flex-direction: column; gap: 0.75rem; max-width: 400px;", children: [_jsxs("div", { children: [_jsx("label", { for: "username", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Username:" }), _jsx("input", { type: "text", id: "username", name: "username", placeholder: "Enter your VTOP username", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "password", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Password:" }), _jsx("input", { type: "password", id: "password", name: "password", placeholder: "Enter your password", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "regNo", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Registration Number:" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", placeholder: "e.g. 22BCE0001", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsx("button", { type: "submit", "hx-post": "/api/login/form", "hx-target": "#login-result", "hx-swap": "innerHTML", "hx-include": "#login-form", style: "padding: 0.75rem; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: bold;", children: "\uD83D\uDE80 Login" })] }), _jsx("div", { id: "login-result", style: "margin-top: 1rem;" })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCCA Login Status" }), _jsx("button", { "hx-get": "/api/login/status/html", "hx-target": "#login-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Login Status" }), _jsx("div", { id: "login-status", children: "Loading..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDA Upcoming Assignments" }), _jsx("button", { "hx-get": "/api/assignments/html", "hx-target": "#assignments-list", "hx-swap": "innerHTML", "hx-indicator": "#assignments-loading", style: "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: bold; cursor: pointer;", children: "\uD83D\uDD04 Load Assignments" }), _jsx("span", { id: "assignments-loading", class: "htmx-indicator", style: "margin-left: 0.5rem; color: #aaa;", children: "Loading..." }), _jsx("div", { id: "assignments-list", style: "margin-top: 1rem;", children: _jsx("div", { style: "color: #888; padding: 1rem; background: #f5f5f5; border-radius: 8px;", children: "Click \"Load Assignments\" to fetch your upcoming assignments from VTOP." }) })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD27 Session Status" }), _jsx("button", { "hx-get": "/api/session/status", "hx-target": "#session-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Session Status" }), _jsx("div", { id: "session-status", children: "Loading session status..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDD Debug Logs" }), _jsx("button", { "hx-get": "/api/debug/logs", "hx-target": "#debug-logs", "hx-swap": "innerHTML", "hx-trigger": "click", children: "Load Debug Logs" }), _jsx("button", { "hx-post": "/api/debug/logs/clear", "hx-target": "#debug-logs", "hx-swap": "innerHTML", style: "margin-left: 0.5rem; background: #ff6b6b;", children: "Clear Logs" }), _jsx("div", { id: "debug-logs", style: "margin-top: 1rem; max-height: 400px; overflow-y: auto; background: #1e1e1e; color: #0f0; font-family: monospace; padding: 1rem; border-radius: 4px; font-size: 0.85rem;", children: "Click \"Load Debug Logs\" to see login attempts..." })] })] }));
5
5
  };
@@ -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 Login = () => {
4
- return (_jsx(BaseLayout, { title: "Login - Open-VTOP", children: _jsxs("div", { "x-data": "{ \n showPassword: false, \n loading: false \n }", class: "w-full", children: [_jsxs("div", { class: "text-center mb-8", children: [_jsx("h1", { class: "text-3xl font-bold tracking-tight mb-2", children: "Log In" }), _jsx("p", { class: "text-muted text-sm", children: "Enter your VTOP credentials to continue." })] }), _jsx("div", { class: "bg-surface border border-border rounded-lg p-6 shadow-sm", children: _jsxs("form", { id: "login-form", "hx-post": "/api/login/form", "hx-target": "body", "hx-swap": "outerHTML", "x-on:submit": "loading = true", "x-on:htmx:after-request": "loading = false", children: [_jsxs("div", { class: "mb-4", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "username", children: "Username" }), _jsx("input", { type: "text", id: "username", name: "username", required: true, autofocus: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted" })] }), _jsxs("div", { class: "mb-4", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "password", children: "Password" }), _jsxs("div", { class: "relative", children: [_jsx("input", { "x-bind:type": "showPassword ? 'text' : 'password'", id: "password", name: "password", required: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted pr-10" }), _jsxs("button", { type: "button", "x-on:click": "showPassword = !showPassword", class: "absolute inset-y-0 right-0 px-3 flex items-center text-muted hover:text-foreground transition-colors", children: [_jsx("span", { "x-show": "!showPassword", class: "text-xs font-medium", children: "SHOW" }), _jsx("span", { "x-show": "showPassword", style: "display: none;", class: "text-xs font-medium", children: "HIDE" })] })] })] }), _jsxs("div", { class: "mb-6", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "regNo", children: "Registration Number" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", required: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted" })] }), _jsxs("button", { type: "submit", class: "w-full bg-white text-black font-semibold py-2.5 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2", "x-bind:disabled": "loading", children: [_jsx("span", { "x-show": "!loading", children: "Log In" }), _jsxs("span", { "x-show": "loading", style: "display: none;", class: "flex items-center gap-2", children: [_jsxs("svg", { class: "animate-spin h-4 w-4 text-black", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [_jsx("circle", { class: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", "stroke-width": "4" }), _jsx("path", { class: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Authenticating..."] })] }), _jsx("div", { id: "error-message" })] }) }), _jsx("div", { class: "text-center mt-8 text-muted text-xs", children: _jsx("p", { children: "Open-VTOP \u00A9 2026" }) })] }) }));
4
+ return (_jsx(BaseLayout, { title: "Login - Open-VTOP", children: _jsx("div", { class: "min-h-[80vh] flex flex-col items-center justify-center w-full max-w-md mx-auto", "x-data": "{\n showPassword: false,\n loading: false\n }", children: _jsxs("div", { class: "w-full space-y-8", children: [_jsxs("div", { class: "flex flex-col items-center text-center", children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight text-white", children: "Welcome back" }), _jsxs("p", { class: "text-sm mt-2", children: ["Enter your vtop username and password.This app runs completely clientside and has no telemetry", " "] })] }), _jsxs("div", { class: "bg-surface/50 backdrop-blur-sm border border-border rounded-xl p-8 shadow-xl", children: [_jsxs("form", { id: "login-form", "hx-post": "/api/login/form", "hx-target": "body", "hx-swap": "outerHTML", "x-on:submit": "loading = true", "x-on:htmx:after-request": "loading = false", class: "space-y-5", children: [_jsxs("div", { class: "space-y-2", children: [_jsx("label", { class: "text-xs font-medium text-muted uppercase tracking-wider", for: "username", children: "Username" }), _jsx("input", { type: "text", id: "username", name: "username", required: true, autofocus: true, placeholder: "Enter your username", class: "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-sm text-foreground focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/40 transition-all placeholder-muted/50" })] }), _jsxs("div", { class: "space-y-2", children: [_jsx("label", { class: "text-xs font-medium text-muted uppercase tracking-wider", for: "password", children: "Password" }), _jsxs("div", { class: "relative", children: [_jsx("input", { "x-bind:type": "showPassword ? 'text' : 'password'", id: "password", name: "password", required: true, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", class: "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-sm text-foreground focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/40 transition-all placeholder-muted/50 pr-12" }), _jsxs("button", { type: "button", "x-on:click": "showPassword = !showPassword", class: "absolute inset-y-0 right-0 px-3 flex items-center text-muted hover:text-foreground transition-colors", children: [_jsx("span", { "x-show": "!showPassword", class: "text-[0.65rem] font-bold tracking-wider opacity-70", children: "SHOW" }), _jsx("span", { "x-show": "showPassword", style: "display: none;", class: "text-[0.65rem] font-bold tracking-wider opacity-70", children: "HIDE" })] })] })] }), _jsxs("button", { type: "submit", class: "w-full bg-white text-black font-bold text-sm py-3 rounded-lg hover:bg-gray-100 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-white transition-all disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2 mt-2", "x-bind:disabled": "loading", children: [_jsx("span", { "x-show": "!loading", children: "Sign In" }), _jsxs("span", { "x-show": "loading", style: "display: none;", class: "flex items-center gap-2", children: [_jsxs("svg", { class: "animate-spin h-4 w-4 text-black", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [_jsx("circle", { class: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", "stroke-width": "4" }), _jsx("path", { class: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Authenticating..."] })] }), _jsx("div", { id: "error-message" })] }), _jsx("template", { "x-if": "loading", children: _jsx("div", { class: "mt-6", "x-init": "htmx.process($el)", children: _jsxs("div", { class: "relative bg-black/40 border border-white/10 rounded-lg p-4 overflow-hidden group", children: [_jsx("div", { class: "absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" }), _jsxs("div", { class: "flex items-center gap-3", children: [_jsx("div", { class: "flex-shrink-0", children: _jsxs("div", { class: "relative flex h-3 w-3", children: [_jsx("span", { class: "animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" }), _jsx("span", { class: "relative inline-flex rounded-full h-3 w-3 bg-green-500" })] }) }), _jsx("div", { class: "font-mono text-xs text-green-200/90 w-full truncate", "hx-ext": "sse", "sse-connect": "/api/login/events", "sse-swap": "log", "sse-close": "login-complete", "hx-swap": "innerHTML", children: "Initializing connection..." })] })] }) }) })] })] }) }) }));
5
5
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
2
  export const BaseLayout = ({ title, children, }) => {
3
- return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("script", { src: "/static/htmx.js" }), _jsx("script", { src: "https://cdn.tailwindcss.com" }), _jsx("script", { dangerouslySetInnerHTML: {
3
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("script", { src: "/static/htmx.js" }), _jsx("script", { src: "/static/sse.js" }), _jsx("script", { src: "https://cdn.tailwindcss.com" }), _jsx("script", { dangerouslySetInnerHTML: {
4
4
  __html: `
5
5
  tailwind.config = {
6
6
  theme: {
@@ -39,5 +39,5 @@ export const BaseLayout = ({ title, children, }) => {
39
39
  opacity: 1;
40
40
  }
41
41
  `,
42
- } })] }), _jsx("body", { class: "bg-background text-foreground antialiased min-h-screen flex flex-col items-center justify-center p-4", children: _jsx("main", { class: "w-full max-w-md flex flex-col gap-6", children: children }) })] }));
42
+ } })] }), _jsx("body", { class: "bg-background text-foreground antialiased min-h-screen", children: _jsx("main", { class: "w-full max-w-[1800px] mx-auto p-4 md:p-6 flex flex-col gap-6", children: children }) })] }));
43
43
  };
package/package.json CHANGED
@@ -3,19 +3,22 @@
3
3
  "type": "module",
4
4
  "scripts": {
5
5
  "dev": "tsx watch src/index.tsx",
6
- "build": "tsc",
7
- "start": "node dist/index.js",
6
+ "dev-cli": "node src/cli.cjs",
7
+ "build": "tsc && cp src/cli.cjs dist/cli.cjs",
8
+ "start": "node dist/cli.cjs",
9
+ "logs": "node dist/cli.cjs logs",
8
10
  "prepare": "npm run build"
9
11
  },
10
12
  "dependencies": {
11
13
  "@hono/node-server": "^1.19.9",
12
14
  "hono": "^4.11.4",
15
+ "htmx-ext-sse": "^2.2.4",
13
16
  "htmx.org": "^2.0.8",
14
17
  "jpeg-js": "^0.4.4",
15
18
  "pngjs": "^7.0.0"
16
19
  },
17
20
  "bin": {
18
- "open-vtop": "dist/index.js"
21
+ "open-vtop": "dist/cli.cjs"
19
22
  },
20
23
  "files": [
21
24
  "dist"
@@ -27,5 +30,5 @@
27
30
  "tsx": "^4.7.1",
28
31
  "typescript": "^5.8.3"
29
32
  },
30
- "version": "1.0.6"
33
+ "version": "1.0.7"
31
34
  }
@@ -1,4 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- export const Container = ({ title, children }) => {
3
- return (_jsxs("div", { class: "container", children: [_jsx("h2", { children: title }), children] }));
4
- };
@@ -1,4 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- export const Item = ({ id, name }) => {
3
- return (_jsxs("div", { id: `item-${id}`, class: "item", children: [name, _jsx("button", { "hx-delete": `/api/items/${id}`, "hx-confirm": "Are you sure you want to delete this item?", "hx-target": `#item-${id}`, "hx-swap": "outerHTML", class: "danger", children: "Delete" })] }));
4
- };