|
|
@@ -83,6 +83,18 @@ namespace ClankerSaddle {
|
|
|
/** Public data of the dashboard's `dashboard` slot. */
|
|
|
public class DashboardPublic : Object {
|
|
|
public int64 draft_count { get; set; }
|
|
|
+ /**
|
|
|
+ * Waiting-for-attention work (goal 2 QoL): everything the
|
|
|
+ * system owes a dispatch or resume to — ready-to-dispatch
|
|
|
+ * queued tasks (the ready_count subset) + parked work awaiting
|
|
|
+ * resume (suspended tasks, suspended goal-agent runs included)
|
|
|
+ * + tasks whose raised blocks are all answered and now wait
|
|
|
+ * for a saddle to pick the resume up (these leave
|
|
|
+ * blocked_count, which stays human-answerable rows only).
|
|
|
+ * Raw queued rows gated on an unfinished auto-enqueue
|
|
|
+ * predecessor are deliberately OUT — sequenced future work,
|
|
|
+ * the predecessor itself is already counted.
|
|
|
+ */
|
|
|
public int64 queued_count { get; set; }
|
|
|
public int64 ready_count { get; set; }
|
|
|
public int64 active_count { get; set; }
|
|
|
@@ -125,12 +137,27 @@ namespace ClankerSaddle {
|
|
|
var pub = new DashboardPublic();
|
|
|
var titles = new Dictionary<string, string>();
|
|
|
var all_tasks = yield tasks.list_tasks(null, null);
|
|
|
+ // The ready-to-dispatch subset of the queued rows (see the
|
|
|
+ // queued_count docblock), keyed by id so the status loop can
|
|
|
+ // tell a dispatchable queued task from one still gated on
|
|
|
+ // its auto-enqueue predecessor.
|
|
|
+ var ready_ids = new HashSet<string>();
|
|
|
+ int64 ready_count = 0;
|
|
|
+ foreach (var ready in yield tasks.get_ready_tasks()) {
|
|
|
+ ready_ids.add(ready.id.to_string());
|
|
|
+ if (ready.kind != TASK_KIND_GOAL_RUN) {
|
|
|
+ ready_count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pub.ready_count = ready_count;
|
|
|
foreach (var task in all_tasks) {
|
|
|
titles.set(task.id.to_string(), task.title);
|
|
|
// Goal-agent runs are internal tasks — they never count
|
|
|
// against the human-facing queue summary (the goal page
|
|
|
- // owns their visibility).
|
|
|
- if (task.kind == TASK_KIND_GOAL_RUN) {
|
|
|
+ // owns their visibility) EXCEPT parked: a suspended run
|
|
|
+ // is goal work the system owes a resume, so it joins
|
|
|
+ // queued_count (goal 2 QoL).
|
|
|
+ if (task.kind == TASK_KIND_GOAL_RUN && task.status != STATUS_SUSPENDED) {
|
|
|
continue;
|
|
|
}
|
|
|
switch (task.status) {
|
|
|
@@ -138,15 +165,35 @@ namespace ClankerSaddle {
|
|
|
pub.draft_count++;
|
|
|
break;
|
|
|
case STATUS_QUEUED:
|
|
|
- pub.queued_count++;
|
|
|
+ // Ready rows are the queued metric's core;
|
|
|
+ // predecessor-gated rows are sequenced future
|
|
|
+ // work the counters leave out (docblock above).
|
|
|
+ if (ready_ids.has(task.id.to_string())) {
|
|
|
+ pub.queued_count++;
|
|
|
+ }
|
|
|
break;
|
|
|
case STATUS_ASSIGNED:
|
|
|
case STATUS_WORKING:
|
|
|
pub.active_count++;
|
|
|
break;
|
|
|
+ case STATUS_SUSPENDED:
|
|
|
+ // Parked awaiting resume (goal 2 QoL): a
|
|
|
+ // suspended task waits on the system, not on a
|
|
|
+ // human — it reads as waiting, not blocked.
|
|
|
+ pub.queued_count++;
|
|
|
+ break;
|
|
|
case STATUS_BLOCKED:
|
|
|
case STATUS_REVIEW_REQUESTED:
|
|
|
- pub.blocked_count++;
|
|
|
+ // Goal 2 QoL: answered blocks mean the human
|
|
|
+ // already acted — the task now waits for a
|
|
|
+ // saddle to resume it, so it joins the queued
|
|
|
+ // metric and leaves blocked_count (which stays
|
|
|
+ // genuinely-needs-a-human rows).
|
|
|
+ if (yield blocks_all_answered(tasks, task.id)) {
|
|
|
+ pub.queued_count++;
|
|
|
+ } else {
|
|
|
+ pub.blocked_count++;
|
|
|
+ }
|
|
|
break;
|
|
|
case STATUS_DONE:
|
|
|
pub.done_count++;
|
|
|
@@ -158,13 +205,6 @@ namespace ClankerSaddle {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
- int64 ready_count = 0;
|
|
|
- foreach (var task in yield tasks.get_ready_tasks()) {
|
|
|
- if (task.kind != TASK_KIND_GOAL_RUN) {
|
|
|
- ready_count++;
|
|
|
- }
|
|
|
- }
|
|
|
- pub.ready_count = ready_count;
|
|
|
|
|
|
|
|
|
var saddle_rows = new Vector<DashboardSaddleView>();
|
|
|
@@ -320,6 +360,23 @@ namespace ClankerSaddle {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * True when the task has at least one block and every one of
|
|
|
+ * them carries an answer (goal 2 QoL): the human already
|
|
|
+ * acted, the dispatcher's resume is the only thing owed. A
|
|
|
+ * task with no blocks at all is NOT answered-waiting — the
|
|
|
+ * counters leave such rows in blocked_count untouched.
|
|
|
+ */
|
|
|
+ private static async bool blocks_all_answered(TaskService tasks, int64 task_id) throws GLib.Error {
|
|
|
+ bool any = false;
|
|
|
+ foreach (var block in yield tasks.list_blocks(task_id)) {
|
|
|
+ any = true;
|
|
|
+ if (block.answer == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return any;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|