fix(web): 进度页时间线越界+因子任务日志空(用户08-30验收两处) [vps]
CI/CD / test (push) Failing after 14m58s
CI/CD / nas-deploy (push) Has been skipped
CI/CD / nas-verify (push) Has been skipped

①时间线:原「末节点flex 0 0 auto+bar宽100%相对本节点」=4号点贴屏幕右缘、
3号bar越界伸出容器 → 全节点等宽flex:1(dot落各等分区中心),bar改
left:calc(50%+16px)/width:calc(100%-32px)精确连接相邻dot。
②因子任务日志空:log端点读的{db目录}/{tid}.log因子任务从不写 → 心跳
追加流水:_report_progress同时append [HH:MM:SS] stage·detail 到
{tid}.progress.log,/task/{id}/log factor_前缀优先返流水(行情加载i/N、
因子i/M、tears✓即用户要看的运行过程);189测+build绿
This commit is contained in:
2026-08-30 10:46:05 +08:00
parent 20be2d5152
commit ad2270821c
3 changed files with 29 additions and 7 deletions
+7 -5
View File
@@ -180,7 +180,6 @@ const pageTitle = computed(() => (isFactor.value ? '因子分析' : isOptimize.v
v-for="(s, i) in (isFactor ? fxSteps : btSteps)" :key="s.key"
class="tl-node"
:class="[isFactor ? fxStepState(i) : btStepState(i), { failed: status === 'failed' && i === (isFactor ? fxCurrentIdx : btCurrentIdx) }]"
:style="{ flex: i === (isFactor ? fxSteps : btSteps).length - 1 ? '0 0 auto' : '1' }"
>
<div class="tl-dot">
<span v-if="(isFactor ? fxStepState(i) : btStepState(i)) === 'done'"></span>
@@ -242,19 +241,22 @@ const pageTitle = computed(() => (isFactor.value ? '因子分析' : isOptimize.v
.hb-pct { font-size: 30px; font-weight: 700; color: var(--brand); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.hb-pct .pct-sign { font-size: 14px; color: var(--text-3); }
/* ===== 时间线 ===== */
/* ===== 时间线 =====
全节点等宽(flex:1)→dot 落在各等分区中心;连接线从本区 dot 右 16px 到
下一区 dot 左 16px(等宽下精确对齐;原「末节点不伸缩+bar 宽 100% 越界」
会把 4 顶到屏幕右缘且 bar 伸出容器)。 */
.timeline { display: flex; align-items: flex-start; }
.tl-node { display: flex; flex-direction: column; align-items: center; position: relative; padding: 0 8px; }
.tl-node { flex: 1; display: flex; flex-direction: column; align-items: center; position: relative; padding: 0 8px; min-width: 0; }
.tl-dot {
width: 26px; height: 26px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
background: var(--bg-hover); color: var(--text-3);
border: 1px solid var(--border-2);
z-index: 1;
z-index: 1; flex-shrink: 0;
}
.tl-label { margin-top: 8px; font-size: 12px; color: var(--text-3); white-space: nowrap; }
.tl-bar { position: absolute; top: 13px; left: 50%; width: 100%; height: 2px; background: var(--border-2); z-index: 0; }
.tl-bar { position: absolute; top: 13px; left: calc(50% + 16px); width: calc(100% - 32px); height: 2px; background: var(--border-2); z-index: 0; }
.tl-node.done .tl-dot { background: var(--down); color: #fff; border-color: var(--down); }
.tl-node.done .tl-label { color: var(--text); }
.tl-node.done .tl-bar { background: var(--down); }
+15 -1
View File
@@ -730,7 +730,21 @@ def daily_holdings(task_id: str):
@router.get("/task/{task_id}/log", dependencies=[Depends(verify_token)])
def log_endpoint(task_id: str):
"""Get backtest log text."""
"""Get backtest log text.
因子任务无传统日志文件 → 返回心跳流水(progress.log,由 analyzer
_report_progress 逐条 append),行情加载 i/N / 因子 i/M / tears ✓
即用户要看的"过程"
"""
if task_id.startswith("factor_"):
try:
lp = os.path.join(_FACTOR_PROGRESS_DIR, f"{task_id}.progress.log")
if os.path.exists(lp):
with open(lp, encoding="utf-8") as f:
return {"task_id": task_id, "log": f.read()}
except Exception:
pass
return {"task_id": task_id, "log": ""}
orch = get_orchestrator()
if hasattr(orch, 'db_path') and orch.db_path:
file_dir = os.path.dirname(os.path.abspath(orch.db_path))
+7 -1
View File
@@ -62,7 +62,9 @@ class FactorReport:
def _report_progress(task_id: str, output_dir: str, stage: str, detail: str) -> None:
"""跨进程进度心跳:worker 写 {output_dir}/{task_id}.progress,
GET /task/{id} 读它合并返回(前端显示"最近活动 Xs 前 · detail")
GET /task/{id} 读它合并返回(前端显示"最近活动 Xs 前 · detail");
同时 append 一行到 {task_id}.progress.log 作活动流水因子任务
没有传统日志文件,心跳流水就是 /task/{id}/log 的内容
失败静默进度是可观测性锦上添花,不能影响分析主流程"""
if not task_id:
return
@@ -72,6 +74,10 @@ def _report_progress(task_id: str, output_dir: str, stage: str, detail: str) ->
p = os.path.join(output_dir, f"{task_id}.progress")
with open(p, "w", encoding="utf-8") as f:
f.write(_j.dumps({"stage": stage, "detail": detail, "ts": _t.time()}))
lp = os.path.join(output_dir, f"{task_id}.progress.log")
stamp = _t.strftime("%H:%M:%S")
with open(lp, "a", encoding="utf-8") as f:
f.write(f"[{stamp}] {stage} · {detail}\n")
except Exception:
pass