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 -77
View File
@@ -185,83 +185,7 @@ body::before {
</div>
</div>
<script>
(function() {
// Nav scroll effect
var nav = document.getElementById('nav');
if (nav) window.addEventListener('scroll', function() {
nav.classList.toggle('scrolled', window.scrollY > 10);
}, { passive: true });
// Hamburger
var btn = document.getElementById('nav-hamburger');
var menu = document.getElementById('nav-mobile');
if (btn && menu) {
function navClose() { menu.classList.remove('open'); btn.setAttribute('aria-expanded','false'); }
function navOpen() { menu.classList.add('open'); btn.setAttribute('aria-expanded','true'); }
btn.addEventListener('click', function(e) { e.stopPropagation(); menu.classList.contains('open') ? navClose() : navOpen(); });
menu.querySelectorAll('a').forEach(function(a) { a.addEventListener('click', navClose); });
document.addEventListener('click', function(e) { if (!nav.contains(e.target)) navClose(); });
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') navClose(); });
window.addEventListener('resize', function() { if (window.innerWidth > 1060) navClose(); });
}
// Dropdown - Mission
var ddBtn = document.querySelector('.nav-dropdown-btn');
var dd = document.querySelector('.nav-dropdown');
if (ddBtn && dd) {
ddBtn.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = dd.classList.contains('open');
dd.classList.toggle('open');
ddBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
dd.querySelectorAll('.nav-dropdown-item').forEach(function(a) { a.addEventListener('click', function() { dd.classList.remove('open'); }); });
document.addEventListener('click', function() { dd.classList.remove('open'); });
}
// Search
var searchEl = document.getElementById('gal-search');
var grid = document.getElementById('gallery-grid');
var noResults = document.getElementById('no-results');
function filterCards() {
var q = (searchEl.value || '').toLowerCase().trim();
var cards = grid.querySelectorAll('.gal-card');
var visible = 0;
cards.forEach(function(c) {
var text = c.textContent.toLowerCase();
var match = !q || text.indexOf(q) !== -1;
c.classList.toggle('hidden', !match);
if (match) visible++;
});
noResults.style.display = visible === 0 && q ? 'block' : 'none';
}
if (searchEl) searchEl.addEventListener('input', filterCards);
// Sort
var currentSort = 'top';
window.setSort = function(mode, btn) {
document.querySelectorAll('.sort-btn').forEach(function(b){ b.classList.remove('active'); });
btn.classList.add('active');
currentSort = mode;
var cards = Array.from(grid.querySelectorAll('.gal-card'));
cards.sort(function(a, b) {
if (mode === 'top') {
var aV = parseInt(a.getAttribute('data-score') || '0');
var bV = parseInt(b.getAttribute('data-score') || '0');
return bV - aV;
} else {
var aT = parseInt(a.getAttribute('data-ts') || '0');
var bT = parseInt(b.getAttribute('data-ts') || '0');
return bT - aT;
}
});
cards.forEach(function(c){ grid.appendChild(c); });
};
})();
</script>
<script src=\"/assets/js/cd30551e3c3b.js\" defer></script>
</body>
</html>"
}