1286 lines
42 KiB
Markdown
1286 lines
42 KiB
Markdown
# ISMIS Helper Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Build a personal Firefox extension (MV3, no build step) for `ismis.usc.edu.ph` that provides auto-login, auto-accept of the privacy policy, and Study Load PDF generation.
|
|
|
|
**Architecture:** A single MV3 extension with a content script (auto-login, privacy accept, scraping), a background service worker (message routing + PDF generation), and a popup (profile manager + study-load editor). Pure parsing/PDF logic lives in UMD modules (`parsers.js`, `pdf.js`) shared between the browser and Node tests.
|
|
|
|
**Tech Stack:** Plain JavaScript, Manifest V3, `pdf-lib` (vendored), Node `node:test` for unit tests.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-08-18-ismis-helper-design.md`
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
```
|
|
ismis-helper/
|
|
├── manifest.json # MV3 manifest
|
|
├── background.js # service worker: message routing + PDF generation + download
|
|
├── content.js # content script: auto-login, privacy accept, scraping
|
|
├── parsers.js # UMD: parseStudyLoadTable, parseProfilePage, parseOfferedCourses
|
|
├── pdf.js # UMD: buildStudyLoadPdf
|
|
├── popup.html # popup UI
|
|
├── popup.js # popup logic
|
|
├── popup.css # popup styles
|
|
├── libs/pdf-lib.min.js # vendored pdf-lib
|
|
├── icons/ # extension icons (16, 32, 48, 128)
|
|
├── test/
|
|
│ ├── parsers.test.js # node:test for parsers
|
|
│ ├── pdf.test.js # node:test for PDF builder
|
|
│ └── fixtures/ # real HTML samples (already extracted)
|
|
│ ├── study-load-table.html
|
|
│ ├── profile.html
|
|
│ └── offered-courses.html
|
|
└── package.json # npm test script + pdf-lib devDependency
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1: Project scaffold (manifest, package.json, icons)
|
|
|
|
**Files:**
|
|
- Create: `manifest.json`
|
|
- Create: `package.json`
|
|
- Create: `icons/icon16.png`, `icons/icon32.png`, `icons/icon48.png`, `icons/icon128.png`
|
|
|
|
- [ ] **Step 1: Write `manifest.json`**
|
|
|
|
```json
|
|
{
|
|
"manifest_version": 3,
|
|
"name": "ISMIS Helper",
|
|
"version": "1.0.0",
|
|
"description": "Auto-login, auto-accept privacy policy, and Study Load PDF for the USC ISMIS portal.",
|
|
"permissions": ["storage", "downloads", "activeTab"],
|
|
"host_permissions": ["https://ismis.usc.edu.ph/*"],
|
|
"background": {
|
|
"service_worker": "background.js"
|
|
},
|
|
"content_scripts": [
|
|
{
|
|
"matches": ["https://ismis.usc.edu.ph/*"],
|
|
"js": ["parsers.js", "content.js"],
|
|
"run_at": "document_idle"
|
|
}
|
|
],
|
|
"action": {
|
|
"default_popup": "popup.html",
|
|
"default_title": "ISMIS Helper",
|
|
"default_icon": {
|
|
"16": "icons/icon16.png",
|
|
"32": "icons/icon32.png",
|
|
"48": "icons/icon48.png"
|
|
}
|
|
},
|
|
"icons": {
|
|
"16": "icons/icon16.png",
|
|
"32": "icons/icon32.png",
|
|
"48": "icons/icon48.png",
|
|
"128": "icons/icon128.png"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Write `package.json`**
|
|
|
|
```json
|
|
{
|
|
"name": "ismis-helper",
|
|
"version": "1.0.0",
|
|
"private": true,
|
|
"description": "Firefox extension for the USC ISMIS portal",
|
|
"scripts": {
|
|
"test": "node --test"
|
|
},
|
|
"devDependencies": {
|
|
"pdf-lib": "^1.17.1"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Install pdf-lib and vendor the minified build**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
npm install
|
|
cp node_modules/pdf-lib/dist/pdf-lib.min.js libs/pdf-lib.min.js
|
|
```
|
|
Expected: `libs/pdf-lib.min.js` exists and is non-empty.
|
|
|
|
- [ ] **Step 4: Generate icons**
|
|
|
|
Run (requires Python PIL; if unavailable, create 4 solid-color PNGs any way you like):
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
python3 -c "
|
|
from PIL import Image, ImageDraw
|
|
for size in (16,32,48,128):
|
|
img = Image.new('RGBA',(size,size),(0,0,0,0))
|
|
d = ImageDraw.Draw(img)
|
|
d.rounded_rectangle([0,0,size-1,size-1], radius=size//5, fill=(0,102,153,255))
|
|
# simple 'I' letter
|
|
w = max(2, size//6)
|
|
d.rectangle([size//2-w//2, size//5, size//2+w//2, size*4//5], fill=(255,255,255,255))
|
|
img.save(f'icons/icon{size}.png')
|
|
print('icons written')
|
|
"
|
|
```
|
|
Expected: four PNG files in `icons/`.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add manifest.json package.json package-lock.json libs/ icons/
|
|
git commit -m "chore: scaffold ISMIS Helper extension"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: `parsers.js` — parseStudyLoadTable
|
|
|
|
**Files:**
|
|
- Create: `parsers.js`
|
|
- Test: `test/parsers.test.js`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Create `test/parsers.test.js`:
|
|
```js
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { parseStudyLoadTable } = require('../parsers.js');
|
|
|
|
const fixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'study-load-table.html'), 'utf8');
|
|
|
|
test('parseStudyLoadTable extracts all course rows', () => {
|
|
const rows = parseStudyLoadTable(fixture);
|
|
assert.strictEqual(rows.length, 5);
|
|
});
|
|
|
|
test('parseStudyLoadTable maps columns correctly', () => {
|
|
const rows = parseStudyLoadTable(fixture);
|
|
const first = rows[0];
|
|
assert.strictEqual(first.group, '1');
|
|
assert.strictEqual(first.courseCode, 'CS 5105');
|
|
assert.strictEqual(first.units, '3.00');
|
|
assert.strictEqual(first.status, 'DISSOLVED');
|
|
assert.ok(first.schedule.includes('F 01:30 PM'));
|
|
});
|
|
|
|
test('parseStudyLoadTable handles empty faculty', () => {
|
|
const rows = parseStudyLoadTable(fixture);
|
|
const it4102 = rows.find(r => r.courseCode === 'IT 4102N');
|
|
assert.strictEqual(it4102.faculty, '');
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: FAIL — `Cannot find module '../parsers.js'`.
|
|
|
|
- [ ] **Step 3: Write `parsers.js`**
|
|
|
|
```js
|
|
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else {
|
|
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
|
|
}
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
|
|
function cleanText(s) {
|
|
return String(s == null ? '' : s)
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/ /gi, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function extractRows(html) {
|
|
const rows = [];
|
|
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
let trMatch;
|
|
while ((trMatch = trRe.exec(html)) !== null) {
|
|
const rowHtml = trMatch[1];
|
|
if (/<th/i.test(rowHtml)) continue;
|
|
const tds = [];
|
|
const tdRe = /<td[^>]*>([\s\S]*?)<\/td>/gi;
|
|
let tdMatch;
|
|
while ((tdMatch = tdRe.exec(rowHtml)) !== null) {
|
|
tds.push(cleanText(tdMatch[1]));
|
|
}
|
|
rows.push(tds);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function parseStudyLoadTable(html) {
|
|
return extractRows(html)
|
|
.filter((tds) => tds.length >= 6)
|
|
.map((tds) => ({
|
|
group: tds[0],
|
|
courseCode: tds[1],
|
|
schedule: tds[2],
|
|
faculty: tds[3],
|
|
units: tds[4],
|
|
status: tds[5]
|
|
}));
|
|
}
|
|
|
|
return { parseStudyLoadTable };
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: PASS (3 tests).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add parsers.js test/parsers.test.js
|
|
git commit -m "feat: parse study load table"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: `parsers.js` — parseProfilePage
|
|
|
|
**Files:**
|
|
- Modify: `parsers.js`
|
|
- Test: `test/parsers.test.js`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Append to `test/parsers.test.js`:
|
|
```js
|
|
const { parseProfilePage } = require('../parsers.js');
|
|
const profileFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'profile.html'), 'utf8');
|
|
|
|
test('parseProfilePage extracts header fields', () => {
|
|
const p = parseProfilePage(profileFixture);
|
|
assert.strictEqual(p.fullName, 'ADRIAN ALFRED CHIONG BONPIN');
|
|
assert.strictEqual(p.studentId, '22103625');
|
|
assert.strictEqual(p.yearLevel, '4');
|
|
assert.strictEqual(p.program, 'BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY');
|
|
assert.strictEqual(p.semester, '1ST Semester 2026');
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: FAIL — `parseProfilePage is not a function`.
|
|
|
|
- [ ] **Step 3: Add `parseProfilePage` to `parsers.js`**
|
|
|
|
Add inside the returned object and define the function:
|
|
```js
|
|
function parseProfilePage(html) {
|
|
const result = {};
|
|
const bMatch = /<b>([\s\S]*?)<\/b>/i.exec(html);
|
|
if (bMatch) result.program = cleanText(bMatch[1]);
|
|
|
|
const smallMatch = /<small>([\s\S]*?)<\/small>/i.exec(html);
|
|
if (smallMatch) {
|
|
const lines = smallMatch[1].split(/<br\s*\/?>/i).map(cleanText).filter(Boolean);
|
|
result.semester = lines[lines.length - 1] || '';
|
|
}
|
|
|
|
const kvRe = /<td[^>]*>([\s\S]*?)<\/td>\s*<td[^>]*class="bold"[^>]*>([\s\S]*?)<\/td>/gi;
|
|
let m;
|
|
while ((m = kvRe.exec(html)) !== null) {
|
|
const key = cleanText(m[1]);
|
|
const val = cleanText(m[2]);
|
|
if (/full name/i.test(key)) result.fullName = val;
|
|
else if (/student id/i.test(key)) result.studentId = val;
|
|
else if (/year level/i.test(key)) result.yearLevel = val;
|
|
else if (/student type/i.test(key)) result.studentType = val;
|
|
}
|
|
return result;
|
|
}
|
|
```
|
|
|
|
Update the return statement:
|
|
```js
|
|
return { parseStudyLoadTable, parseProfilePage };
|
|
```
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: PASS (4 tests).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add parsers.js test/parsers.test.js
|
|
git commit -m "feat: parse profile page header"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: `parsers.js` — parseOfferedCourses
|
|
|
|
**Files:**
|
|
- Modify: `parsers.js`
|
|
- Test: `test/parsers.test.js`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Append to `test/parsers.test.js`:
|
|
```js
|
|
const { parseOfferedCourses } = require('../parsers.js');
|
|
const offeredFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'offered-courses.html'), 'utf8');
|
|
|
|
test('parseOfferedCourses extracts course code and description', () => {
|
|
const courses = parseOfferedCourses(offeredFixture);
|
|
const it3206 = courses.find(c => c.courseCode === 'IT 3206');
|
|
assert.ok(it3206, 'should find IT 3206');
|
|
assert.strictEqual(it3206.description, 'INTEGRATIVE PROGRAMMING AND TECHNOLOGY');
|
|
});
|
|
|
|
test('parseOfferedCourses strips group suffix from code', () => {
|
|
const courses = parseOfferedCourses(offeredFixture);
|
|
assert.ok(courses.every(c => !/Group/.test(c.courseCode)));
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: FAIL — `parseOfferedCourses is not a function`.
|
|
|
|
- [ ] **Step 3: Add `parseOfferedCourses` to `parsers.js`**
|
|
|
|
```js
|
|
function parseOfferedCourses(html) {
|
|
return extractRows(html)
|
|
.filter((tds) => tds.length >= 2)
|
|
.map((tds) => ({
|
|
courseCode: tds[0].split('-')[0].trim(),
|
|
description: tds[1]
|
|
}));
|
|
}
|
|
```
|
|
|
|
Update the return statement:
|
|
```js
|
|
return { parseStudyLoadTable, parseProfilePage, parseOfferedCourses };
|
|
```
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: PASS (6 tests).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add parsers.js test/parsers.test.js
|
|
git commit -m "feat: parse offered courses descriptions"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: `pdf.js` — buildStudyLoadPdf
|
|
|
|
**Files:**
|
|
- Create: `pdf.js`
|
|
- Test: `test/pdf.test.js`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Create `test/pdf.test.js`:
|
|
```js
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
global.PDFLib = require('pdf-lib');
|
|
const { buildStudyLoadPdf } = require('../pdf.js');
|
|
|
|
const sampleData = {
|
|
header: {
|
|
fullName: 'ADRIAN ALFRED CHIONG BONPIN',
|
|
studentId: '22103625',
|
|
program: 'BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY',
|
|
yearLevel: '4',
|
|
semester: '1ST Semester 2026',
|
|
dateEnrolled: '8/5/2024',
|
|
dateRange: 'AUGUST - JANUARY'
|
|
},
|
|
courses: [
|
|
{ group: '7', courseCode: 'CIS 2101', description: 'DATA STRUCTURES AND ALGORITHMS', units: '3.00', teacher: 'CHRISTIAN MADERAZO', schedule: 'M,W 03:00 PM - 05:30 PM LB443TC', status: 'REGULAR' },
|
|
{ group: '2', courseCode: 'IT 3103N', description: 'SYSTEMS INTEGRATION AND ARCHITECTURE', units: '3.00', teacher: 'KHENT DELA PAZ', schedule: 'T,Th 01:30 PM - 03:00 PM DCISMVR3TC', status: 'REGULAR' }
|
|
],
|
|
totalUnits: '6.00'
|
|
};
|
|
|
|
test('buildStudyLoadPdf returns valid PDF bytes', async () => {
|
|
const bytes = await buildStudyLoadPdf(sampleData);
|
|
assert.ok(bytes instanceof Uint8Array);
|
|
const head = Buffer.from(bytes.slice(0, 5)).toString('latin1');
|
|
assert.strictEqual(head, '%PDF-');
|
|
const doc = await PDFLib.PDFDocument.load(bytes);
|
|
assert.strictEqual(doc.getPageCount(), 1);
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: FAIL — `Cannot find module '../pdf.js'`.
|
|
|
|
- [ ] **Step 3: Write `pdf.js`**
|
|
|
|
```js
|
|
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else {
|
|
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
|
|
}
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
|
|
function getPDFLib() {
|
|
return (typeof self !== 'undefined' && self.PDFLib) ||
|
|
(typeof globalThis !== 'undefined' && globalThis.PDFLib);
|
|
}
|
|
|
|
function wrapText(text, font, size, maxWidth) {
|
|
const words = String(text).split(/\s+/).filter(Boolean);
|
|
const lines = [];
|
|
let line = '';
|
|
for (const word of words) {
|
|
const candidate = line ? line + ' ' + word : word;
|
|
if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
|
|
line = candidate;
|
|
} else {
|
|
if (line) lines.push(line);
|
|
line = word;
|
|
}
|
|
}
|
|
if (line) lines.push(line);
|
|
return lines.length ? lines : [''];
|
|
}
|
|
|
|
function buildStudyLoadPdf(data) {
|
|
const PDFLib = getPDFLib();
|
|
const { PDFDocument, StandardFonts, rgb } = PDFLib;
|
|
const header = data.header || {};
|
|
const courses = data.courses || [];
|
|
|
|
const doc = PDFDocument.create();
|
|
const page = doc.addPage([612, 792]); // Letter
|
|
const font = doc.getFont(StandardFonts.Helvetica);
|
|
const bold = doc.getFont(StandardFonts.HelveticaBold);
|
|
const margin = 50;
|
|
const contentWidth = 612 - margin * 2;
|
|
let y = 792 - 50;
|
|
|
|
const black = rgb(0, 0, 0);
|
|
const gray = rgb(0.3, 0.3, 0.3);
|
|
|
|
function center(text, size, fontToUse, dy) {
|
|
const w = fontToUse.widthOfTextAtSize(text, size);
|
|
page.drawText(text, { x: (612 - w) / 2, y: y, size, font: fontToUse, color: black });
|
|
y -= dy;
|
|
}
|
|
function left(text, size, fontToUse, x, dy) {
|
|
page.drawText(text, { x, y, size, font: fontToUse, color: black });
|
|
y -= dy;
|
|
}
|
|
|
|
// Header
|
|
center(header.semester || '', 14, bold, 20);
|
|
center("STUDENT'S STUDY LOAD", 16, bold, 24);
|
|
center(header.dateRange || '', 11, font, 18);
|
|
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 10, font, margin, 16);
|
|
y -= 6;
|
|
if (header.studentId) left('Student ID: ' + header.studentId, 10, font, margin, 14);
|
|
if (header.fullName) left('Name: ' + header.fullName, 10, font, margin, 14);
|
|
if (header.program) left('Program: ' + header.program, 10, font, margin, 14);
|
|
if (header.yearLevel) left('Year Level: ' + header.yearLevel, 10, font, margin, 14);
|
|
y -= 10;
|
|
|
|
// Table columns: NOTE, UNITS, TEACHER, COURSE DESCRIPTION, COURSE, GROUP
|
|
const cols = [
|
|
{ key: 'status', label: 'NOTE', x: margin, w: 40 },
|
|
{ key: 'units', label: 'UNITS', x: margin + 40, w: 45 },
|
|
{ key: 'teacher', label: 'TEACHER', x: margin + 85, w: 110 },
|
|
{ key: 'description', label: 'COURSE DESCRIPTION', x: margin + 195, w: 150 },
|
|
{ key: 'courseCode', label: 'COURSE', x: margin + 345, w: 70 },
|
|
{ key: 'groupSchedule', label: 'GROUP', x: margin + 415, w: 97 }
|
|
];
|
|
const cellSize = 9;
|
|
const headerSize = 9;
|
|
|
|
function drawRow(cells, isHeader) {
|
|
const f = isHeader ? bold : font;
|
|
const size = isHeader ? headerSize : cellSize;
|
|
const linesPerCol = cols.map((c) => wrapText(cells[c.key] || '', f, size, c.w - 6));
|
|
const rowHeight = Math.max.apply(null, linesPerCol.map((l) => l.length)) * 11 + 8;
|
|
if (y - rowHeight < 50) {
|
|
// new page
|
|
page.drawText('', { x: margin, y: y, size: 1, font, color: black });
|
|
// (simple: just stop drawing further rows on overflow)
|
|
return false;
|
|
}
|
|
// background for header
|
|
if (isHeader) {
|
|
page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, color: rgb(0.85, 0.9, 0.95) });
|
|
}
|
|
// cell borders
|
|
page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, borderColor: black, borderWidth: 0.5 });
|
|
for (const c of cols) {
|
|
page.drawRectangle({ x: c.x, y: y - rowHeight, width: c.w, height: rowHeight, borderColor: black, borderWidth: 0.5 });
|
|
}
|
|
// text
|
|
for (let i = 0; i < cols.length; i++) {
|
|
const c = cols[i];
|
|
const lines = linesPerCol[i];
|
|
let ty = y - 10;
|
|
for (const line of lines) {
|
|
page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black });
|
|
ty -= 11;
|
|
}
|
|
}
|
|
y -= rowHeight;
|
|
return true;
|
|
}
|
|
|
|
// header row
|
|
const headerCells = {};
|
|
cols.forEach((c) => { headerCells[c.key] = c.label; });
|
|
drawRow(headerCells, true);
|
|
|
|
// data rows
|
|
for (const course of courses) {
|
|
const cells = {
|
|
status: course.status,
|
|
units: course.units,
|
|
teacher: course.teacher,
|
|
description: course.description,
|
|
courseCode: course.courseCode,
|
|
groupSchedule: (course.group ? course.group + ' ' : '') + (course.schedule || '')
|
|
};
|
|
if (!drawRow(cells, false)) break;
|
|
}
|
|
|
|
// Footer
|
|
y -= 12;
|
|
if (data.totalUnits) {
|
|
page.drawText('Total Units: ' + data.totalUnits, { x: margin, y: y, size: 11, font: bold, color: black });
|
|
y -= 16;
|
|
}
|
|
const printedBy = 'Printed by: ' + (header.studentId || '') + ' on ' + new Date().toLocaleDateString();
|
|
page.drawText(printedBy, { x: margin, y: y, size: 9, font, color: gray });
|
|
|
|
return doc.save(); // returns Promise<Uint8Array>
|
|
}
|
|
|
|
return { buildStudyLoadPdf };
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: PASS (7 tests).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add pdf.js test/pdf.test.js
|
|
git commit -m "feat: build study load PDF"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: `content.js` — auto-login, privacy accept, scraping
|
|
|
|
**Files:**
|
|
- Create: `content.js`
|
|
|
|
- [ ] **Step 1: Write `content.js`**
|
|
|
|
```js
|
|
(function () {
|
|
'use strict';
|
|
|
|
const MASTER_KEY = 'enabled';
|
|
|
|
async function isEnabled() {
|
|
const { enabled } = await browser.storage.local.get(MASTER_KEY);
|
|
return enabled !== false;
|
|
}
|
|
|
|
function autoAcceptPrivacy() {
|
|
const modal = document.getElementById('myModal');
|
|
if (!modal) return;
|
|
const checkbox = document.getElementById('Confirmation');
|
|
const submit = document.getElementById('Submit');
|
|
if (checkbox && !checkbox.checked) checkbox.click();
|
|
if (submit) submit.click();
|
|
}
|
|
|
|
function watchPrivacyModal() {
|
|
autoAcceptPrivacy();
|
|
const observer = new MutationObserver(() => autoAcceptPrivacy());
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
setTimeout(() => observer.disconnect(), 30000);
|
|
}
|
|
|
|
async function autoLogin() {
|
|
const { profiles } = await browser.storage.local.get('profiles');
|
|
const active = (profiles || []).find((p) => p.enabled);
|
|
if (!active) return;
|
|
const userField = document.querySelector(
|
|
'input[name="UserName"], input[name="username"], input[id="UserName"], input[id="username"]'
|
|
);
|
|
const passField = document.querySelector('input[type="password"]');
|
|
if (!userField || !passField) return;
|
|
userField.value = active.username;
|
|
passField.value = active.password;
|
|
userField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
passField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const form = userField.closest('form') || passField.closest('form');
|
|
if (!form) return;
|
|
setTimeout(() => {
|
|
const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
|
|
if (submitBtn) submitBtn.click();
|
|
else form.submit();
|
|
}, 300);
|
|
}
|
|
|
|
async function fetchCourseDescription(courseCode, academicPeriod, academicYear, token) {
|
|
const body = new URLSearchParams();
|
|
body.set('Courses', courseCode);
|
|
body.set('AcademicPeriod', academicPeriod);
|
|
body.set('AcademicYear', academicYear);
|
|
body.set('AjaxFormVM.Controller', 'CourseSchedule');
|
|
body.set('AjaxFormVM.Action', 'CourseScheduleOffered');
|
|
body.set('AjaxFormVM.ResultTargetId', 'CourseScheduleOfferedList');
|
|
body.set('AjaxFormVM.LinkToReturn', '');
|
|
body.set('AjaxFormVM.LinkToReturnTargetId', '');
|
|
body.set('AjaxFormVM.FormHeader', 'Choose term to view courses offered');
|
|
body.set('AjaxFormVM.FormHeaderIcon', '');
|
|
body.set('AjaxFormVM.FormId', 'CourseScheduleCourseScheduleOffered');
|
|
body.set('AjaxFormVM.ShowCloseButton', 'True');
|
|
body.set('AjaxFormVM.PreventAutoUpdateTarget', 'True');
|
|
body.set('AjaxFormVM.CloseModalAfterSubmit', 'False');
|
|
body.set('AjaxFormVM.AjaxFormType', 'Search');
|
|
body.set('AjaxFormVM.FormNoteLocation', '');
|
|
body.set('AjaxFormVM.SubmitButton.FaIcon', '');
|
|
body.set('AjaxFormVM.SubmitButton.ButtonText', '');
|
|
body.set('AjaxFormVM.SubmitButton.ButtonSize', '');
|
|
body.set('__RequestVerificationToken', token);
|
|
const resp = await fetch('/CourseSchedule/CourseScheduleOffered?Length=14', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: body.toString(),
|
|
credentials: 'same-origin'
|
|
});
|
|
const html = await resp.text();
|
|
const courses = ISMIStools.parseOfferedCourses(html);
|
|
const match = courses.find((c) => c.courseCode === courseCode);
|
|
return match ? match.description : '';
|
|
}
|
|
|
|
function extractTotalUnits(text) {
|
|
const m = /Total Units:\s*([\d.]+)/i.exec(text);
|
|
return m ? m[1] : '';
|
|
}
|
|
|
|
async function scrapeStudyLoad() {
|
|
const table = document.querySelector('table.table-forum');
|
|
if (!table) throw new Error('No study load table found on this page.');
|
|
const courses = ISMIStools.parseStudyLoadTable(table.outerHTML);
|
|
if (!courses.length) throw new Error('No courses found in study load table.');
|
|
|
|
const periodEl = document.getElementById('AcademicPeriod');
|
|
const yearEl = document.getElementById('AcademicYear');
|
|
const tokenEl = document.querySelector('input[name="__RequestVerificationToken"]');
|
|
const academicPeriod = periodEl ? periodEl.value : '';
|
|
const academicYear = yearEl ? yearEl.value : '';
|
|
const token = tokenEl ? tokenEl.value : '';
|
|
|
|
const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))];
|
|
const descMap = {};
|
|
for (const code of uniqueCodes) {
|
|
try {
|
|
descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token);
|
|
} catch (e) {
|
|
descMap[code] = '';
|
|
}
|
|
}
|
|
courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; });
|
|
|
|
let header = {};
|
|
try {
|
|
const resp = await fetch('/Manage/Student', { credentials: 'same-origin' });
|
|
const html = await resp.text();
|
|
header = ISMIStools.parseProfilePage(html);
|
|
} catch (e) {
|
|
header = {};
|
|
}
|
|
|
|
const totalUnits = extractTotalUnits(document.body.innerText);
|
|
return { header, courses, totalUnits };
|
|
}
|
|
|
|
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
if (msg && msg.type === 'scrape-study-load') {
|
|
scrapeStudyLoad()
|
|
.then((data) => sendResponse({ ok: true, data }))
|
|
.catch((err) => sendResponse({ ok: false, error: String((err && err.message) || err) }));
|
|
return true; // async response
|
|
}
|
|
});
|
|
|
|
(async function init() {
|
|
if (!(await isEnabled())) return;
|
|
watchPrivacyModal();
|
|
if (/\/Account\/Login/i.test(location.href)) autoLogin();
|
|
})();
|
|
})();
|
|
```
|
|
|
|
- [ ] **Step 2: Sanity-check syntax**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && node --check content.js`
|
|
Expected: no output (syntax OK).
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add content.js
|
|
git commit -m "feat: content script for auto-login, privacy, scraping"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: `background.js` — message routing + PDF generation
|
|
|
|
**Files:**
|
|
- Create: `background.js`
|
|
|
|
- [ ] **Step 1: Write `background.js`**
|
|
|
|
```js
|
|
'use strict';
|
|
|
|
importScripts('libs/pdf-lib.min.js', 'parsers.js', 'pdf.js');
|
|
|
|
function errMsg(err) {
|
|
return String((err && err.message) || err);
|
|
}
|
|
|
|
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
if (msg && msg.type === 'scrape-study-load') {
|
|
browser.tabs.query({ active: true, currentWindow: true })
|
|
.then((tabs) => {
|
|
const tab = tabs[0];
|
|
if (!tab || tab.id == null) throw new Error('No active tab.');
|
|
return browser.tabs.sendMessage(tab.id, { type: 'scrape-study-load' });
|
|
})
|
|
.then((data) => sendResponse(data))
|
|
.catch((err) => sendResponse({ ok: false, error: errMsg(err) }));
|
|
return true;
|
|
}
|
|
|
|
if (msg && msg.type === 'generate-pdf') {
|
|
generatePdf(msg.data)
|
|
.then(() => sendResponse({ ok: true }))
|
|
.catch((err) => sendResponse({ ok: false, error: errMsg(err) }));
|
|
return true;
|
|
}
|
|
});
|
|
|
|
async function generatePdf(data) {
|
|
const bytes = await ISMIStools.buildStudyLoadPdf(data);
|
|
const blob = new Blob([bytes], { type: 'application/pdf' });
|
|
const url = URL.createObjectURL(blob);
|
|
const name = (data.header && data.header.fullName) || 'Student';
|
|
const filename = 'Study Load - ' + name + '.pdf';
|
|
await browser.downloads.download({ url, filename, saveAs: true });
|
|
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Sanity-check syntax**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && node --check background.js`
|
|
Expected: no output (syntax OK).
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add background.js
|
|
git commit -m "feat: background message routing and PDF download"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Popup UI — profiles, master toggle, study-load editor
|
|
|
|
**Files:**
|
|
- Create: `popup.html`
|
|
- Create: `popup.css`
|
|
- Create: `popup.js`
|
|
|
|
- [ ] **Step 1: Write `popup.html`**
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<link rel="stylesheet" href="popup.css">
|
|
</head>
|
|
<body>
|
|
<header class="top">
|
|
<h1>ISMIS Helper</h1>
|
|
<div class="master">
|
|
<label class="switch">
|
|
<input type="checkbox" id="masterToggle">
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span id="masterLabel">Enabled</span>
|
|
</div>
|
|
</header>
|
|
|
|
<nav class="tabs">
|
|
<button data-tab="login" class="active">Auto-Login</button>
|
|
<button data-tab="studyload">Study Load</button>
|
|
</nav>
|
|
|
|
<section id="tab-login" class="tab">
|
|
<h2>Profiles</h2>
|
|
<ul id="profileList" class="profile-list"></ul>
|
|
<button id="addProfile" class="btn">+ Add Profile</button>
|
|
<div id="profileForm" class="form" hidden>
|
|
<input id="pName" placeholder="Profile name">
|
|
<input id="pUsername" placeholder="Username">
|
|
<input id="pPassword" type="password" placeholder="Password">
|
|
<label class="row"><input type="checkbox" id="pEnabled"> Enabled</label>
|
|
<div class="btn-row">
|
|
<button id="saveProfile" class="btn primary">Save</button>
|
|
<button id="cancelProfile" class="btn">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="tab-studyload" class="tab" hidden>
|
|
<h2>Study Load PDF</h2>
|
|
<button id="loadFromPortal" class="btn primary">Load from portal</button>
|
|
<div id="studyLoadForm" class="form" hidden>
|
|
<h3>Header</h3>
|
|
<input id="hFullName" placeholder="Full name">
|
|
<input id="hStudentId" placeholder="Student ID">
|
|
<input id="hProgram" placeholder="Program">
|
|
<input id="hYearLevel" placeholder="Year level">
|
|
<input id="hSemester" placeholder="Semester">
|
|
<input id="hDateEnrolled" placeholder="Date enrolled">
|
|
<input id="hDateRange" placeholder="Date range (e.g. AUGUST - JANUARY)">
|
|
<h3>Courses</h3>
|
|
<div id="coursesEditor"></div>
|
|
<button id="addCourse" class="btn">+ Add course</button>
|
|
<input id="totalUnits" placeholder="Total units">
|
|
<button id="generatePdf" class="btn primary">Generate PDF</button>
|
|
</div>
|
|
<div id="status" class="status"></div>
|
|
</section>
|
|
|
|
<script src="popup.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
- [ ] **Step 2: Write `popup.css`**
|
|
|
|
```css
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
width: 360px;
|
|
margin: 0;
|
|
font-family: system-ui, sans-serif;
|
|
font-size: 13px;
|
|
color: #222;
|
|
}
|
|
.top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
background: #006699;
|
|
color: #fff;
|
|
}
|
|
.top h1 { margin: 0; font-size: 16px; }
|
|
.master { display: flex; align-items: center; gap: 6px; }
|
|
.switch { position: relative; display: inline-block; width: 40px; height: 22px; }
|
|
.switch input { display: none; }
|
|
.slider {
|
|
position: absolute; cursor: pointer; inset: 0;
|
|
background: #ccc; border-radius: 22px; transition: 0.2s;
|
|
}
|
|
.slider::before {
|
|
content: ""; position: absolute; height: 18px; width: 18px;
|
|
left: 2px; top: 2px; background: #fff; border-radius: 50%; transition: 0.2s;
|
|
}
|
|
.switch input:checked + .slider { background: #4caf50; }
|
|
.switch input:checked + .slider::before { transform: translateX(18px); }
|
|
.tabs { display: flex; border-bottom: 1px solid #ddd; }
|
|
.tabs button {
|
|
flex: 1; padding: 8px; border: none; background: #f5f5f5; cursor: pointer;
|
|
}
|
|
.tabs button.active { background: #fff; font-weight: bold; border-bottom: 2px solid #006699; }
|
|
.tab { padding: 12px; }
|
|
.tab h2 { margin: 0 0 8px; font-size: 14px; }
|
|
.tab h3 { margin: 12px 0 6px; font-size: 12px; color: #555; }
|
|
.profile-list { list-style: none; margin: 0 0 8px; padding: 0; }
|
|
.profile-list li {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 6px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 6px;
|
|
}
|
|
.profile-list .name { flex: 1; }
|
|
.profile-list .user { color: #777; font-size: 11px; }
|
|
.btn {
|
|
padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px;
|
|
background: #fff; cursor: pointer;
|
|
}
|
|
.btn.primary { background: #006699; color: #fff; border-color: #006699; }
|
|
.btn-row { display: flex; gap: 6px; margin-top: 6px; }
|
|
.form input {
|
|
width: 100%; padding: 6px; margin-bottom: 6px;
|
|
border: 1px solid #ccc; border-radius: 4px;
|
|
}
|
|
.form .row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|
.course-row {
|
|
border: 1px solid #ddd; border-radius: 4px; padding: 6px; margin-bottom: 6px;
|
|
}
|
|
.course-row .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
|
.course-row input { margin-bottom: 4px; }
|
|
.status { margin-top: 8px; font-size: 12px; }
|
|
.status.error { color: #c00; }
|
|
.status.ok { color: #080; }
|
|
```
|
|
|
|
- [ ] **Step 3: Write `popup.js`**
|
|
|
|
```js
|
|
'use strict';
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
|
|
const MASTER_KEY = 'enabled';
|
|
const PROFILES_KEY = 'profiles';
|
|
|
|
let editingProfileId = null;
|
|
|
|
// ---- Master toggle ----
|
|
async function loadMaster() {
|
|
const { enabled } = await browser.storage.local.get(MASTER_KEY);
|
|
$('#masterToggle').checked = enabled !== false;
|
|
$('#masterLabel').textContent = enabled === false ? 'Disabled' : 'Enabled';
|
|
}
|
|
$('#masterToggle').addEventListener('change', async (e) => {
|
|
await browser.storage.local.set({ [MASTER_KEY]: e.target.checked });
|
|
$('#masterLabel').textContent = e.target.checked ? 'Enabled' : 'Disabled';
|
|
});
|
|
|
|
// ---- Profiles ----
|
|
async function getProfiles() {
|
|
const { profiles } = await browser.storage.local.get(PROFILES_KEY);
|
|
return profiles || [];
|
|
}
|
|
async function saveProfiles(profiles) {
|
|
await browser.storage.local.set({ [PROFILES_KEY]: profiles });
|
|
}
|
|
|
|
function renderProfiles(profiles) {
|
|
const list = $('#profileList');
|
|
list.innerHTML = '';
|
|
for (const p of profiles) {
|
|
const li = document.createElement('li');
|
|
const name = document.createElement('div');
|
|
name.className = 'name';
|
|
name.innerHTML = '<strong>' + escapeHtml(p.name) + '</strong><br><span class="user">' + escapeHtml(p.username) + '</span>';
|
|
const toggle = document.createElement('input');
|
|
toggle.type = 'checkbox';
|
|
toggle.checked = !!p.enabled;
|
|
toggle.title = 'Enable/disable';
|
|
toggle.addEventListener('change', async () => {
|
|
p.enabled = toggle.checked;
|
|
await saveProfiles(profiles);
|
|
});
|
|
const edit = document.createElement('button');
|
|
edit.className = 'btn';
|
|
edit.textContent = 'Edit';
|
|
edit.addEventListener('click', () => openProfileForm(p));
|
|
const del = document.createElement('button');
|
|
del.className = 'btn';
|
|
del.textContent = 'Delete';
|
|
del.addEventListener('click', async () => {
|
|
await saveProfiles(profiles.filter((x) => x.id !== p.id));
|
|
renderProfiles(await getProfiles());
|
|
});
|
|
li.append(toggle, name, edit, del);
|
|
list.appendChild(li);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[c]));
|
|
}
|
|
|
|
function openProfileForm(p) {
|
|
editingProfileId = p ? p.id : null;
|
|
$('#pName').value = p ? p.name : '';
|
|
$('#pUsername').value = p ? p.username : '';
|
|
$('#pPassword').value = p ? p.password : '';
|
|
$('#pEnabled').checked = p ? !!p.enabled : true;
|
|
$('#profileForm').hidden = false;
|
|
}
|
|
$('#addProfile').addEventListener('click', () => openProfileForm(null));
|
|
$('#cancelProfile').addEventListener('click', () => { $('#profileForm').hidden = true; });
|
|
$('#saveProfile').addEventListener('click', async () => {
|
|
const profiles = await getProfiles();
|
|
const data = {
|
|
id: editingProfileId || Date.now().toString(),
|
|
name: $('#pName').value.trim(),
|
|
username: $('#pUsername').value.trim(),
|
|
password: $('#pPassword').value,
|
|
enabled: $('#pEnabled').checked
|
|
};
|
|
if (!data.name || !data.username) return;
|
|
if (editingProfileId) {
|
|
const i = profiles.findIndex((x) => x.id === editingProfileId);
|
|
if (i >= 0) profiles[i] = data;
|
|
} else {
|
|
profiles.push(data);
|
|
}
|
|
await saveProfiles(profiles);
|
|
$('#profileForm').hidden = true;
|
|
renderProfiles(profiles);
|
|
});
|
|
|
|
// ---- Tabs ----
|
|
document.querySelectorAll('.tabs button').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.tabs button').forEach((b) => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
$('#tab-login').hidden = btn.dataset.tab !== 'login';
|
|
$('#tab-studyload').hidden = btn.dataset.tab !== 'studyload';
|
|
});
|
|
});
|
|
|
|
// ---- Study Load ----
|
|
function setStatus(msg, cls) {
|
|
const el = $('#status');
|
|
el.textContent = msg;
|
|
el.className = 'status ' + (cls || '');
|
|
}
|
|
|
|
function renderCourses(courses) {
|
|
const box = $('#coursesEditor');
|
|
box.innerHTML = '';
|
|
courses.forEach((c, i) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'course-row';
|
|
row.innerHTML =
|
|
'<div class="grid">' +
|
|
'<input data-i="' + i + '" data-f="group" placeholder="Group" value="' + escapeHtml(c.group || '') + '">' +
|
|
'<input data-i="' + i + '" data-f="courseCode" placeholder="Course code" value="' + escapeHtml(c.courseCode || '') + '">' +
|
|
'<input data-i="' + i + '" data-f="description" placeholder="Description" value="' + escapeHtml(c.description || '') + '">' +
|
|
'<input data-i="' + i + '" data-f="units" placeholder="Units" value="' + escapeHtml(c.units || '') + '">' +
|
|
'<input data-i="' + i + '" data-f="teacher" placeholder="Teacher" value="' + escapeHtml(c.teacher || '') + '">' +
|
|
'<input data-i="' + i + '" data-f="status" placeholder="Status" value="' + escapeHtml(c.status || '') + '">' +
|
|
'</div>' +
|
|
'<input data-i="' + i + '" data-f="schedule" placeholder="Schedule" value="' + escapeHtml(c.schedule || '') + '">' +
|
|
'<button class="btn remove-course" data-i="' + i + '">Remove</button>';
|
|
box.appendChild(row);
|
|
});
|
|
box.querySelectorAll('input').forEach((inp) => {
|
|
inp.addEventListener('input', () => {
|
|
const i = +inp.dataset.i;
|
|
const f = inp.dataset.f;
|
|
studyLoadCourses[i][f] = inp.value;
|
|
});
|
|
});
|
|
box.querySelectorAll('.remove-course').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
studyLoadCourses.splice(+btn.dataset.i, 1);
|
|
renderCourses(studyLoadCourses);
|
|
});
|
|
});
|
|
}
|
|
|
|
let studyLoadCourses = [];
|
|
|
|
$('#loadFromPortal').addEventListener('click', async () => {
|
|
setStatus('Loading from portal...');
|
|
try {
|
|
const res = await browser.runtime.sendMessage({ type: 'scrape-study-load' });
|
|
if (!res || !res.ok) throw new Error((res && res.error) || 'Scrape failed');
|
|
const d = res.data;
|
|
$('#hFullName').value = d.header.fullName || '';
|
|
$('#hStudentId').value = d.header.studentId || '';
|
|
$('#hProgram').value = d.header.program || '';
|
|
$('#hYearLevel').value = d.header.yearLevel || '';
|
|
$('#hSemester').value = d.header.semester || '';
|
|
$('#hDateEnrolled').value = d.header.dateEnrolled || '';
|
|
$('#hDateRange').value = d.header.dateRange || '';
|
|
$('#totalUnits').value = d.totalUnits || '';
|
|
studyLoadCourses = d.courses || [];
|
|
renderCourses(studyLoadCourses);
|
|
$('#studyLoadForm').hidden = false;
|
|
setStatus('Loaded. Review and generate.', 'ok');
|
|
} catch (err) {
|
|
setStatus('Could not load from portal: ' + err.message + ' — enter manually below.', 'error');
|
|
studyLoadCourses = [];
|
|
renderCourses(studyLoadCourses);
|
|
$('#studyLoadForm').hidden = false;
|
|
}
|
|
});
|
|
|
|
$('#addCourse').addEventListener('click', () => {
|
|
studyLoadCourses.push({ group: '', courseCode: '', description: '', units: '', teacher: '', schedule: '', status: '' });
|
|
renderCourses(studyLoadCourses);
|
|
});
|
|
|
|
$('#generatePdf').addEventListener('click', async () => {
|
|
const data = {
|
|
header: {
|
|
fullName: $('#hFullName').value.trim(),
|
|
studentId: $('#hStudentId').value.trim(),
|
|
program: $('#hProgram').value.trim(),
|
|
yearLevel: $('#hYearLevel').value.trim(),
|
|
semester: $('#hSemester').value.trim(),
|
|
dateEnrolled: $('#hDateEnrolled').value.trim(),
|
|
dateRange: $('#hDateRange').value.trim()
|
|
},
|
|
courses: studyLoadCourses,
|
|
totalUnits: $('#totalUnits').value.trim()
|
|
};
|
|
if (!data.courses.length) { setStatus('Add at least one course.', 'error'); return; }
|
|
setStatus('Generating PDF...');
|
|
try {
|
|
const res = await browser.runtime.sendMessage({ type: 'generate-pdf', data });
|
|
if (!res || !res.ok) throw new Error((res && res.error) || 'Generation failed');
|
|
setStatus('PDF downloaded.', 'ok');
|
|
} catch (err) {
|
|
setStatus('Error: ' + err.message, 'error');
|
|
}
|
|
});
|
|
|
|
// ---- Init ----
|
|
(async function init() {
|
|
await loadMaster();
|
|
renderProfiles(await getProfiles());
|
|
})();
|
|
```
|
|
|
|
- [ ] **Step 4: Sanity-check syntax**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && node --check popup.js`
|
|
Expected: no output (syntax OK).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add popup.html popup.css popup.js
|
|
git commit -m "feat: popup UI for profiles and study load"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: End-to-end manual verification in Firefox
|
|
|
|
**Files:** none (verification only)
|
|
|
|
- [ ] **Step 1: Run the full test suite**
|
|
|
|
Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
|
|
Expected: all tests PASS.
|
|
|
|
- [ ] **Step 2: Load the extension in Firefox**
|
|
|
|
1. Open Firefox → `about:debugging` → "This Firefox" → "Load Temporary Add-on".
|
|
2. Select `manifest.json` in `ismis-helper/`.
|
|
3. Confirm the extension appears with no load errors.
|
|
|
|
- [ ] **Step 3: Verify auto-login**
|
|
1. Open `https://ismis.usc.edu.ph/Account/Login`.
|
|
2. Add a profile in the popup (Auto-Login tab) and enable it.
|
|
3. Reload the login page. Expected: username/password filled and form submitted.
|
|
|
|
- [ ] **Step 4: Verify privacy auto-accept**
|
|
1. With the master toggle on, open any portal page that shows the privacy modal.
|
|
2. Expected: the `#myModal` modal is dismissed automatically.
|
|
|
|
- [ ] **Step 5: Verify master toggle**
|
|
1. Turn the master toggle OFF in the popup.
|
|
2. Reload the login page. Expected: no auto-fill/submit occurs.
|
|
|
|
- [ ] **Step 6: Verify Study Load PDF**
|
|
1. Open the Study Load page in the portal.
|
|
2. In the popup Study Load tab, click "Load from portal".
|
|
3. Expected: header and courses populate; descriptions filled in.
|
|
4. Click "Generate PDF". Expected: a PDF downloads matching the official layout.
|
|
|
|
- [ ] **Step 7: Verify manual fallback**
|
|
1. Open a non-portal page (or a portal page without the table).
|
|
2. Click "Load from portal". Expected: manual entry form appears.
|
|
3. Enter a course, click "Generate PDF". Expected: PDF downloads.
|
|
|
|
- [ ] **Step 8: Commit any fixes**
|
|
|
|
```bash
|
|
cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
|
|
git add -A
|
|
git commit -m "fix: verification adjustments"
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
**Spec coverage:**
|
|
- Auto-login (fill + submit) → Task 6, Task 9.
|
|
- Master toggle → Task 8 (popup), Task 6 (content gate), Task 9.
|
|
- Auto-accept privacy policy → Task 6, Task 9.
|
|
- Profiles (add/edit/delete/toggle) → Task 8.
|
|
- Study Load scrape → Task 6.
|
|
- Description lookup via Offered Courses → Task 6 + Task 4 parser.
|
|
- Header from My Profile → Task 6 + Task 3 parser.
|
|
- Manual fallback → Task 8 (popup editor), Task 9.
|
|
- PDF generation replicating official look → Task 5.
|
|
- Download via downloads API → Task 7.
|
|
|
|
**Placeholder scan:** No TBD/TODO; all steps contain concrete code and commands.
|
|
|
|
**Type consistency:** `ISMIStools` namespace used consistently across `parsers.js`, `pdf.js`, `content.js`, `background.js`. Message types `scrape-study-load` and `generate-pdf` match between popup, background, and content. Data shape `{ header, courses, totalUnits }` consistent across scrape, popup, and PDF builder.
|