([\s\S]*?)<\/small>/i.exec(html);
- if (smallMatch) {
- const lines = smallMatch[1].split(/
/i).map(cleanText).filter(Boolean);
- result.semester = lines[lines.length - 1] || '';
- }
-
- const kvRe = /]*>([\s\S]*?)<\/td>\s* | ]*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
- }
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
- Profiles
-
-
-
-
-
-
- Study Load PDF
-
-
-
-
-
-
-
-
-```
-
-- [ ] **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 = '' + escapeHtml(p.name) + ' ' + escapeHtml(p.username) + '';
- 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 =
- '' +
- '' +
- '' +
- '' +
- '' +
- '' +
- '' +
- ' ' +
- '' +
- '';
- 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.
diff --git a/docs/superpowers/specs/2026-08-18-ismis-helper-design.md b/docs/superpowers/specs/2026-08-18-ismis-helper-design.md
deleted file mode 100644
index e7e551f..0000000
--- a/docs/superpowers/specs/2026-08-18-ismis-helper-design.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# ISMIS Helper — Firefox Extension Design
-
-**Date:** 2026-08-18
-**Status:** Approved
-**Target site:** `https://ismis.usc.edu.ph` (USC ISMIS student portal)
-
-## Overview
-
-A personal Firefox extension (Manifest V3, no build step) that improves the USC ISMIS
-student portal experience. It provides three features:
-
-1. **Auto-login** — auto-fill and auto-submit the login form from a saved profile.
-2. **Auto-accept Privacy Policy** — dismiss the recurring privacy-policy modal.
-3. **Study Load PDF** — an on-page "Download PDF" button on the Study Load page that
- generates a PDF replicating the official ISMIS Study Load document (Letter size,
- with course descriptions looked up from the Offered Courses endpoint) and
- downloads it directly.
-
-A single **master toggle** disables all extension behavior on the portal.
-
-## Architecture
-
-Single MV3 extension, plain JavaScript, no bundler. No background script — all logic
-runs in the content script (scraping and print-document building happen in-page).
-
-```
-ismis-helper/
-├── manifest.json # MV3 manifest, permissions, content script, popup, background
-├── background.js # message handler: build PDF (pdf-lib) + download via downloads API
-├── content.js # runs on ismis.usc.edu.ph: auto-login, privacy, button, scrape
-├── parsers.js # UMD: table/profile/offered-courses parsing + filtering
-├── pdf.js # UMD: buildStudyLoadPdf (Letter PDF, official layout, pagination)
-├── popup.html # popup UI (master toggle + profiles)
-├── popup.js # popup logic
-├── popup.css # popup styles
-├── libs/pdf-lib.min.js # vendored pdf-lib library (used by background)
-└── icons/ # extension icons (16, 32, 48, 128)
-```
-
-### Permissions (manifest.json)
-- `storage` — persist profiles and the master toggle.
-- `downloads` — save the generated Study Load PDF.
-- `host_permissions` — `https://ismis.usc.edu.ph/*` for the content script.
-- Background loads `libs/pdf-lib.min.js` + `pdf.js` via `background.scripts`.
-
-## Background script
-- `background.js` handles the `generate-pdf` message from the content script:
- builds the PDF via `ISMIStools.buildStudyLoadPdf`, wraps the bytes in a Blob,
- downloads via `browser.downloads.download({ saveAs: true })`, and revokes the
- object URL when the download reaches `complete`/`interrupted` (and on failure).
-
-## Component 1 — Auto-login
-
-- Content script runs on `https://ismis.usc.edu.ph/Account/Login`.
-- Reads the active (enabled) profile from `browser.storage.local`.
-- Locates the username and password input fields (`name`/`id` = `Username` /
- `Password`, with fallback variants).
-- Fills both fields, then submits the form (auto-submit).
-- If no profile is enabled, or the master toggle is off, does nothing.
-
-## Component 2 — Auto-accept Privacy Policy
-
-- On any portal page, if the privacy modal `#myModal` is present and visible:
- - Check the `#Confirmation` checkbox ("I have read and understood the Privacy
- Policy Statement").
- - Click the `#Submit` button (Continue), which is disabled until the checkbox is
- checked.
-- Gated by the master toggle.
-
-## Component 3 — Profiles
-
-- Stored in `browser.storage.local` under key `profiles` as an array of:
- `{ id, name, username, password, enabled }`.
-- Popup provides add, edit, delete, and enable/disable per profile.
-- Only one profile can be enabled at a time (enabling one disables the others);
- that profile is used for auto-login.
-- Master toggle stored under key `enabled` (boolean).
-
-## Component 4 — Study Load PDF
-
-### On-page button
-- The content script watches the Study Load page (`#StudyLoad`). When the course
- table appears (after the user selects academic period/year and searches), it
- injects a **"Download PDF"** button inline with the
- `12.00 Units / Total Units: 18.00` footer (button on the left, units text on the
- right, via a flex row), styled like the page's buttons (`btn green`).
-
-### Scrape
-- Parse the course table (columns: Group #, Course Code, Schedule, Faculty, Units,
- Status) via `parseStudyLoadTable`.
-- **Header info** (full name, student ID, program, year level, semester) is scraped
- from the **My Profile** page (`/Manage/Student`).
-
-### Filtering
-- **DISSOLVED courses are excluded** (`filterStudyLoadCourses`).
-- Total units = sum of the included courses' units (`sumUnits`).
-
-### Description lookup
-- For each unique course code, POST to
- `https://ismis.usc.edu.ph/CourseSchedule/CourseScheduleOffered` with the
- `__RequestVerificationToken` and selected `AcademicPeriod`/`AcademicYear`, then
- parse the returned table for the **Course Description**.
-- If lookup fails for a course, the description is left blank and generation
- continues.
-
-### PDF generation
-- `pdf.js` (`buildStudyLoadPdf`, using **pdf-lib**) builds a Letter-size (612×792pt)
- PDF replicating the official layout: title block (STUDENT'S STUDY LOAD, semester,
- date range, date enrolled), student info row (ID, name, program, year level), the
- course table (GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE — NOTE is
- the status), and a footer (total units, "Printed by: …"). Multi-page
- pagination re-draws the table header. NOTE column is wide enough for `REGULAR`.
-- The content script sends the data to the background (`generate-pdf`), which
- downloads the PDF via the `downloads` API (`saveAs: true`).
-
-## Data flow
-
-```
-Study Load page ──▶ content script (scrape + filter DISSOLVED + description lookup)
- │
- ▼ browser.runtime.sendMessage({ type: 'generate-pdf', data })
- background.js (pdf-lib buildStudyLoadPdf)
- │
- ▼
- browser.downloads.download → Study Load PDF file
-```
-
-## Error handling
-
-- Master toggle off → no extension behavior on the portal.
-- No enabled profile → no auto-login.
-- Description lookup fails for a course → blank description, PDF still generated.
-- No enrolled (non-DISSOLVED) courses → error toast, no download.
-- Download fails → error toast; object URL is revoked.
-
-## Testing
-
-- Node `node:test` suites: parsers (table/profile/offered-courses/filter/sum) and
- pdf (valid Letter PDF output).
-- Load via `about:debugging` → "Load Temporary Add-on".
-- Verify auto-login on the login page.
-- Verify privacy modal auto-accept.
-- Verify profile add/edit/delete/toggle and master toggle.
-- Verify the on-page Download PDF button: appears inline with the total-units
- footer, excludes DISSOLVED courses, and downloads a Letter-size PDF.
|