feat: free-tier credit system + Syttende Mai access for Google users
- FreeTier.php: credit check/deduct/reset engine with hourly rate limit - bootstrap.php: dbnmDb() singleton, dbnToolsIsFreeTier(), credit gate helpers - index.php: store tier=free|approved in session from SSO JWT - All 7 API endpoints: credit gate (402/429) + X-Credits-Remaining header - layout.php: credit meta tag, JS balance var, Syttende Mai banner (05-17 only) - tools.js: credit badge in topbar, 402 modal, 429 toast, dbnUpdateCredits() - barnevernet.js + deep-research.js: wire 402/429 handling for NDJSON streams - tools.css: styles for credit badge, no-credits modal, rate-limit toast Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -432,10 +432,19 @@
|
||||
}
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
setStatus(`Request failed (${response.status}).`, 'error');
|
||||
if (response.status === 402 || response.status === 429) {
|
||||
const d = await response.json().catch(() => ({}));
|
||||
if (typeof window.dbnFreeTierError === 'function') window.dbnFreeTierError(response.status, d);
|
||||
} else {
|
||||
setStatus(`Request failed (${response.status}).`, 'error');
|
||||
}
|
||||
els.runButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
const creditsRemaining = response.headers.get('X-Credits-Remaining');
|
||||
if (creditsRemaining !== null && typeof window.dbnUpdateCredits === 'function') {
|
||||
window.dbnUpdateCredits(parseInt(creditsRemaining, 10));
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
@@ -364,10 +364,19 @@
|
||||
}
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
setStatus(`Request failed (${response.status}).`, 'error');
|
||||
if (response.status === 402 || response.status === 429) {
|
||||
const d = await response.json().catch(() => ({}));
|
||||
if (typeof window.dbnFreeTierError === 'function') window.dbnFreeTierError(response.status, d);
|
||||
} else {
|
||||
setStatus(`Request failed (${response.status}).`, 'error');
|
||||
}
|
||||
els.runButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
const creditsRemaining = response.headers.get('X-Credits-Remaining');
|
||||
if (creditsRemaining !== null && typeof window.dbnUpdateCredits === 'function') {
|
||||
window.dbnUpdateCredits(parseInt(creditsRemaining, 10));
|
||||
}
|
||||
|
||||
// Read the NDJSON stream
|
||||
const reader = response.body.getReader();
|
||||
|
||||
@@ -1202,6 +1202,12 @@ async function postJson(url, payload) {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (response.status === 402 || response.status === 429) {
|
||||
dbnFreeTierError(response.status, data);
|
||||
throw new Error(data.error?.message || (response.status === 429 ? 'rate_limit' : 'no_credits'));
|
||||
}
|
||||
const remaining = response.headers.get('X-Credits-Remaining');
|
||||
if (remaining !== null) { dbnUpdateCredits(parseInt(remaining, 10)); }
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || `Request failed with HTTP ${response.status}.`);
|
||||
}
|
||||
@@ -1929,3 +1935,66 @@ function escapeHtml(value) {
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
// ── Free-tier credit badge ────────────────────────────────────────────────
|
||||
|
||||
let _freeTierBalance = (typeof window.DBN_FREE_TIER_BALANCE === 'number') ? window.DBN_FREE_TIER_BALANCE : -1;
|
||||
|
||||
function dbnUpdateCredits(balance) {
|
||||
if (typeof balance !== 'number' || balance < 0) return;
|
||||
_freeTierBalance = balance;
|
||||
const badge = document.getElementById('creditBadge');
|
||||
if (!badge) return;
|
||||
badge.textContent = '🪙 ' + balance + ' credit' + (balance !== 1 ? 's' : '');
|
||||
badge.classList.toggle('is-low', balance > 0 && balance <= 2);
|
||||
badge.classList.toggle('is-empty', balance === 0);
|
||||
}
|
||||
|
||||
// Exposed so barnevernet.js / deep-research.js can call it
|
||||
window.dbnUpdateCredits = dbnUpdateCredits;
|
||||
|
||||
function dbnFreeTierError(status, data) {
|
||||
if (status === 429) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'credit-toast';
|
||||
toast.textContent = 'Rate limit reached — you can make up to 10 requests per hour on the free tier.';
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 5000);
|
||||
return;
|
||||
}
|
||||
// 402 — no credits
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'credit-modal-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="credit-modal" role="dialog" aria-modal="true" aria-labelledby="cmTitle">
|
||||
<div class="credit-modal__icon">🪙</div>
|
||||
<h3 id="cmTitle">Free credits used up</h3>
|
||||
<p>Your 10 free credits for this month have been used.<br>
|
||||
Credits reset on the 1st of next month — or upgrade to CaveauAI for unlimited access.</p>
|
||||
<div class="credit-modal__actions">
|
||||
<a href="https://dobetternorge.no/tools/" class="credit-modal__cta">Learn about CaveauAI</a>
|
||||
<button class="credit-modal__dismiss" onclick="this.closest('.credit-modal-overlay').remove()">Close</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
}
|
||||
|
||||
// Exposed so barnevernet.js / deep-research.js can call it
|
||||
window.dbnFreeTierError = dbnFreeTierError;
|
||||
|
||||
// Inject credit badge into topbar on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (_freeTierBalance < 0) return;
|
||||
const actions = document.querySelector('.topbar-actions');
|
||||
if (!actions) return;
|
||||
const badge = document.createElement('span');
|
||||
badge.id = 'creditBadge';
|
||||
badge.className = 'credit-badge';
|
||||
badge.title = 'Free monthly credits remaining';
|
||||
dbnUpdateCredits(_freeTierBalance); // set initial state before inserting
|
||||
badge.textContent = '🪙 ' + _freeTierBalance + ' credit' + (_freeTierBalance !== 1 ? 's' : '');
|
||||
badge.classList.toggle('is-low', _freeTierBalance > 0 && _freeTierBalance <= 2);
|
||||
badge.classList.toggle('is-empty', _freeTierBalance === 0);
|
||||
actions.insertBefore(badge, actions.firstChild);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user