Dynamic Shell Loader
Spec v1 · Shattique · 7/24/26 · Status: awaiting Operator GO · Fixes: recurring "wrong / missing / dual shell" bugs
1 · Problem
Today every page hardcodes which shell it loads (
pure-appshell.js or admin-appshell.js) plus its mount() call. Consequences we keep firefighting: pages with the wrong shell (admin page on the front shell), pages with no shell (pure-avm, pure-calculators), pages that load both shells (52 admin pages pre-CP3), and mount calls stripped by cleanup edits (pure-admin, pure-cron). Menu placement (pure_nav_tree.menu_key) already knows whether a page is primary or admin — the page just doesn't read it.
2 · Goal
One shared loader,
pure-shell.js, that every page includes with a single line. At runtime it looks up the current page in the registry, decides pure / admin / none, loads the matching shell bundle, and mounts it. No page hardcodes a shell or a mount call ever again. New pages inherit correct chrome automatically.
3 · Source of truth
Add one column to the page registry:
pure_pages.shell — enum pure | admin | none | auto (default auto).
1
auto resolves from nav: page in
menu_key='admin' admin; in primary (or path under pure-admin/ = admin) pure; else pure.2
Explicit wins: full-screen surfaces (pure-meet, pure-chat, sign-in, index, onboarding) set
shell='none'. An admin tool surfaced on the front menu can force shell='admin'.3
Resolution is exposed by RPC
mcp_page_shell(p_path) {shell, active, title}, cached in localStorage (5 min TTL) so there is no network hit on the critical path after first load.4 · The loader
<!-- the ONE line every page carries, replacing all shell script tags + mount block --> <script src="/pure-shell.js" defer></script>
pure-shell.js logic: read location.pathname resolve shell (cache RPC path heuristic fallback if offline) if none, do nothing else inject the matching bundle scripts (pure-nav-tree.js + pure-appshell.js or admin-appshell.js) once (de-duped), wait for the global, call its .mount({active,title}). Guards: never mount twice, never load both bundles, honor window.PURE_SHELL_OFF for exempt/public pages.
5 · Migration (checkpointed, reversible)
1
CP1 — add
pure_pages.shell column + seed from current nav; build mcp_page_shell RPC; write pure-shell.js. Ship the loader dormant (no page uses it yet). Reversible: unused file. safe2
CP2 — pilot on 3 pages (1 front, 1 admin, 1 full-screen=pure-meet). Verify correct shell, no dual-load, no double mount. Backup branch first. pilot
3
CP3 — sweep: replace hardcoded shell tags + mount blocks with the one loader line across all ~220 menu pages via additive commit, in batches per turn-sizing. Each batch a separate commit = granular revert.
4
CP4 — add a pre-push gate check (
mcp_contract_check): any new page missing pure-shell.js OR still hardcoding a shell fails review. Record pure_memory.standard.dynamic_shell + Poppy note.6 · Guardrails
· Global-header standard preserved: the shells themselves are unchanged — this only changes how they're chosen/mounted, so header + rail stay identical everywhere.
· Full-screen pages (pure-meet, pure-chat) map to
· Offline/first-paint: path heuristic fallback means the shell still resolves if the RPC is slow — no blank chrome.
· Reversible at every CP: dormant file pilot batched sweep gate. Backup branch before the sweep.
· Changelog row per CP as operator; no silent page deletes.
· Full-screen pages (pure-meet, pure-chat) map to
shell='none' — keep their immersive layout; optional " Back to workspace" chip added separately.· Offline/first-paint: path heuristic fallback means the shell still resolves if the RPC is slow — no blank chrome.
· Reversible at every CP: dormant file pilot batched sweep gate. Backup branch before the sweep.
· Changelog row per CP as operator; no silent page deletes.
7 · What I need from you
Confirm the shell mapping for the edge cases: which pages should be none (full-screen). My proposed list:
pure-meet, pure-chat, sign-in, index, landing, onboarding, property.html (public microsite). Everything else = auto. Then say "CP1 go".