the-custodian/state-hub/dashboard/src/ui-feedback.md
tegwick 8ffebd2381 feat(dashboard): right-click improvement modal + UI Feedback page
- improvement-modal.js: global contextmenu handler that opens a modal
  showing page/widget context; submits suggestions as TD items with
  debt_type="dashboard-improvement" to POST /technical-debt/
- _footer.md: shared Observable footer that auto-initialises the modal
  on every dashboard page
- ui-feedback.md: review/approval page — lists open suggestions with
  resolve / won't-fix / in-progress action buttons; archived items shown
  below; live-polled
- observablehq.config.js: "UI Feedback" added under Workstreams group

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:47:59 +01:00

7.6 KiB

title
UI Feedback
import {API, POLL} from "./components/config.js";
const feedbackState = (async function*() {
  while (true) {
    let data = [], ok = false;
    try {
      const r = await fetch(`${API}/technical-debt/?debt_type=dashboard-improvement`);
      ok = r.ok;
      if (ok) {
        const items = await r.json();
        data = items
          .filter(t => t.debt_type === "dashboard-improvement")
          .sort((a, b) => {
            const st = {open: 0, in_progress: 1, deferred: 2, resolved: 3, wont_fix: 4};
            return (st[a.status] ?? 9) - (st[b.status] ?? 9);
          });
      }
    } catch {}
    yield {data, ok, ts: new Date()};
    await new Promise(res => setTimeout(res, POLL));
  }
})();
const data = feedbackState.data ?? [];
const _ok  = feedbackState.ok   ?? false;
const _ts  = feedbackState.ts;

UI Feedback

import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp}  from "./components/doc-overlay.js";

const _open       = data.filter(t => t.status === "open" || t.status === "in_progress");
const _resolved   = data.filter(t => t.status === "resolved");
const _wontFix    = data.filter(t => t.status === "wont_fix");

const _kpiBox = html`<div class="kpi-infobox">
  <div class="kpi-infobox-title">UI Feedback</div>
  <div class="kpi-row">
    <span class="kpi-row-label">open</span>
    <div class="kpi-row-right"><div class="kpi-row-value">${_open.length}</div></div>
  </div>
  <div class="kpi-row">
    <span class="kpi-row-label">resolved</span>
    <div class="kpi-row-right"><div class="kpi-row-value">${_resolved.length}</div></div>
  </div>
  <div class="kpi-row">
    <span class="kpi-row-label">won't fix</span>
    <div class="kpi-row-right"><div class="kpi-row-value">${_wontFix.length}</div></div>
  </div>
  <div class="kpi-row" style="border-top:1px solid var(--theme-foreground-faint,#eee)">
    <span class="kpi-row-label">total</span>
    <div class="kpi-row-right"><div class="kpi-row-value">${data.length}</div></div>
  </div>
</div>`;

const _liveEl = html`<div class="live-indicator">
  <span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
  ${_ok
    ? `Live · updated ${_ts?.toLocaleTimeString()}`
    : html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");

injectTocTop("fb-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);

Right-click any widget or section on any dashboard page to submit a suggestion. Items appear here for review and can be resolved or dismissed.

Open Suggestions

async function _setStatus(td_id, status) {
  try {
    const r = await fetch(`${API}/technical-debt/${td_id}`, {
      method: "PATCH",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({status}),
    });
    if (!r.ok) alert(`Failed to update status (HTTP ${r.status})`);
  } catch {
    alert("API unreachable");
  }
}
const _openItems = data.filter(t => t.status === "open" || t.status === "in_progress");

if (_openItems.length === 0) {
  display(html`<p class="dim">No open suggestions. Right-click any dashboard widget to submit one.</p>`);
} else {
  display(html`<div class="fb-list">${_openItems.map(t => {
    const card = html`<div class="fb-card fb-status-${t.status}">
      <div class="fb-card-header">
        ${t.td_id ? html`<span class="td-ref">${t.td_id}</span>` : ""}
        <span class="td-badge td-badge-${t.status}">${t.status.replace("_", " ")}</span>
        <span class="fb-location">${t.location ?? ""}</span>
        <div class="fb-actions">
          <button class="fb-btn fb-btn-resolve"
                  onclick=${async () => { await _setStatus(t.id, "resolved"); card.remove(); }}>
            ✓ resolve
          </button>
          <button class="fb-btn fb-btn-wontfix"
                  onclick=${async () => { await _setStatus(t.id, "wont_fix"); card.remove(); }}>
            ✕ won't fix
          </button>
          ${t.status === "open" ? html`<button class="fb-btn fb-btn-inprogress"
                  onclick=${async () => { await _setStatus(t.id, "in_progress"); card.style.opacity = "0.7"; }}>
            → in progress
          </button>` : ""}
        </div>
      </div>
      <div class="fb-suggestion">${t.description}</div>
    </div>`;
    return card;
  })}</div>`);
}

Resolved / Won't Fix

const _doneItems = data.filter(t => t.status === "resolved" || t.status === "wont_fix");

if (_doneItems.length === 0) {
  display(html`<p class="dim">Nothing resolved yet.</p>`);
} else {
  display(html`<div class="fb-list fb-done">${_doneItems.map(t => html`
    <div class="fb-card fb-status-${t.status}">
      <div class="fb-card-header">
        ${t.td_id ? html`<span class="td-ref">${t.td_id}</span>` : ""}
        <span class="td-badge td-badge-${t.status}">${t.status.replace("_", " ")}</span>
        <span class="fb-location">${t.location ?? ""}</span>
      </div>
      <div class="fb-suggestion">${t.description}</div>
    </div>
  `)}</div>`);
}