open-vtop 1.0.7 → 1.0.9
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 +9 -8
- package/dist/components/AssignmentsList.js +4 -3
- package/dist/components/CoursesList.js +6 -3
- package/dist/components/ExamsList.js +4 -0
- package/dist/constants.js +18 -0
- package/dist/index.js +35 -5
- package/dist/parsers.js +45 -0
- package/dist/session-manager.js +122 -5
- package/dist/views/Dashboard.js +3 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,14 +9,15 @@ npx open-vtop logs
|
|
|
9
9
|
bunx open-vtop logs
|
|
10
10
|
```
|
|
11
11
|
## Todo
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
- [x] Save USN and password for future use
|
|
13
|
+
- [x] Grab regno from the responses itself
|
|
14
|
+
- [x] Attendance
|
|
15
|
+
- [ ] Timetable
|
|
16
|
+
- [ ] CGPA (unsure if people actually want to see their marks and grades on this)
|
|
17
|
+
- [ ] Course page
|
|
18
|
+
- [ ] Calendar
|
|
19
|
+
- [x] QoL: automatic browser open, better logging
|
|
20
|
+
- [x] Upcoming exams
|
|
20
21
|
|
|
21
22
|
## Getting Started
|
|
22
23
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
import { AssignmentCard } from "./AssignmentCard.js";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
3
2
|
export function AssignmentsList({ assignments, }) {
|
|
4
|
-
return (_jsx("div", { class: "
|
|
3
|
+
return (_jsx("div", { class: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { class: "w-full text-xs text-left", children: [_jsx("thead", { class: "bg-surface border-b border-border/50 text-[0.65rem] uppercase tracking-wider text-muted font-bold", children: _jsxs("tr", { children: [_jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Course" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Assignment" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Due Date" }), _jsx("th", { class: "px-3 py-2 text-right text-foreground/80", children: "Status" })] }) }), _jsx("tbody", { class: "divide-y divide-border/30", children: assignments.map((ass, i) => (_jsxs("tr", { class: "hover:bg-muted/5 transition-colors group", children: [_jsx("td", { class: "px-3 py-2 font-mono text-[0.7rem] text-muted whitespace-nowrap", title: ass.courseName, children: ass.courseCode }), _jsx("td", { class: "px-3 py-2 font-medium text-foreground max-w-[150px] truncate", title: ass.assignmentTitle, children: ass.assignmentTitle }), _jsx("td", { class: "px-3 py-2 text-red-400 whitespace-nowrap text-[0.7rem]", children: ass.dueDate || "N/A" }), _jsx("td", { class: "px-3 py-2 text-right", children: _jsx("span", { class: `inline-block text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap ${ass.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: ass.status || "Pending" }) })] }, i))) })] }) }));
|
|
5
6
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
import { CourseCard } from "./CourseCard.js";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
3
2
|
export function CoursesList({ courses }) {
|
|
4
|
-
return (_jsx("div", { class: "
|
|
3
|
+
return (_jsx("div", { class: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { class: "w-full text-xs text-left", children: [_jsx("thead", { class: "bg-surface border-b border-border/50 text-[0.65rem] uppercase tracking-wider text-muted font-bold", children: _jsxs("tr", { children: [_jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Code" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Course Name" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Type" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Attendance" }), _jsx("th", { class: "px-3 py-2 text-right text-foreground/80", children: "Remarks" })] }) }), _jsx("tbody", { class: "divide-y divide-border/30", children: courses.map((course, i) => (_jsxs("tr", { class: "hover:bg-muted/5 transition-colors group", children: [_jsx("td", { class: "px-3 py-2 font-mono text-[0.7rem] text-muted", children: course.code }), _jsx("td", { class: "px-3 py-2 font-medium text-foreground max-w-[200px] truncate", title: course.name, children: course.name }), _jsx("td", { class: "px-3 py-2", children: _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type }) }), _jsx("td", { class: "px-3 py-2", children: _jsxs("span", { class: `font-bold ${course.attendanceColor === "danger"
|
|
4
|
+
? "text-red-500"
|
|
5
|
+
: course.attendanceColor === "warning"
|
|
6
|
+
? "text-yellow-500"
|
|
7
|
+
: "text-green-500"}`, children: [course.attendance, "%"] }) }), _jsx("td", { class: "px-3 py-2 text-right", children: course.remarks && (_jsx("span", { class: `inline-block 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 whitespace-nowrap`, children: course.remarks })) })] }, i))) })] }) }));
|
|
5
8
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
export const ExamsList = ({ exams }) => {
|
|
3
|
+
return (_jsx("div", { class: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { class: "w-full text-xs text-left", children: [_jsx("thead", { class: "bg-surface border-b border-border/50 text-[0.65rem] uppercase tracking-wider text-muted font-bold", children: _jsxs("tr", { children: [_jsx("th", { class: "px-3 py-2", children: "Code" }), _jsx("th", { class: "px-3 py-2", children: "Course Title" }), _jsx("th", { class: "px-3 py-2", children: "Slot" }), _jsx("th", { class: "px-3 py-2", children: "Date (Session)" }), _jsx("th", { class: "px-3 py-2", children: "Time" }), _jsx("th", { class: "px-3 py-2", children: "Venue" }), _jsx("th", { class: "px-3 py-2 text-right", children: "Seat" })] }) }), _jsx("tbody", { class: "divide-y divide-border/30", children: exams.map((exam) => (_jsxs("tr", { class: "hover:bg-muted/5 transition-colors group", children: [_jsxs("td", { class: "px-3 py-2 font-semibold text-foreground whitespace-nowrap", children: [exam.courseCode, _jsx("span", { class: "ml-1.5 text-[0.6rem] px-1 rounded border border-border text-muted font-normal", children: exam.courseType })] }), _jsx("td", { class: "px-3 py-2 font-medium text-foreground/90 max-w-[200px] truncate", title: exam.courseTitle, children: exam.courseTitle }), _jsx("td", { class: "px-3 py-2 text-muted whitespace-nowrap font-mono text-[0.65rem]", children: exam.slot }), _jsxs("td", { class: "px-3 py-2 text-foreground/80 whitespace-nowrap", children: [exam.examDate, _jsx("span", { class: "ml-1.5 text-[0.6rem] font-bold text-blue-400 bg-blue-400/10 px-1 rounded", children: exam.examSession })] }), _jsx("td", { class: "px-3 py-2 text-muted whitespace-nowrap font-mono text-[0.65rem]", children: exam.examTime }), _jsx("td", { class: "px-3 py-2 text-foreground/80 whitespace-nowrap truncate max-w-[100px]", title: exam.venue, children: exam.venue }), _jsxs("td", { class: "px-3 py-2 text-right font-mono text-muted text-[0.65rem] whitespace-nowrap", children: [exam.seatLocation, _jsx("span", { class: "mx-1 text-border", children: "/" }), _jsx("span", { class: "text-foreground", children: exam.seatNo })] })] }))) })] }) }));
|
|
4
|
+
};
|
package/dist/constants.js
CHANGED
|
@@ -12,6 +12,7 @@ export const CONTENT = `${BASE}/vtop/content`;
|
|
|
12
12
|
export const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
|
|
13
13
|
export const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
|
|
14
14
|
export const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
|
|
15
|
+
export const EXAM_SCHEDULE = `${BASE}/vtop/examinations/doSearchExamScheduleForStudent`;
|
|
15
16
|
// HTTP Headers
|
|
16
17
|
// Common browser headers
|
|
17
18
|
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";
|
|
@@ -80,3 +81,20 @@ export const ACADEMICS_CHECK_HEADERS = {
|
|
|
80
81
|
Referer: CONTENT,
|
|
81
82
|
"User-Agent": USER_AGENT_CHROME_139,
|
|
82
83
|
};
|
|
84
|
+
export const LOGOUT_URL = `${BASE}/vtop/logout`;
|
|
85
|
+
export const LOGOUT_HEADERS = {
|
|
86
|
+
Accept: ACCEPT_HTML_EXTENDED,
|
|
87
|
+
"Accept-Encoding": ACCEPT_ENCODING_ZSTD,
|
|
88
|
+
"Accept-Language": ACCEPT_LANGUAGE,
|
|
89
|
+
"Cache-Control": "no-cache",
|
|
90
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
91
|
+
Origin: BASE,
|
|
92
|
+
Pragma: "no-cache",
|
|
93
|
+
Priority: "u=0, i",
|
|
94
|
+
Referer: CONTENT,
|
|
95
|
+
"Sec-Fetch-Dest": "document",
|
|
96
|
+
"Sec-Fetch-Mode": "navigate",
|
|
97
|
+
"Sec-Fetch-Site": "same-origin",
|
|
98
|
+
"Sec-Fetch-User": "?1",
|
|
99
|
+
"Upgrade-Insecure-Requests": "1",
|
|
100
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CoursesList } from "./components/CoursesList.js";
|
|
|
12
12
|
import { AssignmentsList } from "./components/AssignmentsList.js";
|
|
13
13
|
import { EmptyState } from "./components/EmptyState.js";
|
|
14
14
|
import { SessionExpired } from "./components/SessionExpired.js";
|
|
15
|
+
import { ExamsList } from "./components/ExamsList.js";
|
|
15
16
|
const app = new Hono();
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
|
|
@@ -55,6 +56,10 @@ app.post("/api/login/form", async (c) => {
|
|
|
55
56
|
return c.html(_jsx(ErrorMessage, { message: `Server error: ${String(error)}` }));
|
|
56
57
|
}
|
|
57
58
|
});
|
|
59
|
+
app.post("/api/logout", async (c) => {
|
|
60
|
+
await sessionManager.logout();
|
|
61
|
+
return c.redirect("/login");
|
|
62
|
+
});
|
|
58
63
|
app.get("/api/login/events", async (c) => {
|
|
59
64
|
return streamSSE(c, async (stream) => {
|
|
60
65
|
console.log("SSE connected");
|
|
@@ -120,9 +125,34 @@ app.get("/api/assignments/html", async (c) => {
|
|
|
120
125
|
return c.html(_jsx(ErrorMessage, { message: `Failed to load assignments: ${String(error)}` }));
|
|
121
126
|
}
|
|
122
127
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
+
app.get("/api/exams/html", async (c) => {
|
|
129
|
+
if (!sessionManager.isLoggedIn()) {
|
|
130
|
+
return c.html(_jsx(SessionExpired, {}));
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const exams = await sessionManager.fetchExamSchedule();
|
|
134
|
+
if (exams.length === 0) {
|
|
135
|
+
return c.html(_jsx(EmptyState, { message: "No exam schedule found for this semester." }));
|
|
136
|
+
}
|
|
137
|
+
return c.html(_jsx(ExamsList, { exams: exams }));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return c.html(_jsx(ErrorMessage, { message: `Failed to load exam schedule: ${String(error)}` }));
|
|
141
|
+
}
|
|
128
142
|
});
|
|
143
|
+
const main = async () => {
|
|
144
|
+
try {
|
|
145
|
+
await sessionManager.initialize();
|
|
146
|
+
await sessionManager.tryAutoLogin();
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
console.error("Initialization failed:", e);
|
|
150
|
+
}
|
|
151
|
+
serve({
|
|
152
|
+
fetch: app.fetch,
|
|
153
|
+
port: 6767,
|
|
154
|
+
}, (info) => {
|
|
155
|
+
console.log(`Server is running on http://localhost:${info.port}`);
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
main();
|
package/dist/parsers.js
CHANGED
|
@@ -136,3 +136,48 @@ export function parseCourseDetailsHtml(html) {
|
|
|
136
136
|
}
|
|
137
137
|
return courses;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Parses exam schedule from HTML response
|
|
141
|
+
*/
|
|
142
|
+
export function parseExamScheduleHtml(html) {
|
|
143
|
+
const exams = [];
|
|
144
|
+
// Find the table content rows
|
|
145
|
+
const rowRegex = /<tr[^>]*class="tableContent"[^>]*>([\s\S]*?)<\/tr>/g;
|
|
146
|
+
const matches = [...html.matchAll(rowRegex)];
|
|
147
|
+
for (const match of matches) {
|
|
148
|
+
const rowContent = match[1];
|
|
149
|
+
// Skip rows that are just headers (e.g., colspan="13")
|
|
150
|
+
if (rowContent.includes('colspan="13"')) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const cells = [];
|
|
154
|
+
const cellRegex = /<td[^>]*>([\s\S]*?)<\/td>/g;
|
|
155
|
+
const cellMatches = [...rowContent.matchAll(cellRegex)];
|
|
156
|
+
for (const cellMatch of cellMatches) {
|
|
157
|
+
// Strip HTML and whitespace
|
|
158
|
+
let content = cellMatch[1].replace(/<[^>]+>/g, "").trim();
|
|
159
|
+
// Replace multiple spaces/newlines with single space
|
|
160
|
+
content = content.replace(/\s+/g, " ");
|
|
161
|
+
// Remove "-" placeholders if preferred, or keep them. keeping them is fine.
|
|
162
|
+
cells.push(content);
|
|
163
|
+
}
|
|
164
|
+
if (cells.length >= 13) {
|
|
165
|
+
exams.push({
|
|
166
|
+
sNo: cells[0],
|
|
167
|
+
courseCode: cells[1],
|
|
168
|
+
courseTitle: cells[2],
|
|
169
|
+
courseType: cells[3],
|
|
170
|
+
classId: cells[4],
|
|
171
|
+
slot: cells[5],
|
|
172
|
+
examDate: cells[6],
|
|
173
|
+
examSession: cells[7],
|
|
174
|
+
reportingTime: cells[8], // The HTML has separate columns but let's map them by index
|
|
175
|
+
examTime: cells[9],
|
|
176
|
+
venue: cells[10],
|
|
177
|
+
seatLocation: cells[11],
|
|
178
|
+
seatNo: cells[12],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return exams;
|
|
183
|
+
}
|
package/dist/session-manager.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
//nvm i did sepearations of concerns
|
|
3
3
|
import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import * as fs from "fs/promises";
|
|
5
6
|
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";
|
|
7
|
+
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, LOGOUT_URL, LOGOUT_HEADERS, EXAM_SCHEDULE, } from "./constants.js";
|
|
8
|
+
import { extractCsrf, extractRegNo, detectCaptcha, parseAssignmentsHtml, parseCourseDetailsHtml, parseExamScheduleHtml, } from "./parsers.js";
|
|
9
|
+
const CREDENTIALS_FILE = path.resolve(process.cwd(), ".credentials.json");
|
|
8
10
|
class VTOPSessionManager {
|
|
9
11
|
state = {
|
|
10
12
|
cookies: new Map(),
|
|
@@ -226,6 +228,7 @@ class VTOPSessionManager {
|
|
|
226
228
|
console.log("Login POST submitted successfully!");
|
|
227
229
|
this.state.loggedIn = true;
|
|
228
230
|
this.state.username = username;
|
|
231
|
+
await this.saveCredentials(username, password);
|
|
229
232
|
this.events.emit("login-complete");
|
|
230
233
|
return true;
|
|
231
234
|
}
|
|
@@ -402,8 +405,122 @@ class VTOPSessionManager {
|
|
|
402
405
|
}
|
|
403
406
|
return [];
|
|
404
407
|
}
|
|
408
|
+
async fetchExamSchedule() {
|
|
409
|
+
if (!this.state.loggedIn || !this.state.regNo) {
|
|
410
|
+
console.error("Cannot fetch exam schedule: not logged in or no regNo");
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
const cookies = this.getCookieHeader();
|
|
414
|
+
const now = new Date(); // Although payload doesn't seem to use 'x' timestamp, we'll keep it consistent if needed or omit.
|
|
415
|
+
// Based on user image, payload is: authorizedID, _csrf, semesterSubId.
|
|
416
|
+
const apiHeaders = {
|
|
417
|
+
...API_REQUEST_HEADERS,
|
|
418
|
+
Cookie: cookies,
|
|
419
|
+
};
|
|
420
|
+
const params = new URLSearchParams();
|
|
421
|
+
params.set("authorizedID", this.state.regNo);
|
|
422
|
+
if (this.state.csrf)
|
|
423
|
+
params.set("_csrf", this.state.csrf);
|
|
424
|
+
params.set("semesterSubId", "VL20252605"); // Hardcoded as requested
|
|
425
|
+
try {
|
|
426
|
+
console.log("Fetching exam schedule...");
|
|
427
|
+
const res = await fetch(EXAM_SCHEDULE, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
headers: apiHeaders,
|
|
430
|
+
body: params.toString(),
|
|
431
|
+
});
|
|
432
|
+
console.log(` -> Exam schedule status: ${res.status}`);
|
|
433
|
+
const html = await res.text();
|
|
434
|
+
// console.log(` -> Exam schedule response length: ${html.length}`);
|
|
435
|
+
if (html) {
|
|
436
|
+
const schedule = parseExamScheduleHtml(html);
|
|
437
|
+
console.log(` Parsed ${schedule.length} exam entries`);
|
|
438
|
+
return schedule;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
console.error("Failed to fetch exam schedule:", e);
|
|
443
|
+
}
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
async logout() {
|
|
447
|
+
if (!this.state.loggedIn) {
|
|
448
|
+
console.log("Already logged out or not logged in.");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
console.log("Logging out...");
|
|
452
|
+
// Attempt to notify VTOP server
|
|
453
|
+
try {
|
|
454
|
+
const cookies = this.getCookieHeader();
|
|
455
|
+
const headers = {
|
|
456
|
+
...LOGOUT_HEADERS,
|
|
457
|
+
Cookie: cookies,
|
|
458
|
+
};
|
|
459
|
+
const formData = new URLSearchParams();
|
|
460
|
+
if (this.state.csrf) {
|
|
461
|
+
formData.set("_csrf", this.state.csrf);
|
|
462
|
+
}
|
|
463
|
+
const res = await fetch(LOGOUT_URL, {
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers,
|
|
466
|
+
body: formData.toString(),
|
|
467
|
+
redirect: "manual",
|
|
468
|
+
});
|
|
469
|
+
console.log(` -> Logout POST status: ${res.status}`);
|
|
470
|
+
if (res.status === 302 || res.status === 200) {
|
|
471
|
+
console.log("Server session cleared effectively.");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (e) {
|
|
475
|
+
console.warn("Logout request failed (network error?), clearing local session anyway.", e);
|
|
476
|
+
}
|
|
477
|
+
// Clear local session
|
|
478
|
+
this.state.cookies.clear();
|
|
479
|
+
this.state.csrf = null;
|
|
480
|
+
this.state.initialized = false;
|
|
481
|
+
this.state.loggedIn = false;
|
|
482
|
+
this.state.username = null;
|
|
483
|
+
this.state.regNo = null;
|
|
484
|
+
this.events.emit("logout");
|
|
485
|
+
console.log("Local session cleared.");
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
async saveCredentials(username, password) {
|
|
489
|
+
try {
|
|
490
|
+
await fs.writeFile(CREDENTIALS_FILE, JSON.stringify({ username, password }));
|
|
491
|
+
console.log("Credentials saved locally.");
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
console.error("Failed to save credentials:", e);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async loadCredentials() {
|
|
498
|
+
try {
|
|
499
|
+
const data = await fs.readFile(CREDENTIALS_FILE, "utf-8");
|
|
500
|
+
return JSON.parse(data);
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async tryAutoLogin() {
|
|
507
|
+
const creds = await this.loadCredentials();
|
|
508
|
+
if (creds) {
|
|
509
|
+
console.log(`Found saved credentials for user: ${creds.username}`);
|
|
510
|
+
console.log("Attemping auto-login...");
|
|
511
|
+
const success = await this.login(creds.username, creds.password);
|
|
512
|
+
if (success) {
|
|
513
|
+
console.log("Auto-login successful!");
|
|
514
|
+
await this.navigatePostLogin();
|
|
515
|
+
await this.performAcademicsCheck();
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
console.log("Auto-login failed.");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
console.log("No saved credentials found.");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
405
525
|
}
|
|
406
526
|
export const sessionManager = new VTOPSessionManager();
|
|
407
|
-
sessionManager.initialize().catch((error) => {
|
|
408
|
-
console.error("Failed to auto-initialize session:", error);
|
|
409
|
-
});
|
package/dist/views/Dashboard.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
2
|
import { BaseLayout } from "./layouts/Base.js";
|
|
3
|
+
import { CoursesList } from "../components/CoursesList.js";
|
|
4
|
+
import { AssignmentsList } from "../components/AssignmentsList.js";
|
|
3
5
|
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: "
|
|
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..." })] }) }) }))] })] })] }));
|
|
6
|
+
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", { children: _jsx("form", { action: "/api/logout", method: "post", children: _jsx("button", { type: "submit", class: "text-xs text-red-400 hover:text-red-300 transition-colors font-medium cursor-pointer", children: "Logout" }) }) })] }), _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: "Attendance" }), _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(CoursesList, { courses: courses }))) : (_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(AssignmentsList, { assignments: assignments }))) : (_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..." })] }) }) }))] }), _jsxs("section", { class: "lg:col-span-12 space-y-4 pt-4 border-t border-border/50", children: [_jsxs("div", { class: "flex items-center justify-between", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Exam Schedule" }), _jsx("span", { class: "text-[0.65rem] text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), _jsx("div", { id: "exams-loader", "hx-get": "/api/exams/html", "hx-trigger": courses ? "load" : "htmx:afterOnLoad from:#courses-loader", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 animate-pulse", children: [1, 2, 3].map((i) => (_jsx("div", { class: "h-40 bg-surface/50 border border-border rounded-lg" }, i))) }) })] })] })] }));
|
|
15
7
|
};
|
package/package.json
CHANGED