Make scanning an admin setting, and document SSO

The per-feed schedule picker is gone and global Settings is admin-only,
enforced in the handlers with 403s rather than just hidden: polling costs
bandwidth and affects everyone reading the feed, so it belongs to the
operator. Folders, keywords and per-feed limits stay open to anyone.

docs/sso.md covers Cloudflare Zero Trust and Authentik end to end,
including why trusted_proxies names the proxy and not a subnet.

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 23:16:33 +00:00
parent 06f182b555
commit 4810bb5cfb
5 changed files with 261 additions and 14 deletions

View File

@@ -500,7 +500,7 @@ function nav(on){ $('#sidebar').classList.toggle('open',on); $('#scrim').hidden=
/* ---------------- state ---------------- */
const S = {
feeds:[], feed:null, entries:[], total:0, offset:0, limit:50,
filter:'all', q:'', sel:null, busy:new Set(),
filter:'all', q:'', sel:null, busy:new Set(), me:null,
};
const LIMIT = 50;
@@ -1223,7 +1223,6 @@ async function prefsModal(){
}
function settingsModal(f){
const fs = splitEvery(f.schedule_mins || globalEvery);
const isGroup = S.feeds.some(c=>c.group===f.id);
openModal(`<h3>${esc(f.title||f.id)}</h3>
${isGroup?`<p class="hint" style="margin:-6px 0 12px">This is an OPML subscription. These
@@ -1236,13 +1235,6 @@ function settingsModal(f){
<div class="field"><label>Keywords</label>
<input type="text" id="skw" value="${esc(f.keywords.join(', '))}">
<span class="hint">Comma separated. Empty takes everything.</span></div>
<div class="field"><label>Check schedule</label>
<div class="inline">
<input type="number" id="snum" min="1" max="999" value="${fs.n}" ${f.schedule_mins?'':'disabled'}>
<select id="sunit">${unitOptions(f.schedule_mins?fs.u:'', `Use the default — every ${everyText(globalEvery)}`)}</select>
</div>
<span class="hint">Overrides the global schedule, and the feed's own suggested
interval, for this feed only.</span></div>
<div class="field"><label>Max new downloads per scan</label>
<input type="number" id="smax" min="0" value="${f.max_new_per_check??''}">
<span class="hint">Blank follows the global default (${globalMax}). The rest wait for
@@ -1259,16 +1251,12 @@ function settingsModal(f){
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn primary" id="ssave">Save</button></div>`);
$('#scopy').onclick=()=>copyText($('#surl').value,$('#scopy'));
// An empty unit means "follow the global default", so the number has nothing to say.
$('#sunit').onchange=()=>{ $('#snum').disabled = !$('#sunit').value; };
$('#ssave').onclick=async()=>{
const max=$('#smax').value;
try{
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({
url:$('#surl').value.trim(),
folder:$('#sfolder').value.trim()||null,
schedule:$('#sunit').value
? `every ${Math.max(1,Number($('#snum').value)||1)}${$('#sunit').value}` : null,
keywords:$('#skw').value.split(',').map(s=>s.trim()).filter(Boolean),
max_new_per_check:max===''?null:Number(max),
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked})});
@@ -1335,7 +1323,12 @@ function on(sel,ev,fn){
$('#scanAll').onclick=async()=>{ toast('Scanning all feeds…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({force:true})}); };
on('#prefs','onclick',prefsModal);
on('#signout','onclick',async()=>{ await api('/api/logout',{method:'POST'}); location.href='/login'; });
api('/api/me').then(u=>{ $('#who').textContent=u.name+(u.admin?' · admin':''); }).catch(()=>{});
api('/api/me').then(u=>{
S.me=u;
$('#who').textContent=u.name+(u.admin?' · admin':'');
// Scanning, quotas and the download folder are the operator's business.
if(!u.admin) $('#prefs').hidden=true;
}).catch(()=>{});
on('#logs','onclick',logsModal);
$('#feedFilter').oninput=renderFeeds;
$('#burger').onclick=()=>nav(!$('#sidebar').classList.contains('open'));