Canvas User Joined Date

Fetch each user's profile page on Canvas roster, extract the "Created" date, and add a new "Joined On" column reliably.

// ==UserScript==
// @name         Canvas User Joined Date
// @namespace    hacker09
// @version      1
// @description  Fetch each user's profile page on Canvas roster, extract the "Created" date, and add a new "Joined On" column reliably.
// @match        https://canvas.instructure.com/courses/*/users
// @icon         https://du11hjcvx0uqb.cloudfront.net/br/dist/images/favicon-e10d657a73.ico
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict'

  let enrollmentData = [] //global array for debugging all td innerHTML
  let pending = 0 //counter for pending fetch requests

  function extractCreatedDate(tdInnerHTML) { //helper: extract Created date from td innerHTML
    let text = tdInnerHTML.replace(/<br\s*\/?>/gi, ' ') //replace <br> with space
    .replace(/<\/?[^>]+(>|$)/g, '') //remove all other HTML tags
    .replace(/\s+/g, ' ') //normalize multiple spaces to single space
    .trim(); //remove leading/trailing whitespace
    let match = text.match(/Created\s+(.+?)(?:\s+Completed|$)/i) //match "Created" followed by date until "Completed" or end
    if(!match) { //if first pattern fails
      match = text.match(/Created\s+(.+)/i) //fallback: grab everything after "Created"
    }
    return match ? match[1].trim() : 'N/A'; //return extracted date or N/A if not found
  }

  function processTable(){ //main function to process the roster table
    let header=document.querySelector("table.roster thead tr") //find table header row
    if(header && !document.querySelector("th.joined-on")) { //if header exists and joined column not added yet
      let th=document.createElement("th") //create new header cell
      th.className="joined-on" //add class for identification
      th.textContent="Joined On" //set header text
      th.scope="col" //add accessibility attribute
      let lastTh = header.querySelector('th:last-child') //find last header (admin links)
      header.insertBefore(th, lastTh) //insert before admin links column
    }

    let rows=document.querySelectorAll("tbody.collectionViewItems tr:not(.joined-processed)") //find unprocessed rows
    rows.forEach(row=>{ //iterate through each row
      row.classList.add("joined-processed") //mark row as processed
      let link=row.querySelector('a[href^="https://canvas.instructure.com/courses/"][href*="/users/"]') //find user profile link
      if(!link) return //skip if no link found

      let td=document.createElement("td") //create new data cell
      td.textContent="Loading..." //set loading text
      let lastTd = row.querySelector('td:last-child') //find last cell (admin links)
      row.insertBefore(td, lastTd) //insert before admin links cell

      pending++ //increment pending counter
      fetch(link.href).then(r=>r.text()).then(html=>{ //fetch user profile page
        let parser=new DOMParser() //create HTML parser
        let doc=parser.parseFromString(html,"text/html") //parse fetched HTML
        let cell=doc.querySelector('td[style*="width: 50%"]') //find enrollment info cell
        if(cell){ //if cell found
          enrollmentData.push(cell.innerHTML.trim()) //store full HTML for debugging
          td.textContent = extractCreatedDate(cell.innerHTML) //extract and display date
        } else { //if cell not found
          enrollmentData.push("NO TD FOUND") //log error for debugging
          td.textContent = "N/A" //display N/A
        }
      }).catch(()=>{ //handle fetch errors
        enrollmentData.push("FETCH ERROR") //log error for debugging
        td.textContent = "Error" //display error
      }).finally(()=>{ //always execute after fetch
        pending-- //decrement pending counter
        if(pending===0){ //if all requests completed
          console.log("All fetched td innerHTML:", enrollmentData) //log all data for debugging
        }
      })
    })
  }

  window.addEventListener('load', ()=>{ //wait for window load event
    setTimeout(processTable, 500) //delay to ensure dynamic content loaded
  })

  let observer=new MutationObserver(()=>{ //create observer for DOM changes
    clearTimeout(window.canvasJoinedDateTimeout) //clear previous timeout
    window.canvasJoinedDateTimeout = setTimeout(processTable, 100) //debounce with 100ms delay
  })

  if(document.readyState === 'loading') { //if document still loading
    document.addEventListener('DOMContentLoaded', ()=>{ //wait for DOM ready
      observer.observe(document.body,{childList:true,subtree:true}) //start observing body for changes
    })
  } else { //if document already loaded
    observer.observe(document.body,{childList:true,subtree:true}) //start observing immediately
  }
})()