Shows CULPA professor ratings directly on Columbia's Vergil course registration page. Available as a Chrome extension and a Safari extension.
Chrome — load unpacked from the chrome/ folder (see instructions below)
Safari — requires Xcode (free, ~7GB from the Mac App Store):
git clone https://github.com/HoldenB2007/ProfRater.git
cd ProfRater
xcrun safari-web-extension-converter safari/ --app-name "CULPA on Vergil" --bundle-identifier com.holdenb.culpavergil --macos-only --no-prompt
open "CULPA on Vergil/CULPA on Vergil.xcodeproj"Then in Xcode: set both targets' Signing Certificate to Sign to Run Locally → press ⌘R. In Safari: Develop → Developer Settings → Allow Unsigned Extensions, then enable in Safari → Settings → Extensions.
Note: "Allow Unsigned Extensions" must be re-enabled each time Safari relaunches.
When you browse courses on Vergil (vergil.columbia.edu), the extension automatically:
- Detects instructor names in course section rows and the instructor autocomplete dropdown
- Looks them up on the live culpa.info API
- Injects a compact badge next to each instructor showing:
- Average rating (e.g.
3.1) - Nugget status: 🥉 Bronze (3.0+) / 🥈 Silver (3.5+) / 🥇 Gold (4.0+)
- Review count
- Hover tooltip with rating, nugget, review count, and "Click box to view on CULPA"
- Click to open the professor's full CULPA page
- Average rating (e.g.
https://github.com/HoldenB2007/ProfRater.git
- Clone or download this repo
- Open Chrome →
chrome://extensions - Enable Developer mode (top right)
- Click Load unpacked → select the
chrome/folder - Navigate to Vergil — badges appear automatically
Safari Web Extensions must be wrapped in a native app using Xcode.
- Clone this repo
- Run the converter (requires Xcode command-line tools):
xcrun safari-web-extension-converter safari/ \ --app-name "CULPA on Vergil" \ --bundle-identifier com.holdenb.culpavergil \ --macos-only - Open the generated Xcode project → Product → Run (⌘R)
- In Safari: Develop → Allow Unsigned Extensions (one-time)
- Open Safari → Settings → Extensions → enable CULPA on Vergil
- Navigate to Vergil — badges appear automatically
If the Develop menu isn't visible: Safari → Settings → Advanced → Show features for web developers
culpa-vergil-extension/
├── chrome/ ← Load this folder as the Chrome extension
│ ├── manifest.json MV3 config, permissions, host rules
│ ├── background.js Service worker: CULPA API lookups + 24h cache
│ ├── content.js Injected into Vergil: finds instructors, injects badges
│ ├── content.css Badge + tooltip styles
│ ├── popup.html Extension popup: status, legend, cache clear
│ ├── popup.js Popup script (status check + cache clear handler)
│ └── icons/ 16/48/128/256px icons (transparent bg, from logo.png)
├── safari/ ← Web extension source for Safari (same structure as chrome/)
│ ├── manifest.json
│ ├── background.js
│ ├── content.js
│ ├── content.css
│ ├── popup.html
│ ├── popup.js
│ └── icons/
├── logo.png Project logo (source for icons)
└── .gitignore
Runs on Vergil pages. Uses a MutationObserver to handle Vergil's Angular SPA navigation.
Two scanners run on every DOM change (debounced 600ms):
| Scanner | Selector | Name format |
|---|---|---|
| Course section rows | span.instructor > div.text |
Last, First (uni) |
| Instructor autocomplete dropdown | mat-option containing a UNI pattern (abc123) |
First Last (uni) |
parseVergilName() handles both formats: strips the UNI, flips comma-separated names.
A single shared tooltip div is appended to document.body (not inside the badge) to avoid inheriting opacity from Vergil's table row transitions.
Receives CULPA_LOOKUP messages from the content script.
Lookup flow (2 API calls in sequence):
GET /api/professor/search?queryString={First Last}&maxResults=20- Returns array of
{ professor_header: { professor_id, first_name, last_name, nugget } } - Nugget values:
0=None,1=Bronze,2=Silver,3=Gold - Matches by exact name → last name only → first result
- Returns array of
GET /api/professor_page/card/{professor_id}- Returns
professor_summary.avg_ratingandprofessor_summary.num_reviews
- Returns
Results cached in chrome.storage.local for 24 hours. Clear from the extension popup.
Base: https://culpa.info
| Endpoint | Description |
|---|---|
GET /api/professor/search?queryString={name}&maxResults=20 |
Search professors by name |
GET /api/professor_page/card/{id} |
Professor details: avg_rating, num_reviews, ai_overview |
GET /api/review/professor/{id}?page=1 |
Paginated reviews, returns number_of_reviews |
GET /api/front_page |
Homepage data |
GET /api/departments/all |
All departments |
The API was discovered by downloading culpa.info/static/js/main.*.js and grepping for api/ strings. The search parameter name is queryString (not q or query).
CORS note: culpa.info/api/* has Access-Control-Allow-Origin: http://localhost:3000. Chrome and Safari extension service workers with host_permissions for https://culpa.info/* bypass this restriction. Do NOT add Content-Type: application/json to GET requests — it triggers a preflight that fails.
Vergil is an Angular Material SPA at vergil.columbia.edu.
- Course section rows:
<span class="instructor"><a><div class="text">Last, First (uni)</div></a></span> - Badges are inserted with
span.instructor.insertAdjacentElement("afterend", badge)— not inside the<a>tag (invalid HTML) - Instructor autocomplete:
<mat-option>First Last (uni)</mat-option>— badges appended inside the option - Vergil applies opacity transitions to table rows; the tooltip must live on
document.bodyto avoid inheriting opacity
If badges stop appearing:
- Open Vergil → right-click an instructor name → Inspect
- Find the element containing
Last, First (uni)text - Update
findInstructorElements()selector incontent.js - Check
processElement()if the child text element path changed
| What | Where |
|---|---|
| Badge appearance | content.css — .culpa-badge, .culpa-gold/silver/bronze |
| Tooltip size/style | content.css — .culpa-tooltip and .culpa-tip-* |
| Cache duration | background.js — CACHE_TTL (default 24h) |
| Scan debounce | content.js — DEBOUNCE_MS (default 600ms) |
| Popup content | popup.html + popup.js |
- Only communicates with
culpa.info - Results cached locally via
chrome.storage.local - No analytics or tracking
- Only runs on
vergil.columbia.edupages
Full privacy policy: https://holdenb2007.github.io/ProfRater/privacy.html
Not affiliated with Columbia University, CULPA, or the Columbia Spectator.