// ================== 「日志 → 步骤明细」子分栏 ================== // 数据源:task_step_log 表(结构化),接口 /api/step_logs*(见 web/admin_api.py)。 // 与「文件日志」的分工:那边是原始文本,这边能按设备/任务/时间/结果过滤、能导出。 let _slOffset = 0; // 分页偏移 let _slTotal = 0; // 命中总数 let _slRunId = ''; // "只看某次运行"时的 run_id(空 = 不限) let _slInited = false; // 下拉选项只拉一次 function _slVal(id){const el=document.getElementById(id);return el?el.value:'';} function _slParams(){ const p=new URLSearchParams(); const put=(k,v)=>{if(v)p.set(k,v);}; put('serial', _slVal('sl-device')); put('job_id', _slVal('sl-job')); put('result', _slVal('sl-result')); put('q', _slVal('sl-q').trim()); put('since', _slVal('sl-since')); put('until', _slVal('sl-until')); put('run_id', _slRunId); p.set('limit', _slVal('sl-limit')||200); p.set('offset', _slOffset); return p; } async function loadStepLogs(reset){ const body=document.getElementById('sl-body'); if(!body)return; if(reset)_slOffset=0; const p=_slParams(); const r=await apiGet('/api/step_logs?'+p.toString()); if(!r||!r.ok){body.innerHTML='读取失败';return;} _slTotal=r.total||0; const rows=r.rows||[]; body.innerHTML = rows.length ? rows.map(_slRowHtml).join('') : '(没有匹配的步骤记录——先跑一个任务,' +'或在上面放宽筛选条件)'; // 提示栏:分页位置 + 保留期 + 运行期间丢弃计数(排障用) const from=_slTotal?(_slOffset+1):0, to=_slOffset+rows.length; let hint=`命中 ${_slTotal} 条 · 显示 ${from}-${to}`; if(_slRunId)hint=`只看运行 ${_slRunId} · `+hint; const st=r.stats||{}; if(st.dropped)hint+=` · 队列溢出丢弃 ${st.dropped} 条`; if(st.failed)hint+=` · 落库失败 ${st.failed} 条`; document.getElementById('sl-hint').textContent=hint; document.getElementById('sl-page').textContent= `第 ${Math.floor(_slOffset/(parseInt(_slVal('sl-limit'),10)||200))+1} 页`; document.getElementById('sl-prev').disabled = _slOffset<=0; document.getElementById('sl-next').disabled = _slOffset+rows.length>=_slTotal; if(!_slInited){_slInited=true;loadStepFilters();} if(reset)loadStepRuns(); } // 结果 → 颜色。注意不能复用 .log-err/.log-warn/.log-info:那套的作用域是 // .log-content(文件日志面板),表格里得用下面这套 res-* const _SL_RESULT={error:'res-error',unknown:'res-error',miss:'res-miss', cap:'res-miss',skip:'res-skip',ok:'res-ok'}; function _slRowHtml(r){ const cls=_SL_RESULT[r.result]||''; const dev=esc(r.device_name||r.serial||''); const dur=r.duration_ms>=1000?(r.duration_ms/1000).toFixed(1)+'s':(r.duration_ms||0)+'ms'; return '' +''+esc(r.created_at||'')+'' +''+dev+'' +''+esc(r.job_name||'')+'' +''+esc(r.step_path||'')+'' +''+esc(r.step_label||'')+'' +''+esc(r.step_type||'')+'' +''+esc(r.result||'')+'' +''+dur+'' +''+esc(_slTrim(r.detail,120))+'' +''; } function _slTrim(s,n){s=s||'';return s.length>n?s.slice(0,n)+'…':s;} function stepLogPage(delta){ const size=parseInt(_slVal('sl-limit'),10)||200; _slOffset=Math.max(0,_slOffset+delta*size); loadStepLogs(false); } function resetStepFilters(){ ['sl-q','sl-since','sl-until'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); ['sl-device','sl-job','sl-result'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); _slRunId=''; loadStepLogs(true); } function downloadStepLogs(){ const p=_slParams(); p.delete('limit');p.delete('offset'); window.location='/api/step_logs/download?'+p.toString(); } // ---------- 筛选下拉(只列"明细里真的出现过"的设备/任务) ---------- async function loadStepFilters(){ const r=await apiGet('/api/step_logs/filters'); if(!r||!r.ok)return; const dev=document.getElementById('sl-device'); const job=document.getElementById('sl-job'); const keep=(sel,val)=>{const cur=sel.value;sel.innerHTML=val;sel.value=cur;}; keep(dev,''+(r.devices||[]).map(d=> '').join('')); keep(job,''+(r.jobs||[]).map(j=> '').join('')); document.getElementById('sl-note').textContent= `明细保留 ${r.keep_days} 天(超期自动清理);单次运行最多记录 ` +`${r.max_rows_per_run} 条(无限循环任务不会写爆这张表)。`; } // ---------- 最近运行概览(点一行 = 只看那次运行) ---------- async function loadStepRuns(){ const p=new URLSearchParams(); if(_slVal('sl-device'))p.set('serial',_slVal('sl-device')); if(_slVal('sl-job'))p.set('job_id',_slVal('sl-job')); if(_slVal('sl-since'))p.set('since',_slVal('sl-since')); if(_slVal('sl-until'))p.set('until',_slVal('sl-until')); p.set('limit','20'); const r=await apiGet('/api/step_logs/runs?'+p.toString()); const body=document.getElementById('sl-runs-body'); if(!r||!r.ok){body.innerHTML='';return;} const runs=r.runs||[]; document.getElementById('sl-runs-summary').textContent= `最近运行概览(${runs.length} 次,点「查看」只看那次)`; body.innerHTML = runs.length ? runs.map(x=>'' +''+esc(x.started_at||'')+'' +''+esc(x.device_name||x.serial||'')+'' +''+esc(x.job_name||'')+'' +''+x.steps+'' +''+(x.failures||0)+'' +'' +'').join('') : '(暂无运行记录)'; } function filterByRun(runId){ _slRunId=runId||''; document.getElementById('sl-runs-box').open=false; loadStepLogs(true); }