feat(dashboard): детали задачи по клику — метаданные, промоушены, отчёт закрытия, JS-раскрытие строки (t-260929220004-033c0f)

This commit is contained in:
Simbiot Harness 2026-09-30 06:42:08 +00:00
parent 2d70740e76
commit 62e9d756d7
2 changed files with 54 additions and 5 deletions

View file

@ -39,6 +39,15 @@
th{background:var(--ink);color:var(--paper);text-align:left;padding:8px 9px;font-family:ui-monospace,Menlo,monospace;font-size:10.5px;letter-spacing:.09em;text-transform:uppercase}
td{border-bottom:1px solid var(--line);padding:8px 9px;vertical-align:top}
tr:hover td{background:#fff3ea}
tr.task{cursor:pointer}
tr.task.open td{background:#fff3ea}
tr.taskdetail td{background:var(--card);border-bottom:1px solid var(--ink)}
.taskdetail .box{display:grid;grid-template-columns:150px 1fr;gap:4px 14px;padding:8px 2px 12px;font-size:13px}
.taskdetail .k{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--dim);padding-top:3px}
.taskdetail pre{white-space:pre-wrap;word-break:break-word;margin:0;font-family:ui-monospace,Menlo,monospace;font-size:12px;line-height:1.5}
.hint{color:var(--dim);font-size:12px;font-family:ui-monospace,Menlo,monospace}
.arrow{display:inline-block;transition:transform .15s;color:var(--dim);font-size:10px}
tr.task.open .arrow{transform:rotate(90deg)}
.m{font-family:ui-monospace,Menlo,monospace;font-size:11.5px}
.dim{color:var(--dim)}
@ -83,7 +92,7 @@
<div id="ladder"><div class="skel">загружаю…</div></div>
<h2>Задачи <span class="m dim" id="taskcount"></span></h2>
<div class="lead">сортировка: ready → running → failed → done, внутри — новые сверху</div>
<div class="lead">сортировка: ready → running → failed → done, внутри — новые сверху · <span class="hint">клик по строке — детали задачи</span></div>
<div id="tasks"><div class="skel">загружаю…</div></div>
<h2>Открытые эскалации <span class="m dim" id="esccount"></span></h2>
@ -120,6 +129,30 @@ $("refresh").textContent = REFRESH;
function pill(status){ return `<span class="pill ${esc(status)}">${esc(status)}</span>`; }
function size(s){ return `<span class="pill size${esc(s)}">${esc(s)}</span>`; }
// детали задачи по клику (т-260929220004): раскрытие строки без перезагрузки
const openTasks = new Set(); // id раскрытых строк (переживает автообновление)
function toggleTask(id){
if(openTasks.has(id)) openTasks.delete(id); else openTasks.add(id);
const el = document.getElementById("tr-" + id);
if(el) el.classList.toggle("open", openTasks.has(id));
const det = document.getElementById("det-" + id);
if(det) det.style.display = openTasks.has(id) ? "" : "none";
}
function taskDetail(t){
const row = [
["id", t.id],
["размер / статус", `${t.size} · ${t.status}${t.level ? " · уровень " + t.level : ""}`],
["создана", fmtTime(t.created)],
["завершена", t.finished ? fmtTime(t.finished) : "—"],
["session", t.session_id || "—"],
t.promoted_to ? ["промоушен", `${t.promotions || 1}× → ${t.promoted_to}`] : null,
t.error ? ["ошибка", t.error] : null,
t.report ? ["отчёт (" + (t.report_level || "?") + ")", t.report] : null,
].filter(Boolean);
const box = row.map(([k, v]) => `<div class="k">${esc(k)}</div><div><pre>${esc(v)}</pre></div>`).join("");
return `<tr class="taskdetail" id="det-${esc(t.id)}" style="display:${openTasks.has(t.id) ? "" : "none"}"><td colspan="7"><div class="box">${box}</div></td></tr>`;
}
function setConn(cls, text){ $("dot").className = "dot " + cls; $("conn").textContent = text; }
async function tick(){
@ -168,18 +201,20 @@ function render(s){
</tr>`).join("")}</table>`
: `<div class="skel">нет данных</div>`;
// задачи
// задачи (клик по строке — детали, т-260929220004)
$("taskcount").textContent = `(${s.tasks.length})`;
$("tasks").innerHTML = s.tasks.length ? `<table>
<tr><th>id</th><th>задача</th><th>размер</th><th>статус</th><th>уровень</th><th>создана</th></tr>
${s.tasks.map(t => `<tr>
<tr><th></th><th>id</th><th>задача</th><th>размер</th><th>статус</th><th>уровень</th><th>создана</th></tr>
${s.tasks.map(t => `<tr class="task${openTasks.has(t.id) ? " open" : ""}" id="tr-${esc(t.id)}" onclick="toggleTask('${esc(t.id)}')">
<td class="arrow">▶</td>
<td class="m dim">${esc(t.id)}</td>
<td><b>${esc(t.title)}</b>${t.error ? `<br><span class="m" style="color:var(--err)">${esc(String(t.error).slice(0,140))}</span>` : ""}</td>
<td>${size(t.size)}</td>
<td>${pill(t.status)}</td>
<td class="m">${esc(t.level || "—")}</td>
<td class="m dim">${fmtTime(t.created)}</td>
</tr>`).join("")}</table>`
</tr>
${taskDetail(t)}`).join("")}</table>`
: `<div class="skel">задач нет</div>`;
// эскалации

View file

@ -51,13 +51,27 @@ def build_state() -> dict:
"""
model = es.run_projections()
# отчёт тика, закрывшего задачу: digest с summary_ref == result_ref
digest_by_ref = {}
for d in (model.get("digests") or {}).values():
if d.get("summary_ref"):
digest_by_ref[d["summary_ref"]] = d
tasks = []
for t in model.get("tasks", {}).values():
report = digest_by_ref.get(t.get("result_ref")) or {}
tasks.append({
"id": t.get("id"), "title": t.get("title", ""),
"size": t.get("size", "?"), "status": t.get("status", "?"),
"level": t.get("level"), "created": t.get("created"),
"finished": t.get("finished"), "error": t.get("error"),
# детали для раскрытия по клику (т-260929220004)
"details": (t.get("details") or "")[:2000],
"promotions": t.get("promotions"), "promoted_to": t.get("promoted_to"),
"session_id": t.get("session_id"),
"report_level": report.get("level"),
"report_ts": report.get("ts"),
"report": (report.get("text") or "")[:1500],
})
# два стабильных прохода: новые сверху внутри статуса,
# статусы по порядку ready→running→failed→done