Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/decodex/src/orchestrator/entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,8 @@ fn operator_project_status_from_registration(
retained_worktree_count: 0,
waiting_lane_count: 0,
attention_count: 0,
cleanup_blocked_count: 0,
cleanup_pending_count: 0,
connector_state: if project.enabled() {
if warning_count == 0 {
String::from("ok")
Expand Down Expand Up @@ -861,6 +863,8 @@ fn operator_project_status_from_api_only_registration(
retained_worktree_count: 0,
waiting_lane_count: 0,
attention_count: 0,
cleanup_blocked_count: 0,
cleanup_pending_count: 0,
connector_state: if project.enabled() {
String::from("api_only")
} else {
Expand Down
103 changes: 88 additions & 15 deletions apps/decodex/src/orchestrator/operator_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -4142,6 +4142,7 @@ <h2 id="recent-title">Run History</h2>
case "wait_for_review":
return "tone-review";
case "cleanup_blocked":
return "tone-wait";
case "closeout_blocked":
case "blocked":
return "tone-blocked";
Expand Down Expand Up @@ -7705,6 +7706,12 @@ <h2 id="recent-title">Run History</h2>
(candidate) => candidate.classification === "closed",
);
const worktrees = snapshot?.worktrees ?? [];
const cleanupIssueKeys = new Set();
for (const worktree of worktrees) {
if (worktree.hygiene) {
cleanupIssueKeys.add(issueDisplayKey(worktree));
}
}
const waitingItems = [];
const readyItems = [];
const attentionItems = [];
Expand Down Expand Up @@ -7800,6 +7807,9 @@ <h2 id="recent-title">Run History</h2>

if (isPostReviewBlocker(lane)) {
const blockerScope = postReviewBlockerScope(lane);
if (blockerScope === "Cleanup") {
cleanupIssueKeys.add(issueKey);
}
attentionItems.push({
tone,
scope: blockerScope,
Expand Down Expand Up @@ -7861,8 +7871,9 @@ <h2 id="recent-title">Run History</h2>
(item) => item.scope === "Review",
).length;
const reviewBlockerCount = attentionItems.filter((item) =>
["Review", "Closeout", "Cleanup"].includes(item.scope),
["Review", "Closeout"].includes(item.scope),
).length;
const cleanupCount = cleanupIssueKeys.size;
const runningAttentionCount = attentionItems.filter((item) => item.scope === "Running").length;
const liveRuns = activeRuns.filter(runCountsAsRunning).length;
const intakeAttentionCount = queueBacklogCandidates.filter(queuedCandidateNeedsAttention).length;
Expand Down Expand Up @@ -7891,6 +7902,7 @@ <h2 id="recent-title">Run History</h2>
readyItems,
reviewWaitingCount,
reviewBlockerCount,
cleanupCount,
runningAttentionCount,
sessionHistoryRuns: historyRuns,
worktrees,
Expand Down Expand Up @@ -7944,6 +7956,8 @@ <h2 id="recent-title">Run History</h2>
(project.queued_candidate_count ?? 0) +
(project.waiting_lane_count ?? 0) +
(project.attention_count ?? 0) +
(project.cleanup_blocked_count ?? 0) +
(project.cleanup_pending_count ?? 0) +
(project.post_review_lane_count ?? 0);
const connector = projectConnectorSummary(project);
const syncNeedsAttention = project.enabled && ["backoff", "degraded", "stale"].includes(connector);
Expand All @@ -7968,6 +7982,9 @@ <h2 id="recent-title">Run History</h2>
if ((project.waiting_lane_count ?? 0) > 0) {
return { label: "waiting", tone: "tone-wait", title: "Lanes waiting to resume" };
}
if ((project.cleanup_blocked_count ?? 0) > 0) {
return { label: "cleanup blocked", tone: "tone-wait", title: "Post-land cleanup needs operator action" };
}
if (project.connector_state === "backoff") {
return {
label: "sync backoff",
Expand All @@ -7979,17 +7996,27 @@ <h2 id="recent-title">Run History</h2>
(project.warning_count ?? 0) > 0 ||
["degraded", "stale_cache"].includes(project.connector_state)
) {
if ((project.cleanup_pending_count ?? 0) > 0) {
return { label: "cleanup pending", tone: "tone-retained", title: "Post-land cleanup pending" };
}

return { label: "sync degraded", tone: "tone-wait", title: "Tracker sync or retry state degraded" };
}
if ((project.cleanup_pending_count ?? 0) > 0) {
return { label: "cleanup pending", tone: "tone-retained", title: "Post-land cleanup pending" };
}

return { label: "ok", tone: "tone-ready", title: "No project warnings" };
}

function projectCapacitySummary(project) {
const cleanup = (project.cleanup_blocked_count ?? 0) + (project.cleanup_pending_count ?? 0);

return [
`${project.active_run_count ?? 0} running`,
`${project.waiting_lane_count ?? 0} waiting`,
`${project.attention_count ?? 0} attention`,
`${cleanup} cleanup`,
].join(" · ");
}

Expand All @@ -8005,10 +8032,11 @@ <h2 id="recent-title">Run History</h2>
const running = project.active_run_count ?? 0;
const waiting = project.waiting_lane_count ?? 0;
const attention = project.attention_count ?? 0;
const cleanup = (project.cleanup_blocked_count ?? 0) + (project.cleanup_pending_count ?? 0);

return `
<span class="project-work-ratio" role="cell" aria-label="${escapeHtml(`${running} running, ${waiting} waiting, ${attention} attention`)}" title="running / waiting / attention">
<strong>${escapeHtml(running)}</strong><span class="project-work-separator">/</span><strong>${escapeHtml(waiting)}</strong><span class="project-work-separator">/</span><strong>${escapeHtml(attention)}</strong>
<span class="project-work-ratio" role="cell" aria-label="${escapeHtml(`${running} running, ${waiting} waiting, ${attention} attention, ${cleanup} cleanup`)}" title="running / waiting / attention / cleanup">
<strong>${escapeHtml(running)}</strong><span class="project-work-separator">/</span><strong>${escapeHtml(waiting)}</strong><span class="project-work-separator">/</span><strong>${escapeHtml(attention)}</strong><span class="project-work-separator">/</span><strong>${escapeHtml(cleanup)}</strong>
</span>
`;
}
Expand All @@ -8020,6 +8048,12 @@ <h2 id="recent-title">Run History</h2>
if ((project.attention_count ?? 0) > 0) {
return `${project.attention_count} attention`;
}
if ((project.cleanup_blocked_count ?? 0) > 0) {
return `${project.cleanup_blocked_count} cleanup blocked`;
}
if ((project.cleanup_pending_count ?? 0) > 0) {
return `${project.cleanup_pending_count} cleanup pending`;
}
if ((project.warning_count ?? 0) > 0) {
return pluralize(project.warning_count, "warning");
}
Expand Down Expand Up @@ -8079,14 +8113,14 @@ <h2 id="recent-title">Run History</h2>
function projectWorkInfoMarkup() {
return `
<span class="project-work-info-wrap">
<button class="project-work-info" type="button" data-project-work-info aria-expanded="false" aria-label="Work format: running / waiting / attention">
<button class="project-work-info" type="button" data-project-work-info aria-expanded="false" aria-label="Work format: running / waiting / attention / cleanup">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6"></circle>
<path d="M8 7.3v3.8"></path>
<path d="M8 4.8h.01"></path>
</svg>
</button>
<span class="project-work-tooltip" role="tooltip">running / waiting / attention</span>
<span class="project-work-tooltip" role="tooltip">running / waiting / attention / cleanup</span>
</span>
`;
}
Expand Down Expand Up @@ -8195,6 +8229,8 @@ <h2 id="recent-title">Run History</h2>
projectNumber(project.active_run_count),
projectNumber(project.waiting_lane_count),
projectNumber(project.attention_count),
projectNumber(project.cleanup_blocked_count),
projectNumber(project.cleanup_pending_count),
];
}

Expand Down Expand Up @@ -8271,20 +8307,29 @@ <h2 id="recent-title">Run History</h2>
if ((project.active_run_count ?? 0) > 0) {
return 0;
}
if ((project.attention_count ?? 0) > 0 || (project.warning_count ?? 0) > 0) {
if ((project.attention_count ?? 0) > 0) {
return 1;
}
if (["backoff", "degraded", "stale_cache"].includes(project.connector_state)) {
if ((project.waiting_lane_count ?? 0) > 0) {
return 2;
}
if ((project.waiting_lane_count ?? 0) > 0) {
if ((project.cleanup_blocked_count ?? 0) > 0) {
return 3;
}
if (projectHasActiveWork(project)) {
if ((project.cleanup_pending_count ?? 0) > 0) {
return 4;
}
if (["backoff", "degraded", "stale_cache"].includes(project.connector_state)) {
return 5;
}
if ((project.warning_count ?? 0) > 0) {
return 6;
}
if (projectHasActiveWork(project)) {
return 7;
}

return 5;
return 8;
}

function compareProjectRowsStable(left, right) {
Expand Down Expand Up @@ -8846,6 +8891,20 @@ <h4>${escapeHtml(title)}</h4>
};
}
if (reviewMatch) {
if (hygiene) {
const isDirty = hygiene.classification === "merged_dirty_worktree" || hygiene.dirty === true;

return {
sortRank: 1,
tone: isDirty ? "tone-wait" : "tone-retained",
label: isDirty ? "post-review cleanup blocked" : "post-review cleanup",
summary:
hygiene.reason ||
worktree.ownership_reason ||
"Post-review cleanup pending.",
};
}

return {
sortRank: 1,
tone: toneForLane(reviewMatch),
Expand Down Expand Up @@ -8895,6 +8954,20 @@ <h4>${escapeHtml(title)}</h4>
};
}
if (worktree.ownership === "post_review_lane") {
if (hygiene) {
const isDirty = hygiene.classification === "merged_dirty_worktree" || hygiene.dirty === true;

return {
sortRank: 1,
tone: isDirty ? "tone-wait" : "tone-retained",
label: isDirty ? "post-review cleanup blocked" : "post-review cleanup",
summary:
hygiene.reason ||
worktree.ownership_reason ||
"Post-review cleanup pending.",
};
}

return {
sortRank: 1,
tone: "tone-retained",
Expand All @@ -8909,8 +8982,8 @@ <h4>${escapeHtml(title)}</h4>

return {
sortRank: 2,
tone: isDirty ? "tone-blocked" : "tone-retained",
label: isDirty ? "post-land dirty worktree" : "post-land cleanup",
tone: isDirty ? "tone-wait" : "tone-retained",
label: isDirty ? "post-land cleanup blocked" : "post-land cleanup",
summary:
hygiene.reason ||
worktree.ownership_reason ||
Expand Down Expand Up @@ -8953,7 +9026,7 @@ <h4>${escapeHtml(title)}</h4>
function recoveryWorktreeShouldDefaultOpen(renderedWorktree) {
const role = renderedWorktree.role;

return role.tone === "tone-blocked" || role.label.startsWith("post-land");
return role.tone === "tone-blocked" || role.label.includes("cleanup");
}

function renderWorktrees(snapshot) {
Expand Down Expand Up @@ -9486,8 +9559,8 @@ <h4>${escapeHtml(worktree.branch_name)}</h4>
setPanelMeta(
nodes.reviewLanesMeta,
snapshot
? `${pluralize(derived.postReviewLanes.length, "PR")} · ${pluralize(derived.reviewBlockerCount, "needs attention", "need attention")} · ${derived.readyItems.length} ready · ${derived.reviewWaitingCount} waiting`
: "0 PRs · 0 need attention · 0 ready · 0 waiting",
? `${pluralize(derived.postReviewLanes.length, "PR")} · ${pluralize(derived.reviewBlockerCount, "needs attention", "need attention")} · ${derived.readyItems.length} ready · ${derived.reviewWaitingCount} waiting · ${derived.cleanupCount} cleanup`
: "0 PRs · 0 need attention · 0 ready · 0 waiting · 0 cleanup",
);
renderWorktrees(snapshot);
}
Expand Down
61 changes: 56 additions & 5 deletions apps/decodex/src/orchestrator/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ fn build_operator_status_snapshot(
retained_worktree_count: 0,
waiting_lane_count: 0,
attention_count: 0,
cleanup_blocked_count: 0,
cleanup_pending_count: 0,
connector_state: String::from("ok"),
last_activity_at: None,
warning_count: 0,
Expand Down Expand Up @@ -488,6 +490,8 @@ fn refresh_operator_project_summary(snapshot: &mut OperatorStatusSnapshot) {
let retained_worktree_count = rendered_recovery_worktrees(snapshot).len();
let waiting_lane_count = project_waiting_lane_count(snapshot);
let attention_count = project_attention_count(snapshot);
let cleanup_blocked_count = project_cleanup_blocked_count(snapshot);
let cleanup_pending_count = project_cleanup_pending_count(snapshot);
let connector_state = project_connector_state(snapshot);
let last_activity_at = project_last_activity_at(snapshot);
let warning_count = snapshot.warnings.len();
Expand All @@ -499,6 +503,8 @@ fn refresh_operator_project_summary(snapshot: &mut OperatorStatusSnapshot) {
project_status.retained_worktree_count = retained_worktree_count;
project_status.waiting_lane_count = waiting_lane_count;
project_status.attention_count = attention_count;
project_status.cleanup_blocked_count = cleanup_blocked_count;
project_status.cleanup_pending_count = cleanup_pending_count;
project_status.connector_state = connector_state;
project_status.last_activity_at = last_activity_at;
project_status.warning_count = warning_count;
Expand Down Expand Up @@ -559,17 +565,62 @@ fn project_attention_count(snapshot: &OperatorStatusSnapshot) -> usize {
.filter(|lane| {
matches!(
lane.classification.as_str(),
"blocked" | "needs_review_repair" | "closeout_blocked" | "cleanup_blocked"
"blocked" | "needs_review_repair" | "closeout_blocked"
)
})
.count();
let hygiene_attention = snapshot

active_attention + queued_attention + review_attention
}

fn project_cleanup_blocked_count(snapshot: &OperatorStatusSnapshot) -> usize {
let mut cleanup_keys = HashSet::new();

for lane in snapshot
.post_review_lanes
.iter()
.filter(|lane| lane.classification == "cleanup_blocked")
{
cleanup_keys.insert(post_review_lane_cleanup_key(lane));
}
for worktree in snapshot.worktrees.iter().filter(|worktree| {
worktree.hygiene.as_ref().is_some_and(|hygiene| {
hygiene.dirty || hygiene.classification == "merged_dirty_worktree"
})
}) {
cleanup_keys.insert(worktree_cleanup_key(worktree));
}

cleanup_keys.len()
}

fn project_cleanup_pending_count(snapshot: &OperatorStatusSnapshot) -> usize {
snapshot
.worktrees
.iter()
.filter(|worktree| worktree.hygiene.is_some())
.count();
.filter(|worktree| {
worktree.hygiene.as_ref().is_some_and(|hygiene| {
!hygiene.dirty && hygiene.classification == "merged_worktree_cleanup_pending"
})
})
.map(worktree_cleanup_key)
.collect::<HashSet<_>>()
.len()
}

active_attention + queued_attention + review_attention + hygiene_attention
fn post_review_lane_cleanup_key(lane: &OperatorPostReviewLaneStatus) -> String {
if lane.issue_identifier.is_empty() {
return lane.issue_id.clone();
}

lane.issue_identifier.clone()
}

fn worktree_cleanup_key(worktree: &OperatorWorktreeStatus) -> String {
worktree
.issue_identifier
.clone()
.unwrap_or_else(|| worktree.issue_id.clone())
}

fn operator_run_counts_as_active(run: &OperatorRunStatus) -> bool {
Expand Down
Loading