## 9. Alpha: one restore path when leaving Settings (Hypha item 9) **Cause:** `openPlace` hides galaxy nodes for Settings and marks them `data-flc-was-hidden` (`garden-rooms.js:1649-1661`), but only `closePlace` puts them back. Settings then Nursery keeps both hide classes. Settings twice marks the already hidden nodes as `'1'`, so close never unhides them. **Fix:** a small `flcRestoreGalaxy()` inside `bindPlaceDoors`: unmount FlConnect, hide the mount, drop both classes, unhide only `'0'` nodes, and remove every mark. Call it first in `openPlace` and in `closePlace`. The v0.3 restore code stays (it becomes a no-op). ```diff --- a/docs/modules/garden-rooms.js +++ b/docs/modules/garden-rooms.js @@ -1585,8 +1585,27 @@ var closeBtn = document.getElementById('place-veil-close'); var doors = document.querySelectorAll('[data-garden-place]'); + // v-connect-heal-v0.4 / Hypha walk 3 item 9: one restore path for leaving + // Settings (close, Nursery, Core, or Settings again). Put back only what + // Settings hid, then forget the marks so a second open records fresh. + function flcRestoreGalaxy() { + try { + if (window.FlConnect && typeof FlConnect.unmount === 'function') FlConnect.unmount(); + var host = document.getElementById('fl-connect-mount-garden'); + if (host) host.hidden = true; + document.documentElement.classList.remove('flc-hide-galaxy'); + document.body.classList.remove('fl-connect-open'); + var marked = document.querySelectorAll('[data-flc-was-hidden]'); + for (var mi = 0; mi < marked.length; mi++) { + if (marked[mi].getAttribute('data-flc-was-hidden') === '0') marked[mi].hidden = false; + marked[mi].removeAttribute('data-flc-was-hidden'); + } + } catch (eR) {} + } + function closePlace(opts) { if (!veil) return; + flcRestoreGalaxy(); // v-connect-under-more-v0 heal v0.1 — stop FlConnect loop when Settings room closes try { if (window.FlConnect && typeof FlConnect.unmount === 'function') FlConnect.unmount(); @@ -1617,6 +1636,7 @@ function openPlace(id) { if (!veil) return; + flcRestoreGalaxy(); hideLuminoMenu(); if (window.GardenRooms && GardenRooms.closeThread) { GardenRooms.closeThread({ silentLabel: true }); @@ -1648,6 +1668,7 @@ } } else if (id === 'settings') { veil.classList.add('is-settings'); + try { veil.scrollTop = 0; } catch (eTop) {} // v-connect-port-picker-v0 / Hypha H5 — hide orb/planets while Connect/Settings open try { document.documentElement.classList.add('flc-hide-galaxy'); ``` Measured (galaxy nav, galaxy word, luminos): | Sequence | Main | After | |---|---|---| | Settings then Nursery | nav and word hidden in Nursery, `fl-connect-open` stays | same as a fresh Nursery | | Settings, Settings, close | `.galaxy-nav` and `#galaxy-word` stuck hidden | all back | --- ## 10. Shared `fl-connect.js`: remember() keeps isLocal (Hypha item 10) **Cause:** `remember()` sets `state.isLocal = true`, then calls `handleLocalToggle(true)` (`fl-connect.js:345`), which reads the unchecked `#localToggle` and writes `state.isLocal = false` (`app.html:35423-35426`). **Fix:** check the toggle first (the same thing `connectOllama` already does at `27730-27731`). This hunk also hides the Local AI toast after a mind is chosen (item 7). ```diff --- a/docs/modules/fl-connect.js +++ b/docs/modules/fl-connect.js @@ -342,9 +354,13 @@ root.state.provider = 'ollama'; if (name) root.state.ollamaModel = name; } + // v-connect-heal-v0.4 item 10: handleLocalToggle reads #localToggle, so check it first + try { var lt = document.getElementById('localToggle'); if (lt) lt.checked = true; } catch (eLt) {} if (typeof root.handleLocalToggle === 'function') root.handleLocalToggle(true); if (typeof root.updateStatus === 'function') root.updateStatus(); if (root.AiSetup && typeof root.AiSetup.updateStatus === 'function') root.AiSetup.updateStatus(); + // v-connect-heal-v0.4 item 7: once they chose a mind, the Local AI toast stops asking + try { var tt = document.getElementById('flOllamaToast'); if (tt) tt.classList.remove('fl-toast-show'); } catch (eTt) {} } catch (eF) {} stopLoop(); return { name: name, base: base }; ``` Markers and one readability line (the "All Bridge downloads" link was dark blue on night, now gold like Other ways): ```diff --- a/docs/modules/fl-connect.js +++ b/docs/modules/fl-connect.js @@ -1,7 +1,7 @@ // ═══════════════════════════════════════════════════════════════ // fl-connect.js — FlConnect shared core (FreeLattice + theLatticeTree) // -// Marker: v-connect-under-more-v0 · heal v0.1 · v-connect-port-picker-v0 · heal v0.2 +// Marker: v-connect-under-more-v0 · heal v0.1 · v-connect-port-picker-v0 · heal v0.2 · v-connect-heal-v0.4 // Layer, never delete. Prefer helped Bridge (11435…) before bare 11434. // Port-only loopback: fl_localPort_manual holds "11500" only — never foreign hosts. // Soft leave sw.js. Alpha: fl_alpha_local_mind. Never bare *. @@ -622,6 +650,7 @@ '.flc-btn{display:inline-block;min-height:48px;line-height:48px;padding:0 1.1rem;border-radius:10px;text-decoration:none;font-weight:600;border:0;cursor:pointer;font:inherit}', '.flc-primary{background:rgba(52,211,153,.18);border:1px solid rgba(52,211,153,.45);color:#34d399}', '.flc-soft{font-size:.8rem;color:rgba(148,163,184,.95);margin:.65rem 0 0;line-height:1.45}', + '.flc-soft a{color:#e8b019}', '.flc-other,.flc-builders{margin:1rem 0;font-size:.88rem;color:rgba(200,210,230,.8)}', '.flc-other ul{margin:.5rem 0 0;padding-left:1.2rem}', '.flc-other a{color:#e8b019}', ``` After this, copy `docs/modules/fl-connect.js` to Alpha byte for byte. --- ## 11. FreeLattice smoke: `docs/scripts/smoke-connect-heal-v0.4.js` (new) Static checks plus runtime checks in `vm` (Settings address save, un-helped manual Bridge, refusal, Use automatic, remember). Locks: index equals app, sw.js equals main, no `confirm(` and no em dash in added lines, no conflict markers, inline scripts parse. It fails on main at the first marker check, and fails if the item 4 `return report;` is removed. ```js #!/usr/bin/env node // v-connect-heal-v0.4: Hypha walk 3 heals (FreeLattice side). // Quiet Bridge interval, cold #connect lands once, manual port asks for // Yes, help, Use automatic really clears, loopback-only Ollama Address, // no silent connect or model pick, modal self-heal, remember keeps isLocal. 'use strict'; const assert = require('assert'); const fs = require('fs'); const path = require('path'); const vm = require('vm'); const { execSync } = require('child_process'); const root = path.join(__dirname, '..'); const repo = path.join(root, '..'); const read = (r) => fs.readFileSync(path.join(root, r), 'utf8'); const app = read('app.html'); const mod = read('modules/fl-connect.js'); const indexHtml = fs.readFileSync(path.join(repo, 'index.html'), 'utf8'); const rootSw = fs.readFileSync(path.join(repo, 'sw.js'), 'utf8'); function fnBody(src, sig) { const i = src.indexOf(sig); assert.ok(i >= 0, 'missing ' + sig); const end = src.indexOf('\n}\n', i); return src.slice(i, end + 2); } // Markers assert.ok(/v-connect-heal-v0\.4/.test(mod), 'marker in fl-connect.js'); assert.ok(/v-connect-heal-v0\.4/.test(app), 'marker in app.html'); // Item 2: Bridge interval is quiet const boot = app.slice(app.indexOf('(function flBridgeDetectBoot()'), app.indexOf('window.flBridgeDetect = flBridgeDetect;')); assert.ok(!/setInterval\(run,\s*20000\)/.test(boot), 'no forever 11-port scan'); assert.ok(/flBridgeQuietTick/.test(boot) && /document\.hidden/.test(boot) && /flBridgeHealthAt\(saved\)/.test(boot), 'quiet tick: visible, saved port only'); // Item 3: cold #connect const go = app.slice(app.indexOf('var _flcUserMoved = false;'), app.indexOf("window.addEventListener('hashchange', goConnect)")); assert.ok(/_flcUserMoved/.test(go) && /pointerdown/.test(app) && /history\.replaceState/.test(go), 'first tap ends retries and clears #connect'); assert.ok(/tab-connect[\s\S]{0,120}classList\.contains\('active'\)\) return;/.test(go), 'no remount when Connect already showing'); // Item 5: manual port wins in getOllamaBaseUrl const gob = fnBody(app, 'function getOllamaBaseUrl() {'); assert.ok(gob.indexOf('flManualBaseFirst()') > 0 && gob.indexOf('flManualBaseFirst()') < gob.indexOf("fl_ollamaHost"), 'manual before fl_ollamaHost'); // Item 6: loopback-only Settings address, runtime check assert.ok(!/Change to a remote IP/.test(app), 'no remote IP invite'); assert.ok(/id="ollamaHostRefusal"/.test(app), 'refusal line present'); { const store = { fl_ollamaHost: '127.0.0.1:11434' }; const els = { ollamaHostInput: { value: '' }, ollamaHostRefusal: { hidden: true, textContent: '' } }; const ctx = { localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } }, document: { getElementById: (id) => els[id] || null }, URL, _ollamaResolvedBase: { base: 'x' }, updateOllamaProviderUrl() {}, FLBoxPointer: undefined }; vm.runInNewContext(fnBody(app, 'function flWriteOllamaHostLoopbackOnly(host) {') + fnBody(app, 'function flSaveOllamaHost() {') + '\nthis.save = flSaveOllamaHost;', ctx); els.ollamaHostInput.value = '192.168.1.50:11434'; ctx.save(); assert.strictEqual(store.fl_ollamaHost, '127.0.0.1:11434', 'LAN refused, saved address unchanged'); assert.strictEqual(els.ollamaHostRefusal.hidden, false, 'friendly refusal shown'); assert.ok(/only talks to your own computer/.test(els.ollamaHostRefusal.textContent), 'refusal words'); els.ollamaHostInput.value = 'localhost:11500'; ctx.save(); assert.strictEqual(store.fl_ollamaHost, '127.0.0.1:11500', 'loopback port saved as 127.0.0.1'); assert.strictEqual(els.ollamaHostRefusal.hidden, true, 'refusal clears'); } // Item 7: no silent connect or model pick const co = app.slice(app.indexOf('function connectOllama(models) {'), app.indexOf('// Main app load detection')); assert.ok(co.indexOf('FlConnect.open()') > 0 && co.indexOf('FlConnect.open()') < co.indexOf('models[0].name'), 'connectOllama opens Connect before any models[0]'); assert.ok(!/Connected to a mind on this computer!/.test(app), 'toast no longer claims connected'); const ac = app.slice(app.indexOf('function flAutoConnect(opts) {'), app.indexOf('var targets = [')); assert.ok(/FlConnect\.open/.test(ac) && /return;\s*\}/.test(ac), 'Zero-Click returns before probing when FlConnect exists'); const ad = app.slice(app.indexOf('async function autoDiscoverAI() {'), app.indexOf('// Auto-connect to the best ready server')); assert.ok(/if \(window\.FlConnect && typeof FlConnect\.open === 'function'\) return false;/.test(ad), 'autoDiscoverAI finds but never connects by itself'); assert.ok(/flOllamaToast'\); if \(tt\) tt\.classList\.remove\('fl-toast-show'\)/.test(mod), 'toast stops asking after a mind is chosen'); // Item 8: Settings Local opens Connect; modal self-heals const ssm = app.slice(app.indexOf("if (mode === 'local') {"), app.indexOf("flOpenConnectWizard('local')")); assert.ok(/FlConnect\.open\(\)[\s\S]{0,40}return;/.test(ssm), 'Local mode opens Connect first'); assert.ok(/modalOverlayEl && !document\.body\.contains\(modalOverlayEl\)\) modalOverlayEl = null;/.test(app), 'openModal forgets detached overlay'); assert.ok(/data-flc-cloud-key/.test(mod) && /AiSetup\.openModal\(\)/.test(mod), 'Connect has a real cloud key door'); // fl-connect.js runtime: items 4, 5, 10 function sandbox(store, extra) { const sb = Object.assign({ localStorage: { getItem: (k) => (Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } }, location: { hostname: 'freelattice.com', protocol: 'https:', hash: '', search: '' }, document: { hidden: false, documentElement: { getAttribute: () => '', classList: { add() {}, remove() {}, contains() { return false; } } }, getElementById: () => null, head: { appendChild() {} }, body: { contains: () => false, classList: { add() {}, remove() {} } }, addEventListener() {}, removeEventListener() {}, querySelectorAll: () => [] }, fetch: async () => { throw new Error('offline'); }, setTimeout: () => 1, clearTimeout() {}, navigator: { userAgent: 't' }, console, JSON, String, parseInt, URL, Date, Math, AbortSignal: { timeout: () => undefined } }, extra || {}); sb.window = sb; vm.runInNewContext(mod, sb); return sb; }