IRG IRG-GDPGold Denominated Product
Dashboard
(User's name)
Notifications
Register Customer
Earmark Gold
What you can do
Your Reports
Disputes
IRG-GDP
Your Wallet
GDPs @
g 24-carat gold
@ LBMA rate
Rs.
View details
The IPRs of the GDP, GIC, JDB, JR, and the system built around them are registered in the name of Rohit Tidke, who has exclusively assigned to Intech Research Group
Ask me

Ask anything about IRG-GDP, FTRs, swaps, audits or how the platform works.

'; var blob=new Blob([html],{type:'text/html'}); var a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='IRG-GDP-User-Policy.html'; a.click(); URL.revokeObjectURL(a.href); } /* ── Sign in / OTP / Change password ────────────────────────────────────── Ported from dashboard.html:533-700. Same fields, same links, same endpoints, same 24-second resend, same Firebase Google-SMS path, same messages. Two necessary adaptations, and only two: - the token is written to localStorage, which is what this shell and the 27 pages it opens both read (dashboard.html uses sessionStorage); - after sign-in it paints this shell's own screens instead of calling dashboard.html's renderRail/viewDashboard. */ var S={lastEmail:'',lastMobile:'',otpPurpose:''}; /* fx0807b — ONE storage rule for every door (7 Aug 2026). Remember me ticked -> localStorage only. Unticked -> sessionStorage only. Never both: writing to both let the two disagree and signed users out. */ function irgPutToken(t,remember){ try{ localStorage.removeItem('irg_token'); sessionStorage.removeItem('irg_token'); (remember?localStorage:sessionStorage).setItem('irg_token',t); localStorage.setItem('irg_remember',remember?'1':'0'); }catch(e){} } function irgGetToken(){ try{ return localStorage.getItem('irg_token')||sessionStorage.getItem('irg_token')||''; }catch(e){ return ''; } } function irgClearToken(){ try{ localStorage.removeItem('irg_token'); sessionStorage.removeItem('irg_token'); localStorage.removeItem('irg_user'); sessionStorage.removeItem('irg_user'); }catch(e){} } function renderSignIn(el,note){ var _rem='1'; try{ _rem=localStorage.getItem('irg_remember')||'1'; if(!S.lastEmail){ S.lastEmail=localStorage.getItem('irg_last_email')||''; } }catch(e){} el.innerHTML='
'+ '

Sign In

Welcome back. Enter your credentials to continue.
'+ (note?'
'+esc(note)+'
':'')+ '
'+ ''+ ''+ ''+ ''+ '
'+ ''+ 'Forgot password?'+ '
'+ ''+ '
New to IRG-GDP? Sign Up'+ '
Change password
'+ '
'; el.querySelector('#siGo').onclick=doLogin; el.querySelector('#lkReg').onclick=function(){ openPane('Register',renderRegister); }; el.querySelector('#lkForgot').onclick=doForgot; el.querySelector('#lkChange').onclick=function(){ openPane('Change password',renderChangePassword); }; } function viewLater(t){ openPane(t,function(el){ el.innerHTML='

'+esc(t)+'

'+ '
The '+esc(t)+' screen ships in a later part of this build.
'; }); } function doLogin(){ var email=document.getElementById('siEmail').value.trim().toLowerCase(); var pass=document.getElementById('siPass').value, m=document.getElementById('siMsg'); if(!email||!pass){ m.className='msg err'; m.textContent='Enter email and password.'; return; } api('/api/v1/auth/login/',{method:'POST',body:{email:email,password:pass}}) .then(function(j){ /* 31 Jul 2026 — no OTP on sign-in. Same end state as before. */ if(j&&j.token){ TOKEN=j.token; irgPutToken(j.token,false); /* fx0807i: session-only, always */ var _r=document.getElementById('siRem'); try{ if(_r&&_r.checked){ localStorage.setItem('irg_last_email',(document.getElementById('siEmail').value||'').trim()); } else{ localStorage.removeItem('irg_last_email'); } }catch(e){} authState(); loadMe(); loadWallet(); loadNotifCount(); /* fx0807h — return the caller to the page that sent them here. */ if(window.__authNext){ var _n=window.__authNext; window.__authNext=''; location.href=_n; return; } irgRouteAfterSignin(); } else { m.className='msg err'; m.textContent=(j&&j.message)||'Sign-in failed.'; } }) .catch(function(e){ m.className='msg err'; m.textContent='Sign-in failed.'; }); } function doForgot(){ var email=document.getElementById('siEmail').value.trim().toLowerCase(); var m=document.getElementById('siMsg'); if(!email){ m.className='msg err'; m.textContent='Enter your email first, then tap Forgot password.'; return; } api('/api/v1/auth/forgot-password/',{method:'POST',body:{email:email}}) .then(function(j){ m.className='msg ok'; m.textContent=(j&&(j.message||j.detail))||'Reset code sent to your registered contact.'; }) .catch(function(){ m.className='msg err'; m.textContent='Request failed.'; }); } /* Firebase Google-SMS helpers — dashboard.html:646-670, unchanged */ var _fbConfirmation=null,_fbVerifier=null; function _waitFirebase(cb,t){ t=t||0; if(window.firebase&&window.firebase.auth){cb();return;} if(t>60){cb(new Error('Firebase SDK not loaded'));return;} setTimeout(function(){_waitFirebase(cb,t+1);},100); } function _ensureVerifier(){ if(_fbVerifier) return _fbVerifier; _fbVerifier=new firebase.auth.RecaptchaVerifier('recaptcha-container', {size:'invisible',callback:function(){},'expired-callback':function(){_fbVerifier=null;}}); return _fbVerifier; } function _normMobile(raw){ var m=String(raw||'').replace(/[\s\-]/g,''); if(/^0\d{10}$/.test(m)) m='+91'+m.slice(1); if(/^\d{10}$/.test(m)) m='+91'+m; if(/^91\d{10}$/.test(m)) m='+'+m; return m; } function _fbErr(e){ var c=(e&&e.code)||''; if(c==='auth/invalid-phone-number') return 'Registered mobile is not valid for OTP.'; if(c==='auth/too-many-requests') return 'Too many OTP requests. Wait a few minutes.'; if(c==='auth/quota-exceeded') return 'Daily SMS quota exceeded. Try tomorrow.'; if(c==='auth/unauthorized-domain') return 'Domain not authorised in Firebase.'; return (e&&e.message)||'Could not send OTP.'; } function _sendGoogleSMS(mobile,onSent){ _waitFirebase(function(err){ if(err){onSent(err);return;} try{ var v=_ensureVerifier(); firebase.auth().signInWithPhoneNumber(_normMobile(mobile),v) .then(function(cr){_fbConfirmation=cr; onSent(null);}) .catch(function(e){onSent(e);}); }catch(e){ onSent(e); } }); } function renderOtp(el,email,purpose,mobile){ purpose=purpose||'LOGIN_2FA'; var _titles={REGISTER:'Confirm registration', PASSWORD_RESET:'Confirm password reset', PROFILE_EDIT:'Confirm profile change'}; var title=_titles[purpose]||'Confirm registration'; el.innerHTML='

'+esc(title)+'

'+ '
sending a one-time code to your registered mobile\u2026
'+ '
'+ '
'+ '
'+ '
'+ '
'+ ''+ ''+ '
'+ '
'; el.querySelector('#otpGo').onclick=function(){ doVerify(email,purpose); }; el.querySelector('#resendBtn').onclick=doResend; var om=el.querySelector('#otpMsg'); var targetMobile=mobile||S.lastMobile||''; if(targetMobile){ om.className='msg info'; om.textContent='Sending code to your mobile\u2026'; _sendGoogleSMS(targetMobile,function(err){ if(err){ om.className='msg err'; om.textContent='Could not send mobile code: '+_fbErr(err)+' You may also check your email for a code.'; } else { om.className='msg info'; om.textContent='Code sent to your mobile. Enter it above.'; } }); } else { om.className='msg info'; om.textContent='A code has been sent to your registered contact. Enter it above.'; } var secs=24; var t=setInterval(function(){ secs--; var b=document.getElementById('resendBtn'); if(!b){clearInterval(t);return;} if(secs<=0){ b.disabled=false; b.textContent='Resend OTP'; clearInterval(t); } else b.textContent='Resend in '+secs+' seconds'; },1000); } function doVerify(email,purpose){ purpose=purpose||'LOGIN_2FA'; var otp=document.getElementById('otpVal').value.trim(), m=document.getElementById('otpMsg'); if(!/^\d{6}$/.test(otp)){ m.className='msg err'; m.textContent='Enter the 6-digit code.'; return; } var finish=function(firebaseToken){ var payload={email:email,otp:otp,purpose:purpose}; if(firebaseToken){ payload.firebase_id_token=firebaseToken; payload.phone_verified_by_firebase=true; } api('/api/v1/auth/verify-otp/',{method:'POST',body:payload}) .then(function(j){ if(j&&j.token){ TOKEN=j.token; irgPutToken(j.token,true); authState(); loadMe(); loadWallet(); loadNotifCount(); menuOpen('products'); } else if(firebaseToken){ m.className='msg err'; m.textContent=(j&&(j.message||j.error))||'Mobile confirmed, but sign-in could not complete. Try the code sent to your email.'; } else { m.className='msg err'; m.textContent=(j&&(j.message||j.error))||'OTP verification failed.'; } }) .catch(function(){ m.className='msg err'; m.textContent='OTP verification failed.'; }); }; if(_fbConfirmation){ _fbConfirmation.confirm(otp).then(function(cred){ return (cred&&cred.user)?cred.user.getIdToken():''; }).then(finish).catch(function(e){ var c=(e&&e.code)||''; m.className='msg err'; m.textContent = c==='auth/invalid-verification-code' ? 'That code is not correct. Please re-check.' : c==='auth/code-expired' ? 'The code expired. Please resend.' : _fbErr(e); }); } else finish(''); } function doResend(){ api('/api/v1/auth/resend-otp/',{method:'POST', body:{email:S.lastEmail,purpose:S.otpPurpose||'LOGIN_2FA'}}) .then(function(j){ var m=document.getElementById('otpMsg'); if(!m) return; m.className='msg info'; m.textContent=(j&&(j.message||j.detail))||'OTP resent.'; }) .catch(function(){ var m=document.getElementById('otpMsg'); if(!m) return; m.className='msg err'; m.textContent='Could not resend.'; }); } /* Change password — dashboard.html:672-706. forgot-password code, then reset. */ /* ── Change password, in two screens ─────────────────────────────────────── Screen 1 takes the email and the code. Screen 2 is the password protocol on its own, as it should be. One limitation, stated because it cannot be engineered away from the front end: screen 1 checks that the code is six digits, it does not verify it against the server. core/views.py:135-157 _consume_otp is destructive — a correct code is marked is_used and a wrong one burns an attempt against a lockout — and there is no check-only endpoint. So verifying on screen 1 would spend the code and /auth/reset-password/ would then refuse with 'No verification code is pending.' The code is therefore proven when the new password is submitted, and a bad code returns you to screen 1. */ function renderChangePassword(el){ el.innerHTML='

Change password

'+ '
Step 1 of 2 \u2014 confirm who you are
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ ''+ ''+ '
'+ '
'; el.querySelector('#cpSendBtn').onclick=cpSend; el.querySelector('#cpNext').onclick=function(){ var m=el.querySelector('#cpMsg'); var email=el.querySelector('#cpEmail').value.trim().toLowerCase(); var otp=el.querySelector('#cpOtp').value.trim(); if(!email){ m.className='msg err'; m.textContent='Enter your email.'; return; } if(!/^\d{6}$/.test(otp)){ m.className='msg err'; m.textContent='Enter the 6-digit code sent to you.'; return; } S.lastEmail=email; openPane('Set new password', function(e){ renderNewPassword(e,email,otp); }); }; } function cpSend(){ var email=document.getElementById('cpEmail').value.trim().toLowerCase(); var m=document.getElementById('cpMsg'); if(!email){ m.className='msg err'; m.textContent='Enter your email first.'; return; } m.className='msg info'; m.textContent='Sending\u2026'; api('/api/v1/auth/forgot-password/',{method:'POST',body:{email:email}}) .then(function(j){ m.className='msg ok'; m.textContent=(j&&(j.message||j.detail))||'Code sent to your registered contact.'; }) .catch(function(){ m.className='msg err'; m.textContent='Could not send the code.'; }); } function renderNewPassword(el,email,otp){ el.innerHTML='

Set new password

'+ '
Step 2 of 2 \u2014 for '+esc(email)+'
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ ''+ '
'+ '
'+ ''+ ''+ '
'+ '
'; function tick(){ var v=el.querySelector('#npNew').value, w=el.querySelector('#npNew2').value; var ok={len:v.length>=8,up:/[A-Z]/.test(v),num:/[0-9]/.test(v), sp:/[^A-Za-z0-9]/.test(v),same:v.length>0&&v===w}; el.querySelectorAll('#npRules li').forEach(function(li){ li.classList.toggle('ok',!!ok[li.getAttribute('data-k')]); }); return ok; } el.querySelector('#npNew').oninput=tick; el.querySelector('#npNew2').oninput=tick; el.querySelector('#npBack').onclick=function(){ openPane('Change password',renderChangePassword); }; el.querySelector('#npGo').onclick=function(){ var m=el.querySelector('#npMsg'), ok=tick(); if(!ok.len||!ok.up||!ok.num||!ok.sp){ m.className='msg err'; m.textContent='The password does not meet all four rules above.'; return; } if(!ok.same){ m.className='msg err'; m.textContent='The two entries do not match.'; return; } var b=el.querySelector('#npGo'); b.disabled=true; b.textContent='Changing\u2026'; api('/api/v1/auth/reset-password/',{method:'POST',body:{ identifier:email, otp:otp, new_password:el.querySelector('#npNew').value, confirm_password:el.querySelector('#npNew2').value}}) .then(function(j){ m.className='msg ok'; m.textContent=(j&&(j.message||j.detail))||'Password changed. Sign in with the new password.'; b.textContent='Changed'; }) .catch(function(e){ var msg=(e&&e.message)||'Change failed.'; m.className='msg err'; m.innerHTML=esc(msg)+ ' Request a new code'; var r=el.querySelector('#npRetry'); if(r) r.onclick=function(){ openPane('Change password',renderChangePassword); }; b.disabled=false; b.textContent='Change password'; }); }; } /* Sign on — dashboard.html:708, word for word. It is not a registration form. */ /* renderSignOn deleted 31 Jul 2026 — there is no separate Sign on page. The Sign on button opens Register directly. */ function renderLanguage(el){ el.innerHTML='

Language

Choose the language for this session.

'+ '

'+ '

'; el.querySelector('#lgGo').onclick=function(){ var v=el.querySelector('#lgSel').value; try{ localStorage.setItem('irg_lang',v); }catch(e){} document.documentElement.lang=v; el.querySelector('#lgOut').innerHTML='

Language set to '+esc(v)+' for this session.

'; }; } /* ---------------- live data ---------------- */ function loadMe(){ if(!TOKEN) return; api('/api/v1/auth/users/me/').then(function(u){ var n=u.full_name||u.name||u.email||''; if(n){ document.getElementById('uname').textContent=n; /* blue circle carries the first initial of that same name */ var ini=(n.trim().charAt(0)||''); document.getElementById('avatar').textContent=ini; } ROLES = (u.roles||[]).filter(function(r){ return r.status==='ACTIVE'; }) .map(function(r){ return r.role; }); applyRoleGating(); refreshRoleViews(); loadOfficer(); /* 8 Aug 2026 — ckRoute() used to be called here. Wrong place: a superuser's status lives in IS_SU, which loadOfficer() sets from a second, asynchronous call. At this point IS_SU is still false. The call now sits inside loadOfficer(), where the answer exists. */ }).catch(function(){ ROLES=[]; applyRoleGating(); }); } /* Officer caps and superuser status. officer_views.py:199-220 */ function loadOfficer(){ if(!TOKEN) return; api('/m/officer/whoami/').then(function(w){ IS_SU = !!w.is_superuser; OFFICER = (w.officer_roles||[]).map(function(o){ return o.role; }).filter(Boolean); showRoles(); refreshRoleViews(); /* 8 Aug 2026 — the FIRST moment isAdminRole() can be true. A superuser holds no UserRole rows for the officer caps; the whole answer is IS_SU, assigned two lines above. */ try{ ckRoute(); }catch(e){} }).catch(function(){ IS_SU=false; OFFICER=[]; showRoles(); refreshRoleViews(); /* called on failure too: ckRoute() will decline, which is right, but it must not be left permanently pending. */ try{ ckRoute(); }catch(e){} }); } function loadWallet(){ var err=document.getElementById('wErr'); err.textContent=''; if(!TOKEN){ err.textContent='Sign in to see your wallet.'; return; } /* portfolio counts are nested under .summary (irg_gdp/views.py:156). /oracle/benchmark/ is a ModelViewSet LIST over BenchmarkValue and carries none of these fields; /benchmark/today/ (oracle/urls.py:12) returns the derivation card with inr_per_gram, inr_per_gdp_unit, grams_per_gdp_unit (oracle/benchmark.py:202-214). Values arrive as strings. */ Promise.all([ api('/api/v1/gdp/units/portfolio/'), api('/api/v1/oracle/benchmark/today/') ]) .then(function(r){ var sm=(r[0]||{}).summary||{}, b=r[1]||{}; var num=function(x){ var n=Number(x); return isFinite(n)?n:null; }; var units = num(sm.total_units); var perG = num(b.inr_per_gram); var perU = num(b.grams_per_gdp_unit); var perUnitInr = num(b.inr_per_gdp_unit); if(units!=null) document.getElementById('wUnits').textContent=units; if(perU!=null) document.getElementById('wGrams').textContent=perU; if(units!=null){ var total = perUnitInr!=null ? units*perUnitInr : (perU!=null&&perG!=null ? units*perU*perG : null); if(total!=null) document.getElementById('wInr').textContent=inr(Math.round(total)); } }) .catch(function(e){ err.textContent = (e.message==='NOT_SIGNED_IN') ? 'Sign in to see your wallet.' : 'Wallet unavailable just now.'; }); } function loadNotifCount(){ if(!TOKEN) return; api('/api/v1/notifications/merged/unread-count/').then(function(j){ var n=j.count!=null?j.count:j.unread, b=document.getElementById('notifBadge'); if(n>0){ b.textContent=n; b.style.display='inline-block'; } }).catch(function(){}); } document.getElementById('wdet').onclick=function(){ openUrl('Your Wallet','wallet.html'); }; /* ---------------- Ask me (floating, inside the window) ---------------- /api/v1/ask/ is AllowAny (system_ask/views.py:67), so this works signed out. Nothing here writes to #wbody — the content already in the window stays. */ var ASKW=document.getElementById('askwin'), ASKT=document.getElementById('askThread'), ASKQ=document.getElementById('askQ2'); function askOpen(){ ASKW.classList.add('on'); ASKQ.focus(); } function askClose(){ ASKW.classList.remove('on'); } document.getElementById('btnAsk').onclick=function(){ ASKW.classList.contains('on') ? askClose() : askOpen(); }; document.getElementById('askClose').onclick=askClose; function askSend(){ var q=ASKQ.value.trim(); if(!q) return; ASKQ.value=''; var qe=document.createElement('p'); qe.className='q'; qe.textContent=q; ASKT.appendChild(qe); var ae=document.createElement('p'); ae.className='a'; ae.textContent='Asking\u2026'; ASKT.appendChild(ae); ASKT.scrollTop=ASKT.scrollHeight; api('/api/v1/ask/',{method:'POST',body:{question:q}}) .then(function(j){ ae.textContent=j.answer||j.reply||JSON.stringify(j); ASKT.scrollTop=ASKT.scrollHeight; }) .catch(function(e){ ae.className='a err'; ae.textContent='Could not reach the answer service ('+e.message+').'; ASKT.scrollTop=ASKT.scrollHeight; }); } document.getElementById('askGo2').onclick=askSend; ASKQ.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); askSend(); } }); window.IRGShellOpen=function(t,u){ openUrl(t,u); return true; }; /* Register Customer and Earmark Gold are jeweller-only. activeRoles() mirrors dashboard.html:305 — S.user.roles is a list of {role,status}; only ACTIVE ones count. Not signed in means no roles, so both items are inactive, which is the correct default. This is display gating only. The Django API must repeat the check on /api/v1/mint-proposal/register-customer/ and the earmark endpoints — a browser-side test is not a control. */ var ROLES = []; /* active UserRoles */ var OFFICER = []; /* active OfficerAppointment roles */ var IS_SU = false; /* Django is_superuser, from whoami */ /* Superuser holds every officer cap that has no appointed holder (system/models.py:1832-1840). */ function effectiveRoles(){ var r = ROLES.slice(); OFFICER.forEach(function(o){ if(r.indexOf(o)===-1) r.push(o); }); if(IS_SU) OFFICER_ROLES.forEach(function(o){ if(r.indexOf(o)===-1) r.push(o); }); if(IS_SU && r.indexOf('SUPERUSER')===-1) r.push('SUPERUSER'); return r; } function hasRole(x){ return effectiveRoles().indexOf(x) !== -1; } function isJeweller(){ return ROLES.indexOf('JEWELER') !== -1; } /* Build one sub-menu page per role block, so the user can see which of their roles each item belongs to. Roles they do not hold are simply absent. */ function roleBlocks(map, title, makeHandler){ return function(el){ var mine = effectiveRoles(); var blocks = []; if(map.PUBLIC && map.PUBLIC.length) blocks.push(['Open to everyone', map.PUBLIC]); if(TOKEN && map.EVERYONE && map.EVERYONE.length) blocks.push(['Any signed-in user', map.EVERYONE]); mine.forEach(function(r){ if(map[r] && map[r].length) blocks.push([ROLE_LABEL[r]||r, map[r]]); }); var via=servedViaSuperuser(); if(via.length && map.SUPERUSER && map.SUPERUSER.length && mine.indexOf('SUPERUSER')===-1){ blocks.push([via.map(function(r){return ROLE_LABEL[r]||r;}).join(' / ')+ ' \u2014 via Superuser', map.SUPERUSER]); } if(!blocks.length){ el.innerHTML='

'+esc(title)+'

'+ (TOKEN ? 'Your account holds no active role yet, so there is nothing here. '+ 'A role is activated by the operator after registration.' : 'Please sign in. What you can do depends on the roles your account holds.')+ '
'; return; } var h='

'+esc(title)+'

', idx=0, flat=[]; blocks.forEach(function(b){ h+='
'+esc(b[0])+'
'; }); el.innerHTML=h+'
'; el.querySelectorAll('.submenu a').forEach(function(a){ a.onclick=function(){ makeHandler(flat[Number(a.getAttribute('data-i'))]); }; }); }; } function roleActions(){ return roleBlocks(ACTIONS,'What you can do',function(row){ openTarget(row); }); } function roleReports(){ return roleBlocks(REPORTS_BY_ROLE,'Your Reports',function(row){ openPane(row[0], misReport(row[0],row[1])); }); } /* Aliases taken verbatim from the established table in dashboard.html:292-295. Banned terms never appear on screen: 'investor' -> 'Buyer / Holder', 'minter' -> 'Creator (FTR)'. ADVISORY_BOARD and SUPERUSER are not in that table; their labels carry no banned term. */ var ROLE_LABEL={ JEWELER:'Jeweller', HOUSEHOLD:'Household', INVESTOR:'Buyer / Holder', RETURNEE:'JR Holder', DESIGNER:'Jewellery Designer', LICENSEE:'GIC Licensee', MARKETMAKER:'Market Maker', CONSULTANT:'Consultant', MINTER:'Creator (FTR)', OMBUDSMAN:'Ombudsman', TRUSTEE:'Trustee Banker', ADMIN:'Administrator', ADVISORY_BOARD:'Advisory Board', SUPERUSER:'Superuser', AUDITOR:'Auditor' }; function isAdvertiser(){ return hasRole('ADVERTISER'); } function isAdminRole(){ return hasRole('ADMIN') || hasRole('SUPERUSER'); } /* 2 Aug 2026 — role gating used to cover jewellers alone, so every signed-in person saw the advertising and admin rail entries whether they held the role or not. Part 4 will drive this from /api/v1/taxonomy/me/; until then the two rails added yesterday are gated the same way jewellers already were. */ function applyRoleGating(){ var ok = isJeweller(); document.querySelectorAll('.rit.jw').forEach(function(r){ r.classList.toggle('off', !ok); r.title = ok ? '' : 'Available to jewellers only'; }); var adv = isAdvertiser(); document.querySelectorAll('.rit.adv').forEach(function(r){ r.classList.toggle('off', !adv); r.title = adv ? '' : 'Available to advertisers only'; }); var adm = isAdminRole(); document.querySelectorAll('.rit.adm').forEach(function(r){ r.classList.toggle('off', !adm); r.title = adm ? '' : 'Available to administrators only'; }); } /* The products pane is painted at boot, before /users/me/ has answered, so it starts with no roles. Repaint it — and only it — once the roles are known. Never repaints a form pane, which would wipe what the user has typed. */ function refreshRoleViews(){ var cur=HIST[HPOS]; if(cur && cur.key==='products') paint(cur); } function showRoles(){ var el=document.getElementById('rroles'); el.innerHTML=''; effectiveRoles().forEach(function(r){ var c=document.createElement('span'); c.textContent=ROLE_LABEL[r]||r; el.appendChild(c); }); } /* ECO decision: Sign In / Sign on go inactive once signed in */ function authState(){ var on = !!TOKEN; document.getElementById('btnSignin').disabled = on; document.getElementById('btnSignon').disabled = on; var off = document.getElementById('btnSignoff'); if (off) off.style.display = on ? '' : 'none'; } /* 31 Jul 2026 — X is Sign off. The token lives in sessionStorage, so closing the browser signs off on its own; this does it on demand. */ document.getElementById('btnSignoff').onclick=function(){ TOKEN=''; irgClearToken(); authState(); menuOpen('products'); openPane('Sign in',function(e){ renderSignIn(e,'You have been signed off.'); }); }; /* start — 8 Aug 2026: the first screen now depends on who signed in. gdp-shell has always painted Products at boot, BEFORE /users/me/ has answered, so the roles were not known yet. A superuser therefore landed on the customer screen. Painting Products and then jumping would look broken, so the menu opens as before (the rail must be there) and the PANE is decided once the roles arrive. A non-superuser sees exactly what they saw before. */ menuOpen('products'); window.__ckRouted=false; function ckRoute(){ if(window.__ckRouted) return; if(!TOKEN) return; /* signed out: leave as-is */ /* 8 Aug 2026 — the auth= guard that used to sit here blocked the cockpit for good: sign-in happens inside the shell now, and ?auth=signin stays in the address bar afterwards. The !TOKEN check above already covers anyone still typing their password. */ if(location.search.indexOf('pane=')>-1) return; /* explicit pane wins */ if(location.search.indexOf('v=')>-1) return; /* explicit ?v= wins too */ if(typeof isAdminRole!=='function' || !isAdminRole()) return; window.__ckRouted=true; /* fx0815c (15 Aug 2026) - the cockpit repaints the window, so the menu highlight left behind by the boot-time menuOpen('products') must be cleared. Without this the strip says GDP Products while the window shows Administration, and the menu looks dead. Same one line that menuOpen() itself uses. Superuser still LANDS on Administration. */ document.querySelectorAll('.menu a').forEach(function(a){ a.classList.remove('on'); }); try{ openPane('Administration', renderCockpit, 'cockpit'); }catch(e){} } if (location.search.indexOf('pane=advertise') > -1) { try{ openPane('Advertise with IRG', renderAdvertise); }catch(e){} } /* fx0815b (15 Aug 2026) - every menu and dashboard target opens INSIDE the shell window. Public pages link here as gdp-shell.html?v=. MENU keys are dispatched through menuOpen(); rail keys are dispatched by clicking the real .rit element, so the code that runs is exactly the code a user click runs - one path, nothing decorative. Unknown keys are ignored. */ (function(){ var q; try{ q=new URLSearchParams(location.search); }catch(e){ return; } var v=q.get('v'); if(!v) return; if(typeof MENU==='object' && MENU[v]){ try{ menuOpen(v); }catch(e){} return; } var r=document.querySelector('.rit[data-r="'+v+'"]'); if(r){ try{ r.click(); }catch(e){} } })(); /* fx0807h (7 Aug 2026) — sign-in belongs in the shell window. sign-in.html now redirects here carrying ?next= and ?role=. The shell previously understood neither, which is why redirecting alone did not work. */ window.__authNext=''; window.__authRole=''; (function(){ var q; try{ q=new URLSearchParams(location.search); }catch(e){ return; } if(q.get('auth')!=='signin') return; var nx=q.get('next')||''; /* same-origin only: a bare filename or a rooted path. Never an absolute URL. */ if(nx && /^[A-Za-z0-9._~\/-]+$/.test(nx) && nx.indexOf('//')<0){ window.__authNext=nx; } window.__authRole=q.get('role')||''; try{ openPane('Sign in', function(e){ renderSignIn(e); }); }catch(e){} })(); /* fx0807i (7 Aug 2026) — launch rule, same on desktop and mobile. Registered on this device (flag written at first successful sign-in) means sign-in comes FIRST on every launch: this platform deals with money, and a phone can land in someone else's hand. Tokens therefore live only in sessionStorage now — nothing walks a holder of the device into an account. */ (function(){ var reg='',seen=''; try{ reg=localStorage.getItem('irg_registered')||''; seen=sessionStorage.getItem('irg_launch')||''; }catch(e){} if(reg==='1' && seen!=='1'){ try{ localStorage.removeItem('irg_token'); }catch(e){} /* retire any legacy persistent token */ window.__launchGate=true; try{ openPane('Sign in', function(e){ renderSignIn(e); }); }catch(e){} } })(); function irgRouteAfterSignin(){ try{ sessionStorage.setItem('irg_launch','1'); }catch(e){} try{ localStorage.setItem('irg_registered','1'); }catch(e){} api('/api/v1/notifications/merged/unread-count/').then(function(j){ var n=(j&&(j.count!=null?j.count:j.unread))||0; if(n>0){ openPane('Notifications', renderInbox); } else { openPane('What you can do', roleActions()); } }).catch(function(){ openPane('What you can do', roleActions()); }); } authState(); applyRoleGating(); loadMe(); loadWallet(); loadNotifCount(); arrows(); tools(); })();