diff --git a/static/js/admin.js b/static/js/admin.js
index c6e117020..5d071c6bd 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -1394,34 +1394,77 @@ function initEndpointForm() {
_refreshOfflineCount();
}
- const probeAllBtn = el('adm-epProbeAllBtn');
- if (probeAllBtn) {
- probeAllBtn.addEventListener('click', async () => {
+ const _fetchWithTimeout = async (url, opts = {}, timeoutMs = 25000) => {
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
+ try {
+ return await fetch(url, { ...opts, signal: ctrl.signal });
+ } finally {
+ clearTimeout(timer);
+ }
+ };
+ const _collectAddedEndpointIds = async () => {
+ const domIds = Array.from(document.querySelectorAll('[data-adm-ep-id]'))
+ .map(r => r.getAttribute('data-adm-ep-id'))
+ .filter(Boolean);
+ if (domIds.length) return Array.from(new Set(domIds));
+ try {
+ const res = await fetch('/api/model-endpoints', { credentials: 'same-origin' });
+ const data = await res.json().catch(() => []);
+ return (Array.isArray(data) ? data : []).map(ep => ep && ep.id).filter(Boolean);
+ } catch (_) {
+ return [];
+ }
+ };
+ const _setProbeAllButtonLabel = async (btn, text, whirlpoolRef) => {
+ btn.innerHTML = '';
+ if (whirlpoolRef && whirlpoolRef.element) btn.appendChild(whirlpoolRef.element);
+ btn.appendChild(document.createTextNode(text));
+ };
+ if (!window.__admEpProbeAllWired) {
+ window.__admEpProbeAllWired = true;
+ document.addEventListener('click', async (ev) => {
+ const probeAllBtn = ev.target.closest('#adm-epProbeAllBtn');
+ if (!probeAllBtn || probeAllBtn.disabled) return;
+ ev.preventDefault();
probeAllBtn.disabled = true;
const origHTML = probeAllBtn.innerHTML;
let _wp = null;
try {
- const sp = window.spinnerModule || (await import('./spinner.js')).default;
- _wp = sp.createWhirlpool(11);
- _wp.element.style.cssText = 'display:inline-flex;width:11px;height:11px;margin:0 4px 0 0;';
- probeAllBtn.innerHTML = '';
- probeAllBtn.appendChild(_wp.element);
- probeAllBtn.appendChild(document.createTextNode('Probing'));
- } catch (_) {
- probeAllBtn.innerHTML = 'Probing…';
- }
- try {
- // Hit the bulk local probe (same one the model picker uses).
- await fetch('/api/model-endpoints/probe-local', { credentials: 'same-origin' }).catch(() => {});
- // Then per-endpoint /probe for the rest so API/cloud endpoints
- // refresh too. Parallel — capped to 6 at a time so we don't
- // hammer the backend on a big list.
- const ids = Array.from(document.querySelectorAll('[data-adm-ep-id]')).map(r => r.getAttribute('data-adm-ep-id')).filter(Boolean);
+ try {
+ const sp = window.spinnerModule || (await import('./spinner.js')).default;
+ _wp = sp.createWhirlpool(11);
+ _wp.element.style.cssText = 'display:inline-flex;width:11px;height:11px;margin:0 4px 0 0;';
+ await _setProbeAllButtonLabel(probeAllBtn, 'Probing', _wp);
+ } catch (_) {
+ probeAllBtn.innerHTML = 'Probing...';
+ }
+ await _fetchWithTimeout('/api/model-endpoints/probe-local', { credentials: 'same-origin' }, 12000).catch(() => null);
+ const ids = await _collectAddedEndpointIds();
+ if (!ids.length) {
+ await loadEndpoints();
+ if (uiModule && uiModule.showToast) uiModule.showToast('No endpoints to probe', 1800);
+ return;
+ }
+ let done = 0;
+ let failed = 0;
const lane = async (id) => {
- try { await fetch(`/api/model-endpoints/${id}/probe`, { credentials: 'same-origin' }); } catch (_) {}
+ try {
+ const res = await _fetchWithTimeout(`/api/model-endpoints/${encodeURIComponent(id)}/models?refresh=true&refresh_timeout=20`, {
+ credentials: 'same-origin'
+ }, 25000);
+ if (!res || !res.ok || res.headers.get('X-Model-Refresh-Status') === 'failed') failed += 1;
+ else await res.json().catch(() => null);
+ } catch (err) {
+ failed += 1;
+ console.warn('Endpoint probe failed', id, err);
+ } finally {
+ done += 1;
+ try { await _setProbeAllButtonLabel(probeAllBtn, `Probing ${done}/${ids.length}`, _wp); } catch (_) {}
+ }
};
const queue = [...ids];
- const workers = Array.from({length: Math.min(6, queue.length)}, () => (async () => {
+ const workers = Array.from({ length: Math.min(4, queue.length) }, () => (async () => {
while (queue.length) {
const id = queue.shift();
if (id) await lane(id);
@@ -1429,7 +1472,11 @@ function initEndpointForm() {
})());
await Promise.all(workers);
await loadEndpoints();
- if (uiModule && uiModule.showToast) uiModule.showToast('Endpoint status refreshed', 1800);
+ _refreshOfflineCount();
+ if (uiModule && uiModule.showToast) {
+ const ok = Math.max(0, ids.length - failed);
+ uiModule.showToast(failed ? `Probed ${ok}/${ids.length} endpoints; ${failed} failed` : `Probed ${ids.length} endpoints`, failed ? 4200 : 1800);
+ }
} finally {
if (_wp) { try { _wp.destroy(); } catch (_) {} }
probeAllBtn.innerHTML = origHTML;