diff --git a/frontend/index.html b/frontend/index.html
index 9f00950..f77b52d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -171,6 +171,7 @@ const drop=document.getElementById('drop'), fileInput=document.getElementById('f
logBox=document.getElementById('logBox'),
logHint=document.getElementById('logHint');
let chosen=null, polling=null, currentJobId=null, sheetPage={}, viewerZoom=1, reviewDirty=false;
+let defaultVisionModel='', defaultTextModel='';
function modelLabel(m){
// Include per-1M-token pricing when the catalog provides it.
@@ -212,6 +213,12 @@ function saveModelsCache(data){
try{ localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify({ts:Date.now(), data})); }
catch(e){}
}
+function fetchWithTimeout(url, ms){
+ return Promise.race([
+ fetch(url, {cache:'no-store'}),
+ new Promise((_,reject)=>setTimeout(()=>reject(new Error('timeout')), ms))
+ ]);
+}
async function loadModels(){
const note=document.getElementById('modelNote');
@@ -225,7 +232,8 @@ async function loadModels(){
return;
}
try{
- const res=await fetch('/models');
+ note.textContent='fetching models...';
+ const res=await fetchWithTimeout('/models', 5000);
if(!res.ok) throw new Error('models HTTP '+res.status);
const data=await res.json();
saveModelsCache(data);
@@ -235,10 +243,12 @@ async function loadModels(){
modelsLoaded=true;
note.textContent='('+(data.text||[]).length+' text / '+(data.vision||[]).length+' vision available)';
}catch(err){
- visionSel.innerHTML='';
- textSel.innerHTML='';
+ const fallback=[{id:defaultVisionModel||'', name:defaultVisionModel||'(default)', prompt_usd_per_mtok:null, completion_usd_per_mtok:null}];
+ fillSelect(visionSel, fallback.filter(m=>m.id), defaultVisionModel);
+ const fallbackText=[{id:defaultTextModel||'', name:defaultTextModel||'(default)', prompt_usd_per_mtok:null, completion_usd_per_mtok:null}];
+ fillSelect(textSel, fallbackText.filter(m=>m.id), defaultTextModel);
visionSel.disabled=false; textSel.disabled=false;
- note.textContent='using configured defaults (list unavailable)';
+ note.textContent='using configured defaults ('+(err.message==='timeout'?'fetch timed out':'list unavailable')+')';
console.warn('Could not load models:', err);
}
}
@@ -674,11 +684,17 @@ async function finalizeReview(){
// If opened from an email link (/?job=), load that job's results directly.
(function init(){
- // Show the deployed build in the header so it's obvious which version is up.
- fetch('/health').then(r=>r.ok?r.json():null).then(h=>{
- if(h&&h.build) document.getElementById('buildTag').textContent=' · build '+h.build;
- }).catch(()=>{});
- loadModels();
+ // Fetch /health first: build tag for the header and default models as a
+ // fallback if the larger /models catalog fails or times out.
+ fetch('/health', {cache:'no-store'}).then(r=>r.ok?r.json():null).then(h=>{
+ if(h){
+ if(h.build) document.getElementById('buildTag').textContent=' · build '+h.build;
+ if(h.model) defaultVisionModel=h.model;
+ if(h.text_model) defaultTextModel=h.text_model;
+ }
+ }).catch(()=>{}).finally(()=>{
+ loadModels();
+ });
const jobId=new URLSearchParams(location.search).get('job');
if(jobId){ statusEl.innerHTML='Loading job '+esc(jobId)+'...'; poll(jobId); }
})();