Compare commits
1
Commits
0e23532113
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae39f52563 |
+1
-1
@@ -4,7 +4,7 @@ Thumbs.db
|
|||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
# User Export Data & Backups (Prevent leaking child photos/videos/records)
|
# User Export Data & Backups (Prevent leaking child photos/videos/records)
|
||||||
emmy/
|
emmy*/
|
||||||
exports/
|
exports/
|
||||||
data/
|
data/
|
||||||
test_data/
|
test_data/
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<details class="manual-id-details" id="manualDetails">
|
<details class="manual-id-details" id="manualDetails">
|
||||||
<summary>Enter Student UUID Manually</summary>
|
<summary>Enter Student UUID Manually</summary>
|
||||||
<div class="manual-input-row">
|
<div class="manual-input-row">
|
||||||
<input type="text" id="manualStudentId" placeholder="e.g. edda433b-2a24-4da7-bbd5-9f131b4b2565" value="edda433b-2a24-4da7-bbd5-9f131b4b2565">
|
<input type="text" id="manualStudentId" placeholder="Auto-detected from active Brightwheel tab, or enter UUID">
|
||||||
<button id="setManualIdBtn" class="secondary-btn">Use ID</button>
|
<button id="setManualIdBtn" class="secondary-btn">Use ID</button>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -291,6 +291,7 @@
|
|||||||
<div class="card-header log-header">
|
<div class="card-header log-header">
|
||||||
<h3>Live Activity Log</h3>
|
<h3>Live Activity Log</h3>
|
||||||
<div class="log-actions">
|
<div class="log-actions">
|
||||||
|
<button id="inspectFeedBtn" class="link-btn" style="color: var(--primary); font-weight: 600; margin-right: 8px;">🔬 Inspect Live Tab</button>
|
||||||
<button id="clearLogBtn" class="link-btn">Clear</button>
|
<button id="clearLogBtn" class="link-btn">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+262
-146
@@ -5,7 +5,7 @@
|
|||||||
// Application State
|
// Application State
|
||||||
const state = {
|
const state = {
|
||||||
students: [],
|
students: [],
|
||||||
selectedStudentId: 'edda433b-2a24-4da7-bbd5-9f131b4b2565',
|
selectedStudentId: '',
|
||||||
selectedStudentData: null,
|
selectedStudentData: null,
|
||||||
destinationDirectoryHandle: null,
|
destinationDirectoryHandle: null,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
@@ -31,13 +31,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
initDOMElements();
|
initDOMElements();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
initDatePresets();
|
initDatePresets();
|
||||||
|
|
||||||
if (window.JSZip || typeof JSZip !== 'undefined') {
|
if (window.JSZip || typeof JSZip !== 'undefined') {
|
||||||
log('Zip compression engine initialized (JSZip v3.10.1).', 'info');
|
log('Zip compression engine initialized (JSZip v3.10.1).', 'info');
|
||||||
} else {
|
} else {
|
||||||
log('Warning: JSZip library not detected on page load.', 'warn');
|
log('Warning: JSZip library not detected on page load.', 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
await checkSessionAndDiscoverStudents();
|
await checkSessionAndDiscoverStudents();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,14 +78,15 @@ function initDOMElements() {
|
|||||||
metricSize: document.getElementById('metricSize'),
|
metricSize: document.getElementById('metricSize'),
|
||||||
metricRateLimit: document.getElementById('metricRateLimit'),
|
metricRateLimit: document.getElementById('metricRateLimit'),
|
||||||
terminalLog: document.getElementById('terminalLog'),
|
terminalLog: document.getElementById('terminalLog'),
|
||||||
clearLogBtn: document.getElementById('clearLogBtn')
|
clearLogBtn: document.getElementById('clearLogBtn'),
|
||||||
|
inspectFeedBtn: document.getElementById('inspectFeedBtn')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(message, type = 'info') {
|
function log(message, type = 'info') {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const timeFormatted = new Date().toLocaleTimeString();
|
const timeFormatted = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
state.executionLogs.push({
|
state.executionLogs.push({
|
||||||
timestamp,
|
timestamp,
|
||||||
time: timeFormatted,
|
time: timeFormatted,
|
||||||
@@ -104,65 +105,176 @@ function log(message, type = 'info') {
|
|||||||
// Session & Student Discovery
|
// 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() {
|
async function checkSessionAndDiscoverStudents() {
|
||||||
log('Checking active Brightwheel browser session...', 'info');
|
log('Checking active Brightwheel browser session...', 'info');
|
||||||
|
|
||||||
// Check stored active student ID first
|
|
||||||
const stored = await chrome.storage.local.get(['lastActiveStudentId']);
|
const stored = await chrome.storage.local.get(['lastActiveStudentId']);
|
||||||
if (stored.lastActiveStudentId) {
|
if (stored.lastActiveStudentId) {
|
||||||
state.selectedStudentId = stored.lastActiveStudentId;
|
state.selectedStudentId = stored.lastActiveStudentId;
|
||||||
el.manualStudentId.value = 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 {
|
try {
|
||||||
// 1. Try to fetch user/guardian profile to list students
|
|
||||||
const userRes = await fetchBrightwheelAPI('https://schools.mybrightwheel.com/api/v1/users/me');
|
const userRes = await fetchBrightwheelAPI('https://schools.mybrightwheel.com/api/v1/users/me');
|
||||||
|
|
||||||
if (userRes && userRes.success && userRes.data) {
|
if (userRes && userRes.success && userRes.data) {
|
||||||
el.sessionDot.classList.add('online');
|
el.sessionDot.classList.add('online');
|
||||||
el.sessionStatusText.textContent = 'Brightwheel Session Active';
|
el.sessionStatusText.textContent = 'Brightwheel Session Active';
|
||||||
log('Successfully connected to Brightwheel user profile.', 'success');
|
|
||||||
|
|
||||||
const userData = userRes.data;
|
const found = extractStudentsFromPayload(userRes.data);
|
||||||
const studentsFound = userData.students || userData.children || (userData.guardians && userData.guardians[0]?.students) || [];
|
if (found.length > 0) {
|
||||||
|
state.students = found;
|
||||||
if (studentsFound.length > 0) {
|
if (!state.selectedStudentId) state.selectedStudentId = found[0].id;
|
||||||
state.students = studentsFound;
|
state.selectedStudentData = found.find(s => s.id === state.selectedStudentId) || found[0];
|
||||||
|
el.manualStudentId.value = state.selectedStudentId;
|
||||||
renderStudentsList();
|
renderStudentsList();
|
||||||
|
log(`Loaded ${found.length} student profile(s) from user account.`, 'success');
|
||||||
return;
|
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) {
|
if (state.selectedStudentId) {
|
||||||
const studentRes = await fetchBrightwheelAPI(`https://schools.mybrightwheel.com/api/v1/students/${state.selectedStudentId}`);
|
const studentRes = await fetchBrightwheelAPI(`https://schools.mybrightwheel.com/api/v1/students/${state.selectedStudentId}`);
|
||||||
if (studentRes && studentRes.success && studentRes.data) {
|
if (studentRes && studentRes.success && studentRes.data) {
|
||||||
el.sessionDot.classList.add('online');
|
el.sessionDot.classList.add('online');
|
||||||
el.sessionStatusText.textContent = 'Brightwheel Session Active';
|
el.sessionStatusText.textContent = 'Brightwheel Session Active';
|
||||||
|
|
||||||
const s = studentRes.data.student || studentRes.data;
|
const s = studentRes.data.student || studentRes.data;
|
||||||
state.students = [s];
|
state.students = [s];
|
||||||
state.selectedStudentData = s;
|
state.selectedStudentData = s;
|
||||||
renderStudentsList();
|
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;
|
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
|
// No student found yet
|
||||||
el.sessionDot.classList.add('online');
|
el.sessionStatusText.textContent = 'Open Brightwheel in another tab to auto-connect';
|
||||||
el.sessionStatusText.textContent = 'Session Connected';
|
|
||||||
state.students = [{
|
|
||||||
id: state.selectedStudentId,
|
|
||||||
first_name: 'Student',
|
|
||||||
last_name: `(${state.selectedStudentId.substring(0, 8)}...)`,
|
|
||||||
school_name: 'Brightwheel School'
|
|
||||||
}];
|
|
||||||
renderStudentsList();
|
renderStudentsList();
|
||||||
log(`Using student UUID: ${state.selectedStudentId}`, 'info');
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(`Session check note: ${err.message}. Ready with manual ID.`, 'warn');
|
log(`Session check note: ${err.message}.`, 'warn');
|
||||||
renderStudentsList();
|
renderStudentsList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,9 +302,9 @@ async function fetchBrightwheelAPI(url) {
|
|||||||
|
|
||||||
function renderStudentsList() {
|
function renderStudentsList() {
|
||||||
el.studentsList.innerHTML = '';
|
el.studentsList.innerHTML = '';
|
||||||
|
|
||||||
if (state.students.length === 0) {
|
if (state.students.length === 0) {
|
||||||
el.studentsList.innerHTML = '<div class="loading-placeholder">No students auto-detected. Use manual UUID below.</div>';
|
el.studentsList.innerHTML = '<div class="loading-placeholder">No students auto-detected. Please ensure Brightwheel is open in a tab or enter UUID below.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +314,7 @@ function renderStudentsList() {
|
|||||||
const firstName = student.first_name || 'My';
|
const firstName = student.first_name || 'My';
|
||||||
const lastName = student.last_name || 'Child';
|
const lastName = student.last_name || 'Child';
|
||||||
const initials = (firstName[0] || 'B') + (lastName[0] || 'W');
|
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');
|
const card = document.createElement('div');
|
||||||
card.className = `student-card ${isSelected ? 'selected' : ''}`;
|
card.className = `student-card ${isSelected ? 'selected' : ''}`;
|
||||||
@@ -227,6 +339,7 @@ function renderStudentsList() {
|
|||||||
state.selectedStudentId = sId;
|
state.selectedStudentId = sId;
|
||||||
state.selectedStudentData = student;
|
state.selectedStudentData = student;
|
||||||
el.manualStudentId.value = sId;
|
el.manualStudentId.value = sId;
|
||||||
|
chrome.storage.local.set({ lastActiveStudentId: sId });
|
||||||
log(`Selected child: ${firstName} ${lastName} (${sId})`, 'info');
|
log(`Selected child: ${firstName} ${lastName} (${sId})`, 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,11 +352,8 @@ function renderStudentsList() {
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
function initDatePresets() {
|
function initDatePresets() {
|
||||||
const today = new Date();
|
|
||||||
const currentYear = today.getFullYear();
|
|
||||||
|
|
||||||
el.startDate.value = '';
|
el.startDate.value = '';
|
||||||
el.endDate.value = today.toISOString().split('T')[0];
|
el.endDate.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDateRange(rangeType) {
|
function setDateRange(rangeType) {
|
||||||
@@ -252,7 +362,7 @@ function setDateRange(rangeType) {
|
|||||||
|
|
||||||
if (rangeType === 'all') {
|
if (rangeType === 'all') {
|
||||||
el.startDate.value = '';
|
el.startDate.value = '';
|
||||||
el.endDate.value = todayStr;
|
el.endDate.value = '';
|
||||||
} else if (rangeType === 'this-year') {
|
} else if (rangeType === 'this-year') {
|
||||||
el.startDate.value = `${today.getFullYear()}-01-01`;
|
el.startDate.value = `${today.getFullYear()}-01-01`;
|
||||||
el.endDate.value = todayStr;
|
el.endDate.value = todayStr;
|
||||||
@@ -387,45 +497,30 @@ async function handleInspectFeed() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`✅ Live Inspection Complete for: ${res.currentUrl}`, 'success');
|
log(`✅ Live Tab Diagnostic Complete: ${res.currentUrl}`, 'success');
|
||||||
log(`Page Title: "${res.pageTitle}"`, 'info');
|
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) {
|
if (res.endpointProbes) {
|
||||||
res.domCards.slice(0, 3).forEach((dc, i) => {
|
for (const [name, probe] of Object.entries(res.endpointProbes)) {
|
||||||
log(` [DOM Card ${i + 1}] Text: "${dc.raw_text.replace(/\s+/g, ' ').substring(0, 100)}..."`, 'info');
|
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
|
// Automatically download inspection JSON report
|
||||||
const blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
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);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
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) {
|
} catch (e) {
|
||||||
log('Inspection error: ' + e.message, 'error');
|
log('Inspection error: ' + e.message, 'error');
|
||||||
@@ -555,12 +650,11 @@ function updateMetrics() {
|
|||||||
|
|
||||||
async function fetchAllActivities(studentId) {
|
async function fetchAllActivities(studentId) {
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
let page = 1;
|
const startDate = el.startDate.value?.trim();
|
||||||
let hasMore = true;
|
const endDate = el.endDate.value?.trim();
|
||||||
const startDate = el.startDate.value;
|
|
||||||
const endDate = el.endDate.value;
|
|
||||||
const pacingDelay = parseInt(el.concurrencySelect.value) || 200;
|
const pacingDelay = parseInt(el.concurrencySelect.value) || 200;
|
||||||
const seenTypes = new Set();
|
const seenTypes = new Set();
|
||||||
|
const seenActivityIds = new Set();
|
||||||
|
|
||||||
updateProgress(5, 'Harvesting activity logs...');
|
updateProgress(5, 'Harvesting activity logs...');
|
||||||
|
|
||||||
@@ -578,102 +672,124 @@ async function fetchAllActivities(studentId) {
|
|||||||
// Non-critical
|
// Non-critical
|
||||||
}
|
}
|
||||||
|
|
||||||
while (hasMore && !state.isCancelled) {
|
// Harvester for a specific Brightwheel feed endpoint (0-indexed pagination)
|
||||||
while (state.isPaused && !state.isCancelled) {
|
async function harvestEndpoint(endpointBaseUrl, label) {
|
||||||
await sleep(500);
|
let page = 0;
|
||||||
}
|
let hasMore = true;
|
||||||
if (state.isCancelled) break;
|
log(`Harvesting from ${label}...`, 'info');
|
||||||
|
|
||||||
// Build URL query
|
while (hasMore && !state.isCancelled) {
|
||||||
let url = `https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=${pageSize}&page=${page}`;
|
while (state.isPaused && !state.isCancelled) {
|
||||||
if (startDate) url += `&start_date=${startDate}`;
|
await sleep(500);
|
||||||
if (endDate) url += `&end_date=${endDate}`;
|
}
|
||||||
|
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 response = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
const maxRetries = 5;
|
const maxRetries = 5;
|
||||||
|
|
||||||
while (retries < maxRetries) {
|
while (retries < maxRetries) {
|
||||||
response = await fetchBrightwheelAPI(url);
|
response = await fetchBrightwheelAPI(url);
|
||||||
|
|
||||||
if (response && response.status === 429) {
|
if (response && response.status === 429) {
|
||||||
state.stats.retryCount++;
|
state.stats.retryCount++;
|
||||||
updateMetrics();
|
updateMetrics();
|
||||||
const backoffTime = Math.pow(2, retries) * 1000;
|
const backoffTime = Math.pow(2, retries) * 1000;
|
||||||
log(`Rate limited (429). Backing off for ${backoffTime / 1000}s...`, 'warn');
|
log(`Rate limited (429). Backing off for ${backoffTime / 1000}s...`, 'warn');
|
||||||
await sleep(backoffTime);
|
await sleep(backoffTime);
|
||||||
retries++;
|
retries++;
|
||||||
} else {
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawActivities = response?.data?.activities || response?.data?.items || (Array.isArray(response?.data) ? response.data : []);
|
||||||
|
|
||||||
|
if (!Array.isArray(rawActivities) || rawActivities.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!response || !response.success || !response.data) {
|
for (const act of rawActivities) {
|
||||||
if (page === 1) {
|
const actId = act.object_id || act.id;
|
||||||
log('Trying alternate activity feed endpoint...', 'info');
|
if (actId && seenActivityIds.has(actId)) {
|
||||||
const altUrl = `https://schools.mybrightwheel.com/api/v1/activities?student_ids[]=${studentId}&page_size=${pageSize}&page=${page}`;
|
continue;
|
||||||
const altRes = await fetchBrightwheelAPI(altUrl);
|
}
|
||||||
if (altRes && altRes.success && altRes.data) {
|
if (actId) {
|
||||||
response = altRes;
|
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) {
|
// If video activity has no immediate URL, fetch single activity detail
|
||||||
log(`No more activities found after page ${page - 1}. Done harvesting.`, 'info');
|
if ((actType === 'ac_video' || actType === 'video') && !act.video_url && !act.video_info && !act.media?.video_url) {
|
||||||
hasMore = false;
|
try {
|
||||||
break;
|
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}`);
|
||||||
// Filter and normalize activities
|
if (detailRes && detailRes.success && detailRes.data) {
|
||||||
for (const act of rawActivities) {
|
const detailObj = detailRes.data.activity || detailRes.data;
|
||||||
// Schema inspection for debugging
|
Object.assign(act, detailObj);
|
||||||
const actType = act.activity_type || act.action_type || act.type || 'unknown';
|
}
|
||||||
if (!seenTypes.has(actType)) {
|
}
|
||||||
seenTypes.add(actType);
|
} catch (e) {
|
||||||
const presentKeys = Object.keys(act).filter(k => act[k] !== null && act[k] !== undefined && act[k] !== '');
|
// Continue
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
}
|
||||||
// Continue
|
|
||||||
|
if (matchesSelectedCategories(act)) {
|
||||||
|
state.activities.push(normalizeActivity(act));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchesSelectedCategories(act)) {
|
state.stats.activitiesCount = state.activities.length;
|
||||||
state.activities.push(normalizeActivity(act));
|
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) {
|
function matchesSelectedCategories(act) {
|
||||||
const type = (act.activity_type || act.action_type || act.type || '').toLowerCase();
|
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 (type.includes('photo') || type.includes('video') || act.media || act.image_url || act.video_url || act.video_stream_url) {
|
||||||
if (el.catPhotos.checked) return true;
|
if (el.catPhotos.checked) return true;
|
||||||
}
|
}
|
||||||
@@ -947,7 +1063,7 @@ function capitalize(s) {
|
|||||||
|
|
||||||
function extractMediaQueue() {
|
function extractMediaQueue() {
|
||||||
state.mediaQueue = [];
|
state.mediaQueue = [];
|
||||||
|
|
||||||
state.activities.forEach((act) => {
|
state.activities.forEach((act) => {
|
||||||
const url = act.video_url || act.image_url;
|
const url = act.video_url || act.image_url;
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -1140,7 +1256,7 @@ async function buildAndDownloadZip(studentProfile) {
|
|||||||
await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'activities.json', exportJsonStr);
|
await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'activities.json', exportJsonStr);
|
||||||
}
|
}
|
||||||
await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'exporter_job_info.json', jobInfoStr);
|
await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'exporter_job_info.json', jobInfoStr);
|
||||||
|
|
||||||
if (csvContent) {
|
if (csvContent) {
|
||||||
await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'activities.csv', csvContent);
|
await writeBlobToDirectoryHandle(state.destinationDirectoryHandle, 'activities.csv', csvContent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,13 @@
|
|||||||
return;
|
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();
|
renderProfile();
|
||||||
renderFeed('all', '');
|
renderFeed('all', '');
|
||||||
setupEvents();
|
setupEvents();
|
||||||
|
|||||||
+2
-1
@@ -30,7 +30,8 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"downloads",
|
"downloads",
|
||||||
"tabs"
|
"tabs",
|
||||||
|
"cookies"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://schools.mybrightwheel.com/*",
|
"https://schools.mybrightwheel.com/*",
|
||||||
|
|||||||
+267
-25
@@ -6,13 +6,55 @@
|
|||||||
(function () {
|
(function () {
|
||||||
console.log('[Brightwheel Exporter] Content script initialized.');
|
console.log('[Brightwheel Exporter] Content script initialized.');
|
||||||
|
|
||||||
// Extract student ID from URL if present
|
// Extract student ID from URL, DOM, or Storage
|
||||||
function detectCurrentStudentId() {
|
function detectCurrentStudentId() {
|
||||||
const match = window.location.pathname.match(/\/students\/([a-zA-Z0-9-]+)/);
|
// 1. Check URL pathname & search params
|
||||||
if (match && match[1]) {
|
const pathMatch = window.location.pathname.match(/\/(?:students|children|profiles)\/([a-zA-Z0-9-]+)/);
|
||||||
chrome.storage.local.set({ lastActiveStudentId: match[1] });
|
if (pathMatch && pathMatch[1]) {
|
||||||
return match[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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +70,118 @@
|
|||||||
}
|
}
|
||||||
}).observe(document, { subtree: true, childList: true });
|
}).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
|
// Listen for messages from extension popup or dashboard
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.action === 'GET_PAGE_CONTEXT') {
|
if (message.action === 'GET_PAGE_CONTEXT') {
|
||||||
@@ -40,49 +194,137 @@
|
|||||||
return true;
|
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') {
|
if (message.action === 'INSPECT_LIVE_FEED') {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const studentId = message.studentId || detectCurrentStudentId();
|
const studentId = message.studentId || detectCurrentStudentId();
|
||||||
const apiListRes = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=25`, {
|
const endpointProbes = {};
|
||||||
credentials: 'include',
|
|
||||||
headers: { 'Accept': 'application/json' }
|
|
||||||
}).then(r => r.json()).catch(e => ({ error: e.message }));
|
|
||||||
|
|
||||||
const rawActs = apiListRes?.activities || apiListRes?.items || [];
|
async function probe(name, url) {
|
||||||
|
try {
|
||||||
// Sample detail fetches for video, nap, checkin
|
const res = await fetch(url, {
|
||||||
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}`, {
|
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { 'Accept': 'application/json' }
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
}).then(r => r.json()).catch(e => ({ error: e.message }));
|
});
|
||||||
sampleDetails[type] = detail;
|
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 domCards = scrapeDomFeedCards();
|
||||||
|
const reactFiberActivities = extractActivitiesFromReactFiber();
|
||||||
|
|
||||||
sendResponse({
|
sendResponse({
|
||||||
success: true,
|
success: true,
|
||||||
studentId,
|
studentId,
|
||||||
apiSampleActivities: rawActs.slice(0, 10),
|
currentUrl: window.location.href,
|
||||||
sampleDetails,
|
|
||||||
domCards: domCards.slice(0, 20),
|
|
||||||
pageTitle: document.title,
|
pageTitle: document.title,
|
||||||
currentUrl: window.location.href
|
endpointProbes,
|
||||||
|
domCards: domCards.slice(0, 20),
|
||||||
|
reactFiberCount: reactFiberActivities.length,
|
||||||
|
reactFiberSample: reactFiberActivities.slice(0, 5)
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendResponse({ success: false, error: err.message });
|
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
|
// DOM Scraper for Brightwheel Web Feed
|
||||||
function scrapeDomFeedCards() {
|
function scrapeDomFeedCards() {
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user