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
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 10m59s
This commit is contained in:
parent
b2688c0cce
commit
5b59a7a010
6 changed files with 386 additions and 1 deletions
|
|
@ -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
146
public/usage.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue