Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>FreeBG Admin</title> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#07070f;--s:#0f0f1a;--card:#14141f;--card2:#1a1a28; | |
| --b:#252535;--b2:#2e2e45; | |
| --g:#00e5a0;--g2:rgba(0,229,160,.12); | |
| --bl:#4d8eff;--bl2:rgba(77,142,255,.12); | |
| --red:#ff4d6d;--red2:rgba(255,77,109,.12); | |
| --gold:#ffc850;--gold2:rgba(255,200,80,.12); | |
| --t:#eeeef5;--m:#5e5e78;--m2:#888899; | |
| --r:10px; | |
| } | |
| body{background:var(--bg);color:var(--t);font-family:system-ui,sans-serif;min-height:100vh} | |
| /* LOGIN */ | |
| #login-screen{ | |
| position:fixed;inset:0;background:var(--bg); | |
| display:flex;align-items:center;justify-content:center;z-index:999; | |
| } | |
| .login-box{ | |
| background:var(--card);border:1px solid var(--b);border-radius:14px; | |
| padding:40px;width:340px;text-align:center; | |
| } | |
| .login-box h2{font-size:22px;font-weight:800;margin-bottom:6px} | |
| .login-box p{color:var(--m2);font-size:13px;margin-bottom:28px} | |
| .login-box input{ | |
| width:100%;background:var(--card2);border:1px solid var(--b2); | |
| color:var(--t);padding:12px 14px;border-radius:8px; | |
| font-size:15px;outline:none;margin-bottom:14px; | |
| } | |
| .login-box input:focus{border-color:var(--g)} | |
| .btn-login{ | |
| width:100%;padding:13px;background:var(--g);color:#000; | |
| border:none;border-radius:9px;font-size:15px;font-weight:800;cursor:pointer; | |
| } | |
| .btn-login:hover{opacity:.88} | |
| .login-err{color:var(--red);font-size:13px;margin-top:10px} | |
| /* HEADER */ | |
| header{ | |
| background:var(--card);border-bottom:1px solid var(--b); | |
| padding:0 28px;height:58px;display:flex;align-items:center;gap:14px; | |
| position:sticky;top:0;z-index:100; | |
| } | |
| .logo{font-size:18px;font-weight:800;color:var(--g)} | |
| .logo span{color:var(--t)} | |
| .nav-tabs{display:flex;gap:2px;margin-left:24px} | |
| .tab-btn{ | |
| padding:7px 16px;border:none;background:none;color:var(--m2); | |
| font-size:13px;font-weight:600;cursor:pointer;border-radius:7px;transition:.2s; | |
| } | |
| .tab-btn:hover{color:var(--t);background:rgba(255,255,255,.05)} | |
| .tab-btn.active{color:var(--g);background:var(--g2)} | |
| .header-right{margin-left:auto;display:flex;gap:8px;align-items:center} | |
| .hf-link{ | |
| font-size:12px;color:var(--m2);text-decoration:none; | |
| padding:6px 12px;border:1px solid var(--b2);border-radius:7px;transition:.2s; | |
| } | |
| .hf-link:hover{border-color:var(--g);color:var(--g)} | |
| .btn-logout{ | |
| font-size:12px;color:var(--m2);background:none;border:none;cursor:pointer;padding:4px 8px; | |
| } | |
| .btn-logout:hover{color:var(--red)} | |
| /* MAIN */ | |
| main{max-width:1100px;margin:0 auto;padding:28px 20px} | |
| /* STATS */ | |
| .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:24px} | |
| @media(max-width:700px){.stats{grid-template-columns:repeat(3,1fr)}} | |
| .stat-card{ | |
| background:var(--card);border:1px solid var(--b);border-radius:var(--r); | |
| padding:16px 14px;text-align:center; | |
| } | |
| .stat-n{font-size:26px;font-weight:800} | |
| .stat-l{font-size:11px;color:var(--m2);margin-top:3px} | |
| /* PANELS */ | |
| .panel{background:var(--card);border:1px solid var(--b);border-radius:var(--r);margin-bottom:20px;overflow:hidden} | |
| .panel-head{ | |
| padding:14px 18px;border-bottom:1px solid var(--b); | |
| display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px; | |
| } | |
| .panel-title{font-size:14px;font-weight:700} | |
| .panel-body{padding:18px} | |
| /* FORM */ | |
| .form-row{display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:end} | |
| @media(max-width:600px){.form-row{grid-template-columns:1fr}} | |
| label{display:block;font-size:11px;font-weight:700;color:var(--m2); | |
| text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px} | |
| input,select{ | |
| width:100%;background:var(--card2);border:1px solid var(--b2); | |
| color:var(--t);padding:9px 12px;border-radius:8px; | |
| font-size:14px;outline:none;transition:.2s; | |
| } | |
| input:focus,select:focus{border-color:var(--g)} | |
| input::placeholder{color:var(--m)} | |
| /* BUTTONS */ | |
| .btn{ | |
| padding:9px 16px;border-radius:8px;font-size:13px;font-weight:700; | |
| cursor:pointer;border:none;transition:.2s;white-space:nowrap; | |
| display:inline-flex;align-items:center;gap:5px; | |
| } | |
| .btn-g{background:var(--g);color:#000} | |
| .btn-g:hover{opacity:.88} | |
| .btn-r{background:var(--red2);color:var(--red);border:1px solid rgba(255,77,109,.3)} | |
| .btn-r:hover{background:rgba(255,77,109,.25)} | |
| .btn-b{background:var(--bl2);color:var(--bl);border:1px solid rgba(77,142,255,.3)} | |
| .btn-b:hover{background:rgba(77,142,255,.25)} | |
| .btn-ghost{background:var(--card2);color:var(--t);border:1px solid var(--b2)} | |
| .btn-ghost:hover{border-color:var(--m2)} | |
| /* TABLE */ | |
| .tbl-wrap{overflow-x:auto} | |
| table{width:100%;border-collapse:collapse;font-size:13px} | |
| th{ | |
| text-align:left;padding:9px 12px; | |
| font-size:11px;font-weight:700;color:var(--m2); | |
| text-transform:uppercase;letter-spacing:.5px; | |
| border-bottom:1px solid var(--b); | |
| } | |
| td{padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle} | |
| tr:hover td{background:rgba(255,255,255,.02)} | |
| tr:last-child td{border-bottom:none} | |
| .key-mono{font-family:monospace;font-size:12px;color:var(--m2);cursor:pointer} | |
| .key-mono:hover{color:var(--g)} | |
| .badge{ | |
| display:inline-block;font-size:10px;font-weight:800; | |
| padding:2px 8px;border-radius:5px;text-transform:uppercase; | |
| } | |
| .b-free{background:rgba(107,107,133,.2);color:var(--m2)} | |
| .b-starter{background:var(--bl2);color:var(--bl)} | |
| .b-pro{background:var(--g2);color:var(--g)} | |
| .b-master{background:var(--gold2);color:var(--gold)} | |
| .actions{display:flex;gap:5px} | |
| /* EXPORT BOX */ | |
| .export-box{ | |
| background:var(--card2);border:1px solid var(--b);border-radius:8px; | |
| padding:14px;font-family:monospace;font-size:12px;color:var(--m2); | |
| word-break:break-all;white-space:pre-wrap; | |
| max-height:200px;overflow-y:auto;user-select:all;margin-top:12px; | |
| } | |
| /* INSTRUCTIONS */ | |
| .steps{display:flex;flex-direction:column;gap:10px} | |
| .step{display:flex;gap:12px;align-items:flex-start} | |
| .step-n{ | |
| width:22px;height:22px;border-radius:50%;background:var(--g); | |
| color:#000;font-size:11px;font-weight:800; | |
| display:flex;align-items:center;justify-content:center;flex-shrink:0; | |
| } | |
| .step-t{font-size:13px;color:var(--m2);line-height:1.5} | |
| .step-t b{color:var(--t)} | |
| .step-t code{ | |
| background:var(--card2);border:1px solid var(--b2); | |
| padding:1px 6px;border-radius:4px;font-family:monospace;font-size:12px;color:var(--g); | |
| } | |
| /* USAGE BAR */ | |
| .usage-bar{height:6px;background:var(--b);border-radius:3px;overflow:hidden;margin-top:4px} | |
| .usage-fill{height:100%;background:var(--g);border-radius:3px;transition:.3s} | |
| /* TOAST */ | |
| .toast{ | |
| position:fixed;bottom:20px;right:20px;z-index:999; | |
| padding:10px 16px;border-radius:9px;font-size:13px;font-weight:600;border:1px solid; | |
| transform:translateY(20px);opacity:0;transition:.3s;pointer-events:none; | |
| } | |
| .toast.show{transform:translateY(0);opacity:1} | |
| .toast.ok{background:#0a1a12;border-color:rgba(0,229,160,.4);color:var(--g)} | |
| .toast.err{background:#1a0a0e;border-color:rgba(255,77,109,.4);color:var(--red)} | |
| /* TABS content */ | |
| .tab-content{display:none} | |
| .tab-content.active{display:block} | |
| /* EDIT INLINE */ | |
| .edit-row td{background:var(--card2)!important;padding:6px 12px} | |
| .edit-row input,.edit-row select{padding:6px 10px;font-size:13px;margin:0} | |
| </style> | |
| </head> | |
| <body> | |
| <!-- LOGIN SCREEN --> | |
| <div id="login-screen"> | |
| <div class="login-box"> | |
| <h2>π FreeBG Admin</h2> | |
| <p>Enter admin password to continue</p> | |
| <input type="password" id="pwd-input" placeholder="Admin password" | |
| onkeydown="if(event.key==='Enter')doLogin()"> | |
| <button class="btn-login" onclick="doLogin()">Login</button> | |
| <div class="login-err" id="login-err"></div> | |
| </div> | |
| </div> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="logo">Free<span>BG</span> <span style="font-size:11px;color:var(--m2);font-weight:400;margin-left:4px">Admin</span></div> | |
| <div class="nav-tabs"> | |
| <button class="tab-btn active" onclick="showTab('customers')">π₯ Customers</button> | |
| <button class="tab-btn" onclick="showTab('export')">π Export</button> | |
| <button class="tab-btn" onclick="showTab('stats')">π Stats</button> | |
| <button class="tab-btn" onclick="showTab('guide')">π Guide</button> | |
| </div> | |
| <div class="header-right"> | |
| <a class="hf-link" href="https://huggingface.co/spaces/freebg/background-remover/settings" | |
| target="_blank">π€ HF Secrets β</a> | |
| <button class="btn-logout" onclick="doLogout()">Logout</button> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- STATS ROW --> | |
| <div class="stats" id="stats-row"> | |
| <div class="stat-card"><div class="stat-n" id="s-total" style="color:var(--g)">0</div><div class="stat-l">Total Keys</div></div> | |
| <div class="stat-card"><div class="stat-n" id="s-free" style="color:var(--m2)">0</div><div class="stat-l">Free</div></div> | |
| <div class="stat-card"><div class="stat-n" id="s-start" style="color:var(--bl)">0</div><div class="stat-l">Starter</div></div> | |
| <div class="stat-card"><div class="stat-n" id="s-pro" style="color:var(--g)">0</div><div class="stat-l">Pro</div></div> | |
| <div class="stat-card"><div class="stat-n" id="s-calls" style="color:var(--gold)">0</div><div class="stat-l">Calls Today</div></div> | |
| </div> | |
| <!-- TAB: CUSTOMERS --> | |
| <div class="tab-content active" id="tab-customers"> | |
| <!-- Add Customer --> | |
| <div class="panel"> | |
| <div class="panel-head"><div class="panel-title">β Add New Customer</div></div> | |
| <div class="panel-body"> | |
| <div class="form-row"> | |
| <div> | |
| <label>Email</label> | |
| <input type="email" id="n-email" placeholder="customer@gmail.com"> | |
| </div> | |
| <div> | |
| <label>Plan</label> | |
| <select id="n-plan"> | |
| <option value="free">Free β 10/day</option> | |
| <option value="starter" selected>Starter β 100/day ($9/mo)</option> | |
| <option value="pro">Pro β 500/day ($29/mo)</option> | |
| <option value="master">Master β Unlimited</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label> </label> | |
| <button class="btn btn-g" onclick="addCustomer()">β¨ Generate Key</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search + Table --> | |
| <div class="panel"> | |
| <div class="panel-head"> | |
| <div class="panel-title">All Customers (<span id="count">0</span>)</div> | |
| <div style="display:flex;gap:8px;align-items:center"> | |
| <input type="text" id="search" placeholder="Search email or key..." | |
| style="width:220px;margin:0;padding:7px 11px;font-size:13px" | |
| oninput="renderTable()"> | |
| <button class="btn btn-r" onclick="if(confirm('Delete ALL keys?'))clearAll()">ποΈ Clear All</button> | |
| </div> | |
| </div> | |
| <div class="tbl-wrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>API Key</th> | |
| <th>Owner</th> | |
| <th>Plan</th> | |
| <th>Calls Today</th> | |
| <th>Limit</th> | |
| <th>Created</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="tbody"></tbody> | |
| </table> | |
| <div id="empty" style="text-align:center;padding:40px;color:var(--m);display:none"> | |
| No customers yet. Add your first one above! | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TAB: EXPORT --> | |
| <div class="tab-content" id="tab-export"> | |
| <div class="panel"> | |
| <div class="panel-head"> | |
| <div class="panel-title">π Export for HuggingFace Secret</div> | |
| <div style="display:flex;gap:8px"> | |
| <button class="btn btn-g" onclick="copyExport()">π Copy JSON</button> | |
| <button class="btn btn-b" onclick="window.open('https://huggingface.co/spaces/freebg/background-remover/settings','_blank')"> | |
| π€ Open HF Settings | |
| </button> | |
| </div> | |
| </div> | |
| <div class="panel-body"> | |
| <div class="steps"> | |
| <div class="step"><div class="step-n">1</div> | |
| <div class="step-t">Neeche JSON copy karo β <b>"Copy JSON"</b> button se</div></div> | |
| <div class="step"><div class="step-n">2</div> | |
| <div class="step-t"><b>"Open HF Settings"</b> click karo</div></div> | |
| <div class="step"><div class="step-n">3</div> | |
| <div class="step-t">Secrets section mein <code>API_KEYS_JSON</code> β <b>Replace</b> β Paste β Save</div></div> | |
| <div class="step"><div class="step-n">4</div> | |
| <div class="step-t">Space automatically restart hoga β done! β </div></div> | |
| </div> | |
| <div class="export-box" id="export-box">{}</div> | |
| <div style="margin-top:10px;display:flex;gap:10px"> | |
| <button class="btn btn-ghost" onclick="importKeys()">π Import existing JSON</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TAB: STATS --> | |
| <div class="tab-content" id="tab-stats"> | |
| <div class="panel"> | |
| <div class="panel-head"><div class="panel-title">π Usage & Revenue</div></div> | |
| <div class="panel-body"> | |
| <div id="stats-detail" style="display:grid;grid-template-columns:1fr 1fr;gap:16px"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TAB: GUIDE --> | |
| <div class="tab-content" id="tab-guide"> | |
| <div class="panel"> | |
| <div class="panel-head"><div class="panel-title">π How to Use This Admin</div></div> | |
| <div class="panel-body"> | |
| <div class="steps" style="gap:16px"> | |
| <div class="step"><div class="step-n">1</div> | |
| <div class="step-t"><b>Customer add karo:</b> Email + Plan β Generate Key β Key popup mein dikhegi β Customer ko bhejo</div></div> | |
| <div class="step"><div class="step-n">2</div> | |
| <div class="step-t"><b>HF Space update karo:</b> Export tab β Copy JSON β HF Settings β API_KEYS_JSON β Replace</div></div> | |
| <div class="step"><div class="step-n">3</div> | |
| <div class="step-t"><b>Plan upgrade:</b> Table mein βοΈ Edit β Plan change karo β Save β Phir Export karo</div></div> | |
| <div class="step"><div class="step-n">4</div> | |
| <div class="step-t"><b>Key revoke:</b> Table mein ποΈ Delete β Phir Export karo</div></div> | |
| <div class="step"><div class="step-n">5</div> | |
| <div class="step-t"><b>Data safe:</b> Keys browser mein save hain. <code>Export β Import</code> se backup rakho</div></div> | |
| </div> | |
| <div style="margin-top:20px;padding:14px;background:var(--card2);border-radius:8px;font-size:13px"> | |
| <div style="font-weight:700;margin-bottom:8px;color:var(--gold)">β οΈ Important</div> | |
| <div style="color:var(--m2);line-height:1.7"> | |
| Har baar customer add/edit/delete karne ke baad <b>Export tab β HF Space update karna zaroori hai</b>.<br> | |
| Otherwise changes sirf browser mein rahenge, Space pe apply nahi honge.<br> | |
| Admin password change karne ke liye HTML mein <code>ADMIN_PASSWORD</code> update karo. | |
| </div> | |
| </div> | |
| <div style="margin-top:14px;padding:14px;background:var(--card2);border-radius:8px;font-size:13px"> | |
| <div style="font-weight:700;margin-bottom:8px">π Useful Links</div> | |
| <div style="display:flex;flex-direction:column;gap:6px"> | |
| <a href="https://huggingface.co/spaces/freebg/background-remover" target="_blank" | |
| style="color:var(--bl)">β HuggingFace Space (App)</a> | |
| <a href="https://huggingface.co/spaces/freebg/background-remover/settings" target="_blank" | |
| style="color:var(--bl)">β HuggingFace Space Settings (Secrets)</a> | |
| <a href="https://huggingface.co/spaces/freebg/background-remover/blob/main/app.py" target="_blank" | |
| style="color:var(--bl)">β app.py source</a> | |
| <a href="/health" target="_blank" style="color:var(--g)">β /health endpoint</a> | |
| <a href="/api/models" target="_blank" style="color:var(--g)">β /api/models endpoint</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| // ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ADMIN_PASSWORD = "freebg-admin-2026"; // β Change this! | |
| const STORAGE_KEY = "freebg_keys_v2"; | |
| const SESSION_KEY = "freebg_admin_auth"; | |
| const PLANS = { | |
| free: { daily:10, models:["fast"], price:"$0" }, | |
| starter: { daily:100, models:["fast","quality"], price:"$9/mo" }, | |
| pro: { daily:500, models:["fast","quality","best"], price:"$29/mo" }, | |
| master: { daily:999999,models:["fast","quality","best"], price:"Custom" }, | |
| }; | |
| // ββ AUTH ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function doLogin() { | |
| const val = document.getElementById("pwd-input").value; | |
| if (val === ADMIN_PASSWORD) { | |
| sessionStorage.setItem(SESSION_KEY, "1"); | |
| document.getElementById("login-screen").style.display = "none"; | |
| init(); | |
| } else { | |
| document.getElementById("login-err").textContent = "β Wrong password"; | |
| setTimeout(() => document.getElementById("login-err").textContent = "", 2000); | |
| } | |
| } | |
| function doLogout() { | |
| sessionStorage.removeItem(SESSION_KEY); | |
| location.reload(); | |
| } | |
| if (!sessionStorage.getItem(SESSION_KEY)) { | |
| document.getElementById("login-screen").style.display = "flex"; | |
| } else { | |
| document.getElementById("login-screen").style.display = "none"; | |
| } | |
| // ββ STORAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function load() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)||"{}"); } catch { return {}; } } | |
| function save(k) { localStorage.setItem(STORAGE_KEY, JSON.stringify(k)); refresh(); } | |
| // ββ KEY GEN βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function genKey(plan) { | |
| const c = "abcdefghijklmnopqrstuvwxyz0123456789"; | |
| const r = n => Array.from({length:n},()=>c[Math.floor(Math.random()*c.length)]).join(""); | |
| return `freebg-${plan.slice(0,2)}-${r(8)}-${r(8)}`; | |
| } | |
| // ββ ADD CUSTOMER ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addCustomer() { | |
| const email = document.getElementById("n-email").value.trim(); | |
| const plan = document.getElementById("n-plan").value; | |
| if (!email || !email.includes("@")) return toast("Valid email required","err"); | |
| const keys = load(); | |
| const key = genKey(plan); | |
| keys[key] = { | |
| plan, owner:email, calls_today:0, reset_at:0, | |
| limit: PLANS[plan].daily, models: PLANS[plan].models, | |
| created_at: new Date().toISOString(), | |
| }; | |
| save(keys); | |
| document.getElementById("n-email").value = ""; | |
| showNewKey(key, email, plan); | |
| toast("Key created! Export β HF Space update karo","ok"); | |
| } | |
| function showNewKey(key, email, plan) { | |
| const d = document.createElement("div"); | |
| d.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;z-index:500;padding:20px"; | |
| d.innerHTML = ` | |
| <div style="background:#14141f;border:1px solid #2e2e45;border-radius:14px;padding:32px;max-width:500px;width:100%;text-align:center"> | |
| <div style="font-size:36px;margin-bottom:12px">π</div> | |
| <h3 style="color:#00e5a0;font-size:20px;margin-bottom:8px">Key Created!</h3> | |
| <p style="color:#888;font-size:13px;margin-bottom:18px"> | |
| <b style="color:#eee">${email}</b> Β· <b style="color:#eee">${plan}</b> plan Β· ${PLANS[plan].daily} calls/day | |
| </p> | |
| <div style="background:#07070f;border:1px solid #252535;border-radius:8px;padding:14px;font-family:monospace;font-size:14px;color:#00e5a0;word-break:break-all;margin-bottom:8px;cursor:pointer" | |
| onclick="navigator.clipboard.writeText('${key}').then(()=>toast('Copied!','ok'))">${key}</div> | |
| <p style="color:#5e5e78;font-size:12px;margin-bottom:18px">π Click to copy Β· Customer ko yeh key bhejo</p> | |
| <div style="background:#1a1a28;border:1px solid #252535;border-radius:8px;padding:12px;font-size:12px;color:#888;margin-bottom:18px;text-align:left"> | |
| β οΈ <b style="color:#eee">Zaroori:</b> Export tab β HF Space update karo taake key activate ho | |
| </div> | |
| <div style="display:flex;gap:10px"> | |
| <button onclick="navigator.clipboard.writeText('${key}').then(()=>{this.textContent='β Copied!'})" | |
| style="flex:1;padding:11px;background:#00e5a0;color:#000;border:none;border-radius:8px;font-weight:800;cursor:pointer"> | |
| π Copy Key | |
| </button> | |
| <button onclick="this.closest('div[style]').remove()" | |
| style="padding:11px 20px;background:#1a1a28;color:#eee;border:1px solid #2e2e45;border-radius:8px;cursor:pointer"> | |
| Close | |
| </button> | |
| </div> | |
| </div>`; | |
| document.body.appendChild(d); | |
| } | |
| // ββ TABLE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderTable() { | |
| const keys = load(); | |
| const q = document.getElementById("search").value.toLowerCase(); | |
| const tbody = document.getElementById("tbody"); | |
| const empty = document.getElementById("empty"); | |
| const count = document.getElementById("count"); | |
| const entries = Object.entries(keys).filter(([k,v]) => | |
| !q || k.includes(q) || (v.owner||"").toLowerCase().includes(q) | |
| ); | |
| count.textContent = Object.keys(keys).length; | |
| if (!entries.length) { | |
| tbody.innerHTML = ""; empty.style.display = "block"; return; | |
| } | |
| empty.style.display = "none"; | |
| tbody.innerHTML = entries.map(([key, d]) => { | |
| const pct = Math.min(100, Math.round((d.calls_today||0)/(d.limit||1)*100)); | |
| return `<tr> | |
| <td class="key-mono" onclick="navigator.clipboard.writeText('${key}').then(()=>toast('Key copied!','ok'))" | |
| title="Click to copy">${key.slice(0,28)}β¦</td> | |
| <td>${d.owner||"β"}</td> | |
| <td><span class="badge b-${d.plan}">${d.plan}</span></td> | |
| <td> | |
| <div style="font-size:12px">${d.calls_today||0} / ${d.limit||0}</div> | |
| <div class="usage-bar"><div class="usage-fill" style="width:${pct}%"></div></div> | |
| </td> | |
| <td style="font-size:12px;color:var(--m2)">${(d.limit||0).toLocaleString()}/day</td> | |
| <td style="font-size:12px;color:var(--m2)">${d.created_at?d.created_at.slice(0,10):"β"}</td> | |
| <td> | |
| <div class="actions"> | |
| <button class="btn btn-b" style="padding:5px 9px;font-size:11px" onclick="editRow('${key}')">βοΈ</button> | |
| <button class="btn btn-r" style="padding:5px 9px;font-size:11px" onclick="delKey('${key}','${d.owner}')">ποΈ</button> | |
| </div> | |
| </td> | |
| </tr>`; | |
| }).join(""); | |
| } | |
| function delKey(key, owner) { | |
| if (!confirm(`Delete key for ${owner}?\n${key}`)) return; | |
| const keys = load(); delete keys[key]; save(keys); | |
| toast("Key deleted. Export β HF update karo","ok"); | |
| } | |
| // ββ EDIT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function editRow(key) { | |
| const keys = load(); const d = keys[key]; if (!d) return; | |
| const rows = document.querySelectorAll("#tbody tr"); | |
| // Remove any existing edit row | |
| document.querySelectorAll(".edit-row").forEach(r=>r.remove()); | |
| // Find the row for this key | |
| let targetRow = null; | |
| rows.forEach(r => { if(r.cells[0]?.textContent.includes(key.slice(0,10))) targetRow = r; }); | |
| if (!targetRow) return; | |
| const editRow = document.createElement("tr"); | |
| editRow.className = "edit-row"; | |
| editRow.innerHTML = ` | |
| <td colspan="7" style="padding:12px"> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 120px auto auto;gap:8px;align-items:end"> | |
| <div><label>Email</label><input id="e-email" value="${d.owner||""}"></div> | |
| <div><label>Plan</label> | |
| <select id="e-plan"> | |
| ${["free","starter","pro","master"].map(p=>`<option value="${p}"${p===d.plan?" selected":""}>${p} (${PLANS[p].daily}/day)</option>`).join("")} | |
| </select> | |
| </div> | |
| <div><label>Custom limit</label><input type="number" id="e-limit" value="${d.limit||""}" placeholder=""></div> | |
| <div><label> </label><button class="btn btn-g" onclick="saveEdit('${key}')">πΎ Save</button></div> | |
| <div><label> </label><button class="btn btn-ghost" onclick="document.querySelectorAll('.edit-row').forEach(r=>r.remove())">β</button></div> | |
| </div> | |
| </td>`; | |
| targetRow.after(editRow); | |
| } | |
| function saveEdit(key) { | |
| const keys = load(); | |
| const plan = document.getElementById("e-plan").value; | |
| const limit = parseInt(document.getElementById("e-limit").value) || PLANS[plan].daily; | |
| keys[key].owner = document.getElementById("e-email").value.trim(); | |
| keys[key].plan = plan; | |
| keys[key].limit = limit; | |
| keys[key].models = PLANS[plan].models; | |
| save(keys); | |
| document.querySelectorAll(".edit-row").forEach(r=>r.remove()); | |
| toast("Updated! Export β HF update karo","ok"); | |
| } | |
| // ββ EXPORT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateExportBox() { | |
| const keys = load(); | |
| document.getElementById("export-box").textContent = JSON.stringify(keys, null, 2); | |
| } | |
| function copyExport() { | |
| const text = document.getElementById("export-box").textContent; | |
| navigator.clipboard.writeText(text).then(() => | |
| toast("JSON copied! HF Space Secrets mein paste karo","ok")); | |
| } | |
| function importKeys() { | |
| const json = prompt("Paste existing api_keys.json content:"); | |
| if (!json) return; | |
| try { save(JSON.parse(json)); toast("Imported!","ok"); } | |
| catch { toast("Invalid JSON","err"); } | |
| } | |
| function clearAll() { save({}); toast("All keys deleted","ok"); } | |
| // ββ STATS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateStats() { | |
| const keys = load(); | |
| const vals = Object.values(keys); | |
| document.getElementById("s-total").textContent = vals.length; | |
| document.getElementById("s-free").textContent = vals.filter(v=>v.plan==="free").length; | |
| document.getElementById("s-start").textContent = vals.filter(v=>v.plan==="starter").length; | |
| document.getElementById("s-pro").textContent = vals.filter(v=>["pro","master"].includes(v.plan)).length; | |
| document.getElementById("s-calls").textContent = vals.reduce((a,v)=>a+(v.calls_today||0),0); | |
| // Detail stats | |
| const det = document.getElementById("stats-detail"); | |
| const rev = vals.filter(v=>v.plan==="starter").length*9 + vals.filter(v=>v.plan==="pro").length*29; | |
| det.innerHTML = ` | |
| <div style="background:var(--card2);border-radius:8px;padding:16px"> | |
| <div style="font-size:12px;color:var(--m2);margin-bottom:4px">EST. MONTHLY REVENUE</div> | |
| <div style="font-size:32px;font-weight:800;color:var(--g)">$${rev}</div> | |
| <div style="font-size:12px;color:var(--m2);margin-top:4px">${vals.filter(v=>v.plan==="starter").length} starter + ${vals.filter(v=>v.plan==="pro").length} pro</div> | |
| </div> | |
| <div style="background:var(--card2);border-radius:8px;padding:16px"> | |
| <div style="font-size:12px;color:var(--m2);margin-bottom:4px">TOTAL API CALLS TODAY</div> | |
| <div style="font-size:32px;font-weight:800;color:var(--gold)">${vals.reduce((a,v)=>a+(v.calls_today||0),0)}</div> | |
| <div style="font-size:12px;color:var(--m2);margin-top:4px">across all customers</div> | |
| </div>`; | |
| } | |
| // ββ TABS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showTab(id) { | |
| document.querySelectorAll(".tab-content").forEach(t=>t.classList.remove("active")); | |
| document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active")); | |
| document.getElementById("tab-"+id).classList.add("active"); | |
| event.target.classList.add("active"); | |
| if (id==="export") updateExportBox(); | |
| if (id==="stats") updateStats(); | |
| } | |
| // ββ TOAST βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toast(msg, type) { | |
| const t = document.getElementById("toast"); | |
| t.textContent = msg; t.className = `toast ${type} show`; | |
| setTimeout(()=>t.classList.remove("show"),3000); | |
| } | |
| // ββ INIT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function refresh() { renderTable(); updateStats(); updateExportBox(); } | |
| function init() { refresh(); } | |
| if (sessionStorage.getItem(SESSION_KEY)) init(); | |
| </script> | |
| </body> | |
| </html> | |