Page type-check: strict's flags that cost nothing, and catch variables as unknown (#235)
why(e) gives a caught error's message; each catch read e.message as any. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,7 @@ async function drawServer(){
|
||||
max_age_days: Number($('#gage').value) || 0,
|
||||
art_cache_mb: Math.max(0, Number($('#gart').value) || 0)})});
|
||||
toast('Settings saved');
|
||||
}catch(e){ toast(e.message, true); }
|
||||
}catch(e){ toast(why(e), true); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ async function drawDirectorySettings(){
|
||||
await api('/api/settings', {method: 'PATCH', body: JSON.stringify(patch)});
|
||||
toast(said);
|
||||
drawDirectorySettings();
|
||||
}catch(e){ toast(e.message, true); }
|
||||
}catch(e){ toast(why(e), true); }
|
||||
};
|
||||
$('#dsave').onclick = () => {
|
||||
const patch: Record<string, unknown> = {directory_url: $('#durl').value.trim(), directory_share: $('#dshare').checked};
|
||||
@@ -115,7 +115,7 @@ async function drawDirectorySettings(){
|
||||
async function drawDirectoryStatus(){
|
||||
const el = $('#dstatus');
|
||||
let s;
|
||||
try{ s = await api('/api/directory-status'); }catch(e){ el.textContent = e.message; return; }
|
||||
try{ s = await api('/api/directory-status'); }catch(e){ el.textContent = why(e); return; }
|
||||
if(!el.isConnected) return;
|
||||
const lines: string[] = [];
|
||||
if(!s.configured) lines.push('Not set up: it needs an address and a token.');
|
||||
@@ -138,7 +138,7 @@ async function drawDirectoryStatus(){
|
||||
async function drawDirectoryAdmin(){
|
||||
const box = $('#dadmin');
|
||||
let a;
|
||||
try{ a = await api('/api/directory-admin'); }catch(e){ box.innerHTML = `<p class="hint">${esc(e.message)}</p>`; return; }
|
||||
try{ a = await api('/api/directory-admin'); }catch(e){ box.innerHTML = `<p class="hint">${esc(why(e))}</p>`; return; }
|
||||
if(!box.isConnected) return;
|
||||
box.innerHTML = `<h2>Servers</h2>
|
||||
<p class="hint">Each server sharing with the directory, by its token's name. Dropping one removes its
|
||||
@@ -170,7 +170,7 @@ async function drawDirectoryAdmin(){
|
||||
toast('Unblocked. It comes back with the next snapshot that has it');
|
||||
}
|
||||
drawDirectoryAdmin();
|
||||
}catch(e){ toast(e.message, true); }
|
||||
}catch(e){ toast(why(e), true); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ async function drawAccounts(){
|
||||
// Demoting yourself takes this page away; go back to the app rather than stay on a page
|
||||
// the server no longer answers. Only on success: a refusal's toast has to stay readable.
|
||||
if(u.name === me?.name){ location.href = '/'; return; }
|
||||
}catch(e){ toast(e.message, true); }
|
||||
}catch(e){ toast(why(e), true); }
|
||||
drawAccounts(); // on a refusal, this puts the checkbox back where the server left it
|
||||
};
|
||||
for(const row of $$('#accounts [data-id]')){
|
||||
@@ -218,7 +218,7 @@ async function drawAccounts(){
|
||||
await api('/api/users', {method: 'POST', body: JSON.stringify({
|
||||
name: $('#uname').value, password: $('#upass').value, admin: $('#uadmin').checked})});
|
||||
toast('Added'); drawAccounts();
|
||||
}catch(e){ toast(e.message, true); } // keep what was typed
|
||||
}catch(e){ toast(why(e), true); } // keep what was typed
|
||||
};
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ async function pollLog(){
|
||||
}else if(!logLines.length){ drawLog(); }
|
||||
}catch(e){
|
||||
const box = $('#logbox');
|
||||
if(box) box.innerHTML = `<p class="empty">Lost contact with the daemon: ${esc(e.message)}</p>`;
|
||||
if(box) box.innerHTML = `<p class="empty">Lost contact with the daemon: ${esc(why(e))}</p>`;
|
||||
}
|
||||
}
|
||||
function visibleLog(){
|
||||
|
||||
@@ -30,7 +30,7 @@ $('#addFeed').onclick=()=>{
|
||||
keywords:$('#nkw').value.split(',').map(s=>s.trim()).filter(Boolean)})});
|
||||
closeModal(); toast(r.existing?`Already subscribed as ${r.id}`:`Added ${r.id}`);
|
||||
await loadFeeds(true); selectFeed(r.id);
|
||||
}catch(e){ toast(e.message,true); $('#nsave').title='Add feed'; $('#nsave').disabled=false; }
|
||||
}catch(e){ toast(why(e),true); $('#nsave').title='Add feed'; $('#nsave').disabled=false; }
|
||||
};
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ async function subscribeListed(p){
|
||||
const r=await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'});
|
||||
toast(`Subscribed to ${NAME(p)}`);
|
||||
await loadFeeds(true); selectFeed(r.id);
|
||||
}catch(e){ toast(e.message,true); }
|
||||
}catch(e){ toast(why(e),true); }
|
||||
}
|
||||
|
||||
// Where you are in the Directory: what kind of feed, which category and subcategory, in what
|
||||
@@ -104,7 +104,7 @@ async function renderDirectory(url,box){
|
||||
// Each visit starts at the front, as going back to a tab does.
|
||||
dirCat=null; dirSub=null; dirShow=null;
|
||||
dirError='';
|
||||
try{ dirRows=await api(url)||[]; }catch(e){ dirRows=[]; dirError=e.message; }
|
||||
try{ dirRows=await api(url)||[]; }catch(e){ dirRows=[]; dirError=why(e); }
|
||||
drawDirectory(box);
|
||||
return dirRows.length;
|
||||
}
|
||||
@@ -208,7 +208,7 @@ async function fillShow(box,p){
|
||||
const list=$('#dshow',box), about=$('#dabout',box);
|
||||
let got;
|
||||
try{ got=await api(`/api/directory/${encodeURIComponent(p.id)}`); }
|
||||
catch(e){ list.innerHTML=`<p class="hint">${esc(e.message)}</p>`; return; }
|
||||
catch(e){ list.innerHTML=`<p class="hint">${esc(why(e))}</p>`; return; }
|
||||
// Gone back, or on to another, while it loaded.
|
||||
if(dirShow!==p.id||!list.isConnected) return;
|
||||
// Some feeds give their title again as their description, which says nothing new.
|
||||
@@ -233,12 +233,12 @@ async function fillShow(box,p){
|
||||
+`<button class="btn ico danger" data-a="dremove" title="Remove from the directory" aria-label="Remove from the directory">${ICON.trash}</button>`);
|
||||
$('[data-a="drefresh"]',acts).onclick=async()=>{
|
||||
try{ await api(`/api/directory/${encodeURIComponent(p.id)}/refresh`,{method:'POST'}); toast('Checked'); fillShow(box,p); }
|
||||
catch(e){ toast(e.message,true); }
|
||||
catch(e){ toast(why(e),true); }
|
||||
};
|
||||
$('[data-a="dremove"]',acts).onclick=async()=>{
|
||||
if(!confirm(`Remove ${NAME(p)} from the directory? It is blocked, so no server's snapshot brings it back. Admin, Directory can unblock it.`)) return;
|
||||
try{ await api(`/api/directory/${encodeURIComponent(p.id)}`,{method:'DELETE'}); }
|
||||
catch(e){ return toast(e.message,true); }
|
||||
catch(e){ return toast(why(e),true); }
|
||||
toast(`Removed ${NAME(p)} from the directory`);
|
||||
dirRows=dirRows.filter(r=>r.id!==p.id); dirShow=null;
|
||||
drawDirectory(box);
|
||||
@@ -347,7 +347,7 @@ async function forget(e){
|
||||
try{
|
||||
await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`,
|
||||
{method:'POST',body:JSON.stringify({secs:0})});
|
||||
}catch(err){ toast(err.message,true); }
|
||||
}catch(err){ toast(why(err),true); }
|
||||
if(S.feed===':listening') renderListed(VIEWS[':listening']);
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ async function prefsModal(){
|
||||
<button class="btn ico" id="tokcopy" title="Copy" aria-label="Copy">${ICON.copy}</button></div></div>`);
|
||||
$('#tokcopy').onclick=()=>copyText(t.token,$('#tokcopy'));
|
||||
$('#tokval').select();
|
||||
}catch(err){ toast(err.message,true); }
|
||||
}catch(err){ toast(why(err),true); }
|
||||
};
|
||||
$('#sblock').onchange=async e=>{
|
||||
const blocked=splitWords(e.target.value);
|
||||
@@ -454,7 +454,7 @@ async function prefsModal(){
|
||||
await api('/api/me',{method:'PATCH',body:JSON.stringify({blocked})});
|
||||
if(S.me) S.me.blocked=blocked;
|
||||
toast('Saved'); await loadFeeds(true); loadEntries();
|
||||
}catch(err){ toast(err.message,true); }
|
||||
}catch(err){ toast(why(err),true); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ async function drawTokens(){
|
||||
<button class="btn ico" data-tok="${t.id}" title="Revoke ${esc(t.name)}" aria-label="Revoke ${esc(t.name)}">${ICON.trash}</button></div>`).join('');
|
||||
for(const b of $$('[data-tok]',box)) b.onclick=async()=>{
|
||||
try{ await api(`/api/tokens/${b.dataset.tok}`,{method:'DELETE'}); toast('Revoked'); drawTokens(); }
|
||||
catch(err){ toast(err.message,true); }
|
||||
catch(err){ toast(why(err),true); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -664,7 +664,7 @@ function settingsModal(f, newUrl?: string){
|
||||
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify(patch)});
|
||||
closeModal(); toast('Saved. It applies from the next check.');
|
||||
await loadFeeds(true); renderFeed(); loadEntries();
|
||||
}catch(e){ toast(e.message,true); }
|
||||
}catch(e){ toast(why(e),true); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -681,7 +681,7 @@ function downloadLatestModal(f){
|
||||
const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/download-latest`,
|
||||
{method:'POST',body:JSON.stringify({count:n})});
|
||||
closeModal(); toast(`Queued ${r.queued} item${r.queued===1?'':'s'}`);
|
||||
}catch(e){ toast(e.message,true); }
|
||||
}catch(e){ toast(why(e),true); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -724,7 +724,7 @@ function opmlModal(){
|
||||
try{
|
||||
const r=await api('/api/opml',{method:'POST',body:JSON.stringify({xml})});
|
||||
letGo(); closeModal(); toast(`Subscribed to ${r.added} feed(s)`+(r.already?`, ${r.already} you already had`:'')); loadFeeds(true);
|
||||
}catch(e){ letGo(); toast(e.message,true); }
|
||||
}catch(e){ letGo(); toast(why(e),true); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ async function feedAction(a,f){
|
||||
try{
|
||||
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({pinned:!f.pinned})});
|
||||
await loadFeeds(true); renderFeed();
|
||||
}catch(e){ toast(e.message,true); }
|
||||
}catch(e){ toast(why(e),true); }
|
||||
}
|
||||
if(a==='settings') settingsModal(f);
|
||||
if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]'));
|
||||
@@ -187,7 +187,7 @@ async function allAction(a){
|
||||
try{
|
||||
const r=await api('/api/read-all',{method:'POST'});
|
||||
toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries();
|
||||
}catch(e){ toast(e.message,true); }
|
||||
}catch(e){ toast(why(e),true); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -396,12 +396,12 @@ async function epAction(a: string, e, el, encId?: number){
|
||||
await api(`/api/enclosures/${enc.id}`,{method:'DELETE'});
|
||||
}catch(err){
|
||||
// 409: somebody else has it starred or unplayed. Their reason, their words.
|
||||
if(!/one copy of this file/.test(err.message)) throw err;
|
||||
if(!confirm(err.message+'\n\nDelete it anyway?')) return;
|
||||
if(!/one copy of this file/.test(why(err))) throw err;
|
||||
if(!confirm(why(err)+'\n\nDelete it anyway?')) return;
|
||||
await api(`/api/enclosures/${enc.id}?force=true`,{method:'DELETE'});
|
||||
}
|
||||
toast('Deleted'); loadEntries(); loadFeeds(true);
|
||||
}
|
||||
}catch(err){ toast(err.message,true); }
|
||||
}catch(err){ toast(why(err),true); }
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ $('#pdead').onclick=async()=>{
|
||||
const f=S.feeds.find(x=>x.id===player.feed); if(!f) return;
|
||||
const on=!f.deadair;
|
||||
try{ await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({deadair:on})}); }
|
||||
catch(err){ toast(err.message,true); return; }
|
||||
catch(err){ toast(why(err),true); return; }
|
||||
f.deadair=on; paintDead();
|
||||
const switched=await switchVersion();
|
||||
toast(!on?'DeadAir is off for this show':switched?'DeadAir is cutting the dead air from this show'
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// smoke test, not the type checker, is what makes sure a selector exists.
|
||||
const $ = (s: string, r: ParentNode = document): any => r.querySelector(s);
|
||||
const $$ = (s: string, r: ParentNode = document): any[] => [...r.querySelectorAll(s)];
|
||||
/// What went wrong, from whatever a catch caught: the page throws Errors, but a catch cannot know.
|
||||
const why = (e: unknown): string => e instanceof Error ? e.message : String(e);
|
||||
const esc = s => (s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
// Icons: Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com
|
||||
// License - https://fontawesome.com/license/free (Icons: CC BY 4.0). Embedded as SVG, only the
|
||||
@@ -151,7 +153,7 @@ async function share(title: string, url: string, btn?){
|
||||
if(navigator.share){
|
||||
// Cancelling the sheet rejects too, and is not a failure worth a word.
|
||||
try{ await navigator.share({title, url}); }
|
||||
catch(e){ if(e.name!=='AbortError') toast(e.message,true); }
|
||||
catch(e){ if((e as Error)?.name!=='AbortError') toast(why(e),true); }
|
||||
return;
|
||||
}
|
||||
await copyText(url,btn);
|
||||
|
||||
Reference in New Issue
Block a user