|
|
@@ -206,9 +206,62 @@ namespace ClankerSaddle {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * State builder shared by the goals list row and the goal page:
|
|
|
- * fills a {@link GoalPublic} plus the section lists from a goal
|
|
|
- * row. `full` builds the narrative/runs/sections the list skips.
|
|
|
+ * The dependency bundle every goal-view build threads (plan 16
|
|
|
+ * R2, arch item 12): the services and registries {@link GoalState}'s
|
|
|
+ * builders need, constructed once per entrypoint or refresh —
|
|
|
+ * one object where the 17-parameter populate signature (and
|
|
|
+ * build's own 10-dependency thread) used to be.
|
|
|
+ */
|
|
|
+ public class GoalViewContext : Object {
|
|
|
+ public GoalService goals { get; set; }
|
|
|
+ public TaskService tasks { get; set; }
|
|
|
+ public ProjectService projects { get; set; }
|
|
|
+ public InvercargillSql.Orm.OrmSession db { get; set; }
|
|
|
+ public ActionRegistry? actions { get; set; default = null; }
|
|
|
+ public SaddleService? saddles { get; set; default = null; }
|
|
|
+ public SaddlePollState? polls { get; set; default = null; }
|
|
|
+ public RuleService? rules { get; set; default = null; }
|
|
|
+
|
|
|
+ public GoalViewContext(GoalService goals, TaskService tasks,
|
|
|
+ ProjectService projects, InvercargillSql.Orm.OrmSession db,
|
|
|
+ ActionRegistry? actions = null, SaddleService? saddles = null,
|
|
|
+ SaddlePollState? polls = null, RuleService? rules = null) {
|
|
|
+ this.goals = goals;
|
|
|
+ this.tasks = tasks;
|
|
|
+ this.projects = projects;
|
|
|
+ this.db = db;
|
|
|
+ this.actions = actions;
|
|
|
+ this.saddles = saddles;
|
|
|
+ this.polls = polls;
|
|
|
+ this.rules = rules;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A built goal view (plan 16 R2, arch item 12): the public scalar
|
|
|
+ * half plus the section lists the goal page serialises —
|
|
|
+ * populate's out-params became fields.
|
|
|
+ */
|
|
|
+ public class GoalView : Object {
|
|
|
+ public GoalPublic pub { get; set; default = new GoalPublic(); }
|
|
|
+ public Vector<GoalLinkView> links { get; set; default = new Vector<GoalLinkView>(); }
|
|
|
+ public Vector<GoalBlockView> blocks { get; set; default = new Vector<GoalBlockView>(); }
|
|
|
+ public Vector<GoalBlockView> blocks_open { get; set; default = new Vector<GoalBlockView>(); }
|
|
|
+ public Vector<GoalBlockView> blocks_answered { get; set; default = new Vector<GoalBlockView>(); }
|
|
|
+ public Vector<GoalTaskView> goal_tasks { get; set; default = new Vector<GoalTaskView>(); }
|
|
|
+ public Vector<GoalEventView> events { get; set; default = new Vector<GoalEventView>(); }
|
|
|
+ public Vector<GoalCommentView> comments { get; set; default = new Vector<GoalCommentView>(); }
|
|
|
+ public Vector<GoalRunView> runs { get; set; default = new Vector<GoalRunView>(); }
|
|
|
+ public Vector<GoalPushBranchView> push_branches { get; set; default = new Vector<GoalPushBranchView>(); }
|
|
|
+ public Vector<GoalProgressView> progress { get; set; default = new Vector<GoalProgressView>(); }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * State builder for the goal surfaces: fills a {@link GoalView}
|
|
|
+ * from a goal row — {@link GoalState.full_view} builds the goal
|
|
|
+ * page's whole aggregate (narrative, runs, sections), {@link
|
|
|
+ * GoalState.summary_view} the goals-list row's scalars (the old
|
|
|
+ * `full` flag became the two methods).
|
|
|
*/
|
|
|
public class GoalState : Object {
|
|
|
|
|
|
@@ -217,36 +270,19 @@ namespace ClankerSaddle {
|
|
|
* shape the entrypoint sets and the sealed refresh action
|
|
|
* re-hydrates. `exists` is false when the goal is missing.
|
|
|
*/
|
|
|
- public static async Statum.Slot build(int64 goal_id, GoalService goals,
|
|
|
- TaskService tasks, ProjectService projects,
|
|
|
- InvercargillSql.Orm.OrmSession db, ActionRegistry? actions,
|
|
|
- StateService state_service,
|
|
|
- SaddleService? saddles = null, SaddlePollState? polls = null,
|
|
|
- RuleService? rules = null) throws GLib.Error {
|
|
|
- var pub = new GoalPublic();
|
|
|
- var links = new Vector<GoalLinkView>();
|
|
|
- var blocks = new Vector<GoalBlockView>();
|
|
|
- var blocks_open = new Vector<GoalBlockView>();
|
|
|
- var blocks_answered = new Vector<GoalBlockView>();
|
|
|
- var goal_tasks = new Vector<GoalTaskView>();
|
|
|
- var events = new Vector<GoalEventView>();
|
|
|
- var comments = new Vector<GoalCommentView>();
|
|
|
- var runs = new Vector<GoalRunView>();
|
|
|
- var push_branches = new Vector<GoalPushBranchView>();
|
|
|
- var progress = new Vector<GoalProgressView>();
|
|
|
- var effective_rules = new Vector<GoalRuleView>();
|
|
|
-
|
|
|
+ public static async Statum.Slot build(int64 goal_id, GoalViewContext ctx,
|
|
|
+ StateService state_service) throws GLib.Error {
|
|
|
+ var view = new GoalView();
|
|
|
GoalBlockView? plan_pending = null;
|
|
|
GoalBlockView? completion_pending = null;
|
|
|
var plan_revisions = new Vector<GoalBlockView>();
|
|
|
var project_options = new Vector<GoalProjectOptionView>();
|
|
|
+ var effective_rules = new Vector<GoalRuleView>();
|
|
|
|
|
|
- var goal = yield goals.get_goal(goal_id);
|
|
|
+ var goal = yield ctx.goals.get_goal(goal_id);
|
|
|
if (goal != null) {
|
|
|
- yield populate(((!)goal), goals, tasks, db, actions, true, pub,
|
|
|
- links, blocks, goal_tasks, events, comments, runs, push_branches,
|
|
|
- progress, saddles, polls, blocks_open, blocks_answered);
|
|
|
- foreach (var block in blocks) {
|
|
|
+ view = yield full_view((!)goal, ctx);
|
|
|
+ foreach (var block in view.blocks) {
|
|
|
if (block.is_plan_review && plan_pending == null) {
|
|
|
plan_pending = block;
|
|
|
}
|
|
|
@@ -257,7 +293,7 @@ namespace ClankerSaddle {
|
|
|
plan_revisions.add(block);
|
|
|
}
|
|
|
}
|
|
|
- foreach (var project in yield projects.list_projects()) {
|
|
|
+ foreach (var project in yield ctx.projects.list_projects()) {
|
|
|
if (project.id != ((!)goal).project_id) {
|
|
|
project_options.add(new GoalProjectOptionView() {
|
|
|
id = project.id,
|
|
|
@@ -269,13 +305,13 @@ namespace ClankerSaddle {
|
|
|
// dispatch ships for the run (and every worker):
|
|
|
// run-task rules, the primary project's, then the
|
|
|
// co-developed goal links' in link order.
|
|
|
- if (rules != null) {
|
|
|
+ if (ctx.rules != null) {
|
|
|
var scope_task = new TaskEntity() {
|
|
|
- id = pub.run_task_id,
|
|
|
+ id = view.pub.run_task_id,
|
|
|
project_id = ((!)goal).project_id
|
|
|
};
|
|
|
- foreach (var rule in yield ((!)rules).effective_rules_for(
|
|
|
- scope_task, projects, ((!)goal).id)) {
|
|
|
+ foreach (var rule in yield ((!)ctx.rules).effective_rules_for(
|
|
|
+ scope_task, ctx.projects, ((!)goal).id)) {
|
|
|
effective_rules.add(new GoalRuleView() {
|
|
|
id = rule.id,
|
|
|
name = rule.name,
|
|
|
@@ -285,44 +321,44 @@ namespace ClankerSaddle {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (actions != null && goal != null) {
|
|
|
- pub.refresh = ((!)actions)
|
|
|
+ if (ctx.actions != null && goal != null) {
|
|
|
+ view.pub.refresh = ((!)ctx.actions)
|
|
|
.author_private<RefreshGoalDataAction, GoalActionPrivate>(
|
|
|
new GoalActionPrivate() { goal_id = goal_id }, "GET");
|
|
|
}
|
|
|
|
|
|
- var pub_root = (PropertyDictionary) GObjectMapping.to_properties(pub);
|
|
|
- pub_root["links"] = StateJson.list<GoalLinkView>(links.to_immutable_buffer());
|
|
|
- pub_root["blocks"] = StateJson.list_rich<GoalBlockView>(blocks.to_immutable_buffer(),
|
|
|
+ var pub_root = (PropertyDictionary) GObjectMapping.to_properties(view.pub);
|
|
|
+ pub_root["links"] = StateJson.list<GoalLinkView>(view.links.to_immutable_buffer());
|
|
|
+ pub_root["blocks"] = StateJson.list_rich<GoalBlockView>(view.blocks.to_immutable_buffer(),
|
|
|
(item, dict) => {
|
|
|
dict["options"] = StateJson.strings(
|
|
|
((GoalBlockView) item).options ?? new string[0]);
|
|
|
});
|
|
|
pub_root["open_blocks"] = StateJson.list_rich<GoalBlockView>(
|
|
|
- blocks_open.to_immutable_buffer(),
|
|
|
+ view.blocks_open.to_immutable_buffer(),
|
|
|
(item, dict) => {
|
|
|
dict["options"] = StateJson.strings(
|
|
|
((GoalBlockView) item).options ?? new string[0]);
|
|
|
});
|
|
|
pub_root["answered_blocks"] = StateJson.list_rich<GoalBlockView>(
|
|
|
- blocks_answered.to_immutable_buffer(),
|
|
|
+ view.blocks_answered.to_immutable_buffer(),
|
|
|
(item, dict) => {
|
|
|
dict["options"] = StateJson.strings(
|
|
|
((GoalBlockView) item).options ?? new string[0]);
|
|
|
});
|
|
|
pub_root["goal_tasks"] = StateJson.list_rich<GoalTaskView>(
|
|
|
- goal_tasks.to_immutable_buffer(),
|
|
|
+ view.goal_tasks.to_immutable_buffer(),
|
|
|
(item, dict) => {
|
|
|
dict["capabilities"] = StateJson.strings(
|
|
|
((GoalTaskView) item).capabilities ?? new string[0]);
|
|
|
});
|
|
|
- pub_root["events"] = StateJson.list<GoalEventView>(events.to_immutable_buffer());
|
|
|
- pub_root["comments"] = StateJson.list<GoalCommentView>(comments.to_immutable_buffer());
|
|
|
- pub_root["runs"] = StateJson.list<GoalRunView>(runs.to_immutable_buffer());
|
|
|
+ pub_root["events"] = StateJson.list<GoalEventView>(view.events.to_immutable_buffer());
|
|
|
+ pub_root["comments"] = StateJson.list<GoalCommentView>(view.comments.to_immutable_buffer());
|
|
|
+ pub_root["runs"] = StateJson.list<GoalRunView>(view.runs.to_immutable_buffer());
|
|
|
pub_root["progress"] = StateJson.list<GoalProgressView>(
|
|
|
- progress.to_immutable_buffer());
|
|
|
+ view.progress.to_immutable_buffer());
|
|
|
pub_root["push_branches"] = StateJson.list<GoalPushBranchView>(
|
|
|
- push_branches.to_immutable_buffer());
|
|
|
+ view.push_branches.to_immutable_buffer());
|
|
|
pub_root["plan_revisions"] = StateJson.list<GoalBlockView>(
|
|
|
plan_revisions.to_immutable_buffer());
|
|
|
pub_root["rules"] = StateJson.list<GoalRuleView>(
|
|
|
@@ -343,18 +379,118 @@ namespace ClankerSaddle {
|
|
|
private_data = new PropertyDictionary()
|
|
|
});
|
|
|
}
|
|
|
- public static async void populate(Goal goal, GoalService goals, TaskService tasks,
|
|
|
- InvercargillSql.Orm.OrmSession db, ActionRegistry? actions,
|
|
|
- bool full, GoalPublic pub,
|
|
|
- Vector<GoalLinkView> links, Vector<GoalBlockView> blocks,
|
|
|
- Vector<GoalTaskView> goal_tasks, Vector<GoalEventView> events,
|
|
|
- Vector<GoalCommentView> comments, Vector<GoalRunView> runs,
|
|
|
- Vector<GoalPushBranchView> push_branches,
|
|
|
- Vector<GoalProgressView>? progress = null,
|
|
|
- SaddleService? saddles = null,
|
|
|
- SaddlePollState? polls = null,
|
|
|
- Vector<GoalBlockView>? blocks_open = null,
|
|
|
- Vector<GoalBlockView>? blocks_answered = null) throws GLib.Error {
|
|
|
+ /**
|
|
|
+ * The goals-list row's view (the old `full=false` half): the
|
|
|
+ * {@link GoalPublic} scalars alone — display fields, the
|
|
|
+ * action DTOs, the completion-pending flag, the run-task id
|
|
|
+ * and the task counts. The section lists stay empty: the row
|
|
|
+ * reads none of them, and the old call threaded thirteen
|
|
|
+ * throwaway vectors (and their per-task display/capability
|
|
|
+ * queries) through the full builder to fill views nobody
|
|
|
+ * serialised.
|
|
|
+ */
|
|
|
+ public static async GoalView summary_view(Goal goal,
|
|
|
+ GoalViewContext ctx) throws GLib.Error {
|
|
|
+ var view = new GoalView();
|
|
|
+ var pub = view.pub;
|
|
|
+ pub.exists = true;
|
|
|
+ pub.id = goal.id;
|
|
|
+ pub.project_id = goal.project_id;
|
|
|
+ pub.title = goal.title;
|
|
|
+ pub.brief = goal.brief;
|
|
|
+ pub.branch_name = goal.branch_name;
|
|
|
+ pub.base_branch = goal.base_branch ?? "";
|
|
|
+ pub.status = goal.status;
|
|
|
+ // Plan 16 S5 (U3): a goal awaiting its completion review
|
|
|
+ // reads "awaiting your review" — the run-task status card
|
|
|
+ // agrees (no bare "blocked" for review parks).
|
|
|
+ pub.display = goal.status == GOAL_COMPLETION_REVIEW
|
|
|
+ ? "awaiting your review" : goal.status.replace("_", " ");
|
|
|
+ pub.priority = goal.priority;
|
|
|
+ pub.plan = goal.plan ?? "";
|
|
|
+ pub.completion_report = goal.completion_report ?? "";
|
|
|
+ pub.created_display = format_stamp(goal.created_at);
|
|
|
+ pub.started_display = format_stamp(goal.started_at);
|
|
|
+ pub.completed_display = format_stamp(goal.completed_at);
|
|
|
+ pub.terminal = goal.status == GOAL_COMPLETE || goal.status == GOAL_FAILED
|
|
|
+ || goal.status == GOAL_CANCELLED;
|
|
|
+
|
|
|
+ var project = yield ctx.db.query<Project>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(goal.project_id)))
|
|
|
+ .first_async();
|
|
|
+ if (project != null) {
|
|
|
+ pub.project_name = ((!)project).name;
|
|
|
+ string base_name = goal.base_branch != null && ((!)goal.base_branch).length > 0
|
|
|
+ ? ((!)goal.base_branch) : ((!)project).default_branch;
|
|
|
+ pub.compare_url = @"$(TaskService.https_git_url(((!)project).git_url))/compare/$(base_name)...$(goal.branch_name)";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ctx.actions != null) {
|
|
|
+ var sealed = new GoalActionPrivate() { goal_id = goal.id };
|
|
|
+ pub.comment = ((!)ctx.actions).author_private<GoalCommentAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.add_link = ((!)ctx.actions).author_private<AddGoalLinkAction, GoalActionPrivate>(sealed);
|
|
|
+ if (goal.status == GOAL_DRAFT) {
|
|
|
+ pub.start = ((!)ctx.actions).author_private<StartGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ }
|
|
|
+ if (goal.status == GOAL_ACTIVE) {
|
|
|
+ pub.pause = ((!)ctx.actions).author_private<PauseGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ }
|
|
|
+ if (goal.status == GOAL_PAUSED) {
|
|
|
+ pub.resume = ((!)ctx.actions).author_private<ResumeGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ }
|
|
|
+ if (!pub.terminal) {
|
|
|
+ pub.cancel = ((!)ctx.actions).author_private<CancelGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var block in yield ctx.goals.list_blocks(goal.id)) {
|
|
|
+ if (block.answer == null
|
|
|
+ && (block.block_type == BLOCK_COMPLETION
|
|
|
+ || block.block_type == BLOCK_REVIEW)) {
|
|
|
+ pub.completion_pending = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var task in yield ctx.tasks.list_tasks_for_goal(goal.id)) {
|
|
|
+ if (task.kind == TASK_KIND_GOAL_RUN) {
|
|
|
+ pub.run_task_id = task.id;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ switch (task.status) {
|
|
|
+ case STATUS_DRAFT:
|
|
|
+ pub.draft_tasks++; break;
|
|
|
+ case STATUS_QUEUED:
|
|
|
+ case STATUS_ASSIGNED:
|
|
|
+ pub.queued_tasks++; break;
|
|
|
+ case STATUS_WORKING:
|
|
|
+ case STATUS_BLOCKED:
|
|
|
+ case STATUS_REVIEW_REQUESTED:
|
|
|
+ case STATUS_SUSPENDED:
|
|
|
+ pub.running_tasks++; break;
|
|
|
+ case STATUS_DONE:
|
|
|
+ pub.done_tasks++; break;
|
|
|
+ case STATUS_FAILED:
|
|
|
+ pub.failed_tasks++; break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The goal page's whole view (the old `full=true` half): the
|
|
|
+ * public scalars plus every section list — links, blocks with
|
|
|
+ * the open/answered split, task rows with their computed
|
|
|
+ * displays, the run-control actions, the narrative, the
|
|
|
+ * comment thread, the runs, the live activity and the push
|
|
|
+ * chips.
|
|
|
+ */
|
|
|
+ public static async GoalView full_view(Goal goal,
|
|
|
+ GoalViewContext ctx) throws GLib.Error {
|
|
|
+ var view = new GoalView();
|
|
|
+ var pub = view.pub;
|
|
|
pub.exists = true;
|
|
|
pub.id = goal.id;
|
|
|
pub.project_id = goal.project_id;
|
|
|
@@ -377,7 +513,7 @@ namespace ClankerSaddle {
|
|
|
pub.terminal = goal.status == GOAL_COMPLETE || goal.status == GOAL_FAILED
|
|
|
|| goal.status == GOAL_CANCELLED;
|
|
|
|
|
|
- var project = yield db.query<Project>()
|
|
|
+ var project = yield ctx.db.query<Project>()
|
|
|
.where(expr("id == $0", elem<int64?>(goal.project_id)))
|
|
|
.first_async();
|
|
|
if (project != null) {
|
|
|
@@ -387,46 +523,48 @@ namespace ClankerSaddle {
|
|
|
pub.compare_url = @"$(TaskService.https_git_url(((!)project).git_url))/compare/$(base_name)...$(goal.branch_name)";
|
|
|
}
|
|
|
|
|
|
- if (actions != null) {
|
|
|
+ if (ctx.actions != null) {
|
|
|
var sealed = new GoalActionPrivate() { goal_id = goal.id };
|
|
|
- pub.comment = ((!)actions).author_private<GoalCommentAction, GoalActionPrivate>(sealed);
|
|
|
- pub.add_link = ((!)actions).author_private<AddGoalLinkAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.comment = ((!)ctx.actions).author_private<GoalCommentAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.add_link = ((!)ctx.actions).author_private<AddGoalLinkAction, GoalActionPrivate>(sealed);
|
|
|
if (goal.status == GOAL_DRAFT) {
|
|
|
- pub.start = ((!)actions).author_private<StartGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.start = ((!)ctx.actions).author_private<StartGoalAction, GoalActionPrivate>(sealed);
|
|
|
}
|
|
|
if (goal.status == GOAL_ACTIVE) {
|
|
|
- pub.pause = ((!)actions).author_private<PauseGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.pause = ((!)ctx.actions).author_private<PauseGoalAction, GoalActionPrivate>(sealed);
|
|
|
}
|
|
|
if (goal.status == GOAL_PAUSED) {
|
|
|
- pub.resume = ((!)actions).author_private<ResumeGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.resume = ((!)ctx.actions).author_private<ResumeGoalAction, GoalActionPrivate>(sealed);
|
|
|
}
|
|
|
if (!pub.terminal) {
|
|
|
- pub.cancel = ((!)actions).author_private<CancelGoalAction, GoalActionPrivate>(sealed);
|
|
|
+ pub.cancel = ((!)ctx.actions).author_private<CancelGoalAction, GoalActionPrivate>(sealed);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- foreach (var link in yield goals.list_links(goal.id)) {
|
|
|
- var view = new GoalLinkView() {
|
|
|
+ foreach (var link in yield ctx.goals.list_links(goal.id)) {
|
|
|
+ var link_view = new GoalLinkView() {
|
|
|
id = link.id,
|
|
|
project_id = link.project_id,
|
|
|
relationship = link.relationship,
|
|
|
- remove = ((!)actions).author_private<RemoveGoalLinkAction, GoalActionPrivate>(
|
|
|
- new GoalActionPrivate() { goal_id = goal.id })
|
|
|
+ remove = ctx.actions != null
|
|
|
+ ? ((!)ctx.actions).author_private<RemoveGoalLinkAction, GoalActionPrivate>(
|
|
|
+ new GoalActionPrivate() { goal_id = goal.id })
|
|
|
+ : null
|
|
|
};
|
|
|
- var linked = yield db.query<Project>()
|
|
|
+ var linked = yield ctx.db.query<Project>()
|
|
|
.where(expr("id == $0", elem<int64?>(link.project_id)))
|
|
|
.first_async();
|
|
|
if (linked != null) {
|
|
|
- view.name = ((!)linked).name;
|
|
|
+ link_view.name = ((!)linked).name;
|
|
|
}
|
|
|
- links.add(view);
|
|
|
+ view.links.add(link_view);
|
|
|
}
|
|
|
|
|
|
bool completion_pending = false;
|
|
|
bool plan_pending_open = false;
|
|
|
// Plan 15 C1: the goal's blocks ARE the goal-run task's
|
|
|
// blocks — one row per question, answered in place.
|
|
|
- foreach (var block in yield goals.list_blocks(goal.id)) {
|
|
|
+ foreach (var block in yield ctx.goals.list_blocks(goal.id)) {
|
|
|
bool pending_plan = block.block_type == BLOCK_PLAN
|
|
|
&& block.answer == null;
|
|
|
// The completion review may ride either kind: the web's
|
|
|
@@ -435,7 +573,7 @@ namespace ClankerSaddle {
|
|
|
bool pending_completion = block.answer == null
|
|
|
&& (block.block_type == BLOCK_COMPLETION
|
|
|
|| block.block_type == BLOCK_REVIEW);
|
|
|
- var view = new GoalBlockView() {
|
|
|
+ var block_view = new GoalBlockView() {
|
|
|
id = block.id,
|
|
|
question = block.question,
|
|
|
block_type = block.block_type,
|
|
|
@@ -455,8 +593,8 @@ namespace ClankerSaddle {
|
|
|
approved = block.answer != null
|
|
|
&& block.answer == NOTICE_PLAN_APPROVED
|
|
|
};
|
|
|
- if (block.answer == null && actions != null) {
|
|
|
- view.answer_action = ((!)actions)
|
|
|
+ if (block.answer == null && ctx.actions != null) {
|
|
|
+ block_view.answer_action = ((!)ctx.actions)
|
|
|
.author_private<AnswerGoalBlockAction, GoalBlockAnswerPrivate>(
|
|
|
new GoalBlockAnswerPrivate() {
|
|
|
goal_id = goal.id,
|
|
|
@@ -469,31 +607,29 @@ namespace ClankerSaddle {
|
|
|
if (pending_plan) {
|
|
|
plan_pending_open = true;
|
|
|
}
|
|
|
- blocks.add(view);
|
|
|
+ view.blocks.add(block_view);
|
|
|
}
|
|
|
pub.completion_pending = completion_pending;
|
|
|
// Plan 15 C4: the current (unanswered) question/RFC blocks
|
|
|
// render in full; the historic answered ones fold behind
|
|
|
// the answered-blocks expander (the task page's pattern).
|
|
|
- var open_sink = blocks_open ?? new Vector<GoalBlockView>();
|
|
|
- var answered_sink = blocks_answered ?? new Vector<GoalBlockView>();
|
|
|
- var block_array = blocks.to_array();
|
|
|
+ var block_array = view.blocks.to_array();
|
|
|
for (var index = block_array.length - 1; index >= 0; index--) {
|
|
|
- var view = (GoalBlockView) block_array[index];
|
|
|
- if (view.answered) {
|
|
|
- answered_sink.add(view);
|
|
|
- } else if (view.is_plan_review) {
|
|
|
+ var block_view = (GoalBlockView) block_array[index];
|
|
|
+ if (block_view.answered) {
|
|
|
+ view.blocks_answered.add(block_view);
|
|
|
+ } else if (block_view.is_plan_review) {
|
|
|
// rendered by the plan section
|
|
|
} else {
|
|
|
// Every pending completion-family row renders (the
|
|
|
// raise-side cross-dedupe keeps them to one —
|
|
|
// fresh databases cannot hold pre-dedupe twins, so
|
|
|
// no render-side guard is needed).
|
|
|
- open_sink.add(view);
|
|
|
+ view.blocks_open.add(block_view);
|
|
|
}
|
|
|
}
|
|
|
var goal_task_rows = new Vector<TaskEntity>();
|
|
|
- foreach (var task in yield tasks.list_tasks_for_goal(goal.id)) {
|
|
|
+ foreach (var task in yield ctx.tasks.list_tasks_for_goal(goal.id)) {
|
|
|
if (task.kind == TASK_KIND_GOAL_RUN) {
|
|
|
pub.run_task_id = task.id;
|
|
|
} else {
|
|
|
@@ -503,10 +639,10 @@ namespace ClankerSaddle {
|
|
|
// The rows consume TaskService.display_map_for directly
|
|
|
// (the same computed display the queue and the dashboard
|
|
|
// read) plus the batch yeehaw presence set.
|
|
|
- var display_map = yield tasks.display_map_for(goal_task_rows.to_immutable_buffer());
|
|
|
- var yeehaw_ids = yield tasks.yeehaw_task_ids();
|
|
|
+ var display_map = yield ctx.tasks.display_map_for(goal_task_rows.to_immutable_buffer());
|
|
|
+ var yeehaw_ids = yield ctx.tasks.yeehaw_task_ids();
|
|
|
foreach (var task in goal_task_rows) {
|
|
|
- var view = new GoalTaskView() {
|
|
|
+ var task_view = new GoalTaskView() {
|
|
|
id = task.id,
|
|
|
title = task.title,
|
|
|
status = task.status,
|
|
|
@@ -518,22 +654,22 @@ namespace ClankerSaddle {
|
|
|
// read straight off the task_capabilities links (no
|
|
|
// editing here).
|
|
|
var task_caps = new Vector<string>();
|
|
|
- foreach (var link in yield db.query<TaskCapability>()
|
|
|
+ foreach (var link in yield ctx.db.query<TaskCapability>()
|
|
|
.where(expr("task_id == $0", elem<int64?>(task.id)))
|
|
|
.order_by(expr("id"))
|
|
|
.materialise_async()) {
|
|
|
- var capability = yield db.query<Capability>()
|
|
|
+ var capability = yield ctx.db.query<Capability>()
|
|
|
.where(expr("id == $0", elem<int64?>(link.capability_id)))
|
|
|
.first_async();
|
|
|
if (capability != null) {
|
|
|
task_caps.add(((!)capability).name);
|
|
|
}
|
|
|
}
|
|
|
- view.capabilities = task_caps.to_array();
|
|
|
+ task_view.capabilities = task_caps.to_array();
|
|
|
var display = display_map.get_or_default(task.id.to_string());
|
|
|
- view.display = display != null ? ((!)display).status : task.status;
|
|
|
- view.yeehaw = yeehaw_ids.has(task.id.to_string());
|
|
|
- goal_tasks.add(view);
|
|
|
+ task_view.display = display != null ? ((!)display).status : task.status;
|
|
|
+ task_view.yeehaw = yeehaw_ids.has(task.id.to_string());
|
|
|
+ view.goal_tasks.add(task_view);
|
|
|
switch (task.status) {
|
|
|
case STATUS_DRAFT:
|
|
|
pub.draft_tasks++; break;
|
|
|
@@ -554,22 +690,18 @@ namespace ClankerSaddle {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!full) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
// Plan 14 B1: run-control parity on the status head —
|
|
|
// "Retry supervision" for a failed goal, requeue-to-resume
|
|
|
// for a run stale/parked on a dead saddle (NO notify-me
|
|
|
// toggle: goals notify unconditionally by design).
|
|
|
- if (actions != null && pub.run_task_id > 0) {
|
|
|
- var run_row = yield db.query<TaskEntity>()
|
|
|
+ if (ctx.actions != null && pub.run_task_id > 0) {
|
|
|
+ var run_row = yield ctx.db.query<TaskEntity>()
|
|
|
.where(expr("id == $0", elem<int64?>(pub.run_task_id)))
|
|
|
.first_async();
|
|
|
if (run_row != null) {
|
|
|
string run_status = ((!)run_row).status;
|
|
|
if (goal.status == GOAL_FAILED && run_status == STATUS_FAILED) {
|
|
|
- pub.retry_run = ((!)actions)
|
|
|
+ pub.retry_run = ((!)ctx.actions)
|
|
|
.author_private<RetryGoalRunAction, GoalActionPrivate>(
|
|
|
new GoalActionPrivate() { goal_id = goal.id });
|
|
|
}
|
|
|
@@ -578,14 +710,14 @@ namespace ClankerSaddle {
|
|
|
|| run_status == STATUS_REVIEW_REQUESTED
|
|
|
|| run_status == STATUS_SUSPENDED;
|
|
|
if (!pub.terminal && stale_or_parked) {
|
|
|
- var open_run = yield tasks.open_session_for_task(pub.run_task_id);
|
|
|
+ var open_run = yield ctx.tasks.open_session_for_task(pub.run_task_id);
|
|
|
if (open_run != null) {
|
|
|
- var host = yield db.query<Saddle>()
|
|
|
+ var host = yield ctx.db.query<Saddle>()
|
|
|
.where(expr("id == $0",
|
|
|
elem<int64?>(((!)open_run).saddle_id)))
|
|
|
.first_async();
|
|
|
if (host != null && ((!)host).status == SADDLE_OFFLINE) {
|
|
|
- pub.requeue_run = ((!)actions)
|
|
|
+ pub.requeue_run = ((!)ctx.actions)
|
|
|
.author_private<RequeueGoalRunAction, GoalActionPrivate>(
|
|
|
new GoalActionPrivate() { goal_id = goal.id });
|
|
|
}
|
|
|
@@ -596,8 +728,8 @@ namespace ClankerSaddle {
|
|
|
|
|
|
// Plan 15 C3: the narrative derives at read time (the
|
|
|
// service returns newest-first rows).
|
|
|
- foreach (var row in yield goals.narrative(goal.id)) {
|
|
|
- events.add(new GoalEventView() {
|
|
|
+ foreach (var row in yield ctx.goals.narrative(goal.id)) {
|
|
|
+ view.events.add(new GoalEventView() {
|
|
|
kind = row.kind,
|
|
|
text = row.text,
|
|
|
task_id = row.task_id,
|
|
|
@@ -609,8 +741,8 @@ namespace ClankerSaddle {
|
|
|
// task's task_comments (injection rides the existing
|
|
|
// delivery paths) — the agent's pushes and the human's
|
|
|
// composer land on the same rows.
|
|
|
- foreach (var row in yield goals.list_comments(goal.id)) {
|
|
|
- comments.add(new GoalCommentView() {
|
|
|
+ foreach (var row in yield ctx.goals.list_comments(goal.id)) {
|
|
|
+ view.comments.add(new GoalCommentView() {
|
|
|
author = row.author,
|
|
|
body = row.body,
|
|
|
quote = row.quote ?? "",
|
|
|
@@ -624,55 +756,53 @@ namespace ClankerSaddle {
|
|
|
if (pub.run_task_id > 0) {
|
|
|
// The goal agent's live progress feed (green, task-page
|
|
|
// pattern) — the run task's own progress lines.
|
|
|
- if (progress != null) {
|
|
|
- foreach (var row in yield tasks.list_progress(pub.run_task_id)) {
|
|
|
- ((!)progress).add(new GoalProgressView() {
|
|
|
- id = row.id,
|
|
|
- summary = row.summary,
|
|
|
- when_display = format_stamp(row.created_at)
|
|
|
- });
|
|
|
- }
|
|
|
+ foreach (var row in yield ctx.tasks.list_progress(pub.run_task_id)) {
|
|
|
+ view.progress.add(new GoalProgressView() {
|
|
|
+ id = row.id,
|
|
|
+ summary = row.summary,
|
|
|
+ when_display = format_stamp(row.created_at)
|
|
|
+ });
|
|
|
}
|
|
|
- var session_rows = yield db.query<ClankerSaddle.Data.Session>()
|
|
|
+ var session_rows = yield ctx.db.query<ClankerSaddle.Data.Session>()
|
|
|
.where(expr("task_id == $0", elem<int64?>(pub.run_task_id)))
|
|
|
.order_by_desc(expr("id"))
|
|
|
.materialise_async();
|
|
|
- foreach (var session in session_rows) {
|
|
|
- string run_saddle = "";
|
|
|
- if (saddles != null) {
|
|
|
- var saddle_row = yield ((!)saddles).get_saddle(session.saddle_id);
|
|
|
- run_saddle = saddle_row != null
|
|
|
- ? ((!)saddle_row).name : @"saddle #$(session.saddle_id)";
|
|
|
- }
|
|
|
- runs.add(new GoalRunView() {
|
|
|
- id = session.id,
|
|
|
- started_display = format_stamp(session.started_at),
|
|
|
- ended_display = format_stamp(session.ended_at),
|
|
|
- open = session.ended_at == null,
|
|
|
- outcome = session.outcome ?? "",
|
|
|
- saddle_name = run_saddle,
|
|
|
- // Deep-links this run's terminal view: the
|
|
|
- // picker's client-side ?session_id=
|
|
|
- // selection lands on exactly this session.
|
|
|
- terminal_url = @"/tasks/$(pub.run_task_id)/terminal?session_id=$(session.id)"
|
|
|
- });
|
|
|
+ foreach (var session in session_rows) {
|
|
|
+ string run_saddle = "";
|
|
|
+ if (ctx.saddles != null) {
|
|
|
+ var saddle_row = yield ((!)ctx.saddles).get_saddle(session.saddle_id);
|
|
|
+ run_saddle = saddle_row != null
|
|
|
+ ? ((!)saddle_row).name : @"saddle #$(session.saddle_id)";
|
|
|
}
|
|
|
+ view.runs.add(new GoalRunView() {
|
|
|
+ id = session.id,
|
|
|
+ started_display = format_stamp(session.started_at),
|
|
|
+ ended_display = format_stamp(session.ended_at),
|
|
|
+ open = session.ended_at == null,
|
|
|
+ outcome = session.outcome ?? "",
|
|
|
+ saddle_name = run_saddle,
|
|
|
+ // Deep-links this run's terminal view: the
|
|
|
+ // picker's client-side ?session_id=
|
|
|
+ // selection lands on exactly this session.
|
|
|
+ terminal_url = @"/tasks/$(pub.run_task_id)/terminal?session_id=$(session.id)"
|
|
|
+ });
|
|
|
+ }
|
|
|
// Current activity (task-page parity): the open round's
|
|
|
// hosting saddle, its health and the newest recorded
|
|
|
// activity line.
|
|
|
- var open = yield tasks.open_session_for_task(pub.run_task_id);
|
|
|
+ var open = yield ctx.tasks.open_session_for_task(pub.run_task_id);
|
|
|
if (open != null) {
|
|
|
open_session_saddle = ((!)open).saddle_id;
|
|
|
- if (saddles != null) {
|
|
|
- var saddle_row = yield ((!)saddles).get_saddle(((!)open).saddle_id);
|
|
|
+ if (ctx.saddles != null) {
|
|
|
+ var saddle_row = yield ((!)ctx.saddles).get_saddle(((!)open).saddle_id);
|
|
|
if (saddle_row != null) {
|
|
|
pub.agent_saddle = ((!)saddle_row).name;
|
|
|
- if (polls != null) {
|
|
|
+ if (ctx.polls != null) {
|
|
|
string daemon_state = "";
|
|
|
int64 uptime = 0;
|
|
|
int64 activity = 0;
|
|
|
int64 contact_ms = 0;
|
|
|
- ((!)polls).health_for(((!)saddle_row).id, out daemon_state,
|
|
|
+ ((!)ctx.polls).health_for(((!)saddle_row).id, out daemon_state,
|
|
|
out uptime, out activity, out contact_ms);
|
|
|
if (daemon_state.length > 0) {
|
|
|
pub.saddle_health = @"saddle '$(((!)saddle_row).name)' — daemon $(daemon_state)";
|
|
|
@@ -681,7 +811,7 @@ namespace ClankerSaddle {
|
|
|
}
|
|
|
}
|
|
|
SessionCommand? newest = null;
|
|
|
- foreach (var command in yield db.query<SessionCommand>()
|
|
|
+ foreach (var command in yield ctx.db.query<SessionCommand>()
|
|
|
.where(expr("session_id == $0", elem<int64?>(((!)open).id)))
|
|
|
.order_by(expr("id"))
|
|
|
.materialise_async()) {
|
|
|
@@ -712,7 +842,7 @@ namespace ClankerSaddle {
|
|
|
// doing right now — running the open round, or parked
|
|
|
// awaiting a review / a question / its tasks.
|
|
|
var activity_run = pub.run_task_id > 0
|
|
|
- ? yield db.query<TaskEntity>()
|
|
|
+ ? yield ctx.db.query<TaskEntity>()
|
|
|
.where(expr("id == $0", elem<int64?>(pub.run_task_id)))
|
|
|
.first_async() : null;
|
|
|
if (activity_run != null) {
|
|
|
@@ -729,7 +859,7 @@ namespace ClankerSaddle {
|
|
|
pub.current_activity = "parked awaiting the completion review";
|
|
|
} else if (plan_pending_open) {
|
|
|
pub.current_activity = "parked awaiting the plan review";
|
|
|
- } else if (open_sink.length > 0) {
|
|
|
+ } else if (view.blocks_open.length > 0) {
|
|
|
pub.current_activity = "parked awaiting a question";
|
|
|
} else {
|
|
|
pub.current_activity = "parked";
|
|
|
@@ -744,20 +874,21 @@ namespace ClankerSaddle {
|
|
|
// daemon-reported per-repo flags of the run's hosting
|
|
|
// saddle (the last /status probe) — never a stored
|
|
|
// snapshot. A restart with no probe yet renders none.
|
|
|
- if (open_session_saddle > 0 && polls != null) {
|
|
|
- string flags = ((!)polls).goal_branches_for(open_session_saddle);
|
|
|
+ if (open_session_saddle > 0 && ctx.polls != null) {
|
|
|
+ string flags = ((!)ctx.polls).goal_branches_for(open_session_saddle);
|
|
|
if (flags.length > 0) {
|
|
|
// The SAME parser the completion-accept gate reads
|
|
|
// (GoalService.parse_goal_branches) feeds the chips —
|
|
|
// gate and display cannot diverge on one payload.
|
|
|
foreach (var row in GoalService.parse_goal_branches(flags)) {
|
|
|
- push_branches.add(new GoalPushBranchView() {
|
|
|
+ view.push_branches.add(new GoalPushBranchView() {
|
|
|
branch = row.label(),
|
|
|
pushed = row.pushed
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ return view;
|
|
|
}
|
|
|
|
|
|
/** A flattened, clipped activity line (the current-activity display). */
|
|
|
@@ -787,20 +918,12 @@ namespace ClankerSaddle {
|
|
|
return (!)guard;
|
|
|
}
|
|
|
|
|
|
+ var ctx = new GoalViewContext(goal_service, task_service, project_service,
|
|
|
+ db, action_registry);
|
|
|
var rows = new JsonArray();
|
|
|
foreach (var goal in yield goal_service.list_goals()) {
|
|
|
- var pub = new GoalPublic();
|
|
|
- var links = new Vector<GoalLinkView>();
|
|
|
- var blocks = new Vector<GoalBlockView>();
|
|
|
- var goal_tasks = new Vector<GoalTaskView>();
|
|
|
- var events = new Vector<GoalEventView>();
|
|
|
- var comments = new Vector<GoalCommentView>();
|
|
|
- var runs = new Vector<GoalRunView>();
|
|
|
- var push_branches = new Vector<GoalPushBranchView>();
|
|
|
- yield GoalState.populate(goal, goal_service, task_service, db,
|
|
|
- action_registry, false, pub, links, blocks, goal_tasks,
|
|
|
- events, comments, runs, push_branches);
|
|
|
- var row = (PropertyDictionary) GObjectMapping.to_properties(pub);
|
|
|
+ var view = yield GoalState.summary_view(goal, ctx);
|
|
|
+ var row = (PropertyDictionary) GObjectMapping.to_properties(view.pub);
|
|
|
row["plan_pending"] = new JsonElement.from_element(
|
|
|
new NativeElement<bool>(goal.status == GOAL_PLAN_REVIEW));
|
|
|
rows.add(new JsonElement.from_properties(row));
|
|
|
@@ -857,9 +980,9 @@ namespace ClankerSaddle {
|
|
|
}
|
|
|
|
|
|
int64 goal_id = int64.parse(request.route("goal_id") ?? "0");
|
|
|
- var state = yield GoalState.build(goal_id, goal_service, task_service,
|
|
|
- project_service, db, action_registry, state_service,
|
|
|
- saddle_service, poll_state, rule_service);
|
|
|
+ var ctx = new GoalViewContext(goal_service, task_service, project_service,
|
|
|
+ db, action_registry, saddle_service, poll_state, rule_service);
|
|
|
+ var state = yield GoalState.build(goal_id, ctx, state_service);
|
|
|
return directives().set(state).subscribe_global(StatePulse.SLOT_TYPE);
|
|
|
}
|
|
|
}
|