generated from coulomb/repo-seed
feat(capability-requests): add routing dispute & reroute workflow (CUST-WP-0027)
Adds a structured dispute mechanism when capability request routing is wrong:
- New `routing_disputed` status with four DB columns (dispute_reason, disputed_by,
dispute_suggested_domain, disputed_at) via Alembic migration m0h1i2j3k4l5
- POST /capability-requests/{id}/dispute — any party can flag misrouting with a reason
and optional suggested domain; notifies custodian + current fulfilling domain
- POST /capability-requests/{id}/reroute — custodian re-routes to correct domain via
catalog_entry_id or direct slug; appends audit trail to routing_note; resets to requested
- Two new MCP tools: dispute_capability_routing and reroute_capability_request
- Dashboard: amber disputed-banner at top of Summary, routing_disputed Kanban column,
dispute details (reason, suggested domain, raised-by) shown on disputed cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/capabilities
|
||||
|
||||
```js
|
||||
// KPI sidebar
|
||||
const open = requests.filter(r => ["requested","accepted","in_progress","ready_for_review"].includes(r.status));
|
||||
const open = requests.filter(r => ["requested","routing_disputed","accepted","in_progress","ready_for_review"].includes(r.status));
|
||||
const completed = requests.filter(r => r.status === "completed");
|
||||
const avgFulfill = completed.length > 0
|
||||
? (completed.reduce((s, r) => s + (new Date(r.completed_at) - new Date(r.created_at)), 0) / completed.length / 86400000).toFixed(1)
|
||||
@@ -74,7 +74,7 @@ const typeFilter = Inputs.select(
|
||||
{label: "Type", value: "all"}
|
||||
);
|
||||
const statFilter = Inputs.select(
|
||||
["all", "requested", "accepted", "in_progress", "ready_for_review", "completed", "rejected", "withdrawn"],
|
||||
["all", "routing_disputed", "requested", "accepted", "in_progress", "ready_for_review", "completed", "rejected", "withdrawn"],
|
||||
{label: "Status", value: "all"}
|
||||
);
|
||||
const domFilter = Inputs.select(
|
||||
@@ -102,6 +102,26 @@ const filtered = requests.filter(r =>
|
||||
|
||||
```js
|
||||
const priorityColors = {critical: "#e53935", high: "orange", medium: "steelblue", low: "#aaa"};
|
||||
const disputed = requests.filter(r => r.status === "routing_disputed");
|
||||
|
||||
// Disputed banner — shown at top when any exist
|
||||
if (disputed.length > 0) {
|
||||
display(html`<div class="disputed-banner">
|
||||
<div class="disputed-banner-title">⚠ Routing Disputed (${disputed.length})</div>
|
||||
${disputed.map(r => html`
|
||||
<div class="disputed-card">
|
||||
<div class="disputed-card-header">
|
||||
<span class="cap-title">${r.title}</span>
|
||||
<span class="cap-domains">${r.requesting_domain_slug} → <strong>${r.fulfilling_domain_slug ?? "unassigned"}</strong></span>
|
||||
</div>
|
||||
<div class="disputed-reason"><strong>Dispute:</strong> ${r.dispute_reason ?? "(no reason given)"}</div>
|
||||
${r.dispute_suggested_domain ? html`<div class="disputed-suggestion">Suggested domain: <strong>${r.dispute_suggested_domain}</strong></div>` : ""}
|
||||
${r.disputed_by ? html`<div class="disputed-meta">Raised by <em>${r.disputed_by}</em> · ${new Date(r.disputed_at).toLocaleString()}</div>` : ""}
|
||||
</div>
|
||||
`)}
|
||||
</div>`);
|
||||
}
|
||||
|
||||
display(html`<div class="grid grid-cols-4" style="gap:1rem;margin-bottom:1.5rem">
|
||||
<div class="card"><h3>Requested</h3><p class="big-num">${requests.filter(r => r.status === "requested").length}</p></div>
|
||||
<div class="card"><h3>In Progress</h3><p class="big-num">${requests.filter(r => ["accepted","in_progress"].includes(r.status)).length}</p></div>
|
||||
@@ -114,13 +134,14 @@ display(html`<div class="grid grid-cols-4" style="gap:1rem;margin-bottom:1.5rem"
|
||||
|
||||
```js
|
||||
const statusCols = [
|
||||
{key: "requested", label: "Requested", color: "steelblue"},
|
||||
{key: "accepted", label: "Accepted", color: "#f0a500"},
|
||||
{key: "in_progress", label: "In Progress", color: "#2196f3"},
|
||||
{key: "ready_for_review", label: "Ready for Review", color: "#4caf50"},
|
||||
{key: "completed", label: "Completed", color: "#2e7d32"},
|
||||
{key: "rejected", label: "Rejected", color: "#e53935"},
|
||||
{key: "withdrawn", label: "Withdrawn", color: "#bbb"},
|
||||
{key: "routing_disputed", label: "⚠ Routing Disputed", color: "#f59e0b"},
|
||||
{key: "requested", label: "Requested", color: "steelblue"},
|
||||
{key: "accepted", label: "Accepted", color: "#f0a500"},
|
||||
{key: "in_progress", label: "In Progress", color: "#2196f3"},
|
||||
{key: "ready_for_review", label: "Ready for Review", color: "#4caf50"},
|
||||
{key: "completed", label: "Completed", color: "#2e7d32"},
|
||||
{key: "rejected", label: "Rejected", color: "#e53935"},
|
||||
{key: "withdrawn", label: "Withdrawn", color: "#bbb"},
|
||||
];
|
||||
|
||||
const colMap = {};
|
||||
@@ -247,4 +268,11 @@ if (catalog.length === 0) {
|
||||
.catalog-desc { font-size: 0.75rem; color: var(--theme-foreground-muted, #666); line-height: 1.35; margin-bottom: 0.3rem; }
|
||||
.catalog-kw { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
.kw-tag { font-size: 0.6rem; background: var(--theme-background-alt, #f0f0f0); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 3px; padding: 0.05rem 0.3rem; font-family: monospace; color: var(--theme-foreground-muted, #666); }
|
||||
.disputed-banner { background: #fff8e1; border: 1.5px solid #f59e0b; border-radius: 8px; padding: 0.85rem 1rem; margin-bottom: 1.25rem; }
|
||||
.disputed-banner-title { font-weight: 700; font-size: 0.9rem; color: #b45309; margin-bottom: 0.6rem; }
|
||||
.disputed-card { background: #fffbf0; border: 1px solid #fcd34d; border-radius: 6px; padding: 0.6rem 0.75rem; margin-bottom: 0.5rem; }
|
||||
.disputed-card-header { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: 0.3rem; flex-wrap: wrap; }
|
||||
.disputed-reason { font-size: 0.8rem; color: #92400e; margin-bottom: 0.2rem; }
|
||||
.disputed-suggestion { font-size: 0.78rem; color: #1d4ed8; margin-bottom: 0.15rem; }
|
||||
.disputed-meta { font-size: 0.72rem; color: gray; margin-top: 0.2rem; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user