Dashboard workstreams: multi-select filters that survive data polls

- Replace single-select Domain/Status dropdowns with checkbox multi-selects
- Use Inputs.form() with a custom template to lay the three filters out
  side by side in a card-style filter bar
- Filter options are now static constants (DOMAINS, STATUSES) — no
  dependency on the polled data, so selections are never reset on refresh
- Empty selection = no filter applied (show all); any checked item = include
- Updated filtered computation and wsWithDeps to use filters.domain /
  filters.status array semantics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 00:05:58 +01:00
parent da71a1bfac
commit de936acd6d

View File

@@ -56,19 +56,32 @@ display(html`<div class="live-bar">
```
```js
const domainOpts = ["(all)", ...new Set(data.map(w => w.domain))].sort();
const statusOpts = ["(all)", "active", "blocked", "completed", "archived"];
// Static options — no dependency on `data`, so selections survive polls
const DOMAINS = ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"];
const STATUSES = ["active", "blocked", "completed", "archived"];
const domainFilter = view(Inputs.select(domainOpts, {label: "Domain"}));
const statusFilter = view(Inputs.select(statusOpts, {label: "Status"}));
const ownerFilter = view(Inputs.text({label: "Owner contains"}));
const filters = view(Inputs.form(
{
domain: Inputs.checkbox(DOMAINS, {label: "Domain"}),
status: Inputs.checkbox(STATUSES, {label: "Status"}),
owner: Inputs.text({label: "Owner contains", placeholder: "filter by owner…"}),
},
{
template: ({domain, status, owner}) => html`<div class="filter-bar">
<div class="filter-group">${domain}</div>
<div class="filter-group">${status}</div>
<div class="filter-group filter-group-text">${owner}</div>
</div>`,
}
));
```
```js
// Empty array = no filter applied (show all)
const filtered = data.filter(w =>
(domainFilter === "(all)" || w.domain === domainFilter) &&
(statusFilter === "(all)" || w.status === statusFilter) &&
(!ownerFilter || (w.owner ?? "").toLowerCase().includes(ownerFilter.toLowerCase()))
(filters.domain.length === 0 || filters.domain.includes(w.domain)) &&
(filters.status.length === 0 || filters.status.includes(w.status)) &&
(!filters.owner || (w.owner ?? "").toLowerCase().includes(filters.owner.toLowerCase()))
);
display(Inputs.table(filtered.map(w => ({
@@ -104,11 +117,12 @@ display(Plot.plot({
```js
// Build dep cards from the enriched open_workstreams in the summary
const wsWithDeps = openWs.filter(w =>
(domainFilter === "(all)" || (data.find(d => d.id === w.id)?.domain ?? "unknown") === domainFilter) &&
(statusFilter === "(all)" || w.status === statusFilter) &&
(w.depends_on.length > 0 || w.blocks.length > 0)
);
const wsWithDeps = openWs.filter(w => {
const domain = data.find(d => d.id === w.id)?.domain ?? "unknown";
return (filters.domain.length === 0 || filters.domain.includes(domain)) &&
(filters.status.length === 0 || filters.status.includes(w.status)) &&
(w.depends_on.length > 0 || w.blocks.length > 0);
});
if (wsWithDeps.length === 0) {
display(html`<p class="dim">No dependency edges recorded for the current filter. Use <code>create_dependency()</code> via the MCP server to link workstreams.</p>`);
@@ -132,6 +146,9 @@ if (wsWithDeps.length === 0) {
<style>
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
.dim { color: gray; font-style: italic; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-start; padding: 0.75rem 1rem; background: var(--theme-background-alt); border-radius: 8px; margin-bottom: 1rem; }
.filter-group { min-width: 140px; }
.filter-group-text { align-self: flex-end; }
.dep-grid { display: flex; flex-direction: column; gap: 0.75rem; }
.dep-card { border: 1px solid #e0e0e0; border-radius: 6px; padding: 0.75rem 1rem; background: var(--theme-background-alt, #fafafa); }
.dep-title { font-weight: 600; margin-bottom: 0.25rem; }