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 -