/** * Brightwheel Exporter - Content Script * Injected on https://schools.mybrightwheel.com/* */ (function () { console.log('[Brightwheel Exporter] Content script initialized.'); // Extract student ID from URL, DOM, or Storage function detectCurrentStudentId() { // 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; } detectCurrentStudentId(); // Watch for SPA URL changes let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; detectCurrentStudentId(); } }).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') { const studentId = detectCurrentStudentId(); sendResponse({ url: window.location.href, studentId: studentId, cookiesPresent: document.cookie.includes('_brightwheel_v2') }); 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 endpointProbes = {}; async function probe(name, url) { try { const res = await fetch(url, { credentials: 'include', 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, currentUrl: window.location.href, pageTitle: document.title, endpointProbes, domCards: domCards.slice(0, 20), reactFiberCount: reactFiberActivities.length, reactFiberSample: reactFiberActivities.slice(0, 5) }); } catch (err) { sendResponse({ success: false, error: err.message }); } })(); 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 = []; // Broad selector covering common Brightwheel feed cards const cardSelectors = [ '[data-testid*="activity"]', '[data-testid*="feed"]', '.activity-item', '.feed-item', '.timeline-item', 'article', '[class*="ActivityCard"]', '[class*="feed-card"]', '[class*="FeedItem"]', '[class*="TimelineItem"]' ]; let cards = document.querySelectorAll(cardSelectors.join(',')); if (!cards || cards.length === 0) { // Fallback: look for cards by common structure cards = document.querySelectorAll('div[class*="card"], div[class*="Card"]'); } cards.forEach((card, index) => { const text = card.innerText || ''; if (!text.trim()) return; const cardId = card.getAttribute('data-id') || card.getAttribute('data-activity-id') || card.id || `dom_${index}`; const img = card.querySelector('img[src*="cdn"], img[src*="brightwheel"], img[src*="cloudfront"], img[src*="s3"]'); const video = card.querySelector('video, source, a[href*=".mp4"], a[href*="video"]'); const videoUrl = video?.src || video?.getAttribute('src') || video?.getAttribute('href') || null; const imageUrl = img?.src || null; results.push({ dom_id: cardId, raw_text: text, image_url: imageUrl, video_url: videoUrl, class_name: card.className, html_snippet: card.outerHTML.substring(0, 800) }); }); return results; } // Inject a discreet floating button on Brightwheel page function injectExporterButton() { if (document.getElementById('bw-exporter-floating-btn')) return; const btn = document.createElement('button'); btn.id = 'bw-exporter-floating-btn'; btn.innerHTML = ` Export Brightwheel Data `; Object.assign(btn.style, { position: 'fixed', bottom: '24px', right: '24px', zIndex: '999999', display: 'inline-flex', alignItems: 'center', padding: '10px 18px', background: 'linear-gradient(135deg, #FF6B35 0%, #FF8E53 100%)', color: '#FFFFFF', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: '14px', fontWeight: '600', border: 'none', borderRadius: '30px', boxShadow: '0 4px 16px rgba(255, 107, 53, 0.4), 0 2px 6px rgba(0, 0, 0, 0.1)', cursor: 'pointer', transition: 'all 0.25s ease', }); btn.addEventListener('mouseenter', () => { btn.style.transform = 'translateY(-2px) scale(1.03)'; btn.style.boxShadow = '0 6px 20px rgba(255, 107, 53, 0.5), 0 3px 8px rgba(0, 0, 0, 0.15)'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'translateY(0) scale(1)'; btn.style.boxShadow = '0 4px 16px rgba(255, 107, 53, 0.4), 0 2px 6px rgba(0, 0, 0, 0.1)'; }); btn.addEventListener('click', () => { chrome.runtime.sendMessage({ action: 'OPEN_DASHBOARD' }); }); document.body.appendChild(btn); } // Inject when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectExporterButton); } else { injectExporterButton(); } })();