mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-25 01:22:19 +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
+65
-17
@@ -6,6 +6,7 @@ import settingsModule from './settings.js';
|
||||
import { providerLogo, providerLogoFromUrl } from './providers.js';
|
||||
import { sortModelObjects } from './modelSort.js';
|
||||
import { PROVIDER_DEVICE_FLOWS, formatDeviceFlowError, runProviderDeviceFlow } from './providerDeviceFlow.js';
|
||||
import { getSettings, getTools, invalidateSettings, invalidateTools } from './appConfig.js';
|
||||
|
||||
let initialized = false;
|
||||
let modalEl = null;
|
||||
@@ -345,8 +346,7 @@ function initSignupToggle() {
|
||||
|
||||
function initShareDefaultsToggle() {
|
||||
const toggle = el('adm-shareDefaultsToggle');
|
||||
fetch('/api/auth/settings', { credentials: 'same-origin' })
|
||||
.then(r => r.json())
|
||||
getSettings()
|
||||
.then(d => { toggle.checked = !!d.share_defaults_with_users; })
|
||||
.catch(e => console.warn('Settings fetch failed:', e));
|
||||
toggle.addEventListener('change', async () => {
|
||||
@@ -361,6 +361,9 @@ function initShareDefaultsToggle() {
|
||||
toggle.checked = !!data.share_defaults_with_users;
|
||||
} catch (e) {
|
||||
toggle.checked = !toggle.checked;
|
||||
} finally {
|
||||
// Drop the shared snapshot: it still says what this toggle used to be.
|
||||
invalidateSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1893,8 +1896,16 @@ async function loadBuiltinTools() {
|
||||
const list = el('adm-builtin-tools-list');
|
||||
if (!list) return;
|
||||
try {
|
||||
const res = await fetch('/api/tools', { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
// This panel is an editor, and its save posts the whole disabled list
|
||||
// rebuilt from the checkboxes below. So it has to render authoritative
|
||||
// state: a snapshot that went stale out of band (the manage_settings tool,
|
||||
// another tab) would be re-posted wholesale on the next unrelated toggle
|
||||
// and would silently undo the newer state. refreshAll() calls this on every
|
||||
// panel open, so drop the shared entry and refill it. The startup read that
|
||||
// chatRenderer.js shares is unaffected; this panel just never edits a cache,
|
||||
// which is the same rule the settings panel follows by reading directly.
|
||||
invalidateTools();
|
||||
const data = await getTools();
|
||||
const tools = data.tools || [];
|
||||
if (!tools.length) { list.innerHTML = '<div class="admin-empty">No tools found</div>'; return; }
|
||||
|
||||
@@ -1968,17 +1979,50 @@ async function loadBuiltinTools() {
|
||||
});
|
||||
});
|
||||
|
||||
// Helper: save disabled tools + update counters
|
||||
async function _saveToolState() {
|
||||
const allChecks = list.querySelectorAll('input[data-tool-id]');
|
||||
const disabled = [];
|
||||
allChecks.forEach(c => { if (!c.checked) disabled.push(c.dataset.toolId); });
|
||||
await fetch('/api/tools', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ disabled }),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
// Merge only the user's intended changes onto authoritative server state.
|
||||
// /api/tools replaces the full disabled list, so rebuilding it from this
|
||||
// panel's DOM can undo a change made by another tab or manage_settings
|
||||
// after the panel was opened.
|
||||
async function _saveToolState(changes) {
|
||||
invalidateTools();
|
||||
const latest = await getTools();
|
||||
const state = new Map(
|
||||
(latest.tools || []).map(t => [t.id, !!t.enabled])
|
||||
);
|
||||
|
||||
for (const change of changes) {
|
||||
if (state.has(change.id)) {
|
||||
state.set(change.id, !!change.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
const disabled = Array.from(state.entries())
|
||||
.filter(([, enabled]) => !enabled)
|
||||
.map(([id]) => id);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/tools', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ disabled }),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to update tools (${res.status})`);
|
||||
|
||||
// Bring the still-open editor forward to the same merged snapshot so an
|
||||
// out-of-band change is visible instead of leaving stale checkboxes.
|
||||
list.querySelectorAll('input[data-tool-id]').forEach(c => {
|
||||
if (state.has(c.dataset.toolId)) {
|
||||
c.checked = state.get(c.dataset.toolId);
|
||||
}
|
||||
});
|
||||
list.querySelectorAll('.admin-tool-category').forEach(_updateCatCounter);
|
||||
} finally {
|
||||
// This route persists disabled_tools into the settings store
|
||||
// (routes/model_routes.py), so both snapshots are now stale.
|
||||
invalidateTools();
|
||||
invalidateSettings();
|
||||
}
|
||||
}
|
||||
function _updateCatCounter(catEl) {
|
||||
if (!catEl) return;
|
||||
@@ -1993,7 +2037,9 @@ async function loadBuiltinTools() {
|
||||
// Wire individual tool toggles
|
||||
list.querySelectorAll('input[data-tool-id]').forEach(chk => {
|
||||
chk.addEventListener('change', async () => {
|
||||
await _saveToolState();
|
||||
await _saveToolState([
|
||||
{ id: chk.dataset.toolId, enabled: chk.checked },
|
||||
]);
|
||||
_updateCatCounter(chk.closest('.admin-tool-category'));
|
||||
});
|
||||
});
|
||||
@@ -2004,8 +2050,10 @@ async function loadBuiltinTools() {
|
||||
const catEl = chk.closest('.admin-tool-category');
|
||||
if (!catEl) return;
|
||||
const checked = chk.checked;
|
||||
const changes = Array.from(catEl.querySelectorAll('input[data-tool-id]'))
|
||||
.map(c => ({ id: c.dataset.toolId, enabled: checked }));
|
||||
catEl.querySelectorAll('input[data-tool-id]').forEach(c => { c.checked = checked; });
|
||||
await _saveToolState();
|
||||
await _saveToolState(changes);
|
||||
_updateCatCounter(catEl);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user