Compare commits
10 Commits
9fd0b59dfb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a2ae277a0f | |||
| 82832117a3 | |||
| 4ee5f17566 | |||
| ff5bd9574e | |||
| e40b8fd0fe | |||
| 4571094527 | |||
| d5f45dba8f | |||
| dbba005327 | |||
| 8f01eaacb5 | |||
| e8730fafb0 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -34,5 +34,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
skyward_screenshots/**
|
skyward_screenshots/**
|
||||||
|
output/**
|
||||||
|
|
||||||
|
|
||||||
skywardstate.json
|
skywardstate.json
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM mcr.microsoft.com/playwright:v1.43.1-jammy
|
||||||
|
|
||||||
|
# Install dependencies needed for bun installer (unzip & curl)
|
||||||
|
RUN apt-get update && apt-get install -y unzip curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Bun and add it to PATH
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash && \
|
||||||
|
echo 'export PATH="/root/.bun/bin:$PATH"' >> ~/.bashrc
|
||||||
|
|
||||||
|
ENV PATH="/root/.bun/bin:$PATH"
|
||||||
|
ENV PORT=5565
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN bun install || npm install
|
||||||
|
RUN npx playwright install-deps
|
||||||
|
RUN npx playwright install
|
||||||
|
|
||||||
|
RUN bun add playwright-extra puppeteer-extra-plugin-stealth
|
||||||
|
|
||||||
|
CMD ["bun","run","."]
|
||||||
|
|
||||||
|
|
||||||
8
bun.lock
8
bun.lock
@@ -5,6 +5,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "skyward-analysis",
|
"name": "skyward-analysis",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"pino": "^10.1.0",
|
"pino": "^10.1.0",
|
||||||
@@ -29,6 +31,8 @@
|
|||||||
|
|
||||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
||||||
|
|
||||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="],
|
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="],
|
||||||
@@ -69,6 +73,8 @@
|
|||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
@@ -151,6 +157,8 @@
|
|||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||||
|
|||||||
341
index.ts
341
index.ts
@@ -1,10 +1,20 @@
|
|||||||
import { firefox } from 'playwright';
|
import { firefox } from 'playwright';
|
||||||
|
import type { Browser, BrowserContext, Page } from 'playwright';
|
||||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { Page } from 'playwright';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import cors from 'cors'; // ADD THIS
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: ['http://localhost:5173', 'http://localhost:4000'],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}));
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
@@ -16,9 +26,34 @@ const logger = pino({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const SCREENSHOTS_DIR = 'skyward_screenshots';
|
const OUTPUT_DIR = 'output';
|
||||||
const TARGET_CATEGORIES = ['NW1', 'NW2', 'SE1', 'NW3', 'NW4', 'SE2'];
|
const TARGET_CATEGORIES = ['NW1', 'NW2', 'SE1', 'NW3', 'NW4', 'SE2'];
|
||||||
|
|
||||||
|
// Browser pool to reuse browser instances
|
||||||
|
let sharedBrowser: Browser | null = null;
|
||||||
|
const activeSessions = new Set<string>();
|
||||||
|
|
||||||
|
async function getSharedBrowser(): Promise<Browser> {
|
||||||
|
if (!sharedBrowser) {
|
||||||
|
logger.info('Creating shared browser instance...');
|
||||||
|
sharedBrowser = await firefox.launch({
|
||||||
|
headless: true,
|
||||||
|
firefoxUserPrefs: {
|
||||||
|
'dom.webdriver.enabled': false,
|
||||||
|
'media.peerconnection.enabled': false,
|
||||||
|
'useAutomationExtension': false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle browser disconnection
|
||||||
|
sharedBrowser.on('disconnected', () => {
|
||||||
|
logger.warn('Browser disconnected, clearing shared instance');
|
||||||
|
sharedBrowser = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sharedBrowser;
|
||||||
|
}
|
||||||
|
|
||||||
// API endpoint to fetch grades
|
// API endpoint to fetch grades
|
||||||
app.post('/fetch-grades', async (req, res) => {
|
app.post('/fetch-grades', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
@@ -29,11 +64,26 @@ app.post('/fetch-grades', async (req, res) => {
|
|||||||
message: 'Please provide both username and password in request body'
|
message: 'Please provide both username and password in request body'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user already has an active session
|
||||||
|
if (activeSessions.has(username)) {
|
||||||
|
return res.status(429).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Request already in progress',
|
||||||
|
message: 'Please wait for your previous request to complete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`Received request to fetch grades for user: ${username}`);
|
logger.info(`Received request to fetch grades for user: ${username}`);
|
||||||
|
|
||||||
|
// Mark session as active
|
||||||
|
activeSessions.add(username);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const grades = await runSkywardBot(username, password);
|
const grades = await runSkywardBot(username, password);
|
||||||
|
|
||||||
|
// writeFileSync(`${OUTPUT_DIR}/grades_${username}_${Date.now()}.json`, JSON.stringify(grades, null, 2));
|
||||||
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -49,12 +99,19 @@ app.post('/fetch-grades', async (req, res) => {
|
|||||||
error: errorMsg,
|
error: errorMsg,
|
||||||
message: 'Failed to fetch grades from Skyward'
|
message: 'Failed to fetch grades from Skyward'
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
// Remove session from active sessions
|
||||||
|
activeSessions.delete(username);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({ status: 'ok', message: 'Skyward API is running' });
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
message: 'Skyward API is running',
|
||||||
|
activeSessions: activeSessions.size
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@@ -74,31 +131,42 @@ app.post('/check-auth', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const browser = await firefox.launch({ headless: true });
|
let context: BrowserContext | null = null;
|
||||||
const context = await browser.newContext();
|
let page: Page | null = null;
|
||||||
const page = await context.newPage();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authenticated = await checkAuth(page, username, password, context, []);
|
const browser = await getSharedBrowser();
|
||||||
|
context = await browser.newContext({
|
||||||
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64; rv:117.0) Gecko/20100101 Firefox/117.0',
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
});
|
||||||
|
page = await context.newPage();
|
||||||
|
|
||||||
if (!authenticated) {
|
const authenticated = await checkAuth(page, username, password, context, []);
|
||||||
|
|
||||||
|
if (!authenticated) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Authentication failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ success: true });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Authentication failed',
|
error: msg,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
if (page && !page.isClosed()) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
}
|
||||||
|
if (context) {
|
||||||
|
await context.close().catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({ success: true });
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
error: msg,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const runSkywardBot = async (username: string, password: string) => {
|
const runSkywardBot = async (username: string, password: string) => {
|
||||||
@@ -119,24 +187,17 @@ const runSkywardBot = async (username: string, password: string) => {
|
|||||||
logger.info('Starting Skyward bot with Firefox on Linux...');
|
logger.info('Starting Skyward bot with Firefox on Linux...');
|
||||||
|
|
||||||
// Create screenshots directory if it doesn't exist
|
// Create screenshots directory if it doesn't exist
|
||||||
if (!existsSync(SCREENSHOTS_DIR)) {
|
if (!existsSync(OUTPUT_DIR)) {
|
||||||
mkdirSync(SCREENSHOTS_DIR);
|
mkdirSync(OUTPUT_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
const browser = await firefox.launch({
|
const browser = await getSharedBrowser();
|
||||||
headless: true,
|
let context: BrowserContext | null = null;
|
||||||
firefoxUserPrefs: {
|
|
||||||
'dom.webdriver.enabled': false,
|
|
||||||
'media.peerconnection.enabled': false,
|
|
||||||
'useAutomationExtension': false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let page: Page | null = null;
|
let page: Page | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await browser.newContext({
|
context = await browser.newContext({
|
||||||
userAgent:
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64; rv:117.0) Gecko/20100101 Firefox/117.0',
|
||||||
'Mozilla/5.0 (X11; Linux x86_64; rv:117.0) Gecko/20100101 Firefox/117.0',
|
|
||||||
viewport: { width: 1280, height: 720 },
|
viewport: { width: 1280, height: 720 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,17 +212,16 @@ const runSkywardBot = async (username: string, password: string) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(`Fatal error in main: ${errorMsg}`);
|
logger.error(`Fatal error in main: ${errorMsg}`);
|
||||||
if (page) {
|
|
||||||
// try {
|
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/FATAL_ERROR.png` });
|
|
||||||
// } catch (screenshotError) {
|
|
||||||
// logger.warn('Could not take error screenshot');
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
// Clean up page and context but NOT the browser
|
||||||
logger.info('Browser closed, done.');
|
if (page && !page.isClosed()) {
|
||||||
|
await page.close().catch((err) => logger.warn('Error closing page:', err));
|
||||||
|
}
|
||||||
|
if (context) {
|
||||||
|
await context.close().catch((err) => logger.warn('Error closing context:', err));
|
||||||
|
}
|
||||||
|
logger.info('Session cleaned up, browser kept alive.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,7 +241,6 @@ async function checkAuth(
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/01-launchpad-page.png` });
|
|
||||||
|
|
||||||
// Step 2: Click "PISD Network Login" button
|
// Step 2: Click "PISD Network Login" button
|
||||||
logger.info('Step 2: Looking for PISD Network Login button...');
|
logger.info('Step 2: Looking for PISD Network Login button...');
|
||||||
@@ -216,7 +275,6 @@ async function checkAuth(
|
|||||||
await pisdButton.click();
|
await pisdButton.click();
|
||||||
logger.info('Clicked PISD Network Login button');
|
logger.info('Clicked PISD Network Login button');
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/02-after-pisd-button-click.png` });
|
|
||||||
|
|
||||||
// Step 3: Enter username
|
// Step 3: Enter username
|
||||||
logger.info('Step 3: Entering username...');
|
logger.info('Step 3: Entering username...');
|
||||||
@@ -226,7 +284,6 @@ async function checkAuth(
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.type(usernameSelector, username, { delay: 100 });
|
await page.type(usernameSelector, username, { delay: 100 });
|
||||||
logger.info('Username entered');
|
logger.info('Username entered');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/03-username-entered.png` });
|
|
||||||
|
|
||||||
// Step 4: Enter password
|
// Step 4: Enter password
|
||||||
logger.info('Step 4: Entering password...');
|
logger.info('Step 4: Entering password...');
|
||||||
@@ -236,12 +293,9 @@ async function checkAuth(
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.type(passwordSelector, password, { delay: 100 });
|
await page.type(passwordSelector, password, { delay: 100 });
|
||||||
logger.info('Password entered');
|
logger.info('Password entered');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/04-password-entered.png` });
|
|
||||||
|
|
||||||
// Step 5: Click submit button
|
// Step 5: Click submit button
|
||||||
logger.info('Step 5: Clicking submit button...');
|
logger.info('Step 5: Clicking submit button...');
|
||||||
|
|
||||||
// First wait a bit for the page to be ready
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
const submitSelectors = [
|
const submitSelectors = [
|
||||||
@@ -313,18 +367,16 @@ async function checkAuth(
|
|||||||
|
|
||||||
if (!submitButton) {
|
if (!submitButton) {
|
||||||
logger.error('Could not find submit button');
|
logger.error('Could not find submit button');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/05-submit-not-found.png`, fullPage: true });
|
|
||||||
throw new Error('Submit button not found');
|
throw new Error('Submit button not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
logger.info('Submit button clicked');
|
logger.info('Submit button clicked');
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/05-after-submit.png` });
|
|
||||||
|
|
||||||
// Wait for redirect back to launchpad
|
// Wait for redirect back to launchpad
|
||||||
logger.info('Waiting for redirect to launchpad...');
|
logger.info('Waiting for redirect to launchpad...');
|
||||||
await page.waitForTimeout(3000); // Wait a bit for redirect to happen
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
const currentUrl = page.url();
|
const currentUrl = page.url();
|
||||||
logger.info(`Current URL after redirect: ${currentUrl}`);
|
logger.info(`Current URL after redirect: ${currentUrl}`);
|
||||||
@@ -332,16 +384,12 @@ async function checkAuth(
|
|||||||
try {
|
try {
|
||||||
await page.waitForURL('**/myapps.classlink.com/**', { timeout: 10000 });
|
await page.waitForURL('**/myapps.classlink.com/**', { timeout: 10000 });
|
||||||
logger.info('Successfully matched myapps.classlink.com URL pattern');
|
logger.info('Successfully matched myapps.classlink.com URL pattern');
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`URL pattern didn't match, but we're at: ${page.url()}`);
|
logger.warn(`URL pattern didn't match, but we're at: ${page.url()}`);
|
||||||
logger.warn('Continuing anyway...');
|
logger.warn('Continuing anyway...');
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loginToSkyward(page: Page, username: string, password: string, context: any, allClassGrades: any[]) {
|
async function loginToSkyward(page: Page, username: string, password: string, context: any, allClassGrades: any[]) {
|
||||||
@@ -354,7 +402,6 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/01-launchpad-page.png` });
|
|
||||||
|
|
||||||
// Step 2: Click "PISD Network Login" button
|
// Step 2: Click "PISD Network Login" button
|
||||||
logger.info('Step 2: Looking for PISD Network Login button...');
|
logger.info('Step 2: Looking for PISD Network Login button...');
|
||||||
@@ -389,7 +436,6 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
await pisdButton.click();
|
await pisdButton.click();
|
||||||
logger.info('Clicked PISD Network Login button');
|
logger.info('Clicked PISD Network Login button');
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/02-after-pisd-button-click.png` });
|
|
||||||
|
|
||||||
// Step 3: Enter username
|
// Step 3: Enter username
|
||||||
logger.info('Step 3: Entering username...');
|
logger.info('Step 3: Entering username...');
|
||||||
@@ -399,7 +445,6 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.type(usernameSelector, username, { delay: 100 });
|
await page.type(usernameSelector, username, { delay: 100 });
|
||||||
logger.info('Username entered');
|
logger.info('Username entered');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/03-username-entered.png` });
|
|
||||||
|
|
||||||
// Step 4: Enter password
|
// Step 4: Enter password
|
||||||
logger.info('Step 4: Entering password...');
|
logger.info('Step 4: Entering password...');
|
||||||
@@ -409,12 +454,9 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.type(passwordSelector, password, { delay: 100 });
|
await page.type(passwordSelector, password, { delay: 100 });
|
||||||
logger.info('Password entered');
|
logger.info('Password entered');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/04-password-entered.png` });
|
|
||||||
|
|
||||||
// Step 5: Click submit button
|
// Step 5: Click submit button
|
||||||
logger.info('Step 5: Clicking submit button...');
|
logger.info('Step 5: Clicking submit button...');
|
||||||
|
|
||||||
// First wait a bit for the page to be ready
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
const submitSelectors = [
|
const submitSelectors = [
|
||||||
@@ -453,7 +495,6 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still not found, try getting all buttons and find one with "Sign in" text
|
|
||||||
if (!submitButton) {
|
if (!submitButton) {
|
||||||
logger.info('Trying to find button by text content...');
|
logger.info('Trying to find button by text content...');
|
||||||
const allButtons = await page.$$('button, input[type="submit"]');
|
const allButtons = await page.$$('button, input[type="submit"]');
|
||||||
@@ -486,18 +527,15 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
|
|
||||||
if (!submitButton) {
|
if (!submitButton) {
|
||||||
logger.error('Could not find submit button');
|
logger.error('Could not find submit button');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/05-submit-not-found.png`, fullPage: true });
|
|
||||||
throw new Error('Submit button not found');
|
throw new Error('Submit button not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
logger.info('Submit button clicked');
|
logger.info('Submit button clicked');
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/05-after-submit.png` });
|
|
||||||
|
|
||||||
// Wait for redirect back to launchpad
|
|
||||||
logger.info('Waiting for redirect to launchpad...');
|
logger.info('Waiting for redirect to launchpad...');
|
||||||
await page.waitForTimeout(3000); // Wait a bit for redirect to happen
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
const currentUrl = page.url();
|
const currentUrl = page.url();
|
||||||
logger.info(`Current URL after redirect: ${currentUrl}`);
|
logger.info(`Current URL after redirect: ${currentUrl}`);
|
||||||
@@ -511,17 +549,16 @@ async function loginToSkyward(page: Page, username: string, password: string, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/06-back-at-launchpad.png` });
|
|
||||||
|
|
||||||
await openSkywardFromLaunchpad(page, context, allClassGrades);
|
await openSkywardFromLaunchpad(page, context, allClassGrades);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSkywardFromLaunchpad(page: Page, context: any, allClassGrades: any[]) {
|
async function openSkywardFromLaunchpad(page: Page, context: any, allClassGrades: any[]) {
|
||||||
|
let newPage: Page | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 6: Search for Skyward
|
|
||||||
logger.info('Step 6: Searching for Skyward app...');
|
logger.info('Step 6: Searching for Skyward app...');
|
||||||
|
|
||||||
// Look for search input
|
|
||||||
const searchSelectors = [
|
const searchSelectors = [
|
||||||
'input[aria-label="Search All Apps"]',
|
'input[aria-label="Search All Apps"]',
|
||||||
'input#searchbar-filter-input',
|
'input#searchbar-filter-input',
|
||||||
@@ -551,7 +588,6 @@ async function openSkywardFromLaunchpad(page: Page, context: any, allClassGrades
|
|||||||
|
|
||||||
if (!searchInput) {
|
if (!searchInput) {
|
||||||
logger.error('Could not find search input');
|
logger.error('Could not find search input');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/07-search-not-found.png`, fullPage: true });
|
|
||||||
throw new Error('Search input not found');
|
throw new Error('Search input not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,46 +596,38 @@ async function openSkywardFromLaunchpad(page: Page, context: any, allClassGrades
|
|||||||
await searchInput.type('Skyward', { delay: 100 });
|
await searchInput.type('Skyward', { delay: 100 });
|
||||||
logger.info('Typed "Skyward" into search');
|
logger.info('Typed "Skyward" into search');
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/07-skyward-search.png` });
|
|
||||||
|
|
||||||
// Step 7: Press Enter to select Skyward (opens in new tab)
|
|
||||||
logger.info('Step 7: Pressing Enter to select Skyward...');
|
logger.info('Step 7: Pressing Enter to select Skyward...');
|
||||||
|
|
||||||
// Set up listener for new tab BEFORE pressing Enter
|
|
||||||
const newPagePromise = new Promise<Page>((resolve) => {
|
const newPagePromise = new Promise<Page>((resolve) => {
|
||||||
context.once('page', async (newPage) => {
|
context.once('page', async (np: Page) => {
|
||||||
logger.info(`New tab opened with URL: ${newPage.url()}`);
|
logger.info(`New tab opened with URL: ${np.url()}`);
|
||||||
resolve(newPage);
|
resolve(np);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await searchInput.press('Enter');
|
await searchInput.press('Enter');
|
||||||
logger.info('✓ Pressed Enter');
|
logger.info('✓ Pressed Enter');
|
||||||
|
|
||||||
// Wait for new tab to open
|
|
||||||
logger.info('Waiting for new tab to open...');
|
logger.info('Waiting for new tab to open...');
|
||||||
const newPage = await Promise.race([
|
newPage = await Promise.race([
|
||||||
newPagePromise,
|
newPagePromise,
|
||||||
page.waitForTimeout(10000).then(() => null),
|
page.waitForTimeout(10000).then(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!newPage) {
|
if (!newPage) {
|
||||||
logger.error('No new tab opened');
|
logger.error('No new tab opened');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/08-no-new-tab.png` });
|
|
||||||
throw new Error('New tab did not open');
|
throw new Error('New tab did not open');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`New tab URL: ${newPage.url()}`);
|
logger.info(`New tab URL: ${newPage.url()}`);
|
||||||
|
|
||||||
// Wait for the new page to load
|
|
||||||
await newPage.waitForLoadState('domcontentloaded');
|
await newPage.waitForLoadState('domcontentloaded');
|
||||||
await newPage.waitForTimeout(3000);
|
await newPage.waitForTimeout(3000);
|
||||||
|
|
||||||
const finalUrl = newPage.url();
|
const finalUrl = newPage.url();
|
||||||
logger.info(`Final URL in new tab: ${finalUrl}`);
|
logger.info(`Final URL in new tab: ${finalUrl}`);
|
||||||
await newPage.screenshot({ path: `${SCREENSHOTS_DIR}/08-new-tab-skyward.png` });
|
|
||||||
|
|
||||||
// Check if we're on Skyward
|
|
||||||
if (finalUrl.includes('skyward')) {
|
if (finalUrl.includes('skyward')) {
|
||||||
logger.info('✓ Successfully opened Skyward in new tab');
|
logger.info('✓ Successfully opened Skyward in new tab');
|
||||||
} else {
|
} else {
|
||||||
@@ -607,23 +635,28 @@ async function openSkywardFromLaunchpad(page: Page, context: any, allClassGrades
|
|||||||
logger.info('Continuing anyway...');
|
logger.info('Continuing anyway...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now navigate to grading on the NEW page
|
|
||||||
await navigateToGrading(newPage, allClassGrades);
|
await navigateToGrading(newPage, allClassGrades);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(`Error opening Skyward from launchpad: ${errorMsg}`);
|
logger.error(`Error opening Skyward from launchpad: ${errorMsg}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Always close the Skyward tab to prevent resource leaks
|
||||||
|
if (newPage && !newPage.isClosed()) {
|
||||||
|
await newPage.close().catch((err) =>
|
||||||
|
logger.warn('Error closing Skyward tab:', err)
|
||||||
|
);
|
||||||
|
logger.info('Closed Skyward tab');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
||||||
logger.info('Step 8: Navigating to Grading section...');
|
logger.info('Step 8: Navigating to Grading section...');
|
||||||
|
|
||||||
// Wait for page to fully load
|
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
// Look for element with exact text "Grading"
|
|
||||||
const gradingSelectors = [
|
const gradingSelectors = [
|
||||||
'text="Grading"',
|
'text="Grading"',
|
||||||
'a:has-text("Grading")',
|
'a:has-text("Grading")',
|
||||||
@@ -638,7 +671,6 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
try {
|
try {
|
||||||
const elements = await page.$$(selector);
|
const elements = await page.$$(selector);
|
||||||
|
|
||||||
// Check each element to find one with EXACT text "Grading"
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
const text = (await element.textContent())?.trim();
|
const text = (await element.textContent())?.trim();
|
||||||
if (text === 'Grading') {
|
if (text === 'Grading') {
|
||||||
@@ -659,55 +691,78 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
|
|
||||||
if (!gradingElement) {
|
if (!gradingElement) {
|
||||||
logger.error('Could not find Grading element with exact text');
|
logger.error('Could not find Grading element with exact text');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/10-grading-not-found.png`, fullPage: true });
|
|
||||||
throw new Error('Grading element not found');
|
throw new Error('Grading element not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll element into view
|
|
||||||
await gradingElement.scrollIntoViewIfNeeded();
|
await gradingElement.scrollIntoViewIfNeeded();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await gradingElement.click();
|
await gradingElement.click();
|
||||||
logger.info('Clicked Grading element');
|
logger.info('Clicked Grading element');
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/10-grading-page.png`, fullPage: true });
|
|
||||||
|
|
||||||
logger.info('Successfully navigated to Grading page');
|
logger.info('Successfully navigated to Grading page');
|
||||||
|
|
||||||
// Now find the grades table
|
// ADD THIS SECTION HERE - Click "All Year" radio button
|
||||||
|
logger.info('Step 8.5: Clicking "All Year" radio button...');
|
||||||
|
try {
|
||||||
|
const allYearRadio = await page.$('input[type="radio"][name="DateRangeModechild"][value="AllYear"]');
|
||||||
|
|
||||||
|
if (!allYearRadio) {
|
||||||
|
logger.warn('Could not find "All Year" radio button');
|
||||||
|
} else {
|
||||||
|
const isVisible = await allYearRadio.isVisible();
|
||||||
|
logger.info(`All Year radio button found - visible: ${isVisible}`);
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
await allYearRadio.scrollIntoViewIfNeeded();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await allYearRadio.click();
|
||||||
|
logger.info('✓ Clicked "All Year" radio button');
|
||||||
|
await page.waitForTimeout(2000); // Wait for page to update with all year data
|
||||||
|
} else {
|
||||||
|
logger.warn('All Year radio button not visible, skipping');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
logger.warn(`Error clicking All Year radio button: ${errorMsg}`);
|
||||||
|
logger.info('Continuing anyway...');
|
||||||
|
}
|
||||||
|
// END OF NEW SECTION
|
||||||
|
|
||||||
logger.info('Step 9: Finding grades table...');
|
logger.info('Step 9: Finding grades table...');
|
||||||
|
|
||||||
// Wait for the table to load
|
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
// Find the table body
|
|
||||||
const tableBody = await page.$('.unlockedBrowseBody.dynamicHeight');
|
const tableBody = await page.$('.unlockedBrowseBody.dynamicHeight');
|
||||||
|
|
||||||
if (!tableBody) {
|
if (!tableBody) {
|
||||||
logger.error('Could not find grades table with class "unlockedBrowseBody dynamicHeight"');
|
logger.error('Could not find grades table with class "unlockedBrowseBody dynamicHeight"');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/11-table-not-found.png`, fullPage: true });
|
|
||||||
throw new Error('Grades table not found');
|
throw new Error('Grades table not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('✓ Found grades table');
|
logger.info('✓ Found grades table');
|
||||||
|
|
||||||
// Find all clickable grade cells (links with numbers)
|
|
||||||
const gradeCells = await tableBody.$$('a.linkPanelButton');
|
const gradeCells = await tableBody.$$('a.linkPanelButton');
|
||||||
|
|
||||||
logger.info(`Found ${gradeCells.length} clickable grade cells`);
|
logger.info(`Found ${gradeCells.length} clickable grade cells`);
|
||||||
|
|
||||||
if (gradeCells.length === 0) {
|
if (gradeCells.length === 0) {
|
||||||
logger.warn('No clickable grade cells found');
|
logger.warn('No clickable grade cells found');
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/11-no-grade-cells.png`, fullPage: true });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click each grade cell
|
|
||||||
for (let i = 0; i < gradeCells.length; i++) {
|
for (let i = 0; i < gradeCells.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
// Check if page is still open
|
||||||
|
if (page.isClosed()) {
|
||||||
|
logger.error('Page was closed during processing');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`\n=== Processing grade cell ${i + 1}/${gradeCells.length} ===`);
|
logger.info(`\n=== Processing grade cell ${i + 1}/${gradeCells.length} ===`);
|
||||||
|
|
||||||
// Re-query the cell each time (in case DOM has changed)
|
|
||||||
const allCells = await tableBody.$$('a.linkPanelButton');
|
const allCells = await tableBody.$$('a.linkPanelButton');
|
||||||
const cell = allCells[i];
|
const cell = allCells[i];
|
||||||
|
|
||||||
@@ -716,7 +771,6 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cell details
|
|
||||||
const gradeText = (await cell.textContent())?.trim();
|
const gradeText = (await cell.textContent())?.trim();
|
||||||
const isVisible = await cell.isVisible();
|
const isVisible = await cell.isVisible();
|
||||||
const dataRow = await cell.getAttribute('data-row');
|
const dataRow = await cell.getAttribute('data-row');
|
||||||
@@ -733,17 +787,27 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll into view
|
|
||||||
await cell.scrollIntoViewIfNeeded();
|
await cell.scrollIntoViewIfNeeded();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
// Click the cell
|
|
||||||
await cell.click();
|
await cell.click();
|
||||||
logger.info(` ✓ Clicked grade cell`);
|
logger.info(` ✓ Clicked grade cell`);
|
||||||
|
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
// Check for the category label - look inside SegmentedAssignmentsInformationCell span (not the one with "left")
|
const gradePercentageLabel = await page.$('label.labelTag.visualLabel.header');
|
||||||
|
let overallGrade = '';
|
||||||
|
|
||||||
|
|
||||||
|
if (gradePercentageLabel) {
|
||||||
|
const percentageText = (await gradePercentageLabel.textContent())?.trim();
|
||||||
|
overallGrade = percentageText || '';
|
||||||
|
logger.info(` 📊 Grade Percentage: ${percentageText}`);
|
||||||
|
} else {
|
||||||
|
logger.info(` ℹ No grade percentage label found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const categoryContainer = await page.$('span.SegmentedAssignmentsInformationCell:not(.left) label.labelTag.visualLabel');
|
const categoryContainer = await page.$('span.SegmentedAssignmentsInformationCell:not(.left) label.labelTag.visualLabel');
|
||||||
|
|
||||||
if (!categoryContainer) {
|
if (!categoryContainer) {
|
||||||
@@ -752,11 +816,9 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
const categoryText = (await categoryContainer.textContent())?.trim();
|
const categoryText = (await categoryContainer.textContent())?.trim();
|
||||||
logger.info(` Category label found: "${categoryText}"`);
|
logger.info(` Category label found: "${categoryText}"`);
|
||||||
|
|
||||||
// Check if this is one of our target categories
|
|
||||||
if (categoryText && TARGET_CATEGORIES.includes(categoryText)) {
|
if (categoryText && TARGET_CATEGORIES.includes(categoryText)) {
|
||||||
logger.info(` ✓ Target category "${categoryText}" detected - reading grades`);
|
logger.info(` ✓ Target category "${categoryText}" detected - reading grades`);
|
||||||
|
|
||||||
// Get the class information from the left span
|
|
||||||
const classInfoSpan = await page.$('span.SegmentedAssignmentsInformationCell.left');
|
const classInfoSpan = await page.$('span.SegmentedAssignmentsInformationCell.left');
|
||||||
let className = '';
|
let className = '';
|
||||||
let teacher = '';
|
let teacher = '';
|
||||||
@@ -782,7 +844,6 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the details panel with grades
|
|
||||||
const detailsPanel = await page.$('#detailsPanel_SegmentedGradeBucketAssignments_unlockedBody');
|
const detailsPanel = await page.$('#detailsPanel_SegmentedGradeBucketAssignments_unlockedBody');
|
||||||
|
|
||||||
if (!detailsPanel) {
|
if (!detailsPanel) {
|
||||||
@@ -790,19 +851,17 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
} else {
|
} else {
|
||||||
logger.info(` ✓ Found details panel, extracting grades...`);
|
logger.info(` ✓ Found details panel, extracting grades...`);
|
||||||
|
|
||||||
// Get all rows in the table
|
|
||||||
const rows = await detailsPanel.$$('tr');
|
const rows = await detailsPanel.$$('tr');
|
||||||
logger.info(` Found ${rows.length} total rows`);
|
logger.info(` Found ${rows.length} total rows`);
|
||||||
|
|
||||||
const grades = [];
|
const grades = [];
|
||||||
let currentGradeType = null; // 'major' or 'minor'
|
let currentGradeType = null;
|
||||||
|
|
||||||
for (let j = 0; j < rows.length; j++) {
|
for (let j = 0; j < rows.length; j++) {
|
||||||
const row = rows[j];
|
const row = rows[j];
|
||||||
if (!row) continue;
|
if (!row) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if this row is a grade type header (Major Grades / Minor Grades)
|
|
||||||
const gradeTypeSpan = await row.$('span.anchorText');
|
const gradeTypeSpan = await row.$('span.anchorText');
|
||||||
|
|
||||||
if (gradeTypeSpan) {
|
if (gradeTypeSpan) {
|
||||||
@@ -817,16 +876,19 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
logger.info(` → Set current grade type to MINOR`);
|
logger.info(` → Set current grade type to MINOR`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
else if (gradeTypeText === 'Semester Exam') {
|
||||||
|
currentGradeType = 'minor';
|
||||||
|
logger.info(` → Semester Exam Detected → defaulting to MINOR`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if we haven't found a grade type yet
|
|
||||||
if (!currentGradeType) {
|
if (!currentGradeType) {
|
||||||
logger.info(` Row ${j}: Skipping (no grade type set yet)`);
|
logger.info(` Row ${j}: Skipping (no grade type set yet)`);
|
||||||
|
currentGradeType = 'minor';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract grade data from this row
|
|
||||||
// Look for any link in the row (could be linkDetails or other classes)
|
|
||||||
const nameLink = await row.$('a');
|
const nameLink = await row.$('a');
|
||||||
if (!nameLink) {
|
if (!nameLink) {
|
||||||
logger.info(` Row ${j}: No link found, skipping`);
|
logger.info(` Row ${j}: No link found, skipping`);
|
||||||
@@ -836,23 +898,20 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
const name = (await nameLink.textContent())?.trim() || '';
|
const name = (await nameLink.textContent())?.trim() || '';
|
||||||
logger.info(` Row ${j}: Found assignment "${name}"`);
|
logger.info(` Row ${j}: Found assignment "${name}"`);
|
||||||
|
|
||||||
let dueDate = '';
|
let dueDate = '';
|
||||||
let score = '';
|
let score = '';
|
||||||
let pointsEarned = '';
|
let pointsEarned = '';
|
||||||
let attempts = '';
|
let attempts = '';
|
||||||
|
|
||||||
// Get all td cells
|
|
||||||
const tdCells = await row.$$('td');
|
const tdCells = await row.$$('td');
|
||||||
logger.info(` Row ${j}: Found ${tdCells.length} td cells`);
|
logger.info(` Row ${j}: Found ${tdCells.length} td cells`);
|
||||||
|
|
||||||
// Due Date is typically in the second column (index 1)
|
|
||||||
if (tdCells && tdCells.length > 1 && tdCells[1]) {
|
if (tdCells && tdCells.length > 1 && tdCells[1]) {
|
||||||
dueDate = (await tdCells[1].textContent())?.trim() || '';
|
dueDate = (await tdCells[1].textContent())?.trim() || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Score and Points are in disabled-link-column spans
|
|
||||||
const disabledSpans = await row.$$('span.disabled-link-column');
|
const disabledSpans = await row.$$('span.disabled-link-column');
|
||||||
logger.info(` Row ${j}: Found ${disabledSpans.length} disabled-link-column spans`);
|
logger.info(` Row ${j}: Found ${disabledSpans ? disabledSpans.length : 0} disabled-link-column spans`);
|
||||||
|
|
||||||
if (disabledSpans && disabledSpans.length >= 1 && disabledSpans[0]) {
|
if (disabledSpans && disabledSpans.length >= 1 && disabledSpans[0]) {
|
||||||
score = (await disabledSpans[0].textContent())?.trim() || '';
|
score = (await disabledSpans[0].textContent())?.trim() || '';
|
||||||
@@ -861,7 +920,6 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
pointsEarned = (await disabledSpans[1].textContent())?.trim() || '';
|
pointsEarned = (await disabledSpans[1].textContent())?.trim() || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempts - look for any number link in last columns
|
|
||||||
const allLinks = await row.$$('a');
|
const allLinks = await row.$$('a');
|
||||||
const lastLink = allLinks && allLinks.length > 1 ? allLinks[allLinks.length - 1] : null;
|
const lastLink = allLinks && allLinks.length > 1 ? allLinks[allLinks.length - 1] : null;
|
||||||
if (lastLink) {
|
if (lastLink) {
|
||||||
@@ -887,31 +945,22 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
|
|
||||||
logger.info(` ✓ Extracted ${grades.length} grades for ${categoryText}`);
|
logger.info(` ✓ Extracted ${grades.length} grades for ${categoryText}`);
|
||||||
|
|
||||||
// Add to the global collection
|
|
||||||
allClassGrades.push({
|
allClassGrades.push({
|
||||||
className: className,
|
className: className,
|
||||||
teacher: teacher,
|
teacher: teacher,
|
||||||
period: period,
|
period: period,
|
||||||
category: categoryText,
|
category: categoryText,
|
||||||
|
overallGrade: overallGrade,
|
||||||
grades: grades,
|
grades: grades,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(` ✓ Added grades to collection (Total classes: ${allClassGrades.length})`);
|
logger.info(` ✓ Added grades to collection (Total classes: ${allClassGrades.length})`);
|
||||||
|
|
||||||
// Take screenshot with category-based name
|
|
||||||
// const screenshotName = `${categoryText}_${className.replace(/\s+/g, '_')}_row${dataRow}.png`.replace(/\s+/g, '_');
|
|
||||||
// await page.screenshot({
|
|
||||||
// path: `${SCREENSHOTS_DIR}/${screenshotName}`,
|
|
||||||
// fullPage: true
|
|
||||||
// });
|
|
||||||
// logger.info(` 📸 Screenshot saved: ${screenshotName}`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(` ℹ Category "${categoryText}" not in target list, skipping`);
|
logger.info(` ℹ Category "${categoryText}" not in target list, skipping`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the modal/panel
|
|
||||||
const closeSelectors = [
|
const closeSelectors = [
|
||||||
'button[aria-label="Close"]',
|
'button[aria-label="Close"]',
|
||||||
'button[aria-label*="close"]',
|
'button[aria-label*="close"]',
|
||||||
@@ -942,29 +991,17 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
// Try pressing Escape key
|
|
||||||
logger.info(` Trying Escape key to close panel`);
|
logger.info(` Trying Escape key to close panel`);
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay between cells
|
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(` ✗ Error processing grade cell ${i + 1}: ${errorMsg}`);
|
logger.error(` ✗ Error processing grade cell ${i + 1}: ${errorMsg}`);
|
||||||
|
|
||||||
// try {
|
|
||||||
// await page.screenshot({
|
|
||||||
// path: `${SCREENSHOTS_DIR}/grade_${i + 1}_ERROR.png`,
|
|
||||||
// fullPage: true
|
|
||||||
// });
|
|
||||||
// } catch (screenshotErr) {
|
|
||||||
// logger.warn(` Could not take error screenshot: ${screenshotErr}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Try to close any open modals before continuing
|
|
||||||
try {
|
try {
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
@@ -972,22 +1009,14 @@ async function navigateToGrading(page: Page, allClassGrades: any[]) {
|
|||||||
logger.warn(` Could not close modal: ${closeErr}`);
|
logger.warn(` Could not close modal: ${closeErr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue to next cell instead of crashing
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`\n🎉 Finished processing all grade cells!`);
|
logger.info(`\n🎉 Finished processing all grade cells!`);
|
||||||
|
|
||||||
// try {
|
|
||||||
// await page.screenshot({ path: `${SCREENSHOTS_DIR}/12-all-grades-complete.png`, fullPage: true });
|
|
||||||
// } catch (err) {
|
|
||||||
// logger.warn(`Could not take final screenshot: ${err}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Write all grades to a JSON file
|
|
||||||
try {
|
try {
|
||||||
const gradesFilePath = `${SCREENSHOTS_DIR}/all_grades.json`;
|
const gradesFilePath = `${OUTPUT_DIR}/all_grades.json`;
|
||||||
writeFileSync(gradesFilePath, JSON.stringify(allClassGrades, null, 2));
|
writeFileSync(gradesFilePath, JSON.stringify(allClassGrades, null, 2));
|
||||||
logger.info(`\n📄 Saved all grades to: ${gradesFilePath}`);
|
logger.info(`\n📄 Saved all grades to: ${gradesFilePath}`);
|
||||||
logger.info(` Total classes processed: ${allClassGrades.length}`);
|
logger.info(` Total classes processed: ${allClassGrades.length}`);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"pino": "^10.1.0",
|
"pino": "^10.1.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user