Page type-check: strict throughout, a typed $, null checks (#236)
$<T> returns the element type it is given and assumes the element is there: 122 lookups take their type from the tag their id is on in the markup. A handler reads its target through a cast, since a typed element will not take a handler whose event names a narrower target. strictNullChecks found no bug the page had; its 94 places were values the code already handles in ways the type-check could not follow (data- attributes the page writes, a map entry set the line before, Share drawn only for an item with a link), each now said so. isPlayable stays a plain boolean: as a type guard its false would have meant "no file", and it is also false for a file that is not audio or video. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,15 +56,15 @@ async function drawServer(){
|
||||
<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 primary" id="gsave">${ICON.check} Save</button></div>`;
|
||||
$('#gsave').onclick = async () => {
|
||||
$<HTMLButtonElement>('#gsave').onclick = async () => {
|
||||
try{
|
||||
await api('/api/settings', {method: 'PATCH', body: JSON.stringify({
|
||||
schedule: `every ${Math.max(1, Number($('#gnum').value) || 1)}${$('#gunit').value}`,
|
||||
max_new_per_check: Math.max(0, Number($('#gmax').value) || 0),
|
||||
media_types: $('#gtypes').value.split(',').map((t: string) => t.trim()).filter(Boolean),
|
||||
max_total_gb: Number($('#gquota').value) || 0,
|
||||
max_age_days: Number($('#gage').value) || 0,
|
||||
art_cache_mb: Math.max(0, Number($('#gart').value) || 0)})});
|
||||
schedule: `every ${Math.max(1, Number($<HTMLInputElement>('#gnum').value) || 1)}${$<HTMLSelectElement>('#gunit').value}`,
|
||||
max_new_per_check: Math.max(0, Number($<HTMLInputElement>('#gmax').value) || 0),
|
||||
media_types: $<HTMLInputElement>('#gtypes').value.split(',').map((t: string) => t.trim()).filter(Boolean),
|
||||
max_total_gb: Number($<HTMLInputElement>('#gquota').value) || 0,
|
||||
max_age_days: Number($<HTMLInputElement>('#gage').value) || 0,
|
||||
art_cache_mb: Math.max(0, Number($<HTMLInputElement>('#gart').value) || 0)})});
|
||||
toast('Settings saved');
|
||||
}catch(e){ toast(why(e), true); }
|
||||
};
|
||||
@@ -101,13 +101,13 @@ async function drawDirectorySettings(){
|
||||
drawDirectorySettings();
|
||||
}catch(e){ toast(why(e), true); }
|
||||
};
|
||||
$('#dsave').onclick = () => {
|
||||
const patch: Record<string, unknown> = {directory_url: $('#durl').value.trim(), directory_share: $('#dshare').checked};
|
||||
$<HTMLButtonElement>('#dsave').onclick = () => {
|
||||
const patch: Record<string, unknown> = {directory_url: $<HTMLInputElement>('#durl').value.trim(), directory_share: $<HTMLInputElement>('#dshare').checked};
|
||||
// Blank leaves the token as it is; Clear is the way to remove it.
|
||||
if($('#dtoken').value.trim()) patch.directory_token = $('#dtoken').value.trim();
|
||||
if($<HTMLInputElement>('#dtoken').value.trim()) patch.directory_token = $<HTMLInputElement>('#dtoken').value.trim();
|
||||
save(patch, 'Directory settings saved');
|
||||
};
|
||||
const clear = $('#dclear');
|
||||
const clear = $<HTMLButtonElement>('#dclear');
|
||||
if(clear) clear.onclick = () => save({directory_token: ''}, 'Token cleared');
|
||||
drawDirectoryStatus();
|
||||
}
|
||||
@@ -161,12 +161,12 @@ async function drawDirectoryAdmin(){
|
||||
const row = b.closest<HTMLElement>('.urow');
|
||||
try{
|
||||
if(b.dataset.a === 'drop'){
|
||||
const name = row.dataset.name;
|
||||
const name = row!.dataset.name!;
|
||||
if(!confirm(`Drop ${name}'s feeds and counts from the directory? They come back the next time ${name} shares.`)) return;
|
||||
await api(`/api/directory-instances/${encodeURIComponent(name)}`, {method: 'DELETE'});
|
||||
toast(`Dropped ${name}'s snapshot`);
|
||||
}else if(b.dataset.a === 'unblock'){
|
||||
await api(`/api/directory-blocked/${encodeURIComponent(row.dataset.id)}`, {method: 'DELETE'});
|
||||
await api(`/api/directory-blocked/${encodeURIComponent(row!.dataset.id!)}`, {method: 'DELETE'});
|
||||
toast('Unblocked. It comes back with the next snapshot that has it');
|
||||
}
|
||||
drawDirectoryAdmin();
|
||||
@@ -205,18 +205,18 @@ async function drawAccounts(){
|
||||
drawAccounts(); // on a refusal, this puts the checkbox back where the server left it
|
||||
};
|
||||
for(const row of $$('#accounts [data-id]')){
|
||||
const u = users.find(x => String(x.id) === row.dataset.id);
|
||||
$('[data-a="admin"]', row).onchange = (e: Event & {target: HTMLInputElement}) =>
|
||||
change(u, {method: 'PATCH', body: JSON.stringify({admin: e.target.checked})});
|
||||
const u = users.find(x => String(x.id) === row.dataset.id)!;
|
||||
$('[data-a="admin"]', row).onchange = (e: Event) =>
|
||||
change(u, {method: 'PATCH', body: JSON.stringify({admin: (e.target as HTMLInputElement).checked})});
|
||||
$('[data-a="rm"]', row).onclick = () => {
|
||||
if(confirm(`Remove ${u.name}? Their subscriptions and read state go with them. Downloaded files stay.`))
|
||||
change(u, {method: 'DELETE'});
|
||||
};
|
||||
}
|
||||
$('#uadd').onclick = async () => {
|
||||
$<HTMLButtonElement>('#uadd').onclick = async () => {
|
||||
try{
|
||||
await api('/api/users', {method: 'POST', body: JSON.stringify({
|
||||
name: $('#uname').value, password: $('#upass').value, admin: $('#uadmin').checked})});
|
||||
name: $<HTMLInputElement>('#uname').value, password: $<HTMLInputElement>('#upass').value, admin: $<HTMLInputElement>('#uadmin').checked})});
|
||||
toast('Added'); drawAccounts();
|
||||
}catch(e){ toast(why(e), true); } // keep what was typed
|
||||
};
|
||||
@@ -259,14 +259,14 @@ function drawLogView(){
|
||||
The buffer keeps debug detail even when the terminal does not; <b>IPX_UI_LOG</b> changes
|
||||
what it captures.</span>`;
|
||||
for(const b of $$('#logtabs button')) b.onclick = () => {
|
||||
logTab = b.dataset.t;
|
||||
logTab = b.dataset.t!;
|
||||
for(const x of $$('#logtabs button')) x.classList.toggle('on', x.dataset.t === logTab);
|
||||
drawLog();
|
||||
};
|
||||
$('#loglevel').onchange = (e: Event & {target: HTMLSelectElement}) => { logLevel = e.target.value; drawLog(); };
|
||||
$('#logq').oninput = (e: Event & {target: HTMLInputElement}) => { logFilter = e.target.value.toLowerCase(); drawLog(); };
|
||||
$('#logcopy').onclick = () => copyText(visibleLog().map(l =>
|
||||
`${new Date(l.ts * 1000).toISOString()} ${l.level} ${l.target} ${l.msg}`).join('\n'), $('#logcopy'));
|
||||
$<HTMLSelectElement>('#loglevel').onchange = (e: Event) => { logLevel = (e.target as HTMLSelectElement).value; drawLog(); };
|
||||
$<HTMLInputElement>('#logq').oninput = (e: Event) => { logFilter = (e.target as HTMLInputElement).value.toLowerCase(); drawLog(); };
|
||||
$<HTMLButtonElement>('#logcopy').onclick = () => copyText(visibleLog().map(l =>
|
||||
`${new Date(l.ts * 1000).toISOString()} ${l.level} ${l.target} ${l.msg}`).join('\n'), $<HTMLButtonElement>('#logcopy'));
|
||||
pollLog();
|
||||
if(!logTimer) logTimer = setInterval(pollLog, 2000);
|
||||
}
|
||||
@@ -294,7 +294,7 @@ function visibleLog(){
|
||||
}
|
||||
function drawLog(){
|
||||
const box = $('#logbox'); if(!box) return;
|
||||
const follow = $('#logfollow')?.checked;
|
||||
const follow = $<HTMLInputElement>('#logfollow')?.checked;
|
||||
const rows = visibleLog();
|
||||
box.innerHTML = rows.length ? rows.map(l => {
|
||||
const t = new Date(l.ts * 1000).toLocaleTimeString();
|
||||
|
||||
Reference in New Issue
Block a user