From ae39f525630847c9ab702e45f1b3e3111a9cc927 Mon Sep 17 00:00:00 2001 From: haopengzhan Date: Sat, 29 Aug 2026 22:39:58 -0700 Subject: [PATCH] feat: implement robust student ID discovery and multi-endpoint data extraction for activities --- .gitignore | 2 +- dashboard/dashboard.html | 3 +- dashboard/dashboard.js | 408 +++++++++++++++++++++------------ dashboard/viewer_template.html | 7 + manifest.json | 3 +- scripts/content_script.js | 292 +++++++++++++++++++++-- 6 files changed, 541 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index 6cb7dab..5337ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ Thumbs.db *.tmp # User Export Data & Backups (Prevent leaking child photos/videos/records) -emmy/ +emmy*/ exports/ data/ test_data/ diff --git a/dashboard/dashboard.html b/dashboard/dashboard.html index bb10401..8d20e32 100644 --- a/dashboard/dashboard.html +++ b/dashboard/dashboard.html @@ -63,7 +63,7 @@
Enter Student UUID Manually
- +
@@ -291,6 +291,7 @@

Live Activity Log

+
diff --git a/dashboard/dashboard.js b/dashboard/dashboard.js index 53b580a..d28c05d 100644 --- a/dashboard/dashboard.js +++ b/dashboard/dashboard.js @@ -5,7 +5,7 @@ // Application State const state = { students: [], - selectedStudentId: 'edda433b-2a24-4da7-bbd5-9f131b4b2565', + selectedStudentId: '', selectedStudentData: null, destinationDirectoryHandle: null, isExporting: false, @@ -31,13 +31,13 @@ document.addEventListener('DOMContentLoaded', async () => { initDOMElements(); setupEventListeners(); initDatePresets(); - + if (window.JSZip || typeof JSZip !== 'undefined') { log('Zip compression engine initialized (JSZip v3.10.1).', 'info'); } else { log('Warning: JSZip library not detected on page load.', 'warn'); } - + await checkSessionAndDiscoverStudents(); }); @@ -78,14 +78,15 @@ function initDOMElements() { metricSize: document.getElementById('metricSize'), metricRateLimit: document.getElementById('metricRateLimit'), terminalLog: document.getElementById('terminalLog'), - clearLogBtn: document.getElementById('clearLogBtn') + clearLogBtn: document.getElementById('clearLogBtn'), + inspectFeedBtn: document.getElementById('inspectFeedBtn') }; } function log(message, type = 'info') { const timestamp = new Date().toISOString(); const timeFormatted = new Date().toLocaleTimeString(); - + state.executionLogs.push({ timestamp, time: timeFormatted, @@ -104,65 +105,176 @@ function log(message, type = 'info') { // Session & Student Discovery // ------------------------------------------------------------- +function extractStudentsFromPayload(data) { + if (!data) return []; + const students = []; + const seenIds = new Set(); + + function addStudent(s) { + if (!s) return; + const sId = s.id || s.object_id || s.student_id; + if (sId && !seenIds.has(sId)) { + seenIds.add(sId); + students.push({ + id: sId, + first_name: s.first_name || s.name?.split(' ')?.[0] || 'Child', + last_name: s.last_name || s.name?.split(' ')?.slice(1).join(' ') || '', + picture_url: s.picture_url || s.avatar_url || s.photo_url || s.profile_photo?.image_url || s.profile_photo?.thumbnail_url || null, + school_name: s.school_name || s.school?.name || s.room_name || s.room?.name || 'Brightwheel Account', + room: s.room || null + }); + } + } + + if (Array.isArray(data.students)) data.students.forEach(addStudent); + if (Array.isArray(data.children)) data.children.forEach(addStudent); + if (Array.isArray(data.profiles)) data.profiles.forEach(addStudent); + if (Array.isArray(data.guardians)) { + data.guardians.forEach(g => { + if (Array.isArray(g.students)) g.students.forEach(addStudent); + if (Array.isArray(g.children)) g.children.forEach(addStudent); + }); + } + if (data.student) addStudent(data.student); + if (data.user?.students) data.user.students.forEach(addStudent); + + if (Array.isArray(data.activities) || Array.isArray(data)) { + const items = Array.isArray(data.activities) ? data.activities : data; + for (const item of items) { + if (item.target && (item.target.user_type === 'student' || item.target.object_id)) { + addStudent(item.target); + } + } + } + + return students; +} + async function checkSessionAndDiscoverStudents() { log('Checking active Brightwheel browser session...', 'info'); - - // Check stored active student ID first + const stored = await chrome.storage.local.get(['lastActiveStudentId']); if (stored.lastActiveStudentId) { state.selectedStudentId = stored.lastActiveStudentId; el.manualStudentId.value = stored.lastActiveStudentId; } + // 1. Try communicating with active Brightwheel tab first (most accurate) + try { + const [bwTab] = await chrome.tabs.query({ url: 'https://schools.mybrightwheel.com/*' }); + if (bwTab && bwTab.id) { + const tabRes = await new Promise(resolve => { + chrome.tabs.sendMessage(bwTab.id, { action: 'GET_STUDENTS_AND_SESSION' }, res => { + resolve(chrome.runtime.lastError ? null : res); + }); + }); + + if (tabRes && tabRes.success && tabRes.students && tabRes.students.length > 0) { + el.sessionDot.classList.add('online'); + el.sessionStatusText.textContent = 'Brightwheel Tab Connected'; + state.students = tabRes.students; + if (tabRes.activeStudentId) { + state.selectedStudentId = tabRes.activeStudentId; + } else if (!state.selectedStudentId) { + state.selectedStudentId = tabRes.students[0].id; + } + state.selectedStudentData = state.students.find(s => s.id === state.selectedStudentId) || state.students[0]; + el.manualStudentId.value = state.selectedStudentId; + renderStudentsList(); + log(`Discovered ${state.students.length} student profile(s) from live Brightwheel session.`, 'success'); + return; + } + } + } catch (err) { + // Continue to direct API discovery + } + + // 2. Direct API discovery from extension context try { - // 1. Try to fetch user/guardian profile to list students const userRes = await fetchBrightwheelAPI('https://schools.mybrightwheel.com/api/v1/users/me'); - if (userRes && userRes.success && userRes.data) { el.sessionDot.classList.add('online'); el.sessionStatusText.textContent = 'Brightwheel Session Active'; - log('Successfully connected to Brightwheel user profile.', 'success'); - const userData = userRes.data; - const studentsFound = userData.students || userData.children || (userData.guardians && userData.guardians[0]?.students) || []; - - if (studentsFound.length > 0) { - state.students = studentsFound; + const found = extractStudentsFromPayload(userRes.data); + if (found.length > 0) { + state.students = found; + if (!state.selectedStudentId) state.selectedStudentId = found[0].id; + state.selectedStudentData = found.find(s => s.id === state.selectedStudentId) || found[0]; + el.manualStudentId.value = state.selectedStudentId; renderStudentsList(); + log(`Loaded ${found.length} student profile(s) from user account.`, 'success'); return; } } - // 2. Try fetching specific student profile if selected + const guardianRes = await fetchBrightwheelAPI('https://schools.mybrightwheel.com/api/v1/guardians/me'); + if (guardianRes && guardianRes.success && guardianRes.data) { + el.sessionDot.classList.add('online'); + el.sessionStatusText.textContent = 'Brightwheel Session Active'; + + const found = extractStudentsFromPayload(guardianRes.data); + if (found.length > 0) { + state.students = found; + if (!state.selectedStudentId) state.selectedStudentId = found[0].id; + state.selectedStudentData = found.find(s => s.id === state.selectedStudentId) || found[0]; + el.manualStudentId.value = state.selectedStudentId; + renderStudentsList(); + log(`Loaded ${found.length} student profile(s) from guardian account.`, 'success'); + return; + } + } + + // Probe recent activities to extract student target from live feed + const targetId = state.selectedStudentId; + const actProbe = await fetchBrightwheelAPI(`https://schools.mybrightwheel.com/api/v1/students/${targetId}/activities?page_size=10`); + if (actProbe && actProbe.success && actProbe.data) { + const found = extractStudentsFromPayload(actProbe.data); + if (found.length > 0) { + el.sessionDot.classList.add('online'); + el.sessionStatusText.textContent = 'Brightwheel Session Active'; + state.students = found; + if (!state.selectedStudentId) state.selectedStudentId = found[0].id; + state.selectedStudentData = found.find(s => s.id === state.selectedStudentId) || found[0]; + el.manualStudentId.value = state.selectedStudentId; + renderStudentsList(); + log(`Detected active student: ${state.selectedStudentData.first_name || 'Student'} (${state.selectedStudentId})`, 'success'); + return; + } + } + + // 3. Fallback: If student ID stored or entered manually if (state.selectedStudentId) { const studentRes = await fetchBrightwheelAPI(`https://schools.mybrightwheel.com/api/v1/students/${state.selectedStudentId}`); if (studentRes && studentRes.success && studentRes.data) { el.sessionDot.classList.add('online'); el.sessionStatusText.textContent = 'Brightwheel Session Active'; - const s = studentRes.data.student || studentRes.data; state.students = [s]; state.selectedStudentData = s; renderStudentsList(); - log(`Loaded student profile: ${s.first_name || 'Student'} (${state.selectedStudentId})`, 'success'); + log(`Loaded profile for student: ${s.first_name || 'Student'} (${state.selectedStudentId})`, 'success'); return; } + + el.sessionDot.classList.add('online'); + el.sessionStatusText.textContent = 'Session Connected'; + state.students = [{ + id: state.selectedStudentId, + first_name: 'Student', + last_name: `(${state.selectedStudentId.substring(0, 8)}...)`, + school_name: 'Brightwheel Account' + }]; + renderStudentsList(); + log(`Using student UUID: ${state.selectedStudentId}`, 'info'); + return; } - // Fallback: Default student card with manual ID - el.sessionDot.classList.add('online'); - el.sessionStatusText.textContent = 'Session Connected'; - state.students = [{ - id: state.selectedStudentId, - first_name: 'Student', - last_name: `(${state.selectedStudentId.substring(0, 8)}...)`, - school_name: 'Brightwheel School' - }]; + // No student found yet + el.sessionStatusText.textContent = 'Open Brightwheel in another tab to auto-connect'; renderStudentsList(); - log(`Using student UUID: ${state.selectedStudentId}`, 'info'); - } catch (err) { - log(`Session check note: ${err.message}. Ready with manual ID.`, 'warn'); + log(`Session check note: ${err.message}.`, 'warn'); renderStudentsList(); } } @@ -190,9 +302,9 @@ async function fetchBrightwheelAPI(url) { function renderStudentsList() { el.studentsList.innerHTML = ''; - + if (state.students.length === 0) { - el.studentsList.innerHTML = '
No students auto-detected. Use manual UUID below.
'; + el.studentsList.innerHTML = '
No students auto-detected. Please ensure Brightwheel is open in a tab or enter UUID below.
'; return; } @@ -202,7 +314,7 @@ function renderStudentsList() { const firstName = student.first_name || 'My'; const lastName = student.last_name || 'Child'; const initials = (firstName[0] || 'B') + (lastName[0] || 'W'); - const school = student.school_name || student.room_name || 'Brightwheel Account'; + const school = student.school_name || student.room_name || student.room?.name || 'Brightwheel Account'; const card = document.createElement('div'); card.className = `student-card ${isSelected ? 'selected' : ''}`; @@ -227,6 +339,7 @@ function renderStudentsList() { state.selectedStudentId = sId; state.selectedStudentData = student; el.manualStudentId.value = sId; + chrome.storage.local.set({ lastActiveStudentId: sId }); log(`Selected child: ${firstName} ${lastName} (${sId})`, 'info'); }); @@ -239,11 +352,8 @@ function renderStudentsList() { // ------------------------------------------------------------- function initDatePresets() { - const today = new Date(); - const currentYear = today.getFullYear(); - el.startDate.value = ''; - el.endDate.value = today.toISOString().split('T')[0]; + el.endDate.value = ''; } function setDateRange(rangeType) { @@ -252,7 +362,7 @@ function setDateRange(rangeType) { if (rangeType === 'all') { el.startDate.value = ''; - el.endDate.value = todayStr; + el.endDate.value = ''; } else if (rangeType === 'this-year') { el.startDate.value = `${today.getFullYear()}-01-01`; el.endDate.value = todayStr; @@ -387,45 +497,30 @@ async function handleInspectFeed() { return; } - log(`✅ Live Inspection Complete for: ${res.currentUrl}`, 'success'); + log(`✅ Live Tab Diagnostic Complete: ${res.currentUrl}`, 'success'); log(`Page Title: "${res.pageTitle}"`, 'info'); - log(`DOM Cards Detected on Page: ${res.domCards?.length || 0}`, 'info'); + log(`DOM Cards: ${res.domCards?.length || 0} | React Fiber Activities: ${res.reactFiberCount || 0}`, 'info'); - if (res.domCards && res.domCards.length > 0) { - res.domCards.slice(0, 3).forEach((dc, i) => { - log(` [DOM Card ${i + 1}] Text: "${dc.raw_text.replace(/\s+/g, ' ').substring(0, 100)}..."`, 'info'); - }); + if (res.endpointProbes) { + for (const [name, probe] of Object.entries(res.endpointProbes)) { + if (probe.error) { + log(`[Probe: ${name}] Error: ${probe.error}`, 'error'); + } else { + log(`[Probe: ${name}] Status ${probe.status} | Items: ${probe.itemCount} | Latest Date: ${probe.latestDate || 'none'}`, probe.itemCount > 0 ? 'success' : 'warn'); + } + } } - const acts = res.apiSampleActivities || []; - log(`API Sample Activities Received: ${acts.length}`, 'success'); - - acts.forEach((a, idx) => { - const type = a.activity_type || a.action_type || a.type; - const presentKeys = Object.keys(a).filter(k => a[k] !== null && a[k] !== undefined && a[k] !== ''); - log(`[API Item ${idx + 1}] Type="${type}", Note="${a.note || a.body || ''}", Keys=[${presentKeys.join(', ')}]`, 'info'); - - if (type === 'ac_video' || type === 'video') { - log(` -> Video payload: media=${JSON.stringify(a.media || null)}, video_url=${a.video_url || null}`, 'warn'); - } - if (type === 'ac_nap' || type === 'nap') { - log(` -> Nap payload: action_type=${a.action_type}, nap_start=${a.nap_start || a.start_time}, nap_end=${a.nap_end || a.end_time}, duration=${a.duration || a.duration_minutes}`, 'warn'); - } - if (type === 'ac_checkin' || type === 'checkin') { - log(` -> Checkin payload: action_type=${a.action_type}, signed_in_by=${a.signed_in_by || a.actor_name || a.guardian_name}, event_type=${a.event_type}`, 'warn'); - } - }); - // Automatically download inspection JSON report const blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `brightwheel_inspection_${state.selectedStudentId}_${Date.now()}.json`; + a.download = `brightwheel_inspection_${state.selectedStudentId || 'active'}_${Date.now()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); - log(`📁 Downloaded full inspection report to your Downloads folder (${(blob.size/1024).toFixed(1)} KB).`, 'success'); + log(`📁 Downloaded full diagnostic inspection JSON report (${(blob.size / 1024).toFixed(1)} KB).`, 'success'); }); } catch (e) { log('Inspection error: ' + e.message, 'error'); @@ -555,12 +650,11 @@ function updateMetrics() { async function fetchAllActivities(studentId) { const pageSize = 50; - let page = 1; - let hasMore = true; - const startDate = el.startDate.value; - const endDate = el.endDate.value; + const startDate = el.startDate.value?.trim(); + const endDate = el.endDate.value?.trim(); const pacingDelay = parseInt(el.concurrencySelect.value) || 200; const seenTypes = new Set(); + const seenActivityIds = new Set(); updateProgress(5, 'Harvesting activity logs...'); @@ -578,102 +672,124 @@ async function fetchAllActivities(studentId) { // Non-critical } - while (hasMore && !state.isCancelled) { - while (state.isPaused && !state.isCancelled) { - await sleep(500); - } - if (state.isCancelled) break; + // Harvester for a specific Brightwheel feed endpoint (0-indexed pagination) + async function harvestEndpoint(endpointBaseUrl, label) { + let page = 0; + let hasMore = true; + log(`Harvesting from ${label}...`, 'info'); - // Build URL query - let url = `https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=${pageSize}&page=${page}`; - if (startDate) url += `&start_date=${startDate}`; - if (endDate) url += `&end_date=${endDate}`; + while (hasMore && !state.isCancelled) { + while (state.isPaused && !state.isCancelled) { + await sleep(500); + } + if (state.isCancelled) break; - log(`Fetching activities page ${page}...`, 'info'); + let url = `${endpointBaseUrl}${endpointBaseUrl.includes('?') ? '&' : '?'}page_size=${pageSize}&page=${page}`; + if (startDate) url += `&start_date=${encodeURIComponent(startDate)}`; + if (endDate) url += `&end_date=${encodeURIComponent(endDate)}`; - let response = null; - let retries = 0; - const maxRetries = 5; + let response = null; + let retries = 0; + const maxRetries = 5; - while (retries < maxRetries) { - response = await fetchBrightwheelAPI(url); - - if (response && response.status === 429) { - state.stats.retryCount++; - updateMetrics(); - const backoffTime = Math.pow(2, retries) * 1000; - log(`Rate limited (429). Backing off for ${backoffTime / 1000}s...`, 'warn'); - await sleep(backoffTime); - retries++; - } else { + while (retries < maxRetries) { + response = await fetchBrightwheelAPI(url); + + if (response && response.status === 429) { + state.stats.retryCount++; + updateMetrics(); + const backoffTime = Math.pow(2, retries) * 1000; + log(`Rate limited (429). Backing off for ${backoffTime / 1000}s...`, 'warn'); + await sleep(backoffTime); + retries++; + } else { + break; + } + } + + const rawActivities = response?.data?.activities || response?.data?.items || (Array.isArray(response?.data) ? response.data : []); + + if (!Array.isArray(rawActivities) || rawActivities.length === 0) { break; } - } - if (!response || !response.success || !response.data) { - if (page === 1) { - log('Trying alternate activity feed endpoint...', 'info'); - const altUrl = `https://schools.mybrightwheel.com/api/v1/activities?student_ids[]=${studentId}&page_size=${pageSize}&page=${page}`; - const altRes = await fetchBrightwheelAPI(altUrl); - if (altRes && altRes.success && altRes.data) { - response = altRes; + for (const act of rawActivities) { + const actId = act.object_id || act.id; + if (actId && seenActivityIds.has(actId)) { + continue; + } + if (actId) { + seenActivityIds.add(actId); } - } - } - const rawActivities = response?.data?.activities || response?.data?.items || response?.data || []; + // Schema inspection for debugging + const actType = act.activity_type || act.action_type || act.type || 'unknown'; + if (!seenTypes.has(actType)) { + seenTypes.add(actType); + const presentKeys = Object.keys(act).filter(k => act[k] !== null && act[k] !== undefined && act[k] !== ''); + log(`[Schema] ${actType} fields: ${presentKeys.join(', ')}`, 'info'); + } - if (!Array.isArray(rawActivities) || rawActivities.length === 0) { - log(`No more activities found after page ${page - 1}. Done harvesting.`, 'info'); - hasMore = false; - break; - } - - // Filter and normalize activities - for (const act of rawActivities) { - // Schema inspection for debugging - const actType = act.activity_type || act.action_type || act.type || 'unknown'; - if (!seenTypes.has(actType)) { - seenTypes.add(actType); - const presentKeys = Object.keys(act).filter(k => act[k] !== null && act[k] !== undefined && act[k] !== ''); - log(`[Schema] ${actType} fields: ${presentKeys.join(', ')}`, 'info'); - } - - // If video activity has no immediate URL, fetch single activity detail - if ((actType === 'ac_video' || actType === 'video') && !act.video_url && !act.media?.video_url) { - try { - const detailRes = await fetchBrightwheelAPI(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities/${act.id}`); - if (detailRes && detailRes.success && detailRes.data) { - const detailObj = detailRes.data.activity || detailRes.data; - Object.assign(act, detailObj); + // If video activity has no immediate URL, fetch single activity detail + if ((actType === 'ac_video' || actType === 'video') && !act.video_url && !act.video_info && !act.media?.video_url) { + try { + const sid = act.target?.object_id || studentId; + if (sid && act.id) { + const detailRes = await fetchBrightwheelAPI(`https://schools.mybrightwheel.com/api/v1/students/${sid}/activities/${act.id}`); + if (detailRes && detailRes.success && detailRes.data) { + const detailObj = detailRes.data.activity || detailRes.data; + Object.assign(act, detailObj); + } + } + } catch (e) { + // Continue } - } catch (e) { - // Continue + } + + if (matchesSelectedCategories(act)) { + state.activities.push(normalizeActivity(act)); } } - if (matchesSelectedCategories(act)) { - state.activities.push(normalizeActivity(act)); + state.stats.activitiesCount = state.activities.length; + updateMetrics(); + updateProgress(Math.min(30, 5 + (state.activities.length / 50) * 2), `Harvested ${state.activities.length} activities (Page ${page + 1})`); + + if (rawActivities.length < pageSize) { + hasMore = false; + break; + } + + page++; + await sleep(pacingDelay); + } + } + + // 1. Primary: Student-scoped activity feed + if (studentId) { + await harvestEndpoint(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities`, `Student Activity Feed (${studentId.substring(0, 8)}...)`); + } else if (state.students.length > 0) { + for (const st of state.students) { + if (st.id) { + await harvestEndpoint(`https://schools.mybrightwheel.com/api/v1/students/${st.id}/activities`, `Student Activity Feed (${st.first_name || st.id})`); } } - - state.stats.activitiesCount = state.activities.length; - updateMetrics(); - updateProgress(Math.min(30, 5 + page * 2), `Harvested ${state.activities.length} activities (Page ${page})`); - - if (rawActivities.length < pageSize) { - hasMore = false; - break; - } - - page++; - await sleep(pacingDelay); } + + // Sort activities chronologically descending + state.activities.sort((a, b) => { + const da = new Date(a.created_at || a.event_date || 0).getTime(); + const db = new Date(b.created_at || b.event_date || 0).getTime(); + return db - da; + }); + + state.stats.activitiesCount = state.activities.length; + updateMetrics(); } function matchesSelectedCategories(act) { const type = (act.activity_type || act.action_type || act.type || '').toLowerCase(); - + if (type.includes('photo') || type.includes('video') || act.media || act.image_url || act.video_url || act.video_stream_url) { if (el.catPhotos.checked) return true; } @@ -947,7 +1063,7 @@ function capitalize(s) { function extractMediaQueue() { state.mediaQueue = []; - + state.activities.forEach((act) => { const url = act.video_url || act.image_url; if (url) { @@ -1140,7 +1256,7 @@ async function buildAndDownloadZip(studentProfile) { await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'activities.json', exportJsonStr); } await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'exporter_job_info.json', jobInfoStr); - + if (csvContent) { await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'activities.csv', csvContent); } diff --git a/dashboard/viewer_template.html b/dashboard/viewer_template.html index a6407f7..96fa37b 100644 --- a/dashboard/viewer_template.html +++ b/dashboard/viewer_template.html @@ -287,6 +287,13 @@ return; } + // Sort activities descending by date (newest first) + ARCHIVE_DATA.activities.sort((a, b) => { + const da = new Date(a.created_at || a.event_date || 0).getTime(); + const db = new Date(b.created_at || b.event_date || 0).getTime(); + return db - da; + }); + renderProfile(); renderFeed('all', ''); setupEvents(); diff --git a/manifest.json b/manifest.json index e31e06d..d322887 100644 --- a/manifest.json +++ b/manifest.json @@ -30,7 +30,8 @@ "permissions": [ "storage", "downloads", - "tabs" + "tabs", + "cookies" ], "host_permissions": [ "https://schools.mybrightwheel.com/*", diff --git a/scripts/content_script.js b/scripts/content_script.js index cfb3963..4b161ef 100644 --- a/scripts/content_script.js +++ b/scripts/content_script.js @@ -6,13 +6,55 @@ (function () { console.log('[Brightwheel Exporter] Content script initialized.'); - // Extract student ID from URL if present + // Extract student ID from URL, DOM, or Storage function detectCurrentStudentId() { - const match = window.location.pathname.match(/\/students\/([a-zA-Z0-9-]+)/); - if (match && match[1]) { - chrome.storage.local.set({ lastActiveStudentId: match[1] }); - return match[1]; + // 1. Check URL pathname & search params + const pathMatch = window.location.pathname.match(/\/(?:students|children|profiles)\/([a-zA-Z0-9-]+)/); + if (pathMatch && pathMatch[1]) { + chrome.storage.local.set({ lastActiveStudentId: pathMatch[1] }); + return pathMatch[1]; } + + const urlParams = new URLSearchParams(window.location.search); + const paramId = urlParams.get('student_id') || urlParams.get('studentId') || urlParams.get('child_id'); + if (paramId) { + chrome.storage.local.set({ lastActiveStudentId: paramId }); + return paramId; + } + + // 2. Check DOM links or attributes + const studentLink = document.querySelector('a[href*="/students/"], a[href*="/children/"], [data-student-id]'); + if (studentLink) { + const href = studentLink.getAttribute('href') || ''; + const hrefMatch = href.match(/\/(?:students|children)\/([a-zA-Z0-9-]+)/); + if (hrefMatch && hrefMatch[1]) { + chrome.storage.local.set({ lastActiveStudentId: hrefMatch[1] }); + return hrefMatch[1]; + } + const dataId = studentLink.getAttribute('data-student-id'); + if (dataId) { + chrome.storage.local.set({ lastActiveStudentId: dataId }); + return dataId; + } + } + + // 3. Check localStorage for student UUID + try { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + const val = localStorage.getItem(key); + if (typeof val === 'string' && (key.includes('student') || key.includes('child') || key.includes('user') || key.includes('auth'))) { + const uuidMatch = val.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); + if (uuidMatch && uuidMatch[0]) { + chrome.storage.local.set({ lastActiveStudentId: uuidMatch[0] }); + return uuidMatch[0]; + } + } + } + } catch (e) { + // Storage access blocked or restricted + } + return null; } @@ -28,6 +70,118 @@ } }).observe(document, { subtree: true, childList: true }); + // Deep extract student profiles from API payloads + function extractStudentsFromData(data) { + if (!data) return []; + const students = []; + const seenIds = new Set(); + + function addStudent(s) { + if (!s) return; + const sId = s.id || s.object_id || s.student_id; + if (sId && !seenIds.has(sId)) { + seenIds.add(sId); + students.push({ + id: sId, + first_name: s.first_name || s.name?.split(' ')?.[0] || 'Child', + last_name: s.last_name || s.name?.split(' ')?.slice(1).join(' ') || '', + picture_url: s.picture_url || s.avatar_url || s.photo_url || s.profile_photo?.image_url || s.profile_photo?.thumbnail_url || null, + school_name: s.school_name || s.school?.name || s.room_name || s.room?.name || 'Brightwheel Profile', + room: s.room || null + }); + } + } + + if (Array.isArray(data.students)) data.students.forEach(addStudent); + if (Array.isArray(data.children)) data.children.forEach(addStudent); + if (Array.isArray(data.profiles)) data.profiles.forEach(addStudent); + if (Array.isArray(data.guardians)) { + data.guardians.forEach(g => { + if (Array.isArray(g.students)) g.students.forEach(addStudent); + if (Array.isArray(g.children)) g.children.forEach(addStudent); + }); + } + if (data.student) addStudent(data.student); + if (data.user?.students) data.user.students.forEach(addStudent); + + if (Array.isArray(data.activities) || Array.isArray(data)) { + const items = Array.isArray(data.activities) ? data.activities : data; + for (const item of items) { + if (item.target && item.target.user_type === 'student') { + addStudent(item.target); + } + } + } + + return students; + } + + // Fetch full student & session discovery directly within authenticated tab + async function discoverSessionAndStudents() { + const results = { + authenticated: false, + user: null, + students: [], + activeStudentId: detectCurrentStudentId(), + liveActivitiesSample: [] + }; + + try { + const meRes = await fetch('https://schools.mybrightwheel.com/api/v1/users/me', { + credentials: 'include', + headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } + }).then(r => r.ok ? r.json() : null).catch(() => null); + + if (meRes) { + results.authenticated = true; + results.user = meRes; + const found = extractStudentsFromData(meRes); + found.forEach(s => results.students.push(s)); + } + + const guardianRes = await fetch('https://schools.mybrightwheel.com/api/v1/guardians/me', { + credentials: 'include', + headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } + }).then(r => r.ok ? r.json() : null).catch(() => null); + + if (guardianRes) { + results.authenticated = true; + const found = extractStudentsFromData(guardianRes); + found.forEach(s => { + if (!results.students.some(existing => existing.id === s.id)) { + results.students.push(s); + } + }); + } + + const actProbe = await fetch('https://schools.mybrightwheel.com/api/v1/activities?page_size=10', { + credentials: 'include', + headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } + }).then(r => r.ok ? r.json() : null).catch(() => null); + + if (actProbe) { + results.authenticated = true; + const acts = actProbe.activities || actProbe.items || []; + results.liveActivitiesSample = acts.slice(0, 5); + const fromActs = extractStudentsFromData(acts); + fromActs.forEach(s => { + if (!results.students.some(existing => existing.id === s.id)) { + results.students.push(s); + } + }); + } + + if (results.students.length > 0 && !results.activeStudentId) { + results.activeStudentId = results.students[0].id; + chrome.storage.local.set({ lastActiveStudentId: results.activeStudentId }); + } + } catch (e) { + console.warn('[Brightwheel Exporter] Discovery note:', e); + } + + return results; + } + // Listen for messages from extension popup or dashboard chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'GET_PAGE_CONTEXT') { @@ -40,49 +194,137 @@ return true; } + if (message.action === 'GET_STUDENTS_AND_SESSION') { + discoverSessionAndStudents().then(res => { + sendResponse({ success: true, ...res }); + }).catch(err => { + sendResponse({ success: false, error: err.message }); + }); + return true; + } + + if (message.action === 'SCRAPE_DOM_FEED') { + const cards = scrapeDomFeedCards(); + sendResponse({ + success: true, + activities: cards, + count: cards.length + }); + return true; + } + if (message.action === 'INSPECT_LIVE_FEED') { (async () => { try { const studentId = message.studentId || detectCurrentStudentId(); - const apiListRes = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=25`, { - credentials: 'include', - headers: { 'Accept': 'application/json' } - }).then(r => r.json()).catch(e => ({ error: e.message })); + const endpointProbes = {}; - const rawActs = apiListRes?.activities || apiListRes?.items || []; - - // Sample detail fetches for video, nap, checkin - const sampleDetails = {}; - for (const act of rawActs.slice(0, 10)) { - const type = act.activity_type || act.action_type || act.type; - if (!sampleDetails[type] && act.id) { - const detail = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities/${act.id}`, { + async function probe(name, url) { + try { + const res = await fetch(url, { credentials: 'include', - headers: { 'Accept': 'application/json' } - }).then(r => r.json()).catch(e => ({ error: e.message })); - sampleDetails[type] = detail; + headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } + }); + const json = await res.json().catch(() => null); + const items = json?.activities || json?.items || (Array.isArray(json) ? json : []); + endpointProbes[name] = { + url, + status: res.status, + ok: res.ok, + itemCount: items.length, + latestDate: items[0]?.event_date || items[0]?.created_at || null, + earliestDate: items[items.length - 1]?.event_date || items[items.length - 1]?.created_at || null, + sampleItems: items.slice(0, 5), + rawSample: json + }; + } catch (e) { + endpointProbes[name] = { url, error: e.message }; } } + // 1. Probe general activities endpoint + await probe('parent_activities_feed', 'https://schools.mybrightwheel.com/api/v1/activities?page_size=25'); + + // 2. Probe student-specific endpoint + if (studentId) { + await probe('student_activities_feed', `https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=25`); + } + + // 3. Probe user/guardian profiles + await probe('users_me', 'https://schools.mybrightwheel.com/api/v1/users/me'); + await probe('guardians_me', 'https://schools.mybrightwheel.com/api/v1/guardians/me'); + + // 4. Probe feed / timeline + await probe('feed_endpoint', 'https://schools.mybrightwheel.com/api/v1/feed?page_size=25'); + const domCards = scrapeDomFeedCards(); + const reactFiberActivities = extractActivitiesFromReactFiber(); sendResponse({ success: true, studentId, - apiSampleActivities: rawActs.slice(0, 10), - sampleDetails, - domCards: domCards.slice(0, 20), + currentUrl: window.location.href, pageTitle: document.title, - currentUrl: window.location.href + endpointProbes, + domCards: domCards.slice(0, 20), + reactFiberCount: reactFiberActivities.length, + reactFiberSample: reactFiberActivities.slice(0, 5) }); } catch (err) { sendResponse({ success: false, error: err.message }); } })(); - return true; // async + return true; } }); + // Deep React Fiber state extractor for rendered activities + function extractActivitiesFromReactFiber() { + const rawItems = []; + const seenIds = new Set(); + + function inspectProps(props) { + if (!props || typeof props !== 'object') return; + const candidate = props.activity || props.item || props.feedItem || props.record || props.event; + if (candidate && typeof candidate === 'object') { + const id = candidate.object_id || candidate.id; + if (id && !seenIds.has(id)) { + seenIds.add(id); + rawItems.push(candidate); + } + } + if (Array.isArray(props.activities)) { + props.activities.forEach(a => { + const id = a?.object_id || a?.id; + if (id && !seenIds.has(id)) { + seenIds.add(id); + rawItems.push(a); + } + }); + } + } + + try { + document.querySelectorAll('*').forEach(node => { + const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')); + if (fiberKey && node[fiberKey]) { + let fiber = node[fiberKey]; + let depth = 0; + while (fiber && depth < 10) { + inspectProps(fiber.memoizedProps); + inspectProps(fiber.pendingProps); + fiber = fiber.return; + depth++; + } + } + }); + } catch (e) { + // Ignore + } + + return rawItems; + } + // DOM Scraper for Brightwheel Web Feed function scrapeDomFeedCards() { const results = [];