Accounts and sign-in, and fix the read toggle
epAction's redraw closure called itself when handed a row, so Mark read recursed until the stack blew; it now swaps that row in place. Opening an item also marks it read, redrawn where it stands so nothing vanishes from under the pointer on the Unread tab. Step A of multi-user: users and sessions tables, Argon2id, a session cookie, ipx user subcommands, and a trusted proxy header for Cloudflare Zero Trust -- honoured only from a trusted_proxies address. The shared token still works and is the admin. A new database starts with admin/ipodderx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -80,7 +80,15 @@ a{color:var(--accent)}
|
||||
}
|
||||
.sidetools button:hover,.sidefoot button:hover{border-color:var(--accent);color:var(--fg)}
|
||||
/* Settings and the log are housekeeping: they sit under the feeds, out of the way. */
|
||||
.sidefoot{display:flex;gap:6px;padding:8px 12px;border-top:1px solid var(--line);flex:none}
|
||||
.sidefoot{
|
||||
display:flex;flex-direction:column;gap:7px;padding:8px 12px;
|
||||
border-top:1px solid var(--line);flex:none;
|
||||
}
|
||||
.sidefoot .row{display:flex;gap:6px}
|
||||
.who{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--faint)}
|
||||
.who span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.who button{flex:none;background:none;border:0;padding:2px 4px;color:var(--faint);text-decoration:underline}
|
||||
.who button:hover{color:var(--fg)}
|
||||
.sidefoot button{display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
||||
.sidefoot i{font-style:normal;opacity:.75}
|
||||
.searchwrap{padding:0 12px 8px}
|
||||
@@ -368,8 +376,11 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
|
||||
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
||||
<div id="feedlist"></div>
|
||||
<div class="sidefoot">
|
||||
<button id="prefs" title="Settings"><i>⚙</i>Settings</button>
|
||||
<button id="logs" title="Daemon and web log"><i>▤</i>Log</button>
|
||||
<div class="who"><span id="who"></span><button id="signout" title="Sign out">Sign out</button></div>
|
||||
<div class="row">
|
||||
<button id="prefs" title="Settings"><i>⚙</i>Settings</button>
|
||||
<button id="logs" title="Daemon and web log"><i>▤</i>Log</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div id="main">
|
||||
@@ -422,6 +433,7 @@ const esc = s => (s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>
|
||||
|
||||
async function api(url,opts){
|
||||
const r = await fetch(url,{headers:{'Content-Type':'application/json'},...opts});
|
||||
if(r.status===401){ location.href='/login'; throw new Error('signed out'); }
|
||||
if(!r.ok) throw new Error(await r.text().catch(()=>r.status)||r.status);
|
||||
return r.status===204?null:r.json().catch(()=>null);
|
||||
}
|
||||
@@ -765,9 +777,27 @@ function kindOf(enc){
|
||||
function feedArt(){ const f=S.feeds.find(x=>x.id===S.feed); return f&&f.image; }
|
||||
|
||||
/// Selecting an item shows it in the pane below, rather than expanding the row.
|
||||
/// Replaces one row with a fresh one, leaving the rest of the list and its scroll alone.
|
||||
function swapRow(e){
|
||||
const row=$(`#eps .ep[data-guid="${CSS.escape(e.guid)}"]`);
|
||||
if(row) row.replaceWith(epEl(e));
|
||||
}
|
||||
|
||||
/// Opening an item is reading it. The row is redrawn where it stands rather than the list
|
||||
/// reloaded, so an item does not vanish from under the pointer on the Unread tab.
|
||||
function markRead(e){
|
||||
if(e.read) return;
|
||||
e.read=true;
|
||||
api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`,
|
||||
{method:'POST',body:JSON.stringify({read:true})})
|
||||
.then(()=>loadFeeds(true)).catch(err=>{ e.read=false; toast(err.message,true); });
|
||||
}
|
||||
|
||||
function selectEntry(e){
|
||||
S.sel=e.guid;
|
||||
markRead(e);
|
||||
$$('#eps .ep').forEach(x=>x.classList.toggle('sel', x.dataset.guid===e.guid));
|
||||
swapRow(e);
|
||||
showDetail(e);
|
||||
const d=$('#detail'); if(d) d.scrollTop=0;
|
||||
}
|
||||
@@ -861,8 +891,8 @@ function encBox(x){
|
||||
}
|
||||
|
||||
async function epAction(a,e,el,encId){
|
||||
// From a row we have the element to swap; from the detail pane we do not, so redraw.
|
||||
const redraw=()=>{ if(el) redraw(); else renderEntries(); showDetail(e); };
|
||||
// Swap the row in place and refresh the text below when it is the one being read.
|
||||
const redraw=()=>{ swapRow(e); if(!el || S.sel===e.guid) showDetail(e); };
|
||||
const enc=(encId!=null && e.enclosures.find(x=>x.id===encId)) || e.enclosures[0];
|
||||
const path=`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}`;
|
||||
try{
|
||||
@@ -1304,6 +1334,8 @@ 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(()=>{});
|
||||
on('#logs','onclick',logsModal);
|
||||
$('#feedFilter').oninput=renderFeeds;
|
||||
$('#burger').onclick=()=>nav(!$('#sidebar').classList.contains('open'));
|
||||
|
||||
91
web/login.html
Normal file
91
web/login.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user