using Invercargill; using Invercargill.DataStructures; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle.Data; using ClankerSaddle.Services; namespace ClankerSaddle { /** A task row on the task queue page, with a dependency summary. */ public class TaskRowView : Object { public int64 id { get; set; } public string title { get; set; default = ""; } public string status { get; set; default = ""; } public string display { get; set; default = ""; } public bool terminal { get; set; } public bool failed { get; set; } public int64 priority { get; set; } public string project_name { get; set; default = ""; } public string dependency_summary { get; set; default = ""; } public bool has_dependencies { get; set; } public bool ready { get; set; } public string[] capabilities { get; set; } public bool waiting_for_saddle { get; set; } public bool unreachable { get; set; } /** * The task earned its 🤠 badge — any of its sessions carries a * `saddle yeehaw` entry (derived, nothing stored for it). */ public bool has_yeehaw { get; set; } /** Plan 12: the task's work mode (badge). */ public string mode { get; set; default = "implement"; } /** Plan 12: the task's creator ("human"/"goal"). */ public string creator { get; set; default = "human"; } /** Plan 12: the goal the task runs under (0 = standalone). */ public int64 goal_id { get; set; } public string goal_title { get; set; default = ""; } /** Plan 12: the auto-enqueue predecessor (0 = none). */ public int64 enqueue_after_task_id { get; set; } public int64 project_id { get; set; } } /** * A project option in the queue page's filter and create forms, * carrying the default branch so the create form can prefill * `branch_from` as the project selection changes. */ public class TaskProjectOptionView : Object { public int64 id { get; set; } public string name { get; set; default = ""; } public string default_branch { get; set; default = ""; } } /** Public data of the `tasks` slot. */ public class TasksPublic : Object { public string filter_status { get; set; default = ""; } public int64 filter_project_id { get; set; } /** Plan 12: the goal filter ("all" | "standalone" | goal id). */ public string filter_goal { get; set; default = "all"; } public Statum.Model.ActionDto? create { get; set; } /** The sealed GET refresh (live pulses + filter re-renders). */ public Statum.Model.ActionDto? refresh { get; set; } } /** * One goal option in the queue page's filter and create forms: * the goal picker for the plan-12 task fan-out view. */ public class GoalOptionView : Object { public int64 id { get; set; } public string title { get; set; default = ""; } public string status { get; set; default = ""; } } /** * The queue page's state builder, shared by the entrypoint and the * sealed refresh action (live pulse rehydration + the client-side * filter clicks re-render through it). */ public class TasksState : Object { public static async Statum.Slot build(StateService state_service, TaskService task_service, ProjectService project_service, CapabilityService capability_service, SaddleService saddle_service, GoalService goal_service, ActionRegistry? actions, StatumRequest? request = null) throws GLib.Error { // Query filters arrive only on direct entrypoint hits (the // browser client's entrypoint fetch never carries the // page's own query string — the selects filter // client-side). string? status = null; if (request != null) { request.query.try_get_any("status", out status); } if (status == null || status.length == 0 || status == "all") { status = null; } string? goal_filter = null; if (request != null) { request.query.try_get_any("goal", out goal_filter); } if (goal_filter == null || goal_filter.length == 0) { goal_filter = "all"; } int64? project_id = null; string? project_text = null; if (request != null) { request.query.try_get_any("project_id", out project_text); } if (project_text != null && project_text.length > 0) { int64 parsed = int64.parse(project_text); if (parsed > 0) { project_id = parsed; } } var projects = yield project_service.list_projects(); var project_names = new Dictionary(); foreach (var project in projects) { project_names.set(project.id.to_string(), project.name); } // ONE materialisation of the task list feeds every consumer // below (the status lookup, the goal-filtered strip and the // enqueue dropdown) — the triple list_tasks died with arch // item 14. var tasks_all = yield task_service.list_tasks(null, null); var statuses = new Dictionary(); foreach (var task in tasks_all) { statuses.set(task.id.to_string(), task.status); } var ready = yield task_service.get_ready_tasks(); var ready_ids = new Invercargill.DataStructures.HashSet(); foreach (var ready_task in ready) { ready_ids.add(ready_task.id.to_string()); } // Computed display state for every listed task (review // requested / delegating / suspended surface here). The // plan-12 goal filter narrows the list AFTER the display // map (the strip needs every task's display state). The // GoalService arrives as a parameter — an inject inside // this async builder would resume past an await where the // injection context stack is empty. var goal_titles = new Dictionary(); foreach (var goal in yield goal_service.list_goals()) { goal_titles.set(goal.id.to_string(), goal.title); } int64 goal_filter_id = 0; bool standalone_only = goal_filter == "standalone"; if (goal_filter != "all" && !standalone_only) { goal_filter_id = int64.parse(goal_filter); } var listed = new Vector(); foreach (var task in tasks_all) { // Goal-agent runs are internal: they never list here — // goals are browsed from the goal page, which alone // exposes the run task (its terminals, its progress). if (task.kind == TASK_KIND_GOAL_RUN) { continue; } if (goal_filter_id > 0 && task.goal_id != goal_filter_id) { continue; } if (standalone_only && task.goal_id > 0) { continue; } listed.add(task); } var display_map = yield task_service.display_map_for(listed.to_immutable_buffer()); // Capability routing context: required names per task, carried // names per idle saddle — a queued task waits for a capable // saddle when no idle saddle's set covers its requirements. var capability_names = new Dictionary(); foreach (var capability in yield capability_service.list_capabilities()) { capability_names.set(capability.id.to_string(), capability.name); } var required_by_task = new Dictionary>(); foreach (var link in yield capability_service.all_task_attachments()) { string name = capability_names.get_or_default(link.capability_id.to_string()) ?? "?"; var names = required_by_task.get_or_default(link.task_id.to_string()); if (names == null) { names = new Vector(); required_by_task.set(link.task_id.to_string(), names); } ((!)names).add(name); } var unreachable_ids = yield task_service.unreachable_task_ids(); // The 🤠 badge's batch presence set: every task with a // yeehaw entry, one DISTINCT query for the whole queue. var yeehaw_ids = yield task_service.yeehaw_task_ids(); var idle_capability_sets = new Vector>(); foreach (var saddle in yield saddle_service.list_idle_saddles()) { var carried = new Invercargill.DataStructures.HashSet(); foreach (var link in yield capability_service.list_saddle_attachments(saddle.id)) { carried.add(capability_names.get_or_default(link.capability_id.to_string()) ?? "?"); } idle_capability_sets.add(carried); } var rows = new Vector(); foreach (var task in listed) { var display = display_map.get_or_default(task.id.to_string()); var view = new TaskRowView() { id = task.id, title = task.title, status = task.status, display = display != null ? ((!)display).status : task.status, terminal = display != null && ((!)display).terminal, failed = display != null && ((!)display).failed, priority = task.priority, project_id = task.project_id, project_name = project_names.get_or_default(task.project_id.to_string()) ?? "", mode = task.mode, creator = task.creator, goal_id = task.goal_id, goal_title = goal_titles.get_or_default(task.goal_id.to_string()) ?? "", enqueue_after_task_id = task.enqueue_after_task_id, ready = ready_ids.has(task.id.to_string()) }; // Plan 12: the dependency summary became the // auto-enqueue-after predecessor note. if (task.enqueue_after_task_id > 0) { view.has_dependencies = true; view.dependency_summary = @"after #$(task.enqueue_after_task_id) (%s)".printf( statuses.get_or_default(task.enqueue_after_task_id.to_string()) ?? "?"); } var required = required_by_task.get_or_default(task.id.to_string()); view.capabilities = required != null ? ((!)required).to_array() : new string[0]; if (view.capabilities.length > 0 && task.status == STATUS_QUEUED) { bool covered = false; foreach (var carried in idle_capability_sets) { bool missing = false; foreach (var name in view.capabilities) { if (!carried.has(name)) { missing = true; break; } } if (!missing) { covered = true; break; } } view.waiting_for_saddle = !covered && idle_capability_sets.length > 0; } view.unreachable = unreachable_ids.has(task.id.to_string()); view.has_yeehaw = yeehaw_ids.has(task.id.to_string()); rows.add(view); } var options = new Vector(); foreach (var project in projects) { options.add(new TaskProjectOptionView() { id = project.id, name = project.name, default_branch = project.default_branch }); } var pub = new TasksPublic() { filter_status = status ?? "", filter_project_id = project_id != null ? ((!)project_id) : 0, filter_goal = goal_filter, create = actions != null ? actions.author() : null, refresh = actions != null ? actions.author("GET") : null }; var capability_options = new Vector(); foreach (var capability in yield capability_service.list_capabilities()) { capability_options.add(new CapabilityOptionView() { id = capability.id, name = capability.name }); } // Plan 12: the goal filter's options (live goals, newest // status badge) and the create form's auto-enqueue-after // dropdown — every human-created worker task of the global // list (goal tasks are excluded: the goal agent sequences // its own). var goal_options = new Vector(); foreach (var goal in yield goal_service.list_goals()) { goal_options.add(new GoalOptionView() { id = goal.id, title = goal.title, status = goal.status }); } var enqueue_options = new Vector(); foreach (var task in tasks_all) { if (task.kind != TASK_KIND_TASK || task.goal_id > 0 || task.creator != CREATOR_HUMAN) { continue; } enqueue_options.add(new TaskProjectOptionView() { id = task.id, name = @"#$(task.id) $(task.title) ($(task.status))" }); } var props = (PropertyDictionary) GObjectMapping.to_properties(pub); props["tasks"] = StateJson.list_rich(rows.to_immutable_buffer(), (item, dict) => { dict["capabilities"] = StateJson.strings( ((TaskRowView) item).capabilities ?? new string[0]); }); props["projects"] = StateJson.list(options.to_immutable_buffer()); props["capability_options"] = StateJson.list(capability_options.to_immutable_buffer()); props["goal_options"] = StateJson.list(goal_options.to_immutable_buffer()); props["enqueue_after_options"] = StateJson.list( enqueue_options.to_immutable_buffer()); return state_service.new_slot(Scope.PAGE, new State() { type_name = "tasks", public_data = props, private_data = new PropertyDictionary() }); } } /** * Entrypoint for /tasks: the filterable, sortable queue. Plan 12: * the status/project/goal filters apply client-side (the Statum * entrypoint fetch carries no page query string), rows carry the * mode badge and goal link, and the create form offers the mode * select and the auto-enqueue-after dropdown. The page subscribes * to the global pulse and re-runs its sealed refresh action. */ public class TasksEntrypoint : StatumEntrypoint { private TaskService task_service = Inversion.inject(); private ProjectService project_service = Inversion.inject(); private CapabilityService capability_service = Inversion.inject(); private SaddleService saddle_service = Inversion.inject(); private GoalService goal_service = Inversion.inject(); private UserService users = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } var slot = yield TasksState.build(state_service, task_service, project_service, capability_service, saddle_service, goal_service, action_registry, request); return directives().set(slot).subscribe_global(StatePulse.SLOT_TYPE); } } /** * The sealed GET refresh action the queue page re-runs on global * pulses and after a filter click (the client-side filter survives * the re-render in window.taskFilters). */ public class RefreshTasksDataAction : StatumAction { private TaskService task_service = Inversion.inject(); private ProjectService project_service = Inversion.inject(); private CapabilityService capability_service = Inversion.inject(); private SaddleService saddle_service = Inversion.inject(); private GoalService goal_service = Inversion.inject(); private UserService users = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } var slot = yield TasksState.build(state_service, task_service, project_service, capability_service, saddle_service, goal_service, action_registry); return directives().set(slot); } } }