HERMES-GIELIE/static/onboarding.js

412 lines
21 KiB
JavaScript
Raw Normal View History

const ONBOARDING={status:null,step:0,steps:['system','setup','workspace','password','finish'],form:{provider:'openrouter',workspace:'',model:'',password:'',apiKey:'',baseUrl:''},active:false};
function _getOnboardingSetupProviders(){
return (((ONBOARDING.status||{}).setup||{}).providers)||[];
}
function _getOnboardingSetupProvider(id){
return _getOnboardingSetupProviders().find(p=>p.id===id)||null;
}
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
function _getOnboardingSetupCategories(){
return (((ONBOARDING.status||{}).setup||{}).categories)||[];
}
/** Render the provider <select> with <optgroup> per category. */
function _renderProviderSelectOptions(selectedId){
const providers=_getOnboardingSetupProviders();
const categories=_getOnboardingSetupCategories();
const provMap={};
providers.forEach(p=>{provMap[p.id]=p;});
if(!categories.length){
// Fallback: flat list when no categories are available.
return providers.map(p=>`<option value="${esc(p.id)}">${esc(p.label)}${p.quick?' — '+esc(t('onboarding_quick_setup_badge')):''}</option>`).join('');
}
return categories.map(cat=>{
const opts=cat.providers.map(pid=>{
const p=provMap[pid];
if(!p)return '';
return `<option value="${esc(p.id)}"${p.id===selectedId?' selected':''}>${esc(p.label)}${p.quick?' — '+esc(t('onboarding_quick_setup_badge')):''}</option>`;
}).join('');
return `<optgroup label="${esc(t('provider_category_'+cat.id)||cat.label)}">${opts}</optgroup>`;
}).join('');
}
function _getOnboardingCurrentSetup(){
return (((ONBOARDING.status||{}).setup||{}).current)||{};
}
function _onboardingStepMeta(key){
return ({
system:{title:t('onboarding_step_system_title'),desc:t('onboarding_step_system_desc')},
setup:{title:t('onboarding_step_setup_title'),desc:t('onboarding_step_setup_desc')},
workspace:{title:t('onboarding_step_workspace_title'),desc:t('onboarding_step_workspace_desc')},
password:{title:t('onboarding_step_password_title'),desc:t('onboarding_step_password_desc')},
finish:{title:t('onboarding_step_finish_title'),desc:t('onboarding_step_finish_desc')}
})[key];
}
function _renderOnboardingSteps(){
const wrap=$('onboardingSteps');
if(!wrap)return;
wrap.innerHTML='';
ONBOARDING.steps.forEach((key,idx)=>{
const meta=_onboardingStepMeta(key);
const item=document.createElement('div');
item.className='onboarding-step'+(idx===ONBOARDING.step?' active':idx<ONBOARDING.step?' done':'');
item.innerHTML=`<div class="onboarding-step-index">${idx+1}</div><div><div class="onboarding-step-title">${meta.title}</div><div class="onboarding-step-desc">${meta.desc}</div></div>`;
wrap.appendChild(item);
});
}
function _setOnboardingNotice(msg,kind='info'){
const el=$('onboardingNotice');
if(!el)return;
if(!msg){el.style.display='none';el.textContent='';el.className='onboarding-status';return;}
el.style.display='block';
el.className='onboarding-status '+kind;
el.textContent=msg;
}
function _getOnboardingWorkspaceChoices(){
const items=((ONBOARDING.status||{}).workspaces||{}).items||[];
return items.length?items:[{name:'Home',path:ONBOARDING.form.workspace||''}];
}
function _getOnboardingProviderModelChoices(){
const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
return provider?(provider.models||[]):[];
}
function _getOnboardingSelectedModel(){
return ONBOARDING.form.model||'';
}
function _renderOnboardingModelField(){
const choices=_getOnboardingProviderModelChoices();
if(ONBOARDING.form.provider==='custom'){
return `<label class="onboarding-field"><span>${t('onboarding_model_label')}</span><input id="onboardingModelInput" value="${esc(_getOnboardingSelectedModel())}" placeholder="${t('onboarding_custom_model_placeholder')}" oninput="ONBOARDING.form.model=this.value"></label><p class="onboarding-copy">${t('onboarding_custom_model_help')}</p>`;
}
const options=choices.map(m=>`<option value="${esc(m.id)}">${esc(m.label)}</option>`).join('');
return `<label class="onboarding-field"><span>${t('onboarding_model_label')}</span><select id="onboardingModelSelect" onchange="ONBOARDING.form.model=this.value">${options}</select></label><p class="onboarding-copy">${t('onboarding_workspace_help')}</p>`;
}
function _providerStatusLabel(system){
if(system.chat_ready) return t('onboarding_check_provider_ready');
if(system.provider_configured) return t('onboarding_check_provider_partial');
return t('onboarding_check_provider_pending');
}
function _renderOnboardingBody(){
const body=$('onboardingBody');
if(!body||!ONBOARDING.status)return;
const key=ONBOARDING.steps[ONBOARDING.step];
const system=ONBOARDING.status.system||{};
const settings=ONBOARDING.status.settings||{};
const setup=ONBOARDING.status.setup||{};
const nextBtn=$('onboardingNextBtn');
const backBtn=$('onboardingBackBtn');
if(backBtn) backBtn.style.display=ONBOARDING.step>0?'':'none';
if(nextBtn) nextBtn.textContent=key==='finish'?t('onboarding_open'):t('onboarding_continue');
if(key==='system'){
const hermesOk=system.hermes_found&&system.imports_ok;
const setupOk=!!system.chat_ready;
_setOnboardingNotice(system.provider_note|| (setupOk?t('onboarding_notice_system_ready'):t('onboarding_notice_system_unavailable')),setupOk?'success':(hermesOk?'info':'warn'));
body.innerHTML=`
<div class="onboarding-panel-grid">
<div class="onboarding-check ${hermesOk?'ok':'warn'}"><strong>${t('onboarding_check_agent')}</strong><span>${hermesOk?t('onboarding_check_agent_ready'):t('onboarding_check_agent_missing')}</span></div>
<div class="onboarding-check ${(setupOk?'ok':system.provider_configured?'warn':'muted')}"><strong>${t('onboarding_check_provider')}</strong><span>${_providerStatusLabel(system)}</span></div>
<div class="onboarding-check ${(settings.password_enabled?'ok':'muted')}"><strong>${t('onboarding_check_password')}</strong><span>${settings.password_enabled?t('onboarding_check_password_enabled'):t('onboarding_check_password_disabled')}</span></div>
</div>
<div class="onboarding-copy">
<p><strong>${t('onboarding_config_file')}</strong> ${esc(system.config_path||t('onboarding_unknown'))}</p>
<p><strong>${t('onboarding_env_file')}</strong> ${esc(system.env_path||t('onboarding_unknown'))}</p>
<p>${esc(system.provider_note||'')}</p>
${system.current_provider?`<p><strong>${t('onboarding_current_provider')}</strong> ${esc(system.current_provider)}${system.current_model?` — ${esc(system.current_model)}`:''}</p>`:''}
${system.current_base_url?`<p><strong>${t('onboarding_base_url_label')}</strong> ${esc(system.current_base_url)}</p>`:''}
${system.missing_modules&&system.missing_modules.length?`<p><strong>${t('onboarding_missing_imports')}</strong> ${esc(system.missing_modules.join(', '))}</p>`:''}
</div>`;
return;
}
if(key==='setup'){
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
const selectedId=ONBOARDING.form.provider;
const groupedOptions=_renderProviderSelectOptions(selectedId);
const provider=_getOnboardingSetupProvider(selectedId)||_getOnboardingSetupProviders()[0]||null;
const showBaseUrl=provider&&provider.requires_base_url;
const keyHelp=provider?`${t('onboarding_api_key_help_prefix')} ${esc(provider.env_var)}.`:'';
// OAuth provider path: configured via CLI, no API key input needed.
const currentIsOauth=!!(ONBOARDING.status.setup||{}).current_is_oauth;
const currentProviderName=((ONBOARDING.status.setup||{}).current||{}).provider||'';
if(currentIsOauth){
const isReady=!!(ONBOARDING.status.system||{}).chat_ready;
const providerLabel=esc(currentProviderName);
if(isReady){
_setOnboardingNotice(t('onboarding_notice_setup_already_ready'),'success');
body.innerHTML=`
<div class="onboarding-oauth-card onboarding-oauth-ready">
<div class="onboarding-oauth-icon">✓</div>
<div>
<strong>${t('onboarding_oauth_provider_ready_title')}</strong>
<p>${t('onboarding_oauth_provider_ready_body').replace('{provider}',providerLabel)}</p>
</div>
</div>
<p class="onboarding-copy" style="margin-top:20px">${t('onboarding_oauth_switch_hint')}</p>
<label class="onboarding-field">
<span>${t('onboarding_provider_label')}</span>
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
<select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${groupedOptions}</select>
</label>
<label class="onboarding-field" id="onboardingApiKeyField">
<span>${t('onboarding_api_key_label')}</span>
<input id="onboardingApiKeyInput" type="password" value="${esc(ONBOARDING.form.apiKey||'')}" placeholder="${t('onboarding_api_key_placeholder')}" oninput="ONBOARDING.form.apiKey=this.value">
</label>
${showBaseUrl?`<label class="onboarding-field"><span>${t('onboarding_base_url_label')}</span><input id="onboardingBaseUrlInput" value="${esc(ONBOARDING.form.baseUrl||'')}" placeholder="${t('onboarding_base_url_placeholder')}" oninput="ONBOARDING.form.baseUrl=this.value"></label>`:''}
<p class="onboarding-copy">${keyHelp}</p>`;
} else {
_setOnboardingNotice(t('onboarding_notice_setup_required'),'warn');
body.innerHTML=`
<div class="onboarding-oauth-card onboarding-oauth-pending">
<div class="onboarding-oauth-icon">⚠</div>
<div>
<strong>${t('onboarding_oauth_provider_not_ready_title')}</strong>
<p>${t('onboarding_oauth_provider_not_ready_body').replace('{provider}',providerLabel)}</p>
</div>
</div>
<p class="onboarding-copy" style="margin-top:20px">${t('onboarding_oauth_switch_hint')}</p>
<label class="onboarding-field">
<span>${t('onboarding_provider_label')}</span>
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
<select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${groupedOptions}</select>
</label>
<label class="onboarding-field" id="onboardingApiKeyField">
<span>${t('onboarding_api_key_label')}</span>
<input id="onboardingApiKeyInput" type="password" value="${esc(ONBOARDING.form.apiKey||'')}" placeholder="${t('onboarding_api_key_placeholder')}" oninput="ONBOARDING.form.apiKey=this.value">
</label>
${showBaseUrl?`<label class="onboarding-field"><span>${t('onboarding_base_url_label')}</span><input id="onboardingBaseUrlInput" value="${esc(ONBOARDING.form.baseUrl||'')}" placeholder="${t('onboarding_base_url_placeholder')}" oninput="ONBOARDING.form.baseUrl=this.value"></label>`:''}
<p class="onboarding-copy">${keyHelp}</p>`;
}
return;
}
_setOnboardingNotice(system.chat_ready?t('onboarding_notice_setup_already_ready'):t('onboarding_notice_setup_required'),system.chat_ready?'success':'info');
body.innerHTML=`
<label class="onboarding-field">
<span>${t('onboarding_provider_label')}</span>
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
<select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${groupedOptions}</select>
</label>
<label class="onboarding-field">
<span>${t('onboarding_api_key_label')}</span>
<input id="onboardingApiKeyInput" type="password" value="${esc(ONBOARDING.form.apiKey||'')}" placeholder="${t('onboarding_api_key_placeholder')}" oninput="ONBOARDING.form.apiKey=this.value">
</label>
${showBaseUrl?`<label class="onboarding-field"><span>${t('onboarding_base_url_label')}</span><input id="onboardingBaseUrlInput" value="${esc(ONBOARDING.form.baseUrl||'')}" placeholder="${t('onboarding_base_url_placeholder')}" oninput="ONBOARDING.form.baseUrl=this.value"></label>`:''}
<p class="onboarding-copy">${keyHelp}</p>
${showBaseUrl?`<p class="onboarding-copy">${t('onboarding_base_url_help')}</p>`:''}
<p class="onboarding-copy">${esc(setup.unsupported_note||'')||''}</p>`;
return;
}
if(key==='workspace'){
const workspaceOptions=_getOnboardingWorkspaceChoices().map(ws=>`<option value="${esc(ws.path)}">${esc(ws.name||ws.path)} — ${esc(ws.path)}</option>`).join('');
_setOnboardingNotice(t('onboarding_notice_workspace'), 'info');
body.innerHTML=`
<label class="onboarding-field">
<span>${t('onboarding_workspace_label')}</span>
<select id="onboardingWorkspaceSelect" onchange="syncOnboardingWorkspaceSelect(this.value)">${workspaceOptions}</select>
</label>
<label class="onboarding-field">
<span>${t('onboarding_workspace_or_path')}</span>
<input id="onboardingWorkspaceInput" value="${esc(ONBOARDING.form.workspace||'')}" placeholder="${t('onboarding_workspace_placeholder')}" oninput="ONBOARDING.form.workspace=this.value">
</label>
${_renderOnboardingModelField()}`;
const wsSel=$('onboardingWorkspaceSelect');
if(wsSel && ONBOARDING.form.workspace) wsSel.value=ONBOARDING.form.workspace;
const modelSel=$('onboardingModelSelect');
if(modelSel && ONBOARDING.form.model) modelSel.value=ONBOARDING.form.model;
return;
}
if(key==='password'){
_setOnboardingNotice(settings.password_enabled?t('onboarding_notice_password_enabled'):t('onboarding_notice_password_recommended'), settings.password_enabled?'success':'info');
body.innerHTML=`
<label class="onboarding-field">
<span>${t('onboarding_password_label')}</span>
<input id="onboardingPasswordInput" type="password" value="${esc(ONBOARDING.form.password||'')}" placeholder="${t('onboarding_password_placeholder')}" oninput="ONBOARDING.form.password=this.value">
</label>
<p class="onboarding-copy">${t('onboarding_password_help')}</p>`;
return;
}
const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
_setOnboardingNotice(t('onboarding_notice_finish'), 'success');
body.innerHTML=`
<div class="onboarding-summary">
<div><strong>${t('onboarding_provider_label')}</strong><span>${esc((provider&&provider.label)||ONBOARDING.form.provider||t('onboarding_not_set'))}</span></div>
<div><strong>${t('onboarding_model_label')}</strong><span>${esc(_getOnboardingSelectedModel()||t('onboarding_not_set'))}</span></div>
<div><strong>${t('onboarding_workspace_label')}</strong><span>${esc(ONBOARDING.form.workspace||t('onboarding_not_set'))}</span></div>
<div><strong>${t('onboarding_check_password')}</strong><span>${t(_getOnboardingPasswordSummaryKey(settings))}</span></div>
</div>
${ONBOARDING.form.baseUrl?`<p class="onboarding-copy"><strong>${t('onboarding_base_url_label')}</strong> ${esc(ONBOARDING.form.baseUrl)}</p>`:''}
<p class="onboarding-copy">${t('onboarding_finish_help')}</p>`;
}
function _getOnboardingPasswordSummaryKey(settings){
const hasExistingPassword=!!(settings&&settings.password_enabled);
const hasNewPassword=!!((ONBOARDING.form.password||'').trim());
if(hasNewPassword) return hasExistingPassword?'onboarding_password_will_replace':'onboarding_password_will_enable';
return hasExistingPassword?'onboarding_password_keep_existing':'onboarding_password_remains_disabled';
}
function syncOnboardingWorkspaceSelect(value){
ONBOARDING.form.workspace=value;
const input=$('onboardingWorkspaceInput');
if(input) input.value=value;
}
function syncOnboardingProvider(value){
const provider=_getOnboardingSetupProvider(value);
ONBOARDING.form.provider=value;
if(provider){
if(!ONBOARDING.form.model || !_getOnboardingProviderModelChoices().some(m=>m.id===ONBOARDING.form.model) || value==='custom'){
ONBOARDING.form.model=provider.default_model||'';
}
if(provider.requires_base_url){
ONBOARDING.form.baseUrl=ONBOARDING.form.baseUrl||provider.default_base_url||'';
}else{
ONBOARDING.form.baseUrl=provider.default_base_url||'';
}
}
_renderOnboardingBody();
}
async function loadOnboardingWizard(){
try{
const status=await api('/api/onboarding/status');
ONBOARDING.status=status;
const current=((status.setup||{}).current)||{};
ONBOARDING.form.provider=current.provider||'openrouter';
ONBOARDING.form.workspace=(status.workspaces&&status.workspaces.last)||status.settings.default_workspace||'';
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
ONBOARDING.form.model=status.settings.default_model||current.model||'';
ONBOARDING.form.password='';
ONBOARDING.form.apiKey='';
ONBOARDING.form.baseUrl=current.base_url||'';
ONBOARDING.active=!status.completed;
if(!ONBOARDING.active) return false;
$('onboardingOverlay').style.display='flex';
_renderOnboardingSteps();
_renderOnboardingBody();
return true;
}catch(e){
console.warn('onboarding status failed',e);
return false;
}
}
function prevOnboardingStep(){
if(ONBOARDING.step===0)return;
ONBOARDING.step--;
_renderOnboardingSteps();
_renderOnboardingBody();
}
async function _saveOnboardingProviderSetup(){
const provider=(ONBOARDING.form.provider||'').trim();
const model=(ONBOARDING.form.model||'').trim();
const apiKey=(ONBOARDING.form.apiKey||'').trim();
const baseUrl=(ONBOARDING.form.baseUrl||'').trim();
const current=_getOnboardingCurrentSetup();
const isUnchanged=current.provider===provider&&((current.model||'')===model)&&((current.base_url||'')===baseUrl);
// Skip the POST when nothing changed. We also skip when the provider is
// unsupported/OAuth-based and already working — chat_ready may be false for
// providers not in the quick-setup list (e.g. minimax-cn) even though they are
// fully configured. Posting in that case would either be a no-op (the server
// just marks complete for unsupported providers) or could silently overwrite
// config.yaml if the user accidentally changed the provider dropdown.
const currentIsOauth=!!(ONBOARDING.status&&ONBOARDING.status.setup&&ONBOARDING.status.setup.current_is_oauth);
if(isUnchanged && !apiKey && ((ONBOARDING.status.system||{}).chat_ready || currentIsOauth)) return;
const body={provider,model};
if(apiKey) body.api_key=apiKey;
if(baseUrl) body.base_url=baseUrl;
const status=await api('/api/onboarding/setup',{method:'POST',body:JSON.stringify(body)});
ONBOARDING.status=status;
}
async function _saveOnboardingDefaults(){
const workspace=(ONBOARDING.form.workspace||'').trim();
const model=(ONBOARDING.form.model||'').trim();
const password=(ONBOARDING.form.password||'').trim();
if(!workspace) throw new Error(t('onboarding_error_choose_workspace'));
if(!model) throw new Error(t('onboarding_error_choose_model'));
const known=_getOnboardingWorkspaceChoices().some(ws=>ws.path===workspace);
if(!known){
await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path:workspace})});
}
// Model persisted by /api/onboarding/setup — no /api/default-model call needed here
const body={default_workspace:workspace};
if(password) body._set_password=password;
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
if(ONBOARDING.status){
ONBOARDING.status.settings={...(ONBOARDING.status.settings||{}),password_enabled:!!saved.auth_enabled};
}
localStorage.setItem('hermes-webui-model',model);
if($('modelSelect')) _applyModelToDropdown(model,$('modelSelect'));
}
async function _finishOnboarding(){
await _saveOnboardingProviderSetup();
await _saveOnboardingDefaults();
const done=await api('/api/onboarding/complete',{method:'POST',body:'{}'});
ONBOARDING.status=done;
ONBOARDING.active=false;
$('onboardingOverlay').style.display='none';
showToast(t('onboarding_complete'));
await loadWorkspaceList();
if(typeof renderSessionList==='function') await renderSessionList();
if(!S.session && typeof newSession==='function'){
await newSession(true);
await renderSessionList();
}
}
async function skipOnboarding(){
try{
// Mark onboarding completed server-side without changing any config
await api('/api/onboarding/complete',{method:'POST',body:'{}'});
ONBOARDING.active=false;
$('onboardingOverlay').style.display='none';
showToast(t('onboarding_skipped')||'Setup skipped');
}catch(e){
_setOnboardingNotice((e.message||String(e)),'warn');
}
}
async function nextOnboardingStep(){
try{
if(ONBOARDING.steps[ONBOARDING.step]==='setup'){
ONBOARDING.form.provider=(($('onboardingProviderSelect')||{}).value||ONBOARDING.form.provider||'').trim();
ONBOARDING.form.apiKey=(($('onboardingApiKeyInput')||{}).value||'').trim();
ONBOARDING.form.baseUrl=(($('onboardingBaseUrlInput')||{}).value||ONBOARDING.form.baseUrl||'').trim();
if(!ONBOARDING.form.provider) throw new Error(t('onboarding_error_provider_required'));
if(ONBOARDING.form.provider==='custom' && !ONBOARDING.form.baseUrl) throw new Error(t('onboarding_error_base_url_required'));
}
if(ONBOARDING.steps[ONBOARDING.step]==='workspace'){
ONBOARDING.form.workspace=(($('onboardingWorkspaceInput')||{}).value||ONBOARDING.form.workspace||'').trim();
ONBOARDING.form.model=(($('onboardingModelInput')||{}).value||($('onboardingModelSelect')||{}).value||ONBOARDING.form.model||'').trim();
if(!ONBOARDING.form.workspace) throw new Error(t('onboarding_error_workspace_required'));
if(!ONBOARDING.form.model) throw new Error(t('onboarding_error_model_required'));
}
if(ONBOARDING.steps[ONBOARDING.step]==='password'){
ONBOARDING.form.password=(($('onboardingPasswordInput')||{}).value||'').trim();
}
if(ONBOARDING.step===ONBOARDING.steps.length-1){
await _finishOnboarding();
return;
}
ONBOARDING.step++;
_renderOnboardingSteps();
_renderOnboardingBody();
}catch(e){
_setOnboardingNotice(e.message||String(e),'warn');
}
}