Fix the UI dying at load, and add a page smoke test

$('#prefs').onclick referenced a prefsModal that was never defined, and an
uncaught ReferenceError stops the whole script -- taking the theme toggle,
the feed filter, the event stream and loadFeeds() down with it, so the app
rendered an empty shell.

The scheduling patch had anchored on a function the rewrite already
deleted; str.replace matched nothing and said nothing.

tests/page-smoke.js executes the page against a stub DOM so this class of
failure is visible, since every server-side check passed while the UI was
completely dead. Handler wiring now skips a bad reference instead of
throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 02:31:18 +00:00
parent 9dc4c1ddfa
commit 9960befed5
3 changed files with 149 additions and 1 deletions

View File

@@ -670,6 +670,52 @@ $('#addFeed').onclick=()=>{
};
};
let globalEvery = 60;
function everyText(m){
if(!m) return '\u2014';
if(m % 1440 === 0) return (m/1440)+(m===1440?' day':' days');
if(m % 60 === 0) return (m/60)+(m===60?' hour':' hours');
return m+' min';
}
function due(ts){
const d = ts - Date.now()/1000;
if(d <= 0) return 'due now';
if(d < 3600) return 'in '+Math.max(1,Math.round(d/60))+'m';
if(d < 86400) return 'in '+Math.round(d/3600)+'h';
return 'in '+Math.round(d/86400)+'d';
}
async function prefsModal(){
const g = await api('/api/settings');
globalEvery = g.every_mins;
openModal(`<h3>Settings</h3>
<div class="field"><label>Check feeds</label>
<input type="text" id="gsched" value="${esc(g.schedule)}">
<span class="hint">How often every feed is re-checked unless it overrides this.
Try <b>every 30m</b>, <b>every 4h</b>, <b>1d</b>. A feed's own suggested interval
(its <b>ttl</b>) is honoured when it asks to be polled less often than this.</span></div>
<div class="field"><label>Disk quota (GB, 0 = unlimited)</label>
<input type="number" id="gquota" min="0" step="0.5" value="${g.max_total_gb}">
<span class="hint">Over this, the oldest played episodes are deleted first. Starred
episodes are never touched.</span></div>
<div class="field"><label>Delete episodes older than (days, 0 = keep)</label>
<input type="number" id="gage" min="0" value="${g.max_age_days}"></div>
<div class="field"><label>Download folder</label>
<span class="hint" style="overflow-wrap:anywhere">${esc(g.download_dir)}</span></div>
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="gsave">Save</button></div>`);
$('#gsave').onclick=async()=>{
try{
await api('/api/settings',{method:'PATCH',body:JSON.stringify({
schedule:$('#gsched').value.trim(),
max_total_gb:Number($('#gquota').value)||0,
max_age_days:Number($('#gage').value)||0})});
closeModal(); toast('Settings saved');
await loadFeeds(true); if(S.feed){ renderFeed(); loadEntries(); }
}catch(e){ toast(e.message,true); }
};
}
function settingsModal(f){
openModal(`<h3>${esc(f.title||f.id)}</h3>
<div class="field"><label>Download folder</label>
@@ -760,8 +806,14 @@ $('#opml').onclick=()=>{
};
};
function on(sel,ev,fn){
const el=$(sel);
if(!el){ console.error('ipx: no element',sel); return; }
if(typeof fn!=='function'){ console.error('ipx: handler for',sel,'is not a function'); return; }
el[ev]=fn;
}
$('#scanAll').onclick=async()=>{ toast('Scanning all feeds…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({force:true})}); };
$('#prefs').onclick=prefsModal;
on('#prefs','onclick',prefsModal);
$('#feedFilter').oninput=renderFeeds;
$('#burger').onclick=()=>$('#sidebar').classList.toggle('open');
$('#theme').onclick=()=>{