// ==UserScript==
// @name RDS3 Assistant - Faculty Initial + Time
// @namespace https://northsouth.app/
// @version 2.1.1
// @description Show time/faculty, enhance search, remove courses by prefix, Update data to rds2.northsouth.app
// @author Nihal, Rayed & Walid - NSU CTRL ALT DELETE (NSU - CSE231)
// @match https://rds3.northsouth.edu/*
// @match https://rds3.northsouth.edu/students/*
// @match https://rds3.northsouth.edu/students/advising*
// @match https://rds3.northsouth.edu/index.php/students/advising
// @icon https://rds3.northsouth.edu/assets/img/favicon.png
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @license MIT
// @connect rds2csv.northsouth.app
// ==/UserScript==
(function () {
'use strict';
/******** PART 1: AUTO PUSH CSV FUNCTIONALITY ********/
/******** CONFIG ********/
const API_BASE = GM_getValue('API_BASE', 'https://rds2csv.northsouth.app/data'); // -> POST {API_BASE}/csv
const RUN_KEY = GM_getValue('RUN_KEY', ''); // set only if your server checks X-Run-Key
const FILENAME = 'course_data.csv';
const COURSE_CELL_SELECTOR = 'td[onclick*="addNewCourses"]';
// Observer & fallback timing
const waitTimeoutMs = 5000; // wait up to 5s for cells to appear
const pollFallbackMs = 800; // also try once after 800ms even if no cells (site sometimes builds time map first)
const alreadyRanKey = `rds3_auto_push_once_${Date.now()}`;
// (No per-URL run-lock anymore; we want this to try each load)
console.log('[RDS3 AutoPush] Script loaded');
/******** ROOM MAP (as provided) ********/
const roomArr = [];
roomArr[0]="TBA";roomArr[1]="NAC601";roomArr[2]="NAC602";roomArr[3]="NAC603";roomArr[4]="NAC604";roomArr[5]="NAC605";
roomArr[6]="NAC501";roomArr[7]="NAC502";roomArr[8]="NAC503";roomArr[9]="NAC504";roomArr[10]="NAC505";roomArr[11]="NAC506";
roomArr[12]="NAC507";roomArr[13]="NAC508";roomArr[14]="NAC509";roomArr[15]="NAC510";roomArr[16]="NAC409";roomArr[17]="NAC410";
roomArr[18]="NAC411";roomArr[19]="NAC414";roomArr[20]="NAC309";roomArr[21]="NAC310";roomArr[22]="NAC206";roomArr[23]="NAC207";
roomArr[24]="NAC208";roomArr[25]="NAC209";roomArr[26]="NAC210";roomArr[27]="SAC401";roomArr[28]="SAC402";roomArr[29]="SAC403";
roomArr[30]="SAC404";roomArr[31]="SAC405";roomArr[32]="SAC406";roomArr[33]="SAC407";roomArr[34]="NAC401";roomArr[35]="NAC402";
roomArr[36]="NAC403";roomArr[37]="NAC404";roomArr[38]="NAC405";roomArr[39]="NAC406";roomArr[40]="NAC407";roomArr[41]="NAC408";
roomArr[42]="NAC412";roomArr[43]="NAC413";roomArr[44]="SAC304";roomArr[45]="SAC305";roomArr[46]="SAC306";roomArr[47]="SAC307";
roomArr[48]="SAC308";roomArr[49]="SAC309";roomArr[50]="SAC310";roomArr[51]="SAC311";roomArr[52]="SAC312";roomArr[53]="SAC313";
roomArr[54]="SAC201";roomArr[55]="SAC207";roomArr[56]="SAC208";roomArr[57]="NAC301";roomArr[58]="NAC302";roomArr[59]="NAC303";
roomArr[60]="NAC304";roomArr[61]="NAC305";roomArr[62]="NAC306";roomArr[63]="NAC307";roomArr[64]="NAC308";roomArr[65]="SAC203";
roomArr[66]="SAC209";roomArr[67]="SAC210";roomArr[68]="SAC211";roomArr[69]="NAC201";roomArr[70]="NAC202";roomArr[71]="NAC203";
roomArr[72]="NAC204";roomArr[73]="NAC205";roomArr[81]="LAB1";roomArr[82]="LAB2";roomArr[84]="LAB4";roomArr[85]="OAT1001";
roomArr[86]="OAT1002";roomArr[87]="OAT1003";roomArr[89]="OAT901";roomArr[90]="OAT902";roomArr[91]="OAT903";roomArr[93]="OAT803";
roomArr[94]="LIB901";roomArr[95]="LIB902";roomArr[96]="LIB903";roomArr[97]="LIB904";roomArr[98]="LIB905";roomArr[100]="LIB907";
roomArr[101]="LIB908";roomArr[103]="SAC802";roomArr[121]="NAC514";roomArr[122]="NAC415";roomArr[123]="NAC311";roomArr[124]="SAC202";
roomArr[128]="NAC213";roomArr[129]="NAC214";roomArr[130]="NAC215";roomArr[131]="NAC216";roomArr[132]="SAC204";roomArr[133]="NAC517";
roomArr[135]="OAT601";roomArr[136]="OAT602";roomArr[144]="NAC990";roomArr[145]="NAC991";roomArr[146]="NAC992";roomArr[147]="NAC993";
roomArr[148]="LIB906";roomArr[150]="SAC501";roomArr[151]="SAC502";roomArr[152]="SAC504";roomArr[153]="SAC508";roomArr[154]="SAC206";
roomArr[155]="SAC205";roomArr[156]="NAC620";roomArr[158]="SAC510";roomArr[159]="SAC512";roomArr[160]="SAC514";roomArr[162]="SAC511";
roomArr[163]="SAC513";roomArr[164]="LIB602";roomArr[165]="LIB603";roomArr[166]="LIB601";roomArr[174]="NAC621";roomArr[175]="NAC619";
roomArr[176]="NAC619A";roomArr[177]="NAC211";roomArr[178]="LIB604";roomArr[179]="LIB605";roomArr[180]="LIB606";roomArr[181]="LIB607";
roomArr[182]="LIB608";roomArr[183]="SAC506";roomArr[184]="SAC507";roomArr[185]="SAC1018";roomArr[186]="NAC511";roomArr[187]="LIB609";
roomArr[188]="LIB610";roomArr[189]="LIB611";roomArr[190]="SAC509";roomArr[192]="SAC503";roomArr[193]="NAC512";roomArr[194]="NAC513";
roomArr[195]="NAC313";roomArr[196]="NAC314";roomArr[197]="NAC315";roomArr[198]="SAC314";roomArr[199]="SAC315";roomArr[200]="SAC316";
roomArr[203]="B113";roomArr[204]="B115";roomArr[205]="B117";roomArr[206]="B118";roomArr[207]="B310A";roomArr[209]="SAC724";
roomArr[210]="SAC726";roomArr[211]="SAC409";roomArr[212]="SAC412";roomArr[213]="SAC413";roomArr[214]="SAC414";roomArr[215]="SAC415";
roomArr[221]="OAT803_V";roomArr[224]="LIB903_V";roomArr[225]="LIB906_V";roomArr[230]="LIB901_V";roomArr[232]="LIB905_V";roomArr[234]="OAT902_V";
roomArr[235]="SAC411";roomArr[236]="SAC411_V";roomArr[244]="NAC617";roomArr[245]="NAC618";roomArr[246]="NAC1077";roomArr[247]="NAC1078";
roomArr[248]="NAC1079";roomArr[249]="NAC1080";roomArr[252]="SAC726_V";roomArr[254]="SAC412_V";roomArr[255]="SAC414_V";roomArr[256]="SAC413_V";
roomArr[260]="SAC409_V";roomArr[266]="NAC505";roomArr[275]="SAC726_V1";roomArr[280]="LIB910";roomArr[281]="LIB913";roomArr[284]="LIB913_V";
roomArr[286]="LIB1002";roomArr[287]="LIB1002_V";roomArr[288]="SAC801A";roomArr[290]="OAT1002_V";roomArr[292]="LIB902_V";roomArr[293]="LIB904_V";
roomArr[294]="";roomArr[295]="SAC415_v1";roomArr[305]="OAT803_V1";roomArr[306]="TBA_v2";roomArr[307]="SAC801";roomArr[308]="OAT903_V1";
roomArr[309]="LIB904_V1";roomArr[320]="LIB901_v1";roomArr[321]="SAC415B";roomArr[326]="SAC415B_V";roomArr[332]="Upper Plaza";roomArr[341]="NAC515";
roomArr[343]="OAT803_V2";roomArr[345]="TV LAB";roomArr[351]="NAC201-v1";roomArr[352]="LIB902_V1";roomArr[358]="TV STUDIO";roomArr[360]="NAC512A";
roomArr[361]="NTR304";roomArr[362]="NAC514";roomArr[363]="NTR301";
/******** TIME RESOLUTION HELPERS ********/
function getGlobalTimeMap() {
const names = ['timeArray','TimeArray','TIME_ARRAY','timeArr','times','courseTimeArray'];
for (const n of names) {
try { if (n in unsafeWindow) { const v = unsafeWindow[n]; if (v && (Array.isArray(v)||typeof v==='object')) return v; } } catch {}
try { if (n in window) { const v = window[n]; if (v && (Array.isArray(v)||typeof v==='object')) return v; } } catch {}
}
return null;
}
function buildTimeMapFromScripts() {
const map = {};
const scripts = Array.from(document.scripts);
const assignRe = /(?:^|[\s;])([A-Za-z_$][\w$]*)\s*\[\s*(\d+)\s*\]\s*=\s*["']([^"']+)["']/g;
for (const s of scripts) {
const txt = s.textContent || '';
let m;
while ((m = assignRe.exec(txt)) !== null) {
const idx = parseInt(m[2], 10);
const val = m[3].trim();
if (!Number.isNaN(idx) && val) map[idx] = val;
}
}
// Also try literal array/object assignments
if (Object.keys(map).length === 0) {
const all = scripts.map(s => s.textContent || '').join('\n');
const literalRe = /([A-Za-z_$][\w$]*)\s*=\s*(\{[\s\S]*?\}|\[[\s\S]*?\]);/g;
let m;
while ((m = literalRe.exec(all)) !== null) {
try {
const obj = Function(`"use strict";return (${m[2]});`)();
if (obj && typeof obj === 'object') {
if (Array.isArray(obj)) obj.forEach((v,i)=>{ if (typeof v==='string') map[i]=v; });
else for (const [k,v] of Object.entries(obj)) {
const idx = parseInt(k,10);
if (!Number.isNaN(idx) && typeof v==='string') map[idx]=v;
}
}
} catch {}
}
}
return map;
}
function resolveCourseTime(timeId) {
const id = Number.parseInt(timeId, 10);
if (Number.isNaN(id)) return 'Unknown';
const g = getGlobalTimeMap();
if (g) {
const v = Array.isArray(g) ? g[id] : g[id] || g[String(id)];
if (typeof v === 'string' && v.trim()) return v.trim();
}
if (!window.__rds_time_map__) window.__rds_time_map__ = buildTimeMapFromScripts();
const m = window.__rds_time_map__;
if (m && typeof m[id] === 'string' && m[id].trim()) return m[id].trim();
return 'Unknown';
}
/******** CSV + UPLOAD ********/
function csvEscape(val){ if(val==null) return ''; const s=String(val); return /[",\n]/.test(s) ? `"${s.replace(/"/g,'""')}"` : s; }
function convertToCSV(courses){
const ts = new Date().toISOString();
const headers = ['CourseCode','Section','Faculty','CourseTime','TotalSeat','TakenSeat','RoomId','Room'];
const lines = [`# lastUpdated: ${ts}`, headers.join(',')];
for(const c of courses){
lines.push([csvEscape(c.CourseCode),csvEscape(c.Section),csvEscape(c.Faculty),
csvEscape(c.CourseTime),csvEscape(c.TotalSeat),csvEscape(c.TakenSeat),
csvEscape(c.RoomId),csvEscape(c.Room)].join(','));
}
return lines.join('\n')+'\n';
}
function makeMultipartBody(fieldName, filename, content){
const boundary = '----TMFormBoundary' + Math.random().toString(36).slice(2);
const CRLF = '\r\n';
const parts = [];
parts.push(`--${boundary}${CRLF}`);
parts.push(`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"${CRLF}`);
parts.push(`Content-Type: text/csv${CRLF}${CRLF}`);
parts.push(content + CRLF);
parts.push(`--${boundary}--${CRLF}`);
return { body: parts.join(''), boundary };
}
function httpPostMultipart(url, fieldName, filename, content, extraHeaders){
const { body, boundary } = makeMultipartBody(fieldName, filename, content);
const headers = Object.assign({ 'Content-Type': 'multipart/form-data; boundary='+boundary }, extraHeaders||{});
return new Promise((resolve,reject)=>{
GM_xmlhttpRequest({
method:'POST',
url,
data: body,
headers,
onload: res => {
console.log('[RDS3 AutoPush] POST status:', res.status);
if (res.status>=200 && res.status<300) resolve(res);
else reject(new Error(`HTTP ${res.status}: ${res.responseText||res.statusText}`));
},
onerror: e => reject(new Error(`Network error`))
});
});
}
/******** SCRAPER ********/
function extractCourseData(){
const cells = document.querySelectorAll(COURSE_CELL_SELECTOR);
console.log('[RDS3 AutoPush] Found cells:', cells.length);
const out = []; const seen = new Set();
cells.forEach(cell=>{
const onclickAttr = cell.getAttribute('onclick'); if(!onclickAttr) return;
let params = [];
try{
const fn = new Function(`
let params = [];
const mockFunction = function(){ params = Array.from(arguments); };
const addNewCourses = mockFunction;
${onclickAttr};
return params;
`);
params = fn();
}catch(e){
console.warn('[RDS3 AutoPush] Param capture failed:', e);
return;
}
if(!params || params.length<10) return;
const courseCode = params[0];
const section = params[4];
const roomId = parseInt(params[5]);
const timeId = parseInt(params[6]);
const faculty = params[7];
const totalSeat = params[8];
const takenSeat = params[9];
const key = `${courseCode}-${section}-${timeId}`;
if(seen.has(key)) return; seen.add(key);
const courseTime = resolveCourseTime(timeId);
out.push({
CourseCode: courseCode,
Section: section,
Faculty: faculty,
CourseTime: courseTime,
TotalSeat: totalSeat,
TakenSeat: takenSeat,
RoomId: roomId,
Room: roomArr[roomId] || 'Unknown'
});
});
console.log('[RDS3 AutoPush] Extracted courses:', out.length);
return out;
}
async function runUpload(alwaysPostEvenIfEmpty = false){
try{
const courses = extractCourseData();
const csv = convertToCSV(courses);
const headers = RUN_KEY ? {'X-Run-Key': RUN_KEY} : {};
if (courses.length == 0 ) {
console.log('[RDS3 AutoPush] No courses; skipping upload');
return;
}
await httpPostMultipart(`${API_BASE}/csv`, 'file', FILENAME, csv, headers);
console.log('[RDS3 AutoPush] Upload complete');
}catch(e){
console.warn('[RDS3 AutoPush] Upload failed:', e.message || e);
}
}
/******** ORCHESTRATION ********/
function startObserver() {
let ran = false;
const stop = () => { try { obs.disconnect(); } catch {} };
const obs = new MutationObserver(() => {
const cells = document.querySelectorAll(COURSE_CELL_SELECTOR);
if (!ran && cells.length > 0) {
ran = true;
stop();
console.log('[RDS3 AutoPush] Cells detected by observer; uploading…');
runUpload(true);
}
});
obs.observe(document.documentElement || document.body, { childList: true, subtree: true });
// Fallback: try once after a short delay even if cells not found (some pages still have time map ready)
setTimeout(() => {
if (!ran) {
console.log('[RDS3 AutoPush] Fallback attempt after', pollFallbackMs, 'ms');
runUpload(true);
}
}, pollFallbackMs);
// Hard timeout: stop observing after waitTimeoutMs
setTimeout(() => {
if (!ran) {
console.log('[RDS3 AutoPush] Timeout reached; stopping observer (no cells detected).');
}
stop();
}, waitTimeoutMs);
}
/******** PART 2: FACULTY INITIAL + TIME DISPLAY ********/
// Inject custom styles
const style = document.createElement('style');
style.innerHTML = `
.courselist td:nth-child(3) {
width: 180px !important;
white-space: nowrap;
overflow: visible;
color: blue;
font-weight: bold;
}
#mainBody {
text-align: center !important;
width: 1200px !important;
height: 900px !important;
}
#noticebar {
width: 1190px !important;
}
#offeredCourses.body, #offeredCourses {
width: 495px !important;
height: 750px !important;
}
#coursesbox {
width: 190% !important;
height: 776px !important;
}
.right {
width: 270px !important;
}
#topbar {
width: 1190px !important;
}
#searchDiv {
width: 350px !important;
}
.time-text {
color: black !important;
display: inline-block !important;
vertical-align: middle !important;
line-height: 1.4 !important;
}
#coursesbox {
width: 505px !important;
}
#legendbox {
position: relative !important;
left: 130px !important;
height: 115px !important;
}
img[src*="legend.png"] {
padding: 10px !important;
border: 2px solid black !important;
}
.courselist td {
line-height: 23px !important;
border-bottom: 2px solid #000 !important;
font-size: 14px !important;
cursor: pointer !important;
font-weight: bold !important;
padding: 0px !important;
vertical-align: middle !important;
width: 100% !important;
}
.abouticon.smallicon {
display: none !important;
}
`;
document.head.appendChild(style);
function updateTimeText() {
const icons = document.querySelectorAll('.abouticon');
icons.forEach(icon => {
const onclickStr = icon.getAttribute('onclick');
if (!onclickStr) return;
const paramsMatch = onclickStr.match(/displayToolTip\((.*)\)/);
if (!paramsMatch) return;
const paramsRaw = paramsMatch[1];
const params = paramsRaw.split(',').map(p => p.trim().replace(/^['"]|['"]$/g, ''));
const timeIndex = params[3];
if (!timeIndex) return;
// Use the same time resolution logic as the CSV extractor
const courseTime = resolveCourseTime(timeIndex);
if (courseTime === 'Unknown') return;
let timeTextSpan = icon.parentElement.querySelector('.time-text');
if (!timeTextSpan) {
timeTextSpan = document.createElement('span');
timeTextSpan.className = 'time-text';
icon.parentElement.appendChild(timeTextSpan);
}
timeTextSpan.textContent = ' ' + courseTime;
});
}
function updateCourseCodes() {
const courseCells = document.querySelectorAll('table.courselist td:first-child');
courseCells.forEach(cell => {
const onclickStr = cell.getAttribute('onclick');
if (!onclickStr) return;
const params = onclickStr.match(/addNewCourses\(([^)]+)\)/);
if (!params) return;
const parts = params[1].split(',').map(p => p.trim().replace(/^['"]|['"]$/g, ''));
const courseId = parts[0];
const section = parts[4];
const roomCode = parts[7];
if (courseId && section && roomCode) {
cell.textContent = `${courseId}.${section}.${roomCode}`;
}
});
}
function setupSearchFilter() {
const searchInput = document.getElementById('searchText');
if (!searchInput) return;
searchInput.addEventListener('input', function () {
const filter = this.value.toLowerCase().trim();
const searchParts = filter.split(/\s+/);
const rows = document.querySelectorAll('#courseList tr');
rows.forEach(row => {
const courseCell = row.querySelector('td:first-child');
const timeSpan = row.querySelector('.time-text');
if (!courseCell) return;
const courseText = courseCell.textContent.toLowerCase();
const timeText = timeSpan ? timeSpan.textContent.toLowerCase() : '';
const combinedText = `${courseText} ${timeText}`;
const allMatch = searchParts.every(part => combinedText.includes(part));
row.style.display = allMatch ? '' : 'none';
});
});
}
/******** COMBINED INITIALIZATION ********/
function initializeCombinedScript() {
// Part 1: Start auto-push observer
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver, { once: true });
} else {
startObserver();
}
// Part 2: Initialize UI enhancements
updateTimeText();
updateCourseCodes();
setupSearchFilter();
}
// Start the combined script
initializeCombinedScript();
})();