feat: add usage dashboard (GET /v1/usage endpoint + usage.html page)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 10m59s

This commit is contained in:
Hoid 2026-02-25 14:06:07 +00:00
parent b2688c0cce
commit 5b59a7a010
6 changed files with 386 additions and 1 deletions

View file

@ -243,6 +243,7 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
<a href="#pricing">Pricing</a>
<a href="#docs">API Docs</a>
<a href="/docs" target="_blank">Swagger</a>
<a href="/usage">Usage</a>
<a href="#pricing" class="btn btn-primary btn-sm nav-cta">Get API Key</a>
</div>
<button class="nav-mobile" onclick="document.querySelector('.nav-links').classList.toggle('show')" aria-label="Menu"></button>
@ -702,12 +703,14 @@ screenshot = snap.<span class="fn">capture</span>(
<a href="#pricing">Pricing</a>
<a href="#playground">Playground</a>
<a href="/docs">API Docs</a>
<a href="/usage">Check Usage</a>
</div>
<div class="footer-col">
<h5>Developers</h5>
<a href="/docs">Swagger / OpenAPI</a>
<a href="#docs">Quick Start</a>
<a href="/health">Status</a>
<a href="/usage">Usage Dashboard</a>
</div>
<div class="footer-col">
<h5>Legal</h5>

146
public/usage.html Normal file
View file

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Usage Dashboard — SnapAPI</title>
<meta name="description" content="Check your SnapAPI usage statistics, remaining quota, and plan details.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📸</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0a0e17;--bg2:#0f1420;--card:#141a28;
--border:#1e2a3f;--border-light:#2a3752;
--text:#f0f2f7;--text-secondary:#94a3c0;--muted:#6b7a96;
--primary:#4f8fff;--primary-light:#6da3ff;--primary-dark:#3a6fd8;
--accent:#10b981;--orange:#f59e0b;--red:#ef4444;
--gradient:linear-gradient(135deg,#4f8fff 0%,#a78bfa 50%,#ec4899 100%);
--radius:12px;
}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased;min-height:100vh;display:flex;flex-direction:column}
a{color:var(--primary-light);text-decoration:none}a:hover{color:var(--primary)}
nav{background:rgba(10,14,23,0.85);backdrop-filter:blur(20px);border-bottom:1px solid rgba(30,42,63,0.5);padding:0 24px}
.nav-inner{max-width:1180px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:64px}
.nav-logo{font-size:1.15rem;font-weight:800;display:flex;align-items:center;gap:8px;color:var(--text)}
.nav-logo span{background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.nav-links{display:flex;gap:24px;align-items:center}
.nav-links a{color:var(--muted);font-size:.9rem;font-weight:500}
.nav-links a:hover{color:var(--text)}
main{flex:1;max-width:560px;margin:80px auto;padding:0 24px;width:100%}
h1{font-size:2rem;font-weight:800;margin-bottom:8px}
.subtitle{color:var(--text-secondary);margin-bottom:40px}
label{display:block;font-size:.85rem;font-weight:600;color:var(--text-secondary);margin-bottom:8px}
.input-row{display:flex;gap:12px;margin-bottom:32px}
input[type="text"]{flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px 16px;color:var(--text);font-size:.95rem;font-family:inherit;outline:none;transition:border-color .2s}
input[type="text"]:focus{border-color:var(--primary)}
button{background:var(--primary);color:#fff;border:none;border-radius:10px;padding:12px 24px;font-weight:600;font-size:.95rem;cursor:pointer;font-family:inherit;transition:background .2s}
button:hover{background:var(--primary-dark)}
button:disabled{opacity:.5;cursor:not-allowed}
.result{display:none}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px}
.stat{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px}
.stat-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:4px}
.stat-value{font-size:1.8rem;font-weight:800;font-family:'JetBrains Mono',monospace}
.stat-value.plan{text-transform:capitalize;font-family:'Inter',sans-serif;font-size:1.4rem}
.progress-wrap{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:24px}
.progress-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}
.progress-title{font-weight:600}
.progress-pct{font-family:'JetBrains Mono',monospace;font-size:1.1rem;font-weight:700}
.progress-bar{height:12px;background:rgba(255,255,255,0.06);border-radius:6px;overflow:hidden}
.progress-fill{height:100%;border-radius:6px;background:var(--primary);transition:width .6s ease}
.progress-fill.warning{background:var(--orange)}
.progress-fill.danger{background:var(--red)}
.progress-msg{margin-top:10px;font-size:.85rem;color:var(--muted)}
.progress-msg.warning{color:var(--orange)}
.progress-msg.danger{color:var(--red)}
.links{display:flex;gap:16px;flex-wrap:wrap;margin-top:8px}
.links a{font-size:.9rem;font-weight:500}
.error-msg{background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:10px;padding:14px 18px;color:var(--red);font-size:.9rem;margin-bottom:16px;display:none}
footer{border-top:1px solid var(--border);padding:24px;text-align:center;font-size:.8rem;color:var(--muted);margin-top:auto}
@media(max-width:480px){.stats{grid-template-columns:1fr}.input-row{flex-direction:column}}
</style>
</head>
<body>
<nav><div class="nav-inner">
<a href="/" class="nav-logo">📸 <span>SnapAPI</span></a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/docs">API Docs</a>
</div>
</div></nav>
<main>
<h1>Usage Dashboard</h1>
<p class="subtitle">Check your API usage for the current billing month.</p>
<label for="apikey">API Key</label>
<div class="input-row">
<input type="text" id="apikey" placeholder="snap_..." autocomplete="off" spellcheck="false" aria-label="API Key">
<button id="checkBtn" type="button">Check Usage</button>
</div>
<div class="error-msg" id="error" role="alert"></div>
<div class="result" id="result">
<div class="stats">
<div class="stat">
<div class="stat-label">Used / Limit</div>
<div class="stat-value" id="usedLimit"></div>
</div>
<div class="stat">
<div class="stat-label">Plan</div>
<div class="stat-value plan" id="plan"></div>
</div>
</div>
<div class="progress-wrap">
<div class="progress-header">
<span class="progress-title">Monthly Usage</span>
<span class="progress-pct" id="pct">0%</span>
</div>
<div class="progress-bar"><div class="progress-fill" id="bar" style="width:0%"></div></div>
<div class="progress-msg" id="msg"></div>
</div>
<div class="links">
<a href="/">← Home</a>
<a href="/docs">API Docs</a>
</div>
</div>
</main>
<footer>© 2026 Cloonar Technologies GmbH · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></footer>
<script>
const btn=document.getElementById('checkBtn'),input=document.getElementById('apikey'),
result=document.getElementById('result'),error=document.getElementById('error');
async function check(){
const key=input.value.trim();
if(!key){input.focus();return}
btn.disabled=true;btn.textContent='Checking...';
error.style.display='none';result.style.display='none';
try{
const r=await fetch('/v1/usage',{headers:{'Authorization':'Bearer '+key}});
if(!r.ok){const e=await r.json().catch(()=>({error:'Request failed'}));throw new Error(e.error||'Error '+r.status)}
const d=await r.json();
document.getElementById('usedLimit').textContent=d.used.toLocaleString()+' / '+d.limit.toLocaleString();
document.getElementById('plan').textContent=d.plan;
document.getElementById('pct').textContent=d.percentUsed+'%';
const bar=document.getElementById('bar'),msg=document.getElementById('msg');
bar.style.width=Math.min(d.percentUsed,100)+'%';
bar.className='progress-fill'+(d.percentUsed>95?' danger':d.percentUsed>80?' warning':'');
msg.className='progress-msg';
if(d.percentUsed>95){msg.textContent='⚠️ Critical — almost at your limit!';msg.classList.add('danger')}
else if(d.percentUsed>80){msg.textContent='⚠️ Approaching your monthly limit.';msg.classList.add('warning')}
else{msg.textContent=d.remaining.toLocaleString()+' screenshots remaining for '+d.month+'.'}
result.style.display='block';
}catch(e){error.textContent=e.message;error.style.display='block'}
finally{btn.disabled=false;btn.textContent='Check Usage'}
}
btn.addEventListener('click',check);
input.addEventListener('keydown',e=>{if(e.key==='Enter')check()});
</script>
</body>
</html>