| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- 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<string, string>();
- 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<string, string>();
- 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<string>();
- 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<string, string>();
- 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<ClankerSaddle.Data.TaskEntity>();
- 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<string, string>();
- foreach (var capability in yield capability_service.list_capabilities()) {
- capability_names.set(capability.id.to_string(), capability.name);
- }
- var required_by_task = new Dictionary<string, Vector<string>>();
- 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<string>();
- 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<Invercargill.DataStructures.HashSet<string>>();
- foreach (var saddle in yield saddle_service.list_idle_saddles()) {
- var carried = new Invercargill.DataStructures.HashSet<string>();
- 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<TaskRowView>();
- 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<TaskProjectOptionView>();
- 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<CreateTaskAction>() : null,
- refresh = actions != null
- ? actions.author<RefreshTasksDataAction>("GET") : null
- };
- var capability_options = new Vector<CapabilityOptionView>();
- 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<GoalOptionView>();
- 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<TaskProjectOptionView>();
- 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<TaskRowView>(rows.to_immutable_buffer(),
- (item, dict) => {
- dict["capabilities"] = StateJson.strings(
- ((TaskRowView) item).capabilities ?? new string[0]);
- });
- props["projects"] = StateJson.list<TaskProjectOptionView>(options.to_immutable_buffer());
- props["capability_options"] = StateJson.list<CapabilityOptionView>(capability_options.to_immutable_buffer());
- props["goal_options"] = StateJson.list<GoalOptionView>(goal_options.to_immutable_buffer());
- props["enqueue_after_options"] = StateJson.list<TaskProjectOptionView>(
- 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<TaskService>();
- private ProjectService project_service = Inversion.inject<ProjectService>();
- private CapabilityService capability_service = Inversion.inject<CapabilityService>();
- private SaddleService saddle_service = Inversion.inject<SaddleService>();
- private GoalService goal_service = Inversion.inject<GoalService>();
- private UserService users = Inversion.inject<UserService>();
- 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<TaskService>();
- private ProjectService project_service = Inversion.inject<ProjectService>();
- private CapabilityService capability_service = Inversion.inject<CapabilityService>();
- private SaddleService saddle_service = Inversion.inject<SaddleService>();
- private GoalService goal_service = Inversion.inject<GoalService>();
- private UserService users = Inversion.inject<UserService>();
- 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);
- }
- }
- }
|