mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
feat(platform): Add support for APFEL as part of the dependencies and models for the Cookbook. (#2657)
* feat(platform): add support for Apple Silicon detection in platform compatibility test(tests): enhance shell_routes tests for Apple Silicon compatibility * fix issues with missing import * fix: correct package name in package-lock.json and enhance package installation commands in shell_routes.py and cookbook.js * feat: add Apfel startup and health checks on macOS - bootstrap Apfel via Homebrew on arm64 macOS - start `apfel --serve --port 11435` detached for Odysseus - verify readiness via `/health` - clean up the Apfel process on exit or Ctrl+C * fix: duplicate variable declaration post-merge conflict - Should fix `node` CI issues. * fix: issues with the update status of the APFEL dependency. - fixed by changing the main conditional that determines the update. * Fix: Remove unnecessary whitespaces and formatting for the model_routes.py file. * Fix: whitespace issues with the model_routes file * Fix: Remove unnecessary whitespaces and formatting for the model_routes.py file. Final * Fix: Fixed updates using PIP for APFEL instead of custom cmd
This commit is contained in:
committed by
GitHub
parent
8f2c8d2dc8
commit
8d9d4ec9c6
+132
-91
@@ -89,8 +89,8 @@ function _setCookbookOpening(on) {
|
||||
].filter(Boolean);
|
||||
if (!on) {
|
||||
_cookbookOpeningSpinners.forEach(({ spinner, wrap, target }) => {
|
||||
try { spinner?.stop?.(); } catch {}
|
||||
try { wrap?.remove?.(); } catch {}
|
||||
try { spinner?.stop?.(); } catch { }
|
||||
try { wrap?.remove?.(); } catch { }
|
||||
target?.classList?.remove('cookbook-opening');
|
||||
});
|
||||
_cookbookOpeningSpinners = [];
|
||||
@@ -595,7 +595,7 @@ function _fallbackCopy(text) {
|
||||
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (_) {}
|
||||
try { document.execCommand('copy'); } catch (_) { }
|
||||
document.body.removeChild(ta);
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -628,7 +628,7 @@ function _readStoredEnvState() {
|
||||
|
||||
export function _persistEnvState() {
|
||||
try { localStorage.setItem(LAST_STATE_KEY, JSON.stringify(_envStateForStorage())); }
|
||||
catch (_) {}
|
||||
catch (_) { }
|
||||
_saveTasks(_loadTasks());
|
||||
}
|
||||
|
||||
@@ -681,18 +681,20 @@ async function _fetchDependencies() {
|
||||
|
||||
const _statusTag = (pkg, isLocal, isSystemDep, winBlocked) => {
|
||||
if (winBlocked) return `<span class="cookbook-dep-tag cookbook-dep-na">N/A</span>`;
|
||||
if (pkg.installed && isSystemDep) return `<span class="cookbook-dep-tag cookbook-dep-installed" title="Found on selected server">Installed</span>`;
|
||||
if (pkg.installed && pkg.pip_update_available === false) {
|
||||
const hasCustomInstall = !!pkg.install_cmd;
|
||||
const hasCustomUpdate = !!pkg.update_cmd;
|
||||
if (pkg.installed && isSystemDep && !hasCustomUpdate) return `<span class="cookbook-dep-tag cookbook-dep-installed" title="Found on selected server">Installed</span>`;
|
||||
if (pkg.installed && pkg.pip_update_available === false && !hasCustomUpdate) {
|
||||
const tip = esc(pkg.update_note || pkg.status_note || 'Found externally; update outside Odysseus.');
|
||||
return `<span class="cookbook-dep-tag cookbook-dep-installed" title="${tip}">Installed</span>`;
|
||||
}
|
||||
if (pkg.installed) return `<button class="cookbook-dep-tag cookbook-dep-installed cookbook-dep-installed-btn" title="Installed — click for actions"><span class="cookbook-dep-installed-label">Installed</span><span class="cookbook-dep-caret">▾</span></button>`;
|
||||
if (isSystemDep) {
|
||||
if (isSystemDep && !hasCustomInstall) {
|
||||
const depTip = esc(pkg.install_hint || 'Install this OS package on the selected server.');
|
||||
const depLabel = pkg.applicable === false ? 'N/A ?' : 'Missing';
|
||||
return `<span class="cookbook-dep-tag cookbook-dep-na" title="${depTip}">${depLabel}</span>`;
|
||||
}
|
||||
return `<button class="cookbook-dep-tag cookbook-dep-install" data-dep-pip="${esc(pkg.pip)}" data-dep-target="${isLocal ? 'local' : 'remote'}">Install</button>`;
|
||||
return `<button class="cookbook-dep-tag cookbook-dep-install" data-dep-pip="${esc(pkg.pip || '')}" data-dep-install-cmd="${esc(pkg.install_cmd || '')}" data-dep-update-cmd="${esc(pkg.update_cmd || '')}" data-dep-target="${isLocal ? 'local' : 'remote'}">Install</button>`;
|
||||
};
|
||||
|
||||
const _depRow = (pkg) => {
|
||||
@@ -715,7 +717,7 @@ async function _fetchDependencies() {
|
||||
} else if (pkg.name === 'sglang' && pkg.installed) {
|
||||
_rebuildBtn = `<button type="button" class="cookbook-dep-tag cookbook-dep-rebuild cookbook-dep-reinstall" data-reinstall-pkg="sglang" title="Force-reinstall SGLang (pulls a matching torch). Runs as a tmux task in the Running tab.">Reinstall</button>`;
|
||||
}
|
||||
return `<div class="cookbook-dep-row${winBlocked ? ' cookbook-dep-blocked' : ''}" data-pkg-name="${esc(pkg.name)}" data-dep-pip="${esc(pkg.pip || '')}" data-dep-target="${isLocal ? 'local' : 'remote'}" data-dep-kind="${esc(pkg.kind || 'python')}">`
|
||||
return `<div class="cookbook-dep-row${winBlocked ? ' cookbook-dep-blocked' : ''}" data-pkg-name="${esc(pkg.name)}" data-dep-pip="${esc(pkg.pip || '')}" data-dep-install-cmd="${esc(pkg.install_cmd || '')}" data-dep-update-cmd="${esc(pkg.update_cmd || '')}" data-dep-target="${isLocal ? 'local' : 'remote'}" data-dep-kind="${esc(pkg.kind || 'python')}">`
|
||||
+ `<div class="cookbook-dep-info">`
|
||||
+ `<div class="memory-item-title">${esc(pkg.name)}</div>`
|
||||
+ `<div class="memory-item-meta" style="font-size:10px;opacity:0.5;margin-top:2px;">${esc(pkg.desc)}</div>`
|
||||
@@ -745,7 +747,7 @@ async function _fetchDependencies() {
|
||||
// Shared install/update routine — used by the Install button and the
|
||||
// "Update" item in an installed package's ⋮ menu. `upgrade` adds pip -U;
|
||||
// `statusEl`, when given, shows "Installing…/Updating…" and is disabled.
|
||||
async function _installDep(pipName, pkgName, isLocalOnly, upgrade, statusEl) {
|
||||
async function _installDep(pipName, pkgName, isLocalOnly, upgrade, statusEl, actionCmd = '') {
|
||||
if (isLocalOnly) {
|
||||
_envState.remoteHost = '';
|
||||
_envState.env = 'none';
|
||||
@@ -790,6 +792,43 @@ async function _fetchDependencies() {
|
||||
envPrefix = 'eval "$(conda shell.bash hook)" && conda activate ' + _shellQuote(_envState.envPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (actionCmd) {
|
||||
const shellCmd = envPrefix ? `${envPrefix} ${actionCmd}` : actionCmd;
|
||||
const fullCmd = (!isLocalOnly && _envState.remoteHost)
|
||||
? _sshCmd(_envState.remoteHost, shellCmd, _getPort(_envState.remoteHost))
|
||||
: shellCmd;
|
||||
try {
|
||||
if (statusEl) { statusEl.textContent = upgrade ? 'Updating...' : 'Installing...'; statusEl.disabled = true; }
|
||||
const res = await fetch('/api/shell/stream', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command: fullCmd }),
|
||||
});
|
||||
uiModule.showToast(`${upgrade ? 'Updating' : 'Installing'} ${pkgName} on ${targetHost}...`);
|
||||
const body = await res.text();
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const exitMatches = [...body.matchAll(/"exit_code":\s*(-?\d+)/g)].map(m => Number(m[1]));
|
||||
const exitCode = exitMatches.length ? exitMatches[exitMatches.length - 1] : 0;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error((body.slice(-500).trim() || `${pkgName} command failed`) + ` (exit ${exitCode})`);
|
||||
}
|
||||
|
||||
if (upgrade) { uiModule.showToast(`Successfully updated ${pkgName} on ${targetHost}.`); } else { uiModule.showToast(`Successfully installed ${pkgName} on ${targetHost}.`); }
|
||||
await _fetchDependencies();
|
||||
return;
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = 'Install'; statusEl.disabled = false; }
|
||||
uiModule.showToast(`${upgrade ? 'Update' : 'Install'} failed: ` + err.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Always go through `python -m pip` so the leading token is `python`
|
||||
// — matches the /api/model/serve allow-list (bare `pip` is blocked).
|
||||
// Inside a venv/conda env, `--user` is invalid (pip refuses), so we
|
||||
// only add `--user --break-system-packages` when there's no env —
|
||||
// for PEP-668-locked system pythons (Arch, newer Debian).
|
||||
try {
|
||||
const reqBody = {
|
||||
repo_id: pipName,
|
||||
@@ -828,8 +867,9 @@ async function _fetchDependencies() {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const pipName = btn.dataset.depPip;
|
||||
const installCmd = btn.dataset.depInstallCmd || '';
|
||||
const pkgName = btn.closest('.cookbook-dep-row')?.querySelector('.memory-item-title')?.textContent || pipName;
|
||||
await _installDep(pipName, pkgName, btn.dataset.depTarget === 'local', !!btn.dataset.upgrade, btn);
|
||||
await _installDep(pipName, pkgName, btn.dataset.depTarget === 'local', !!btn.dataset.upgrade, btn, installCmd);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -852,11 +892,12 @@ async function _fetchDependencies() {
|
||||
const it = document.createElement('div');
|
||||
it.className = 'dropdown-item-compact';
|
||||
it.innerHTML = `<span class="dropdown-icon">${upIco}</span><span>Update</span>`;
|
||||
it.title = `Update ${pkgName} to the latest version (pip install -U)`;
|
||||
it.title = row.dataset.depUpdateCmd ? `Update ${pkgName} using its custom command` : `Update ${pkgName} to the latest version (pip install -U)`;
|
||||
it.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.remove();
|
||||
await _installDep(pipName, pkgName, isLocalOnly, true, null);
|
||||
const updateCmd = row.dataset.depUpdateCmd || '';
|
||||
await _installDep(pipName, pkgName, isLocalOnly, true, null, updateCmd);
|
||||
});
|
||||
dropdown.appendChild(it);
|
||||
document.body.appendChild(dropdown);
|
||||
@@ -954,7 +995,7 @@ function _wireTabEvents(body) {
|
||||
// Ignore swipes that start in a horizontally-scrollable tag row — those
|
||||
// should scroll the chips, not flip the tab.
|
||||
if (window.innerWidth > 768 || e.touches.length !== 1
|
||||
|| e.target.closest('input, textarea, select, .doclib-lang-chips')) { _sx = null; return; }
|
||||
|| e.target.closest('input, textarea, select, .doclib-lang-chips')) { _sx = null; return; }
|
||||
_sx = e.touches[0].clientX; _sy = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
body.addEventListener('touchend', (e) => {
|
||||
@@ -1353,7 +1394,7 @@ function _wireTabEvents(body) {
|
||||
// the section is collapsed (the body's content normally provides
|
||||
// separation; with no body visible, the line gives the h2 definition).
|
||||
dlFold.classList.toggle('is-folded', !folded);
|
||||
try { localStorage.setItem('cookbook_dl_tab_folded_v1', folded ? '0' : '1'); } catch {}
|
||||
try { localStorage.setItem('cookbook_dl_tab_folded_v1', folded ? '0' : '1'); } catch { }
|
||||
});
|
||||
}
|
||||
const hfToggle = document.getElementById('cookbook-hf-latest-toggle');
|
||||
@@ -1399,7 +1440,7 @@ function _wireTabEvents(body) {
|
||||
_hwCache[cacheKey] = hw;
|
||||
return hw;
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
_hwCache[cacheKey] = { vram: 0, backend: '' };
|
||||
return _hwCache[cacheKey];
|
||||
}
|
||||
@@ -1524,7 +1565,7 @@ function _wireTabEvents(body) {
|
||||
hfInput.addEventListener('change', async () => {
|
||||
const val = hfInput.value.trim();
|
||||
_envState.hfToken = val;
|
||||
try { await _persistEnvState(); } catch {}
|
||||
try { await _persistEnvState(); } catch { }
|
||||
if (val) {
|
||||
_envState.hfTokenConfigured = true;
|
||||
const masked = val.length > 6 ? val.slice(0, 3) + '…' + val.slice(-3) : '••••';
|
||||
@@ -1724,7 +1765,7 @@ function _renderRecipes() {
|
||||
html += '<option value="general" selected>Standard</option><option value="coding">Coding</option>';
|
||||
html += '<option value="reasoning">Reasoning</option><option value="chat">Chat</option>';
|
||||
// Image tab removed — text→image gen is gone from this build (only inpaint
|
||||
// remains, which uses its own settings panel). Vision (multimodal) stays.
|
||||
// remains, which uses its own settings panel). Vision (multimodal) stays.
|
||||
html += '<option value="multimodal">Vision</option></select>';
|
||||
// Engine sits next to the type filter so the "what category / which serving
|
||||
// path" filters live together; Quant + Context are storage-format and budget
|
||||
@@ -1790,12 +1831,12 @@ function _renderRecipes() {
|
||||
// to the curated model list. Sits below the list so it reads as a callout
|
||||
// after browsing, not a header.
|
||||
html += '<div class="hwfit-list-footer" style="margin-top:8px;padding-top:6px;border-top:1px solid color-mix(in srgb, var(--border) 50%, transparent);font-size:9.5px;opacity:0.65;text-align:right;">'
|
||||
+ 'Don\'t see a model? '
|
||||
+ '<a href="https://github.com/pewdiepie-archdaemon/odysseus/discussions/1962" target="_blank" rel="noopener" style="color:var(--accent,var(--red));text-decoration:none;display:inline-flex;align-items:center;gap:4px;vertical-align:middle;">'
|
||||
+ 'Request it →'
|
||||
+ '<svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style="flex-shrink:0;"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>'
|
||||
+ '</a>'
|
||||
+ '</div>';
|
||||
+ 'Don\'t see a model? '
|
||||
+ '<a href="https://github.com/pewdiepie-archdaemon/odysseus/discussions/1962" target="_blank" rel="noopener" style="color:var(--accent,var(--red));text-decoration:none;display:inline-flex;align-items:center;gap:4px;vertical-align:middle;">'
|
||||
+ 'Request it →'
|
||||
+ '<svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style="flex-shrink:0;"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>'
|
||||
+ '</a>'
|
||||
+ '</div>';
|
||||
|
||||
html += '</div></div>';
|
||||
|
||||
@@ -1883,7 +1924,7 @@ function _renderRecipes() {
|
||||
html += '<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:2px;margin-top:-4px;">';
|
||||
html += '<h2 style="margin:0;padding:0;line-height:1;">Servers</h2>';
|
||||
// Reuse the calendar +New pill: spinning plus, label fades in idea uses
|
||||
// the same `.cal-add-btn-text` rules, so styling stays consistent.
|
||||
// the same `.cal-add-btn-text` rules, so styling stays consistent.
|
||||
html += '<button class="cal-add-btn cal-add-btn-text" id="cookbook-server-add" title="Add server" style="margin-left:auto;"><span class="cal-add-plus">+</span><span class="cal-add-label">Add</span></button>';
|
||||
html += '</div>';
|
||||
html += '<p class="memory-desc doclib-desc">Configure SSH servers, install Odysseus keys, choose model directories, and set the default server. Local is this machine.</p>';
|
||||
@@ -1979,73 +2020,73 @@ export async function open(opts) {
|
||||
}
|
||||
_setCookbookOpening(true);
|
||||
try {
|
||||
// Invalidate any pending close() animation handlers so they won't re-hide us
|
||||
_closeGen++;
|
||||
// Clear any leftover inline styles from a previous swipe-dismiss or close animation
|
||||
const _content = modal.querySelector('.modal-content');
|
||||
if (_content) {
|
||||
_content.classList.remove('modal-closing', 'sheet-ready', 'cookbook-modal-entering');
|
||||
_content.style.transform = '';
|
||||
_content.style.transition = '';
|
||||
_content.style.animation = '';
|
||||
_content.style.opacity = '';
|
||||
}
|
||||
modal.style.display = '';
|
||||
Modals.register('cookbook-modal', {
|
||||
railBtnId: 'rail-cookbook',
|
||||
sidebarBtnId: 'tool-cookbook-btn',
|
||||
closeFn: () => _doClose(),
|
||||
restoreFn: () => { _renderRunningTab(); },
|
||||
});
|
||||
_wireCookbookDrag(modal);
|
||||
await _syncFromServer();
|
||||
// `_syncFromServer` lives in cookbookRunning.js and populates *its* _envState
|
||||
// (a different object reference than this module's), then mirrors the merged
|
||||
// state to localStorage. So ALWAYS hydrate our _envState from that mirror —
|
||||
// on a successful sync it holds the freshly-fetched servers; on failure it
|
||||
// holds the last-known state. Gating this on `!synced` left the render's
|
||||
// _envState empty whenever sync succeeded → "servers don't show".
|
||||
try { Object.assign(_envState, _readStoredEnvState()); } catch {}
|
||||
// Honour a user-set default server: always land on it when Cookbook opens, so
|
||||
// every dropdown (scan/download/serve/cache/deps) starts on the same machine.
|
||||
if (_envState.defaultServer) {
|
||||
const _dk = _envState.defaultServer;
|
||||
if (_dk === 'local') {
|
||||
_envState.remoteHost = ''; _envState.env = 'none'; _envState.envPath = ''; _envState.platform = '';
|
||||
} else {
|
||||
const _ds = (_envState.servers || []).find(s => s.host === _dk);
|
||||
if (_ds) { _envState.remoteHost = _ds.host; _envState.env = _ds.env || 'none'; _envState.envPath = _ds.envPath || ''; _envState.platform = _ds.platform || ''; }
|
||||
// Invalidate any pending close() animation handlers so they won't re-hide us
|
||||
_closeGen++;
|
||||
// Clear any leftover inline styles from a previous swipe-dismiss or close animation
|
||||
const _content = modal.querySelector('.modal-content');
|
||||
if (_content) {
|
||||
_content.classList.remove('modal-closing', 'sheet-ready', 'cookbook-modal-entering');
|
||||
_content.style.transform = '';
|
||||
_content.style.transition = '';
|
||||
_content.style.animation = '';
|
||||
_content.style.opacity = '';
|
||||
}
|
||||
}
|
||||
// Re-render on every open AFTER sync so the freshly-fetched state (servers,
|
||||
// HF token, presets) is always reflected. Gating this to once-per-page used
|
||||
// to freeze a stale/empty servers list whenever the first sync raced or
|
||||
// returned before hydration — and since close/reopen doesn't reset the page,
|
||||
// only a full reload recovered it. Re-rendering is cheap and the in-progress
|
||||
// Running tab is rendered separately just below.
|
||||
_renderRecipes();
|
||||
_rendered = true;
|
||||
_clearCookbookNotif();
|
||||
_renderRunningTab();
|
||||
// Self-heal: revive any download tasks whose tmux session is still alive
|
||||
// but were persisted as done/error (covers the "restarted server while a
|
||||
// big multi-shard download was in flight" case — the task survived in
|
||||
// tmux, the cookbook just lost track of it).
|
||||
try { _selfHealStaleTasks({ oneShot: true }); } catch {}
|
||||
if (_content) {
|
||||
// Put the panel in its entering state before it becomes visible. On
|
||||
// mobile, showing first and adding the class a frame later can paint the
|
||||
// sheet at its final position, which makes the slide-up look like a snap.
|
||||
_content.classList.add('cookbook-modal-entering');
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
if (_content) {
|
||||
void _content.offsetWidth;
|
||||
_content.addEventListener('animationend', () => {
|
||||
_content.classList.remove('cookbook-modal-entering');
|
||||
}, { once: true });
|
||||
}
|
||||
setTimeout(_applyIntent, 0);
|
||||
modal.style.display = '';
|
||||
Modals.register('cookbook-modal', {
|
||||
railBtnId: 'rail-cookbook',
|
||||
sidebarBtnId: 'tool-cookbook-btn',
|
||||
closeFn: () => _doClose(),
|
||||
restoreFn: () => { _renderRunningTab(); },
|
||||
});
|
||||
_wireCookbookDrag(modal);
|
||||
await _syncFromServer();
|
||||
// `_syncFromServer` lives in cookbookRunning.js and populates *its* _envState
|
||||
// (a different object reference than this module's), then mirrors the merged
|
||||
// state to localStorage. So ALWAYS hydrate our _envState from that mirror —
|
||||
// on a successful sync it holds the freshly-fetched servers; on failure it
|
||||
// holds the last-known state. Gating this on `!synced` left the render's
|
||||
// _envState empty whenever sync succeeded → "servers don't show".
|
||||
try { Object.assign(_envState, _readStoredEnvState()); } catch { }
|
||||
// Honour a user-set default server: always land on it when Cookbook opens, so
|
||||
// every dropdown (scan/download/serve/cache/deps) starts on the same machine.
|
||||
if (_envState.defaultServer) {
|
||||
const _dk = _envState.defaultServer;
|
||||
if (_dk === 'local') {
|
||||
_envState.remoteHost = ''; _envState.env = 'none'; _envState.envPath = ''; _envState.platform = '';
|
||||
} else {
|
||||
const _ds = (_envState.servers || []).find(s => s.host === _dk);
|
||||
if (_ds) { _envState.remoteHost = _ds.host; _envState.env = _ds.env || 'none'; _envState.envPath = _ds.envPath || ''; _envState.platform = _ds.platform || ''; }
|
||||
}
|
||||
}
|
||||
// Re-render on every open AFTER sync so the freshly-fetched state (servers,
|
||||
// HF token, presets) is always reflected. Gating this to once-per-page used
|
||||
// to freeze a stale/empty servers list whenever the first sync raced or
|
||||
// returned before hydration — and since close/reopen doesn't reset the page,
|
||||
// only a full reload recovered it. Re-rendering is cheap and the in-progress
|
||||
// Running tab is rendered separately just below.
|
||||
_renderRecipes();
|
||||
_rendered = true;
|
||||
_clearCookbookNotif();
|
||||
_renderRunningTab();
|
||||
// Self-heal: revive any download tasks whose tmux session is still alive
|
||||
// but were persisted as done/error (covers the "restarted server while a
|
||||
// big multi-shard download was in flight" case — the task survived in
|
||||
// tmux, the cookbook just lost track of it).
|
||||
try { _selfHealStaleTasks({ oneShot: true }); } catch { }
|
||||
if (_content) {
|
||||
// Put the panel in its entering state before it becomes visible. On
|
||||
// mobile, showing first and adding the class a frame later can paint the
|
||||
// sheet at its final position, which makes the slide-up look like a snap.
|
||||
_content.classList.add('cookbook-modal-entering');
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
if (_content) {
|
||||
void _content.offsetWidth;
|
||||
_content.addEventListener('animationend', () => {
|
||||
_content.classList.remove('cookbook-modal-entering');
|
||||
}, { once: true });
|
||||
}
|
||||
setTimeout(_applyIntent, 0);
|
||||
} finally {
|
||||
_setCookbookOpening(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user