Fix question counter daily reset, rate-limit timer, and founding member pricing clarity
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m0s
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m0s
- chat-widget: session.count now resets on new UTC day (keeps client in sync with server's daily quota reset) - chat-widget: fix rate-limit timer — wrong element IDs (neuron-demo-msgs → neuron-demo-messages) and wrong class (.neuron-msg-ai → .demo-msg-ai) meant the countdown never updated - chat-widget: remove btn.disabled=false that immediately re-enabled the send button after rate-limiting - main.el: add POST /api/admin/reset-rate-limits endpoint (requires NEURON_ADMIN_TOKEN, deletes all demo_rate_limits rows) - pricing.el: clarify founding member card — software updates are free forever, inference is pay-per-use at founding member rate
This commit is contained in:
+32
-20
@@ -142,14 +142,27 @@ fn main() -> Void {
|
||||
}
|
||||
}
|
||||
|
||||
function _todayUTC() { return Math.floor(Date.now() / 86400000); }
|
||||
function loadSession() {
|
||||
try {
|
||||
var s = localStorage.getItem('neuron_demo_session');
|
||||
return s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
|
||||
var parsed = s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
|
||||
// Reset count (and conversation) on new UTC day — keeps client in sync with server
|
||||
var today = _todayUTC();
|
||||
if (parsed.day !== today) {
|
||||
parsed.count = 0;
|
||||
parsed.messages = [];
|
||||
parsed.greeted = false;
|
||||
parsed.day = today;
|
||||
}
|
||||
return parsed;
|
||||
} catch(e) { return { messages: [], count: 0, context: '' }; }
|
||||
}
|
||||
function saveSession(session) {
|
||||
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
|
||||
try {
|
||||
if (!session.day) session.day = _todayUTC();
|
||||
localStorage.setItem('neuron_demo_session', JSON.stringify(session));
|
||||
} catch(e) {}
|
||||
}
|
||||
function clearSession() {
|
||||
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
|
||||
@@ -525,34 +538,33 @@ fn main() -> Void {
|
||||
// Server-side rate limit — show a live countdown to reset
|
||||
if (d.rate_limited && d.reset_at) {
|
||||
var _showRateTimer = function() {
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var secsLeft = Math.max(0, d.reset_at - now);
|
||||
var hh = Math.floor(secsLeft / 3600);
|
||||
var mm = Math.floor((secsLeft % 3600) / 60);
|
||||
var ss = secsLeft % 60;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss));
|
||||
return \"You've had 10 conversations today. Come back in \" + ts + \".\";
|
||||
var hh = Math.floor(secsLeft / 3600);
|
||||
var mm = Math.floor((secsLeft % 3600) / 60);
|
||||
var ss = secsLeft % 60;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
return \"You've reached today's limit. Resets in \" + hh + ':' + pad(mm) + ':' + pad(ss) + '.';
|
||||
};
|
||||
addMsg('ai', _showRateTimer());
|
||||
// Update the last ai message with a live ticker
|
||||
// Update the bubble text with a live ticker
|
||||
var _timerInterval = setInterval(function() {
|
||||
var thMsgsInner = document.getElementById('neuron-demo-msgs');
|
||||
if (!thMsgsInner) { clearInterval(_timerInterval); return; }
|
||||
var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai');
|
||||
var lastAi = aiMsgs[aiMsgs.length - 1];
|
||||
if (lastAi) { lastAi.textContent = _showRateTimer(); }
|
||||
var msgsEl = document.getElementById('neuron-demo-messages');
|
||||
if (!msgsEl) { clearInterval(_timerInterval); return; }
|
||||
var aiMsgs = msgsEl.querySelectorAll('.demo-msg-ai');
|
||||
var lastAi = aiMsgs[aiMsgs.length - 1];
|
||||
var lastBubble = lastAi ? lastAi.querySelector('.demo-msg-bubble') : null;
|
||||
if (lastBubble) { lastBubble.textContent = _showRateTimer(); }
|
||||
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
|
||||
clearInterval(_timerInterval);
|
||||
if (lastAi) { lastAi.textContent = \"You're all set — conversations reset. Say hello!\"; }
|
||||
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
||||
if (btn) { btn.disabled = false; }
|
||||
if (lastBubble) { lastBubble.textContent = \"You're all set — conversations reset. Say hello!\"; }
|
||||
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
||||
if (btn) { btn.disabled = false; }
|
||||
msgCount = 0; session.count = 0; session.day = _todayUTC(); saveSession(session); updateCountdown();
|
||||
}
|
||||
}, 1000);
|
||||
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
|
||||
if (btn) { btn.disabled = true; }
|
||||
if (btn) { btn.disabled = false; }
|
||||
if (input) { input.focus(); }
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+26
@@ -873,6 +873,32 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
return "{\"rows\":" + ac_resp + "}"
|
||||
}
|
||||
|
||||
// ── Admin: reset all demo rate limits ────────────────────────────────────
|
||||
// POST { "admin_token": "<NEURON_ADMIN_TOKEN>" }
|
||||
// Deletes all rows from demo_rate_limits — resets every user's daily quota.
|
||||
if str_eq(path, "/api/admin/reset-rate-limits") {
|
||||
if !str_eq(method, "POST") {
|
||||
return "{\"__status__\":405,\"error\":\"POST required\"}"
|
||||
}
|
||||
let rrl_token_in: String = json_get(body, "admin_token")
|
||||
let rrl_token_exp: String = env("NEURON_ADMIN_TOKEN")
|
||||
if str_eq(rrl_token_exp, "") {
|
||||
return "{\"__status__\":503,\"error\":\"admin_token_not_configured\"}"
|
||||
}
|
||||
if !str_eq(rrl_token_in, rrl_token_exp) {
|
||||
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
||||
}
|
||||
let rrl_sb_url: String = state_get("__supabase_project_url__")
|
||||
let rrl_sb_key: String = state_get("__supabase_service_key__")
|
||||
if str_eq(rrl_sb_url, "") || str_eq(rrl_sb_key, "") {
|
||||
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
||||
}
|
||||
// DELETE /rest/v1/demo_rate_limits?uid=not.is.null (all rows)
|
||||
let rrl_url: String = rrl_sb_url + "/rest/v1/demo_rate_limits?uid=not.is.null"
|
||||
let _rrl_resp: String = http_delete_auth(rrl_url, rrl_sb_key, rrl_sb_key)
|
||||
return "{\"ok\":true,\"message\":\"rate limits cleared\"}"
|
||||
}
|
||||
|
||||
// ── My plan: server-side waitlist read with JWT verification ─────────────
|
||||
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
|
||||
// /auth/v1/user, extract the email, then read the waitlist row with the
|
||||
|
||||
+3
-3
@@ -51,9 +51,9 @@ fn pricing_pro_features() -> String {
|
||||
}
|
||||
|
||||
fn pricing_founding_features() -> String {
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - pay-per-use at the founding member rate, below the major APIs")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Never pay again - lifetime updates included")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "No subscription — software updates are free forever")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) +
|
||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
|
||||
@@ -125,7 +125,7 @@ fn pricing(sold: Int, total: Int) -> String {
|
||||
el_span("class=\"pricing-price\"", "$199") +
|
||||
el_span("class=\"pricing-cadence\"", "lifetime")
|
||||
) +
|
||||
el_p("class=\"pricing-tagline\"", "Pay once. Everything, forever. Including Neuron Inference when it launches.") +
|
||||
el_p("class=\"pricing-tagline\"", "Pay once for the platform — free software updates, forever. Inference is pay-per-use at your founding member rate.") +
|
||||
spots_html +
|
||||
el_ul("class=\"pricing-features\"", pricing_founding_features()) +
|
||||
el_div("style=\"flex:1\"", "") +
|
||||
|
||||
Reference in New Issue
Block a user