| |
| |
| |
| |
| |
| |
| |
| |
|
|
| (async function () { |
| 'use strict'; |
|
|
| var grid = document.getElementById('billing-grid'); |
| var section = document.getElementById('billing-section'); |
| if (!grid || !section) return; |
|
|
| var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| var pollTimer = null; |
| var lastBalance = -1; |
|
|
| |
| function esc(s) { |
| return String(s).replace(/[&<>"']/g, function (c) { |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; |
| }); |
| } |
|
|
| |
| var ICON = { |
| wallet: '<svg class="bc-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg>', |
| deposit: '<svg class="bc-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>', |
| key: '<svg class="bc-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3"/></svg>' , |
| alert: '<svg class="bc-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>' |
| }; |
|
|
| |
| function copyButton(text) { |
| var btn = document.createElement('button'); |
| btn.type = 'button'; |
| btn.className = 'copy-btn'; |
| btn.textContent = 'Copy'; |
| btn.addEventListener('click', function () { |
| navigator.clipboard.writeText(text).then(function () { |
| btn.classList.add('copied'); |
| btn.textContent = 'Copied!'; |
| setTimeout(function () { btn.classList.remove('copied'); btn.textContent = 'Copy'; }, 1500); |
| }).catch(function () { |
| |
| var ta = document.createElement('textarea'); |
| ta.value = text; document.body.appendChild(ta); ta.select(); |
| try { document.execCommand('copy'); btn.classList.add('copied'); btn.textContent = 'Copied!'; } catch (e) {} |
| document.body.removeChild(ta); |
| setTimeout(function () { btn.classList.remove('copied'); btn.textContent = 'Copy'; }, 1500); |
| }); |
| }); |
| return btn; |
| } |
|
|
| |
| function renderKeyReveal(plaintextKey) { |
| var banner = document.createElement('div'); |
| banner.className = 'key-reveal'; |
| banner.setAttribute('role', 'alert'); |
| banner.innerHTML = |
| '<svg class="kr-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3"/></svg>' + |
| '<div class="kr-body">' + |
| '<h3>Your API key — welcome aboard</h3>' + |
| '<p>Here’s your key. It’s <code>sk-</code> + your deposit address, so you can always find it again on the dashboard — no need to panic if you lose it. Use it as <code>Authorization: Bearer</code> for API calls outside the browser.</p>' + |
| '<div class="kr-key">' + |
| '<code>' + esc(plaintextKey) + '</code>' + |
| '</div>' + |
| '</div>' + |
| '<button type="button" class="kr-dismiss" aria-label="Dismiss" title="Got it">\u00d7</button>'; |
| section.parentNode.insertBefore(banner, section); |
|
|
| |
| var keyWrap = banner.querySelector('.kr-key'); |
| keyWrap.appendChild(copyButton(plaintextKey)); |
|
|
| |
| banner.querySelector('.kr-dismiss').addEventListener('click', function () { |
| fetch('/auth/clear-reveal', { method: 'POST' }).catch(function () {}); |
| banner.remove(); |
| }); |
| } |
|
|
| |
| function renderCards(billing) { |
| var bal = billing.balance || 0; |
| var lowCls = bal === 0 ? 'is-zero' : (bal < 50 ? 'is-low' : ''); |
| var flash = (lastBalance >= 0 && lastBalance !== bal) ? ' flash' : ''; |
| lastBalance = bal; |
|
|
| |
| var balCard = document.createElement('div'); |
| balCard.className = 'bill-card ' + lowCls; |
| balCard.innerHTML = |
| ICON.wallet + |
| '<span class="bc-label">credits</span>' + |
| '<span class="bc-value' + flash + '">' + bal.toLocaleString() + '</span>' + |
| '<span class="bc-sub">\u2248 $' + (bal / 10000).toFixed(4) + ' USD</span>'; |
|
|
| |
| var depCard = document.createElement('div'); |
| depCard.className = 'bill-card'; |
| depCard.innerHTML = |
| ICON.deposit + |
| '<span class="bc-label">deposit (Base L2)</span>' + |
| '<span class="bc-sub" style="margin-top:.2rem;"><code>' + esc(billing.deposit_address) + '</code></span>'; |
| var depCopy = copyButton(billing.deposit_address); |
| depCopy.style.marginTop = '.4rem'; |
| depCard.appendChild(depCopy); |
|
|
| grid.innerHTML = ''; |
| grid.appendChild(balCard); |
| grid.appendChild(depCard); |
|
|
| |
| if (flash && !reduceMotion) { |
| var val = balCard.querySelector('.bc-value'); |
| void val.offsetWidth; |
| val.classList.add('flash'); |
| } |
| } |
|
|
| |
| function renderEmpty() { |
| grid.innerHTML = |
| '<div class="bill-card is-zero" style="grid-column:1/-1;flex-direction:row;align-items:center;gap:1rem;">' + |
| ICON.alert + |
| '<div><div class="bc-value" style="font-size:1.1rem;">No credits remaining</div>' + |
| '<div class="bc-sub" style="text-transform:none;letter-spacing:0;">Send ETH to your deposit address above to top up. Credits appear within 60 seconds.</div></div>' + |
| '</div>'; |
| } |
|
|
| |
| function fmtTime(iso) { |
| if (!iso) return '\u2014'; |
| try { |
| var d = new Date(iso); |
| return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); |
| } catch (e) { return iso; } |
| } |
|
|
| function renderLedger(entries) { |
| var wrap = document.getElementById('ledger-wrap'); |
| var body = document.getElementById('ledger-body'); |
| var empty = document.getElementById('ledger-empty'); |
| if (!wrap || !body) return; |
| if (!entries || !entries.length) { |
| body.innerHTML = ''; |
| wrap.hidden = false; |
| if (empty) empty.hidden = false; |
| return; |
| } |
| if (empty) empty.hidden = true; |
| |
| var rows = entries.slice().reverse().slice(0, 50); |
| var html = ''; |
| for (var i = 0; i < rows.length; i++) { |
| var e = rows[i]; |
| var isDeposit = e.type === 'deposit'; |
| var cls = isDeposit ? 'is-deposit' : 'is-burn'; |
| var sign = isDeposit ? '+' : '\u2212'; |
| var detail; |
| if (isDeposit) { |
| detail = (typeof e.eth === 'number' ? e.eth.toFixed(6) : e.eth) + ' ETH' + |
| (e.usd ? ' (\u2248 $' + e.usd + ')' : ''); |
| } else { |
| detail = esc(e.model || 'model') + ' \u00b7 ' + |
| (e.tok_in || 0) + ' in / ' + (e.tok_out || 0) + ' out'; |
| } |
| html += |
| '<tr class="' + cls + '">' + |
| '<td>' + esc(fmtTime(e.ts)) + '</td>' + |
| '<td><span class="tag ' + cls + '">' + (isDeposit ? 'deposit' : 'usage') + '</span></td>' + |
| '<td class="ledger-detail">' + detail + '</td>' + |
| '<td class="num">' + sign + (e.credits || e.cost || 0).toLocaleString() + '</td>' + |
| '</tr>'; |
| } |
| body.innerHTML = html; |
| wrap.hidden = false; |
| } |
|
|
| |
| async function pollBalance() { |
| try { |
| var r = await fetch('/api/me/balance'); |
| if (!r.ok) return; |
| var d = await r.json(); |
| if (d.balance !== lastBalance) { |
| renderCards({ balance: d.balance, deposit_address: grid._deposit || '' }); |
| if (d.balance === 0) renderEmpty(); |
| } |
| } catch (e) { } |
| } |
|
|
| |
| try { |
| var res = await fetch('/auth/me'); |
| if (!res.ok) { section.hidden = true; return; } |
| var me = await res.json(); |
|
|
| |
| if (!me.billing || !me.billing.enabled) { |
| section.hidden = true; |
| return; |
| } |
|
|
| |
| if (!me.billing.provisioned) { |
| section.hidden = true; |
| return; |
| } |
|
|
| |
| if (me.reveal_key) { |
| renderKeyReveal(me.reveal_key); |
| } |
|
|
| |
| grid._deposit = me.billing.deposit_address; |
| grid._prefix = me.billing.key_prefix; |
|
|
| |
| renderCards(me.billing); |
| if (me.billing.balance === 0) renderEmpty(); |
|
|
| |
| renderLedger((me.billing.ledger && me.billing.ledger.entries) || []); |
|
|
| section.hidden = false; |
|
|
| |
| pollTimer = setInterval(pollBalance, 10000); |
| } catch (e) { |
| section.hidden = true; |
| } |
|
|
| |
| window.addEventListener('beforeunload', function () { |
| if (pollTimer) clearInterval(pollTimer); |
| }); |
| })(); |
|
|