Files
state-hub/dashboard/src/wsjf-triage.md
tegwick 166aedfa8d feat: add workplan aliases and legacy meter
Adds preferred workplan REST/event surfaces, legacy-meter telemetry and weekly review summaries, documentation/dashboard terminology updates, dashboard API loading fixes, and close-out sync for STATE-WP-0052 and STATE-WP-0054.
2026-06-04 08:25:31 +02:00

12 KiB

title
title
Daily WSJF Triage
import {POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
import {
  ACTION_META,
  actionMeta,
  buildCandidateIndex,
  buildPatternRows,
  isWithinDays,
  normalizeTriageReports,
  resolveCandidate,
  topAction,
  truncateSummary,
} from "./components/wsjf-triage.js";
const triageState = (async function*() {
  let failures = 0;
  while (true) {
    let events = [], workplanIndex = {workstreams: {}}, ok = false;
    try {
      const [reportsResp, indexResp] = await Promise.all([
        apiFetch("/progress/?event_type=daily_triage&limit=14"),
        apiFetch("/workplans/index"),
      ]);
      ok = reportsResp.ok && indexResp.ok;
      events = reportsResp.ok ? await reportsResp.json() : [];
      workplanIndex = indexResp.ok ? await indexResp.json() : {workstreams: {}};
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {events, workplanIndex, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
  }
})();
const reports = normalizeTriageReports(triageState.events ?? []);
const candidateIndex = buildCandidateIndex(triageState.workplanIndex ?? {workstreams: {}});
const _ok = triageState.ok ?? false;
const _ts = triageState.ts;
const latestReport = reports[0] ?? null;

Daily WSJF Triage

function fmtDateTime(iso) {
  if (!iso) return "-";
  const d = new Date(iso);
  return Number.isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
}

function fmtDate(iso) {
  if (!iso) return "-";
  const d = new Date(iso);
  return Number.isNaN(d.getTime()) ? String(iso).slice(0, 10) : d.toLocaleDateString(undefined, {year: "numeric", month: "short", day: "numeric"});
}

function candidateNode(candidate, index) {
  const resolved = resolveCandidate(candidate, index);
  return resolved
    ? html`<a href="/workstreams/${resolved.id}" title=${resolved.filename ?? resolved.id}>${candidate}</a>`
    : html`<span>${candidate || "-"}</span>`;
}

function actionBadge(action, extraText = "") {
  const meta = actionMeta(action);
  return html`<span class="triage-action triage-action-${meta.tone}" title=${meta.description}>${meta.label}${extraText}</span>`;
}

function recommendationTable(report, index) {
  const recommendations = report.recommendations ?? [];
  if (recommendations.length === 0) {
    return html`<p class="triage-muted">No recommendations were recorded for this report.</p>`;
  }
  return html`<div>
    <table class="triage-table triage-recommendations">
      <thead><tr><th>#</th><th>Candidate</th><th>Action</th><th>Confidence</th><th>Why</th></tr></thead>
      <tbody>${recommendations.map(rec => html`<tr>
        <td>${rec.rank}</td>
        <td>${candidateNode(rec.candidate, index)}</td>
        <td>${actionBadge(rec.action)}</td>
        <td><span class="triage-confidence">${rec.confidence}</span></td>
        <td>${rec.why || "-"}</td>
      </tr>`)}</tbody>
    </table>
    <div class="triage-legend">
      ${Object.values(ACTION_META).map(meta => html`<span>${actionBadge(meta.label)} ${meta.description}</span>`)}
    </div>
  </div>`;
}

function reportMetadata(report) {
  return html`<div class="triage-metadata">
    <div><span>Scheduled for</span><strong>${fmtDateTime(report.scheduled_for)}</strong></div>
    <div><span>Created</span><strong>${fmtDateTime(report.created_at)}</strong></div>
    <div><span>Instruction</span><strong>${report.instruction_id ?? "-"}</strong></div>
    <div><span>Activity run</span><strong>${report.activity_core_run_id ?? "-"}</strong></div>
    <div><span>Activity</span><strong>${report.activity_id ?? "-"}</strong></div>
    <div><span>Memory note</span><strong>${report.memory_path ?? "-"}</strong></div>
  </div>`;
}

function renderReportDetail(report, index) {
  return html`<div>
    <div class="triage-section-heading">
      <h2>Report Detail</h2>
      <span>${fmtDate(report.scheduled_for ?? report.created_at)}</span>
    </div>
    <section class="triage-detail-block">
      <h3>Summary</h3>
      <p>${report.summary || "-"}</p>
    </section>
    <section class="triage-detail-block">
      <h3>Recommendations</h3>
      ${recommendationTable(report, index)}
    </section>
    <section class="triage-detail-block">
      <h3>Run Metadata</h3>
      ${reportMetadata(report)}
    </section>
  </div>`;
}

function renderPatterns(reports, index) {
  const windowReports = reports.filter(report => isWithinDays(report.created_at, 14));
  const rows = buildPatternRows(windowReports);
  return html`<section class="triage-section">
    <div class="triage-section-heading">
      <h2>Patterns</h2>
      <span>Last 14 days</span>
    </div>
    ${rows.length === 0
      ? html`<p class="triage-muted">No repeated recommendations are visible in the loaded 14-day window.</p>`
      : html`<table class="triage-table">
        <thead><tr><th>Workstream</th><th>Times Recommended</th><th>Most Frequent Action</th></tr></thead>
        <tbody>${rows.map(row => html`<tr>
          <td>${candidateNode(row.candidate, index)}</td>
          <td>${row.count} / ${Math.max(1, windowReports.length)} reports</td>
          <td>${actionBadge(row.action, ` x${row.actionCount}`)}</td>
        </tr>`)}</tbody>
      </table>`}
  </section>`;
}

function renderExplorer(reports, index) {
  const root = html`<div class="triage-explorer"></div>`;
  const detail = html`<section id="triage-report-detail" class="triage-section"></section>`;
  const tableBody = html`<tbody></tbody>`;
  const rows = [];
  let selectedId = reports[0]?.id;

  function selectReport(report, {scroll = true} = {}) {
    selectedId = report.id;
    for (const row of rows) {
      row.classList.toggle("is-selected", row.dataset.reportId === selectedId);
      row.setAttribute("aria-selected", row.dataset.reportId === selectedId ? "true" : "false");
    }
    detail.replaceChildren(renderReportDetail(report, index));
    if (scroll) detail.scrollIntoView({behavior: "smooth", block: "start"});
  }

  for (const report of reports) {
    const top = topAction(report.recommendations);
    const row = html`<tr class="triage-report-row" tabindex="0" role="button" data-report-id=${report.id} aria-selected="false">
      <td>${fmtDate(report.scheduled_for ?? report.created_at)}</td>
      <td>${truncateSummary(report.summary)}</td>
      <td>${report.recommendations.length}</td>
      <td>${top ? actionBadge(top.action, ` x${top.count}`) : "-"}</td>
    </tr>`;
    row.addEventListener("click", () => selectReport(report));
    row.addEventListener("keydown", event => {
      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        selectReport(report);
      }
    });
    rows.push(row);
  }
  tableBody.append(...rows);

  root.append(
    html`<section class="triage-section">
      <div class="triage-section-heading">
        <h2>Recent Reports</h2>
        <span>${reports.length} loaded</span>
      </div>
      <table class="triage-table triage-reports">
        <thead><tr><th>Date</th><th>Summary</th><th># Recs</th><th>Top Action</th></tr></thead>
        ${tableBody}
      </table>
    </section>`,
    detail,
    renderPatterns(reports, index),
  );

  if (reports[0]) selectReport(reports[0], {scroll: false});
  return root;
}
const _liveEl = html`<div class="live-indicator">
  <span class="live-dot" style="background:${_ok ? "var(--theme-foreground-focus)" : "red"}"></span>
  ${_ok
    ? `Live - updated ${_ts?.toLocaleTimeString()} - ${reports.length} triage reports`
    : html`<span style="color:red">Offline - run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);

const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/wsjf-triage"); }

display(html`<p class="triage-subtitle">Daily State Hub triage from activity-core. Recommendations are advisory; the operator and workplan owners decide what to act on.</p>`);
display(html`<div class="triage-latest">
  <span>Last updated</span>
  <strong>${latestReport ? fmtDateTime(latestReport.created_at) : "No daily_triage events yet"}</strong>
</div>`);

if (reports.length === 0) {
  const empty = html`<div class="triage-empty">
    <h2>No daily triage reports yet.</h2>
    <p>The next run is scheduled for 07:20 Europe/Berlin (activity-core <code>daily-statehub-wsjf-triage</code>).</p>
  </div>`;
  withDocHelp(empty, "/docs/wsjf-triage");
  display(empty);
} else {
  display(renderExplorer(reports, candidateIndex));
}
<style> .live-indicator { color: gray; font-size: 0.8rem; margin-bottom: 0.75rem; padding: 0.55rem 1.8rem 0.55rem 0.7rem; position: relative; } .live-dot { border-radius: 50%; display: inline-block; height: 0.5rem; margin-right: 0.35rem; width: 0.5rem; } .triage-subtitle { color: var(--theme-foreground-muted, #666); margin-top: -0.2rem; max-width: 820px; } .triage-latest { align-items: baseline; border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 6px; display: inline-flex; gap: 0.45rem; margin: 0.2rem 0 1rem; padding: 0.35rem 0.55rem; } .triage-latest span, .triage-section-heading span, .triage-metadata span { color: var(--theme-foreground-muted, #666); font-size: 0.72rem; text-transform: uppercase; } .triage-empty { border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 6px; margin-top: 1rem; max-width: 760px; padding: 1rem; position: relative; } .triage-empty h2 { margin-top: 0; } .triage-section { margin: 1.4rem 0 1.8rem; } .triage-section-heading { align-items: baseline; display: flex; gap: 0.65rem; justify-content: space-between; margin-bottom: 0.45rem; } .triage-section-heading h2 { margin: 0; } .triage-detail-block { margin: 0.9rem 0 1.2rem; } .triage-detail-block h3 { font-size: 0.95rem; margin-bottom: 0.35rem; } .triage-table { border-collapse: collapse; width: 100%; } .triage-table th, .triage-table td { border-bottom: 1px solid var(--theme-foreground-faint, #ddd); padding: 0.45rem 0.5rem; text-align: left; vertical-align: top; } .triage-reports td:nth-child(2), .triage-recommendations td:nth-child(5) { max-width: 520px; } .triage-report-row { cursor: pointer; } .triage-report-row:hover, .triage-report-row:focus { background: var(--theme-background-alt, #f7f7f7); outline: none; } .triage-report-row.is-selected { background: color-mix(in srgb, var(--theme-foreground-focus, steelblue) 9%, transparent); } .triage-action { border-radius: 4px; display: inline-block; font-size: 0.74rem; font-weight: 700; line-height: 1.2; padding: 0.14rem 0.4rem; white-space: nowrap; } .triage-action-good { background: #dcfce7; color: #166534; } .triage-action-warn { background: #fef3c7; color: #92400e; } .triage-action-muted { background: #f3f4f6; color: #4b5563; } .triage-action-bad { background: #fee2e2; color: #991b1b; } .triage-confidence { color: var(--theme-foreground-muted, #666); font-size: 0.82rem; } .triage-legend { color: var(--theme-foreground-muted, #666); display: flex; flex-wrap: wrap; gap: 0.45rem 0.8rem; margin-top: 0.6rem; } .triage-legend span { font-size: 0.76rem; } .triage-metadata { display: grid; gap: 0.65rem 1rem; grid-template-columns: repeat(2, minmax(0, 1fr)); } .triage-metadata div { min-width: 0; } .triage-metadata strong { display: block; font-size: 0.86rem; overflow-wrap: anywhere; } .triage-muted { color: var(--theme-foreground-muted, #666); } @media (max-width: 760px) { .triage-section-heading, .triage-latest { align-items: flex-start; flex-direction: column; } .triage-metadata { grid-template-columns: 1fr; } } </style>