Explorar o código

Dashboard Queue metric reflects waiting-for-attention work

queued_count now counts ready-to-dispatch queued tasks, parked (suspended) tasks including suspended goal-agent runs, and tasks whose blocks are all answered that wait for a saddle to resume them; answered tasks leave blocked_count, which stays human-answerable rows only. Queued rows gated on an unfinished auto-enqueue predecessor stay out - sequenced future work whose predecessor is itself counted. ready_count keeps its meaning via the ready id set built in one pass. Docblock names the semantics.
clanker hai 1 mes
pai
achega
2d7526ff96
Modificáronse 1 ficheiros con 68 adicións e 11 borrados
  1. 68 11
      src/entrypoints/HomeEntrypoint.vala

+ 68 - 11
src/entrypoints/HomeEntrypoint.vala

@@ -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;
+        }
     }
 
     /**