${g}, ${esc(n)}! Ready to learn something new? 🔥 ${streak()}-day streak
`;document.body.appendChild(p);setTimeout(()=>p.classList.add('out'),4500);setTimeout(()=>p.remove(),5100)}
const wkSec=(subj,off)=>{let t=0;for(let i=0;i<7;i++){const d=new Date();d.setDate(d.getDate()-i-off*7);S.sessions.forEach(x=>{if(x.day===dkey(d)&&(!subj||x.subject===subj))t+=x.sec})}return t};
const wkDays=()=>{let n=0;for(let i=0;i<7;i++){const d=new Date();d.setDate(d.getDate()-i);if(secOn(dkey(d)))n++}return n};
function repData(){const names=[...new Set(S.sessions.map(x=>x.subject))];const rows=names.map(n=>{const sb=S.subjects.find(s=>s.name===n);return{n,w:wkSec(n,0),p:wkSec(n,1),pr:sb?subjProg(sb):0}});const acc=S.results.length?Math.round(S.results.reduce((a,x)=>a+(x.total?(x.correct??x.score??0)/x.total*100:0),0)/S.results.length):0;return{rows,tot:wkSec('',0),prev:wkSec('',1),days:wkDays(),acc,tests:S.results.length,lessons:allLessons().filter(x=>x.done).length}}
function repText(){const r=repData();return `ScholarX weekly progress report - ${new Date().toLocaleDateString()}\nStudent: ${S.user.name}\n\nOVERALL\nStudy time this week: ${fmt(r.tot)} (last week ${fmt(r.prev)})\nActive days: ${r.days}/7\nStreak: ${streak()} days\nLessons completed: ${r.lessons}\nTests: ${r.tests}, average accuracy ${r.acc}%\n\nSUBJECT-WISE\n`+(r.rows.length?r.rows.map(x=>`${x.n}: ${fmt(x.w)} this week, ${fmt(x.p)} last week, course progress ${x.pr}%`).join('\n'):'No study sessions recorded yet.')}
function repCard(){const r=repData(),f=UI.rep||'all',rows=f==='all'?r.rows:r.rows.filter(x=>x.n===f);const dl=(a,b)=>a===b?'same as last week':a>b?'up from last week':'down from last week';return `
Weekly progress report
${r.rows.map(x=>``).join('')}
`+(f==='all'?`
${fmt(r.tot)}Study time this week
${r.days}/7Active days
${r.lessons}Lessons done
${r.acc}%Test accuracy
This week is ${dl(r.tot,r.prev)}.
`:'')+(rows.length?`
Subject
This week
Last week
Course progress
${rows.map(x=>`
${esc(x.n)}
${fmt(x.w)}
${fmt(x.p)}
${x.pr}%
`).join('')}
`:'
Record a study session to see subject-wise numbers.
')+`
`}
const DEMO={u1:310*60,u2:220*60,u3:90*60};
function boardCard(){const me=meUser();const list=S.users.map(u=>({n:u.name,tag:u.tag,s:u.me?wkSec('',0):(DEMO[u.id]||0),me:!!u.me,demo:!u.me})).sort((a,b)=>b.s-a.s);return `
Leaderboard · this week
Ranked by study time in the last 7 days. Other students are demo entries in this offline file; a real shared ranking needs the server step.
Add as many other exams as you like, one by one or separated by commas.
`;
/* login: ScholarX login page (runs in its own layer, talks to the app by message) */
let SX_PENDING=null;
function sxMountLogin(){const app=$('#app');if(app.querySelector('#sxlgf'))return;app.innerHTML='';const f=sxFrame('login');f.id='sxlgf';app.appendChild(f)}
function sxAuthLogin(d){
const un=String(d.username||'').trim().toLowerCase(),pw=String(d.password||'');
const u=S.users.find(x=>(x.name||'').trim().toLowerCase()===un);
if(!u)return{ok:false,field:'username',message:'No account with that username. Use Create account first.'};
if(u.blocked)return{ok:false,field:'username',message:'This account is blocked.'};
let msg='Login successful.';
if(u.pw){if(hashPw(pw,u.salt)!==u.pw)return{ok:false,field:'password',message:'Wrong password.'}}
else{
if(pw.length<8)return{ok:false,field:'password',message:'This account has no password yet. Enter a new one (at least 8 characters) to set it.'};
u.salt=newSalt();u.pw=hashPw(pw,u.salt);save();msg='Login successful. Your password is now set.'}
SX_PENDING={kind:'student',id:u.id};return{ok:true,message:msg}}
function sxAuthSignup(d){
const fn=String(d.fullName||'').trim(),em=String(d.email||'').trim().toLowerCase(),un=String(d.username||'').trim(),pw=String(d.password||'');
if(fn.length<2)return{ok:false,field:'fullName',message:'Enter your full name.'};
if(!okEmail(em))return{ok:false,field:'email',message:'Enter a valid email address.'};
if(un.length<2)return{ok:false,field:'username',message:'Username must be at least 2 characters.'};
if(pw.length<8)return{ok:false,field:'password',message:'Use at least 8 characters for your password.'};
if(S.users.some(x=>(x.name||'').trim().toLowerCase()===un.toLowerCase()))return{ok:false,field:'username',message:'That username is taken. Pick another, or log in.'};
if(S.users.some(x=>(x.email||'').toLowerCase()===em))return{ok:false,field:'email',message:'This email is already registered. Log in instead.'};
const salt=newSalt();S.users.push({id:uid(),name:un,fullName:fn,email:em,role:'student',blocked:false,muted:false,salt,pw:hashPw(pw,salt)});save();
return{ok:true,message:'Account created. Now log in.'}}
function sxAuthReset(d){
const un=String(d.username||'').trim().toLowerCase(),em=String(d.email||'').trim().toLowerCase(),pw=String(d.password||'');
const u=S.users.find(x=>(x.name||'').trim().toLowerCase()===un&&(x.email||'').toLowerCase()===em);
if(!u)return{ok:false,field:'username',message:'That username and email do not match an account.'};
if(pw.length<8)return{ok:false,field:'password',message:'Use at least 8 characters for your password.'};
u.salt=newSalt();u.pw=hashPw(pw,u.salt);save();
return{ok:true,message:'Password updated. Now log in.'}}
function sxAuthOwner(d){
if(!OWN)return{ok:false,message:'Owner login is not set up on this device.'};
if(Date.now()=5){ownerLockUntil=Date.now()+30000;ownerFails=0}
return{ok:false,message:'Email or password is wrong.'}}
function sxEnter(){
const p=SX_PENDING;SX_PENDING=null;if(!p)return;
if(p.kind==='student'){const u=S.users.find(x=>x.id===p.id);if(!u)return;S.users.forEach(x=>{delete x.me});u.me=true;S.user.name=u.name;S.role='student';UI.page='dash';sessionStorage.removeItem('sx_out');save();location.reload()}
else{ownerUnlocked=true;sessionStorage.removeItem('sx_out');S.role='owner';UI.page='owner';save();render()}}
function sxHandle(d){
try{
if(d.t==='introend'){const f=document.getElementById('sxintro');if(f&&f._end)f._end();return true}
if(d.t==='login')return sxAuthLogin(d);
if(d.t==='signup')return sxAuthSignup(d);
if(d.t==='reset')return sxAuthReset(d);
if(d.t==='owner')return sxAuthOwner(d);
if(d.t==='enter'){sxEnter();return true}
}catch(err){return{ok:false,message:'Error: '+(err&&err.message||err)}}
return{ok:false,message:'Unknown request.'}}
window.__sxHandle=sxHandle;
window.addEventListener('message',ev=>{
const d=ev.data;if(!d||d.sx!==1||d.t==='result')return;
const fr=[...document.querySelectorAll('iframe[data-sx]')].find(x=>x.contentWindow===ev.source);if(!fr)return;
if(fr.dataset.sx==='intro'&&d.t!=='introend')return;
if(fr.dataset.sx==='login'&&d.t==='introend')return;
const r=sxHandle(d);
if(d.id){try{ev.source.postMessage(Object.assign({sx:1,t:'result',id:d.id},r&&typeof r==='object'?r:{ok:true}),'*')}catch(e){}}});
/* PDF + download */
function sxPdf(text){const wrap=[];text.split('\n').forEach(l=>{l=l.replace(/[^\x20-\x7E]/g,'?');if(!l){wrap.push('');return}while(l.length>88){let k=l.lastIndexOf(' ',88);if(k<20)k=88;wrap.push(l.slice(0,k));l=l.slice(k).trimStart()}wrap.push(l)});
const per=46,pages=[];for(let i=0;it.replace(/\\/g,'\\\\').replace(/\(/g,'\\(').replace(/\)/g,'\\)');const objs=[];const add=o=>{objs.push(o);return objs.length};
add('<< /Type /Catalog /Pages 2 0 R >>');add('');add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>');add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>');const kids=[];
pages.forEach((pg,pi)=>{let c='BT 15 TL 50 800 Td\n';pg.forEach((ln,i)=>{const h=pi===0&&i===0;c+='/'+(h?'F2 14':'F1 11')+' Tf ('+q(ln)+') Tj T*\n'});c+='ET';const cid=add('<< /Length '+c.length+' >>\nstream\n'+c+'\nendstream');kids.push(add('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents '+cid+' 0 R >>')+' 0 R')});
objs[1]='<< /Type /Pages /Kids ['+kids.join(' ')+'] /Count '+kids.length+' >>';let out='%PDF-1.4\n';const off=[];objs.forEach((o,i)=>{off.push(out.length);out+=(i+1)+' 0 obj\n'+o+'\nendobj\n'});const x=out.length;
out+='xref\n0 '+(objs.length+1)+'\n0000000000 65535 f \n'+off.map(o=>String(o).padStart(10,'0')+' 00000 n \n').join('')+'trailer\n<< /Size '+(objs.length+1)+' /Root 1 0 R >>\nstartxref\n'+x+'\n%%EOF';return new Blob([out],{type:'application/pdf'})}
function sxSave(blob,name){const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.rel='noopener';a.style.display='none';document.body.appendChild(a);a.click();setTimeout(()=>{a.remove();URL.revokeObjectURL(url)},5000);toast('Downloading '+name)}
/* doubts to administrator / moderator */
const canSeeDoubt=d=>{if(S.role==='owner')return true;const me=meUser();if(d.to===me.id)return true;if(me.role==='administrator')return !d.to||d.to==='administrator'||d.to==='moderator';if(me.role==='moderator')return !d.to||d.to==='moderator';return false};
function doubtInbox(f,title){const list=S.doubts.filter(canSeeDoubt).filter(f||(()=>true)).slice().reverse();return `
Reply by ${esc(d.repliedBy||'staff')}: ${esc(d.reply)}
`:``}
`}).join(''):'
No doubts here yet.
'}
`}
const _mod2=moderation;moderation=function(){return _mod2()+doubtInbox(d=>d.to!=='owner')};
/* owner: user search, dashboard notes, display name */
const ownerUsers=()=>{const q=(UI.ouq||'').trim().toLowerCase();return q?S.users.filter(u=>(u.name||'').toLowerCase().includes(q)||(u.email||'').toLowerCase().includes(q)):S.users};
function ownerDescs(){const ed=UI.editDesc&&S.descs.find(x=>x.id===UI.editDesc);return `
${ed?'Edit description':'Add a description'}
Whatever you write here appears in a box on every student's dashboard. Add as many descriptions as you need.
${ed?'':''}
Published descriptions (${S.descs.length})
${S.descs.length?`
${S.descs.map(d=>`
${d.title?`${esc(d.title)}`:''}
${esc(d.body)}
${new Date(d.ts).toLocaleString()}
`).join('')}
`:'
No descriptions yet.
'}
`}
function descBox(){if(!S.descs.length)return '';return `
From the owner
${S.descs.map(d=>`
${d.title?`${esc(d.title)}`:''}
${esc(d.body)}
${new Date(d.ts).toLocaleDateString()}
`).join('')}
`}
const ownerNameCard=()=>`
Your name in chat
This name (with an Owner badge) shows on your community chat messages.
`;
Object.assign(ACT,{
addotherexam:v=>{const el=$('#ox-n'),names=((el&&el.value)||'').split(',').map(x=>x.trim().slice(0,40)).filter(Boolean);if(!names.length){toast('Type an exam name first');return}
if(v==='modal')S.exams=[...document.querySelectorAll('#exm .chip.on')].map(x=>x.dataset.v);
let n=0;names.forEach(nm=>{const ex=examList().find(x=>x.toLowerCase()===nm.toLowerCase());const name=ex||nm;if(!ex)S.customExams.push(name);if(!S.exams.includes(name)){S.exams.push(name);n++}});
save();if(v==='modal')examModal();else render();toast(n?n+' exam'+(n>1?'s':'')+' added':'Already in your list')},
sxdl:()=>sxSave(sxPdf(repText()),'ScholarX-weekly-report.pdf'),
sxdltxt:()=>sxSave(new Blob([repText()],{type:'text/plain'}),'ScholarX-weekly-report.txt'),
submitdoubt:()=>{const t=val('doubt-text').trim();if(!t){toast('Write your doubt first');return}const u=meUser();if(isMuted(u)){toast('You are muted from chat');return}S.doubts.push({id:uid(),userId:u.id,name:u.name,to:val('doubt-to')||'moderator',text:t,ts:Date.now(),reply:''});save();render();toast('Doubt sent')},
replydoubt:v=>{const d=S.doubts.find(x=>x.id===v),r=val('reply-'+v).trim();if(!d||!r)return;d.reply=r;d.repliedAt=Date.now();d.repliedBy=S.role==='owner'?chatName()+' (owner)':meUser().name+' ('+meUser().role+')';own('Replied to doubt from '+(d.name||'student'));save();render();toast('Reply sent')},
descsave:()=>{const t=val('ds-t').trim(),b=val('ds-b').trim();if(!b){toast('Write the description first');return}if(UI.editDesc){const d=S.descs.find(x=>x.id===UI.editDesc);if(d){d.title=t;d.body=b}UI.editDesc=null;own('Edited dashboard description')}else{S.descs.unshift({id:uid(),title:t,body:b,ts:Date.now()});own('Added dashboard description')}save();render();toast('Saved')},
descedit:v=>{UI.editDesc=v||null;render()},
descdel:v=>{if(!confirm('Delete this description?'))return;S.descs=S.descs.filter(x=>x.id!==v);if(UI.editDesc===v)UI.editDesc=null;own('Deleted dashboard description');save();render()},
savownername:()=>{S.ownerName=val('on-n').trim().slice(0,40);own('Updated owner display name');save();render();toast('Name saved')},
send:()=>{const t=val('cm-t').trim();if(!t)return;const r=UI.tab.comm;if(S.role==='owner'&&!ownerName())toast('Tip: set your name in Owner > Account');(S.chat[r]=S.chat[r]||[]).push({n:chatName(),t,me:true,role:chatRole(),u:S.role==='owner'?'':meUser().id});save();render();const m=$('#msgs');if(m)m.scrollTop=1e5}
});
document.addEventListener('input',e=>{if(e.target.dataset.in==='ou'){UI.ouq=e.target.value;const p=e.target.selectionStart;render();const el=$('#ou-q');if(el){el.focus();el.setSelectionRange(p,p)}}});
document.addEventListener('keydown',e=>{if(e.key!=='Enter')return;const id=e.target.id;if(id==='ox-n')ACT.addotherexam(e.target.closest('#modal')?'modal':'page')});
/* ===== v3: owner Ask Doubt tab, upload power, secret Pokeball ===== */
const POKE='';
(function(){const st=document.createElement('style');st.textContent='.sxpoke{background:none;border:0;padding:2px;cursor:pointer;line-height:0;border-radius:50%;vertical-align:middle;margin-right:6px}.sxpoke.wig svg{animation:sxwig .35s}@keyframes sxwig{25%{transform:rotate(-25deg)}75%{transform:rotate(25deg)}}';document.head.appendChild(st)})();
function doubtPending(){const n=S.doubts.filter(d=>!d.reply&&d.to==='owner').length;return n?' ('+n+')':''}
const hasUploadPower=u=>!!(u&&u.directUpload&&['administrator','moderator'].includes(u.role));
function uploadPowerCard(){const staff=S.users.filter(u=>['administrator','moderator'].includes(u.role));return `
Upload power for administrators and moderators
By default their study material waits for your approval. Give upload power to publish material straight to students without a request. You can take it back at any time.
The owner has given you upload power. Material you submit here is published to students immediately.
':'')+_am().replace('Send to owner for approval',hasUploadPower(me)?'Publish now':'Send to owner for approval')};
let _pk={n:0,t:0};
Object.assign(ACT,{
uppower:v=>{if(S.role!=='owner'){toast('Only the owner can do this');return}const u=S.users.find(x=>x.id===v);if(!u||!['administrator','moderator'].includes(u.role)){toast('Only administrators and moderators can get upload power');return}u.directUpload=!u.directUpload;own((u.directUpload?'Gave upload power to ':'Removed upload power from ')+u.name);save();render();toast(u.directUpload?'Upload power given':'Upload power removed')},
submitmaterial:()=>{const title=val('mat-title').trim(),url=val('mat-url').trim();if(!title||!/^https?:\/\//.test(url)){toast('Enter a title and valid material URL');return}const me=meUser(),direct=S.role==='owner'||hasUploadPower(me);const x={id:uid(),title,type:val('mat-type'),url,description:val('mat-desc').trim(),by:me.id,byName:me.name,status:direct?'Approved':'Pending',ts:Date.now()};S.materialRequests.push(x);if(direct){S.materials.push({...x,approvedAt:Date.now()});own('Published material directly: '+title)}save();render();toast(direct?'Material published to students':'Sent to owner for approval')},
sxpoke:(v,t)=>{const now=Date.now();if(now-_pk.t>3000)_pk.n=0;_pk.t=now;_pk.n++;if(t){t.classList.remove('wig');void t.offsetWidth;t.classList.add('wig')}if(_pk.n>=5){_pk.n=0;if(OWN)ownerPrompt();else{toast('Set up your owner email and password under Account');ACT.role('owner')}}}
});
/* ===== v4: separate Ask Owner section, member tags ===== */
const tagHtml=t=>t?` ${esc(t)}`:'';
const nameTag=m=>{if(m.role==='owner')return '';const u=(m.u&&S.users.find(x=>x.id===m.u))||S.users.find(x=>x.name===m.n);return tagHtml(u&&u.tag)};
function askOwnerPage(){const me=meUser(),mine=S.doubts.filter(d=>d.userId===me.id&&d.to==='owner'),muted=isMuted(me);
return `
Ask the Owner
This goes only to the owner. For subject doubts you can also use Doubt to Admin / Mod.
${muted?`
Your chat permissions are muted${me.mutedUntil?' until '+new Date(me.mutedUntil).toLocaleString():''}.
`:``}
My messages to the owner
${mine.length?mine.slice().reverse().map(d=>`
${esc(d.text)}
${new Date(d.ts).toLocaleString()}
${d.reply?`
Owner reply
${esc(d.reply)}
`:'Waiting for owner'}
`).join(''):'
Nothing sent yetYour messages and the owner replies appear here.
'}
`}
function tagsCard(){const list=ownerUsers();return `
Member tags
Give any user a tag, or write your own text. It shows beside their name in chat, leaderboard and the users list. Leave it empty and save to remove.
${list.length?`
${list.map(u=>`
${esc(u.name)}${tagHtml(u.tag)}
${esc(u.email||'')}
${u.tag?``:''}
`).join('')}
`:'
No user matches this search.
'}
`}
Object.assign(ACT,{
submitaskowner:()=>{const t=val('ao-text').trim();if(!t){toast('Write your message first');return}const u=meUser();if(isMuted(u)){toast('You are muted from chat');return}S.doubts.push({id:uid(),userId:u.id,name:u.name,to:'owner',text:t,ts:Date.now(),reply:''});save();render();toast('Sent to the owner')},
savetag:v=>{if(S.role!=='owner')return;const u=S.users.find(x=>x.id===v);if(!u)return;u.tag=val('tg-'+v).trim().slice(0,24);own('Set tag for '+u.name+(u.tag?': '+u.tag:' (removed)'));save();render();toast(u.tag?'Tag saved':'Tag removed')},
mkmember:v=>{if(S.role!=='owner')return;const u=S.users.find(x=>x.id===v);if(!u)return;u.tag='Member';own('Gave Member tag to '+u.name);save();render();toast('Member tag given')},
cleartag:v=>{if(S.role!=='owner')return;const u=S.users.find(x=>x.id===v);if(!u)return;delete u.tag;own('Removed tag from '+u.name);save();render()}
});
/* ===== v5: push notifications (announcements + doubt replies) and user activity tracking ===== */
addNotif=function(t,b,to){S.notifs.unshift({id:uid(),t,b,to:to||'all',ts:Date.now(),rb:[]});S.notifs=S.notifs.slice(0,200);save()};
const nVisible=(n,me)=>!n.to||n.to==='all'||n.to===me.id;
const nRead=(n,me)=>n.rb?n.rb.includes(me.id):!!n.read;
function unreadCount(){const me=meUser();return S.notifs.filter(n=>nVisible(n,me)&&!nRead(n,me)).length}
function pushLocal(t,b){toast(t);notify(t,b)}
Object.assign(ACT,{
notifs:()=>{const me=meUser();const list=S.notifs.filter(n=>nVisible(n,me));const canAsk='Notification' in window&&Notification.permission==='default';
openModal(`
Notifications
${canAsk?'
Turn on push alerts to get a pop-up when the owner announces something or replies to you.
':''}${list.length?`
${list.map(n=>`
${esc(n.t)}
${esc(n.b)}
${new Date(n.ts).toLocaleString()}
${nRead(n,me)?'':'New'}
`).join('')}
`:'
No notifications.
'}`);
S.notifs.forEach(n=>{if(nVisible(n,me)){if(n.rb){if(!n.rb.includes(me.id))n.rb.push(me.id)}else n.read=true}});save();render();$('#modal').classList.add('show')},
announce:()=>{if(S.role!=='owner')return;const t=val('o-nt').trim();if(!t){toast('Write a title first');return}addNotif(t,val('o-nb').trim(),'all');own('Announcement: '+t);render();toast('Announcement sent as a notification to all users')}
});
(function(){const _rd=ACT.replydoubt;ACT.replydoubt=function(v,...a){const d=S.doubts.find(x=>x.id===v);const had=d&&d.reply;_rd(v,...a);if(d&&d.reply&&!had){const who=S.role==='owner'?'The owner':(meUser().name||'Staff');addNotif(who+' replied to your doubt',d.reply.length>140?d.reply.slice(0,137)+'...':d.reply,d.userId);render()}}})();
/* sync between tabs on this device: new notifications arrive instantly as push */
window.addEventListener('storage',e=>{if(e.key!==KEY||!e.newValue)return;let n;try{n=JSON.parse(e.newValue)}catch(x){return}
const old=new Set(S.notifs.map(x=>x.id)),myRole=S.role;S=Object.assign(defaults(),n);S.role=myRole;S.descs=S.descs||[];S.customExams=S.customExams||[];S.doubts=S.doubts||[];S.materials=S.materials||[];S.materialRequests=S.materialRequests||[];S.reports=S.reports||[];S.results=S.results||[];
if(!sessionStorage.getItem('sx_out')&&S.role==='student'){const me=meUser();S.notifs.filter(x=>x.to&&!old.has(x.id)&&nVisible(x,me)&&!nRead(x,me)).slice(0,3).forEach(x=>pushLocal(x.t,x.b))}
const a=document.activeElement;if(!a||!/INPUT|TEXTAREA|SELECT/.test(a.tagName))render()});
/* user activity: opens + time on site (kept under its own key so it never overwrites app data) */
const ACTKEY='scholarx_activity_v1';
const actRead=()=>{try{return JSON.parse(localStorage.getItem(ACTKEY)||'{}')||{}}catch(e){return {}}};
function actUpdate(id,f){if(!id)return;const m=actRead();const r=m[id]||(m[id]={opens:0,secs:0,last:0});f(r);try{localStorage.setItem(ACTKEY,JSON.stringify(m))}catch(e){}}
function trackOpen(){if(sessionStorage.getItem('sx_out'))return;const u=meUser();if(!u.id||S.role!=='student')return;const k='sx_c_'+u.id;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');actUpdate(u.id,r=>{r.opens++;r.last=Date.now()})}
let _lt=Date.now();
setInterval(()=>{const now=Date.now(),dt=Math.min(15,Math.round((now-_lt)/1000));_lt=now;if(document.visibilityState!=='visible'||sessionStorage.getItem('sx_out')||S.role!=='student')return;const u=meUser();if(!u.id)return;if(!sessionStorage.getItem('sx_c_'+u.id))trackOpen();actUpdate(u.id,r=>{r.secs+=dt;r.last=now})},5000);
document.addEventListener('visibilitychange',()=>{_lt=Date.now()});
const fmtDur=sec=>{sec=Math.round(sec||0);if(sec<60)return sec+'s';const h=Math.floor(sec/3600),m=Math.floor(sec%3600/60);return h?h+'h '+m+'m':m+'m '+(sec%60)+'s'};
function activityCard(){const m=actRead();const rows=S.users.map(u=>{const r=m[u.id]||{opens:0,secs:0,last:0};return{u,...r}}).sort((a,b)=>b.secs-a.secs);const tot=rows.reduce((a,x)=>a+x.secs,0),op=rows.reduce((a,x)=>a+x.opens,0);
return `
User activity on the website
Time spent on the site (counted while the page is open and visible) and how many times each user opened it. Only users who used this website on this device are counted until the Supabase step.
${rows.filter(x=>x.opens).length}Users who opened the site
Everyone gets it as a notification. The owner can turn this power off at any time.
My published messages
${mine.length?`
${mine.map(n=>`
${esc(n.t)}
${esc(n.b)}
${new Date(n.ts).toLocaleString()}
`).join('')}
`:'
You have not published anything yet.
'}
`}
const swRow=(title,sub,on,act)=>`
${title}
${sub}
`;
const controlsCard=()=>`
Chat and publishing controls
${swRow('Administrators can publish messages','When on, administrators get a Publish message section and can send notifications to all users. Turn off to remove the power.',adminPubOn(),'adminpubtoggle')}${swRow('Emoji in community chat','When on, everyone gets an emoji picker in chat. When off, the picker is hidden and emoji in new messages are removed.',emojiOn(),'emojitoggle')}
`);
else if(!emojiOn())h=h.replace('Share a doubt or a resource link','Share a doubt or a resource link (emoji are turned off)');return h};
(function(){const _send=ACT.send;ACT.send=function(...a){const el=$('#cm-t');if(el&&!emojiOn()){const c=el.value.replace(EMO_RE,'');if(c!==el.value){el.value=c;toast('Emoji are turned off by the owner')}}return _send.apply(this,a)}})();
Object.assign(ACT,{
emopanel:()=>{const p=$('#emo-panel');if(p)p.style.display=p.style.display==='none'?'flex':'none'},
emo:v=>{if(!emojiOn())return;const el=$('#cm-t');if(!el)return;const a=el.selectionStart??el.value.length,b=el.selectionEnd??a;el.value=el.value.slice(0,a)+v+el.value.slice(b);el.focus();el.setSelectionRange(a+v.length,a+v.length)},
adminpubtoggle:(v,t)=>{if(S.role!=='owner'){render();return}S.flags.adminPublish=t.checked;own('Administrator publishing '+(t.checked?'turned on':'turned off'));save();toast('Administrator publishing '+(t.checked?'on':'off'))},
emojitoggle:(v,t)=>{if(S.role!=='owner'){render();return}S.flags.emoji=t.checked;own('Chat emoji '+(t.checked?'turned on':'turned off'));save();toast('Chat emoji '+(t.checked?'on':'off'))},
pubmsg:()=>{if(!canPublish()){toast('Publishing is turned off by the owner');render();return}const me=meUser(),t=val('pb-t').trim(),b=val('pb-b').trim();if(!t){toast('Write a title first');return}addNotif(t,(b?b+' — ':'')+me.name+' (administrator)','all');S.notifs[0].by=me.id;own('Administrator '+me.name+' published: '+t);save();render();toast('Published to all users')}
});
/* Powered by footer */
(function(){const st=document.createElement('style');st.textContent='.sxpow{display:flex;justify-content:center;align-items:center;gap:8px;margin:40px 0 6px;font:600 13px/1 system-ui,sans-serif;letter-spacing:.04em;flex-wrap:wrap}.sxpow .pb{color:#2f7bff}.sxpow .vn{font:800 17px/1 "Bricolage Grotesque",system-ui,sans-serif;letter-spacing:.14em;background:linear-gradient(100deg,#b87800,#f5b800 35%,#ffd23f 50%,#f5b800 65%,#b87800);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;filter:drop-shadow(0 0 1px rgba(60,40,0,.55));animation:sxshine 3.5s linear infinite}.sxpow svg{filter:drop-shadow(0 0 4px rgba(255,200,0,.9));animation:sxzap 1.6s ease-in-out infinite}.sxpow svg:last-child{animation-delay:.8s}@keyframes sxshine{to{background-position:-200% 0}}@keyframes sxzap{0%,100%{opacity:.55;transform:scale(.92)}50%{opacity:1;transform:scale(1.1)}}';document.head.appendChild(st)})();
const BOLT='';
const _r6=render;render=function(){_r6();if(document.querySelector('.sxpow'))return;const host=document.querySelector('.main')||document.querySelector('#app .full>div')||document.querySelector('#app .full');if(host)host.insertAdjacentHTML('beforeend',`
Powered by${BOLT}VISHNOI${BOLT}
`)};
['magsave','magreset','magov','magdel','maghubs'].forEach(k=>{const f=ACT[k];ACT[k]=function(){if(!canMagEdit()){toast('Only the owner, administrators and moderators can change magazine links');return}return f.apply(this,arguments)}});
/* ---------- start ---------- */
if(OWN&&S.role==='owner'){S.role='student';UI.page='dash';save()}
magCheck();magFeed();setInterval(magCheck,6*3600*1000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)magCheck()});
trackOpen();render();
if(!sessionStorage.getItem('sx_out'))sxIntro();
setTimeout(()=>{if(!sessionStorage.getItem('sx_out')&&S.role==='student'){const c=unreadCount();if(c)pushLocal('You have '+c+' new notification'+(c>1?'s':''),'Tap the bell to read')}},6000);
if(!S.flags.onboarded)setTimeout(()=>{if(!sessionStorage.getItem('sx_out'))examModal()},5200);else if(!S.flags.tg)setTimeout(()=>{if(!sessionStorage.getItem('sx_out'))tgModal()},5200);