generated from coulomb/repo-seed
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>
This commit is contained in:
211
dashboard/src/ui-feedback.md
Normal file
211
dashboard/src/ui-feedback.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
title: UI Feedback
|
||||
---
|
||||
|
||||
```js
|
||||
import {API, POLL} from "./components/config.js";
|
||||
```
|
||||
|
||||
```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));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```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 _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
|
||||
|
||||
```js
|
||||
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");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
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
|
||||
|
||||
```js
|
||||
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>`);
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
||||
.kpi-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.3rem 0; border-top: 1px solid var(--theme-foreground-faint, #eee); }
|
||||
.kpi-row:first-of-type { border-top: none; }
|
||||
.kpi-row-label { font-size: 0.8rem; color: var(--theme-foreground-muted, #666); }
|
||||
.kpi-row-right { text-align: right; }
|
||||
.kpi-row-value { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
|
||||
|
||||
.fb-list { display: flex; flex-direction: column; gap: 0.55rem; }
|
||||
.fb-done { opacity: 0.7; }
|
||||
|
||||
.fb-card {
|
||||
border-left: 3px solid #6366f1;
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: var(--theme-background-alt);
|
||||
padding: 0.7rem 1rem;
|
||||
}
|
||||
.fb-status-resolved { border-left-color: #16a34a; }
|
||||
.fb-status-wont_fix { border-left-color: #94a3b8; }
|
||||
.fb-status-in_progress { border-left-color: #d97706; }
|
||||
|
||||
.fb-card-header {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
.fb-location {
|
||||
font-size: 0.75rem; color: var(--theme-foreground-faint);
|
||||
font-style: italic; flex: 1;
|
||||
}
|
||||
.fb-suggestion {
|
||||
font-size: 0.88rem; line-height: 1.55;
|
||||
color: var(--theme-foreground);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.fb-actions { display: flex; gap: 0.35rem; margin-left: auto; }
|
||||
.fb-btn {
|
||||
padding: 0.2rem 0.65rem; border-radius: 5px; font-size: 0.75rem;
|
||||
font-family: inherit; cursor: pointer; font-weight: 600; border: 1px solid transparent;
|
||||
transition: background 0.1s, opacity 0.1s;
|
||||
}
|
||||
.fb-btn-resolve { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
|
||||
.fb-btn-resolve:hover { background: #bbf7d0; }
|
||||
.fb-btn-wontfix { background: #f1f5f9; color: #475569; border-color: #cbd5e1; }
|
||||
.fb-btn-wontfix:hover { background: #e2e8f0; }
|
||||
.fb-btn-inprogress { background: #fef3c7; color: #92400e; border-color: #fde68a; }
|
||||
.fb-btn-inprogress:hover { background: #fde68a; }
|
||||
|
||||
/* reuse td-badge styles */
|
||||
.td-ref { font-family: monospace; font-size: 0.72rem; color: var(--theme-foreground-muted); background: var(--theme-background); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 4px; padding: 0.05rem 0.35rem; }
|
||||
.td-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 500; }
|
||||
.td-badge-open { background: #dbeafe; color: #1e40af; }
|
||||
.td-badge-in_progress { background: #fef3c7; color: #92400e; }
|
||||
.td-badge-resolved { background: #dcfce7; color: #166534; }
|
||||
.td-badge-wont_fix { background: #f3f4f6; color: #9ca3af; }
|
||||
|
||||
.dim { color: gray; font-style: italic; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user