migrate stage build to native elc; chat restores from localStorage on return
Build pipeline - build-stage.sh replaces the old in-Dockerfile bootstrap.py path. Host pre-compiles src/*.el into dist/main.c via the canonical native elc at foundation/el/dist/platform/elc and applies the stub-decl sed before docker buildx runs. - Dockerfile.stage drops bootstrap.py + python3 from the builder stage and just runs cc on the host-supplied dist/main.c. - Pre-rendered HTML shells under /srv/landing/ are now chowned to the landing user so the El page-builder's fs_write at startup can rewrite them — without that, post-COPY edits never reach the served HTML and the served page stays as the stale build-time fallback. Chat restore - session.verified + session.verifiedAt persist through localStorage so a return visit within 24h skips the Turnstile gate and lands directly in the restored conversation. - restoreOrGreet() is the single source of truth for what shows up in the message pane after the gate clears: replays prior messages with skipSave, else drops the canned hello once and remembers it. - applyVerifiedDom() hides the gate / reveals the chat row, called both from the verified-on-load path (DOMContentLoaded if loading, else immediate) and from the Turnstile callback. - neuronDemoReset clears verified + verifiedAt so the gate returns next open. Extracted JS assets (src/assets/js/*.js + manifest.json) and the extract-js.py helper land here too — they match what the new build-stage flow produces and removes the inline <script> blobs from the served HTML.
This commit is contained in:
+1
-348
@@ -896,354 +896,7 @@ fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
|
||||
</div>
|
||||
|
||||
<script src=\"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js\"></script>
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var SUPABASE_URL = '" + supabase_url + "';
|
||||
var SUPABASE_ANON_KEY = '" + supabase_anon_key + "';
|
||||
|
||||
var sb = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
auth: { flowType: 'implicit' }
|
||||
});
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function show(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.style.display = '';
|
||||
}
|
||||
|
||||
function hide(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
}
|
||||
|
||||
function setText(id, text) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
function setHtml(id, html) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Sign-in ────────────────────────────────────────────────────────────────
|
||||
|
||||
window.signInWith = async function(provider) {
|
||||
var btn = document.getElementById('btn-' + provider);
|
||||
if (btn) { btn.disabled = true; btn.style.opacity = '0.6'; }
|
||||
try {
|
||||
var result = await sb.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo: window.location.origin + '/account'
|
||||
}
|
||||
});
|
||||
if (result.error) {
|
||||
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
|
||||
console.error('Sign-in error:', result.error.message);
|
||||
}
|
||||
} catch (e) {
|
||||
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
|
||||
console.error('Sign-in failed:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Email sign-in ──────────────────────────────────────────────────────────
|
||||
|
||||
window.signInWithEmail = async function() {
|
||||
var email = document.getElementById('acct-email-input').value.trim();
|
||||
var pass = document.getElementById('acct-pass-input').value;
|
||||
var msg = document.getElementById('acct-email-msg');
|
||||
var signinBtn = document.getElementById('acct-signin-btn');
|
||||
if (!sb) { msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Loading... try again in a moment.'; return; }
|
||||
if (!email || !pass) {
|
||||
msg.style.display = 'block'; msg.style.color = '#c44';
|
||||
msg.textContent = 'Please enter your email and password.'; return;
|
||||
}
|
||||
if (signinBtn) { signinBtn.disabled = true; signinBtn.textContent = 'Signing in...'; }
|
||||
// Try sign in first, then sign up if not found
|
||||
var result = await sb.auth.signInWithPassword({ email: email, password: pass });
|
||||
if (result.error) {
|
||||
if (result.error.message && result.error.message.toLowerCase().includes('invalid')) {
|
||||
// Try sign up
|
||||
var signupResult = await sb.auth.signUp({
|
||||
email: email, password: pass,
|
||||
options: { emailRedirectTo: window.location.origin + '/account' }
|
||||
});
|
||||
if (signupResult.error) {
|
||||
msg.style.display = 'block'; msg.style.color = '#c44';
|
||||
msg.textContent = signupResult.error.message; return;
|
||||
}
|
||||
msg.style.display = 'block'; msg.style.color = 'var(--navy)';
|
||||
msg.textContent = 'Check your email to confirm your account.'; return;
|
||||
}
|
||||
if (signinBtn) { signinBtn.disabled = false; signinBtn.textContent = 'Sign in'; }
|
||||
msg.style.display = 'block'; msg.style.color = '#c44';
|
||||
msg.textContent = result.error.message; return;
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// ── Sign-out ───────────────────────────────────────────────────────────────
|
||||
|
||||
window.signOut = async function() {
|
||||
var btn = document.getElementById('signout-btn');
|
||||
var btnTop = document.getElementById('signout-btn-top');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Signing out...'; }
|
||||
if (btnTop) { btnTop.disabled = true; btnTop.textContent = 'Signing out...'; }
|
||||
await sb.auth.signOut();
|
||||
show('signin-section');
|
||||
hide('dashboard-section');
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Sign out'; }
|
||||
if (btnTop) { btnTop.disabled = false; btnTop.textContent = 'Sign out'; }
|
||||
};
|
||||
|
||||
// ── Render plan card ───────────────────────────────────────────────────────
|
||||
|
||||
async function renderPlanCard(row) {
|
||||
var plan = (row && row.plan) ? row.plan : 'free';
|
||||
var memberNum = (row && row.member_number) ? row.member_number : null;
|
||||
var source = (row && row.source) ? row.source : '';
|
||||
var createdAt = (row && row.created_at) ? row.created_at : null;
|
||||
|
||||
// Plan display name
|
||||
var planNames = { 'founding': 'Founding Member', 'professional': 'Professional', 'free': 'Free' };
|
||||
var planDisplay = planNames[plan] || 'Free';
|
||||
|
||||
// Status
|
||||
var statusLabel = 'Preorder';
|
||||
if (plan === 'free') statusLabel = 'Active';
|
||||
|
||||
setText('plan-name-el', planDisplay);
|
||||
|
||||
// Status badge
|
||||
var statusHtml = '';
|
||||
if (plan === 'founding' || plan === 'professional') {
|
||||
statusHtml += '<span class=\"status-badge-preorder\" style=\"margin-top:.625rem;display:inline-flex\">' +
|
||||
'<svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg>' +
|
||||
'Launching within 30 days</span>';
|
||||
} else {
|
||||
statusHtml += '<span class=\"plan-status\" style=\"margin-top:.625rem;display:inline-flex\"><span class=\"plan-status-dot\"></span>' + statusLabel + '</span>';
|
||||
}
|
||||
setHtml('plan-status-el', statusHtml);
|
||||
|
||||
// Billing note (replaces, never appends - this function may run more than once on auth state changes)
|
||||
var billingNote = '';
|
||||
if (plan === 'founding') {
|
||||
billingNote = '<p class=\"plan-billing-note\">Lifetime · Never billed again</p>';
|
||||
} else if (plan === 'professional') {
|
||||
billingNote = '<p class=\"plan-billing-note\">Billed monthly · <button class=\"plan-billing-link\" onclick=\"window.location.href=\\\'/contact\\\'\">Cancel</button></p>';
|
||||
} else {
|
||||
billingNote = '<p class=\"plan-billing-note\">On the waitlist</p>';
|
||||
}
|
||||
setHtml('plan-billing-note-el', billingNote);
|
||||
|
||||
// Meta
|
||||
var meta = '';
|
||||
if (createdAt) {
|
||||
var d = new Date(createdAt);
|
||||
var dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
meta += '<div class=\"plan-meta-item\"><span class=\"plan-meta-label\">Joined</span><span class=\"plan-meta-value\">' + dateStr + '</span></div>';
|
||||
}
|
||||
if (memberNum) {
|
||||
meta += '<div class=\"plan-meta-item\"><span class=\"plan-meta-label\">Member number</span><span class=\"plan-meta-value\">#' + memberNum + ' of 1,000</span></div>';
|
||||
}
|
||||
if (meta) {
|
||||
setHtml('plan-meta-el', meta);
|
||||
}
|
||||
|
||||
// Founding badge - always show for founding members, with member number if assigned
|
||||
if (plan === 'founding') {
|
||||
var badgeSection = document.getElementById('badge-section');
|
||||
var badgeContainer = document.getElementById('badge-html-container');
|
||||
if (badgeSection) badgeSection.style.display = '';
|
||||
var badgeN = memberNum || 0;
|
||||
fetch('/api/founding-badge?n=' + badgeN)
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(html) {
|
||||
if (badgeContainer) badgeContainer.innerHTML = html;
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
// Show roadmap for founding members
|
||||
var roadmapSection = document.getElementById('roadmap-section');
|
||||
if (plan === 'founding' && roadmapSection) roadmapSection.style.display = '';
|
||||
|
||||
// Family section
|
||||
if (plan === 'founding') {
|
||||
document.getElementById('family-section').style.display = 'block';
|
||||
var session = await sb.auth.getSession();
|
||||
var userEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
||||
if (userEmail) loadFamilyMembers(userEmail);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Family plan ────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadFamilyMembers(parentEmail) {
|
||||
var r = await fetch('/api/family/members?parent_email=' + encodeURIComponent(parentEmail));
|
||||
var members = await r.json();
|
||||
var list = document.getElementById('family-list');
|
||||
if (!list) return;
|
||||
if (!members || !members.length) {
|
||||
list.innerHTML = '<p style=\"color:var(--t3);font-size:.875rem;margin-bottom:1rem\">No family members yet.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = members.map(function(m) {
|
||||
return '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--border)\">' +
|
||||
'<div><p style=\"font-size:.875rem;color:var(--t1)\">' + m.child_email + '</p>' +
|
||||
'<p style=\"font-size:.75rem;color:var(--t3);text-transform:uppercase;letter-spacing:.06em\">' + m.status + '</p></div>' +
|
||||
'<button onclick=\"removeFamilyMember(\\\'' + m.child_email + '\\\')\" style=\"background:none;border:none;color:var(--t3);cursor:pointer;font-size:.75rem\">Remove</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.addFamilyMember = async function() {
|
||||
var email = document.getElementById('child-email').value.trim();
|
||||
var year = document.getElementById('child-dob-year').value;
|
||||
var attest = document.getElementById('family-attest').checked;
|
||||
var msg = document.getElementById('family-msg');
|
||||
if (!email || !year || !attest) {
|
||||
msg.style.display = 'block'; msg.style.color = '#c44';
|
||||
msg.textContent = 'Please fill in all fields and confirm the attestation.'; return;
|
||||
}
|
||||
if (parseInt(year) < 2008) {
|
||||
msg.style.display = 'block'; msg.style.color = '#c44';
|
||||
msg.textContent = 'Child must be under 18. Birth year must be 2008 or later.'; return;
|
||||
}
|
||||
var session = await sb.auth.getSession();
|
||||
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
||||
var r = await fetch('/api/family/invite', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({parent_email: parentEmail, child_email: email, child_dob_year: parseInt(year), attested: true})
|
||||
});
|
||||
var d = await r.json();
|
||||
msg.style.display = 'block';
|
||||
if (d.ok) {
|
||||
msg.style.color = 'var(--navy)';
|
||||
msg.textContent = 'Invitation sent to ' + email + '. They will receive an email to set up their account.';
|
||||
document.getElementById('child-email').value = '';
|
||||
document.getElementById('child-dob-year').value = '';
|
||||
document.getElementById('family-attest').checked = false;
|
||||
loadFamilyMembers(parentEmail);
|
||||
} else {
|
||||
msg.style.color = '#c44';
|
||||
msg.textContent = d.error || 'Something went wrong.';
|
||||
}
|
||||
};
|
||||
|
||||
window.removeFamilyMember = async function(childEmail) {
|
||||
var session = await sb.auth.getSession();
|
||||
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
||||
await fetch('/api/family/remove', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({parent_email: parentEmail, child_email: childEmail})
|
||||
});
|
||||
loadFamilyMembers(parentEmail);
|
||||
};
|
||||
|
||||
// ── Render user info ───────────────────────────────────────────────────────
|
||||
|
||||
function renderUserChip(user) {
|
||||
var email = user.email || '';
|
||||
var avatarEl = document.getElementById('user-avatar-el');
|
||||
var emailEl = document.getElementById('user-email-el');
|
||||
var headerEmailEl = document.getElementById('acct-header-email');
|
||||
|
||||
if (emailEl) emailEl.textContent = email;
|
||||
if (headerEmailEl) headerEmailEl.textContent = email;
|
||||
|
||||
var avatarUrl = user.user_metadata && user.user_metadata.avatar_url;
|
||||
if (avatarEl) {
|
||||
if (avatarUrl) {
|
||||
avatarEl.innerHTML = '<img src=\"' + avatarUrl + '\" alt=\"\" referrerpolicy=\"no-referrer\">';
|
||||
} else {
|
||||
// Initials fallback
|
||||
var initial = email ? email.charAt(0).toUpperCase() : '?';
|
||||
avatarEl.textContent = initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Load waitlist data ─────────────────────────────────────────────────────
|
||||
|
||||
async function loadWaitlistData(email) {
|
||||
try {
|
||||
var sess = await sb.auth.getSession();
|
||||
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
|
||||
if (!token) { showNoPlan(); return; }
|
||||
|
||||
var r = await fetch('/api/my-plan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ access_token: token })
|
||||
});
|
||||
var row = await r.json();
|
||||
|
||||
if (!row || !row.plan) { showNoPlan(); return; }
|
||||
renderPlanCard(row);
|
||||
} catch (e) {
|
||||
showNoPlan();
|
||||
}
|
||||
}
|
||||
|
||||
// ── No plan — show pricing ─────────────────────────────────────────────────
|
||||
|
||||
function showNoPlan() {
|
||||
var el = document.getElementById('plan-card');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class=\"card-label\">Your plan</div>' +
|
||||
'<p style=\"font-family:var(--body);font-weight:500;font-size:1.125rem;color:var(--t1);margin-bottom:.75rem\">No active plan</p>' +
|
||||
'<p style=\"font-family:var(--body);font-weight:300;font-size:.9rem;color:var(--t2);line-height:1.7;margin-bottom:1.5rem\">You have an account but no plan selected yet. Pick one below to preorder.</p>' +
|
||||
'<div style=\"display:flex;gap:1rem;flex-wrap:wrap\">' +
|
||||
'<a href=\"/checkout?plan=founding\" class=\"btn-primary\" style=\"padding:.75rem 1.5rem\">Founding Member - $199 →</a>' +
|
||||
'<a href=\"/checkout?plan=professional\" class=\"btn-ghost\" style=\"padding:.75rem 1.5rem\">Professional - $19/mo</a>' +
|
||||
'<a href=\"/checkout?plan=free\" class=\"btn-ghost\" style=\"padding:.75rem 1.5rem\">Free tier</a>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ── Show dashboard ─────────────────────────────────────────────────────────
|
||||
|
||||
function showDashboard(user) {
|
||||
hide('signin-section');
|
||||
show('dashboard-section');
|
||||
renderUserChip(user);
|
||||
loadWaitlistData(user.email);
|
||||
}
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function init() {
|
||||
var result = await sb.auth.getSession();
|
||||
var session = result.data && result.data.session;
|
||||
|
||||
if (session && session.user) {
|
||||
showDashboard(session.user);
|
||||
} else {
|
||||
show('signin-section');
|
||||
hide('dashboard-section');
|
||||
}
|
||||
|
||||
// Listen for auth changes (e.g. OAuth redirect return)
|
||||
sb.auth.onAuthStateChange(function(event, session) {
|
||||
if (session && session.user) {
|
||||
showDashboard(session.user);
|
||||
} else {
|
||||
show('signin-section');
|
||||
hide('dashboard-section');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
})();
|
||||
</script>
|
||||
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";</script><script src=\"/assets/js/6dafc1586705.js\" defer></script>
|
||||
|
||||
</body>
|
||||
</html>"
|
||||
|
||||
Reference in New Issue
Block a user