diff --git a/harness/dashboard.html b/harness/dashboard.html
index 5efa290..6c8f905 100644
--- a/harness/dashboard.html
+++ b/harness/dashboard.html
@@ -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 @@
Задачи
- сортировка: ready → running → failed → done, внутри — новые сверху
+ сортировка: ready → running → failed → done, внутри — новые сверху · клик по строке — детали задачи
Открытые эскалации
@@ -120,6 +129,30 @@ $("refresh").textContent = REFRESH;
function pill(status){ return `${esc(status)}`; }
function size(s){ return `${esc(s)}`; }
+// детали задачи по клику (т-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]) => `${esc(k)}
`).join("");
+ return `${box} |
`;
+}
+
function setConn(cls, text){ $("dot").className = "dot " + cls; $("conn").textContent = text; }
async function tick(){
@@ -168,18 +201,20 @@ function render(s){
`).join("")}`
: `нет данных
`;
- // задачи
+ // задачи (клик по строке — детали, т-260929220004)
$("taskcount").textContent = `(${s.tasks.length})`;
$("tasks").innerHTML = s.tasks.length ? `
- | id | задача | размер | статус | уровень | создана |
- ${s.tasks.map(t => `
+
| id | задача | размер | статус | уровень | создана |
+ ${s.tasks.map(t => `
+ | ▶ |
${esc(t.id)} |
${esc(t.title)}${t.error ? ` ${esc(String(t.error).slice(0,140))}` : ""} |
${size(t.size)} |
${pill(t.status)} |
${esc(t.level || "—")} |
${fmtTime(t.created)} |
-
`).join("")}
`
+
+ ${taskDetail(t)}`).join("")}`
: `задач нет
`;
// эскалации
diff --git a/harness/dashboard.py b/harness/dashboard.py
index e338ae4..2e2fe95 100644
--- a/harness/dashboard.py
+++ b/harness/dashboard.py
@@ -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