← Back to Hub

Readability Lab



Live Collect (drop files or paste text)

Drop student_response.json files here, or paste JSON lines.

Collect Live (drag files / paste lines)

0 collected
Drop student_response.json files or paste JSON lines here
Fit breakdown
Understanding breakdown

Live Collect (drop files or paste text)

Drop student_response.json files here, or paste JSON lines.

Live Collector (drag or paste responses)

Drop JSON/CSV files or paste text here

  

Live Collect (drop files or paste text)

Drop student_response.json files here, or paste JSON lines.

Collect Live (drag files / paste lines)

0 collected
Drop student_response.json files or paste JSON lines here
Fit breakdown
Understanding breakdown

Live Collect (drop files or paste text)

Drop student_response.json files here, or paste JSON lines.

Import Student Responses


  
`+ `

Sample (first 50)

`+ `${rows}
TimeIDFitUnderClassTextQ/A
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare Mode (current filter vs selected class) --- function aggregateLabel(a){ return `Resp ${a.n} · Fit E/J/H ${a.fit.too_easy}/${a.fit.just_right}/${a.fit.too_hard} · Under L/M/H ${a.understanding.low}/${a.understanding.medium}/${a.understanding.high} · Q/A ${a.qaTotal}`; } function runCompare(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const panel = document.getElementById('comparePanel'); panel.style.display = 'flex'; panel.innerHTML = '' + '

Current Filter

'+aggregateLabel(A)+'
' + '

Compare: '+cmpClass+'

'+aggregateLabel(B)+'
'; } // Wire buttons (function initFilteredExports(){ const b1 = document.getElementById('exportFilteredCSV'); const b2 = document.getElementById('exportFilteredJSON'); const b3 = document.getElementById('snapshotPDF'); const b4 = document.getElementById('runCompare'); if (b1) b1.addEventListener('click', exportFilteredCSV); if (b2) b2.addEventListener('click', exportFilteredJSON); if (b3) b3.addEventListener('click', snapshotPDF); if (b4) b4.addEventListener('click', runCompare); })(); // --- Mini bar charts (simple canvas) --- function drawMiniBar(canvasId, labels, values){ const cv = document.getElementById(canvasId); if (!cv) return; const ctx = cv.getContext('2d'); ctx.clearRect(0,0,cv.width,cv.height); const max = Math.max(1, ...values); const bw = Math.floor((cv.width - 40) / values.length); const baseY = cv.height - 16; // axes ctx.beginPath(); ctx.moveTo(30, 6); ctx.lineTo(30, baseY); ctx.lineTo(cv.width-6, baseY); ctx.stroke(); for (let i=0; i{ const btn = document.createElement('button'); btn.className = 'mlh-chip'; btn.innerHTML = `${p.name} (${p.class||'(all)'} · ${p.text||'(all)'})`; btn.addEventListener('click', ()=>{ const fc = document.getElementById('filterClass'); const ft = document.getElementById('filterText'); if (fc) fc.value = p.class || ''; if (ft) ft.value = p.text || ''; renderFilteredMetrics(); }); const del = document.createElement('button'); del.textContent = '×'; del.title = 'Delete'; del.style.marginLeft = '6px'; del.addEventListener('click', ()=>{ const confirmDel = confirm('Delete preset "'+p.name+'"?'); if(!confirmDel) return; const cur = loadPresets(); cur.splice(idx,1); savePresets(cur); renderPresetChips(); }); const wrapDiv = document.createElement('span'); wrapDiv.appendChild(btn); wrapDiv.appendChild(del); wrap.appendChild(wrapDiv); }); } function saveCurrentAsPreset(){ const name = (document.getElementById('presetName')||{}).value || ''; const cls = (document.getElementById('filterClass')||{}).value || ''; const txt = (document.getElementById('filterText')||{}).value || ''; if (!name.trim()){ alert('Name your preset.'); return; } const list = loadPresets(); list.push({ name, class: cls, text: txt }); savePresets(list); renderPresetChips(); try{ document.getElementById('presetName').value=''; }catch{} } // --- Snapshot bundle (by class) --- function uniqueClasses(arr){ const set = new Set(); arr.forEach(r=>{ if (r.classTag) set.add(r.classTag); }); return Array.from(set); } function bundleSnapshots(){ const all = getAllResponses(); const currentTxt = (document.getElementById('filterText')||{}).value || ''; const classes = uniqueClasses(all); if (!classes.length){ alert('No class tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle

Generated ${now}
Text filter: ${currentTxt||'(all)'}
`; classes.forEach(cls=>{ const subset = all.filter(r=> (r.classTag||'')===cls && (!currentTxt || String(r.textTag||'').toLowerCase().includes(currentTxt.toLowerCase()))); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.textTag||''}${(r.qa||[]).length}`).join(''); body += `

${cls}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderTextQ/A
`; }); w.document.write(`Snapshot Bundle ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } (function initEnhancedControls(){ const sp = document.getElementById('savePreset'); if (sp) sp.addEventListener('click', saveCurrentAsPreset); renderPresetChips(); // Ensure charts render when metrics change const _oldRender = renderFilteredMetrics; renderFilteredMetrics = function(){ _oldRender(); try{ const subset = applyFilters(getAllResponses()); const A = aggregateWith(subset); drawMiniBar('fitChart', ['E','J','H'], [A.fit.too_easy, A.fit.just_right, A.fit.too_hard]); drawMiniBar('underChart', ['L','M','H'], [A.understanding.low, A.understanding.medium, A.understanding.high]); }catch(e){} }; renderFilteredMetrics(); const bs = document.getElementById('bundleSnapshots'); if (bs) bs.addEventListener('click', bundleSnapshots); })(); // --- Snapshot bundle (by text) --- function uniqueTexts(arr){ const set = new Set(); arr.forEach(r=>{ if (r.textTag) set.add(r.textTag); }); return Array.from(set); } function bundleByText(){ const all = getAllResponses(); const currentClass = (document.getElementById('filterClass')||{}).value || ''; const texts = uniqueTexts(all); if (!texts.length){ alert('No text tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle (by Text)

Generated ${now}
Class filter: ${currentClass||'(all)'}
`; texts.forEach(txt=>{ const subset = all.filter(r=> (!currentClass || (r.classTag||'')===currentClass) && (r.textTag||'')===txt); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.classTag||''}${(r.qa||[]).length}`).join(''); body += `

${txt}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderClassQ/A
`; }); w.document.write(`Snapshot Bundle (by Text) ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare PDF export --- function comparePDF(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const w = window.open('', '_blank', 'width=800,height=600'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; w.document.write(`Compare PDF `+ `

Comparison Report

`+ `

Current Filter

Responses: ${A.n}
Fit E/J/H ${A.fit.too_easy}/${A.fit.just_right}/${A.fit.too_hard}
Understanding L/M/H ${A.understanding.low}/${A.understanding.medium}/${A.understanding.high}
Q/A total: ${A.qaTotal}
`+ `

Compare: ${cmpClass}

Responses: ${B.n}
Fit E/J/H ${B.fit.too_easy}/${B.fit.just_right}/${B.fit.too_hard}
Understanding L/M/H ${B.understanding.low}/${B.understanding.medium}/${B.understanding.high}
Q/A total: ${B.qaTotal}
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // Wire new buttons (function initNewButtons(){ const bb = document.getElementById('bundleByText'); if (bb) bb.addEventListener('click', bundleByText); const cp = document.getElementById('comparePDF'); if (cp) cp.addEventListener('click', comparePDF); })(); // --- Logo URL persistence --- function getLogoUrl(){ try{ return localStorage.getItem('mlh.readability.logoUrl') || 'assets/logo-nusd.png'; }catch{return 'assets/logo-nusd.png'} } function setLogoUrl(url){ try{ localStorage.setItem('mlh.readability.logoUrl', url||''); }catch{} } function getLogoHTML(){ const url = getLogoUrl(); if (!url) return ''; return `logo`; } // --- DOK distribution from QA arrays --- function dokDist(subset){ const d = {1:0,2:0,3:0,4:0,5:0,6:0}; subset.forEach(r=>{ (Array.isArray(r.qa)? r.qa: []).forEach(q=>{ const lvl = Number(q && q.lvl || 0); if (lvl>=1 && lvl<=6) d[lvl]++; }); }); return d; } function dokSummaryHTML(d){ const total = Object.values(d).reduce((a,b)=>a+b,0) || 1; const pct = (n)=> Math.round(100*n/total); return `DOK — 1:${d[1]} (${pct(d[1])}%) · 2:${d[2]} (${pct(d[2])}%) · 3:${d[3]} (${pct(d[3])}%) · 4:${d[4]} (${pct(d[4])}%) · 5:${d[5]} (${pct(d[5])}%) · 6:${d[6]} (${pct(d[6])}%)`; } (function initLogoControls(){ const inp = document.getElementById('headerLogoUrl'); const btn = document.getElementById('saveLogoUrl'); if (inp){ inp.value = getLogoUrl(); } if (btn){ btn.addEventListener('click', ()=>{ setLogoUrl(inp.value||''); alert('Logo URL saved for PDFs.'); }); } })(); // --- Header styling settings --- function getLogoSize(){ return Number(localStorage.getItem('mlh.readability.logoSize')||48); } function setLogoSize(n){ localStorage.setItem('mlh.readability.logoSize', String(n)); } function getLogoAlign(){ return localStorage.getItem('mlh.readability.logoAlign')||'left'; } function setLogoAlign(v){ localStorage.setItem('mlh.readability.logoAlign', v); } function getLogoPad(){ return Number(localStorage.getItem('mlh.readability.logoPad')||8); } function setLogoPad(n){ localStorage.setItem('mlh.readability.logoPad', String(n)); } function getHeaderBg(){ return localStorage.getItem('mlh.readability.headerBg')==='1'; } function setHeaderBg(b){ localStorage.setItem('mlh.readability.headerBg', b?'1':'0'); } function getHeaderSubtitle(){ return localStorage.getItem('mlh.readability.headerSubtitle')||''; } function setHeaderSubtitle(s){ localStorage.setItem('mlh.readability.headerSubtitle', s||''); } function getHeaderHTML(titleText){ const url = getLogoUrl(); const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const jc = align==='center' ? 'center' : (align==='right' ? 'flex-end' : 'flex-start'); const bgcss = bg ? 'background:#f8fafc;' : ''; return `
logo
${titleText}
${getHeaderSubtitle()}
`; } // --- Logo preview modal --- function applyHeaderPreview(){ const el = document.getElementById('logoPreview'); if (!el) return; el.innerHTML = getHeaderHTML('Readability PDF Header'); } function openLogoPreview(){ // load controls from storage const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const sizeEl = document.getElementById('logoSize'); const padEl = document.getElementById('logoPad'); const alignEl = document.getElementById('logoAlign'); const bgEl = document.getElementById('headerBg'); if (sizeEl) sizeEl.value = size; if (padEl) padEl.value = pad; if (alignEl) alignEl.value = align; if (bgEl) bgEl.checked = bg; const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); applyHeaderPreview(); document.getElementById('logoModal').style.display='flex'; } function closeLogoPreview(){ document.getElementById('logoModal').style.display='none'; } (function hookLogoPreview(){ const btn = document.getElementById('testLogo'); const close = document.getElementById('logoClose'); const save = document.getElementById('logoSave'); if (btn) btn.addEventListener('click', openLogoPreview); if (close) close.addEventListener('click', closeLogoPreview); const restore = document.getElementById('logoRestore'); if (restore) restore.addEventListener('click', restoreHeaderDefaults); if (save) save.addEventListener('click', ()=>{ const size = Number(document.getElementById('logoSize').value||48); const pad = Number(document.getElementById('logoPad').value||8); const align = String(document.getElementById('logoAlign').value||'left'); const bg = !!document.getElementById('headerBg').checked; setLogoSize(size); setLogoPad(pad); setLogoAlign(align); setHeaderBg(bg); setHeaderSubtitle(String(document.getElementById('logoSubtitle').value||'')); applyHeaderPreview(); alert('Header style saved. PDFs will use this styling.'); }); // live update ['logoSize','logoPad','logoAlign','headerBg'].forEach(id=>{ const el = document.getElementById(id); if (!el) return; el.addEventListener('input', applyHeaderPreview); el.addEventListener('change', applyHeaderPreview); }); })(); function restoreHeaderDefaults(){ // NUSD default + sensible style defaults setLogoUrl('assets/logo-nusd.png'); setLogoSize(48); setLogoPad(8); setLogoAlign('left'); setHeaderBg(true); setHeaderSubtitle(''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); document.getElementById('logoSize').value = getLogoSize(); document.getElementById('logoPad').value = getLogoPad(); document.getElementById('logoAlign').value = getLogoAlign(); document.getElementById('headerBg').checked = getHeaderBg(); document.getElementById('logoSubtitle').value = getHeaderSubtitle(); }catch(e){} applyHeaderPreview(); } initSitesUI(); // --- Sites persistence --- function loadSites(){ try{ return JSON.parse(localStorage.getItem('mlh.readability.sites')||'[]'); }catch{return []} } function saveSites(list){ localStorage.setItem('mlh.readability.sites', JSON.stringify(list)); } const DefaultSites = [ { "name": "Novato Unified School District", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hamilton TK-8 School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hill Education Center", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Loma Verde Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lu Sutton Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lynwood Dual Immersion School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Marin Oaks High", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "NOVA Independent Study", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Novato High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Olive Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Pleasant Valley Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Rancho Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Jose Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Marin High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Ramon Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Sinaloa Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" } ]; function ensureDefaultSite(){ const list = loadSites(); if (!list.length){ list.push(...DefaultSites); list.push({ name: 'District', logoUrl: 'assets/logo-nusd.png', subtitle: '' }); saveSites(list); } } function renderSiteSelect(){ ensureDefaultSite(); const sel = document.getElementById('siteSelect'); if (!sel) return; const list = loadSites(); sel.innerHTML = ''; list.forEach((s, idx)=>{ const opt = document.createElement('option'); opt.value = String(idx); opt.textContent = s.name; sel.appendChild(opt); }); // Try to keep current index if saved const idx = Number(localStorage.getItem('mlh.readability.siteIndex')||0); sel.value = String(Math.min(idx, list.length-1)); } function applySiteToHeader(idx){ const list = loadSites(); if (!list[idx]) return; const s = list[idx]; setLogoUrl(s.logoUrl||'assets/logo-nusd.png'); setHeaderSubtitle(s.subtitle||''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); }catch{} try{ const sub = document.getElementById('logoSubtitle'); if (sub) sub.value = getHeaderSubtitle(); }catch{} applyHeaderPreview(); } function renderSiteList(){ const wrap = document.getElementById('siteList'); if(!wrap) return; const list = loadSites(); wrap.innerHTML = ''; list.forEach((s, idx)=>{ const row = document.createElement('div'); row.className = 'rowc'; row.innerHTML = `${s.name}${s.subtitle||''}`; const useBtn = document.createElement('button'); useBtn.textContent = 'Use'; useBtn.addEventListener('click', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(idx)); renderSiteSelect(); applySiteToHeader(idx); alert('Site applied to header.'); }); const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; editBtn.addEventListener('click', ()=>{ document.getElementById('siteName').value = s.name || ''; document.getElementById('siteLogo').value = s.logoUrl || ''; document.getElementById('siteSubtitle').value = s.subtitle || ''; }); const delBtn = document.createElement('button'); delBtn.textContent = 'Delete'; delBtn.addEventListener('click', ()=>{ if (!confirm('Delete site "'+s.name+'"?')) return; const cur = loadSites(); cur.splice(idx,1); saveSites(cur); renderSiteList(); renderSiteSelect(); }); row.appendChild(useBtn); row.appendChild(editBtn); row.appendChild(delBtn); wrap.appendChild(row); }); } function initSitesUI(){ ensureDefaultSite(); renderSiteSelect(); renderSiteList(); const sel = document.getElementById('siteSelect'); if (sel){ sel.addEventListener('change', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(sel.value)); applySiteToHeader(Number(sel.value)); }); } const mg = document.getElementById('manageSites'); const modal = document.getElementById('siteModal'); const close = document.getElementById('siteClose'); if (mg) mg.addEventListener('click', ()=>{ renderSiteList(); modal.style.display='flex'; }); if (close) close.addEventListener('click', ()=>{ modal.style.display='none'; }); const add = document.getElementById('siteAdd'); const name = document.getElementById('siteName'); const logo = document.getElementById('siteLogo'); const sub = document.getElementById('siteSubtitle'); const up = document.getElementById('siteLogoUpload'); if (up){ up.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ logo.value = String(r.result); }; r.readAsDataURL(f); }); } if (add){ add.addEventListener('click', ()=>{ const nm = (name.value||'').trim(); if (!nm){ alert('Enter site name'); return; } const list = loadSites(); const idx = list.findIndex(x=> (x.name||'').toLowerCase()===nm.toLowerCase()); const entry = { name: nm, logoUrl: logo.value||'assets/logo-nusd.png', subtitle: sub.value||'' }; if (idx>=0) list[idx]=entry; else list.push(entry); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Saved.'); }); } // Apply chosen site on load applySiteToHeader(Number(localStorage.getItem('mlh.readability.siteIndex')||0)); } // Also update preview inputs live when site changes function updatePreviewOnSiteChange(){ try{ const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); const urlEl = document.getElementById('headerLogoUrl'); if (urlEl) urlEl.value = getLogoUrl(); applyHeaderPreview(); }catch(e){} } function backupSites(){ const data = JSON.stringify(loadSites(), null, 2); const blob = new Blob([data], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'nusd-sites-backup.json'; a.click(); URL.revokeObjectURL(url); } function restoreSitesFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const list = JSON.parse(r.result); if (!Array.isArray(list)) throw new Error("Invalid format"); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Sites restored. Select and apply as needed.'); }catch(e){ alert('Restore failed: '+e.message); } }; r.readAsText(file); } (function hookBackupRestore(){ const b = document.getElementById('siteBackup'); if (b) b.addEventListener('click', backupSites); const rbtn = document.getElementById('siteRestore'); const rfile = document.getElementById('siteRestoreFile'); if (rbtn && rfile){ rbtn.addEventListener('click', ()=> rfile.click()); rfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesFromFile(f); }); } })(); function normalizeName(n){ return String(n||'').trim().toLowerCase(); } function restoreSitesMergeFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const incoming = JSON.parse(r.result); if (!Array.isArray(incoming)) throw new Error("Invalid format"); const current = loadSites(); const map = new Map(current.map((s, i)=> [normalizeName(s.name), {i, s}])); incoming.forEach(entry=>{ if (!entry || !entry.name) return; const key = normalizeName(entry.name); if (map.has(key)){ // Update existing entry (replace fields provided; keep missing fields from existing) const idx = map.get(key).i; const merged = Object.assign({}, current[idx], entry); current[idx] = merged; }else{ current.push(entry); } }); saveSites(current); renderSiteList(); renderSiteSelect(); alert('Merge complete. New sites added; existing sites updated by name.'); }catch(e){ alert('Merge failed: '+e.message); } }; r.readAsText(file); } (function hookMergeRestore(){ const mbtn = document.getElementById('siteRestoreMerge'); const mfile = document.getElementById('siteRestoreFileMerge'); if (mbtn && mfile){ mbtn.addEventListener('click', ()=> mfile.click()); mfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesMergeFromFile(f); }); } })(); `+ `

Sample (first 50)

`+ `${rows}
TimeIDFitUnderClassTextQ/A
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare Mode (current filter vs selected class) --- function aggregateLabel(a){ return `Resp ${a.n} · Fit E/J/H ${a.fit.too_easy}/${a.fit.just_right}/${a.fit.too_hard} · Under L/M/H ${a.understanding.low}/${a.understanding.medium}/${a.understanding.high} · Q/A ${a.qaTotal}`; } function runCompare(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const panel = document.getElementById('comparePanel'); panel.style.display = 'flex'; panel.innerHTML = '' + '

Current Filter

'+aggregateLabel(A)+'
' + '

Compare: '+cmpClass+'

'+aggregateLabel(B)+'
'; } // Wire buttons (function initFilteredExports(){ const b1 = document.getElementById('exportFilteredCSV'); const b2 = document.getElementById('exportFilteredJSON'); const b3 = document.getElementById('snapshotPDF'); const b4 = document.getElementById('runCompare'); if (b1) b1.addEventListener('click', exportFilteredCSV); if (b2) b2.addEventListener('click', exportFilteredJSON); if (b3) b3.addEventListener('click', snapshotPDF); if (b4) b4.addEventListener('click', runCompare); })(); // --- Mini bar charts (simple canvas) --- function drawMiniBar(canvasId, labels, values){ const cv = document.getElementById(canvasId); if (!cv) return; const ctx = cv.getContext('2d'); ctx.clearRect(0,0,cv.width,cv.height); const max = Math.max(1, ...values); const bw = Math.floor((cv.width - 40) / values.length); const baseY = cv.height - 16; // axes ctx.beginPath(); ctx.moveTo(30, 6); ctx.lineTo(30, baseY); ctx.lineTo(cv.width-6, baseY); ctx.stroke(); for (let i=0; i{ const btn = document.createElement('button'); btn.className = 'mlh-chip'; btn.innerHTML = `${p.name} (${p.class||'(all)'} · ${p.text||'(all)'})`; btn.addEventListener('click', ()=>{ const fc = document.getElementById('filterClass'); const ft = document.getElementById('filterText'); if (fc) fc.value = p.class || ''; if (ft) ft.value = p.text || ''; renderFilteredMetrics(); }); const del = document.createElement('button'); del.textContent = '×'; del.title = 'Delete'; del.style.marginLeft = '6px'; del.addEventListener('click', ()=>{ const confirmDel = confirm('Delete preset "'+p.name+'"?'); if(!confirmDel) return; const cur = loadPresets(); cur.splice(idx,1); savePresets(cur); renderPresetChips(); }); const wrapDiv = document.createElement('span'); wrapDiv.appendChild(btn); wrapDiv.appendChild(del); wrap.appendChild(wrapDiv); }); } function saveCurrentAsPreset(){ const name = (document.getElementById('presetName')||{}).value || ''; const cls = (document.getElementById('filterClass')||{}).value || ''; const txt = (document.getElementById('filterText')||{}).value || ''; if (!name.trim()){ alert('Name your preset.'); return; } const list = loadPresets(); list.push({ name, class: cls, text: txt }); savePresets(list); renderPresetChips(); try{ document.getElementById('presetName').value=''; }catch{} } // --- Snapshot bundle (by class) --- function uniqueClasses(arr){ const set = new Set(); arr.forEach(r=>{ if (r.classTag) set.add(r.classTag); }); return Array.from(set); } function bundleSnapshots(){ const all = getAllResponses(); const currentTxt = (document.getElementById('filterText')||{}).value || ''; const classes = uniqueClasses(all); if (!classes.length){ alert('No class tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle

Generated ${now}
Text filter: ${currentTxt||'(all)'}
`; classes.forEach(cls=>{ const subset = all.filter(r=> (r.classTag||'')===cls && (!currentTxt || String(r.textTag||'').toLowerCase().includes(currentTxt.toLowerCase()))); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.textTag||''}${(r.qa||[]).length}`).join(''); body += `

${cls}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderTextQ/A
`; }); w.document.write(`Snapshot Bundle ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } (function initEnhancedControls(){ const sp = document.getElementById('savePreset'); if (sp) sp.addEventListener('click', saveCurrentAsPreset); renderPresetChips(); // Ensure charts render when metrics change const _oldRender = renderFilteredMetrics; renderFilteredMetrics = function(){ _oldRender(); try{ const subset = applyFilters(getAllResponses()); const A = aggregateWith(subset); drawMiniBar('fitChart', ['E','J','H'], [A.fit.too_easy, A.fit.just_right, A.fit.too_hard]); drawMiniBar('underChart', ['L','M','H'], [A.understanding.low, A.understanding.medium, A.understanding.high]); }catch(e){} }; renderFilteredMetrics(); const bs = document.getElementById('bundleSnapshots'); if (bs) bs.addEventListener('click', bundleSnapshots); })(); // --- Snapshot bundle (by text) --- function uniqueTexts(arr){ const set = new Set(); arr.forEach(r=>{ if (r.textTag) set.add(r.textTag); }); return Array.from(set); } function bundleByText(){ const all = getAllResponses(); const currentClass = (document.getElementById('filterClass')||{}).value || ''; const texts = uniqueTexts(all); if (!texts.length){ alert('No text tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle (by Text)

Generated ${now}
Class filter: ${currentClass||'(all)'}
`; texts.forEach(txt=>{ const subset = all.filter(r=> (!currentClass || (r.classTag||'')===currentClass) && (r.textTag||'')===txt); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.classTag||''}${(r.qa||[]).length}`).join(''); body += `

${txt}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderClassQ/A
`; }); w.document.write(`Snapshot Bundle (by Text) ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare PDF export --- function comparePDF(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const w = window.open('', '_blank', 'width=800,height=600'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; w.document.write(`Compare PDF `+ `

Comparison Report

`+ `

Current Filter

Responses: ${A.n}
Fit E/J/H ${A.fit.too_easy}/${A.fit.just_right}/${A.fit.too_hard}
Understanding L/M/H ${A.understanding.low}/${A.understanding.medium}/${A.understanding.high}
Q/A total: ${A.qaTotal}
`+ `

Compare: ${cmpClass}

Responses: ${B.n}
Fit E/J/H ${B.fit.too_easy}/${B.fit.just_right}/${B.fit.too_hard}
Understanding L/M/H ${B.understanding.low}/${B.understanding.medium}/${B.understanding.high}
Q/A total: ${B.qaTotal}
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // Wire new buttons (function initNewButtons(){ const bb = document.getElementById('bundleByText'); if (bb) bb.addEventListener('click', bundleByText); const cp = document.getElementById('comparePDF'); if (cp) cp.addEventListener('click', comparePDF); })(); // --- Logo URL persistence --- function getLogoUrl(){ try{ return localStorage.getItem('mlh.readability.logoUrl') || 'assets/logo-nusd.png'; }catch{return 'assets/logo-nusd.png'} } function setLogoUrl(url){ try{ localStorage.setItem('mlh.readability.logoUrl', url||''); }catch{} } function getLogoHTML(){ const url = getLogoUrl(); if (!url) return ''; return `logo`; } // --- DOK distribution from QA arrays --- function dokDist(subset){ const d = {1:0,2:0,3:0,4:0,5:0,6:0}; subset.forEach(r=>{ (Array.isArray(r.qa)? r.qa: []).forEach(q=>{ const lvl = Number(q && q.lvl || 0); if (lvl>=1 && lvl<=6) d[lvl]++; }); }); return d; } function dokSummaryHTML(d){ const total = Object.values(d).reduce((a,b)=>a+b,0) || 1; const pct = (n)=> Math.round(100*n/total); return `DOK — 1:${d[1]} (${pct(d[1])}%) · 2:${d[2]} (${pct(d[2])}%) · 3:${d[3]} (${pct(d[3])}%) · 4:${d[4]} (${pct(d[4])}%) · 5:${d[5]} (${pct(d[5])}%) · 6:${d[6]} (${pct(d[6])}%)`; } (function initLogoControls(){ const inp = document.getElementById('headerLogoUrl'); const btn = document.getElementById('saveLogoUrl'); if (inp){ inp.value = getLogoUrl(); } if (btn){ btn.addEventListener('click', ()=>{ setLogoUrl(inp.value||''); alert('Logo URL saved for PDFs.'); }); } })(); // --- Header styling settings --- function getLogoSize(){ return Number(localStorage.getItem('mlh.readability.logoSize')||48); } function setLogoSize(n){ localStorage.setItem('mlh.readability.logoSize', String(n)); } function getLogoAlign(){ return localStorage.getItem('mlh.readability.logoAlign')||'left'; } function setLogoAlign(v){ localStorage.setItem('mlh.readability.logoAlign', v); } function getLogoPad(){ return Number(localStorage.getItem('mlh.readability.logoPad')||8); } function setLogoPad(n){ localStorage.setItem('mlh.readability.logoPad', String(n)); } function getHeaderBg(){ return localStorage.getItem('mlh.readability.headerBg')==='1'; } function setHeaderBg(b){ localStorage.setItem('mlh.readability.headerBg', b?'1':'0'); } function getHeaderSubtitle(){ return localStorage.getItem('mlh.readability.headerSubtitle')||''; } function setHeaderSubtitle(s){ localStorage.setItem('mlh.readability.headerSubtitle', s||''); } function getHeaderHTML(titleText){ const url = getLogoUrl(); const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const jc = align==='center' ? 'center' : (align==='right' ? 'flex-end' : 'flex-start'); const bgcss = bg ? 'background:#f8fafc;' : ''; return `
logo
${titleText}
${getHeaderSubtitle()}
`; } // --- Logo preview modal --- function applyHeaderPreview(){ const el = document.getElementById('logoPreview'); if (!el) return; el.innerHTML = getHeaderHTML('Readability PDF Header'); } function openLogoPreview(){ // load controls from storage const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const sizeEl = document.getElementById('logoSize'); const padEl = document.getElementById('logoPad'); const alignEl = document.getElementById('logoAlign'); const bgEl = document.getElementById('headerBg'); if (sizeEl) sizeEl.value = size; if (padEl) padEl.value = pad; if (alignEl) alignEl.value = align; if (bgEl) bgEl.checked = bg; const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); applyHeaderPreview(); document.getElementById('logoModal').style.display='flex'; } function closeLogoPreview(){ document.getElementById('logoModal').style.display='none'; } (function hookLogoPreview(){ const btn = document.getElementById('testLogo'); const close = document.getElementById('logoClose'); const save = document.getElementById('logoSave'); if (btn) btn.addEventListener('click', openLogoPreview); if (close) close.addEventListener('click', closeLogoPreview); const restore = document.getElementById('logoRestore'); if (restore) restore.addEventListener('click', restoreHeaderDefaults); if (save) save.addEventListener('click', ()=>{ const size = Number(document.getElementById('logoSize').value||48); const pad = Number(document.getElementById('logoPad').value||8); const align = String(document.getElementById('logoAlign').value||'left'); const bg = !!document.getElementById('headerBg').checked; setLogoSize(size); setLogoPad(pad); setLogoAlign(align); setHeaderBg(bg); setHeaderSubtitle(String(document.getElementById('logoSubtitle').value||'')); applyHeaderPreview(); alert('Header style saved. PDFs will use this styling.'); }); // live update ['logoSize','logoPad','logoAlign','headerBg'].forEach(id=>{ const el = document.getElementById(id); if (!el) return; el.addEventListener('input', applyHeaderPreview); el.addEventListener('change', applyHeaderPreview); }); })(); function restoreHeaderDefaults(){ // NUSD default + sensible style defaults setLogoUrl('assets/logo-nusd.png'); setLogoSize(48); setLogoPad(8); setLogoAlign('left'); setHeaderBg(true); setHeaderSubtitle(''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); document.getElementById('logoSize').value = getLogoSize(); document.getElementById('logoPad').value = getLogoPad(); document.getElementById('logoAlign').value = getLogoAlign(); document.getElementById('headerBg').checked = getHeaderBg(); document.getElementById('logoSubtitle').value = getHeaderSubtitle(); }catch(e){} applyHeaderPreview(); } initSitesUI(); // --- Sites persistence --- function loadSites(){ try{ return JSON.parse(localStorage.getItem('mlh.readability.sites')||'[]'); }catch{return []} } function saveSites(list){ localStorage.setItem('mlh.readability.sites', JSON.stringify(list)); } const DefaultSites = [ { "name": "Novato Unified School District", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hamilton TK-8 School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hill Education Center", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Loma Verde Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lu Sutton Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lynwood Dual Immersion School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Marin Oaks High", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "NOVA Independent Study", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Novato High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Olive Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Pleasant Valley Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Rancho Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Jose Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Marin High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Ramon Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Sinaloa Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" } ]; function ensureDefaultSite(){ const list = loadSites(); if (!list.length){ list.push(...DefaultSites); list.push({ name: 'District', logoUrl: 'assets/logo-nusd.png', subtitle: '' }); saveSites(list); } } function renderSiteSelect(){ ensureDefaultSite(); const sel = document.getElementById('siteSelect'); if (!sel) return; const list = loadSites(); sel.innerHTML = ''; list.forEach((s, idx)=>{ const opt = document.createElement('option'); opt.value = String(idx); opt.textContent = s.name; sel.appendChild(opt); }); // Try to keep current index if saved const idx = Number(localStorage.getItem('mlh.readability.siteIndex')||0); sel.value = String(Math.min(idx, list.length-1)); } function applySiteToHeader(idx){ const list = loadSites(); if (!list[idx]) return; const s = list[idx]; setLogoUrl(s.logoUrl||'assets/logo-nusd.png'); setHeaderSubtitle(s.subtitle||''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); }catch{} try{ const sub = document.getElementById('logoSubtitle'); if (sub) sub.value = getHeaderSubtitle(); }catch{} applyHeaderPreview(); } function renderSiteList(){ const wrap = document.getElementById('siteList'); if(!wrap) return; const list = loadSites(); wrap.innerHTML = ''; list.forEach((s, idx)=>{ const row = document.createElement('div'); row.className = 'rowc'; row.innerHTML = `${s.name}${s.subtitle||''}`; const useBtn = document.createElement('button'); useBtn.textContent = 'Use'; useBtn.addEventListener('click', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(idx)); renderSiteSelect(); applySiteToHeader(idx); alert('Site applied to header.'); }); const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; editBtn.addEventListener('click', ()=>{ document.getElementById('siteName').value = s.name || ''; document.getElementById('siteLogo').value = s.logoUrl || ''; document.getElementById('siteSubtitle').value = s.subtitle || ''; }); const delBtn = document.createElement('button'); delBtn.textContent = 'Delete'; delBtn.addEventListener('click', ()=>{ if (!confirm('Delete site "'+s.name+'"?')) return; const cur = loadSites(); cur.splice(idx,1); saveSites(cur); renderSiteList(); renderSiteSelect(); }); row.appendChild(useBtn); row.appendChild(editBtn); row.appendChild(delBtn); wrap.appendChild(row); }); } function initSitesUI(){ ensureDefaultSite(); renderSiteSelect(); renderSiteList(); const sel = document.getElementById('siteSelect'); if (sel){ sel.addEventListener('change', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(sel.value)); applySiteToHeader(Number(sel.value)); }); } const mg = document.getElementById('manageSites'); const modal = document.getElementById('siteModal'); const close = document.getElementById('siteClose'); if (mg) mg.addEventListener('click', ()=>{ renderSiteList(); modal.style.display='flex'; }); if (close) close.addEventListener('click', ()=>{ modal.style.display='none'; }); const add = document.getElementById('siteAdd'); const name = document.getElementById('siteName'); const logo = document.getElementById('siteLogo'); const sub = document.getElementById('siteSubtitle'); const up = document.getElementById('siteLogoUpload'); if (up){ up.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ logo.value = String(r.result); }; r.readAsDataURL(f); }); } if (add){ add.addEventListener('click', ()=>{ const nm = (name.value||'').trim(); if (!nm){ alert('Enter site name'); return; } const list = loadSites(); const idx = list.findIndex(x=> (x.name||'').toLowerCase()===nm.toLowerCase()); const entry = { name: nm, logoUrl: logo.value||'assets/logo-nusd.png', subtitle: sub.value||'' }; if (idx>=0) list[idx]=entry; else list.push(entry); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Saved.'); }); } // Apply chosen site on load applySiteToHeader(Number(localStorage.getItem('mlh.readability.siteIndex')||0)); } // Also update preview inputs live when site changes function updatePreviewOnSiteChange(){ try{ const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); const urlEl = document.getElementById('headerLogoUrl'); if (urlEl) urlEl.value = getLogoUrl(); applyHeaderPreview(); }catch(e){} } function backupSites(){ const data = JSON.stringify(loadSites(), null, 2); const blob = new Blob([data], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'nusd-sites-backup.json'; a.click(); URL.revokeObjectURL(url); } function restoreSitesFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const list = JSON.parse(r.result); if (!Array.isArray(list)) throw new Error("Invalid format"); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Sites restored. Select and apply as needed.'); }catch(e){ alert('Restore failed: '+e.message); } }; r.readAsText(file); } (function hookBackupRestore(){ const b = document.getElementById('siteBackup'); if (b) b.addEventListener('click', backupSites); const rbtn = document.getElementById('siteRestore'); const rfile = document.getElementById('siteRestoreFile'); if (rbtn && rfile){ rbtn.addEventListener('click', ()=> rfile.click()); rfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesFromFile(f); }); } })(); function normalizeName(n){ return String(n||'').trim().toLowerCase(); } function restoreSitesMergeFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const incoming = JSON.parse(r.result); if (!Array.isArray(incoming)) throw new Error("Invalid format"); const current = loadSites(); const map = new Map(current.map((s, i)=> [normalizeName(s.name), {i, s}])); incoming.forEach(entry=>{ if (!entry || !entry.name) return; const key = normalizeName(entry.name); if (map.has(key)){ // Update existing entry (replace fields provided; keep missing fields from existing) const idx = map.get(key).i; const merged = Object.assign({}, current[idx], entry); current[idx] = merged; }else{ current.push(entry); } }); saveSites(current); renderSiteList(); renderSiteSelect(); alert('Merge complete. New sites added; existing sites updated by name.'); }catch(e){ alert('Merge failed: '+e.message); } }; r.readAsText(file); } (function hookMergeRestore(){ const mbtn = document.getElementById('siteRestoreMerge'); const mfile = document.getElementById('siteRestoreFileMerge'); if (mbtn && mfile){ mbtn.addEventListener('click', ()=> mfile.click()); mfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesMergeFromFile(f); }); } })(); `+ `

Sample (first 50)

`+ `${rows}
TimeIDFitUnderClassTextQ/A
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare Mode (current filter vs selected class) --- function aggregateLabel(a){ return `Resp ${a.n} · Fit E/J/H ${a.fit.too_easy}/${a.fit.just_right}/${a.fit.too_hard} · Under L/M/H ${a.understanding.low}/${a.understanding.medium}/${a.understanding.high} · Q/A ${a.qaTotal}`; } function runCompare(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const panel = document.getElementById('comparePanel'); panel.style.display = 'flex'; panel.innerHTML = '' + '

Current Filter

'+aggregateLabel(A)+'
' + '

Compare: '+cmpClass+'

'+aggregateLabel(B)+'
'; } // Wire buttons (function initFilteredExports(){ const b1 = document.getElementById('exportFilteredCSV'); const b2 = document.getElementById('exportFilteredJSON'); const b3 = document.getElementById('snapshotPDF'); const b4 = document.getElementById('runCompare'); if (b1) b1.addEventListener('click', exportFilteredCSV); if (b2) b2.addEventListener('click', exportFilteredJSON); if (b3) b3.addEventListener('click', snapshotPDF); if (b4) b4.addEventListener('click', runCompare); })(); // --- Mini bar charts (simple canvas) --- function drawMiniBar(canvasId, labels, values){ const cv = document.getElementById(canvasId); if (!cv) return; const ctx = cv.getContext('2d'); ctx.clearRect(0,0,cv.width,cv.height); const max = Math.max(1, ...values); const bw = Math.floor((cv.width - 40) / values.length); const baseY = cv.height - 16; // axes ctx.beginPath(); ctx.moveTo(30, 6); ctx.lineTo(30, baseY); ctx.lineTo(cv.width-6, baseY); ctx.stroke(); for (let i=0; i{ const btn = document.createElement('button'); btn.className = 'mlh-chip'; btn.innerHTML = `${p.name} (${p.class||'(all)'} · ${p.text||'(all)'})`; btn.addEventListener('click', ()=>{ const fc = document.getElementById('filterClass'); const ft = document.getElementById('filterText'); if (fc) fc.value = p.class || ''; if (ft) ft.value = p.text || ''; renderFilteredMetrics(); }); const del = document.createElement('button'); del.textContent = '×'; del.title = 'Delete'; del.style.marginLeft = '6px'; del.addEventListener('click', ()=>{ const confirmDel = confirm('Delete preset "'+p.name+'"?'); if(!confirmDel) return; const cur = loadPresets(); cur.splice(idx,1); savePresets(cur); renderPresetChips(); }); const wrapDiv = document.createElement('span'); wrapDiv.appendChild(btn); wrapDiv.appendChild(del); wrap.appendChild(wrapDiv); }); } function saveCurrentAsPreset(){ const name = (document.getElementById('presetName')||{}).value || ''; const cls = (document.getElementById('filterClass')||{}).value || ''; const txt = (document.getElementById('filterText')||{}).value || ''; if (!name.trim()){ alert('Name your preset.'); return; } const list = loadPresets(); list.push({ name, class: cls, text: txt }); savePresets(list); renderPresetChips(); try{ document.getElementById('presetName').value=''; }catch{} } // --- Snapshot bundle (by class) --- function uniqueClasses(arr){ const set = new Set(); arr.forEach(r=>{ if (r.classTag) set.add(r.classTag); }); return Array.from(set); } function bundleSnapshots(){ const all = getAllResponses(); const currentTxt = (document.getElementById('filterText')||{}).value || ''; const classes = uniqueClasses(all); if (!classes.length){ alert('No class tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle

Generated ${now}
Text filter: ${currentTxt||'(all)'}
`; classes.forEach(cls=>{ const subset = all.filter(r=> (r.classTag||'')===cls && (!currentTxt || String(r.textTag||'').toLowerCase().includes(currentTxt.toLowerCase()))); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.textTag||''}${(r.qa||[]).length}`).join(''); body += `

${cls}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderTextQ/A
`; }); w.document.write(`Snapshot Bundle ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } (function initEnhancedControls(){ const sp = document.getElementById('savePreset'); if (sp) sp.addEventListener('click', saveCurrentAsPreset); renderPresetChips(); // Ensure charts render when metrics change const _oldRender = renderFilteredMetrics; renderFilteredMetrics = function(){ _oldRender(); try{ const subset = applyFilters(getAllResponses()); const A = aggregateWith(subset); drawMiniBar('fitChart', ['E','J','H'], [A.fit.too_easy, A.fit.just_right, A.fit.too_hard]); drawMiniBar('underChart', ['L','M','H'], [A.understanding.low, A.understanding.medium, A.understanding.high]); }catch(e){} }; renderFilteredMetrics(); const bs = document.getElementById('bundleSnapshots'); if (bs) bs.addEventListener('click', bundleSnapshots); })(); // --- Snapshot bundle (by text) --- function uniqueTexts(arr){ const set = new Set(); arr.forEach(r=>{ if (r.textTag) set.add(r.textTag); }); return Array.from(set); } function bundleByText(){ const all = getAllResponses(); const currentClass = (document.getElementById('filterClass')||{}).value || ''; const texts = uniqueTexts(all); if (!texts.length){ alert('No text tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle (by Text)

Generated ${now}
Class filter: ${currentClass||'(all)'}
`; texts.forEach(txt=>{ const subset = all.filter(r=> (!currentClass || (r.classTag||'')===currentClass) && (r.textTag||'')===txt); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.classTag||''}${(r.qa||[]).length}`).join(''); body += `

${txt}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderClassQ/A
`; }); w.document.write(`Snapshot Bundle (by Text) ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare PDF export --- function comparePDF(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const w = window.open('', '_blank', 'width=800,height=600'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; w.document.write(`Compare PDF `+ `

Comparison Report

`+ `

Current Filter

Responses: ${A.n}
Fit E/J/H ${A.fit.too_easy}/${A.fit.just_right}/${A.fit.too_hard}
Understanding L/M/H ${A.understanding.low}/${A.understanding.medium}/${A.understanding.high}
Q/A total: ${A.qaTotal}
`+ `

Compare: ${cmpClass}

Responses: ${B.n}
Fit E/J/H ${B.fit.too_easy}/${B.fit.just_right}/${B.fit.too_hard}
Understanding L/M/H ${B.understanding.low}/${B.understanding.medium}/${B.understanding.high}
Q/A total: ${B.qaTotal}
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // Wire new buttons (function initNewButtons(){ const bb = document.getElementById('bundleByText'); if (bb) bb.addEventListener('click', bundleByText); const cp = document.getElementById('comparePDF'); if (cp) cp.addEventListener('click', comparePDF); })(); // --- Logo URL persistence --- function getLogoUrl(){ try{ return localStorage.getItem('mlh.readability.logoUrl') || 'assets/logo-nusd.png'; }catch{return 'assets/logo-nusd.png'} } function setLogoUrl(url){ try{ localStorage.setItem('mlh.readability.logoUrl', url||''); }catch{} } function getLogoHTML(){ const url = getLogoUrl(); if (!url) return ''; return `logo`; } // --- DOK distribution from QA arrays --- function dokDist(subset){ const d = {1:0,2:0,3:0,4:0,5:0,6:0}; subset.forEach(r=>{ (Array.isArray(r.qa)? r.qa: []).forEach(q=>{ const lvl = Number(q && q.lvl || 0); if (lvl>=1 && lvl<=6) d[lvl]++; }); }); return d; } function dokSummaryHTML(d){ const total = Object.values(d).reduce((a,b)=>a+b,0) || 1; const pct = (n)=> Math.round(100*n/total); return `DOK — 1:${d[1]} (${pct(d[1])}%) · 2:${d[2]} (${pct(d[2])}%) · 3:${d[3]} (${pct(d[3])}%) · 4:${d[4]} (${pct(d[4])}%) · 5:${d[5]} (${pct(d[5])}%) · 6:${d[6]} (${pct(d[6])}%)`; } (function initLogoControls(){ const inp = document.getElementById('headerLogoUrl'); const btn = document.getElementById('saveLogoUrl'); if (inp){ inp.value = getLogoUrl(); } if (btn){ btn.addEventListener('click', ()=>{ setLogoUrl(inp.value||''); alert('Logo URL saved for PDFs.'); }); } })(); // --- Header styling settings --- function getLogoSize(){ return Number(localStorage.getItem('mlh.readability.logoSize')||48); } function setLogoSize(n){ localStorage.setItem('mlh.readability.logoSize', String(n)); } function getLogoAlign(){ return localStorage.getItem('mlh.readability.logoAlign')||'left'; } function setLogoAlign(v){ localStorage.setItem('mlh.readability.logoAlign', v); } function getLogoPad(){ return Number(localStorage.getItem('mlh.readability.logoPad')||8); } function setLogoPad(n){ localStorage.setItem('mlh.readability.logoPad', String(n)); } function getHeaderBg(){ return localStorage.getItem('mlh.readability.headerBg')==='1'; } function setHeaderBg(b){ localStorage.setItem('mlh.readability.headerBg', b?'1':'0'); } function getHeaderSubtitle(){ return localStorage.getItem('mlh.readability.headerSubtitle')||''; } function setHeaderSubtitle(s){ localStorage.setItem('mlh.readability.headerSubtitle', s||''); } function getHeaderHTML(titleText){ const url = getLogoUrl(); const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const jc = align==='center' ? 'center' : (align==='right' ? 'flex-end' : 'flex-start'); const bgcss = bg ? 'background:#f8fafc;' : ''; return `
logo
${titleText}
${getHeaderSubtitle()}
`; } // --- Logo preview modal --- function applyHeaderPreview(){ const el = document.getElementById('logoPreview'); if (!el) return; el.innerHTML = getHeaderHTML('Readability PDF Header'); } function openLogoPreview(){ // load controls from storage const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const sizeEl = document.getElementById('logoSize'); const padEl = document.getElementById('logoPad'); const alignEl = document.getElementById('logoAlign'); const bgEl = document.getElementById('headerBg'); if (sizeEl) sizeEl.value = size; if (padEl) padEl.value = pad; if (alignEl) alignEl.value = align; if (bgEl) bgEl.checked = bg; const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); applyHeaderPreview(); document.getElementById('logoModal').style.display='flex'; } function closeLogoPreview(){ document.getElementById('logoModal').style.display='none'; } (function hookLogoPreview(){ const btn = document.getElementById('testLogo'); const close = document.getElementById('logoClose'); const save = document.getElementById('logoSave'); if (btn) btn.addEventListener('click', openLogoPreview); if (close) close.addEventListener('click', closeLogoPreview); const restore = document.getElementById('logoRestore'); if (restore) restore.addEventListener('click', restoreHeaderDefaults); if (save) save.addEventListener('click', ()=>{ const size = Number(document.getElementById('logoSize').value||48); const pad = Number(document.getElementById('logoPad').value||8); const align = String(document.getElementById('logoAlign').value||'left'); const bg = !!document.getElementById('headerBg').checked; setLogoSize(size); setLogoPad(pad); setLogoAlign(align); setHeaderBg(bg); setHeaderSubtitle(String(document.getElementById('logoSubtitle').value||'')); applyHeaderPreview(); alert('Header style saved. PDFs will use this styling.'); }); // live update ['logoSize','logoPad','logoAlign','headerBg'].forEach(id=>{ const el = document.getElementById(id); if (!el) return; el.addEventListener('input', applyHeaderPreview); el.addEventListener('change', applyHeaderPreview); }); })(); function restoreHeaderDefaults(){ // NUSD default + sensible style defaults setLogoUrl('assets/logo-nusd.png'); setLogoSize(48); setLogoPad(8); setLogoAlign('left'); setHeaderBg(true); setHeaderSubtitle(''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); document.getElementById('logoSize').value = getLogoSize(); document.getElementById('logoPad').value = getLogoPad(); document.getElementById('logoAlign').value = getLogoAlign(); document.getElementById('headerBg').checked = getHeaderBg(); document.getElementById('logoSubtitle').value = getHeaderSubtitle(); }catch(e){} applyHeaderPreview(); } initSitesUI(); // --- Sites persistence --- function loadSites(){ try{ return JSON.parse(localStorage.getItem('mlh.readability.sites')||'[]'); }catch{return []} } function saveSites(list){ localStorage.setItem('mlh.readability.sites', JSON.stringify(list)); } const DefaultSites = [ { "name": "Novato Unified School District", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hamilton TK-8 School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hill Education Center", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Loma Verde Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lu Sutton Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lynwood Dual Immersion School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Marin Oaks High", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "NOVA Independent Study", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Novato High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Olive Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Pleasant Valley Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Rancho Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Jose Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Marin High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Ramon Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Sinaloa Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" } ]; function ensureDefaultSite(){ const list = loadSites(); if (!list.length){ list.push(...DefaultSites); list.push({ name: 'District', logoUrl: 'assets/logo-nusd.png', subtitle: '' }); saveSites(list); } } function renderSiteSelect(){ ensureDefaultSite(); const sel = document.getElementById('siteSelect'); if (!sel) return; const list = loadSites(); sel.innerHTML = ''; list.forEach((s, idx)=>{ const opt = document.createElement('option'); opt.value = String(idx); opt.textContent = s.name; sel.appendChild(opt); }); // Try to keep current index if saved const idx = Number(localStorage.getItem('mlh.readability.siteIndex')||0); sel.value = String(Math.min(idx, list.length-1)); } function applySiteToHeader(idx){ const list = loadSites(); if (!list[idx]) return; const s = list[idx]; setLogoUrl(s.logoUrl||'assets/logo-nusd.png'); setHeaderSubtitle(s.subtitle||''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); }catch{} try{ const sub = document.getElementById('logoSubtitle'); if (sub) sub.value = getHeaderSubtitle(); }catch{} applyHeaderPreview(); } function renderSiteList(){ const wrap = document.getElementById('siteList'); if(!wrap) return; const list = loadSites(); wrap.innerHTML = ''; list.forEach((s, idx)=>{ const row = document.createElement('div'); row.className = 'rowc'; row.innerHTML = `${s.name}${s.subtitle||''}`; const useBtn = document.createElement('button'); useBtn.textContent = 'Use'; useBtn.addEventListener('click', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(idx)); renderSiteSelect(); applySiteToHeader(idx); alert('Site applied to header.'); }); const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; editBtn.addEventListener('click', ()=>{ document.getElementById('siteName').value = s.name || ''; document.getElementById('siteLogo').value = s.logoUrl || ''; document.getElementById('siteSubtitle').value = s.subtitle || ''; }); const delBtn = document.createElement('button'); delBtn.textContent = 'Delete'; delBtn.addEventListener('click', ()=>{ if (!confirm('Delete site "'+s.name+'"?')) return; const cur = loadSites(); cur.splice(idx,1); saveSites(cur); renderSiteList(); renderSiteSelect(); }); row.appendChild(useBtn); row.appendChild(editBtn); row.appendChild(delBtn); wrap.appendChild(row); }); } function initSitesUI(){ ensureDefaultSite(); renderSiteSelect(); renderSiteList(); const sel = document.getElementById('siteSelect'); if (sel){ sel.addEventListener('change', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(sel.value)); applySiteToHeader(Number(sel.value)); }); } const mg = document.getElementById('manageSites'); const modal = document.getElementById('siteModal'); const close = document.getElementById('siteClose'); if (mg) mg.addEventListener('click', ()=>{ renderSiteList(); modal.style.display='flex'; }); if (close) close.addEventListener('click', ()=>{ modal.style.display='none'; }); const add = document.getElementById('siteAdd'); const name = document.getElementById('siteName'); const logo = document.getElementById('siteLogo'); const sub = document.getElementById('siteSubtitle'); const up = document.getElementById('siteLogoUpload'); if (up){ up.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ logo.value = String(r.result); }; r.readAsDataURL(f); }); } if (add){ add.addEventListener('click', ()=>{ const nm = (name.value||'').trim(); if (!nm){ alert('Enter site name'); return; } const list = loadSites(); const idx = list.findIndex(x=> (x.name||'').toLowerCase()===nm.toLowerCase()); const entry = { name: nm, logoUrl: logo.value||'assets/logo-nusd.png', subtitle: sub.value||'' }; if (idx>=0) list[idx]=entry; else list.push(entry); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Saved.'); }); } // Apply chosen site on load applySiteToHeader(Number(localStorage.getItem('mlh.readability.siteIndex')||0)); } // Also update preview inputs live when site changes function updatePreviewOnSiteChange(){ try{ const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); const urlEl = document.getElementById('headerLogoUrl'); if (urlEl) urlEl.value = getLogoUrl(); applyHeaderPreview(); }catch(e){} } function backupSites(){ const data = JSON.stringify(loadSites(), null, 2); const blob = new Blob([data], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'nusd-sites-backup.json'; a.click(); URL.revokeObjectURL(url); } function restoreSitesFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const list = JSON.parse(r.result); if (!Array.isArray(list)) throw new Error("Invalid format"); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Sites restored. Select and apply as needed.'); }catch(e){ alert('Restore failed: '+e.message); } }; r.readAsText(file); } (function hookBackupRestore(){ const b = document.getElementById('siteBackup'); if (b) b.addEventListener('click', backupSites); const rbtn = document.getElementById('siteRestore'); const rfile = document.getElementById('siteRestoreFile'); if (rbtn && rfile){ rbtn.addEventListener('click', ()=> rfile.click()); rfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesFromFile(f); }); } })(); function normalizeName(n){ return String(n||'').trim().toLowerCase(); } function restoreSitesMergeFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const incoming = JSON.parse(r.result); if (!Array.isArray(incoming)) throw new Error("Invalid format"); const current = loadSites(); const map = new Map(current.map((s, i)=> [normalizeName(s.name), {i, s}])); incoming.forEach(entry=>{ if (!entry || !entry.name) return; const key = normalizeName(entry.name); if (map.has(key)){ // Update existing entry (replace fields provided; keep missing fields from existing) const idx = map.get(key).i; const merged = Object.assign({}, current[idx], entry); current[idx] = merged; }else{ current.push(entry); } }); saveSites(current); renderSiteList(); renderSiteSelect(); alert('Merge complete. New sites added; existing sites updated by name.'); }catch(e){ alert('Merge failed: '+e.message); } }; r.readAsText(file); } (function hookMergeRestore(){ const mbtn = document.getElementById('siteRestoreMerge'); const mfile = document.getElementById('siteRestoreFileMerge'); if (mbtn && mfile){ mbtn.addEventListener('click', ()=> mfile.click()); mfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesMergeFromFile(f); }); } })();

Live Collect

Drag JSON/text responses here or paste them to auto-aggregate.

Open Live Collect Inbox →
`+ `

Sample (first 50)

`+ `${rows}
TimeIDFitUnderClassTextQ/A
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare Mode (current filter vs selected class) --- function aggregateLabel(a){ return `Resp ${a.n} · Fit E/J/H ${a.fit.too_easy}/${a.fit.just_right}/${a.fit.too_hard} · Under L/M/H ${a.understanding.low}/${a.understanding.medium}/${a.understanding.high} · Q/A ${a.qaTotal}`; } function runCompare(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const panel = document.getElementById('comparePanel'); panel.style.display = 'flex'; panel.innerHTML = '' + '

Current Filter

'+aggregateLabel(A)+'
' + '

Compare: '+cmpClass+'

'+aggregateLabel(B)+'
'; } // Wire buttons (function initFilteredExports(){ const b1 = document.getElementById('exportFilteredCSV'); const b2 = document.getElementById('exportFilteredJSON'); const b3 = document.getElementById('snapshotPDF'); const b4 = document.getElementById('runCompare'); if (b1) b1.addEventListener('click', exportFilteredCSV); if (b2) b2.addEventListener('click', exportFilteredJSON); if (b3) b3.addEventListener('click', snapshotPDF); if (b4) b4.addEventListener('click', runCompare); })(); // --- Mini bar charts (simple canvas) --- function drawMiniBar(canvasId, labels, values){ const cv = document.getElementById(canvasId); if (!cv) return; const ctx = cv.getContext('2d'); ctx.clearRect(0,0,cv.width,cv.height); const max = Math.max(1, ...values); const bw = Math.floor((cv.width - 40) / values.length); const baseY = cv.height - 16; // axes ctx.beginPath(); ctx.moveTo(30, 6); ctx.lineTo(30, baseY); ctx.lineTo(cv.width-6, baseY); ctx.stroke(); for (let i=0; i{ const btn = document.createElement('button'); btn.className = 'mlh-chip'; btn.innerHTML = `${p.name} (${p.class||'(all)'} · ${p.text||'(all)'})`; btn.addEventListener('click', ()=>{ const fc = document.getElementById('filterClass'); const ft = document.getElementById('filterText'); if (fc) fc.value = p.class || ''; if (ft) ft.value = p.text || ''; renderFilteredMetrics(); }); const del = document.createElement('button'); del.textContent = '×'; del.title = 'Delete'; del.style.marginLeft = '6px'; del.addEventListener('click', ()=>{ const confirmDel = confirm('Delete preset "'+p.name+'"?'); if(!confirmDel) return; const cur = loadPresets(); cur.splice(idx,1); savePresets(cur); renderPresetChips(); }); const wrapDiv = document.createElement('span'); wrapDiv.appendChild(btn); wrapDiv.appendChild(del); wrap.appendChild(wrapDiv); }); } function saveCurrentAsPreset(){ const name = (document.getElementById('presetName')||{}).value || ''; const cls = (document.getElementById('filterClass')||{}).value || ''; const txt = (document.getElementById('filterText')||{}).value || ''; if (!name.trim()){ alert('Name your preset.'); return; } const list = loadPresets(); list.push({ name, class: cls, text: txt }); savePresets(list); renderPresetChips(); try{ document.getElementById('presetName').value=''; }catch{} } // --- Snapshot bundle (by class) --- function uniqueClasses(arr){ const set = new Set(); arr.forEach(r=>{ if (r.classTag) set.add(r.classTag); }); return Array.from(set); } function bundleSnapshots(){ const all = getAllResponses(); const currentTxt = (document.getElementById('filterText')||{}).value || ''; const classes = uniqueClasses(all); if (!classes.length){ alert('No class tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle

Generated ${now}
Text filter: ${currentTxt||'(all)'}
`; classes.forEach(cls=>{ const subset = all.filter(r=> (r.classTag||'')===cls && (!currentTxt || String(r.textTag||'').toLowerCase().includes(currentTxt.toLowerCase()))); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.textTag||''}${(r.qa||[]).length}`).join(''); body += `

${cls}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderTextQ/A
`; }); w.document.write(`Snapshot Bundle ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } (function initEnhancedControls(){ const sp = document.getElementById('savePreset'); if (sp) sp.addEventListener('click', saveCurrentAsPreset); renderPresetChips(); // Ensure charts render when metrics change const _oldRender = renderFilteredMetrics; renderFilteredMetrics = function(){ _oldRender(); try{ const subset = applyFilters(getAllResponses()); const A = aggregateWith(subset); drawMiniBar('fitChart', ['E','J','H'], [A.fit.too_easy, A.fit.just_right, A.fit.too_hard]); drawMiniBar('underChart', ['L','M','H'], [A.understanding.low, A.understanding.medium, A.understanding.high]); }catch(e){} }; renderFilteredMetrics(); const bs = document.getElementById('bundleSnapshots'); if (bs) bs.addEventListener('click', bundleSnapshots); })(); // --- Snapshot bundle (by text) --- function uniqueTexts(arr){ const set = new Set(); arr.forEach(r=>{ if (r.textTag) set.add(r.textTag); }); return Array.from(set); } function bundleByText(){ const all = getAllResponses(); const currentClass = (document.getElementById('filterClass')||{}).value || ''; const texts = uniqueTexts(all); if (!texts.length){ alert('No text tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle (by Text)

Generated ${now}
Class filter: ${currentClass||'(all)'}
`; texts.forEach(txt=>{ const subset = all.filter(r=> (!currentClass || (r.classTag||'')===currentClass) && (r.textTag||'')===txt); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.classTag||''}${(r.qa||[]).length}`).join(''); body += `

${txt}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderClassQ/A
`; }); w.document.write(`Snapshot Bundle (by Text) ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare PDF export --- function comparePDF(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const w = window.open('', '_blank', 'width=800,height=600'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; w.document.write(`Compare PDF `+ `

Comparison Report

`+ `

Current Filter

Responses: ${A.n}
Fit E/J/H ${A.fit.too_easy}/${A.fit.just_right}/${A.fit.too_hard}
Understanding L/M/H ${A.understanding.low}/${A.understanding.medium}/${A.understanding.high}
Q/A total: ${A.qaTotal}
`+ `

Compare: ${cmpClass}

Responses: ${B.n}
Fit E/J/H ${B.fit.too_easy}/${B.fit.just_right}/${B.fit.too_hard}
Understanding L/M/H ${B.understanding.low}/${B.understanding.medium}/${B.understanding.high}
Q/A total: ${B.qaTotal}
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // Wire new buttons (function initNewButtons(){ const bb = document.getElementById('bundleByText'); if (bb) bb.addEventListener('click', bundleByText); const cp = document.getElementById('comparePDF'); if (cp) cp.addEventListener('click', comparePDF); })(); // --- Logo URL persistence --- function getLogoUrl(){ try{ return localStorage.getItem('mlh.readability.logoUrl') || 'assets/logo-nusd.png'; }catch{return 'assets/logo-nusd.png'} } function setLogoUrl(url){ try{ localStorage.setItem('mlh.readability.logoUrl', url||''); }catch{} } function getLogoHTML(){ const url = getLogoUrl(); if (!url) return ''; return `logo`; } // --- DOK distribution from QA arrays --- function dokDist(subset){ const d = {1:0,2:0,3:0,4:0,5:0,6:0}; subset.forEach(r=>{ (Array.isArray(r.qa)? r.qa: []).forEach(q=>{ const lvl = Number(q && q.lvl || 0); if (lvl>=1 && lvl<=6) d[lvl]++; }); }); return d; } function dokSummaryHTML(d){ const total = Object.values(d).reduce((a,b)=>a+b,0) || 1; const pct = (n)=> Math.round(100*n/total); return `DOK — 1:${d[1]} (${pct(d[1])}%) · 2:${d[2]} (${pct(d[2])}%) · 3:${d[3]} (${pct(d[3])}%) · 4:${d[4]} (${pct(d[4])}%) · 5:${d[5]} (${pct(d[5])}%) · 6:${d[6]} (${pct(d[6])}%)`; } (function initLogoControls(){ const inp = document.getElementById('headerLogoUrl'); const btn = document.getElementById('saveLogoUrl'); if (inp){ inp.value = getLogoUrl(); } if (btn){ btn.addEventListener('click', ()=>{ setLogoUrl(inp.value||''); alert('Logo URL saved for PDFs.'); }); } })(); // --- Header styling settings --- function getLogoSize(){ return Number(localStorage.getItem('mlh.readability.logoSize')||48); } function setLogoSize(n){ localStorage.setItem('mlh.readability.logoSize', String(n)); } function getLogoAlign(){ return localStorage.getItem('mlh.readability.logoAlign')||'left'; } function setLogoAlign(v){ localStorage.setItem('mlh.readability.logoAlign', v); } function getLogoPad(){ return Number(localStorage.getItem('mlh.readability.logoPad')||8); } function setLogoPad(n){ localStorage.setItem('mlh.readability.logoPad', String(n)); } function getHeaderBg(){ return localStorage.getItem('mlh.readability.headerBg')==='1'; } function setHeaderBg(b){ localStorage.setItem('mlh.readability.headerBg', b?'1':'0'); } function getHeaderSubtitle(){ return localStorage.getItem('mlh.readability.headerSubtitle')||''; } function setHeaderSubtitle(s){ localStorage.setItem('mlh.readability.headerSubtitle', s||''); } function getHeaderHTML(titleText){ const url = getLogoUrl(); const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const jc = align==='center' ? 'center' : (align==='right' ? 'flex-end' : 'flex-start'); const bgcss = bg ? 'background:#f8fafc;' : ''; return `
logo
${titleText}
${getHeaderSubtitle()}
`; } // --- Logo preview modal --- function applyHeaderPreview(){ const el = document.getElementById('logoPreview'); if (!el) return; el.innerHTML = getHeaderHTML('Readability PDF Header'); } function openLogoPreview(){ // load controls from storage const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const sizeEl = document.getElementById('logoSize'); const padEl = document.getElementById('logoPad'); const alignEl = document.getElementById('logoAlign'); const bgEl = document.getElementById('headerBg'); if (sizeEl) sizeEl.value = size; if (padEl) padEl.value = pad; if (alignEl) alignEl.value = align; if (bgEl) bgEl.checked = bg; const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); applyHeaderPreview(); document.getElementById('logoModal').style.display='flex'; } function closeLogoPreview(){ document.getElementById('logoModal').style.display='none'; } (function hookLogoPreview(){ const btn = document.getElementById('testLogo'); const close = document.getElementById('logoClose'); const save = document.getElementById('logoSave'); if (btn) btn.addEventListener('click', openLogoPreview); if (close) close.addEventListener('click', closeLogoPreview); const restore = document.getElementById('logoRestore'); if (restore) restore.addEventListener('click', restoreHeaderDefaults); if (save) save.addEventListener('click', ()=>{ const size = Number(document.getElementById('logoSize').value||48); const pad = Number(document.getElementById('logoPad').value||8); const align = String(document.getElementById('logoAlign').value||'left'); const bg = !!document.getElementById('headerBg').checked; setLogoSize(size); setLogoPad(pad); setLogoAlign(align); setHeaderBg(bg); setHeaderSubtitle(String(document.getElementById('logoSubtitle').value||'')); applyHeaderPreview(); alert('Header style saved. PDFs will use this styling.'); }); // live update ['logoSize','logoPad','logoAlign','headerBg'].forEach(id=>{ const el = document.getElementById(id); if (!el) return; el.addEventListener('input', applyHeaderPreview); el.addEventListener('change', applyHeaderPreview); }); })(); function restoreHeaderDefaults(){ // NUSD default + sensible style defaults setLogoUrl('assets/logo-nusd.png'); setLogoSize(48); setLogoPad(8); setLogoAlign('left'); setHeaderBg(true); setHeaderSubtitle(''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); document.getElementById('logoSize').value = getLogoSize(); document.getElementById('logoPad').value = getLogoPad(); document.getElementById('logoAlign').value = getLogoAlign(); document.getElementById('headerBg').checked = getHeaderBg(); document.getElementById('logoSubtitle').value = getHeaderSubtitle(); }catch(e){} applyHeaderPreview(); } initSitesUI(); // --- Sites persistence --- function loadSites(){ try{ return JSON.parse(localStorage.getItem('mlh.readability.sites')||'[]'); }catch{return []} } function saveSites(list){ localStorage.setItem('mlh.readability.sites', JSON.stringify(list)); } const DefaultSites = [ { "name": "Novato Unified School District", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hamilton TK-8 School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hill Education Center", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Loma Verde Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lu Sutton Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lynwood Dual Immersion School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Marin Oaks High", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "NOVA Independent Study", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Novato High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Olive Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Pleasant Valley Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Rancho Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Jose Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Marin High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Ramon Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Sinaloa Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" } ]; function ensureDefaultSite(){ const list = loadSites(); if (!list.length){ list.push(...DefaultSites); list.push({ name: 'District', logoUrl: 'assets/logo-nusd.png', subtitle: '' }); saveSites(list); } } function renderSiteSelect(){ ensureDefaultSite(); const sel = document.getElementById('siteSelect'); if (!sel) return; const list = loadSites(); sel.innerHTML = ''; list.forEach((s, idx)=>{ const opt = document.createElement('option'); opt.value = String(idx); opt.textContent = s.name; sel.appendChild(opt); }); // Try to keep current index if saved const idx = Number(localStorage.getItem('mlh.readability.siteIndex')||0); sel.value = String(Math.min(idx, list.length-1)); } function applySiteToHeader(idx){ const list = loadSites(); if (!list[idx]) return; const s = list[idx]; setLogoUrl(s.logoUrl||'assets/logo-nusd.png'); setHeaderSubtitle(s.subtitle||''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); }catch{} try{ const sub = document.getElementById('logoSubtitle'); if (sub) sub.value = getHeaderSubtitle(); }catch{} applyHeaderPreview(); } function renderSiteList(){ const wrap = document.getElementById('siteList'); if(!wrap) return; const list = loadSites(); wrap.innerHTML = ''; list.forEach((s, idx)=>{ const row = document.createElement('div'); row.className = 'rowc'; row.innerHTML = `${s.name}${s.subtitle||''}`; const useBtn = document.createElement('button'); useBtn.textContent = 'Use'; useBtn.addEventListener('click', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(idx)); renderSiteSelect(); applySiteToHeader(idx); alert('Site applied to header.'); }); const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; editBtn.addEventListener('click', ()=>{ document.getElementById('siteName').value = s.name || ''; document.getElementById('siteLogo').value = s.logoUrl || ''; document.getElementById('siteSubtitle').value = s.subtitle || ''; }); const delBtn = document.createElement('button'); delBtn.textContent = 'Delete'; delBtn.addEventListener('click', ()=>{ if (!confirm('Delete site "'+s.name+'"?')) return; const cur = loadSites(); cur.splice(idx,1); saveSites(cur); renderSiteList(); renderSiteSelect(); }); row.appendChild(useBtn); row.appendChild(editBtn); row.appendChild(delBtn); wrap.appendChild(row); }); } function initSitesUI(){ ensureDefaultSite(); renderSiteSelect(); renderSiteList(); const sel = document.getElementById('siteSelect'); if (sel){ sel.addEventListener('change', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(sel.value)); applySiteToHeader(Number(sel.value)); }); } const mg = document.getElementById('manageSites'); const modal = document.getElementById('siteModal'); const close = document.getElementById('siteClose'); if (mg) mg.addEventListener('click', ()=>{ renderSiteList(); modal.style.display='flex'; }); if (close) close.addEventListener('click', ()=>{ modal.style.display='none'; }); const add = document.getElementById('siteAdd'); const name = document.getElementById('siteName'); const logo = document.getElementById('siteLogo'); const sub = document.getElementById('siteSubtitle'); const up = document.getElementById('siteLogoUpload'); if (up){ up.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ logo.value = String(r.result); }; r.readAsDataURL(f); }); } if (add){ add.addEventListener('click', ()=>{ const nm = (name.value||'').trim(); if (!nm){ alert('Enter site name'); return; } const list = loadSites(); const idx = list.findIndex(x=> (x.name||'').toLowerCase()===nm.toLowerCase()); const entry = { name: nm, logoUrl: logo.value||'assets/logo-nusd.png', subtitle: sub.value||'' }; if (idx>=0) list[idx]=entry; else list.push(entry); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Saved.'); }); } // Apply chosen site on load applySiteToHeader(Number(localStorage.getItem('mlh.readability.siteIndex')||0)); } // Also update preview inputs live when site changes function updatePreviewOnSiteChange(){ try{ const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); const urlEl = document.getElementById('headerLogoUrl'); if (urlEl) urlEl.value = getLogoUrl(); applyHeaderPreview(); }catch(e){} } function backupSites(){ const data = JSON.stringify(loadSites(), null, 2); const blob = new Blob([data], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'nusd-sites-backup.json'; a.click(); URL.revokeObjectURL(url); } function restoreSitesFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const list = JSON.parse(r.result); if (!Array.isArray(list)) throw new Error("Invalid format"); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Sites restored. Select and apply as needed.'); }catch(e){ alert('Restore failed: '+e.message); } }; r.readAsText(file); } (function hookBackupRestore(){ const b = document.getElementById('siteBackup'); if (b) b.addEventListener('click', backupSites); const rbtn = document.getElementById('siteRestore'); const rfile = document.getElementById('siteRestoreFile'); if (rbtn && rfile){ rbtn.addEventListener('click', ()=> rfile.click()); rfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesFromFile(f); }); } })(); function normalizeName(n){ return String(n||'').trim().toLowerCase(); } function restoreSitesMergeFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const incoming = JSON.parse(r.result); if (!Array.isArray(incoming)) throw new Error("Invalid format"); const current = loadSites(); const map = new Map(current.map((s, i)=> [normalizeName(s.name), {i, s}])); incoming.forEach(entry=>{ if (!entry || !entry.name) return; const key = normalizeName(entry.name); if (map.has(key)){ // Update existing entry (replace fields provided; keep missing fields from existing) const idx = map.get(key).i; const merged = Object.assign({}, current[idx], entry); current[idx] = merged; }else{ current.push(entry); } }); saveSites(current); renderSiteList(); renderSiteSelect(); alert('Merge complete. New sites added; existing sites updated by name.'); }catch(e){ alert('Merge failed: '+e.message); } }; r.readAsText(file); } (function hookMergeRestore(){ const mbtn = document.getElementById('siteRestoreMerge'); const mfile = document.getElementById('siteRestoreFileMerge'); if (mbtn && mfile){ mbtn.addEventListener('click', ()=> mfile.click()); mfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesMergeFromFile(f); }); } })(); `+ `

Sample (first 50)

`+ `${rows}
TimeIDFitUnderClassTextQ/A
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare Mode (current filter vs selected class) --- function aggregateLabel(a){ return `Resp ${a.n} · Fit E/J/H ${a.fit.too_easy}/${a.fit.just_right}/${a.fit.too_hard} · Under L/M/H ${a.understanding.low}/${a.understanding.medium}/${a.understanding.high} · Q/A ${a.qaTotal}`; } function runCompare(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const panel = document.getElementById('comparePanel'); panel.style.display = 'flex'; panel.innerHTML = '' + '

Current Filter

'+aggregateLabel(A)+'
' + '

Compare: '+cmpClass+'

'+aggregateLabel(B)+'
'; } // Wire buttons (function initFilteredExports(){ const b1 = document.getElementById('exportFilteredCSV'); const b2 = document.getElementById('exportFilteredJSON'); const b3 = document.getElementById('snapshotPDF'); const b4 = document.getElementById('runCompare'); if (b1) b1.addEventListener('click', exportFilteredCSV); if (b2) b2.addEventListener('click', exportFilteredJSON); if (b3) b3.addEventListener('click', snapshotPDF); if (b4) b4.addEventListener('click', runCompare); })(); // --- Mini bar charts (simple canvas) --- function drawMiniBar(canvasId, labels, values){ const cv = document.getElementById(canvasId); if (!cv) return; const ctx = cv.getContext('2d'); ctx.clearRect(0,0,cv.width,cv.height); const max = Math.max(1, ...values); const bw = Math.floor((cv.width - 40) / values.length); const baseY = cv.height - 16; // axes ctx.beginPath(); ctx.moveTo(30, 6); ctx.lineTo(30, baseY); ctx.lineTo(cv.width-6, baseY); ctx.stroke(); for (let i=0; i{ const btn = document.createElement('button'); btn.className = 'mlh-chip'; btn.innerHTML = `${p.name} (${p.class||'(all)'} · ${p.text||'(all)'})`; btn.addEventListener('click', ()=>{ const fc = document.getElementById('filterClass'); const ft = document.getElementById('filterText'); if (fc) fc.value = p.class || ''; if (ft) ft.value = p.text || ''; renderFilteredMetrics(); }); const del = document.createElement('button'); del.textContent = '×'; del.title = 'Delete'; del.style.marginLeft = '6px'; del.addEventListener('click', ()=>{ const confirmDel = confirm('Delete preset "'+p.name+'"?'); if(!confirmDel) return; const cur = loadPresets(); cur.splice(idx,1); savePresets(cur); renderPresetChips(); }); const wrapDiv = document.createElement('span'); wrapDiv.appendChild(btn); wrapDiv.appendChild(del); wrap.appendChild(wrapDiv); }); } function saveCurrentAsPreset(){ const name = (document.getElementById('presetName')||{}).value || ''; const cls = (document.getElementById('filterClass')||{}).value || ''; const txt = (document.getElementById('filterText')||{}).value || ''; if (!name.trim()){ alert('Name your preset.'); return; } const list = loadPresets(); list.push({ name, class: cls, text: txt }); savePresets(list); renderPresetChips(); try{ document.getElementById('presetName').value=''; }catch{} } // --- Snapshot bundle (by class) --- function uniqueClasses(arr){ const set = new Set(); arr.forEach(r=>{ if (r.classTag) set.add(r.classTag); }); return Array.from(set); } function bundleSnapshots(){ const all = getAllResponses(); const currentTxt = (document.getElementById('filterText')||{}).value || ''; const classes = uniqueClasses(all); if (!classes.length){ alert('No class tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle

Generated ${now}
Text filter: ${currentTxt||'(all)'}
`; classes.forEach(cls=>{ const subset = all.filter(r=> (r.classTag||'')===cls && (!currentTxt || String(r.textTag||'').toLowerCase().includes(currentTxt.toLowerCase()))); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.textTag||''}${(r.qa||[]).length}`).join(''); body += `

${cls}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderTextQ/A
`; }); w.document.write(`Snapshot Bundle ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } (function initEnhancedControls(){ const sp = document.getElementById('savePreset'); if (sp) sp.addEventListener('click', saveCurrentAsPreset); renderPresetChips(); // Ensure charts render when metrics change const _oldRender = renderFilteredMetrics; renderFilteredMetrics = function(){ _oldRender(); try{ const subset = applyFilters(getAllResponses()); const A = aggregateWith(subset); drawMiniBar('fitChart', ['E','J','H'], [A.fit.too_easy, A.fit.just_right, A.fit.too_hard]); drawMiniBar('underChart', ['L','M','H'], [A.understanding.low, A.understanding.medium, A.understanding.high]); }catch(e){} }; renderFilteredMetrics(); const bs = document.getElementById('bundleSnapshots'); if (bs) bs.addEventListener('click', bundleSnapshots); })(); // --- Snapshot bundle (by text) --- function uniqueTexts(arr){ const set = new Set(); arr.forEach(r=>{ if (r.textTag) set.add(r.textTag); }); return Array.from(set); } function bundleByText(){ const all = getAllResponses(); const currentClass = (document.getElementById('filterClass')||{}).value || ''; const texts = uniqueTexts(all); if (!texts.length){ alert('No text tags found.'); return; } const w = window.open('', '_blank', 'width=900,height=700'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; const now = new Date().toLocaleString(); let body = `

Readability Snapshot Bundle (by Text)

Generated ${now}
Class filter: ${currentClass||'(all)'}
`; texts.forEach(txt=>{ const subset = all.filter(r=> (!currentClass || (r.classTag||'')===currentClass) && (r.textTag||'')===txt); const a = aggregateWith(subset); const rows = subset.slice(0,40).map(r=>`${new Date(r.ts).toLocaleString()}${r.id||''}${r.fit}${r.under}${r.classTag||''}${(r.qa||[]).length}`).join(''); body += `

${txt}

` + `
${a.n} responses
` + `
Fit — Easy ${a.fit.too_easy} · Just ${a.fit.just_right} · Hard ${a.fit.too_hard}
` + `
Understanding — Low ${a.understanding.low} · Med ${a.understanding.medium} · High ${a.understanding.high}
` + `
Q/A total: ${a.qaTotal}
` + `${rows}
TimeIDFitUnderClassQ/A
`; }); w.document.write(`Snapshot Bundle (by Text) ${body} `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // --- Compare PDF export --- function comparePDF(){ const baseAll = getAllResponses(); const baseSubset = applyFilters(baseAll); const cmpClass = (document.getElementById('compareClass')||{}).value || ''; if (!cmpClass){ alert('Pick a class to compare.'); return; } const cmpSubset = baseAll.filter(r=> (r.classTag||'') === cmpClass); const A = aggregateWith(baseSubset); const B = aggregateWith(cmpSubset); const w = window.open('', '_blank', 'width=800,height=600'); const style = 'body{font-family:system-ui,Arial,sans-serif;padding:16px} .chip{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:2px} h1,h2{margin:.2em 0} table{border-collapse:collapse;width:100%} th,td{border:1px solid #e5e7eb;padding:6px;text-align:left} .muted{color:#64748b}'; w.document.write(`Compare PDF `+ `

Comparison Report

`+ `

Current Filter

Responses: ${A.n}
Fit E/J/H ${A.fit.too_easy}/${A.fit.just_right}/${A.fit.too_hard}
Understanding L/M/H ${A.understanding.low}/${A.understanding.medium}/${A.understanding.high}
Q/A total: ${A.qaTotal}
`+ `

Compare: ${cmpClass}

Responses: ${B.n}
Fit E/J/H ${B.fit.too_easy}/${B.fit.just_right}/${B.fit.too_hard}
Understanding L/M/H ${B.understanding.low}/${B.understanding.medium}/${B.understanding.high}
Q/A total: ${B.qaTotal}
`+ ` `); w.document.close(); w.focus(); setTimeout(()=> w.print(), 300); } // Wire new buttons (function initNewButtons(){ const bb = document.getElementById('bundleByText'); if (bb) bb.addEventListener('click', bundleByText); const cp = document.getElementById('comparePDF'); if (cp) cp.addEventListener('click', comparePDF); })(); // --- Logo URL persistence --- function getLogoUrl(){ try{ return localStorage.getItem('mlh.readability.logoUrl') || 'assets/logo-nusd.png'; }catch{return 'assets/logo-nusd.png'} } function setLogoUrl(url){ try{ localStorage.setItem('mlh.readability.logoUrl', url||''); }catch{} } function getLogoHTML(){ const url = getLogoUrl(); if (!url) return ''; return `logo`; } // --- DOK distribution from QA arrays --- function dokDist(subset){ const d = {1:0,2:0,3:0,4:0,5:0,6:0}; subset.forEach(r=>{ (Array.isArray(r.qa)? r.qa: []).forEach(q=>{ const lvl = Number(q && q.lvl || 0); if (lvl>=1 && lvl<=6) d[lvl]++; }); }); return d; } function dokSummaryHTML(d){ const total = Object.values(d).reduce((a,b)=>a+b,0) || 1; const pct = (n)=> Math.round(100*n/total); return `DOK — 1:${d[1]} (${pct(d[1])}%) · 2:${d[2]} (${pct(d[2])}%) · 3:${d[3]} (${pct(d[3])}%) · 4:${d[4]} (${pct(d[4])}%) · 5:${d[5]} (${pct(d[5])}%) · 6:${d[6]} (${pct(d[6])}%)`; } (function initLogoControls(){ const inp = document.getElementById('headerLogoUrl'); const btn = document.getElementById('saveLogoUrl'); if (inp){ inp.value = getLogoUrl(); } if (btn){ btn.addEventListener('click', ()=>{ setLogoUrl(inp.value||''); alert('Logo URL saved for PDFs.'); }); } })(); // --- Header styling settings --- function getLogoSize(){ return Number(localStorage.getItem('mlh.readability.logoSize')||48); } function setLogoSize(n){ localStorage.setItem('mlh.readability.logoSize', String(n)); } function getLogoAlign(){ return localStorage.getItem('mlh.readability.logoAlign')||'left'; } function setLogoAlign(v){ localStorage.setItem('mlh.readability.logoAlign', v); } function getLogoPad(){ return Number(localStorage.getItem('mlh.readability.logoPad')||8); } function setLogoPad(n){ localStorage.setItem('mlh.readability.logoPad', String(n)); } function getHeaderBg(){ return localStorage.getItem('mlh.readability.headerBg')==='1'; } function setHeaderBg(b){ localStorage.setItem('mlh.readability.headerBg', b?'1':'0'); } function getHeaderSubtitle(){ return localStorage.getItem('mlh.readability.headerSubtitle')||''; } function setHeaderSubtitle(s){ localStorage.setItem('mlh.readability.headerSubtitle', s||''); } function getHeaderHTML(titleText){ const url = getLogoUrl(); const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const jc = align==='center' ? 'center' : (align==='right' ? 'flex-end' : 'flex-start'); const bgcss = bg ? 'background:#f8fafc;' : ''; return `
logo
${titleText}
${getHeaderSubtitle()}
`; } // --- Logo preview modal --- function applyHeaderPreview(){ const el = document.getElementById('logoPreview'); if (!el) return; el.innerHTML = getHeaderHTML('Readability PDF Header'); } function openLogoPreview(){ // load controls from storage const size = getLogoSize(); const pad = getLogoPad(); const align = getLogoAlign(); const bg = getHeaderBg(); const sizeEl = document.getElementById('logoSize'); const padEl = document.getElementById('logoPad'); const alignEl = document.getElementById('logoAlign'); const bgEl = document.getElementById('headerBg'); if (sizeEl) sizeEl.value = size; if (padEl) padEl.value = pad; if (alignEl) alignEl.value = align; if (bgEl) bgEl.checked = bg; const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); applyHeaderPreview(); document.getElementById('logoModal').style.display='flex'; } function closeLogoPreview(){ document.getElementById('logoModal').style.display='none'; } (function hookLogoPreview(){ const btn = document.getElementById('testLogo'); const close = document.getElementById('logoClose'); const save = document.getElementById('logoSave'); if (btn) btn.addEventListener('click', openLogoPreview); if (close) close.addEventListener('click', closeLogoPreview); const restore = document.getElementById('logoRestore'); if (restore) restore.addEventListener('click', restoreHeaderDefaults); if (save) save.addEventListener('click', ()=>{ const size = Number(document.getElementById('logoSize').value||48); const pad = Number(document.getElementById('logoPad').value||8); const align = String(document.getElementById('logoAlign').value||'left'); const bg = !!document.getElementById('headerBg').checked; setLogoSize(size); setLogoPad(pad); setLogoAlign(align); setHeaderBg(bg); setHeaderSubtitle(String(document.getElementById('logoSubtitle').value||'')); applyHeaderPreview(); alert('Header style saved. PDFs will use this styling.'); }); // live update ['logoSize','logoPad','logoAlign','headerBg'].forEach(id=>{ const el = document.getElementById(id); if (!el) return; el.addEventListener('input', applyHeaderPreview); el.addEventListener('change', applyHeaderPreview); }); })(); function restoreHeaderDefaults(){ // NUSD default + sensible style defaults setLogoUrl('assets/logo-nusd.png'); setLogoSize(48); setLogoPad(8); setLogoAlign('left'); setHeaderBg(true); setHeaderSubtitle(''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); document.getElementById('logoSize').value = getLogoSize(); document.getElementById('logoPad').value = getLogoPad(); document.getElementById('logoAlign').value = getLogoAlign(); document.getElementById('headerBg').checked = getHeaderBg(); document.getElementById('logoSubtitle').value = getHeaderSubtitle(); }catch(e){} applyHeaderPreview(); } initSitesUI(); // --- Sites persistence --- function loadSites(){ try{ return JSON.parse(localStorage.getItem('mlh.readability.sites')||'[]'); }catch{return []} } function saveSites(list){ localStorage.setItem('mlh.readability.sites', JSON.stringify(list)); } const DefaultSites = [ { "name": "Novato Unified School District", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hamilton TK-8 School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Hill Education Center", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Loma Verde Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lu Sutton Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Lynwood Dual Immersion School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Marin Oaks High", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "NOVA Independent Study", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Novato High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Olive Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Pleasant Valley Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Rancho Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Jose Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Marin High School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "San Ramon Elementary", "logoUrl": "assets/logo-nusd.png", "subtitle": "" }, { "name": "Sinaloa Middle School", "logoUrl": "assets/logo-nusd.png", "subtitle": "" } ]; function ensureDefaultSite(){ const list = loadSites(); if (!list.length){ list.push(...DefaultSites); list.push({ name: 'District', logoUrl: 'assets/logo-nusd.png', subtitle: '' }); saveSites(list); } } function renderSiteSelect(){ ensureDefaultSite(); const sel = document.getElementById('siteSelect'); if (!sel) return; const list = loadSites(); sel.innerHTML = ''; list.forEach((s, idx)=>{ const opt = document.createElement('option'); opt.value = String(idx); opt.textContent = s.name; sel.appendChild(opt); }); // Try to keep current index if saved const idx = Number(localStorage.getItem('mlh.readability.siteIndex')||0); sel.value = String(Math.min(idx, list.length-1)); } function applySiteToHeader(idx){ const list = loadSites(); if (!list[idx]) return; const s = list[idx]; setLogoUrl(s.logoUrl||'assets/logo-nusd.png'); setHeaderSubtitle(s.subtitle||''); try{ const inp = document.getElementById('headerLogoUrl'); if (inp) inp.value = getLogoUrl(); }catch{} try{ const sub = document.getElementById('logoSubtitle'); if (sub) sub.value = getHeaderSubtitle(); }catch{} applyHeaderPreview(); } function renderSiteList(){ const wrap = document.getElementById('siteList'); if(!wrap) return; const list = loadSites(); wrap.innerHTML = ''; list.forEach((s, idx)=>{ const row = document.createElement('div'); row.className = 'rowc'; row.innerHTML = `${s.name}${s.subtitle||''}`; const useBtn = document.createElement('button'); useBtn.textContent = 'Use'; useBtn.addEventListener('click', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(idx)); renderSiteSelect(); applySiteToHeader(idx); alert('Site applied to header.'); }); const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; editBtn.addEventListener('click', ()=>{ document.getElementById('siteName').value = s.name || ''; document.getElementById('siteLogo').value = s.logoUrl || ''; document.getElementById('siteSubtitle').value = s.subtitle || ''; }); const delBtn = document.createElement('button'); delBtn.textContent = 'Delete'; delBtn.addEventListener('click', ()=>{ if (!confirm('Delete site "'+s.name+'"?')) return; const cur = loadSites(); cur.splice(idx,1); saveSites(cur); renderSiteList(); renderSiteSelect(); }); row.appendChild(useBtn); row.appendChild(editBtn); row.appendChild(delBtn); wrap.appendChild(row); }); } function initSitesUI(){ ensureDefaultSite(); renderSiteSelect(); renderSiteList(); const sel = document.getElementById('siteSelect'); if (sel){ sel.addEventListener('change', ()=>{ localStorage.setItem('mlh.readability.siteIndex', String(sel.value)); applySiteToHeader(Number(sel.value)); }); } const mg = document.getElementById('manageSites'); const modal = document.getElementById('siteModal'); const close = document.getElementById('siteClose'); if (mg) mg.addEventListener('click', ()=>{ renderSiteList(); modal.style.display='flex'; }); if (close) close.addEventListener('click', ()=>{ modal.style.display='none'; }); const add = document.getElementById('siteAdd'); const name = document.getElementById('siteName'); const logo = document.getElementById('siteLogo'); const sub = document.getElementById('siteSubtitle'); const up = document.getElementById('siteLogoUpload'); if (up){ up.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ logo.value = String(r.result); }; r.readAsDataURL(f); }); } if (add){ add.addEventListener('click', ()=>{ const nm = (name.value||'').trim(); if (!nm){ alert('Enter site name'); return; } const list = loadSites(); const idx = list.findIndex(x=> (x.name||'').toLowerCase()===nm.toLowerCase()); const entry = { name: nm, logoUrl: logo.value||'assets/logo-nusd.png', subtitle: sub.value||'' }; if (idx>=0) list[idx]=entry; else list.push(entry); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Saved.'); }); } // Apply chosen site on load applySiteToHeader(Number(localStorage.getItem('mlh.readability.siteIndex')||0)); } // Also update preview inputs live when site changes function updatePreviewOnSiteChange(){ try{ const subEl = document.getElementById('logoSubtitle'); if (subEl) subEl.value = getHeaderSubtitle(); const urlEl = document.getElementById('headerLogoUrl'); if (urlEl) urlEl.value = getLogoUrl(); applyHeaderPreview(); }catch(e){} } function backupSites(){ const data = JSON.stringify(loadSites(), null, 2); const blob = new Blob([data], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'nusd-sites-backup.json'; a.click(); URL.revokeObjectURL(url); } function restoreSitesFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const list = JSON.parse(r.result); if (!Array.isArray(list)) throw new Error("Invalid format"); saveSites(list); renderSiteList(); renderSiteSelect(); alert('Sites restored. Select and apply as needed.'); }catch(e){ alert('Restore failed: '+e.message); } }; r.readAsText(file); } (function hookBackupRestore(){ const b = document.getElementById('siteBackup'); if (b) b.addEventListener('click', backupSites); const rbtn = document.getElementById('siteRestore'); const rfile = document.getElementById('siteRestoreFile'); if (rbtn && rfile){ rbtn.addEventListener('click', ()=> rfile.click()); rfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesFromFile(f); }); } })(); function normalizeName(n){ return String(n||'').trim().toLowerCase(); } function restoreSitesMergeFromFile(file){ const r = new FileReader(); r.onload = ()=>{ try{ const incoming = JSON.parse(r.result); if (!Array.isArray(incoming)) throw new Error("Invalid format"); const current = loadSites(); const map = new Map(current.map((s, i)=> [normalizeName(s.name), {i, s}])); incoming.forEach(entry=>{ if (!entry || !entry.name) return; const key = normalizeName(entry.name); if (map.has(key)){ // Update existing entry (replace fields provided; keep missing fields from existing) const idx = map.get(key).i; const merged = Object.assign({}, current[idx], entry); current[idx] = merged; }else{ current.push(entry); } }); saveSites(current); renderSiteList(); renderSiteSelect(); alert('Merge complete. New sites added; existing sites updated by name.'); }catch(e){ alert('Merge failed: '+e.message); } }; r.readAsText(file); } (function hookMergeRestore(){ const mbtn = document.getElementById('siteRestoreMerge'); const mfile = document.getElementById('siteRestoreFileMerge'); if (mbtn && mfile){ mbtn.addEventListener('click', ()=> mfile.click()); mfile.addEventListener('change', (e)=>{ const f = e.target.files && e.target.files[0]; if (f) restoreSitesMergeFromFile(f); }); } })();

Live Collect

Drag JSON/text responses here or paste them to auto-aggregate.

Open Live Collect Inbox →