Log tabs with a daemon I/O view, and Playwright UI tests
The log view splits into All / Daemon I/O / Scans / HTTP. Daemon I/O is the control protocol itself, logged where every command funnels through so it covers socket clients, the CLI and the web UI alike. stderr and the in-app buffer now have separate filters, so the UI can keep debug detail the terminal should not carry. Playwright drives a real browser against a daemon on fixture feeds. Eight tests, each mapping to a bug that reached a user -- the Rust tests and the stub-DOM smoke test cannot see a wrong selector or a dead handler. It immediately found one: OPML folders rendered expanded by default, because the code stored closed groups, so any folder never toggled counted as open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -432,14 +432,15 @@ function renderFeeds(){
|
||||
for(const f of shown){
|
||||
if(f.group && byId[f.group]) continue; // drawn under its parent instead
|
||||
order.push([f,0]);
|
||||
// A subscription can hold dozens of feeds, so the folder starts closed.
|
||||
if(!collapsed.has(f.id) || q) for(const c of shown) if(c.group===f.id) order.push([c,1]);
|
||||
// A subscription can hold dozens of feeds, so a folder starts closed. Searching
|
||||
// opens them all, or matches inside a closed folder would be invisible.
|
||||
if(expanded.has(f.id) || q) for(const c of shown) if(c.group===f.id) order.push([c,1]);
|
||||
}
|
||||
for(const [f,depth] of order){
|
||||
const kids=shown.filter(c=>c.group===f.id).length;
|
||||
const el=document.createElement('div');
|
||||
el.className='feed'+(S.feed===f.id?' sel':'')+(depth?' child':'')+(kids?' group':'');
|
||||
const open = kids && (!collapsed.has(f.id) || q);
|
||||
const open = kids && (expanded.has(f.id) || q);
|
||||
el.innerHTML =
|
||||
(kids?`<span class="chev${open?' open':''}" title="Show or hide the feeds inside">▶</span>`:'')+
|
||||
artHTML(f.image,f.title||f.id)+
|
||||
@@ -770,13 +771,26 @@ function closeModal(){
|
||||
}
|
||||
|
||||
/* ---------------- log view ---------------- */
|
||||
let logTimer=null, logSeq=0, logLines=[], logFilter='', logLevel='';
|
||||
let logTimer=null, logSeq=0, logLines=[], logFilter='', logLevel='', logTab='all';
|
||||
// Which sources belong to each tab. "daemon" is the control protocol itself: every
|
||||
// command in and every event out, whatever sent it.
|
||||
const LOG_TABS={
|
||||
all: null,
|
||||
daemon: t=>t==='ipx::io',
|
||||
scan: t=>t==='ipx::scan',
|
||||
web: t=>t==='ipx::http',
|
||||
};
|
||||
const LEVELS={ERROR:3,WARN:2,INFO:1,DEBUG:0,TRACE:0};
|
||||
|
||||
function logsModal(){
|
||||
logSeq=0; logLines=[];
|
||||
openModal(`<h3>Log</h3>
|
||||
<div class="logbar">
|
||||
<div class="tabs" id="logtabs">
|
||||
${Object.keys(LOG_TABS).map(t=>
|
||||
`<button data-t="${t}" class="${logTab===t?'on':''}">${
|
||||
{all:'All',daemon:'Daemon I/O',scan:'Scans',web:'HTTP'}[t]}</button>`).join('')}
|
||||
</div>
|
||||
<select id="loglevel" style="width:auto">
|
||||
<option value="">All levels</option>
|
||||
<option value="INFO">Info and above</option>
|
||||
@@ -789,9 +803,16 @@ function logsModal(){
|
||||
<button class="btn" onclick="closeModal()">Close</button>
|
||||
</div>
|
||||
<div id="logbox"><p class="empty">Loading…</p></div>
|
||||
<span class="hint">Live from the running daemon: feed scans, downloads, torrents and every
|
||||
HTTP request. Set <b>IPX_LOG=ipx=debug</b> for more detail.</span>`, true);
|
||||
<span class="hint"><b>Daemon I/O</b> is the control protocol itself — every command in and
|
||||
every event out. <b>Scans</b> is feed and download activity, <b>HTTP</b> is web requests.
|
||||
The buffer keeps debug detail even when the terminal does not; <b>IPX_UI_LOG</b> changes
|
||||
what it captures.</span>`, true);
|
||||
|
||||
$$('#logtabs button').forEach(b=>b.onclick=()=>{
|
||||
logTab=b.dataset.t;
|
||||
$$('#logtabs button').forEach(x=>x.classList.toggle('on',x.dataset.t===logTab));
|
||||
drawLog();
|
||||
});
|
||||
$('#loglevel').onchange=e=>{ logLevel=e.target.value; drawLog(); };
|
||||
$('#logq').oninput=e=>{ logFilter=e.target.value.toLowerCase(); drawLog(); };
|
||||
$('#logcopy').onclick=()=>copyText(visibleLog().map(l=>
|
||||
@@ -816,7 +837,9 @@ async function pollLog(){
|
||||
}
|
||||
function visibleLog(){
|
||||
const min=logLevel?LEVELS[logLevel]:-1;
|
||||
const tab=LOG_TABS[logTab];
|
||||
return logLines.filter(l=>
|
||||
(!tab || tab(l.target)) &&
|
||||
(LEVELS[l.level]??1)>=min &&
|
||||
(!logFilter || (l.msg+' '+l.target).toLowerCase().includes(logFilter)));
|
||||
}
|
||||
@@ -826,6 +849,12 @@ function drawLog(){
|
||||
const rows=visibleLog();
|
||||
box.innerHTML = rows.length ? rows.map(l=>{
|
||||
const t=new Date(l.ts*1000).toLocaleTimeString();
|
||||
if(logTab==='daemon'){
|
||||
const out=l.msg.startsWith('<-');
|
||||
return `<div class="l"><time>${t}</time>`+
|
||||
`<span class="lv" style="color:${out?'var(--good)':'var(--accent)'}">${out?'out':'in'}</span>`+
|
||||
`<span>${esc(l.msg.replace(/^[<-]+\s*/,''))}</span></div>`;
|
||||
}
|
||||
return `<div class="l"><time>${t}</time><span class="lv ${esc(l.level)}">${esc(l.level)}</span>`+
|
||||
`<span class="tg">${esc(l.target.replace(/^ipx::?/,''))}</span><span>${esc(l.msg)}</span></div>`;
|
||||
}).join('') : '<p class="empty">Nothing matches.</p>';
|
||||
@@ -855,10 +884,10 @@ $('#addFeed').onclick=()=>{
|
||||
};
|
||||
};
|
||||
|
||||
let collapsed = new Set(JSON.parse(localStorage.getItem('ipx.collapsed')||'[]'));
|
||||
let expanded = new Set(JSON.parse(localStorage.getItem('ipx.expanded')||'[]'));
|
||||
function toggleGroup(id){
|
||||
collapsed.has(id) ? collapsed.delete(id) : collapsed.add(id);
|
||||
try{ localStorage.setItem('ipx.collapsed', JSON.stringify([...collapsed])); }catch{}
|
||||
expanded.has(id) ? expanded.delete(id) : expanded.add(id);
|
||||
try{ localStorage.setItem('ipx.expanded', JSON.stringify([...expanded])); }catch{}
|
||||
renderFeeds();
|
||||
}
|
||||
let globalEvery = 60, globalMax = 3;
|
||||
|
||||
Reference in New Issue
Block a user