alphax/static/pipeline.html
2026-05-14 17:56:33 +08:00

79 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 链路日志{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link active admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %}
<style>
.shell{width:min(100% - 40px,1320px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:26px;font-weight:900;letter-spacing:-.6px;color:var(--ink)}.page-head p{margin-top:4px;color:var(--stone);font-size:13px}.head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.select,.btn-lite,.page-btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:800;color:var(--ink)}.btn-lite,.page-btn{cursor:pointer}.page-btn:disabled{opacity:.45;cursor:default}.pager{display:flex;align-items:center;gap:8px}.pager-info{font-size:12px;color:var(--stone);font-weight:800}.kpis{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:10px;margin-bottom:14px}.kpi{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);padding:12px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:11px;font-weight:900}.kpi b{display:block;margin-top:6px;color:var(--ink);font-size:24px;line-height:1;font-weight:900;letter-spacing:-.5px}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.layout{display:grid;grid-template-columns:420px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);min-width:0}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:800}.run-list{display:flex;flex-direction:column;max-height:calc(100vh - 246px);overflow:auto}.run-row{display:grid;grid-template-columns:1fr auto;gap:10px;padding:12px 14px;border-bottom:1px solid var(--hairline-soft);cursor:pointer;background:var(--canvas);transition:.12s}.run-row:hover{background:var(--surface)}.run-row.active{background:rgba(66,98,255,.06);box-shadow:inset 3px 0 0 var(--blue)}.run-main{min-width:0}.run-time{font-size:13px;font-weight:900;color:var(--ink)}.run-sub{margin-top:4px;color:var(--stone);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.status{display:inline-flex;align-items:center;height:22px;border-radius:var(--radius-full);padding:0 8px;font-size:11px;font-weight:900;background:var(--surface);color:var(--slate);border:1px solid var(--hairline-soft)}.status.ok{background:var(--green-light);color:var(--green);border-color:rgba(0,180,115,.18)}.status.err{background:var(--red-light);color:var(--red);border-color:rgba(229,62,62,.18)}.funnel{grid-column:1/-1;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px;margin-top:8px}.funnel div{background:var(--surface);border:1px solid var(--hairline-soft);border-radius:var(--radius-sm);padding:6px 7px;min-width:0}.funnel span{display:block;color:var(--stone);font-size:10px;font-weight:900}.funnel b{display:block;margin-top:2px;color:var(--ink);font-size:14px;font-weight:900}.detail{min-height:520px}.detail-body{padding:14px}.timeline{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin-bottom:14px}.stage{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px;min-width:0}.stage b{display:block;font-size:12px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage span{display:block;margin-top:4px;color:var(--stone);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage.ok{border-color:rgba(0,180,115,.18);background:rgba(0,180,115,.045)}.stage.err{border-color:rgba(229,62,62,.18);background:rgba(229,62,62,.045)}.chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}.chip{height:32px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-full);padding:0 10px;font-size:12px;font-weight:900;color:var(--slate);cursor:pointer}.chip.active{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}.summary-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}.mini{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:9px}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:3px;font-size:16px;color:var(--ink)}.table-wrap{overflow:auto;border:1px solid var(--hairline-soft);border-radius:var(--radius-md)}.table{width:100%;border-collapse:collapse;min-width:760px}.table th,.table td{padding:9px 10px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;vertical-align:top}.table th{color:var(--stone);font-size:11px;font-weight:900;background:var(--surface);position:sticky;top:0}.table td{color:var(--slate)}.table tr:last-child td{border-bottom:0}.sym{font-weight:900;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.num{font-weight:900;color:var(--ink)}.label{display:inline-flex;border-radius:var(--radius-full);padding:3px 8px;font-size:11px;font-weight:900;background:var(--surface);border:1px solid var(--hairline-soft);color:var(--slate);white-space:nowrap}.label.rec{background:rgba(66,98,255,.06);color:var(--blue);border-color:rgba(66,98,255,.15)}.label.win{background:var(--green-light);color:var(--green);border-color:rgba(0,180,115,.18)}.label.fail{background:var(--red-light);color:var(--red);border-color:rgba(229,62,62,.18)}.signals{max-width:320px;line-height:1.45}.empty{padding:38px 20px;text-align:center;color:var(--stone);font-size:13px}.loading{padding:22px;color:var(--stone);font-size:13px}.issue{color:var(--red);font-weight:800}.muted{color:var(--stone)}@media(max-width:1120px){.kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.layout{grid-template-columns:1fr}.run-list{max-height:none}.timeline{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:620px){.shell{width:min(100% - 24px,1320px);padding-top:18px}.kpis,.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.timeline{grid-template-columns:1fr}.funnel{grid-template-columns:repeat(2,minmax(0,1fr))}.page-head h1{font-size:22px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div><h1>链路日志</h1><p>按粗筛批次还原事件、粗筛、确认、推荐、跟踪与复盘结果。</p></div>
<div class="head-actions">
<select class="select" id="hoursSel" onchange="setHoursAndReload()"><option value="24">近 24h</option><option value="72">近 3 天</option><option value="168">近 7 天</option></select>
<button class="btn-lite" onclick="reloadRuns()">刷新</button>
</div>
</div>
<div class="kpis" id="kpis"><div class="loading">加载中...</div></div>
<div class="layout">
<section class="panel">
<div class="panel-head">
<div>
<div class="panel-title">批次</div>
<div class="panel-note" id="runCount">--</div>
</div>
<div class="pager">
<button class="page-btn" id="prevPageBtn" onclick="changePage(-1)">上一页</button>
<button class="page-btn" id="nextPageBtn" onclick="changePage(1)">下一页</button>
<span class="pager-info" id="pageInfo">--</span>
</div>
</div>
<div class="run-list" id="runList"><div class="loading">加载批次...</div></div>
</section>
<section class="panel detail">
<div class="panel-head"><div class="panel-title" id="detailTitle">批次详情</div><div class="panel-note" id="detailWindow">--</div></div>
<div class="detail-body" id="detailBody"><div class="empty">请选择一个批次</div></div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var state = { runs: [], selected: null, detail: null, filter: 'all', page: 1, limit: 12, totalPages: 0, totalCount: 0, offset: 0, hours: 24 };
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function $(id){return document.getElementById(id);}
function fmtTime(t){if(!t)return'--';var d=new Date(t);if(isNaN(d.getTime()))return t;return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');}
function fmtDur(ms){ms=Number(ms||0);if(ms>=60000)return Math.round(ms/60000)+'m';if(ms>=1000)return Math.round(ms/100)/10+'s';return ms+'ms';}
function pct(a,b){return b?((Number(a||0)/Number(b||1))*100).toFixed(1)+'%':'--';}
function statusCls(s){return s==='success'?'ok':'err';}
function label(text,cls){return '<span class="label '+(cls||'')+'">'+esc(text)+'</span>';}
function updatePager(){var info=$('pageInfo');var prev=$('prevPageBtn');var next=$('nextPageBtn');if(!info||!prev||!next)return;var totalPages=state.totalPages||0;info.textContent=totalPages?('第 '+state.page+' / '+totalPages+' 页,共 '+state.totalCount+' 批'):'暂无数据';prev.disabled=state.page<=1;next.disabled=!totalPages||state.page>=totalPages;}
function renderKpis(k){var items=[['批次数',k.run_count||0,''],['粗筛命中',k.rough_candidates||0,''],['细筛通过',k.fine_qualified||0,'blue'],['确认命中',k.confirm_hits||0,'blue'],['推荐生成',k.recommendations||0,'green'],['复盘成功',k.perf_success||0,'green'],['失败/漏选',(k.perf_failed||0)+' / '+(k.missed_count||0),'red']];$('kpis').innerHTML=items.map(function(x){return '<div class="kpi"><span>'+x[0]+'</span><b class="'+x[2]+'">'+x[1]+'</b></div>';}).join('');}
function renderRuns(){var list=state.runs||[];$('runCount').textContent='本页 '+list.length+' / 全部 '+state.totalCount+' 批';updatePager();if(!list.length){$('runList').innerHTML='<div class="empty">暂无粗筛批次</div>';return;}$('runList').innerHTML=list.map(function(r){var active=state.selected===r.run_id?' active':'';var notes=(r.issue_notes||[]).join(' / ')||'链路正常';return '<div class="run-row'+active+'" onclick="selectRun('+r.run_id+')"><div class="run-main"><div class="run-time">'+fmtTime(r.started_at)+'</div><div class="run-sub '+(r.issue_notes&&r.issue_notes.length?'issue':'')+'">'+esc(notes)+'</div></div><span class="status '+statusCls(r.run_status)+'">'+esc(r.result_status||r.run_status)+'</span><div class="funnel"><div><span>粗筛</span><b>'+r.rough_candidates+'</b></div><div><span>细筛</span><b>'+r.fine_qualified+'</b></div><div><span>确认</span><b>'+r.confirm_hits+'/'+r.confirm_processed+'</b></div><div><span>推荐</span><b>'+r.recommendations+'</b></div><div><span>绩效</span><b>'+r.perf_success+'/'+r.perf_failed+'</b></div></div></div>';}).join('');}
function resetSelection(){state.selected=null;state.detail=null;}
async function loadRuns(){try{$('runList').innerHTML='<div class="loading">加载批次...</div>';state.hours=Number($('hoursSel').value||24);var d=await (await fetch('/api/pipeline/runs?hours='+state.hours+'&limit='+state.limit+'&offset='+state.offset)).json();state.runs=d.runs||[];state.totalPages=(d.pagination&&d.pagination.total_pages)||0;state.totalCount=(d.pagination&&d.pagination.total_count)||0;state.page=(d.pagination&&d.pagination.page)||1;renderKpis(d.kpi||{});if(state.selected&&!state.runs.some(function(r){return r.run_id===state.selected;}))resetSelection();if(!state.selected&&state.runs[0])state.selected=state.runs[0].run_id;renderRuns();if(state.selected)selectRun(state.selected,true);else $('detailBody').innerHTML='<div class="empty">暂无可展示批次</div>';}catch(e){$('runList').innerHTML='<div class="empty">加载失败</div>';updatePager();}}
async function selectRun(id,quiet){state.selected=id;renderRuns();$('detailBody').innerHTML='<div class="loading">加载详情...</div>';try{var d=await (await fetch('/api/pipeline/runs/'+id)).json();state.detail=d;state.filter='all';renderDetail();}catch(e){$('detailBody').innerHTML='<div class="empty">详情加载失败</div>';}}
function changePage(step){if(!state.totalPages)return;var next=state.page+step;if(next<1||next>state.totalPages)return;state.offset=(next-1)*state.limit;resetSelection();loadRuns();}
function setHoursAndReload(){state.offset=0;state.page=1;resetSelection();loadRuns();}
function renderDetail(){var d=state.detail||{},s=d.summary||{};$('detailTitle').textContent='批次 #'+(s.run_id||s.id||'--')+' · '+fmtTime(s.started_at);$('detailWindow').textContent=fmtTime(s.window_start)+' - '+fmtTime(s.window_end);var timeline=(d.timeline||[]).map(function(t){return '<div class="stage '+statusCls(t.run_status)+'"><b>'+esc(t.stage)+'</b><span>'+fmtTime(t.started_at)+' · '+fmtDur(t.duration_ms)+'</span><span>'+esc(t.result_status||t.run_status)+'</span></div>';}).join('')||'<div class="empty">暂无阶段日志</div>';var counts=d.stage_counts||{};var chips=[['all','全部',0],['coarse','粗筛',counts.observation],['fine','细筛',counts.fine],['confirm','确认',counts.confirm_rejected],['recommendation','推荐',counts.recommendation],['review','复盘',counts.review_success+counts.review_failed],['missed','漏选',counts.missed]];var chipHtml=chips.map(function(c){return '<button class="chip '+(state.filter===c[0]?'active':'')+'" onclick="setFilter(\''+c[0]+'\')">'+c[1]+(c[2]?(' '+c[2]):'')+'</button>';}).join('');var minis=[['流程漏斗',s.rough_candidates+' / '+s.fine_qualified+' / '+s.confirm_hits+' / '+s.recommendations],['推荐转化',pct(s.recommendations,s.fine_qualified)],['复盘成功/失败',s.perf_success+' / '+s.perf_failed],['漏选',s.missed_count]].map(function(x){return '<div class="mini"><span>'+x[0]+'</span><b>'+x[1]+'</b></div>';}).join('');$('detailBody').innerHTML='<div class="timeline">'+timeline+'</div><div class="summary-grid">'+minis+'</div><div class="chips">'+chipHtml+'</div>'+renderRows();}
function setFilter(f){state.filter=f;renderDetail();}
function rowStage(item){if(item.kind==='screening')return item.layer==='粗筛'?'coarse':item.layer==='细筛'?'fine':'confirm';if(item.kind==='recommendation')return 'recommendation';if(item.kind==='review')return 'review';if(item.kind==='missed')return 'missed';return 'all';}
function collectRows(){var d=state.detail||{},rows=[];(d.screening_items||[]).forEach(function(x){rows.push({kind:'screening',layer:x.layer,time:x.scan_time,symbol:x.symbol,stage:x.stage_label,score:x.score,price:x.price,status:x.state,signals:x.signals||[],note:(x.detail_json&&x.detail_json.reason)||''});});(d.recommendations||[]).forEach(function(x){rows.push({kind:'recommendation',time:x.rec_time,symbol:x.symbol,stage:x.stage_label||'交易推荐',score:x.rec_score,price:x.entry_price,status:x.execution_label||x.action_status||x.status,signals:x.signal_labels&&x.signal_labels.length?x.signal_labels:(x.signals||[]),note:x.execution_reason||x.state_reason||''});});(d.reviews||[]).forEach(function(x){rows.push({kind:'review',time:x.review_time,symbol:x.symbol,stage:x.outcome==='爆发'?'复盘命中':(x.outcome==='失败'?'复盘失败':'复盘未验证'),score:'',price:x.pnl_48h,status:x.outcome,signals:x.hit_signals&&x.hit_signals.length?x.hit_signals:(x.triggered_signals||[]),note:x.lesson||''});});(d.missed_explosions||[]).forEach(function(x){rows.push({kind:'missed',time:x.detect_time,symbol:x.symbol,stage:'漏选',score:'',price:x.gain_pct,status:x.reason_missed||'漏选',signals:[],note:x.lesson||''});});rows.sort(function(a,b){return String(a.time||'').localeCompare(String(b.time||''));});return rows;}
function renderRows(){var rows=collectRows().filter(function(r){return state.filter==='all'||rowStage(r)===state.filter||(state.filter==='review'&&r.kind==='review');});if(!rows.length)return '<div class="empty">当前阶段暂无明细</div>';return '<div class="table-wrap"><table class="table"><thead><tr><th>时间</th><th>阶段</th><th>币种</th><th>分数/收益</th><th>价格/涨幅</th><th>状态</th><th>信号与说明</th></tr></thead><tbody>'+rows.map(function(r){var cls=r.kind==='recommendation'?'rec':r.stage==='复盘命中'?'win':(r.stage==='复盘失败'||r.kind==='missed')?'fail':'';var signals=(r.signals||[]).slice(0,5).join(' / ');var desc=[signals,r.note].filter(Boolean).join(' · ');return '<tr><td>'+fmtTime(r.time)+'</td><td>'+label(r.stage,cls)+'</td><td class="sym">'+esc(r.symbol||'--')+'</td><td class="num">'+esc(r.score===''?'--':r.score)+'</td><td class="num">'+esc(r.price==null?'--':r.price)+'</td><td>'+esc(r.status||'--')+'</td><td class="signals">'+esc(desc||'--')+'</td></tr>';}).join('')+'</tbody></table></div>';}
loadRuns();
</script>
{% endblock %}