mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-02 13:02:20 +02:00
perf(frontend): share one cached fetch for /api/auth/settings and /api/tools (#5997)
* perf(frontend): share one cached fetch for settings and tools /api/auth/settings was fetched independently by eight modules and /api/tools by three on a single load — 4 and 3 requests measured — and any two of those callers could observe a different snapshot of the same object. chatRenderer.js is imported under three different ?v= query strings, so it is three separate module instances each issuing its own /api/tools request. appConfig.js holds one promise per endpoint, so concurrent and later callers share it. Every writer invalidates: the settings panel routes its 16 saves through a single helper, and the admin tools save drops both snapshots because that route persists disabled_tools into the same settings store. A rejected fetch clears its slot rather than being memoised, so one blip at boot cannot leave keybinds, TTS and the search provider on defaults for the session. The settings panel keeps reading directly: it is the writer and edits what it reads, so it must see authoritative state. Cold load, Resource Timing: /api/auth/settings 4 -> 1, /api/tools 3 -> 1, and 0 settings requests on the first load after a login, because the cache now consumes the sessionStorage prefetch that login.html writes. Fixes #5996 * fix(admin): refetch tool state when the Agent Tools panel opens The shared cache made Admin > Tools render the boot snapshot on every reopen. Its save posts the whole disabled list rebuilt from the checkboxes, so a tool disabled out of band (the manage_settings tool, another tab) came back enabled on the next unrelated toggle. Reproduced against the running app: with api_call disabled by a separate client, toggling app_api off posted ['app_api'] and silently re-enabled api_call. The panel now drops the shared entry before reading it, which restores what dev does today and keeps the startup read that chatRenderer.js shares. Cold load is still 1 request each for /api/auth/settings and /api/tools, and the panel costs the same 2 requests per open as dev. * fix(static): preserve concurrent tool setting changes --------- Co-authored-by: Alexandre Teixeira <alexandremagteixeira@gmail.com>
This commit is contained in:
co-authored by
Alexandre Teixeira
parent
895bf896e3
commit
04b8829fb2
+46
-66
@@ -26,11 +26,37 @@ import { sortModelIds } from './modelSort.js';
|
||||
import { providerLogo } from './providers.js';
|
||||
import { isAltGrEvent } from './platform.js';
|
||||
import { bindMenuDismiss } from './escMenuStack.js';
|
||||
import { invalidateSettings } from './appConfig.js';
|
||||
|
||||
let initialized = false;
|
||||
let modalEl = null;
|
||||
let _authPolicy = { password_min_length: 8 };
|
||||
|
||||
/**
|
||||
* POST a settings patch, then drop the shared snapshot in appConfig.js.
|
||||
*
|
||||
* Every write in this file goes through here so no save path can forget the
|
||||
* invalidation — a stale settings object served for the rest of the session is
|
||||
* a worse bug than the duplicate fetches the cache removes. The invalidation is
|
||||
* in a `finally` because a request that throws on the way back may still have
|
||||
* been applied server-side.
|
||||
*
|
||||
* Reads in this file deliberately stay direct fetches: this panel is the writer
|
||||
* and edits what it reads, so it must see the authoritative state, not a cache.
|
||||
*/
|
||||
async function _postSettings(body) {
|
||||
try {
|
||||
return await fetch('/api/auth/settings', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} finally {
|
||||
invalidateSettings();
|
||||
}
|
||||
}
|
||||
|
||||
const el = byId;
|
||||
function esc(s) { return uiModule.esc(s); }
|
||||
function safeRasterDataUrl(raw) {
|
||||
@@ -276,10 +302,7 @@ function _bindFallbackWidget(opts) {
|
||||
var body = {};
|
||||
body[settingKey] = clean;
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
await _postSettings(body);
|
||||
} catch (e) { console.warn('[fallback] save failed for ' + settingKey, e); }
|
||||
}
|
||||
|
||||
@@ -389,12 +412,9 @@ async function initDefaultChat() {
|
||||
|
||||
async function saveDefault() {
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
default_endpoint_id: epSel.value,
|
||||
default_model: modelSel.value
|
||||
})
|
||||
await _postSettings({
|
||||
default_endpoint_id: epSel.value,
|
||||
default_model: modelSel.value
|
||||
});
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)';
|
||||
setTimeout(function() { msg.textContent = ''; }, 2000);
|
||||
@@ -449,12 +469,9 @@ async function initUtilityModel() {
|
||||
// no toggle, "—" means "unset, use chat").
|
||||
async function saveUtility() {
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
utility_endpoint_id: epSel.value || '',
|
||||
utility_model: modelSel.value || ''
|
||||
})
|
||||
await _postSettings({
|
||||
utility_endpoint_id: epSel.value || '',
|
||||
utility_model: modelSel.value || ''
|
||||
});
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)';
|
||||
setTimeout(function() { msg.textContent = ''; }, 1500);
|
||||
@@ -547,10 +564,7 @@ async function initTeacherModel() {
|
||||
spec = ep ? (modelSel.value + '@' + ep.name) : modelSel.value;
|
||||
}
|
||||
var enabled = enabledToggle ? !!enabledToggle.checked : false;
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ teacher_enabled: enabled, teacher_model: spec })
|
||||
});
|
||||
await _postSettings({ teacher_enabled: enabled, teacher_model: spec });
|
||||
msg.textContent = enabled ? (spec ? 'Saved' : 'Pick an endpoint + model') : 'Disabled';
|
||||
msg.style.color = enabled && !spec ? 'var(--red)' : 'var(--fg)';
|
||||
setTimeout(function() { msg.textContent = ''; }, 2000);
|
||||
@@ -625,8 +639,7 @@ async function initImageSettings() {
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image_gen_enabled: enabledToggle ? enabledToggle.checked : false, image_model: modelSel.value, image_quality: qualSel.value }) });
|
||||
const res = await _postSettings({ image_gen_enabled: enabledToggle ? enabledToggle.checked : false, image_model: modelSel.value, image_quality: qualSel.value });
|
||||
if (!res.ok) throw new Error(await res.text().catch(() => `HTTP ${res.status}`));
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)'; setTimeout(() => { msg.textContent = ''; }, 2000);
|
||||
} catch (e) { msg.textContent = 'Failed to save'; msg.style.color = 'var(--red)'; }
|
||||
@@ -700,8 +713,7 @@ async function initVisionSettings() {
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ vision_enabled: enabledToggle ? enabledToggle.checked : true, vision_model: vlSel.value }) });
|
||||
await _postSettings({ vision_enabled: enabledToggle ? enabledToggle.checked : true, vision_model: vlSel.value });
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)'; setTimeout(() => { msg.textContent = ''; }, 2000);
|
||||
} catch (e) { msg.textContent = 'Failed to save'; msg.style.color = 'var(--red)'; }
|
||||
}
|
||||
@@ -782,8 +794,7 @@ async function initTtsSettings() {
|
||||
|
||||
async function saveTTS() {
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tts_enabled: ttsEnabledToggle ? ttsEnabledToggle.checked : true, tts_provider: provSel.value, tts_model: getModel() || 'tts-1', tts_voice: getVoice() || 'alloy', tts_speed: speedSelect.value || '1' }) });
|
||||
await _postSettings({ tts_enabled: ttsEnabledToggle ? ttsEnabledToggle.checked : true, tts_provider: provSel.value, tts_model: getModel() || 'tts-1', tts_voice: getVoice() || 'alloy', tts_speed: speedSelect.value || '1' });
|
||||
ttsMsg.textContent = 'Saved'; ttsMsg.style.color = 'var(--fg)'; setTimeout(() => { ttsMsg.textContent = ''; }, 2000);
|
||||
if (window.aiTTSManager) window.aiTTSManager.checkAvailability();
|
||||
} catch (e) { ttsMsg.textContent = 'Failed to save'; ttsMsg.style.color = 'var(--red)'; }
|
||||
@@ -944,9 +955,7 @@ async function initSttSettings() {
|
||||
async function saveSTT() {
|
||||
try {
|
||||
var enabled = sttEnabledToggle ? sttEnabledToggle.checked : false;
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ stt_enabled: enabled, stt_provider: provSel.value, stt_model: getModel() || 'base', stt_language: langInput.value.trim() }) });
|
||||
await _postSettings({ stt_enabled: enabled, stt_provider: provSel.value, stt_model: getModel() || 'base', stt_language: langInput.value.trim() });
|
||||
sttMsg.textContent = 'Saved'; sttMsg.style.color = 'var(--fg)'; setTimeout(() => { sttMsg.textContent = ''; }, 2000);
|
||||
// Notify voiceRecorder of effective provider and update send button icon
|
||||
if (window.voiceRecorderModule) window.voiceRecorderModule._sttProvider = effectiveProvider();
|
||||
@@ -1102,10 +1111,7 @@ async function initSearchSettings() {
|
||||
payload[kf] = keyInput.value.trim();
|
||||
_settings[kf] = keyInput.value.trim();
|
||||
}
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
await _postSettings(payload);
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)';
|
||||
setTimeout(refreshStatus, 2000);
|
||||
if (searchModule && searchModule.refresh) searchModule.refresh();
|
||||
@@ -1257,11 +1263,7 @@ async function initSearchSettings() {
|
||||
async function _saveFallbackChain(chain) {
|
||||
_settings.search_fallback_chain = chain;
|
||||
try {
|
||||
await fetch('/api/auth/settings', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ search_fallback_chain: chain }),
|
||||
});
|
||||
await _postSettings({ search_fallback_chain: chain });
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)';
|
||||
setTimeout(refreshStatus, 2000);
|
||||
} catch (e) { msg.textContent = 'Failed to save'; msg.style.color = 'var(--red)'; }
|
||||
@@ -1425,10 +1427,7 @@ async function initResearchSettings() {
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
await _postSettings(payload);
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)';
|
||||
setTimeout(showStatus, 2000);
|
||||
} catch (e) { msg.textContent = 'Failed to save'; msg.style.color = 'var(--red)'; }
|
||||
@@ -1492,10 +1491,7 @@ async function initResearchSearchSettings() {
|
||||
|
||||
async function saveResearchSearch() {
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ research_search_provider: searchSel.value })
|
||||
});
|
||||
await _postSettings({ research_search_provider: searchSel.value });
|
||||
msg.textContent = 'Saved'; msg.style.color = 'var(--fg)';
|
||||
setTimeout(function() { msg.textContent = ''; }, 2000);
|
||||
} catch (e) { msg.textContent = 'Failed to save'; msg.style.color = 'var(--red)'; }
|
||||
@@ -1537,10 +1533,7 @@ async function initAgentSettings() {
|
||||
if (rounds != null) payload.agent_max_rounds = rounds;
|
||||
if (supInput) payload.agent_supervisor_ladder = !!supInput.checked;
|
||||
try {
|
||||
await fetch('/api/auth/settings', { method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
await _postSettings(payload);
|
||||
msg.textContent = (tools > 0 ? 'Limit: ' + tools + ' tool calls' : 'Unlimited tool calls') +
|
||||
(rounds != null ? ' · ' + rounds + ' steps/message' : '') +
|
||||
(supInput && supInput.checked ? ' · supervisor on' : '');
|
||||
@@ -1935,11 +1928,7 @@ async function initShortcuts() {
|
||||
|
||||
async function saveKeybinds() {
|
||||
try {
|
||||
await fetch('/api/auth/settings', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ keybinds }),
|
||||
});
|
||||
await _postSettings({ keybinds });
|
||||
// Update global keybinds so they take effect immediately
|
||||
window._odysseusKeybinds = keybinds;
|
||||
if (uiModule && uiModule.showToast) uiModule.showToast('Shortcut saved');
|
||||
@@ -2232,11 +2221,7 @@ async function initReminderSettings() {
|
||||
pubDebounce = setTimeout(async () => {
|
||||
try {
|
||||
const val = pubUrlIn.value.trim().replace(/\/+$/, '');
|
||||
await fetch('/api/auth/settings', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ app_public_url: val }),
|
||||
});
|
||||
await _postSettings({ app_public_url: val });
|
||||
if (pubUrlMsg) {
|
||||
pubUrlMsg.textContent = val ? 'Saved' : 'Cleared (deep-links disabled)';
|
||||
pubUrlMsg.style.color = 'var(--green,#50fa7b)';
|
||||
@@ -2534,12 +2519,7 @@ async function initReminderSettings() {
|
||||
|
||||
async function save(patch) {
|
||||
try {
|
||||
await fetch('/api/auth/settings', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
await _postSettings(patch);
|
||||
} catch (e) { console.warn('Failed to save reminder settings', e); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user