feat: implement robust student ID discovery and multi-endpoint data extraction for activities
CI Verification / Lint & Extension Integrity Check (push) Successful in 18s

This commit is contained in:
2026-08-29 22:39:58 -07:00
parent 0e23532113
commit ae39f52563
6 changed files with 541 additions and 174 deletions
+262 -146
View File
@@ -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 = '<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;
}
@@ -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);
}