TaskActions.vala 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. using Inversion;
  2. using Spry;
  3. using Spry.Authentication;
  4. using Statum;
  5. using ClankerSaddle.Services;
  6. using ClankerSaddle.Data;
  7. namespace ClankerSaddle {
  8. /** Sealed task id carried by the task row actions. */
  9. public class TaskActionPrivate : Object {
  10. public int64 task_id { get; set; }
  11. }
  12. /** Sealed block identity carried by {@link AnswerBlockAction}. */
  13. public class BlockAnswerPrivate : Object {
  14. public int64 task_id { get; set; }
  15. public int64 block_id { get; set; }
  16. }
  17. /**
  18. * Common base for actions sealed against a task id: materialises the
  19. * typed private blob and exposes the resolved id (falling back to the
  20. * hidden `task_id` form field).
  21. */
  22. public abstract class SealedTaskAction : TypedStatumAction<TaskActionPrivate> {
  23. protected override Type private_type { get { return typeof(TaskActionPrivate); } }
  24. /** The sealed task id, or 0 when neither blob nor form field carried one. */
  25. protected int64 sealed_task_id() {
  26. var ctx = request_private;
  27. return ctx != null ? ((!)ctx).task_id : Forms.integer(request, "task_id", 0);
  28. }
  29. }
  30. /**
  31. * Shared base for task-page actions that answer with the
  32. * refreshed task slot instead of a navigate (goal 2 QoL): owns
  33. * the service set {@link TaskDetailState.build} needs, so a
  34. * mutation rebuilds the page view with ONE call -
  35. * {@link task_detail_slot} - instead of every action copy-pasting
  36. * the injections and the build chain. Success paths answer
  37. * notify(...).set(slot); flows whose result genuinely IS
  38. * navigation (create -> detail, delete -> list) keep navigating.
  39. */
  40. public abstract class TaskDetailRefreshingAction : SealedTaskAction {
  41. protected TaskService task_service = inject<TaskService>();
  42. protected ProjectService project_service = inject<ProjectService>();
  43. protected SaddleService saddle_service = inject<SaddleService>();
  44. protected RuleService rule_service = inject<RuleService>();
  45. protected CapabilityService capability_service = inject<CapabilityService>();
  46. protected SaddlePollState poll_state = inject<SaddlePollState>();
  47. protected PushService push_service = inject<PushService>();
  48. protected EventService event_service = inject<EventService>();
  49. protected InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
  50. protected UserService users = inject<UserService>();
  51. /**
  52. * Rebuilds the task detail slot from current data - the ONE
  53. * call a mutation needs to answer with refreshed state (the
  54. * same build the page's refresh action runs).
  55. */
  56. protected async Statum.Slot task_detail_slot(int64 task_id) throws GLib.Error {
  57. var me = SpryAuth.current(request);
  58. return yield TaskDetailState.build(state_service, task_service,
  59. project_service, saddle_service, rule_service, capability_service,
  60. action_registry, poll_state, push_service, db,
  61. me != null ? ((!)me).user_id : 0, task_id);
  62. }
  63. }
  64. /**
  65. * Creates a task from the queue form (project and title required).
  66. * The task lands in DRAFT — the human Enqueue action queues it;
  67. * there is no agent path. Priority clamps to 1–9. Branch fields
  68. * are optional — `branch_from` prefills from the project's default
  69. * branch. The mode select and capabilities apply until a worker is
  70. * assigned; `enqueue_after_task_id` is the human-only auto-enqueue
  71. * predecessor (goal tasks sequence via the goal agent instead).
  72. */
  73. public class CreateTaskAction : StatumAction {
  74. private TaskService tasks = inject<TaskService>();
  75. private CapabilityService capabilities = inject<CapabilityService>();
  76. private UserService users = inject<UserService>();
  77. public override async DirectiveBuilder handle() throws GLib.Error {
  78. var guard = yield SpryAuth.require_login(directives(), request, users);
  79. if (guard != null) {
  80. return (!)guard;
  81. }
  82. int64 project_id = Forms.integer(request, "project_id", 0);
  83. string title = Forms.field(request, "title");
  84. if (project_id <= 0 || title.length == 0) {
  85. return directives().notify("error", "Project and title are required").navigate("/tasks");
  86. }
  87. // Required capabilities: the create form's multi-select posts
  88. // several capability_id fields; every id must resolve before
  89. // the task is created. (A plain array — the Invercargill
  90. // collections mishandle boxed value types in foreach.)
  91. int64[] required = {};
  92. if (request.form != null) {
  93. foreach (var value in request.form.get_field_all("capability_id")) {
  94. int64 parsed = int64.parse(value.strip());
  95. if (parsed <= 0) {
  96. continue;
  97. }
  98. var capability = yield capabilities.get_capability(parsed);
  99. if (capability == null) {
  100. return directives().notify("error",
  101. @"No capability exists with id $(parsed)").navigate("/tasks");
  102. }
  103. required += parsed;
  104. }
  105. }
  106. try {
  107. string mode = Forms.field(request, "mode");
  108. if (mode.length == 0) {
  109. mode = "implement";
  110. }
  111. if (!is_valid_mode(mode)) {
  112. return directives().notify("error",
  113. @"Unknown mode '$(mode)'").navigate("/tasks");
  114. }
  115. int64 enqueue_after = Forms.integer(request, "enqueue_after_task_id", 0);
  116. if (enqueue_after > 0) {
  117. yield tasks.validate_enqueue_after_cycle(-1, enqueue_after);
  118. }
  119. var task = yield tasks.create_task(project_id, title,
  120. Forms.field_raw(request, "description"),
  121. Forms.integer(request, "priority", 5),
  122. Forms.optional(request, "task_branch"),
  123. Forms.optional(request, "branch_from"),
  124. STATUS_DRAFT, TASK_KIND_TASK, mode, CREATOR_HUMAN, 0,
  125. enqueue_after);
  126. foreach (int64 capability_id in required) {
  127. yield capabilities.attach_to_task(capability_id, task.id);
  128. }
  129. return directives().notify("success", @"Task '$(task.title)' created in draft")
  130. .navigate(@"/tasks/$(task.id)");
  131. } catch (Error e) {
  132. return directives().notify("error", @"Could not create the task: $(e.message)").navigate("/tasks");
  133. }
  134. }
  135. }
  136. /**
  137. * Saves the human-editable fields of a task under the draft
  138. * lifecycle's edit matrix (see {@link TaskService.edit_task}): in
  139. * draft every field except status, in non-draft non-terminal
  140. * states priority only, in terminal states nothing. Status is not
  141. * part of the form — Enqueue/Cancel/agent transitions own it.
  142. */
  143. public class UpdateTaskAction : TaskDetailRefreshingAction {
  144. public override async DirectiveBuilder handle() throws GLib.Error {
  145. var guard = yield SpryAuth.require_login(directives(), request, users);
  146. if (guard != null) {
  147. return (!)guard;
  148. }
  149. int64 task_id = sealed_task_id();
  150. if (task_id <= 0) {
  151. return directives().navigate("/tasks");
  152. }
  153. try {
  154. int64? priority = null;
  155. string priority_field = Forms.field(request, "priority");
  156. if (priority_field.length > 0) {
  157. priority = Forms.integer(request, "priority", 5);
  158. }
  159. // The plan-12 mode select and the auto-enqueue
  160. // predecessor (draft-only fields; -1 clears).
  161. string mode = "";
  162. if (request.form != null && request.form.get_field("mode") != null) {
  163. mode = Forms.field(request, "mode");
  164. }
  165. int64? enqueue_after = null;
  166. string enqueue_field = Forms.field(request, "enqueue_after_task_id");
  167. if (enqueue_field.length > 0) {
  168. int64 parsed = int64.parse(enqueue_field);
  169. enqueue_after = parsed > 0 ? parsed : -1;
  170. }
  171. // The carried fix: the mandate only updates when the
  172. // form actually carried the description field (the
  173. // priority-only edit must not blank it — an absent
  174. // field is "leave unchanged", same as title).
  175. yield task_service.edit_task(task_id,
  176. Forms.optional(request, "title"),
  177. request.form != null && request.form.get_field("description") != null
  178. ? Forms.field(request, "description") : null,
  179. priority,
  180. request.form != null && request.form.get_field("task_branch") != null
  181. ? Forms.field(request, "task_branch") : null,
  182. request.form != null && request.form.get_field("branch_from") != null
  183. ? Forms.field(request, "branch_from") : null,
  184. mode.length > 0 ? mode : null,
  185. enqueue_after);
  186. return directives().notify("success", "Task saved").set(yield task_detail_slot(task_id));
  187. } catch (Error e) {
  188. return directives().notify("error", @"Could not save the task: $(e.message)")
  189. .navigate(@"/tasks/$(task_id)");
  190. }
  191. }
  192. }
  193. /**
  194. * Appends a comment to a task as the signed-in user. An optional
  195. * `quote` field carries text the user highlighted in the terminal log
  196. * or an RFC body; the quote is stored with the comment and delivered
  197. * to the saddle as a quote block.
  198. */
  199. public class AddCommentAction : SealedTaskAction {
  200. private TaskService tasks = inject<TaskService>();
  201. private UserService users = inject<UserService>();
  202. public override async DirectiveBuilder handle() throws GLib.Error {
  203. var guard = yield SpryAuth.require_login(directives(), request, users);
  204. if (guard != null) {
  205. return (!)guard;
  206. }
  207. int64 task_id = sealed_task_id();
  208. string body = Forms.field_raw(request, "body");
  209. string quote = Forms.field_raw(request, "quote");
  210. if (task_id <= 0 || body.length == 0) {
  211. return directives().notify("error", "A comment body is required").navigate(@"/tasks/$(task_id)");
  212. }
  213. var me = SpryAuth.current(request);
  214. try {
  215. yield tasks.add_comment(task_id, @"user:$(me.username)", body, quote);
  216. // Terminals post with a return_to so the live view survives
  217. // the comment; every other caller lands on the task page.
  218. string return_to = Forms.field(request, "return_to");
  219. string destination = return_to.length > 0 && return_to.has_prefix("/")
  220. ? return_to : @"/tasks/$(task_id)";
  221. return directives().notify("success", "Comment added").navigate(destination);
  222. } catch (Error e) {
  223. return directives().notify("error", @"Could not add the comment: $(e.message)")
  224. .navigate(@"/tasks/$(task_id)");
  225. }
  226. }
  227. }
  228. /**
  229. * Records the human's answer to a block; the task and block ids come
  230. * from the sealed {@link BlockAnswerPrivate} blob. Question blocks
  231. * submit either a chosen `option` or the free-text `other` field.
  232. * Review and plan blocks submit a `feedback` reply which is stored
  233. * wrapped in the stable verdict wording ("Review feedback
  234. * received:\n…" / "Plan feedback received:\n…") — the answer is the
  235. * resume_notice the daemon reads. Plan 16 S2/R4: accept forms post
  236. * a `verdict` marker (`accept`) instead of the stable constants —
  237. * the server resolves the block kind and owns every verdict string.
  238. * Accepting a plan (the stable approval wording) also applies the
  239. * submitted plan markdown to task.plan. Question/RFC answers are
  240. * stored verbatim — the resume-notice builder applies the "Human
  241. * selected: …" formatting.
  242. */
  243. public class AnswerBlockAction : TypedStatumAction<BlockAnswerPrivate> {
  244. private TaskService tasks = inject<TaskService>();
  245. private GoalService goals = inject<GoalService>();
  246. private UserService users = inject<UserService>();
  247. protected override Type private_type { get { return typeof(BlockAnswerPrivate); } }
  248. public override async DirectiveBuilder handle() throws GLib.Error {
  249. var guard = yield SpryAuth.require_login(directives(), request, users);
  250. if (guard != null) {
  251. return (!)guard;
  252. }
  253. var ctx = request_private;
  254. if (ctx == null) {
  255. return directives().navigate("/tasks");
  256. }
  257. int64 task_id = ((!)ctx).task_id;
  258. string option = Forms.field(request, "option");
  259. string other = Forms.field_raw(request, "other");
  260. string answer = Forms.field_raw(request, "answer");
  261. string feedback = Forms.field_raw(request, "feedback");
  262. string verdict = Forms.field(request, "verdict");
  263. if (option == "__other__") {
  264. answer = other.strip().length > 0 ? other.strip() : answer;
  265. } else if (option.length > 0) {
  266. answer = option;
  267. } else if (verdict == "accept" || feedback.strip().length > 0) {
  268. // S2/R4: the server owns the verdict wording — the
  269. // block's kind picks the stable string.
  270. string type = "";
  271. var detail = yield tasks.get_task(task_id);
  272. if (detail != null) {
  273. foreach (var candidate in ((!)detail).blocks) {
  274. if (candidate.id == ((!)ctx).block_id) {
  275. type = candidate.block_type;
  276. break;
  277. }
  278. }
  279. }
  280. if (verdict == "accept") {
  281. answer = type == BLOCK_PLAN
  282. ? NOTICE_PLAN_APPROVED : NOTICE_REVIEW_APPROVED;
  283. } else {
  284. answer = type == BLOCK_PLAN
  285. ? @"$(NOTICE_PLAN_FEEDBACK)\n$(feedback.strip())"
  286. : @"$(NOTICE_REVIEW_FEEDBACK)\n$(feedback.strip())";
  287. }
  288. }
  289. if (answer.length == 0) {
  290. return directives().notify("error", "An answer is required").navigate(@"/tasks/$(task_id)");
  291. }
  292. try {
  293. // Plan 15 C1: a goal-run task's blocks ARE the goal's
  294. // blocks — the deep-link answer funnels into the ONE
  295. // goal answer path (verdict transitions + row settle +
  296. // resume machinery); every other task answers plainly.
  297. // The answer rides the parked workspace's resume
  298. // verbatim (the stable verdicts).
  299. var detail = yield tasks.get_task(task_id);
  300. if (detail != null && ((!)detail).task.kind == TASK_KIND_GOAL_RUN
  301. && ((!)detail).task.goal_id > 0) {
  302. var me = SpryAuth.current(request);
  303. yield goals.answer_block(((!)ctx).block_id, answer,
  304. me != null ? @"user:$(me.username)" : null);
  305. } else {
  306. yield tasks.answer_block(((!)ctx).block_id, answer);
  307. }
  308. return directives().notify("success", "Answer recorded; the saddle will collect it")
  309. .navigate(@"/tasks/$(task_id)");
  310. } catch (Error e) {
  311. return directives().notify("error", @"Could not answer the block: $(e.message)")
  312. .navigate(@"/tasks/$(task_id)");
  313. }
  314. }
  315. }
  316. /**
  317. * The human-only Enqueue action: moves a draft task into the
  318. * queue. Refused with a shown error while any dependency is still
  319. * in draft; there is no agent path to enqueueing whatsoever.
  320. */
  321. public class EnqueueTaskAction : SealedTaskAction {
  322. private TaskService tasks = inject<TaskService>();
  323. private UserService users = inject<UserService>();
  324. public override async DirectiveBuilder handle() throws GLib.Error {
  325. var guard = yield SpryAuth.require_login(directives(), request, users);
  326. if (guard != null) {
  327. return (!)guard;
  328. }
  329. int64 task_id = sealed_task_id();
  330. if (task_id <= 0) {
  331. return directives().navigate("/tasks");
  332. }
  333. try {
  334. yield tasks.enqueue_task(task_id, CREATOR_HUMAN);
  335. return directives().notify("success", "Task enqueued").navigate(@"/tasks/$(task_id)");
  336. } catch (Error e) {
  337. return directives().notify("error", @"Could not enqueue the task: $(e.message)")
  338. .navigate(@"/tasks/$(task_id)");
  339. }
  340. }
  341. }
  342. /**
  343. * The human-only Delete action: removes a draft task that nothing
  344. * depends on (an error names the dependents otherwise — they must
  345. * be deleted first). Non-draft tasks are refused.
  346. */
  347. public class DeleteTaskAction : SealedTaskAction {
  348. private TaskService tasks = inject<TaskService>();
  349. private UserService users = inject<UserService>();
  350. public override async DirectiveBuilder handle() throws GLib.Error {
  351. var guard = yield SpryAuth.require_login(directives(), request, users);
  352. if (guard != null) {
  353. return (!)guard;
  354. }
  355. int64 task_id = sealed_task_id();
  356. if (task_id <= 0) {
  357. return directives().navigate("/tasks");
  358. }
  359. // Plan 12: goal tasks are the goal agent's to delete (the
  360. // task CLI callback under its goal) — humans cancel goals
  361. // instead; the cascade closes their tasks.
  362. var detail = yield tasks.get_task(task_id);
  363. if (detail != null && (((!)detail).task.creator != CREATOR_HUMAN
  364. || ((!)detail).task.kind == TASK_KIND_GOAL_RUN)) {
  365. return directives().notify("error",
  366. "Goal tasks are managed by their goal agent — cancel the goal instead")
  367. .navigate(@"/tasks/$(task_id)");
  368. }
  369. try {
  370. yield tasks.delete_task(task_id);
  371. return directives().notify("success", "Draft task deleted").navigate("/tasks");
  372. } catch (Error e) {
  373. return directives().notify("error", @"Could not delete the task: $(e.message)")
  374. .navigate(@"/tasks/$(task_id)");
  375. }
  376. }
  377. }
  378. /**
  379. * Cancels a task (status 'failed'); the sealed id comes from the task
  380. * page's cancel button. Only non-draft non-terminal tasks offer the
  381. * action — drafts are deleted, terminal tasks are already finished,
  382. * and completion itself is agent-only. The abort-before-cancel
  383. * orchestration lives in {@link LifecycleService.cancel_task}.
  384. */
  385. public class CancelTaskAction : SealedTaskAction {
  386. private TaskService tasks = inject<TaskService>();
  387. private LifecycleService lifecycle = inject<LifecycleService>();
  388. private UserService users = inject<UserService>();
  389. public override async DirectiveBuilder handle() throws GLib.Error {
  390. var guard = yield SpryAuth.require_login(directives(), request, users);
  391. if (guard != null) {
  392. return (!)guard;
  393. }
  394. int64 task_id = sealed_task_id();
  395. if (task_id <= 0) {
  396. return directives().navigate("/tasks");
  397. }
  398. var detail = yield tasks.get_task(task_id);
  399. if (detail == null) {
  400. return directives().navigate("/tasks");
  401. }
  402. string status = ((!)detail).task.status;
  403. if (status == STATUS_DRAFT || status == STATUS_DONE || status == STATUS_FAILED) {
  404. return directives().notify("error",
  405. "Only non-draft, non-terminal tasks can be cancelled")
  406. .navigate(@"/tasks/$(task_id)");
  407. }
  408. try {
  409. yield lifecycle.cancel_task(task_id);
  410. return directives().notify("success", "Task cancelled").navigate(@"/tasks/$(task_id)");
  411. } catch (Error e) {
  412. return directives().notify("error", @"Could not cancel the task: $(e.message)")
  413. .navigate(@"/tasks/$(task_id)");
  414. }
  415. }
  416. }
  417. /**
  418. * Retries a failed task: the requeue path with the failure summary
  419. * cleared (plan 16 S4: the terminal cache dies with its attempt —
  420. * nothing derived is copied to the thread), all other history
  421. * kept. The next dispatch resumes the daemon-side workspace when
  422. * it still exists; the abort-first orchestration lives in
  423. * {@link LifecycleService.retry_task}.
  424. */
  425. public class RetryTaskAction : SealedTaskAction {
  426. private TaskService tasks = inject<TaskService>();
  427. private LifecycleService lifecycle = inject<LifecycleService>();
  428. private UserService users = inject<UserService>();
  429. public override async DirectiveBuilder handle() throws GLib.Error {
  430. var guard = yield SpryAuth.require_login(directives(), request, users);
  431. if (guard != null) {
  432. return (!)guard;
  433. }
  434. int64 task_id = sealed_task_id();
  435. if (task_id <= 0) {
  436. return directives().navigate("/tasks");
  437. }
  438. var detail = yield tasks.get_task(task_id);
  439. if (detail == null) {
  440. return directives().navigate("/tasks");
  441. }
  442. if (((!)detail).task.status != STATUS_FAILED) {
  443. return directives().notify("error", "Only failed tasks can be retried")
  444. .navigate(@"/tasks/$(task_id)");
  445. }
  446. try {
  447. yield lifecycle.retry_task(task_id);
  448. return directives().notify("success", "Task requeued for another attempt")
  449. .navigate(@"/tasks/$(task_id)");
  450. } catch (Error e) {
  451. return directives().notify("error", @"Could not retry the task: $(e.message)")
  452. .navigate(@"/tasks/$(task_id)");
  453. }
  454. }
  455. }
  456. /**
  457. * Goal 4 task 1 (Force Wake): the human recovery lever for a
  458. * parked PLAIN task — sets the task force-wake flag so the
  459. * allocation offers it again on its affine saddle with the
  460. * explicit wake notice. A suspended GOAL RUN task routes to the
  461. * SAME goal force-wake (GoalForceWakeAction) — one path, no
  462. * duplicate semantics; the page authors that variant's button.
  463. */
  464. public class TaskForceWakeAction : TaskDetailRefreshingAction {
  465. private GoalService goal_service = inject<GoalService>();
  466. public override async DirectiveBuilder handle() throws GLib.Error {
  467. var guard = yield SpryAuth.require_login(directives(), request, users);
  468. if (guard != null) {
  469. return (!)guard;
  470. }
  471. int64 task_id = sealed_task_id();
  472. if (task_id <= 0) {
  473. return directives().navigate("/tasks");
  474. }
  475. try {
  476. var detail = yield task_service.get_task(task_id);
  477. if (detail == null) {
  478. return directives().notify("error", @"No task exists with id $(task_id)").navigate("/tasks");
  479. }
  480. var task = ((!)detail).task;
  481. if (task.kind == TASK_KIND_GOAL_RUN) {
  482. // One path: the run's wake is the GOAL's wake.
  483. yield goal_service.force_wake_goal(task.goal_id);
  484. return directives().notify("success",
  485. "Goal run force-woke — the parked agent resumes on its saddle")
  486. .navigate(@"/goals/$(task.goal_id)");
  487. }
  488. if (task.status != STATUS_SUSPENDED) {
  489. return directives().notify("error",
  490. "Only parked tasks can be force-woke — nothing was woken")
  491. .navigate(@"/tasks/$(task_id)");
  492. }
  493. task.force_wake = true;
  494. yield db.update_async<TaskEntity>(task);
  495. var wake_payload = new Json.Object();
  496. wake_payload.set_string_member("text",
  497. "The user explicitly force-woke this parked task — resume where you left off.");
  498. yield event_service.append(EVENT_FORCE_WAKE, EVENT_ACTOR_USER,
  499. EVENT_SCOPE_TASK, task_id, wake_payload);
  500. return directives().notify("success",
  501. "Task force-woke — the parked run resumes on its saddle")
  502. .set(yield task_detail_slot(task_id));
  503. } catch (Error e) {
  504. return directives().notify("error", @"Could not force-woke the task: $(e.message)")
  505. .navigate(@"/tasks/$(task_id)");
  506. }
  507. }
  508. }
  509. /**
  510. * Manually requeues a task stuck working/blocked on a saddle that
  511. * went offline (the automatic staleness probe may never fire when
  512. * the saddle row was refreshed to offline first). The same path as
  513. * the dispatcher's abandon: requeue + system comment + saddle
  514. * offline — owned by {@link LifecycleService.requeue_for_resume}.
  515. */
  516. public class ResumeTaskAction : SealedTaskAction {
  517. private LifecycleService lifecycle = inject<LifecycleService>();
  518. private UserService users = inject<UserService>();
  519. public override async DirectiveBuilder handle() throws GLib.Error {
  520. var guard = yield SpryAuth.require_login(directives(), request, users);
  521. if (guard != null) {
  522. return (!)guard;
  523. }
  524. int64 task_id = sealed_task_id();
  525. if (task_id <= 0) {
  526. return directives().navigate("/tasks");
  527. }
  528. try {
  529. yield lifecycle.requeue_for_resume(task_id);
  530. return directives().notify("success",
  531. "Task requeued; the daemon restores its context on the next dispatch")
  532. .navigate(@"/tasks/$(task_id)");
  533. } catch (Error e) {
  534. return directives().notify("error", @"Could not requeue the task: $(e.message)")
  535. .navigate(@"/tasks/$(task_id)");
  536. }
  537. }
  538. }
  539. /**
  540. * The task detail page's polling action: rebuilds the `task` slot from
  541. * current data so the three-second client poll shows live comments,
  542. * blocks, output-driven status changes and completions. Navigates
  543. * nowhere; a failed reload leaves the client's state untouched.
  544. */
  545. public class RefreshTaskDataAction : TaskDetailRefreshingAction {
  546. public override async DirectiveBuilder handle() throws GLib.Error {
  547. var guard = yield SpryAuth.require_login(directives(), request, users);
  548. if (guard != null) {
  549. return (!)guard;
  550. }
  551. int64 task_id = sealed_task_id();
  552. if (task_id <= 0) {
  553. return directives();
  554. }
  555. return directives().set(yield task_detail_slot(task_id));
  556. }
  557. }
  558. }