Add a log view, and Docker packaging
The Log button shows the running daemon live: feed scans, downloads, torrents and every HTTP request. It reads a ring buffer filled by a tracing layer rather than tailing a file, so it works under Docker where logs go to stdout. The access-log middleware skips /api/logs, or the panel's poll would log itself forever. Detached torrents could leave a row stuck in 'downloading' across a restart, where nothing would ever revisit it; those are requeued at startup. Dockerfile, entrypoint and compose: 114 MB runtime, config bound to 0.0.0.0 on first run since container loopback is unreachable, drops to PUID:PGID for Unraid, and a healthcheck that goes through the control socket so a wedged worker reads as unhealthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -217,6 +217,19 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
||||
padding:20px;width:min(460px,100%);box-shadow:var(--shadow);max-height:88vh;overflow:auto;
|
||||
}
|
||||
.card.wide{width:min(1000px,100%)}
|
||||
#logbox{
|
||||
background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:10px 12px;
|
||||
height:min(60vh,520px);overflow:auto;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
|
||||
}
|
||||
#logbox .l{display:flex;gap:9px;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
#logbox time{color:var(--faint);flex:none}
|
||||
#logbox .lv{flex:none;width:42px;font-weight:700}
|
||||
#logbox .lv.ERROR{color:var(--bad)} #logbox .lv.WARN{color:var(--warn)}
|
||||
#logbox .lv.INFO{color:var(--accent)} #logbox .lv.DEBUG,#logbox .lv.TRACE{color:var(--faint)}
|
||||
#logbox .tg{color:var(--faint);flex:none}
|
||||
.logbar{display:flex;gap:8px;align-items:center;margin-bottom:9px;flex-wrap:wrap}
|
||||
.logbar .grow{flex:1;min-width:120px}
|
||||
.card h3{margin:0 0 14px;font-size:17px}
|
||||
.field{display:grid;gap:4px;margin-bottom:12px}
|
||||
.field label{font-size:12px;color:var(--dim)}
|
||||
@@ -263,6 +276,7 @@ input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radiu
|
||||
<button id="addFeed">+ Feed</button>
|
||||
<button id="scanAll">Scan all</button>
|
||||
<button id="opml" title="Import / export OPML">OPML</button>
|
||||
<button id="logs" title="Daemon and web log">Log</button>
|
||||
</div>
|
||||
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
||||
<div id="feedlist"></div>
|
||||
@@ -647,8 +661,78 @@ document.addEventListener('keydown',ev=>{
|
||||
});
|
||||
|
||||
/* ---------------- modals ---------------- */
|
||||
function openModal(html){ $('#modalCard').innerHTML=html; $('#modal').classList.add('on'); }
|
||||
function closeModal(){ $('#modal').classList.remove('on'); }
|
||||
function openModal(html,wide){
|
||||
$('#modalCard').innerHTML=html;
|
||||
$('#modalCard').classList.toggle('wide',!!wide);
|
||||
$('#modal').classList.add('on');
|
||||
}
|
||||
function closeModal(){
|
||||
$('#modal').classList.remove('on');
|
||||
if(logTimer){ clearInterval(logTimer); logTimer=null; }
|
||||
}
|
||||
|
||||
/* ---------------- log view ---------------- */
|
||||
let logTimer=null, logSeq=0, logLines=[], logFilter='', logLevel='';
|
||||
const LEVELS={ERROR:3,WARN:2,INFO:1,DEBUG:0,TRACE:0};
|
||||
|
||||
function logsModal(){
|
||||
logSeq=0; logLines=[];
|
||||
openModal(`<h3>Log</h3>
|
||||
<div class="logbar">
|
||||
<select id="loglevel" style="width:auto">
|
||||
<option value="">All levels</option>
|
||||
<option value="INFO">Info and above</option>
|
||||
<option value="WARN">Warnings and errors</option>
|
||||
<option value="ERROR">Errors only</option>
|
||||
</select>
|
||||
<input type="search" id="logq" class="grow" placeholder="Filter…">
|
||||
<label class="check" style="margin:0"><input type="checkbox" id="logfollow" checked> Follow</label>
|
||||
<button class="btn" id="logcopy">Copy</button>
|
||||
<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);
|
||||
|
||||
$('#loglevel').onchange=e=>{ logLevel=e.target.value; drawLog(); };
|
||||
$('#logq').oninput=e=>{ 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'));
|
||||
|
||||
pollLog();
|
||||
logTimer=setInterval(pollLog,2000);
|
||||
}
|
||||
|
||||
async function pollLog(){
|
||||
try{
|
||||
const r=await api(`/api/logs?after=${logSeq}&limit=500`);
|
||||
if(r.lines.length){
|
||||
logLines=logLines.concat(r.lines).slice(-2000);
|
||||
logSeq=r.latest;
|
||||
drawLog();
|
||||
}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>`;
|
||||
}
|
||||
}
|
||||
function visibleLog(){
|
||||
const min=logLevel?LEVELS[logLevel]:-1;
|
||||
return logLines.filter(l=>
|
||||
(LEVELS[l.level]??1)>=min &&
|
||||
(!logFilter || (l.msg+' '+l.target).toLowerCase().includes(logFilter)));
|
||||
}
|
||||
function drawLog(){
|
||||
const box=$('#logbox'); if(!box) return;
|
||||
const follow=$('#logfollow')?.checked;
|
||||
const rows=visibleLog();
|
||||
box.innerHTML = rows.length ? rows.map(l=>{
|
||||
const t=new Date(l.ts*1000).toLocaleTimeString();
|
||||
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>';
|
||||
if(follow) box.scrollTop=box.scrollHeight;
|
||||
}
|
||||
$('#modal').onclick=e=>{ if(e.target.id==='modal') closeModal(); };
|
||||
|
||||
$('#addFeed').onclick=()=>{
|
||||
@@ -842,6 +926,7 @@ 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('#logs','onclick',logsModal);
|
||||
$('#feedFilter').oninput=renderFeeds;
|
||||
$('#burger').onclick=()=>$('#sidebar').classList.toggle('open');
|
||||
$('#theme').onclick=()=>{
|
||||
|
||||
Reference in New Issue
Block a user