---
title: UI Feedback
---
```js
import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
```
```js
// Ordered workflow steps for dashboard-improvement suggestions
const STEPS = ["submitted", "analyse", "plan", "implement", "test", "review", "finished"];
const STEP_LABEL = {
submitted: "Submitted",
analyse: "Analyse",
plan: "Plan",
implement: "Implement",
test: "Test",
review: "Review",
finished: "Finished",
addressed: "Addressed",
resolved: "Resolved",
wont_fix: "Won't Fix",
};
const STEP_COLOR = {
submitted: "#94a3b8",
analyse: "#3b82f6",
plan: "#8b5cf6",
implement: "#f59e0b",
test: "#06b6d4",
review: "#6366f1",
finished: "#16a34a",
addressed: "#16a34a",
resolved: "#16a34a",
wont_fix: "#9ca3af",
};
const STEP_HINT = {
submitted: "New suggestion — not yet triaged.",
analyse: "Investigating the issue and its scope.",
plan: "Designing the solution approach.",
implement: "Building the change.",
test: "Verifying the implementation works correctly.",
review: "Awaiting review by the original suggester.",
finished: "Shipped and confirmed.",
addressed: "Implemented before the current workflow labels existed.",
resolved: "Resolved before the current workflow labels existed.",
wont_fix: "Decided not to implement.",
};
const CLOSED_STATUSES = new Set(["finished", "addressed", "resolved", "wont_fix"]);
function nextStep(current) {
const i = STEPS.indexOf(current);
return i >= 0 && i < STEPS.length - 1 ? STEPS[i + 1] : null;
}
```
```js
const feedbackState = (async function*() {
let failures = 0;
while (true) {
let data = [], ok = false;
try {
const r = await apiFetch("/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 = {submitted:0, analyse:1, plan:2, implement:3, test:4, review:5, finished:6, addressed:7, resolved:8, wont_fix:9, open:10, in_progress:11};
return (st[a.status] ?? 99) - (st[b.status] ?? 99);
});
}
} catch {}
failures = ok ? 0 : failures + 1;
yield {data, ok, ts: new Date()};
await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
}
})();
```
```js
const data = feedbackState.data ?? [];
const _ok = feedbackState.ok ?? false;
const _ts = feedbackState.ts;
```
# UI Feedback
```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
const _active = data.filter(t => !CLOSED_STATUSES.has(t.status));
const _finished = data.filter(t => ["finished", "addressed", "resolved"].includes(t.status));
const _wontfix = data.filter(t => t.status === "wont_fix");
const _kpiBox = html`
`;
const _liveEl = html`
●
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
: html`Offline — run: make api`}
`;
injectTocTop("fb-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
```
> Shift+click any widget on any dashboard page to submit a suggestion.
> Each suggestion moves through the workflow below; the **Review** step
> is for the original suggester to confirm the implementation is correct.
## Active
```js
async function _advance(td, newStatus) {
return fetch(`${API}/technical-debt/${td.id}`, {
method: "PATCH",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({status: newStatus}),
});
}
async function _addNote(td_id, step, content) {
return fetch(`${API}/technical-debt/${td_id}/notes/`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({step, content, author: "custodian"}),
});
}
function renderSuggestionCard(t) {
const isWorkflow = STEPS.includes(t.status);
const next = nextStep(t.status);
const color = STEP_COLOR[t.status] ?? "#94a3b8";
const stepIdx = STEPS.indexOf(t.status);
// ── Stepper ─────────────────────────────────────────────────────────
const stepperEl = html`
${STEPS.map((s, i) => {
const done = i < stepIdx;
const current = i === stepIdx;
return html`
${i < STEPS.length - 1 ? html`
` : ""}`;
})}
`;
// ── Notes ────────────────────────────────────────────────────────────
const notesEl = html`
${(t.notes ?? []).length === 0
? html`
No notes yet for this step.
`
: (t.notes ?? []).map(n => html`
${STEP_LABEL[n.step] ?? n.step}
${n.author ?? "anon"}
${new Date(n.created_at).toLocaleString()}
${n.content}
`)}
`;
// ── Add note form ────────────────────────────────────────────────────
const noteTA = html``;
const noteBtn = html``;
noteBtn.addEventListener("click", async () => {
const txt = noteTA.value.trim();
if (!txt) return;
noteBtn.disabled = true;
const r = await _addNote(t.id, t.status, txt);
if (r.ok) {
noteTA.value = "";
// Refresh: remove and re-fetch by reloading feedbackState on next poll
}
noteBtn.disabled = false;
});
const noteForm = html`${noteTA}${noteBtn}
`;
// ── Actions ──────────────────────────────────────────────────────────
const actionsEl = html`
${t.status === "review"
? html`⬆ Awaiting confirmation from the original suggester`
: ""}
${next ? html`` : ""}
`;
return html`
${t.title.replace(/^UI:\s*/, "")}
${t.description ?? ""}
${stepperEl}
Notes (${(t.notes ?? []).length})
${notesEl}
${noteForm}
${actionsEl}
`;
}
if (_active.length === 0) {
display(html`No active suggestions. Shift+click any dashboard widget to submit one.
`);
} else {
display(html`${_active.map(renderSuggestionCard)}
`);
}
```
## Finished & Won't Fix
```js
const _closed = [..._finished, ..._wontfix];
if (_closed.length === 0) {
display(html`Nothing closed yet.
`);
} else {
display(html`${_closed.map(t => {
const color = STEP_COLOR[t.status] ?? "#94a3b8";
return html`
${t.title.replace(/^UI:\s*/, "")}
${t.description ?? ""}
${(t.notes ?? []).length > 0 ? html`
${t.notes.slice(-2).map(n => html`
${STEP_LABEL[n.step] ?? n.step}
${n.content}
`)}
` : ""}
`;
})}
`);
}
```