Add access-code gate

This commit is contained in:
zouyun
2026-08-24 17:25:06 +08:00
parent d8581e6381
commit 0f214fd1ee
2 changed files with 8 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"version": 1,
"salt": "amazon-za-report-access-v1",
"hash": "3ffcf23473036ad716ae6862a3be38dd9a2158d5a637681d9f0a962e69692161"
}
+3 -4
View File
@@ -12,7 +12,7 @@
@media(prefers-color-scheme:dark){:root{--bg:#0c1220;--panel:#121a2a;--ink:#eef3fb;--muted:#9ba9bd;--line:#28354a;--soft:#182743;--shadow:none}}
</style>
</head>
<body><div id="login" class="login-shell"><form id="loginForm" class="login-card" autocomplete="on"><div class="eyebrow">Restricted Report</div><h1>Sign in to view</h1><p>Enter your authorized email and password.</p><label class="field">Email<input id="email" type="email" autocomplete="username" required></label><label class="field">Password<input id="password" type="password" autocomplete="current-password" required></label><button id="loginButton" type="submit">Sign in</button><div id="loginError" class="login-error" role="alert" aria-live="polite"></div><p class="access-note">Access is limited to authorized email addresses.</p></form></div><main id="report" hidden>
<body><div id="login" class="login-shell"><form id="loginForm" class="login-card" autocomplete="off"><div class="eyebrow">Restricted Report</div><h1>Enter access code</h1><p>Enter the report access code to continue.</p><label class="field">Access code<input id="accessCode" type="password" autocomplete="off" required autofocus></label><button id="loginButton" type="submit">Unlock report</button><div id="loginError" class="login-error" role="alert" aria-live="polite"></div><p class="access-note">Access is limited to people who have the code.</p></form></div><main id="report" hidden>
<header class="hero"><div class="eyebrow">Daily Delivery · ZA</div><h1>Amazon - ZA daily tracker</h1><div class="status"><span class="dot"></span><span id="range">Loading data…</span></div></header>
<section><h2>Key Metrics</h2><div class="summary" id="cards"></div></section>
<section><h2>August Budget Pacing</h2><div class="panel budget-panel"><table class="budget-table"><thead><tr><th>Aug</th><th>ZAR</th><th>USD</th></tr></thead><tbody id="budgetRows"></tbody></table></div></section>
@@ -24,7 +24,6 @@ const fmt=n=>new Intl.NumberFormat('en-US').format(n), money=n=>'$'+new Intl.Num
const label=d=>d.slice(5).replace('-','/');
function bars(el,data,series,max){const showValues=data.length<=12;el.style.minWidth=Math.max(0,data.length*48)+'px';el.innerHTML=data.map(d=>`<div class="bar-group">${series.map(s=>`<div class="bar" style="height:${Math.max(2,d[s.key]/max*100)}%;background:${s.color}">${showValues?`<span>${s.format(d[s.key])}</span>`:''}</div>`).join('')}<div class="bar-label">${label(d.date)}</div></div>`).join('')}
function lineChart(el,data,series,format){const w=Math.max(760,data.length*42),h=260,p={l:48,r:18,t:18,b:34},vals=data.flatMap(d=>series.map(s=>Number(d[s.key])||0)),max=Math.max(...vals,1),min=0,x=i=>p.l+i*(w-p.l-p.r)/Math.max(data.length-1,1),y=v=>h-p.b-(v-min)*(h-p.t-p.b)/(max-min||1),ticks=[0,.25,.5,.75,1];el.innerHTML=`<svg class="trend-svg" viewBox="0 0 ${w} ${h}" role="img">${ticks.map(t=>`<line class="trend-grid" x1="${p.l}" x2="${w-p.r}" y1="${y(max*t)}" y2="${y(max*t)}"/><text class="trend-axis" x="${p.l-7}" y="${y(max*t)+3}" text-anchor="end">${format(max*t)}</text>`).join('')}${data.map((d,i)=>`<text class="trend-axis" x="${x(i)}" y="${h-10}" text-anchor="middle">${label(d.date)}</text>`).join('')}${series.map(s=>`<polyline class="trend-line" stroke="${s.color}" points="${data.map((d,i)=>`${x(i)},${y(d[s.key]||0)}`).join(' ')}"/>${data.map((d,i)=>`<circle class="trend-point" fill="${s.color}" cx="${x(i)}" cy="${y(d[s.key]||0)}" r="3"><title>${d.date} · ${s.name}: ${format(d[s.key]||0)}</title></circle>`).join('')}`).join('')}</svg>`}
const ACCESS={emails:['a91dc567bcdb6d054cf121d7e4016454ee3d316e3b452b851ae4d3087bc99bb2'],salt:'amazon-za-delivery-2026-08',password:'2344c6c9cf72430cafbf48e014d7f721a53484f766f1455f0c12232515ec61e9'};
async function sha256(value){const bytes=new TextEncoder().encode(value);const digest=await crypto.subtle.digest('SHA-256',bytes);return [...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')}
function showReport(){document.getElementById('login').hidden=true;document.getElementById('report').hidden=false;loadReport()}
async function loadReport(){fetch('data.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw Error('data.json '+r.status);return r.json()}).then(data=>{
@@ -36,6 +35,6 @@ async function loadReport(){fetch('data.json',{cache:'no-store'}).then(r=>{if(!r
document.getElementById('rows').innerHTML=all.map((x,i)=>{const prev=i?all[i-1]:null,spike=x.is_actual&&prev&&prev.is_actual&&Math.abs((x.e1e2_rate||0)-(prev.e1e2_rate||0))>=5;return `<tr class="${x.is_actual?'':'future'}"><td>${x.date}</td><td>${fmt(x.install)}</td><td>${fmt(x.event1)}</td><td>${fmt(x.event2)}</td><td>${fmt(x.e1e2)}</td><td class="${spike?'rate-spike':''}">${pct(x.e1e2_rate)}</td><td>${zar(x.e1e2*payinZar)}</td><td>${zar(dailyBudgetZar)}</td><td>${pct(x.e1e2/dailyCap*100)}</td><td>${money(x.midas_spend)}</td></tr>`}).join('');
document.getElementById('weeklyRows').innerHTML=data.weekly.map(w=>`<tr><td>${w.label}</td><td>${fmt(w.install)}</td><td>${fmt(w.e1e2)}</td><td>${pct(w.e1e2_rate)}</td><td>${zar(w.e1e2*payinZar)}</td><td>${money(w.midas_spend)}</td></tr>`).join('');
}).catch(e=>{document.getElementById('range').textContent='Failed to load data: '+e.message})}
document.getElementById('loginForm').addEventListener('submit',async e=>{e.preventDefault();const button=document.getElementById('loginButton'),error=document.getElementById('loginError');button.disabled=true;error.textContent='';try{const email=document.getElementById('email').value.trim().toLowerCase(),password=document.getElementById('password').value;const [emailHash,passwordHash]=await Promise.all([sha256(email),sha256(ACCESS.salt+':'+password)]);if(ACCESS.emails.includes(emailHash)&&passwordHash===ACCESS.password){sessionStorage.setItem('amazonZaAccess','granted');showReport()}else{error.textContent='Incorrect email or password.'}}catch(_){error.textContent='This browser cannot complete verification. Please use a modern browser.'}finally{button.disabled=false}});
if(sessionStorage.getItem('amazonZaAccess')==='granted')showReport();
document.getElementById('loginForm').addEventListener('submit',async e=>{e.preventDefault();const button=document.getElementById('loginButton'),error=document.getElementById('loginError');button.disabled=true;error.textContent='';try{const access=await fetch('access.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw Error('access configuration unavailable');return r.json()}),code=document.getElementById('accessCode').value,codeHash=await sha256(access.salt+':'+code);if(codeHash===access.hash){sessionStorage.setItem('amazonZaCodeAccessV1','granted');showReport()}else{error.textContent='Incorrect access code.'}}catch(_){error.textContent='Unable to verify access. Please try again.'}finally{button.disabled=false}});
if(sessionStorage.getItem('amazonZaCodeAccessV1')==='granted')showReport();
</script></body></html>