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:
Will Anderson
2026-05-02 11:15:09 -05:00
parent cae5028130
commit 640813e42e
27 changed files with 906 additions and 1461 deletions
+1 -348
View File
@@ -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 &middot; Never billed again</p>';
} else if (plan === 'professional') {
billingNote = '<p class=\"plan-billing-note\">Billed monthly &middot; <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 &#8594;</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>"