From ffc3c247514d6ccf0ec1083ca344d4e00e9c5b79 Mon Sep 17 00:00:00 2001
From: Stimmie <137183321+smmariquit@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:07:08 +0800
Subject: [PATCH 01/10] ci(publish): one store rejecting an upload no longer
blocks the others (#70)
Chrome refuses uploads while the previous version is in review
(ITEM_NOT_UPDATABLE), which skipped Firefox, Edge and the release tag.
Store steps now continue on error and a final step fails the run.
Claude-Session: https://claude.ai/code/session_01T3dKAgr7Yo2637teHP4vNU
---
.github/workflows/publish-extension.yml | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml
index 2cbcbdb..8c4148f 100644
--- a/.github/workflows/publish-extension.yml
+++ b/.github/workflows/publish-extension.yml
@@ -8,7 +8,9 @@
# manifests only; the tag records what shipped.
#
# A store whose secrets are missing is skipped, not failed, so stores can be
-# connected one at a time.
+# connected one at a time. A store that rejects an upload (for example Chrome
+# while the previous version is still in review) does not stop the other
+# stores or the release; the run is marked failed at the end so it shows up.
#
# Secrets (repo settings, Actions):
# CWS_CLIENT_ID, CWS_CLIENT_SECRET, CWS_REFRESH_TOKEN Chrome Web Store API
@@ -77,6 +79,8 @@ jobs:
ls -l release
- name: Chrome Web Store
+ id: cws
+ continue-on-error: true
if: steps.version.outputs.new == 'true' && env.HAS_CWS == 'true'
env:
CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }}
@@ -86,6 +90,8 @@ jobs:
run: npx -y chrome-webstore-upload-cli@3 --source "release/elbi-gradesim-chrome-${{ steps.version.outputs.version }}.zip"
- name: Firefox Add-ons
+ id: amo
+ continue-on-error: true
if: steps.version.outputs.new == 'true' && env.HAS_AMO == 'true'
env:
WEB_EXT_API_KEY: ${{ secrets.AMO_JWT_ISSUER }}
@@ -93,6 +99,8 @@ jobs:
run: npx -y web-ext@8 sign --channel listed --source-dir extension/dist/firefox --artifacts-dir release/amo
- name: Edge Add-ons
+ id: edge
+ continue-on-error: true
if: steps.version.outputs.new == 'true' && env.HAS_EDGE == 'true'
env:
EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID }}
@@ -120,3 +128,9 @@ jobs:
run: |
tag="extension-v${{ steps.version.outputs.version }}"
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --title "Elbi GradeSim ${{ steps.version.outputs.version }}" --generate-notes
+
+ - name: Fail if a store rejected the upload
+ if: steps.cws.outcome == 'failure' || steps.amo.outcome == 'failure' || steps.edge.outcome == 'failure'
+ run: |
+ echo "::error::Store results: Chrome=${{ steps.cws.outcome }} Firefox=${{ steps.amo.outcome }} Edge=${{ steps.edge.outcome }}. Re-run this workflow once the store accepts uploads again."
+ exit 1
From 5f925667a97e61c036124254852a164f6949aa3e Mon Sep 17 00:00:00 2001
From: Stimmie <137183321+smmariquit@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:08:51 +0800
Subject: [PATCH 02/10] ci: run extension tests on PRs and before publishing
(#91)
Claude-Session: https://claude.ai/code/session_01T3dKAgr7Yo2637teHP4vNU
---
.github/workflows/pr-checks.yml | 6 ++++++
.github/workflows/publish-extension.yml | 3 +++
2 files changed, 9 insertions(+)
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index dd5edcd..a2c264e 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -24,3 +24,9 @@ jobs:
exit 1
fi
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Extension tests
+ run: for t in extension/*.test.js; do node "$t"; done
diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml
index 8c4148f..d47b6a5 100644
--- a/.github/workflows/publish-extension.yml
+++ b/.github/workflows/publish-extension.yml
@@ -65,6 +65,9 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "new=true" >> "$GITHUB_OUTPUT"
+ - name: Test
+ run: for t in extension/*.test.js; do node "$t"; done
+
- name: Build and zip
if: steps.version.outputs.new == 'true'
run: |
From c0d9f302793a595047e87a6e9c7d1b19041e4767 Mon Sep 17 00:00:00 2001
From: Stimmie <137183321+smmariquit@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:10:40 +0800
Subject: [PATCH 03/10] feat(bridge): answer the gradesim.uplb.tools web app
with stored grades (#63)
Chrome, Edge and Opera list https://gradesim.uplb.tools under
externally_connectable and answer GRADESIM_PING and GRADESIM_GET_GRADES in
the background script, checking sender.origin. Firefox has no
externally_connectable for web pages, so a small content script on that one
host answers the same requests over window.postMessage. Grades go to the page
in the export shape and never over the network.
Closes #62
Claude-Session: https://claude.ai/code/session_01T3dKAgr7Yo2637teHP4vNU
---
PRIVACY_POLICY.md | 3 +-
extension/PRIVACY_POLICY.md | 3 +-
extension/build.sh | 2 +-
extension/manifests/manifest.chrome.json | 7 ++++-
extension/manifests/manifest.firefox.json | 9 ++++++
extension/manifests/manifest.opera.json | 7 ++++-
extension/src/background.js | 29 ++++++++++++++++++
extension/src/bridge.js | 37 +++++++++++++++++++++++
8 files changed, 92 insertions(+), 5 deletions(-)
create mode 100644 extension/src/bridge.js
diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md
index d3c1cec..19d6532 100644
--- a/PRIVACY_POLICY.md
+++ b/PRIVACY_POLICY.md
@@ -38,6 +38,7 @@ Your grades data is used exclusively for:
|------------|----------------|
| `storage` | To save your grades data locally on your device |
| `host_permissions` for amis.uplb.edu.ph | To access the AMIS portal and read your grades |
+| `externally_connectable` for gradesim.uplb.tools (Chrome, Edge, Opera) and a content script there (Firefox) | So the Elbi GradeSim web app can ask for your grades when you click Import. They pass to the page inside your browser and are never sent over the network |
## Data Deletion
@@ -51,7 +52,7 @@ Chrome Settings > Privacy and Security > Clear Browsing Data > Cookies and other
## Security
-- The extension only works on the official UPLB AMIS website (amis.uplb.edu.ph)
+- The extension reads grades only on the official UPLB AMIS website (amis.uplb.edu.ph), and shares them only with the Elbi GradeSim web app at gradesim.uplb.tools when you ask it to
- We use your existing AMIS session - we never ask for or store your password
- All data processing happens locally in your browser
diff --git a/extension/PRIVACY_POLICY.md b/extension/PRIVACY_POLICY.md
index d3c1cec..19d6532 100644
--- a/extension/PRIVACY_POLICY.md
+++ b/extension/PRIVACY_POLICY.md
@@ -38,6 +38,7 @@ Your grades data is used exclusively for:
|------------|----------------|
| `storage` | To save your grades data locally on your device |
| `host_permissions` for amis.uplb.edu.ph | To access the AMIS portal and read your grades |
+| `externally_connectable` for gradesim.uplb.tools (Chrome, Edge, Opera) and a content script there (Firefox) | So the Elbi GradeSim web app can ask for your grades when you click Import. They pass to the page inside your browser and are never sent over the network |
## Data Deletion
@@ -51,7 +52,7 @@ Chrome Settings > Privacy and Security > Clear Browsing Data > Cookies and other
## Security
-- The extension only works on the official UPLB AMIS website (amis.uplb.edu.ph)
+- The extension reads grades only on the official UPLB AMIS website (amis.uplb.edu.ph), and shares them only with the Elbi GradeSim web app at gradesim.uplb.tools when you ask it to
- We use your existing AMIS session - we never ask for or store your password
- All data processing happens locally in your browser
diff --git a/extension/build.sh b/extension/build.sh
index 423b7f1..d906e3b 100755
--- a/extension/build.sh
+++ b/extension/build.sh
@@ -25,7 +25,7 @@ build_target() {
# Copy shared source
cp "$SRC_DIR"/popup.js "$SRC_DIR"/popup.css "$SRC_DIR"/popup.html \
- "$SRC_DIR"/content.js "$SRC_DIR"/background.js \
+ "$SRC_DIR"/content.js "$SRC_DIR"/background.js "$SRC_DIR"/bridge.js \
"$SRC_DIR"/curriculum.js "$SRC_DIR"/catalog.js \
"$SRC_DIR"/planner.html "$SRC_DIR"/planner.js "$SRC_DIR"/planner.css \
"$SRC_DIR"/scheduler.js "$out/"
diff --git a/extension/manifests/manifest.chrome.json b/extension/manifests/manifest.chrome.json
index df8457e..62b00dc 100644
--- a/extension/manifests/manifest.chrome.json
+++ b/extension/manifests/manifest.chrome.json
@@ -32,5 +32,10 @@
],
"run_at": "document_start"
}
- ]
+ ],
+ "externally_connectable": {
+ "matches": [
+ "https://gradesim.uplb.tools/*"
+ ]
+ }
}
diff --git a/extension/manifests/manifest.firefox.json b/extension/manifests/manifest.firefox.json
index c410da1..b384c2d 100644
--- a/extension/manifests/manifest.firefox.json
+++ b/extension/manifests/manifest.firefox.json
@@ -47,6 +47,15 @@
"content.js"
],
"run_at": "document_start"
+ },
+ {
+ "matches": [
+ "https://gradesim.uplb.tools/*"
+ ],
+ "js": [
+ "bridge.js"
+ ],
+ "run_at": "document_start"
}
]
}
diff --git a/extension/manifests/manifest.opera.json b/extension/manifests/manifest.opera.json
index df8457e..62b00dc 100644
--- a/extension/manifests/manifest.opera.json
+++ b/extension/manifests/manifest.opera.json
@@ -32,5 +32,10 @@
],
"run_at": "document_start"
}
- ]
+ ],
+ "externally_connectable": {
+ "matches": [
+ "https://gradesim.uplb.tools/*"
+ ]
+ }
}
diff --git a/extension/src/background.js b/extension/src/background.js
index 4e2b8a3..e7ed73b 100644
--- a/extension/src/background.js
+++ b/extension/src/background.js
@@ -22,3 +22,32 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
});
+
+// Web app bridge (Chrome, Edge, Opera, Brave). The manifest lists
+// https://gradesim.uplb.tools under externally_connectable, so only that site
+// can send these. GRADESIM_PING says whether grades exist; GRADESIM_GET_GRADES
+// returns them in the same shape as Export JSON. The data goes straight to the
+// page in the browser; nothing is sent over the network.
+const WEB_APP_ORIGIN = 'https://gradesim.uplb.tools';
+if (chrome.runtime.onMessageExternal) {
+ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
+ if (sender.origin !== WEB_APP_ORIGIN || !message) return false;
+ if (message.type !== 'GRADESIM_PING' && message.type !== 'GRADESIM_GET_GRADES') return false;
+ chrome.storage.local.get(['gradesData', 'selectedProgram', 'excludedCourses', 'substitutions']).then(data => {
+ const hasGrades = !!(data.gradesData && data.gradesData.student_grades);
+ if (message.type === 'GRADESIM_PING') {
+ sendResponse({ hasGrades });
+ return;
+ }
+ sendResponse(hasGrades ? {
+ source: 'elbi-gradesim',
+ timestamp: new Date().toISOString(),
+ selectedProgram: data.selectedProgram || 'BSCS',
+ excludedCourses: data.excludedCourses || [],
+ substitutions: data.substitutions || {},
+ gradesData: data.gradesData,
+ } : null);
+ });
+ return true; // answer asynchronously
+ });
+}
diff --git a/extension/src/bridge.js b/extension/src/bridge.js
new file mode 100644
index 0000000..c3b042a
--- /dev/null
+++ b/extension/src/bridge.js
@@ -0,0 +1,37 @@
+// extension/src/bridge.js
+
+// Web app bridge for Firefox, which has no externally_connectable for web
+// pages. Runs only on https://gradesim.uplb.tools and answers two requests the
+// page posts to itself: GRADESIM_PING (are there grades?) and
+// GRADESIM_GET_GRADES (hand them over, sent when the student clicks Import).
+// The reply stays inside this browser tab; nothing is sent over the network.
+// Chromium browsers use externally_connectable in background.js instead.
+
+(function () {
+ 'use strict';
+
+ window.addEventListener('message', (event) => {
+ if (event.source !== window || event.origin !== window.location.origin) return;
+ const type = event.data && event.data.type;
+ if (type !== 'GRADESIM_PING' && type !== 'GRADESIM_GET_GRADES') return;
+
+ chrome.storage.local.get(['gradesData', 'selectedProgram', 'excludedCourses', 'substitutions']).then(data => {
+ const hasGrades = !!(data.gradesData && data.gradesData.student_grades);
+ let payload = { hasGrades };
+ if (type === 'GRADESIM_GET_GRADES') {
+ payload = hasGrades ? {
+ source: 'elbi-gradesim',
+ timestamp: new Date().toISOString(),
+ selectedProgram: data.selectedProgram || 'BSCS',
+ excludedCourses: data.excludedCourses || [],
+ substitutions: data.substitutions || {},
+ gradesData: data.gradesData,
+ } : null;
+ }
+ // Firefox content scripts see the page through an Xray wrapper; cloneInto
+ // makes the object readable by the page.
+ const out = { type: 'GRADESIM_REPLY', request: type, payload };
+ window.postMessage(typeof cloneInto === 'function' ? cloneInto(out, window) : out, window.location.origin);
+ });
+ });
+})();
From 490914a1f20a9512a1b43577678dbb6c528f0df5 Mon Sep 17 00:00:00 2001
From: Stimmie <137183321+smmariquit@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:13:43 +0800
Subject: [PATCH 04/10] fix(gwa): failed grades do not complete courses, PEd
counts, no honors with zero units (#92)
- 4.00 and 5.00 stay in the GWA but no longer mark the course completed or
count toward units passed.
- PE/HK/NSTP exclusion uses a word boundary, so PEd majors count.
- No honor track before any graded unit; Honor Roll removed (not a Latin
honor). Scholar badges need 15 units and no 5.00, 4.00 or INC.
Claude-Session: https://claude.ai/code/session_01T3dKAgr7Yo2637teHP4vNU
---
extension/src/curriculum.js | 9 ++---
extension/src/popup.html | 7 ----
extension/src/popup.js | 69 ++++++++++++++++++++-----------------
3 files changed, 42 insertions(+), 43 deletions(-)
diff --git a/extension/src/curriculum.js b/extension/src/curriculum.js
index 5c1c317..b925be9 100644
--- a/extension/src/curriculum.js
+++ b/extension/src/curriculum.js
@@ -30,7 +30,8 @@ function normalizeCourseCode(code) {
function isNonGwaCourseCode(courseCode) {
const code = normalizeCourseCode(courseCode);
- return NON_GWA_PREFIXES.some(prefix => code.startsWith(prefix));
+ // Word boundary so PEd and similar majors are not caught by PE.
+ return NON_GWA_PREFIXES.some(prefix => new RegExp(`^${prefix}\\b`).test(code));
}
function isGECourse(courseCode, courseTitle) {
@@ -76,7 +77,7 @@ function genericRequirementCourses(program) {
});
}
- const existingHK = countExisting(c => normalizeCourseCode(c.code).startsWith('HK') || normalizeCourseCode(c.code).startsWith('PE'));
+ const existingHK = countExisting(c => /^(HK|PE)\b/.test(normalizeCourseCode(c.code)));
const hkRequired = program?.hkCoursesRequired == null ? 2 : program.hkCoursesRequired;
for (let i = existingHK; i < hkRequired; i++) {
const slot = defaultSlot(i);
@@ -172,7 +173,7 @@ function getCompletedRequirementSlotCodes(completedCourses, program) {
(program?.majorCourses || []).forEach(course => {
const code = normalizeCourseCode(course.code);
if (isGECourse(code, course.title)) existing.ge.add(code);
- else if (code.startsWith('HK') || code.startsWith('PE')) existing.hk.add(code);
+ else if (/^(HK|PE)\b/.test(code)) existing.hk.add(code);
else if (code.startsWith('NSTP')) existing.nstp.add(code);
});
@@ -182,7 +183,7 @@ function getCompletedRequirementSlotCodes(completedCourses, program) {
if (isGECourse(code, title)) {
counts.ge++;
if (existing.ge.has(code)) existingDone.ge++;
- } else if (code.startsWith('HK') || code.startsWith('PE')) {
+ } else if (/^(HK|PE)\b/.test(code)) {
counts.hk++;
if (existing.hk.has(code)) existingDone.hk++;
} else if (code.startsWith('NSTP')) {
diff --git a/extension/src/popup.html b/extension/src/popup.html
index 36bf31a..755dbc7 100644
--- a/extension/src/popup.html
+++ b/extension/src/popup.html
@@ -166,13 +166,6 @@
Target Latin Honor
GWA ≤ 1.75
-
-
-
- Honor Roll
- GWA ≤ 2.00
-
-
diff --git a/extension/src/popup.js b/extension/src/popup.js
index 7ea8e82..f3f2ede 100644
--- a/extension/src/popup.js
+++ b/extension/src/popup.js
@@ -445,15 +445,15 @@ async function loadGradesData() {
function displayGradesData(courses) {
// Calculate GWA with exclusions
- const { gwa, totalUnits, totalCourses, gradesBySemester, completedCourses, excludedUnits, excludedCount } = calculateGWA(courses, excludedCourses);
+ const { gwa, totalUnits, passedUnits, totalCourses, gradesBySemester, completedCourses, excludedUnits, excludedCount } = calculateGWA(courses, excludedCourses);
// Display GWA
document.getElementById('currentGWA').textContent = gwa.toFixed(4);
- document.getElementById('totalUnits').textContent = excludedCount > 0 ? `${totalUnits} (${excludedUnits} excl.)` : totalUnits;
+ document.getElementById('totalUnits').textContent = excludedCount > 0 ? `${passedUnits} (${excludedUnits} excl.)` : passedUnits;
document.getElementById('totalCourses').textContent = excludedCount > 0 ? `${totalCourses} (${excludedCount} excl.)` : totalCourses;
// Display honor status
- displayHonorStatus(gwa);
+ displayHonorStatus(gwa, totalUnits);
// Display grades list (pass courses for toggle functionality)
displayGradesList(gradesBySemester, courses);
@@ -477,14 +477,16 @@ function displayGradesData(courses) {
function calculateGWA(courses, excludedIds = new Set()) {
let totalWeightedGrade = 0;
let totalUnits = 0;
+ let passedUnits = 0;
let totalCourses = 0;
let excludedUnits = 0;
let excludedCount = 0;
const gradesBySemester = {};
const completedCourses = [];
- // Courses to exclude from GWA calculation (by prefix)
- const excludedPrefixes = ['NSTP', 'HK', 'PE'];
+ // PE, HK and NSTP are not in the GWA. A word boundary keeps PEd (Physical
+ // Education majors) and similar codes in.
+ const excludedPrefix = /^(NSTP|HK|PE)\b/i;
// Non-numeric grades that should be displayed but not counted in GWA
const nonNumericGrades = ['S', 'U', 'INC', 'DRP', 'W', 'P', 'DFG'];
@@ -494,10 +496,7 @@ function calculateGWA(courses, excludedIds = new Set()) {
const gradeStr = (course.grade || '').toString().toUpperCase().trim();
// Skip NSTP, HK, PE courses entirely
- const isPrefixExcluded = excludedPrefixes.some(prefix =>
- courseCode.toUpperCase().startsWith(prefix)
- );
- if (isPrefixExcluded) {
+ if (excludedPrefix.test(courseCode.trim())) {
return;
}
@@ -551,24 +550,33 @@ function calculateGWA(courses, excludedIds = new Set()) {
totalWeightedGrade += grade * units;
totalUnits += units;
totalCourses++;
-
- completedCourses.push({
- code: course.courseCode,
- title: course.courseTitle,
- units: units,
- grade: grade
- });
+
+ // 4.00 and 5.00 count in the GWA but do not complete the course.
+ if (grade <= 3.0) {
+ passedUnits += units;
+ completedCourses.push({
+ code: course.courseCode,
+ title: course.courseTitle,
+ units: units,
+ grade: grade
+ });
+ }
});
const gwa = totalUnits > 0 ? totalWeightedGrade / totalUnits : 0;
- return { gwa, totalUnits, totalCourses, gradesBySemester, completedCourses, excludedUnits, excludedCount };
+ return { gwa, totalUnits, passedUnits, totalCourses, gradesBySemester, completedCourses, excludedUnits, excludedCount };
}
-function displayHonorStatus(gwa) {
+function displayHonorStatus(gwa, totalUnits) {
const honorEl = document.getElementById('honorStatus');
-
- if (gwa <= 1.20) {
+
+ if (!totalUnits) {
+ // Nothing graded yet, so there is no track to show.
+ honorEl.textContent = '';
+ honorEl.className = 'honor-status';
+ honorEl.style.display = 'none';
+ } else if (gwa <= 1.20) {
honorEl.safeHTML = icon('award') + 'Summa Cum Laude track';
honorEl.className = 'honor-status summa';
honorEl.style.display = '';
@@ -580,10 +588,6 @@ function displayHonorStatus(gwa) {
honorEl.safeHTML = icon('award') + 'Cum Laude track';
honorEl.className = 'honor-status cum-laude';
honorEl.style.display = '';
- } else if (gwa <= 2.00) {
- honorEl.safeHTML = icon('award') + 'Honor Roll track';
- honorEl.className = 'honor-status honor-roll';
- honorEl.style.display = '';
} else {
// Hide the element when not on any honor track
honorEl.textContent = '';
@@ -659,16 +663,14 @@ function displayGradesList(gradesBySemester, allCourses) {
// Build header with group GWA and scholar status
let scholarStatus = '';
let scholarClass = '';
- if (groupGWA.gwa > 0) {
+ // Scholar lists need a full load (15 units) and no 5.00, 4.00 or INC.
+ if (groupGWA.gwa > 0 && groupGWA.totalUnits >= 15 && !groupGWA.hasFailOrInc) {
if (groupGWA.gwa <= 1.45) {
scholarStatus = 'University Scholar';
scholarClass = 'university-scholar';
} else if (groupGWA.gwa <= 1.75) {
scholarStatus = 'College Scholar';
scholarClass = 'college-scholar';
- } else if (groupGWA.gwa <= 2.00) {
- scholarStatus = 'Honor Roll';
- scholarClass = 'honor-roll';
}
}
@@ -749,6 +751,7 @@ function displayGradesList(gradesBySemester, allCourses) {
function calculateGroupGWA(courses) {
let totalWeightedGrade = 0;
let totalUnits = 0;
+ let hasFailOrInc = false;
courses.forEach(course => {
const courseId = String(course.id || `${course.courseCode}-${course.termId}`);
@@ -758,18 +761,20 @@ function calculateGroupGWA(courses) {
return;
}
+ if ((course.grade || '').toString().toUpperCase().trim() === 'INC') hasFailOrInc = true;
const grade = parseFloat(course.grade);
if (isNaN(grade) || grade < 1.0 || grade > 5.0) return;
-
+ if (grade > 3.0) hasFailOrInc = true;
+
const units = course.units || 0;
if (units === 0) return;
-
+
totalWeightedGrade += grade * units;
totalUnits += units;
});
-
+
const gwa = totalUnits > 0 ? totalWeightedGrade / totalUnits : 0;
- return { gwa, totalUnits };
+ return { gwa, totalUnits, hasFailOrInc };
}
// allCourses: every AMIS row, including courses being taken now, so the track
From 7bd993beb264caf84279e590d2438ea51b09bb39 Mon Sep 17 00:00:00 2001
From: Stimmie <137183321+smmariquit@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:25:15 +0800
Subject: [PATCH 05/10] fix(extension): direct grade fetch, full backup, icons
and v2 docs (#103)
* fix(extension): fetch grades directly, slim storage, backup schema, icons
Drop the fetch and XHR patching in content.js, which ran in the isolated
world and never saw AMIS requests. The content script now calls the grades
API once with the page token, stores only the course fields, and records
fetchedAt and lastError. It skips the automatic fetch when grades are under
an hour old. Refresh in the popup messages the open AMIS tab.
Export writes every stored key plus schemaVersion. Import checks source and
schemaVersion and never sets termsAccepted. Clear my data goes through a
CLEAR_DATA message in background.js.
Add 16, 32 and 48 px icons, one name and a v2 description in every
manifest, drop the api-amis host permission from Chrome and Opera, and make
build.sh edge an alias for chrome.
Claude-Session: https://claude.ai/code/session_01T3dKAgr7Yo2637teHP4vNU
* docs: v2 README, contributing, changelog, agents and privacy pointers
Rewrite the README for v2 with all three store links, the web app,
a privacy summary and build steps, and drop the generated block and the
duplicate coffee plug. Document the grades API flow and storage keys in
extension/README.md. Cover Edge, the staging flow and tests in
CONTRIBUTING, bring the CHANGELOG through 2.0.4, and point both privacy
files at gradesim.uplb.tools/privacy with a short summary. Remove the
legacy website folder, scratch test files, the empty package-lock,
fix_curriculum.py and the unused types.d.ts.
Claude-Session: https://claude.ai/code/session_01T3dKAgr7Yo2637teHP4vNU
---
AGENTS.md | 61 ++--
CHANGELOG.md | 42 +++
CONTRIBUTING.md | 34 +-
PRIVACY_POLICY.md | 80 +----
README.md | 82 +++--
extension/PRIVACY_POLICY.md | 80 +----
extension/README.md | 104 ++----
extension/build.sh | 10 +-
extension/content.test.js | 98 ++++++
extension/{src/sims.png => icons/icon128.png} | Bin
extension/icons/icon128.svg | 4 -
extension/icons/icon16.png | Bin 0 -> 813 bytes
extension/icons/icon16.svg | 4 -
extension/icons/icon32.png | Bin 0 -> 1714 bytes
extension/icons/icon48.png | Bin 0 -> 2896 bytes
extension/icons/icon48.svg | 4 -
extension/manifests/manifest.chrome.json | 19 +-
extension/manifests/manifest.firefox.json | 16 +-
extension/manifests/manifest.opera.json | 19 +-
extension/src/background.js | 18 +-
extension/src/content.js | 222 +++++--------
extension/src/popup.html | 9 +
extension/src/popup.js | 186 ++++++-----
extension/src/types.d.ts | 48 ---
fix_curriculum.py | 62 ----
package-lock.json | 6 -
scratch/test.pdf | Bin 160476 -> 0 bytes
scratch/test.txt | 298 ------------------
scratch/test_layout.txt | 120 -------
website/public/arc.svg | 1 -
website/public/brave.svg | 4 -
website/public/edge.svg | 1 -
website/public/opera.svg | 17 -
website/public/safari.svg | 29 --
website/public/vivaldi.svg | 1 -
35 files changed, 497 insertions(+), 1182 deletions(-)
create mode 100644 extension/content.test.js
rename extension/{src/sims.png => icons/icon128.png} (100%)
delete mode 100644 extension/icons/icon128.svg
create mode 100644 extension/icons/icon16.png
delete mode 100644 extension/icons/icon16.svg
create mode 100644 extension/icons/icon32.png
create mode 100644 extension/icons/icon48.png
delete mode 100644 extension/icons/icon48.svg
delete mode 100644 extension/src/types.d.ts
delete mode 100644 fix_curriculum.py
delete mode 100644 package-lock.json
delete mode 100644 scratch/test.pdf
delete mode 100644 scratch/test.txt
delete mode 100644 scratch/test_layout.txt
delete mode 100644 website/public/arc.svg
delete mode 100644 website/public/brave.svg
delete mode 100644 website/public/edge.svg
delete mode 100644 website/public/opera.svg
delete mode 100644 website/public/safari.svg
delete mode 100644 website/public/vivaldi.svg
diff --git a/AGENTS.md b/AGENTS.md
index 5e7a837..e3a877c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,69 +2,58 @@
**Human developers:** start with [README.md](README.md).
-Org-wide agent defaults: see [room-tba/AGENTS.md](https://github.com/uplbtools/room-tba/blob/main/AGENTS.md). This file tailors that playbook to the **GradeSim browser extension**.
+Org-wide agent defaults are in [room-tba/AGENTS.md](https://github.com/uplbtools/room-tba/blob/main/AGENTS.md). This file tailors them to the GradeSim browser extension.
## Doc map
| When | Read |
| --- | --- |
+| How grades get in, storage keys, permissions | [extension/README.md](extension/README.md) |
| Build all browsers | `extension/build.sh` |
| Per-browser manifests | `extension/manifests/` |
-| Landing / marketing site | [uplbtools/gradesim-website](https://github.com/uplbtools/gradesim-website) |
-| Store listings | Chrome Web Store, Firefox Add-ons URLs in README |
+| Web app | [uplbtools/gradesim-website](https://github.com/uplbtools/gradesim-website), live at |
+| Store listings | README |
## Stack
-- **Platform:** Manifest V3 **browser extension** (vanilla JavaScript: no bundler in root)
-- **Targets:** Chrome, Firefox, Opera, Edge (separate manifests)
-- **Build:** `extension/build.sh [chrome|firefox|opera|edge|all]` → `extension/dist//`
-- **Host permission:** `amis.uplb.edu.ph`: content scripts read AMIS DOM locally
-- **No server**: 100% client-side; no `package.json` scripts at repo root
+- Manifest V3 browser extension in plain JavaScript, no bundler and no root `package.json`
+- Targets are Chrome, Firefox and Opera manifests. Edge and Brave install the Chrome build.
+- `extension/build.sh [chrome|firefox|opera|edge|all]` writes `extension/dist//`. `edge` is an alias for `chrome`.
+- Grades come from the AMIS JSON API, not page scraping. `content.js` calls `api-amis.uplb.edu.ph/api/students/grades` with the token AMIS keeps in `localStorage`, then stores only the course fields.
+- No server. Everything stays in `chrome.storage.local`.
## Branches and release
| Branch | Role |
| --- | --- |
-| **`staging`** | Default branch: integration |
-| **`main`** | Release line for store submissions |
+| `staging` | Default branch. All pull requests go here. |
+| `main` | Release line. A push that touches `extension/` publishes to the Chrome, Firefox and Edge stores through `.github/workflows/publish-extension.yml`. |
-**Ship flow:** merge to `main`, build with `./build.sh all`, upload zips to each browser store. Store review is human-gated: do not auto-close issues that depend on store approval.
-
-There is no Vercel/CI deploy: verification is local build + manual load in browser dev mode.
+Never push or open pull requests to `main` directly. Store review is human-gated, so do not close issues that wait on store approval.
## Verify before done
| Step | When |
| --- | --- |
-| `cd extension &&./build.sh chrome` | Minimum before any extension change |
-| `./build.sh all` | Before store release |
-| Manual AMIS | Load unpacked extension; open AMIS schedule page; confirm GWA/simulator still works |
-| Manifest diff | When touching permissions: review all files in `extension/manifests/` |
-
-## Architecture (short)
-
-```
-extension/
- src/ # Popup, content scripts, curriculum data
- manifests/ # Per-browser manifest.json variants
- icons/
- build.sh # Copies src + manifest → dist//
-website/ # Legacy copy: canonical website is gradesim-website repo
-```
+| `for t in extension/*.test.js; do node "$t"; done` | Every change (CI runs it too) |
+| `bash extension/build.sh all` | Every extension change |
+| `npx -y web-ext@8 lint -s extension/dist/firefox` | Must show 0 errors and 0 warnings |
+| Manual AMIS check | Load the unpacked build, log in to AMIS, press Refresh in the popup, confirm grades and GWA |
+| Manifest diff | When touching permissions, update every file in `extension/manifests/` |
## Extension rules
-- **Privacy:** Never send grades or student data to a server: local `chrome.storage` / `browser.storage` only
-- **AMIS DOM scraping:** AMIS markup changes break the extension: prefer defensive selectors; add fixture HTML tests when possible
-- **Multi-browser parity:** A manifest or permission change often needs all four manifest files updated
-- **No decorative UI motion** in popup: keep the panel calm and readable
+- Never send grades or student data to a server.
+- Keep the stored grades shape (`gradesData.student_grades`) compatible with the web app importer in gradesim-website `src/lib/importers.ts`.
+- Firefox add-on review rejects `innerHTML`, `outerHTML` and `insertAdjacentHTML` with dynamic strings. Build markup with `DOMParser` or DOM calls.
+- No decorative motion in the popup. Keep the panel calm and readable.
## Commits
-- Conventional Commits: `fix(content): …`, `feat(curriculum): …`, `chore(build): …`
-- Note which browsers were manually tested in the PR body
+- Conventional Commits, for example `fix(content): ...`, `feat(planner): ...`, `chore(build): ...`
+- Note which browsers you tested in the PR body.
## Security
-- Do not add broad `` permissions without maintainer approval
-- Never exfiltrate AMIS page content to third-party analytics
+- Do not add `` or other broad permissions without maintainer approval.
+- Never send AMIS content to analytics or third parties.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0155ddd..d24d925 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,47 @@
# Changelog
+## [Unreleased]
+
+### Features
+- The web app at gradesim.uplb.tools can import your grades straight from the extension when you press Import there.
+- The planner follows your SP or thesis track.
+
+### Fixes
+- A 4.00 or 5.00 stays in the GWA but no longer counts as a completed course. PEd majors' courses are no longer dropped as PE. No honors show before any graded unit, and scholar badges need 15 units with no 5.00, 4.00 or INC.
+- The popup fills the screen on Firefox for Android.
+- Grades load without reloading AMIS. The extension calls the AMIS grades API once per hour with your login, and Refresh in the popup fetches right away from the open AMIS tab.
+- Only the course fields GradeSim uses are saved, not the whole AMIS response.
+- Export JSON saves every setting, including planner pins, petitions and theme, with a schema version. Import no longer accepts the terms for you.
+- New Clear my data button in Help.
+- Sharper toolbar icons at 16, 32 and 48 px, and one store name, Elbi GradeSim, everywhere.
+
+## [2.0.4] - 2026-10-06
+
+- Same code as 2.0.3, published again.
+
+## [2.0.3] - 2026-10-05
+
+### Fixes
+- Planner legend chips are added without `insertAdjacentHTML`, which the Firefox add-on review flags.
+
+## [2.0.2] - 2026-10-05
+
+### Fixes
+- Planner markup is built with `DOMParser` instead of `innerHTML`.
+
+## [2.0.1] - 2026-10-05
+
+### Fixes
+- Planner icon placeholders are swapped without assigning `outerHTML`.
+
+## [2.0.0] - 2026-10-05
+
+### Features
+- Course planner. A curriculum map of your program with prerequisites from the AMIS catalog, the critical path, retakes, petitions and what-if plans, laid out term by term.
+- UPLB Tools look. New colors, self-hosted fonts, light and dark themes, and a flat plumbob icon.
+- Links point to the new home at gradesim.uplb.tools and its hosted terms and privacy pages.
+- Store builds for Chrome, Firefox and Edge are published from CI.
+
## [1.2.1] - 2026-05-29
### Features
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d694729..ad97e77 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,28 +8,38 @@ Open a [GitHub issue](https://github.com/uplbtools/gradesim/issues) with:
- Browser and extension version
- Steps to reproduce
-- Expected vs actual grade or GWA output
+- Expected and actual grade or GWA output
-You do not need a pull request for typo reports in curriculum data: an issue is enough.
+A typo in curriculum data only needs an issue, not a pull request.
## Developers
-### Setup
+### Build
```sh
-cd extension
-./build.sh chrome # or firefox, all
+bash extension/build.sh all # chrome, firefox and opera
+bash extension/build.sh chrome # one target
```
-Load `extension/dist//` via your browser’s developer / unpacked extension UI, or install from the [Chrome Web Store](https://chromewebstore.google.com/detail/elbi-gradesim-uplb-gwa-ca/mlhklblbhkikcmobmmajckjcbmdinldb) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/elbi-gradesim/).
+Edge, Brave and Opera install the Chrome build, so `build.sh edge` is an alias for `chrome`. Load `extension/dist//` as an unpacked extension in developer mode.
-### Pull requests
+### Test
-1. Branch from **`staging`**.
-2. Keep commits focused; use [Conventional Commits](https://www.conventionalcommits.org/) (`fix(extension): …`, `docs: …`).
-3. Open a PR to **`staging`** and link any related issue.
-4. Do not commit secrets or store listing credentials.
+```sh
+for t in extension/*.test.js; do node "$t"; done
+npx -y web-ext@8 lint -s extension/dist/firefox
+```
+
+CI runs the tests on every pull request. The Firefox lint should report 0 errors and 0 warnings, since addons.mozilla.org rejects or delays builds that warn.
+
+### Branches and pull requests
+
+1. Branch from `staging`.
+2. Keep commits focused and use [Conventional Commits](https://www.conventionalcommits.org/), for example `fix(extension): ...` or `docs: ...`.
+3. Open the pull request against `staging`. Its body must close an issue (`Closes #123`).
+4. Maintainers merge `staging` into `main` to release. Every push to `main` that touches `extension/` publishes to the Chrome, Firefox and Edge stores, so never open a pull request straight to `main`.
+5. Do not commit secrets or store credentials.
## Code of conduct
-See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Be helpful to students; verify grade logic against official UPLB rules when possible.
+See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Be helpful to students, and check grade logic against official UPLB rules when you can.
diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md
index 19d6532..1b3b614 100644
--- a/PRIVACY_POLICY.md
+++ b/PRIVACY_POLICY.md
@@ -1,77 +1,13 @@
# Privacy Policy for Elbi GradeSim
-**Last Updated: January 8, 2026**
+The full, current policy is at ** **.
-## Overview
+In short:
-Elbi GradeSim is a browser extension that helps UPLB students calculate their GWA (General Weighted Average) and simulate grades needed for Latin honors. This privacy policy explains how we handle your data.
+- Elbi GradeSim reads your grades from the UPLB AMIS grades API using your own logged-in session. It never sees or stores your password.
+- It keeps only the course fields it needs, in your browser's extension storage on your device.
+- It sends nothing to any server. There are no analytics, ads or tracking.
+- It shares your grades with the Elbi GradeSim web app at gradesim.uplb.tools only when you press Import there, and that hand-off stays inside your browser.
+- Clear my data in the popup's Help, or removing the extension, deletes everything it stored.
-## Data Collection
-
-### What We Access
-- **Grades Data**: The extension accesses your grades information from the UPLB AMIS (Academic Management Information System) portal when you are logged in.
-
-### What We DO NOT Collect
-- We do NOT collect any personal identification information
-- We do NOT collect your AMIS login credentials
-- We do NOT track your browsing activity
-- We do NOT use analytics or tracking services
-- We do NOT share any data with third parties
-
-## Data Storage
-
-- **Local Storage Only**: All your grades data is stored locally on your device using Chrome's built-in storage (`chrome.storage.local`)
-- **No External Servers**: Your data is NEVER sent to any external servers
-- **No Cloud Sync**: Your data is not synced across devices
-
-## Data Usage
-
-Your grades data is used exclusively for:
-1. Calculating your current GWA
-2. Displaying your grades history
-3. Simulating grades needed for target Latin honors
-4. Tracking remaining courses in your curriculum
-
-## Permissions Explained
-
-| Permission | Why We Need It |
-|------------|----------------|
-| `storage` | To save your grades data locally on your device |
-| `host_permissions` for amis.uplb.edu.ph | To access the AMIS portal and read your grades |
-| `externally_connectable` for gradesim.uplb.tools (Chrome, Edge, Opera) and a content script there (Firefox) | So the Elbi GradeSim web app can ask for your grades when you click Import. They pass to the page inside your browser and are never sent over the network |
-
-## Data Deletion
-
-You can delete all stored data at any time by:
-1. Right-clicking the extension icon
-2. Selecting "Remove from Chrome"
-3. Or going to Chrome Settings > Extensions > Elbi GradeSim > Remove
-
-Alternatively, clear the extension's data via:
-Chrome Settings > Privacy and Security > Clear Browsing Data > Cookies and other site data
-
-## Security
-
-- The extension reads grades only on the official UPLB AMIS website (amis.uplb.edu.ph), and shares them only with the Elbi GradeSim web app at gradesim.uplb.tools when you ask it to
-- We use your existing AMIS session - we never ask for or store your password
-- All data processing happens locally in your browser
-
-## Children's Privacy
-
-This extension is intended for university students and does not knowingly collect information from children under 13.
-
-## Changes to This Policy
-
-We may update this privacy policy from time to time. Any changes will be reflected in the "Last Updated" date.
-
-## Contact
-
-If you have questions about this privacy policy, please open an issue on our GitHub repository.
-
-## Open Source
-
-This extension is open source. You can review the complete source code to verify our privacy practices.
-
----
-
-**Summary**: Your data stays on YOUR device. We don't collect, store, or share anything.
+Questions go to the [GitHub issues](https://github.com/uplbtools/gradesim/issues).
diff --git a/README.md b/README.md
index ae75de5..df81212 100644
--- a/README.md
+++ b/README.md
@@ -1,65 +1,55 @@
# Elbi GradeSim
-A browser extension for UPLB students to calculate their GWA and simulate grades needed for Latin honors. Available on **Chrome**, **Firefox**, Opera, and Edge.
+Elbi GradeSim is a browser extension for UPLB students. It reads your grades from AMIS, works out your GWA, shows what you need for Latin honors, and plans the courses you still have to take.
-## Repository Structure
+It is not affiliated with UP. The GWA it shows is an unofficial estimate, so always verify with the OUR.
-```
-gradesim/
-├── extension/ # Browser extension source
-│ ├── src/ # Shared source (popup, planner, content scripts, curriculum data)
-│ ├── manifests/ # Per-browser manifest files
-│ ├── icons/ # Extension icons
-│ └── build.sh # Build script for each browser target
-├── scratch/ # Curriculum data pipeline (PDF parsing, prereq graphs)
-└── README.md
-```
+## Get it
-The landing page lives in a separate repo: [gradesim-website](https://github.com/smmariquit/gradesim-website).
+- [Chrome Web Store](https://chromewebstore.google.com/detail/elbi-gradesim-uplb-gwa-ca/mlhklblbhkikcmobmmajckjcbmdinldb) (also works in Brave)
+- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/elbi-gradesim/), desktop and Android
+- [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/elbi-gradesim-uplb-gwa-/ebiakebpglddgmkdehdjiadnjgkmgnga)
-## Extension
+No extension? The web app at [gradesim.uplb.tools](https://gradesim.uplb.tools) does the same math with grades you import or type in.
-### Build
+## What it does
-```bash
-cd extension
-./build.sh all # Build all browsers
-./build.sh chrome # Build Chrome only
-./build.sh firefox # Build Firefox only
-```
+- Reads your grades from AMIS while you are logged in and computes your GWA
+- Shows your Latin honors standing and the average you need on your remaining units
+- Lists your grades by semester or year, with per-term scholar status
+- Lets you leave courses out of the GWA and record substitutions
+- Plans your remaining courses term by term on a curriculum map, using real prerequisites
+- Covers 30+ UPLB degree programs
+- Backs up and restores all your data as a JSON file, and clears it with one button
-Output goes to `extension/dist//`. Load the folder in your browser's developer mode.
+## Privacy
-### Install from Store
+Your grades stay in your browser. The extension calls the AMIS grades API with your own login, keeps only the course fields it needs in `chrome.storage.local`, and sends nothing to any server. There are no analytics. It hands your grades to the web app only when you press Import there. Full policy at [gradesim.uplb.tools/privacy](https://gradesim.uplb.tools/privacy/).
-- [Chrome Web Store](https://chromewebstore.google.com/detail/elbi-gradesim-uplb-gwa-ca/mlhklblbhkikcmobmmajckjcbmdinldb)
-- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/elbi-gradesim/)
+## Build from source
-### Features
+```sh
+bash extension/build.sh all # chrome, firefox and opera
+bash extension/build.sh firefox # one target; edge is an alias for chrome
+for t in extension/*.test.js; do node "$t"; done
+```
-- Automatic GWA calculation from AMIS
-- "What If?" simulator for Latin honor targets
-- Grades overview by semester or year
-- 30+ UPLB degree programs supported
-- 100% local: no data leaves your device
+Load `extension/dist//` as an unpacked extension in developer mode. See [CONTRIBUTING.md](CONTRIBUTING.md) for the branch flow.
-## Privacy
+## Repository
-All data stays on your device. No servers, no analytics, no tracking. See [PRIVACY_POLICY.md](extension/PRIVACY_POLICY.md).
+```text
+extension/
+ src/ popup, planner, content and background scripts, curriculum data
+ manifests/ one manifest per browser
+ icons/ 16, 32, 48 and 128 px icons
+ build.sh copies src and the right manifest into dist//
+ *.test.js node self-checks
+scratch/ curriculum pipeline (checklist PDFs, parsers, prerequisite graphs)
+```
+
+The web app lives in [uplbtools/gradesim-website](https://github.com/uplbtools/gradesim-website).
## License
MIT
-
-
----
-*If this project helped you out, consider [treating me to a coffee](https://kape.stimmie.dev) ☕*
-
-## 📊 Current State of the Code
-- **Tech Stack:** Static / Basic Scripts
-- **Repository Size:** 131 tracked files
-- **Latest Update:** `8c99fa0 chore: add stale issue and PR validators`
-
-
----
-*☕ If you found this project useful, you can support my work at [kape.stimmie.dev](https://kape.stimmie.dev)!*
\ No newline at end of file
diff --git a/extension/PRIVACY_POLICY.md b/extension/PRIVACY_POLICY.md
index 19d6532..1b3b614 100644
--- a/extension/PRIVACY_POLICY.md
+++ b/extension/PRIVACY_POLICY.md
@@ -1,77 +1,13 @@
# Privacy Policy for Elbi GradeSim
-**Last Updated: January 8, 2026**
+The full, current policy is at ** **.
-## Overview
+In short:
-Elbi GradeSim is a browser extension that helps UPLB students calculate their GWA (General Weighted Average) and simulate grades needed for Latin honors. This privacy policy explains how we handle your data.
+- Elbi GradeSim reads your grades from the UPLB AMIS grades API using your own logged-in session. It never sees or stores your password.
+- It keeps only the course fields it needs, in your browser's extension storage on your device.
+- It sends nothing to any server. There are no analytics, ads or tracking.
+- It shares your grades with the Elbi GradeSim web app at gradesim.uplb.tools only when you press Import there, and that hand-off stays inside your browser.
+- Clear my data in the popup's Help, or removing the extension, deletes everything it stored.
-## Data Collection
-
-### What We Access
-- **Grades Data**: The extension accesses your grades information from the UPLB AMIS (Academic Management Information System) portal when you are logged in.
-
-### What We DO NOT Collect
-- We do NOT collect any personal identification information
-- We do NOT collect your AMIS login credentials
-- We do NOT track your browsing activity
-- We do NOT use analytics or tracking services
-- We do NOT share any data with third parties
-
-## Data Storage
-
-- **Local Storage Only**: All your grades data is stored locally on your device using Chrome's built-in storage (`chrome.storage.local`)
-- **No External Servers**: Your data is NEVER sent to any external servers
-- **No Cloud Sync**: Your data is not synced across devices
-
-## Data Usage
-
-Your grades data is used exclusively for:
-1. Calculating your current GWA
-2. Displaying your grades history
-3. Simulating grades needed for target Latin honors
-4. Tracking remaining courses in your curriculum
-
-## Permissions Explained
-
-| Permission | Why We Need It |
-|------------|----------------|
-| `storage` | To save your grades data locally on your device |
-| `host_permissions` for amis.uplb.edu.ph | To access the AMIS portal and read your grades |
-| `externally_connectable` for gradesim.uplb.tools (Chrome, Edge, Opera) and a content script there (Firefox) | So the Elbi GradeSim web app can ask for your grades when you click Import. They pass to the page inside your browser and are never sent over the network |
-
-## Data Deletion
-
-You can delete all stored data at any time by:
-1. Right-clicking the extension icon
-2. Selecting "Remove from Chrome"
-3. Or going to Chrome Settings > Extensions > Elbi GradeSim > Remove
-
-Alternatively, clear the extension's data via:
-Chrome Settings > Privacy and Security > Clear Browsing Data > Cookies and other site data
-
-## Security
-
-- The extension reads grades only on the official UPLB AMIS website (amis.uplb.edu.ph), and shares them only with the Elbi GradeSim web app at gradesim.uplb.tools when you ask it to
-- We use your existing AMIS session - we never ask for or store your password
-- All data processing happens locally in your browser
-
-## Children's Privacy
-
-This extension is intended for university students and does not knowingly collect information from children under 13.
-
-## Changes to This Policy
-
-We may update this privacy policy from time to time. Any changes will be reflected in the "Last Updated" date.
-
-## Contact
-
-If you have questions about this privacy policy, please open an issue on our GitHub repository.
-
-## Open Source
-
-This extension is open source. You can review the complete source code to verify our privacy practices.
-
----
-
-**Summary**: Your data stays on YOUR device. We don't collect, store, or share anything.
+Questions go to the [GitHub issues](https://github.com/uplbtools/gradesim/issues).
diff --git a/extension/README.md b/extension/README.md
index e53d72f..a38fe5d 100644
--- a/extension/README.md
+++ b/extension/README.md
@@ -1,92 +1,42 @@
-# Elbi GradeSim - UPLB GWA Calculator
+# Elbi GradeSim extension
-A Chrome extension for UPLB students to calculate their GWA and simulate grades needed for Latin honors.
+Source for the Elbi GradeSim browser extension. Start with the [main README](../README.md) for what it does and where to install it.

-## Features
+## How grades get in
-### 1. Automatic GWA Calculation
-- Automatically captures your grades from AMIS when you visit the Personal Information page
-- Calculates your current GWA (weighted average)
-- Excludes NSTP, PE, and HK courses from calculation (per UP policy)
-- Shows your current Latin honor track
+`src/content.js` runs on `amis.uplb.edu.ph`. When the page loads it reads the AMIS login token from the page's `localStorage` and calls `https://api-amis.uplb.edu.ph/api/students/grades?summarize=true` once. It saves the result to `chrome.storage.local`:
-### 2. Grades Overview
-- View all your grades organized by semester
-- See course codes, titles, units, and grades
-- Color-coded grades for easy visualization
+| Key | Meaning |
+| --- | --- |
+| `gradesData` | `{ student_grades: { : { term, values: [...] } } }` with only id, grade, unit_taken, section, status, course code and title, and grade term |
+| `fetchedAt` | Time of the last good fetch, in ms |
+| `lastError` | `'not-logged-in'`, `'amis-error'` or `null` |
-### 3. Track Detection
-- Automatically detects your track (Thesis or SP) based on enrolled courses
-- CMSC 200 = Thesis Track (15 free elective units)
-- CMSC 190 = SP Track (18 free elective units)
+It skips the automatic fetch when `fetchedAt` is under an hour old. The popup's Refresh button sends `{ type: 'FETCH_GRADES', force: true }` to the open AMIS tab, which fetches right away and replies with `{ ok, lastError, fetchedAt }`.
-### 4. "What If?" Simulator
-- Calculate what grades you need in remaining courses to achieve a target GWA
-- Preset targets:
- - **Summa Cum Laude**: GWA ≤ 1.20
- - **Magna Cum Laude**: GWA ≤ 1.45
- - **Cum Laude**: GWA ≤ 1.75
- - **Honor Roll**: GWA ≤ 2.00
- - **Custom**: Enter any target GWA
-- Shows remaining courses based on your selected curriculum
-- Calculates required average grade for remaining courses
+Other keys: `selectedProgram`, `selectedTracks`, `excludedCourses`, `substitutions`, `customCourseStatus`, `plannerPins`, `plannerPetitions`, `plannerOptions`, `theme`, `termsAccepted`. Export JSON writes all of them plus `schemaVersion`. Import restores all of them except `termsAccepted` and `lastError`. `{ type: 'CLEAR_DATA' }` to the background script clears everything.
-## Installation
+## Permissions
-### From Chrome Web Store
-1. Visit the [Chrome Web Store page](https://chrome.google.com/webstore) (Coming soon)
-2. Click "Add to Chrome"
+| Permission | Why |
+| --- | --- |
+| `storage` | Keeps grades and settings on your device |
+| `amis.uplb.edu.ph` | Runs the content script on AMIS and lets the popup find the AMIS tab for Refresh |
+| `api-amis.uplb.edu.ph` (Firefox only) | Lets the content script call the grades API. Chrome sends content script requests with the page's origin, and the API already allows `amis.uplb.edu.ph`, so Chrome, Edge and Opera do not need it |
+| `externally_connectable` / `bridge.js` | Lets gradesim.uplb.tools ask for your grades when you press Import there |
-### Developer Mode (Manual Installation)
-1. Download or clone this repository
-2. Run `cd extension && ./build.sh chrome`
-3. Open Chrome and go to `chrome://extensions/`
-4. Enable "Developer mode" (toggle in the top right)
-5. Click "Load unpacked"
-6. Select `extension/dist/chrome`
+## Build and test
-## Usage
+```sh
+bash build.sh all
+for t in *.test.js; do node "$t"; done
+npx -y web-ext@8 lint -s dist/firefox
+```
-1. Log in to [AMIS](https://amis.uplb.edu.ph)
-2. Go to the Personal Information page
-3. Click the extension icon to view your GWA and use the What If? simulator
+Load `dist/chrome` (Chrome, Edge, Brave, Opera) or `dist/firefox` as an unpacked extension.
-## Supported Programs
+## Curriculum data
-Currently supports:
-- **BS Computer Science** - University of the Philippines Los Baños
-
-More programs coming soon! Other colleges (CAS, CAFS, CEM, CEAT, CDC, CFNR, CHE, UPRHS) are listed but curriculum data is pending.
-
-## Privacy Policy
-
-**Your data stays on YOUR device.**
-
-- ✅ All data is stored locally using Chrome's built-in storage
-- ✅ No external servers - everything runs in your browser
-- ✅ No analytics or tracking
-- ✅ No data collection or sharing with third parties
-- ✅ We never see or store your AMIS credentials
-
-For full details, see [PRIVACY_POLICY.md](PRIVACY_POLICY.md)
-
-## Permissions Explained
-
-| Permission | Why We Need It |
-|------------|----------------|
-| `storage` | To save your grades data locally on your device |
-| `host_permissions` | To access AMIS portal and read your grades when you're logged in |
-
-## Technical Details
-
-The extension intercepts the `grades?summarize=true` API call made by AMIS to get your grades data. This data is stored locally in your browser and never sent to any external server.
-
-## Contributing
-
-Feel free to add more curricula! Edit `curriculum.js` to add courses for other degree programs.
-
-## License
-
-MIT License
+Programs live in `src/curriculum.js` and the AMIS course catalog in `src/catalog.js`. The pipeline that builds them from the official checklists is in `../scratch/`.
diff --git a/extension/build.sh b/extension/build.sh
index d906e3b..7b72b73 100755
--- a/extension/build.sh
+++ b/extension/build.sh
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Build browser-specific extension packages from shared source.
-# Usage: ./build.sh [chrome|firefox|opera|edge|all]
+# Usage: ./build.sh [chrome|firefox|opera|all]
+# Edge installs the Chrome build, so `edge` is an alias for `chrome`.
set -euo pipefail
@@ -15,7 +16,7 @@ build_target() {
local manifest="$MANIFESTS_DIR/manifest.${target}.json"
if [ ! -f "$manifest" ]; then
- echo "⚠ No manifest found for '$target' at $manifest — skipping"
+ echo "No manifest for '$target' at $manifest, skipping"
return 1
fi
@@ -33,8 +34,6 @@ build_target() {
cp "$SRC_DIR"/tokens.css "$SRC_DIR"/plumbob.svg "$out/"
cp -r "$SRC_DIR"/fonts "$out/"
- [ -f "$SRC_DIR/sims.png" ] && cp "$SRC_DIR/sims.png" "$out/"
-
# Copy icons
mkdir -p "$out/icons"
cp "$ICONS_DIR"/* "$out/icons/"
@@ -46,11 +45,12 @@ build_target() {
}
targets="${1:-all}"
+[ "$targets" = "edge" ] && targets="chrome"
if [ "$targets" = "all" ]; then
for manifest in "$MANIFESTS_DIR"/manifest.*.json; do
name=$(basename "$manifest" .json | sed 's/manifest\.//')
- build_target "$name" || true
+ build_target "$name"
done
else
build_target "$targets"
diff --git a/extension/content.test.js b/extension/content.test.js
new file mode 100644
index 0000000..6c2b87b
--- /dev/null
+++ b/extension/content.test.js
@@ -0,0 +1,98 @@
+// Self-check for content.js. Run: node extension/content.test.js
+// Loads the script into a fake AMIS page (localStorage token, mocked grades
+// API, in-memory chrome.storage.local) and checks what it stores.
+const assert = require('assert');
+const fs = require('fs');
+const vm = require('vm');
+
+const SRC = fs.readFileSync(require.resolve('./src/content.js'), 'utf8');
+
+const AMIS_RESPONSE = {
+ student_grades: {
+ 1241: {
+ term: '1st Semester AY 2024-2025',
+ secret: 'term-level extra',
+ values: [{
+ id: 77, grade: '1.25', unit_taken: '3', section: 'A', status: 'Final',
+ student_number: '2022-00000', remarks: 'x',
+ course: { course_code: 'CMSC 12', title: 'Foundations', description: 'long text' },
+ grade_term: { term: '1st Semester', ay: '2024-2025', id: 9 },
+ }],
+ },
+ },
+ student: { name: 'Juan', address: 'somewhere' },
+};
+
+async function page({ token, status = 200, body = AMIS_RESPONSE, stored = {} }) {
+ const store = { ...stored };
+ const calls = [];
+ let listener;
+ const ctx = {
+ localStorage: { getItem: k => (k === 'auth._token.local' ? token : null) },
+ fetch: async (url, opts) => {
+ calls.push({ url, opts });
+ return { ok: status >= 200 && status < 300, status, json: async () => body };
+ },
+ chrome: {
+ storage: { local: {
+ get: async k => (typeof k === 'string' && k in store ? { [k]: store[k] } : {}),
+ set: async o => { Object.assign(store, o); },
+ } },
+ runtime: { onMessage: { addListener: fn => { listener = fn; } } },
+ },
+ };
+ vm.runInNewContext(SRC, ctx);
+ await new Promise(r => setTimeout(r, 10)); // let the load-time fetch finish
+ const send = msg => new Promise(r => assert.strictEqual(listener(msg, {}, r), true));
+ return { store, calls, send };
+}
+
+(async () => {
+ // Logged in: stores only the course fields, plus fetchedAt and lastError.
+ let p = await page({ token: 'abc' });
+ assert.strictEqual(p.calls.length, 1);
+ assert.strictEqual(p.calls[0].opts.headers.Authorization, 'Bearer abc');
+ assert.strictEqual(p.store.lastError, null);
+ assert.ok(Date.now() - p.store.fetchedAt < 1000);
+ assert.deepStrictEqual(Object.keys(p.store).sort(), ['fetchedAt', 'gradesData', 'lastError']);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(p.store.gradesData)), {
+ student_grades: { 1241: { term: '1st Semester AY 2024-2025', values: [{
+ id: 77, grade: '1.25', unit_taken: '3', section: 'A', status: 'Final',
+ course: { course_code: 'CMSC 12', title: 'Foundations' },
+ grade_term: { term: '1st Semester', ay: '2024-2025' },
+ }] } },
+ });
+
+ // Logged out: no token (Nuxt writes "false") means no request.
+ p = await page({ token: 'false' });
+ assert.strictEqual(p.calls.length, 0);
+ assert.strictEqual(p.store.lastError, 'not-logged-in');
+ assert.strictEqual(p.store.gradesData, undefined);
+
+ // Expired token: AMIS answers 401.
+ p = await page({ token: 'Bearer old', status: 401, body: { message: 'Unauthenticated.' } });
+ assert.strictEqual(p.store.lastError, 'not-logged-in');
+
+ // AMIS error keeps the grades already saved.
+ const old = { gradesData: { student_grades: {} }, fetchedAt: 1 };
+ p = await page({ token: 'abc', status: 500, stored: old });
+ assert.strictEqual(p.store.lastError, 'amis-error');
+ assert.strictEqual(p.store.fetchedAt, 1);
+ assert.deepStrictEqual(p.store.gradesData, old.gradesData);
+
+ // Grades under an hour old: no automatic fetch, but Refresh (force) fetches.
+ p = await page({ token: 'abc', stored: { fetchedAt: Date.now() - 60000 } });
+ assert.strictEqual(p.calls.length, 0);
+ const res = await p.send({ type: 'FETCH_GRADES', force: true });
+ assert.strictEqual(p.calls.length, 1);
+ assert.strictEqual(res.ok, true);
+ assert.strictEqual(res.lastError, null);
+
+ // Refresh while logged out reports it.
+ p = await page({ token: null });
+ const out = await p.send({ type: 'FETCH_GRADES', force: true });
+ assert.strictEqual(out.ok, false);
+ assert.strictEqual(out.lastError, 'not-logged-in');
+
+ console.log('content.test.js: all passed');
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/extension/src/sims.png b/extension/icons/icon128.png
similarity index 100%
rename from extension/src/sims.png
rename to extension/icons/icon128.png
diff --git a/extension/icons/icon128.svg b/extension/icons/icon128.svg
deleted file mode 100644
index eb36037..0000000
--- a/extension/icons/icon128.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- G
-
diff --git a/extension/icons/icon16.png b/extension/icons/icon16.png
new file mode 100644
index 0000000000000000000000000000000000000000..e85c292d95ddbaef3a2acd81706c535c8530ed1b
GIT binary patch
literal 813
zcmV+|1JeA7P)cn@MYD0^(uJiIyODwvnvJN4brb5J
zMoXb!LBvJDMN@=|3k4AcL8&y>)}(1e#x^s_q?tEw-o5vnE=oFHU(U0s}Gh~V${EDqy@
z!;U>#X@^Qt0u})&Qng|Q7W#E1nRtUalT%%y{;{3C_xeFqo@+N&!pEFe*59Ktc9jcHUzC2+va&c7yvuL86lzIjkiqzy6S)3Tf#XWiMAgE)RrU`SFyjgM?opk
rinbG!V{nr|>bs@i8Cy00000NkvXXu0mjfVF-m6
literal 0
HcmV?d00001
diff --git a/extension/icons/icon16.svg b/extension/icons/icon16.svg
deleted file mode 100644
index e52f468..0000000
--- a/extension/icons/icon16.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- G
-
diff --git a/extension/icons/icon32.png b/extension/icons/icon32.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d6402c25fe077b8c8df990504f61ce3acc4bccc
GIT binary patch
literal 1714
zcmV;j22J^iP)pZ)WCcm21vV+VWfw81Q`)fCaH&?-3T4V*w`VjD
zGjr}e-~G<{&YjD!1N+Q(9>nsoxA2V@zQ=~lIE~mn;Z$f)78+%*Rcqoz@;wy(dIj0n
zFZiC*^y?TJ>qU6H4_|omyBM@m2GfuiH`b3{(78-xIP!c$IF`(}MAy{NPSynG*Ky+T
zBQPTYFxxN-^|F*LG4sIGzP~u4CC*%UYj0Oq_qAP#0Yg?@r5u5!SI+vHm;0rD@n@%@
z9KjlyHTl`qNT9d3=LBO61hP_N6DMMyA3@^26uG%7y6+mn@RRrY0$})|zW?DVj6eMq
zfWZtVyLB_pj*!6)_D2#zXh}8UOoXc_w$^KN!J(L?rP5}15F2MRpPqr?C+~%BNz^W9
zO<8VAuhcxiEIvzsCXfZd00UqfFM6eBN>&}A>aL;cEsQ<2|I-1umHOiO#efmAj)O4(
zL~S<)f@GZ8RxETPnjYR!Dz;5QC+Y)3u_N3v4l>4s(#q_M#38gq)opNt8xovNc==k<
zE7X0bVp|3}QO_-77Jvp|ePjS_E{5CYj#AMPz`kc5|B(9H)toH-k9y8FLv=O=K%$+B
zuJkZ^@1G<$S3!Jw6kq)2WBvg=KJ!)lg5F1Tyx*|W1L%El&(FDGj4^^>fWZK;_61F}dEGrdYPz*1?n#pyznIJ3oOH`|DXT6g>P%e6R-DVPu>-|1)w)@+nvbS`
zC7{so9I^}(oNZ!OS|J{aLMny6Z~_MZ@I6yn%Sa$(KQMClQ8pHRSeiid^auU{IIcs*
znJCvPAb7TVxhI-PDXp+v&Y|u(uniMRD~$K-f+JeU)i%HxgU|}GKm=onA(+fSsabws
zd{BkKIqTl0>vsgu6^`K2-~Y*^QYh3)FAWYPzcvz2vhl<)DsCOuD{Ck=s~GO?M^m`i
zXqM3zNg$ONM%WJ0YIWVrR`Mb|6J+(nE=J(Dd$7
zDa+r>%_~J3(?fS4Jvf1YVWZ0qVS4CJq?0MsJqMSv8I`TB!yx|0-BXh*(a}D0=_gsO
zp*sTT*odw-*AYtfffmYU|GKG3D6%a35P|f1qr~%#5~6`HMq){X?I7~?A~J;)l$;t2
zZopuisirq;OTNN9#SVPw2lV?#4ugdHNUUwQACK^NA5Gb0&Mlv9GTG|7GF5+zXkxHvAB-dzTHFv
z$^=kTu6T`c^H~BZ#+Ydv78n4|7;M7=2%z@URnhJgNJNc{M*8IVYY9@*H4}Y
z;~b*mXs^_Ss<)1VNVP!|ZE1*YjcWiVX}VFry7q_0d>*3IP9JS@s
zXjdL>FZ|YV1O^kDJXLF*6r~1a#nIg4SpD5QpB#WFHL>T%kD+z5qQ-tW!;(+jbB45j
zw>@qv3km>8=|2TW;!DBdxN-HlKkNE`ZsK-!+g~zfa)eX;sQoKzHs|x2gd%PlhZHH@
z7m0JBC918)rt};!t6FkJIfB3aWt-l4;^mIg^UVMNY@E%&j0Pb}P30^X;Vc(lho&_K
zX12Fx1%Q*Hx%mC#Z};uLgSo3E^^e29fogiUWx&_D;8saeOT7k;XBn7kR{$XOvFb&C
z(%TUO=pae%=;NeG5nvEtlGZoEtDbM^?HB-Vk>o~9EJ!KeAZSKJ@I3Kqem8te?-K#o
zDoL+UN4ZiyN(69X?#lJ0lldDt-_qM^zK>=AF!`fLq3SJM`{fx{1nAqRp1I~p@$b*2
z?mXm5&v<#}`)d5@{aAYCobNerE5rXo+m_^&;<9<}%6T1vNij2gMl@SjWuwJUr3=2K
zx7B=~j?tE6|G_CFE;b|xsEEd|1KiSG+D2bJppIPfC%vuqU*@X4N?V1>lmGw#07*qo
IM6N<$f{(FBwg3PC
literal 0
HcmV?d00001
diff --git a/extension/icons/icon48.png b/extension/icons/icon48.png
new file mode 100644
index 0000000000000000000000000000000000000000..2d076437183c79219b2b63931d223209de765f8b
GIT binary patch
literal 2896
zcmV-W3$OHvP)$yzpk
z9)wf0s~TP_0#byt-!@Mq%MD8nskF_GkB{o=i`tQTcc^GSNwFrXU)KTVH&EJIOZ&%%
z6v8L!Z{2Wbc*-_E_{-NHJ|6&@ZtWzkw|zk$d*oG2>^e{szqA;W)ZKV4>Tg(s?|Q03pY?7pk<-TPmZZahaqUs;3Tg>C=)47A>UF@~Oa
z1559^l9bd}YURlnsE@A}N{ObbI%atejhiXKxzQ4Tqt|)=gPr^Riz^M0v6XLeITCLV
zEXeS)o>@X@ZvdYPo#x45^rRszDGd;=+ovU(2#@?V$_yB
zefH*Sv3|=Q=_PWc?T*W^_`22e1~4xQ1=?!SyuK5Ydyk;$*0sc6U#YdHUZ6g<8XAHS
z!lY7Ys;YzKIf$ESaKk9m9VP~2UdEP?3PZns
zea^AWnL_pJI^aYy5NV6r;k0LsBpO-SG<80CzK>IB+wupl!}sogQM%DA34OT(bvK?n
zX8?1eP~DB^B6L#+PCWllwBLC-DXE)Loji4B<|81G`V
T44Yj(DIuWsML&6V^Mg0PwD~?TO07M8vSH8sHS8R$2L3kj_-0F6sp^Z<=p-W1|?)
zPC{!9gMBoeBRpKv-hs(n3Li!X5#at=Af6O37)NV$2<_Dkh?yyVEH;LL!DBzI{IDDW
z!;eq%>f!+il$GM>k)zU0n<$6>k0XBezNqn^x>@fW?s?|G`WWm80n1Wl5#XXK?#6mpK3u3}Q)51Cmx2re|Zt
zqIS5_!@zW=6f_LV6(NKwPxc(Wd&epOs^wdk5Pzsr7kd3tFo31szY@nDdIjhG={Ba7
zlGaFK7xnWSpdkq%Olu8S3iudDTXhJn!4Lw5A4+Rv9TUeAV+cj<#^-`wRwn$Px{~8FHT?o$d(@o}57*2&z
zS5b?JsRZ5~IfR^R!JrI`uvvWq0n<{Yt8mF$4PKVkV&h?aA#GpJ|Ae
zuc+r&{OnK(COtq
zt=Ms`G(^_kDs2w{QBw#bdt`i00Or(1PV7F2Kyx*mNJi#*Cy?u(d_hY!2?BmvnLX1F
zADILJv{0`EKik$Fys(X?KNwMOZg>FM!*c^L=QFXFGO_&Lt00BsC5=H-nr=PitP_Bo
zSy{Q4OuB%sHF9l-c7%vUKFhH+80a?iXEiK2j$CF^E*xD4cLc+2bk5jP&_FwhsW18;Is3a^;iM$8G>2pHx>g2)Gt1ZI30N
zCk6ux_@q%?io{#}^8zsMi>;H_tBMG$Mc_|1X+#jkkrRmjE&N>N*lv>eKt`;>Es5IS8ULx1yL^5=A+kTzvi9mY=
z9lna20RYGzJb~8kdl$n>*)3cxeFfC^i7w%`n4y45aU70P$1
zfPf>nqJ%sFLX^(0K49vnY%2p0s3^J-+3`V902Jk!HL|lC{`xA|;k3;5L_mhK+m)1)
zT5GPMAwBU*d@1h=Uf4!c|2U@ht-ou*n|H>F_EImW95ipd7|h3bHmFTyd7v-NGiHw_kbCG|^^@LbnQJ
-
- G
-
diff --git a/extension/manifests/manifest.chrome.json b/extension/manifests/manifest.chrome.json
index 62b00dc..c04bd08 100644
--- a/extension/manifests/manifest.chrome.json
+++ b/extension/manifests/manifest.chrome.json
@@ -1,23 +1,28 @@
{
"manifest_version": 3,
- "name": "Elbi GradeSim - UPLB GWA Calculator",
+ "name": "Elbi GradeSim",
"version": "2.0.0",
- "description": "Calculate your GWA and simulate grades needed for Latin honors at UPLB. All data stays on your device.",
+ "description": "UPLB GWA calculator and Latin honors simulator, with a planner for your remaining courses. Your grades stay on your device.",
"permissions": [
"storage"
],
"host_permissions": [
- "https://amis.uplb.edu.ph/*",
- "https://api-amis.uplb.edu.ph/*"
+ "https://amis.uplb.edu.ph/*"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
- "128": "sims.png"
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
}
},
"icons": {
- "128": "sims.png"
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
},
"background": {
"service_worker": "background.js"
@@ -30,7 +35,7 @@
"js": [
"content.js"
],
- "run_at": "document_start"
+ "run_at": "document_idle"
}
],
"externally_connectable": {
diff --git a/extension/manifests/manifest.firefox.json b/extension/manifests/manifest.firefox.json
index b384c2d..b29b4e8 100644
--- a/extension/manifests/manifest.firefox.json
+++ b/extension/manifests/manifest.firefox.json
@@ -1,6 +1,6 @@
{
"manifest_version": 3,
- "name": "Elbi GradeSim - UPLB GWA Calculator",
+ "name": "Elbi GradeSim",
"version": "2.0.0",
"browser_specific_settings": {
"gecko": {
@@ -16,7 +16,7 @@
"strict_min_version": "120.0"
}
},
- "description": "Calculate your GWA and simulate grades needed for Latin honors at UPLB. All data stays on your device.",
+ "description": "UPLB GWA calculator and Latin honors simulator, with a planner for your remaining courses. Your grades stay on your device.",
"permissions": [
"storage"
],
@@ -27,11 +27,17 @@
"action": {
"default_popup": "popup.html",
"default_icon": {
- "128": "sims.png"
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
}
},
"icons": {
- "128": "sims.png"
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
},
"background": {
"scripts": [
@@ -46,7 +52,7 @@
"js": [
"content.js"
],
- "run_at": "document_start"
+ "run_at": "document_idle"
},
{
"matches": [
diff --git a/extension/manifests/manifest.opera.json b/extension/manifests/manifest.opera.json
index 62b00dc..c04bd08 100644
--- a/extension/manifests/manifest.opera.json
+++ b/extension/manifests/manifest.opera.json
@@ -1,23 +1,28 @@
{
"manifest_version": 3,
- "name": "Elbi GradeSim - UPLB GWA Calculator",
+ "name": "Elbi GradeSim",
"version": "2.0.0",
- "description": "Calculate your GWA and simulate grades needed for Latin honors at UPLB. All data stays on your device.",
+ "description": "UPLB GWA calculator and Latin honors simulator, with a planner for your remaining courses. Your grades stay on your device.",
"permissions": [
"storage"
],
"host_permissions": [
- "https://amis.uplb.edu.ph/*",
- "https://api-amis.uplb.edu.ph/*"
+ "https://amis.uplb.edu.ph/*"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
- "128": "sims.png"
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
}
},
"icons": {
- "128": "sims.png"
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
},
"background": {
"service_worker": "background.js"
@@ -30,7 +35,7 @@
"js": [
"content.js"
],
- "run_at": "document_start"
+ "run_at": "document_idle"
}
],
"externally_connectable": {
diff --git a/extension/src/background.js b/extension/src/background.js
index e7ed73b..cd1ddc1 100644
--- a/extension/src/background.js
+++ b/extension/src/background.js
@@ -4,25 +4,13 @@
// Privacy: All data is stored locally using chrome.storage.local
// No data is ever sent to external servers
-// Store for grades data (in-memory cache)
-let gradesData = null;
-
-// Listen for messages from content script
+// Clear my data (popup Help). Removes every key this extension stored.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
- if (message.type === 'GRADES_DATA') {
- gradesData = message.data;
- // Store locally for popup access
- chrome.storage.local.set({ gradesData: message.data });
- }
-
- if (message.type === 'GET_GRADES_DATA') {
- sendResponse({ data: gradesData });
- }
-
+ if (!message || message.type !== 'CLEAR_DATA') return false;
+ chrome.storage.local.clear().then(() => sendResponse({ ok: true }));
return true;
});
-
// Web app bridge (Chrome, Edge, Opera, Brave). The manifest lists
// https://gradesim.uplb.tools under externally_connectable, so only that site
// can send these. GRADESIM_PING says whether grades exist; GRADESIM_GET_GRADES
diff --git a/extension/src/content.js b/extension/src/content.js
index 0b7f1e6..d23bb89 100644
--- a/extension/src/content.js
+++ b/extension/src/content.js
@@ -1,160 +1,92 @@
// extension/src/content.js
-// Content script for Elbi GradeSim
-// Runs on amis.uplb.edu.ph pages
-// Privacy: Only captures grades data that the user already has access to.
-// All data is stored locally on the user's device and never sent to external servers.
+// Content script for Elbi GradeSim. Runs on amis.uplb.edu.ph.
+// It calls the AMIS grades API once with the student's own login token, keeps
+// only the course fields the extension reads, and saves them in
+// chrome.storage.local. Nothing leaves the browser.
+//
+// Storage keys written here:
+// gradesData { student_grades: { : { term, values: [course] } } }
+// fetchedAt ms timestamp of the last successful fetch
+// lastError 'not-logged-in', 'amis-error' or null
-(function() {
+(function () {
'use strict';
-
- // Flag to track if we already have data this session
- let hasData = false;
- let isFetching = false;
-
- // Get auth token from localStorage (user's own session)
- function getAuthToken() {
- const token = localStorage.getItem('auth._token.local');
- if (token) {
- return token.startsWith('Bearer ') ? token : `Bearer ${token}`;
- }
- return null;
- }
-
- // Get session ID (user's own session)
- function getSessionId() {
- return localStorage.getItem('x-session-id');
+
+ const API = 'https://api-amis.uplb.edu.ph/api/students/grades?summarize=true';
+ const MAX_AGE_MS = 60 * 60 * 1000;
+ let inFlight = null;
+
+ // AMIS (Nuxt auth) keeps the token here and writes the string "false" on logout.
+ function authToken() {
+ const t = localStorage.getItem('auth._token.local');
+ if (!t || t === 'false') return null;
+ return t.startsWith('Bearer ') ? t : `Bearer ${t}`;
}
-
- // Intercept fetch requests to capture grades data from AMIS
- // This only captures responses that the page already receives
- const originalFetch = window.fetch;
- window.fetch = async function(...args) {
- const response = await originalFetch.apply(this, args);
-
- // Skip if we're the ones fetching
- if (isFetching) {
- return response;
- }
-
- // Check if this is the grades API call (fetch accepts a string or a Request)
- const url = (args[0] instanceof Request ? args[0].url : args[0]?.toString()) || '';
- if (url.includes('api-amis.uplb.edu.ph/api/students/grades')) {
- try {
- const clonedResponse = response.clone();
- const data = await clonedResponse.json();
-
- // Only send if we got valid data (not an error response)
- if (data && data.student_grades) {
- hasData = true;
- chrome.runtime.sendMessage({
- type: 'GRADES_DATA',
- data: data
- });
- }
- } catch (e) {
- // Silently ignore parsing errors
- }
- }
-
- return response;
- };
-
- // Also intercept XMLHttpRequest for compatibility
- const originalXHROpen = XMLHttpRequest.prototype.open;
- const originalXHRSend = XMLHttpRequest.prototype.send;
-
- XMLHttpRequest.prototype.open = function(method, url, ...rest) {
- this._url = url;
- return originalXHROpen.apply(this, [method, url, ...rest]);
- };
-
- XMLHttpRequest.prototype.send = function(...args) {
- this.addEventListener('load', function() {
- if (this._url && this._url.includes('api-amis.uplb.edu.ph/api/students/grades')) {
- try {
- const data = JSON.parse(this.responseText);
- if (data && data.student_grades) {
- hasData = true;
- chrome.runtime.sendMessage({
- type: 'GRADES_DATA',
- data: data
- });
- }
- } catch (e) {
- // Silently ignore parsing errors
- }
- }
- });
- return originalXHRSend.apply(this, args);
- };
-
- // Function to fetch grades (only if we don't have data yet)
- async function fetchGradesData() {
- if (hasData || isFetching) {
- return;
- }
-
- const token = getAuthToken();
- const sessionId = getSessionId();
-
- if (!token) {
- return; // User not logged in
- }
-
- isFetching = true;
-
- const headers = {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- 'Authorization': token,
- };
-
- if (sessionId) {
- headers['X-Session-Id'] = sessionId;
+
+ // Keep the AMIS shape (popup.js, planner.js and the web app read it) but drop
+ // every field they do not use.
+ function slim(data) {
+ const out = {};
+ for (const [termId, t] of Object.entries(data.student_grades)) {
+ if (!t || !Array.isArray(t.values)) continue;
+ out[termId] = {
+ term: typeof t.term === 'string' ? t.term : undefined,
+ values: t.values.map(v => ({
+ id: v.id,
+ grade: v.grade,
+ unit_taken: v.unit_taken,
+ section: v.section,
+ status: v.status,
+ course: { course_code: v.course?.course_code, title: v.course?.title },
+ grade_term: { term: v.grade_term?.term, ay: v.grade_term?.ay },
+ })),
+ };
}
-
+ return { student_grades: out };
+ }
+
+ async function fetchGrades() {
+ const token = authToken();
+ if (!token) return { lastError: 'not-logged-in' };
+ const headers = { Accept: 'application/json', Authorization: token };
+ const sessionId = localStorage.getItem('x-session-id');
+ if (sessionId) headers['X-Session-Id'] = sessionId;
try {
- const response = await originalFetch('https://api-amis.uplb.edu.ph/api/students/grades?summarize=true', {
- method: 'GET',
- credentials: 'include',
- headers: headers
- });
-
- isFetching = false;
-
- if (!response.ok) {
- return;
- }
-
- const data = await response.json();
-
- if (data && data.student_grades) {
- hasData = true;
- chrome.runtime.sendMessage({
- type: 'GRADES_DATA',
- data: data
- });
- }
+ const res = await fetch(API, { credentials: 'include', headers });
+ if (res.status === 401 || res.status === 403) return { lastError: 'not-logged-in' };
+ if (!res.ok) return { lastError: 'amis-error' };
+ const data = await res.json();
+ if (!data || !data.student_grades || typeof data.student_grades !== 'object') return { lastError: 'amis-error' };
+ return { gradesData: slim(data), fetchedAt: Date.now(), lastError: null };
} catch (e) {
- isFetching = false;
+ return { lastError: 'amis-error' };
}
}
-
- // Listen for messages from popup
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
- if (message.type === 'FETCH_GRADES') {
- if (message.force) {
- hasData = false;
- isFetching = false;
- }
- fetchGradesData();
+
+ // Resolves to { ok, lastError, fetchedAt, skipped? }.
+ async function refresh(force) {
+ if (!force) {
+ const { fetchedAt } = await chrome.storage.local.get('fetchedAt');
+ if (fetchedAt && Date.now() - fetchedAt < MAX_AGE_MS) return { ok: true, skipped: true, lastError: null, fetchedAt };
+ }
+ if (!inFlight) {
+ inFlight = fetchGrades()
+ .then(async result => {
+ await chrome.storage.local.set(result);
+ return { ok: !result.lastError, lastError: result.lastError, fetchedAt: result.fetchedAt };
+ })
+ .finally(() => { inFlight = null; });
}
+ return inFlight;
+ }
+
+ // The popup's Refresh button sends { type: 'FETCH_GRADES', force: true }.
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (!message || message.type !== 'FETCH_GRADES') return false;
+ refresh(!!message.force).then(sendResponse);
return true;
});
- // Auto-fetch once on page load (with delay to ensure page is ready)
- if (window.location.hostname.includes('amis.uplb.edu.ph')) {
- setTimeout(fetchGradesData, 2000);
- }
+ refresh(false);
})();
diff --git a/extension/src/popup.html b/extension/src/popup.html
index 755dbc7..d926c7c 100644
--- a/extension/src/popup.html
+++ b/extension/src/popup.html
@@ -35,6 +35,7 @@
Refresh
+
@@ -289,6 +290,14 @@
How to use Elbi GradeSim
Use Export JSON to save your grades and settings to a file. Use Import JSON to restore them later or on another device.
+
+
DATA
+
+
Clear my data
+
Delete your grades and every setting GradeSim saved in this browser. Export JSON first if you want a copy.
+
Clear my data
+
+