@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/app/docs/App.tsx +72 -0
  4. package/app/docs/api/client.ts +303 -0
  5. package/app/docs/api/contract.ts +24 -0
  6. package/app/docs/api/docs.ts +38 -0
  7. package/app/docs/api/errors.ts +144 -0
  8. package/app/docs/boot.tsx +58 -0
  9. package/app/docs/components/Badge.tsx +65 -0
  10. package/app/docs/components/Button.tsx +37 -0
  11. package/app/docs/components/Card.tsx +41 -0
  12. package/app/docs/components/ConfirmDialog.tsx +173 -0
  13. package/app/docs/components/CopyButton.tsx +68 -0
  14. package/app/docs/components/Dialog.tsx +230 -0
  15. package/app/docs/components/EmptyState.tsx +34 -0
  16. package/app/docs/components/ErrorView.tsx +70 -0
  17. package/app/docs/components/Field.tsx +93 -0
  18. package/app/docs/components/JsonEditor.tsx +151 -0
  19. package/app/docs/components/JsonView.tsx +338 -0
  20. package/app/docs/components/KeyValue.tsx +63 -0
  21. package/app/docs/components/ProblemBanner.tsx +116 -0
  22. package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
  23. package/app/docs/components/Spinner.tsx +33 -0
  24. package/app/docs/components/Table.tsx +33 -0
  25. package/app/docs/components/Tabs.tsx +226 -0
  26. package/app/docs/components/classNames.ts +4 -0
  27. package/app/docs/components/components.css +691 -0
  28. package/app/docs/components/copy.ts +89 -0
  29. package/app/docs/components/fieldContext.ts +29 -0
  30. package/app/docs/components/inputValues.ts +142 -0
  31. package/app/docs/components/inputs.tsx +387 -0
  32. package/app/docs/components/jsonParse.ts +338 -0
  33. package/app/docs/components/listKeys.ts +30 -0
  34. package/app/docs/config.ts +86 -0
  35. package/app/docs/context.ts +33 -0
  36. package/app/docs/embed.ts +74 -0
  37. package/app/docs/env.d.ts +2 -0
  38. package/app/docs/format.ts +9 -0
  39. package/app/docs/hooks/useUrlTab.ts +16 -0
  40. package/app/docs/main.tsx +11 -0
  41. package/app/docs/queryClient.ts +54 -0
  42. package/app/docs/router.tsx +194 -0
  43. package/app/docs/routing.ts +194 -0
  44. package/app/docs/screens/docs/document.ts +27 -0
  45. package/app/docs/screens/docs/http/OperationView.tsx +407 -0
  46. package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
  47. package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
  48. package/app/docs/screens/docs/http/http.css +405 -0
  49. package/app/docs/screens/docs/http/index.tsx +395 -0
  50. package/app/docs/screens/docs/http/model.ts +604 -0
  51. package/app/docs/screens/docs/http/tryIt.ts +455 -0
  52. package/app/docs/screens/docs/permissions.ts +6 -0
  53. package/app/docs/screens/docs/schema/Prose.tsx +43 -0
  54. package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
  55. package/app/docs/screens/docs/schema/index.ts +30 -0
  56. package/app/docs/screens/docs/schema/resolve.ts +466 -0
  57. package/app/docs/screens/docs/schema/schema.css +137 -0
  58. package/app/docs/screens/docs/schema/skeleton.ts +92 -0
  59. package/app/docs/screens/docs/viewer.tsx +154 -0
  60. package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
  61. package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
  62. package/app/docs/screens/docs/ws/format.ts +82 -0
  63. package/app/docs/screens/docs/ws/index.tsx +286 -0
  64. package/app/docs/screens/docs/ws/items.tsx +786 -0
  65. package/app/docs/screens/docs/ws/model.ts +1178 -0
  66. package/app/docs/screens/docs/ws/panels.tsx +440 -0
  67. package/app/docs/screens/docs/ws/ws.css +231 -0
  68. package/app/docs/shortcuts.ts +94 -0
  69. package/app/docs/styles/base.css +847 -0
  70. package/app/docs/styles/tokens.css +143 -0
  71. package/app/docs/theme.ts +56 -0
  72. package/app/kit/unauthorized.ts +222 -0
  73. package/dist/docs/assets/main-0rchxtv5.js +18 -0
  74. package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
  75. package/dist/docs/assets/main-cqgyvs86.css +1 -0
  76. package/dist/docs/manifest.json +26 -0
  77. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
  78. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
  79. package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
  80. package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
  81. package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
  82. package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
  83. package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
  84. package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
  85. package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
  86. package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
  87. package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
  88. package/dist/jobs/assets/index-02427smp.css +1 -0
  89. package/dist/jobs/assets/index-09z2cjft.js +4 -0
  90. package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
  91. package/dist/jobs/assets/index-18bn0gvy.css +1 -0
  92. package/dist/jobs/assets/index-47pxmyxn.js +4 -0
  93. package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
  94. package/dist/jobs/assets/index-4e55z986.js +4 -0
  95. package/dist/jobs/assets/index-4e55z986.js.map +11 -0
  96. package/dist/jobs/assets/index-5c933424.js +4 -0
  97. package/dist/jobs/assets/index-5c933424.js.map +18 -0
  98. package/dist/jobs/assets/index-5kbyqspm.css +1 -0
  99. package/dist/jobs/assets/index-5ng37hk6.css +1 -0
  100. package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
  101. package/dist/jobs/assets/index-71qs5qyx.css +1 -0
  102. package/dist/jobs/assets/index-btpg4r8b.css +1 -0
  103. package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
  104. package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
  105. package/dist/jobs/assets/index-e0yy2skq.css +1 -0
  106. package/dist/jobs/assets/index-hs60b25v.js +10 -0
  107. package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
  108. package/dist/jobs/assets/index-k2smrr70.js +4 -0
  109. package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
  110. package/dist/jobs/assets/index-q24jw3w9.js +4 -0
  111. package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
  112. package/dist/jobs/assets/index-qfb80xen.css +1 -0
  113. package/dist/jobs/assets/index-rtynx6wj.js +4 -0
  114. package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
  115. package/dist/jobs/assets/live-6k7s6358.js +4 -0
  116. package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
  117. package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
  118. package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
  119. package/dist/jobs/assets/main-3yj6bv90.js +5 -0
  120. package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
  121. package/dist/jobs/assets/main-4g69fn3x.js +5 -0
  122. package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
  123. package/dist/jobs/assets/main-4gte3eqq.js +5 -0
  124. package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
  125. package/dist/jobs/assets/main-4j1fcevh.js +5 -0
  126. package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
  127. package/dist/jobs/assets/main-54qwkz28.css +1 -0
  128. package/dist/jobs/assets/main-5grjvsy7.js +5 -0
  129. package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
  130. package/dist/jobs/assets/main-5zbx60gq.js +5 -0
  131. package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
  132. package/dist/jobs/assets/main-9prgjhg4.js +5 -0
  133. package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
  134. package/dist/jobs/assets/main-a250ffk7.js +17 -0
  135. package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
  136. package/dist/jobs/assets/main-d6ge7w08.js +5 -0
  137. package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
  138. package/dist/jobs/assets/main-gngzht43.js +5 -0
  139. package/dist/jobs/assets/main-gngzht43.js.map +10 -0
  140. package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
  141. package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
  142. package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
  143. package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
  144. package/dist/jobs/assets/main-jkvd92z9.js +5 -0
  145. package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
  146. package/dist/jobs/assets/main-jv5xqd13.js +5 -0
  147. package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
  148. package/dist/jobs/assets/main-jxre7w93.js +5 -0
  149. package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
  150. package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
  151. package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
  152. package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
  153. package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
  154. package/dist/jobs/assets/main-pegp0ygb.js +5 -0
  155. package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
  156. package/dist/jobs/assets/main-qr573853.js +5 -0
  157. package/dist/jobs/assets/main-qr573853.js.map +12 -0
  158. package/dist/jobs/assets/main-rq97rdr4.js +4 -0
  159. package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
  160. package/dist/jobs/assets/main-szjdmydc.js +5 -0
  161. package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
  162. package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
  163. package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
  164. package/dist/jobs/assets/main-ts19j4n0.js +5 -0
  165. package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
  166. package/dist/jobs/assets/main-w93x1dzs.js +5 -0
  167. package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
  168. package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
  169. package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
  170. package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
  171. package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
  172. package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
  173. package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
  174. package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
  175. package/dist/jobs/assets/validation-ysjfy072.js +4 -0
  176. package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
  177. package/dist/jobs/manifest.json +511 -0
  178. package/dts/docs/assets.d.ts +144 -0
  179. package/dts/docs/assets.d.ts.map +1 -0
  180. package/dts/docs/browser/config.d.ts +61 -0
  181. package/dts/docs/browser/config.d.ts.map +1 -0
  182. package/dts/docs/browser/index.d.ts +12 -0
  183. package/dts/docs/browser/index.d.ts.map +1 -0
  184. package/dts/docs/docsUi.d.ts +42 -0
  185. package/dts/docs/docsUi.d.ts.map +1 -0
  186. package/dts/docs/hosted.d.ts +22 -0
  187. package/dts/docs/hosted.d.ts.map +1 -0
  188. package/dts/docs/index.d.ts +18 -0
  189. package/dts/docs/index.d.ts.map +1 -0
  190. package/dts/docs/shell.d.ts +57 -0
  191. package/dts/docs/shell.d.ts.map +1 -0
  192. package/dts/docs/types.d.ts +137 -0
  193. package/dts/docs/types.d.ts.map +1 -0
  194. package/dts/index.d.ts +21 -0
  195. package/dts/index.d.ts.map +1 -0
  196. package/dts/jobs/assets.d.ts +157 -0
  197. package/dts/jobs/assets.d.ts.map +1 -0
  198. package/dts/jobs/browser/config.d.ts +67 -0
  199. package/dts/jobs/browser/config.d.ts.map +1 -0
  200. package/dts/jobs/browser/index.d.ts +10 -0
  201. package/dts/jobs/browser/index.d.ts.map +1 -0
  202. package/dts/jobs/browser/unauthorized.d.ts +9 -0
  203. package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
  204. package/dts/jobs/index.d.ts +24 -0
  205. package/dts/jobs/index.d.ts.map +1 -0
  206. package/dts/jobs/jobsUi.d.ts +63 -0
  207. package/dts/jobs/jobsUi.d.ts.map +1 -0
  208. package/dts/jobs/shell.d.ts +77 -0
  209. package/dts/jobs/shell.d.ts.map +1 -0
  210. package/dts/jobs/types.d.ts +146 -0
  211. package/dts/jobs/types.d.ts.map +1 -0
  212. package/dts/kit/browser/shared.d.ts +10 -0
  213. package/dts/kit/browser/shared.d.ts.map +1 -0
  214. package/dts/kit/browser/unauthorized.d.ts +74 -0
  215. package/dts/kit/browser/unauthorized.d.ts.map +1 -0
  216. package/dts/kit/unauthorized.d.ts +47 -0
  217. package/dts/kit/unauthorized.d.ts.map +1 -0
  218. package/guides/docs.md +170 -0
  219. package/guides/jobs.md +1120 -0
  220. package/lib/docs/assets.ts +427 -0
  221. package/lib/docs/browser/config.ts +66 -0
  222. package/lib/docs/browser/index.ts +24 -0
  223. package/lib/docs/docsUi.ts +623 -0
  224. package/lib/docs/hosted.ts +31 -0
  225. package/lib/docs/index.ts +37 -0
  226. package/lib/docs/shell.ts +156 -0
  227. package/lib/docs/types.ts +149 -0
  228. package/lib/index.ts +27 -0
  229. package/lib/jobs/assets.ts +448 -0
  230. package/lib/jobs/browser/config.ts +65 -0
  231. package/lib/jobs/browser/index.ts +9 -0
  232. package/lib/jobs/browser/unauthorized.ts +11 -0
  233. package/lib/jobs/index.ts +41 -0
  234. package/lib/jobs/jobsUi.ts +709 -0
  235. package/lib/jobs/shell.ts +205 -0
  236. package/lib/jobs/types.ts +162 -0
  237. package/lib/kit/browser/shared.ts +11 -0
  238. package/lib/kit/browser/unauthorized.ts +114 -0
  239. package/lib/kit/unauthorized.ts +222 -0
  240. package/package.json +198 -4
@@ -0,0 +1,32 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["app/jobs/api/runnerActions.ts", "app/jobs/screens/runners/actions/config.ts", "app/jobs/screens/runners/actions/explain.ts", "app/jobs/screens/runners/actions/ConfigEditorDialog.tsx", "app/jobs/screens/runners/actions/gating.ts", "app/jobs/screens/runners/actions/RunnerDialogs.tsx", "app/jobs/screens/runners/actions/schedule.ts", "app/jobs/screens/runners/actions/ScheduleEditorDialog.tsx", "app/jobs/screens/runners/actions/triggerOutcome.tsx", "app/jobs/screens/runners/actions/TriggerDialog.tsx", "app/jobs/screens/runners/actions/index.tsx", "app/jobs/api/runnerLogs.ts", "app/jobs/screens/runners/live.ts", "app/jobs/screens/runners/runnerFormat.ts", "app/jobs/screens/runners/historyPage.ts", "app/jobs/screens/runners/runLogFormat.ts", "app/jobs/screens/runners/runLogGates.ts", "app/jobs/screens/runners/RunLogs.tsx", "app/jobs/screens/runners/RunLogsSection.tsx", "app/jobs/screens/runners/RunRecord.tsx", "app/jobs/screens/runners/RunnerHistory.tsx", "app/jobs/screens/runners/RunnerScreen.tsx", "app/jobs/screens/runners/RunnersListScreen.tsx"],
4
+ "sourcesContent": [
5
+ "import type { ApiClient } from \"./client\";\nimport type {\n ClearRunnerHistoryResultDto,\n KillResultDto,\n KillRunnerBody,\n RunnerConfigBody,\n RunnerConfigDto,\n RunnerPausedDto,\n ScheduleResultDto,\n ScheduleRunnerBody,\n TriggerOutcomeDto,\n TriggerRunnerBody,\n} from \"./types\";\nimport { segment } from \"./client\";\n\n/**\n * The runner writes. After any of them, invalidate\n * `runnerInvalidations(id)` from `./runners`.\n *\n * Bodiless POSTs (pause, resume without `triggerNow`, stats reset) still\n * carry `Content-Type: application/json`: the client adds it to every\n * POST/PUT/PATCH, which the API's JSON rule requires.\n */\n\n/** The API path of a runner, plus a suffix. */\nfunction base(id: string, suffix: string): string {\n return `/runners/${segment(id)}${suffix}`;\n}\n\n/** `POST /runners/:runner/trigger`: 202 started/queued, 200 skipped (both resolve). Sends no body when `body` is empty. */\nexport function triggerRunner(\n api: ApiClient,\n id: string,\n body: TriggerRunnerBody = {},\n): Promise<TriggerOutcomeDto> {\n const hasBody = body.force !== undefined || body.args !== undefined;\n return api.request<TriggerOutcomeDto>(\"POST\", base(id, \"/trigger\"), {\n body: hasBody ? body : undefined,\n });\n}\n\n/** `POST /runners/:runner/pause` (bodiless). */\nexport function pauseRunner(\n api: ApiClient,\n id: string,\n): Promise<RunnerPausedDto> {\n return api.request<RunnerPausedDto>(\"POST\", base(id, \"/pause\"));\n}\n\n/** `POST /runners/:runner/resume`; bodiless unless `triggerNow`. */\nexport function resumeRunner(\n api: ApiClient,\n id: string,\n triggerNow = false,\n): Promise<RunnerPausedDto> {\n return api.request<RunnerPausedDto>(\"POST\", base(id, \"/resume\"), {\n body: triggerNow ? { triggerNow: true } : undefined,\n });\n}\n\n/** `PUT /runners/:runner/schedule`; `null` unschedules. */\nexport function rescheduleRunner(\n api: ApiClient,\n id: string,\n schedule: ScheduleRunnerBody[\"schedule\"],\n): Promise<ScheduleResultDto> {\n return api.request<ScheduleResultDto>(\"PUT\", base(id, \"/schedule\"), {\n body: { schedule } satisfies ScheduleRunnerBody,\n });\n}\n\n/** `POST /runners/:runner/kill`: 202 at once, or 200 once settled with `wait`. Local runners only. */\nexport function killRunner(\n api: ApiClient,\n id: string,\n body: KillRunnerBody = {},\n): Promise<KillResultDto> {\n return api.request<KillResultDto>(\"POST\", base(id, \"/kill\"), {\n body: Object.keys(body).length > 0 ? body : undefined,\n });\n}\n\n/**\n * `PUT /runners/:runner/config`: a **merge patch** of the runner's executor\n * and overlap settings. A key left out is untouched; `null` clears that\n * override, so the runner goes back to what its own code asked for. Answers\n * the configuration after the write, whose `appliedSeq` may still trail\n * `seq` until an owner adopts it.\n */\nexport function configureRunner(\n api: ApiClient,\n id: string,\n body: RunnerConfigBody,\n): Promise<RunnerConfigDto> {\n return api.request<RunnerConfigDto>(\"PUT\", base(id, \"/config\"), { body });\n}\n\n/** `DELETE /runners/:runner/config`: drops every override at once, back to the runner's code. */\nexport function resetRunnerConfig(\n api: ApiClient,\n id: string,\n): Promise<RunnerConfigDto> {\n return api.request<RunnerConfigDto>(\"DELETE\", base(id, \"/config\"));\n}\n\n/** `POST /runners/:runner/stats/reset` (bodiless, 204). Local runners only. */\nexport function resetRunnerStats(api: ApiClient, id: string): Promise<void> {\n return api.request<void>(\"POST\", base(id, \"/stats/reset\"));\n}\n\n/**\n * `DELETE /runners/:runner/history`: removes every finished run (record and\n * log) and keeps each run still in progress, naming them in `kept`. Works for\n * a runner registered in another process; the lifetime counters are\n * untouched (that is {@link resetRunnerStats}).\n */\nexport function clearRunnerHistory(\n api: ApiClient,\n id: string,\n): Promise<ClearRunnerHistoryResultDto> {\n return api.request<ClearRunnerHistoryResultDto>(\n \"DELETE\",\n base(id, \"/history\"),\n );\n}\n",
6
+ "import type {\n ExecutionModeDto,\n RunnerConfigBody,\n RunnerConfigDto,\n RunnerConfigKey,\n RunnerConfigValues,\n} from \"../../../api/types\";\nimport { EXECUTION_MODES, RUNNER_CONFIG_BOUNDS } from \"../../../api/contract\";\nimport { formatNumber } from \"../../../format\";\n\n/**\n * The runner configuration editor's pure half: the form value behind\n * \"Settings…\", its prefill from `RunnerInfoDto.config`, its validation, the\n * `PUT /runners/:runner/config` **merge patch** it becomes, and the warnings\n * the API asks the UI to surface.\n *\n * The API stays the authority — it answers 409 `CONFIG_NOT_ALLOWED` for a\n * mode the runner's code does not permit, and an owner may still refuse an\n * override it has not adopted (`RunnerConfigDto.error`). This catches what\n * can be caught before sending, and says what a change will and will not do.\n */\n\n/**\n * Every execution mode, in the order the picker offers them: the contract's\n * own list, so a mode the API gains reaches the picker without an edit here.\n * `EXECUTION_MODE_LABELS` is keyed by it, so a new one is a compile error\n * until it is given words.\n */\nexport { EXECUTION_MODES };\n\n/**\n * What each execution mode means, for the picker's hint. The values are a\n * worker's `target` kinds too, and the words match its badge.\n */\nexport const EXECUTION_MODE_LABELS: Readonly<Record<ExecutionModeDto, string>> =\n {\n \"child-process\": \"child-process — a child process per run\",\n \"worker-thread\": \"worker-thread — a Worker thread per run\",\n \"in-process\": \"in-process — in the runner's own process\",\n };\n\n/** The two overlap policies, in the order the picker offers them. */\nexport const RUN_MODES: readonly {\n /** The value sent. */\n value: RunnerConfigValues[\"runMode\"];\n /** The text shown. */\n label: string;\n}[] = [\n { value: \"parallel\", label: \"parallel — runs may overlap\" },\n { value: \"single\", label: \"single — one run at a time, cluster-wide\" },\n];\n\n/**\n * The inclusive range a concurrency cap **sent from here** must fall in,\n * straight from the contract, so the field's `min`/`max` are the very numbers\n * the route validates against (400 `VALIDATION`).\n *\n * The ceiling is the API's cap on a remote change, not the runner's: a runner\n * configured in its own code has no ceiling at all, and \"unlimited\" is\n * `maxConcurrency: null` — an empty field — which is not capped either.\n */\nexport const CONCURRENCY_BOUNDS = RUNNER_CONFIG_BOUNDS.maxConcurrency;\n\n/** The two keys `RunnerConfigBody.concurrency` sets together. */\nexport const CONCURRENCY_KEYS: readonly RunnerConfigKey[] = [\n \"runMode\",\n \"maxConcurrency\",\n];\n\n/** How each key is named on screen. */\nexport const CONFIG_KEY_LABELS: Readonly<Record<RunnerConfigKey, string>> = {\n executionMode: \"Execution mode\",\n runMode: \"Run mode\",\n maxConcurrency: \"Max concurrency\",\n};\n\n/**\n * The editor's state. Each of the two patch fields carries its own \"is this\n * an override?\" flag, because clearing an override (`null`) and setting one\n * are different writes; the values stay filled in either way, so turning an\n * override back on loses nothing.\n */\nexport interface ConfigForm {\n /** Whether the execution mode is an override; `false` leaves it to the runner's code. */\n executionModeOverridden: boolean;\n /** The execution mode shown: the override's, or the one in force. */\n executionMode: ExecutionModeDto;\n /** Whether the overlap settings are an override. `runMode` and `maxConcurrency` are one patch field, so they share the flag. */\n concurrencyOverridden: boolean;\n /** Whether runs may overlap. */\n runMode: RunnerConfigValues[\"runMode\"];\n /** The cap in `parallel`, or `undefined` for unlimited. Meaningless in `single`. */\n maxConcurrency: number | undefined;\n}\n\n/** Errors per form field. */\nexport interface ConfigErrors {\n /** Why the cap is unusable. */\n maxConcurrency?: string;\n}\n\n/** Whether `config` overrides either of the two overlap settings. */\nexport function isConcurrencyOverridden(config: RunnerConfigDto): boolean {\n return CONCURRENCY_KEYS.some((key) => config.overridden.includes(key));\n}\n\n/** The form for a runner's stored configuration. */\nexport function formFromConfig(config: RunnerConfigDto): ConfigForm {\n return {\n executionModeOverridden: config.overridden.includes(\"executionMode\"),\n executionMode: config.effective.executionMode,\n concurrencyOverridden: isConcurrencyOverridden(config),\n runMode: config.effective.runMode,\n maxConcurrency: config.effective.maxConcurrency ?? undefined,\n };\n}\n\n/**\n * The values the runner would use once `form` is saved and adopted: an\n * overridden key takes the form's value, one left to the code takes `code`'s\n * (falling back to what is in force, for an API that does not send `code`).\n */\nexport function targetValues(\n form: ConfigForm,\n config: RunnerConfigDto,\n): RunnerConfigValues {\n const fromCode = config.code ?? config.effective;\n return {\n executionMode: form.executionModeOverridden\n ? form.executionMode\n : fromCode.executionMode,\n runMode: form.concurrencyOverridden ? form.runMode : fromCode.runMode,\n maxConcurrency: form.concurrencyOverridden\n ? (form.maxConcurrency ?? null)\n : fromCode.maxConcurrency,\n };\n}\n\n/** What the runner's own code asks for, when the API said; `null` otherwise. */\nexport function codeValue(\n config: RunnerConfigDto,\n key: RunnerConfigKey,\n): string | null {\n if (config.code === undefined) {\n return null;\n }\n if (key === \"maxConcurrency\") {\n return describeCap(config.code.maxConcurrency);\n }\n return config.code[key];\n}\n\n/** A cap in words: a number, or \"unlimited\" for `null`/`undefined`. */\nexport function describeCap(value: number | null | undefined): string {\n return value === null || value === undefined\n ? \"unlimited\"\n : formatNumber(value);\n}\n\n/** What holds the form back, per field. */\nexport function validateConfigForm(form: ConfigForm): ConfigErrors {\n if (!form.concurrencyOverridden || form.runMode !== \"parallel\") {\n return {};\n }\n const cap = form.maxConcurrency;\n if (cap === undefined) {\n return {};\n }\n if (!Number.isInteger(cap)) {\n return {\n maxConcurrency: \"A whole number of runs, or empty for unlimited.\",\n };\n }\n if (cap < CONCURRENCY_BOUNDS.min || cap > CONCURRENCY_BOUNDS.max) {\n return {\n maxConcurrency: `Between ${CONCURRENCY_BOUNDS.min} and ${formatNumber(CONCURRENCY_BOUNDS.max)}, or empty for unlimited.`,\n };\n }\n return {};\n}\n\n/**\n * The `PUT /runners/:runner/config` body: **only what changed**, since the\n * route is a merge patch. A key whose override is being dropped is sent as\n * `null`; an unchanged one is left out entirely. An empty object means there\n * is nothing to save.\n */\nexport function configBody(\n form: ConfigForm,\n config: RunnerConfigDto,\n): RunnerConfigBody {\n const stored = formFromConfig(config);\n const body: RunnerConfigBody = {};\n if (\n form.executionModeOverridden !== stored.executionModeOverridden ||\n (form.executionModeOverridden &&\n form.executionMode !== stored.executionMode)\n ) {\n body.executionMode = form.executionModeOverridden\n ? form.executionMode\n : null;\n }\n const capChanged =\n form.runMode === \"parallel\" &&\n form.maxConcurrency !== stored.maxConcurrency;\n if (\n form.concurrencyOverridden !== stored.concurrencyOverridden ||\n (form.concurrencyOverridden &&\n (form.runMode !== stored.runMode || capChanged))\n ) {\n body.concurrency = !form.concurrencyOverridden\n ? null\n : form.runMode === \"single\"\n ? { runMode: \"single\" }\n : {\n runMode: \"parallel\",\n maxConcurrency: form.maxConcurrency ?? null,\n };\n }\n return body;\n}\n\n/** Whether `form` differs from what is stored, i.e. whether there is anything to send. */\nexport function hasConfigChanges(\n form: ConfigForm,\n config: RunnerConfigDto,\n): boolean {\n return Object.keys(configBody(form, config)).length > 0;\n}\n\n/** One thing a change will (or will not) do, shown before it is saved. */\nexport interface ConfigWarning {\n /** A stable id, also the note's `data-testid` suffix. */\n id: \"execution-mode\" | \"to-single\" | \"lower-concurrency\";\n /** The sentence shown. */\n message: string;\n}\n\n/**\n * What the API asks the UI to say about a pending change. All three are\n * about *when* a change takes hold, never whether it is accepted:\n *\n * - an execution mode applies from the next run, since a run in flight keeps\n * the mode it started with;\n * - `parallel` → `single` is not immediate across processes, because a\n * parallel run holds no lock;\n * - a lowered cap gates new runs only.\n */\nexport function configWarnings(\n form: ConfigForm,\n config: RunnerConfigDto,\n): ConfigWarning[] {\n const target = targetValues(form, config);\n const current = config.effective;\n const warnings: ConfigWarning[] = [];\n if (target.executionMode !== current.executionMode) {\n warnings.push({\n id: \"execution-mode\",\n message: `A run already in flight keeps the mode it started with, so ${target.executionMode} applies from the next run.`,\n });\n }\n if (current.runMode === \"parallel\" && target.runMode === \"single\") {\n warnings.push({\n id: \"to-single\",\n message:\n \"Switching from parallel to single is not immediate across processes: a parallel run holds no lock, so overlap can persist for as long as the longest run in flight, plus however long an owner takes to adopt the change. Pause the runner first if exclusivity matters.\",\n });\n }\n const before = current.maxConcurrency ?? Infinity;\n const after = target.maxConcurrency ?? Infinity;\n if (target.runMode === \"parallel\" && after < before) {\n warnings.push({\n id: \"lower-concurrency\",\n message: `Lowering the cap from ${describeCap(current.maxConcurrency)} to ${describeCap(target.maxConcurrency)} only gates new runs: runs already in flight keep going.`,\n });\n }\n return warnings;\n}\n\n/**\n * The stored override an owner has not picked up yet (`appliedSeq` below\n * `seq`), or `null` when everything in force is adopted. An API that sends no\n * `appliedSeq` has nothing adopted, so a stored override counts as pending.\n */\nexport function pendingAdoption(\n config: RunnerConfigDto,\n): { applied: number; seq: number } | null {\n const applied = config.appliedSeq ?? 0;\n return config.seq > 0 && applied < config.seq\n ? { applied, seq: config.seq }\n : null;\n}\n\n/** The execution modes this runner's code does not permit; `[]` when it permits all of them. */\nexport function unavailableModes(config: RunnerConfigDto): ExecutionModeDto[] {\n const allowed = config.allowed;\n return allowed === undefined\n ? []\n : EXECUTION_MODES.filter((mode) => !allowed.includes(mode));\n}\n\n/** The execution modes to offer: the runner's `allowed`, else all three. */\nexport function availableModes(config: RunnerConfigDto): ExecutionModeDto[] {\n const allowed = config.allowed;\n return allowed === undefined\n ? [...EXECUTION_MODES]\n : EXECUTION_MODES.filter((mode) => allowed.includes(mode));\n}\n\n/**\n * Which settings are overridden, in words: `\"None: as the runner's code\n * asks\"`, else the overridden keys' labels. Shown on the runner's summary.\n */\nexport function describeOverridden(config: RunnerConfigDto): string {\n if (config.overridden.length === 0) {\n return \"None: as the runner's code asks\";\n }\n return config.overridden\n .map((key) => CONFIG_KEY_LABELS[key] ?? key)\n .join(\", \");\n}\n\n/**\n * The settings the owner refused, as `RunnerConfigDto.error.keys` names them;\n * `null` when nothing was refused **or** the refusal names none. An error\n * stored before the API named its keys reads back with `keys: []`, which\n * means \"unknown\", not \"none refused\".\n */\nexport function refusedKeys(config: RunnerConfigDto): RunnerConfigKey[] | null {\n const keys = config.error?.keys;\n return keys === undefined || keys.length === 0 ? null : keys;\n}\n\n/**\n * Whether the owner refused one overridden setting, so it still runs what\n * the code asks for. Read from `error.keys`; where the refusal names no keys\n * (stored before the API did), guessed from the value left in place — the\n * setting runs the code's value — which misreads an override equal to the\n * code's value, hence the keys.\n */\nexport function isRefused(\n config: RunnerConfigDto,\n key: RunnerConfigKey,\n): boolean {\n if (config.error === undefined) {\n return false;\n }\n const keys = refusedKeys(config);\n if (keys !== null) {\n return keys.includes(key);\n }\n return (\n config.code !== undefined && config.effective[key] === config.code[key]\n );\n}\n\n/**\n * The refused settings in words — \"Execution mode\", \"Run mode and Max\n * concurrency\" — or `null` when the refusal names none.\n */\nexport function describeRefused(config: RunnerConfigDto): string | null {\n const keys = refusedKeys(config);\n if (keys === null) {\n return null;\n }\n const labels = keys.map((key) => CONFIG_KEY_LABELS[key] ?? key);\n return labels.length === 1\n ? labels[0]\n : `${labels.slice(0, -1).join(\", \")} and ${labels.at(-1)!}`;\n}\n\n/**\n * The note beside one summary row: whether that setting is an override, and\n * what the runner's code asks for instead. `undefined` when it is the code's\n * own value, or when the runner reports no configuration at all.\n */\nexport function describeOverride(\n config: RunnerConfigDto | undefined,\n key: RunnerConfigKey,\n): string | undefined {\n if (config === undefined || !config.overridden.includes(key)) {\n return undefined;\n }\n const code = codeValue(config, key);\n // Stored but not in force: the owner refused this setting, so it still\n // runs what the code asks for. Saying \"Overridden here\" would read as an\n // override in effect.\n if (isRefused(config, key)) {\n return code === null\n ? \"Override refused by the owner; it runs what its code asks for\"\n : `Override refused by the owner; it runs what its code asks for, ${code}`;\n }\n return code === null\n ? \"Overridden here\"\n : `Overridden here; its code asks for ${code}`;\n}\n\n/**\n * Where the stored override stands with the owner: waiting to be adopted,\n * refused, or `null` when there is nothing to say. A refusal wins, since it\n * is why nothing was adopted.\n */\nexport function describeAdoption(config: RunnerConfigDto): string | null {\n if (config.error !== undefined) {\n const refused = describeRefused(config);\n return refused === null\n ? `The owner refused these settings: ${config.error.message}`\n : `The owner refused ${refused}: ${config.error.message}`;\n }\n const pending = pendingAdoption(config);\n return pending === null\n ? null\n : `Version ${pending.seq} has not been adopted yet (the owner is on ${pending.applied}); it takes effect at its next sync`;\n}\n",
7
+ "import type { TriggerOutcomeDto } from \"../../../api/types\";\nimport { ApiError, isApiError } from \"../../../api/errors\";\nimport { formatNumber, plural } from \"../../../format\";\n\n/** Why a trigger was skipped. */\nexport type SkipReason = Extract<\n TriggerOutcomeDto,\n { outcome: \"skipped\" }\n>[\"reason\"];\n\n/**\n * A skipped trigger's reason in words. Verified against\n * `BunRunner.trigger()` / `RunnerController.trigger()`: `busy`, `lock-held` and\n * `max-concurrency` are skips only because the runner does not queue\n * triggers (`queueRuns: false`); with queueing on they come back `queued`.\n */\nexport const SKIP_REASONS: Readonly<Record<SkipReason, string>> = {\n paused:\n \"The runner is paused. Tick “Run even while paused” to run it anyway, or resume it.\",\n busy: \"A run is already in progress, and this runner does not queue triggers.\",\n \"lock-held\":\n \"Another process holds the runner's lock (a run is in progress there), and this runner does not queue triggers.\",\n \"max-concurrency\":\n \"The runner is at its concurrency limit, and it does not queue triggers.\",\n \"queue-full\":\n \"The runner's trigger queue is full (maxQueuedRuns). Try again once some queued runs have started.\",\n stopped: \"The runner has stopped, so nothing runs.\",\n};\n\n/** A runner write whose failures get a plain-language explanation. */\nexport type RunnerAction =\n | \"trigger\"\n | \"pause\"\n | \"resume\"\n | \"reschedule\"\n | \"kill\"\n | \"resetStats\"\n | \"clearHistory\"\n | \"configure\";\n\n/** What each local-only action does, for the `RUNNER_NOT_LOCAL` explanation. */\nconst LOCAL_ONLY: Partial<Record<RunnerAction, string>> = {\n kill: \"Only the process executing a run can kill it\",\n resetStats: \"Only the process that registered the runner can reset its stats\",\n};\n\n/**\n * The plain-language explanation of a failed runner write, or `null` when\n * the API's own detail says it well enough.\n */\nexport function explainRunnerError(\n error: ApiError,\n action: RunnerAction,\n): string | null {\n switch (error.code) {\n case \"RUNNER_NOT_LOCAL\":\n return `${LOCAL_ONLY[action] ?? \"This action needs the runner registered in the API's process\"}, and this runner is registered only in another process. Use the management API of the process that runs it.`;\n case \"RUN_NOT_FOUND\":\n return \"That run is no longer active in this process: it finished, or was killed already. Refresh to see the runs in flight now.\";\n case \"RUNNER_STOPPED\":\n return \"The runner has stopped (its process is shutting down, or it was stopped), so it cannot be triggered.\";\n case \"ARGS_NOT_ALLOWED\":\n return \"This API does not accept run arguments (it was created without runnerTriggerArgs). Trigger without arguments.\";\n case \"RUNNER_NOT_CONFIGURABLE\":\n return \"This runner cannot be configured from the API yet: no process running it has started since remote configuration shipped, so a setting would be stored and never adopted. Restart the runner on the current version, or change its settings where the runner is defined.\";\n case \"CONFIG_NOT_ALLOWED\":\n return \"The runner's own code does not permit that setting — usually an execution mode outside its `allowedOverrides.executionModes`. Choose one of the modes offered, or widen the list where the runner is defined.\";\n case \"RUNNER_NOT_FOUND\":\n return action === \"clearHistory\"\n ? \"The runner no longer exists in this namespace (it was unregistered, or its state was removed), so it has no history left to clear.\"\n : \"The runner no longer exists in this namespace: it was unregistered, or its state was removed.\";\n default:\n return null;\n }\n}\n\n/**\n * The error to show in a dialog: an `ApiError` whose detail is the\n * explanation when there is one (code, status and context kept), else the\n * error unchanged.\n */\nexport function explained(error: unknown, action: RunnerAction): unknown {\n if (!isApiError(error)) {\n return error;\n }\n const detail = explainRunnerError(error, action);\n if (detail === null) {\n return error;\n }\n return new ApiError({\n kind: error.kind,\n status: error.status,\n code: error.code,\n title: error.title,\n detail,\n type: error.type,\n issues: error.issues,\n context: error.context,\n instance: error.instance,\n cause: error,\n });\n}\n\n/** The success toast of a history clear: how many runs went, and how many in progress were kept. */\nexport function clearHistoryMessage(removed: number, kept: readonly string[]) {\n const keptPart =\n kept.length > 0 ? `, kept ${formatNumber(kept.length)} in progress` : \"\";\n return `Cleared ${plural(removed, \"run\")}${keptPart}`;\n}\n",
8
+ "import type { FormEvent } from \"react\";\nimport type { RunnerConfigBody, RunnerConfigDto } from \"../../../api/types\";\nimport type { ConfigForm } from \"./config\";\nimport type { RunnerDialogProps } from \"./TriggerDialog\";\nimport { useId, useState } from \"react\";\nimport { configureRunner, resetRunnerConfig } from \"../../../api/runnerActions\";\nimport { runnerInvalidations } from \"../../../api/runners\";\nimport { Button } from \"../../../components/Button\";\nimport { Dialog } from \"../../../components/Dialog\";\nimport { Field } from \"../../../components/Field\";\nimport { NumberInput, Select } from \"../../../components/inputs\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../../components/RelativeTime\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport {\n availableModes,\n codeValue,\n CONCURRENCY_BOUNDS,\n configBody,\n configWarnings,\n describeRefused,\n EXECUTION_MODE_LABELS,\n formFromConfig,\n hasConfigChanges,\n pendingAdoption,\n RUN_MODES,\n unavailableModes,\n validateConfigForm,\n} from \"./config\";\nimport { explained } from \"./explain\";\n\n/** Props of {@link ConfigEditorDialog}. */\nexport interface ConfigEditorDialogProps extends RunnerDialogProps {\n /**\n * The runner's configuration, as `GET /runners/:runner` returned it. It is\n * required rather than read off the runner: the Settings… button is offered\n * only for a runner that carries one, so there is nothing to invent here.\n */\n config: RunnerConfigDto;\n}\n\n/**\n * `PUT` / `DELETE /runners/:runner/config`: the runner's execution mode and\n * its overlap settings, with what its own code asked for beside them.\n *\n * Saving sends a **merge patch** of what changed, so an untouched setting is\n * never restated; \"Use the code default\" marks one setting to be cleared on\n * save (`null`), and \"Reset to code defaults\" is the `DELETE`, which drops\n * every override at once.\n */\nexport function ConfigEditorDialog({\n runner,\n config,\n onClose,\n}: ConfigEditorDialogProps) {\n const api = useApiClient();\n const formId = useId();\n const [form, setForm] = useState<ConfigForm>(() => formFromConfig(config));\n const patch = (next: Partial<ConfigForm>) =>\n setForm((current) => ({ ...current, ...next }));\n\n const save = useApiMutation({\n mutationFn: (body: RunnerConfigBody) =>\n configureRunner(api, runner.id, body),\n successMessage: (result) =>\n pendingAdoption(result) === null\n ? `Saved the settings of ${runner.id}`\n : `Saved the settings of ${runner.id}; its owner adopts them at its next sync`,\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n onSuccess: () => onClose(),\n });\n const reset = useApiMutation({\n mutationFn: () => resetRunnerConfig(api, runner.id),\n successMessage: `Reset ${runner.id} to its code defaults`,\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n onSuccess: () => onClose(),\n });\n\n const pending = save.isPending || reset.isPending;\n const errors = validateConfigForm(form);\n const warnings = configWarnings(form, config);\n const body = configBody(form, config);\n const changed = hasConfigChanges(form, config);\n const adoption = pendingAdoption(config);\n const modes = availableModes(config);\n const missingModes = unavailableModes(config);\n const codeMode = codeValue(config, \"executionMode\");\n const codeRunMode = codeValue(config, \"runMode\");\n const codeCap = codeValue(config, \"maxConcurrency\");\n const failure = save.error ?? reset.error;\n // The mode in force can be one the code no longer permits; keep it\n // selectable so the picker never silently shows a different runner. A mode\n // this build has no label for (a newer server's) has an undefined label at\n // runtime, and `Select` shows such an option by its value: raw, not blank.\n const modeOptions = (\n modes.includes(form.executionMode) ? modes : [form.executionMode, ...modes]\n ).map((mode) => ({ value: mode, label: EXECUTION_MODE_LABELS[mode] }));\n\n const onSubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (Object.keys(errors).length > 0 || !changed) {\n return;\n }\n save.mutate(body);\n };\n\n const close = () => {\n if (!pending) {\n onClose();\n }\n };\n\n return (\n <Dialog\n open\n onClose={close}\n title={\n <>\n Settings of <code>{runner.id}</code>\n </>\n }\n description=\"Where runs execute and whether they may overlap, for every process running this runner. A setting left to the runner's code follows the code.\"\n size=\"md\"\n closeOnEscape={!pending}\n closeOnBackdrop={!pending}\n footer={\n <>\n {config.overridden.length > 0 && (\n <Button\n variant=\"danger\"\n onClick={() => reset.mutate()}\n disabled={pending}\n aria-busy={reset.isPending || undefined}\n >\n {reset.isPending ? \"Resetting…\" : \"Reset to code defaults\"}\n </Button>\n )}\n <Button\n onClick={close}\n disabled={pending}\n >\n Cancel\n </Button>\n <Button\n variant=\"primary\"\n type=\"submit\"\n form={formId}\n disabled={pending || !changed}\n aria-busy={save.isPending || undefined}\n >\n {save.isPending ? \"Saving…\" : \"Save settings\"}\n </Button>\n </>\n }\n >\n <form\n id={formId}\n className=\"runner-form\"\n onSubmit={onSubmit}\n noValidate\n >\n {adoption !== null && (\n <p\n className=\"runner-note runner-note-pending\"\n role=\"status\"\n data-testid=\"config-pending\"\n >\n The stored settings (version {adoption.seq}) have not been adopted\n yet: the owner is on version {adoption.applied}. They take effect at\n its next sync.\n </p>\n )}\n {config.error !== undefined && (\n <p\n className=\"runner-note runner-note-refused\"\n role=\"note\"\n data-testid=\"config-refused\"\n >\n The owner refused {describeRefused(config) ?? \"the stored settings\"}{\" \"}\n <RelativeTime value={config.error.at} />: {config.error.message}\n </p>\n )}\n\n <Field\n label=\"Execution mode\"\n hint={\n codeMode === null\n ? \"Where each run executes. It applies from the next run.\"\n : `Where each run executes. This runner's code asks for ${codeMode}.`\n }\n error={save.fieldErrors.executionMode}\n >\n <Select\n options={modeOptions}\n value={form.executionMode}\n onChange={(executionMode) =>\n patch({ executionMode, executionModeOverridden: true })\n }\n disabled={pending}\n />\n </Field>\n <ConfigSource\n testId=\"execution-mode\"\n overridden={form.executionModeOverridden}\n label=\"Execution mode\"\n canReset={config.code !== undefined}\n disabled={pending}\n onReset={() =>\n patch({\n executionModeOverridden: false,\n executionMode: config.code?.executionMode ?? form.executionMode,\n })\n }\n />\n {missingModes.length > 0 && (\n <p\n className=\"runner-note\"\n role=\"note\"\n data-testid=\"config-modes-limited\"\n >\n {missingModes.join(\", \")} {missingModes.length === 1 ? \"is\" : \"are\"}{\" \"}\n not offered: this runner's code permits only {modes.join(\", \")} (its{\" \"}\n <code>allowedOverrides.executionModes</code>). The API refuses any\n other with 409 <code>CONFIG_NOT_ALLOWED</code>.\n </p>\n )}\n\n <Field\n label=\"Run mode\"\n hint={\n codeRunMode === null\n ? \"Whether runs may overlap.\"\n : `Whether runs may overlap. This runner's code asks for ${codeRunMode}.`\n }\n error={save.fieldErrors[\"concurrency.runMode\"]}\n >\n <Select\n options={RUN_MODES}\n value={form.runMode}\n onChange={(runMode) =>\n patch({ runMode, concurrencyOverridden: true })\n }\n disabled={pending}\n />\n </Field>\n\n <Field\n label=\"Max concurrency\"\n hint={\n form.runMode === \"single\"\n ? \"Only applies in parallel: single already allows one run at a time.\"\n : `Most concurrent runs, ${CONCURRENCY_BOUNDS.min} to ${CONCURRENCY_BOUNDS.max} from here. Empty means unlimited.${\n codeCap === null\n ? \"\"\n : ` This runner's code asks for ${codeCap}.`\n }`\n }\n error={\n errors.maxConcurrency ??\n save.fieldErrors[\"concurrency.maxConcurrency\"]\n }\n >\n <NumberInput\n value={form.maxConcurrency}\n onChange={(maxConcurrency) =>\n patch({ maxConcurrency, concurrencyOverridden: true })\n }\n min={CONCURRENCY_BOUNDS.min}\n max={CONCURRENCY_BOUNDS.max}\n placeholder=\"Unlimited\"\n disabled={pending || form.runMode === \"single\"}\n />\n </Field>\n <ConfigSource\n testId=\"concurrency\"\n overridden={form.concurrencyOverridden}\n label=\"Run mode and max concurrency\"\n canReset={config.code !== undefined}\n disabled={pending}\n onReset={() =>\n patch({\n concurrencyOverridden: false,\n runMode: config.code?.runMode ?? form.runMode,\n maxConcurrency:\n config.code?.maxConcurrency ?? form.maxConcurrency,\n })\n }\n />\n\n {warnings.map((warning) => (\n <p\n key={warning.id}\n className=\"runner-note runner-note-pending\"\n role=\"note\"\n data-testid={`config-warning-${warning.id}`}\n >\n {warning.message}\n </p>\n ))}\n\n {failure !== null && (\n <ProblemBanner error={explained(failure, \"configure\")} />\n )}\n </form>\n </Dialog>\n );\n}\n\n/** Props of {@link ConfigSource}. */\ninterface ConfigSourceProps {\n /** The `data-testid` suffix, e.g. `execution-mode`. */\n testId: string;\n /** Whether the setting is an override rather than the code's. */\n overridden: boolean;\n /** What the sentence calls the setting, e.g. \"Execution mode\". */\n label: string;\n /** Whether the code's own value is known, so it can be gone back to. */\n canReset: boolean;\n /** Whether the reset button is unavailable (a write is in flight). */\n disabled: boolean;\n /** Drops the override, back to the code's value. */\n onReset: () => void;\n}\n\n/** Says where one setting's value comes from, with a way back to the code's. */\nfunction ConfigSource({\n testId,\n overridden,\n label,\n canReset,\n disabled,\n onReset,\n}: ConfigSourceProps) {\n return (\n <p\n className=\"config-source\"\n data-testid={`config-source-${testId}`}\n >\n <span>\n {overridden\n ? `${label}: overridden here.`\n : `${label}: as the runner's code asks.`}\n </span>\n {overridden && canReset && (\n <Button\n size=\"sm\"\n onClick={onReset}\n disabled={disabled}\n >\n Use the code default\n </Button>\n )}\n </p>\n );\n}\n",
9
+ "import type { JobsApiAction } from \"../../../api/contract\";\nimport type { RunnerInfoDto } from \"../../../api/types\";\n\n/** Which runner actions are offered. */\nexport interface RunnerActionGates {\n /** `runners.trigger`. */\n trigger: boolean;\n /** `runners.pause`, on a running (not paused) runner. */\n pause: boolean;\n /** `runners.resume`, on a paused runner. */\n resume: boolean;\n /** `runners.reschedule`. */\n reschedule: boolean;\n /**\n * `runners.configure`, on a runner that reports a `config`. The action is\n * opt-in (a host must name it in `actions`), so its absence from the\n * permissions map is the normal case, not a fault; a runner without\n * `config` comes from a deployment that predates remote configuration.\n */\n configure: boolean;\n /** `runners.kill`, on a local runner with a run in flight in this process. */\n kill: boolean;\n /** `runners.resetStats`, on a local runner. */\n resetStats: boolean;\n /**\n * `runners.clearHistory`, on any runner: it works on what the backend\n * stores, so a runner registered in another process qualifies too.\n */\n clearHistory: boolean;\n /** The caller could kill or reset stats, but the runner is registered only in another process. */\n nonLocalOnly: boolean;\n}\n\n/**\n * Whether a local runner has a run in flight in this process. Only\n * `local.activeRuns` says so: `local.status` is the instance's lifecycle\n * (`\"running\"` means started and not paused, even with nothing in flight).\n */\nexport function hasLocalRuns(runner: RunnerInfoDto): boolean {\n return runner.isLocal && (runner.local?.activeRuns.length ?? 0) > 0;\n}\n\n/**\n * The actions to offer for `runner`, given a predicate that is true only\n * for a permitted action on a writable API (`useCanMutate`). Kill and stats\n * reset need the runner registered in the API's process (anything else is\n * 409 `RUNNER_NOT_LOCAL`), so they are not offered for one registered in\n * another process.\n * Configuring needs the runner to report a `config`; without one the API\n * answers 409 `RUNNER_NOT_CONFIGURABLE`. Clearing the history needs neither.\n */\nexport function runnerActionGates(\n runner: RunnerInfoDto,\n canMutate: (action: JobsApiAction) => boolean,\n): RunnerActionGates {\n const canKill = canMutate(\"runners.kill\");\n const canReset = canMutate(\"runners.resetStats\");\n return {\n trigger: canMutate(\"runners.trigger\"),\n pause: !runner.isPaused && canMutate(\"runners.pause\"),\n resume: runner.isPaused && canMutate(\"runners.resume\"),\n reschedule: canMutate(\"runners.reschedule\"),\n configure: runner.config !== undefined && canMutate(\"runners.configure\"),\n kill: canKill && hasLocalRuns(runner),\n resetStats: canReset && runner.isLocal,\n clearHistory: canMutate(\"runners.clearHistory\"),\n nonLocalOnly: !runner.isLocal && (canKill || canReset),\n };\n}\n\n/**\n * Whether clearing the history could remove a run that is still going: on a\n * runner registered in another process, a parallel run holds no lock, so the\n * API keeps a `running` record only while it is under a day old. A local\n * runner's runs are vouched for by this process, and a single-mode runner's\n * by its lock. An unknown run mode counts as parallel.\n */\nexport function clearHistoryMayDropLiveRuns(runner: RunnerInfoDto): boolean {\n if (runner.isLocal) {\n return false;\n }\n const runMode = runner.config?.effective.runMode ?? runner.runMode;\n return runMode !== \"single\";\n}\n",
10
+ "import type { KillRunnerBody } from \"../../../api/types\";\nimport type { RunnerDialogProps } from \"./TriggerDialog\";\nimport { useState } from \"react\";\nimport {\n clearRunnerHistory,\n killRunner,\n resetRunnerStats,\n resumeRunner,\n} from \"../../../api/runnerActions\";\nimport { runnerInvalidations } from \"../../../api/runners\";\nimport { ConfirmDialog } from \"../../../components/ConfirmDialog\";\nimport { Field } from \"../../../components/Field\";\nimport { Checkbox, Select, TextInput } from \"../../../components/inputs\";\nimport { useToast } from \"../../../components/toast\";\nimport { useApiClient } from \"../../../context\";\nimport { plural } from \"../../../format\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { clearHistoryMessage, explained } from \"./explain\";\nimport { clearHistoryMayDropLiveRuns } from \"./gating\";\n\n/** The longest kill reason the API accepts (`KillBodySchema`). */\nconst MAX_KILL_REASON = 200;\n\n/** `POST /runners/:runner/resume`, optionally asking for a run at once. */\nexport function ResumeDialog({ runner, onClose }: RunnerDialogProps) {\n const api = useApiClient();\n const [triggerNow, setTriggerNow] = useState(false);\n const resume = useApiMutation({\n mutationFn: (now: boolean) => resumeRunner(api, runner.id, now),\n successMessage: (_result, now) =>\n `Resumed ${runner.id}${now ? \" and asked for a run\" : \"\"}${\n runner.isLocal ? \"\" : \"; its owner picks it up at its next sync\"\n }`,\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n });\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Resume ${runner.id}?`}\n description=\"Scheduled and manual runs start again, everywhere the runner is registered.\"\n confirmLabel=\"Resume\"\n pendingLabel=\"Resuming…\"\n onConfirm={async () => {\n try {\n await resume.mutateAsync(triggerNow);\n } catch (error) {\n throw explained(error, \"resume\");\n }\n }}\n >\n <Checkbox\n checked={triggerNow}\n onChange={setTriggerNow}\n label=\"Also trigger a run now\"\n />\n </ConfirmDialog>\n );\n}\n\n/**\n * `POST /runners/:runner/kill`: one active run or all of them, typed\n * confirmation by the runner id. With `wait` the API answers only once the\n * runs settled, which can take the runner's closeTimeout plus killTimeout,\n * so the dialog says it is waiting.\n */\nexport function KillDialog({ runner, onClose }: RunnerDialogProps) {\n const api = useApiClient();\n const toast = useToast();\n const activeRuns = runner.local?.activeRuns ?? [];\n const [runId, setRunId] = useState(\"\");\n const [force, setForce] = useState(false);\n const [reason, setReason] = useState(\"\");\n const [wait, setWait] = useState(false);\n const kill = useApiMutation({\n mutationFn: (body: KillRunnerBody) => killRunner(api, runner.id, body),\n onSuccess: (result, body) => {\n const count = result.runIds.length;\n if (count === 0) {\n toast.info(`No run was active on ${runner.id}`);\n return;\n }\n toast.success(\n body.wait\n ? `Killed ${plural(count, \"run\")} on ${runner.id}`\n : `Kill requested for ${plural(count, \"run\")} on ${runner.id}`,\n { description: result.runIds.join(\", \") },\n );\n },\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n });\n const trimmed = reason.trim();\n const waiting = kill.isPending && kill.variables?.wait === true;\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Kill runs of ${runner.id}?`}\n description=\"Stops the chosen run (or every active run) in this process: it is asked to close, then killed once its timeouts pass. The runs are recorded as killed.\"\n variant=\"danger\"\n confirmLabel=\"Kill\"\n pendingLabel={wait ? \"Waiting for the runs to settle…\" : \"Killing…\"}\n confirmText={runner.id}\n confirmDisabled={trimmed.length > MAX_KILL_REASON}\n onConfirm={async () => {\n try {\n await kill.mutateAsync({\n ...(runId ? { runId } : {}),\n ...(force ? { force: true } : {}),\n ...(trimmed ? { reason: trimmed } : {}),\n ...(wait ? { wait: true } : {}),\n });\n } catch (error) {\n throw explained(error, \"kill\");\n }\n }}\n >\n <Field label=\"Run\">\n <Select\n options={[\n {\n value: \"\",\n label: `Every active run (${activeRuns.length})`,\n },\n ...activeRuns.map((run) => ({\n value: run.runId,\n label: `${run.runId} (${run.source}, attempt ${run.attempt})`,\n })),\n ]}\n value={runId}\n onChange={setRunId}\n disabled={kill.isPending}\n />\n </Field>\n <Checkbox\n checked={force}\n onChange={setForce}\n label=\"Force\"\n hint=\"Skip straight to the end of the kill escalation instead of asking the run to close first.\"\n disabled={kill.isPending}\n />\n <Field\n label=\"Reason\"\n hint={`Optional, recorded on the run. At most ${MAX_KILL_REASON} characters.`}\n error={\n kill.fieldErrors.reason ??\n (trimmed.length > MAX_KILL_REASON\n ? `At most ${MAX_KILL_REASON} characters.`\n : undefined)\n }\n >\n <TextInput\n value={reason}\n onChange={setReason}\n disabled={kill.isPending}\n />\n </Field>\n <Checkbox\n checked={wait}\n onChange={setWait}\n label=\"Wait until the runs have settled\"\n hint=\"Answer only once they have stopped. This can take the runner's closeTimeout plus killTimeout.\"\n disabled={kill.isPending}\n />\n {waiting && (\n <p\n className=\"runner-note runner-note-pending\"\n role=\"status\"\n data-testid=\"kill-waiting\"\n >\n Waiting for the runs to settle. This can take the runner's\n closeTimeout plus killTimeout.\n </p>\n )}\n </ConfirmDialog>\n );\n}\n\n/** `POST /runners/:runner/stats/reset` (204): the lifetime counters back to zero. */\nexport function ResetStatsDialog({ runner, onClose }: RunnerDialogProps) {\n const api = useApiClient();\n const reset = useApiMutation({\n mutationFn: () => resetRunnerStats(api, runner.id),\n successMessage: `Reset the stats of ${runner.id}`,\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n });\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Reset the stats of ${runner.id}?`}\n description=\"Sets every lifetime counter (success, failed, timeout, killed, skipped, queued, total) back to zero, for every process. History is kept. This cannot be undone.\"\n variant=\"danger\"\n confirmLabel=\"Reset stats\"\n pendingLabel=\"Resetting…\"\n onConfirm={async () => {\n try {\n await reset.mutateAsync();\n } catch (error) {\n throw explained(error, \"resetStats\");\n }\n }}\n />\n );\n}\n\n/**\n * `DELETE /runners/:runner/history`: finished runs and their logs go, runs in\n * progress stay whole, the counters and charts are untouched. Offered for a\n * runner registered in another process too; the small print about long\n * parallel runs shows only where it can apply.\n */\nexport function ClearHistoryDialog({ runner, onClose }: RunnerDialogProps) {\n const api = useApiClient();\n const toast = useToast();\n const clear = useApiMutation({\n mutationFn: () => clearRunnerHistory(api, runner.id),\n onSuccess: (result) => {\n toast.success(\n clearHistoryMessage(result.removed, result.kept),\n result.kept.length > 0\n ? { description: `Kept: ${result.kept.join(\", \")}` }\n : undefined,\n );\n },\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n });\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Clear the run history of ${runner.id}?`}\n description=\"Removes every finished run and its log, for every process. Runs still in progress are kept, record and log whole. The lifetime counters and the charts are untouched: Reset stats… is separate. This cannot be undone.\"\n variant=\"danger\"\n confirmLabel=\"Clear history\"\n pendingLabel=\"Clearing…\"\n onConfirm={async () => {\n try {\n await clear.mutateAsync();\n } catch (error) {\n throw explained(error, \"clearHistory\");\n }\n }}\n >\n <p className=\"runner-note muted\">\n A run whose record still says running but whose process has gone (no\n lock names it, and it started over a day ago) counts as finished, and is\n removed.\n </p>\n {clearHistoryMayDropLiveRuns(runner) && (\n <p\n className=\"runner-note muted\"\n data-testid=\"clear-history-parallel-note\"\n >\n This runner is registered in another process and its runs may overlap,\n so the API cannot ask the process running them: a parallel run still\n going after a day is taken for crashed and removed too.\n </p>\n )}\n </ConfirmDialog>\n );\n}\n",
11
+ "import type { RunnerScheduleDto, ScheduleRunnerBody } from \"../../../api/types\";\nimport { isApiError } from \"../../../api/errors\";\nimport { issuesToFieldErrors } from \"../../../hooks/useApiMutation\";\n\n/**\n * The schedule editor's pure half: a form value, its prefill from the stored\n * schedule, its validation, and the `PUT /runners/:runner/schedule` body it\n * becomes. The API stays the authority (a cron expression and a time zone\n * are only fully checked by `Bun.cron` on the server, which answers 400\n * `INVALID_SCHEDULE`); this catches what can be caught before sending.\n */\n\n/** Which kind of schedule the editor edits. */\nexport type ScheduleMode = \"cron\" | \"every\" | \"at\" | \"none\";\n\n/** The unit an interval is typed in. */\nexport type IntervalUnit = \"seconds\" | \"minutes\" | \"hours\";\n\n/** A form field that can carry an error. */\nexport type ScheduleField = \"cron\" | \"tz\" | \"every\" | \"anchor\" | \"at\";\n\n/** Errors per field. */\nexport type ScheduleErrors = Partial<Record<ScheduleField, string>>;\n\n/** The editor's state. Every mode's inputs are kept, so switching modes loses nothing. */\nexport interface ScheduleForm {\n /** The chosen kind. */\n mode: ScheduleMode;\n /** Cron expression (5 fields, 6 with seconds first, or a nickname such as `@daily`). */\n cron: string;\n /** IANA time zone for the cron expression; `\"\"` for the server's zone. */\n tz: string;\n /** Interval amount in {@link everyUnit}; `undefined` when empty. */\n everyAmount: number | undefined;\n /** The interval's unit. */\n everyUnit: IntervalUnit;\n /** Anchor of the interval grid, epoch ms; `undefined` for none (measured from each start). */\n anchor: number | undefined;\n /** The one-off time, epoch ms; `undefined` when empty. */\n at: number | undefined;\n}\n\n/** Milliseconds per interval unit. */\nexport const INTERVAL_UNIT_MS: Readonly<Record<IntervalUnit, number>> = {\n seconds: 1_000,\n minutes: 60_000,\n hours: 3_600_000,\n};\n\n/** The longest cron expression the API accepts (`ScheduleBodySchema`). */\nexport const MAX_CRON_LENGTH = 200;\n/** The longest time zone the API accepts (`ScheduleBodySchema`). */\nexport const MAX_TZ_LENGTH = 100;\n\n/** An empty form in `mode`. */\nexport function emptyScheduleForm(mode: ScheduleMode = \"none\"): ScheduleForm {\n return {\n mode,\n cron: \"\",\n tz: \"\",\n everyAmount: undefined,\n everyUnit: \"minutes\",\n anchor: undefined,\n at: undefined,\n };\n}\n\n/** Splits ms into the largest unit that holds it exactly (seconds as a fraction when nothing does). */\nexport function splitInterval(ms: number): {\n amount: number;\n unit: IntervalUnit;\n} {\n for (const unit of [\"hours\", \"minutes\", \"seconds\"] as const) {\n if (ms % INTERVAL_UNIT_MS[unit] === 0) {\n return { amount: ms / INTERVAL_UNIT_MS[unit], unit };\n }\n }\n return { amount: ms / INTERVAL_UNIT_MS.seconds, unit: \"seconds\" };\n}\n\n/** The form for a stored schedule (`RunnerInfoDto.schedule`). */\nexport function formFromSchedule(schedule: RunnerScheduleDto): ScheduleForm {\n const form = emptyScheduleForm();\n if (schedule === null) {\n return form;\n }\n if (\"cron\" in schedule) {\n return {\n ...form,\n mode: \"cron\",\n cron: schedule.cron,\n tz: schedule.tz ?? \"\",\n };\n }\n if (\"every\" in schedule) {\n const { amount, unit } = splitInterval(schedule.every);\n return {\n ...form,\n mode: \"every\",\n everyAmount: amount,\n everyUnit: unit,\n anchor: schedule.anchor,\n };\n }\n return { ...form, mode: \"at\", at: schedule.at };\n}\n\n/** An interval in a unit as whole ms, or `undefined` when it is not a positive amount (or rounds to 0 ms). */\nexport function intervalMs(\n amount: number | undefined,\n unit: IntervalUnit,\n): number | undefined {\n if (amount === undefined || !Number.isFinite(amount) || amount <= 0) {\n return undefined;\n }\n const ms = Math.round(amount * INTERVAL_UNIT_MS[unit]);\n return ms >= 1 ? ms : undefined;\n}\n\n/** The time zones this browser knows (`Intl.supportedValuesOf`), or `null` when it cannot list them. */\nexport function supportedTimeZones(): readonly string[] | null {\n const intl = Intl as typeof Intl & {\n supportedValuesOf?: (key: \"timeZone\") => string[];\n };\n if (typeof intl.supportedValuesOf !== \"function\") {\n return null;\n }\n try {\n return intl.supportedValuesOf(\"timeZone\");\n } catch {\n return null;\n }\n}\n\n/**\n * Whether `tz` names an IANA time zone. It is checked against\n * `Intl.supportedValuesOf(\"timeZone\")` when the browser has it (case\n * insensitively), then against `Intl.DateTimeFormat` — the list holds only\n * canonical names, so an alias such as `US/Eastern`, which the server's\n * `Bun.cron` accepts, would otherwise be refused. A UTC offset (`+01:00`) is\n * not a zone: `Intl` accepts it, `Bun.cron` does not.\n */\nexport function isValidTimeZone(\n tz: string,\n zones: readonly string[] | null = supportedTimeZones(),\n): boolean {\n const name = tz.trim();\n if (name === \"\" || /^[+-]\\d/.test(name)) {\n return false;\n }\n const lower = name.toLowerCase();\n if (zones?.some((zone) => zone.toLowerCase() === lower)) {\n return true;\n }\n try {\n const format = Intl.DateTimeFormat(\"en\", { timeZone: name });\n return format.resolvedOptions().timeZone !== \"\";\n } catch {\n return false;\n }\n}\n\n/**\n * The shape check the server's parser applies before `Bun.cron` sees the\n * expression: 5 fields, 6 with seconds first, or a single `@nickname`.\n * Returns the problem, or `null` when it looks right.\n */\nexport function cronShapeError(expression: string): string | null {\n const trimmed = expression.trim();\n if (trimmed === \"\") {\n return \"Enter a cron expression.\";\n }\n if (trimmed.length > MAX_CRON_LENGTH) {\n return `At most ${MAX_CRON_LENGTH} characters.`;\n }\n const fields = trimmed.split(/\\s+/);\n if (fields.length === 1 && trimmed.startsWith(\"@\")) {\n return null;\n }\n if (fields.length !== 5 && fields.length !== 6) {\n return `${fields.length} field${fields.length === 1 ? \"\" : \"s\"}: expected 5, 6 with seconds first, or a nickname such as @daily.`;\n }\n return null;\n}\n\n/** Validates the form for its mode. `zones` is what {@link isValidTimeZone} checks against. */\nexport function validateScheduleForm(\n form: ScheduleForm,\n zones: readonly string[] | null = supportedTimeZones(),\n): ScheduleErrors {\n const errors: ScheduleErrors = {};\n switch (form.mode) {\n case \"cron\": {\n const cron = cronShapeError(form.cron);\n if (cron) {\n errors.cron = cron;\n }\n const tz = form.tz.trim();\n if (tz.length > MAX_TZ_LENGTH) {\n errors.tz = `At most ${MAX_TZ_LENGTH} characters.`;\n } else if (tz !== \"\" && !isValidTimeZone(tz, zones)) {\n errors.tz = `“${tz}” is not a known time zone. Use an IANA name such as Europe/London or UTC.`;\n }\n break;\n }\n case \"every\":\n if (intervalMs(form.everyAmount, form.everyUnit) === undefined) {\n errors.every = \"Enter an interval greater than zero.\";\n }\n break;\n case \"at\":\n if (form.at === undefined) {\n errors.at = \"Pick a date and time.\";\n }\n break;\n case \"none\":\n break;\n }\n return errors;\n}\n\n/** The request body's `schedule` for a valid form; `null` unschedules. */\nexport function scheduleBody(\n form: ScheduleForm,\n): ScheduleRunnerBody[\"schedule\"] {\n switch (form.mode) {\n case \"cron\": {\n const tz = form.tz.trim();\n return { cron: form.cron.trim(), ...(tz ? { tz } : {}) };\n }\n case \"every\":\n return {\n every: intervalMs(form.everyAmount, form.everyUnit) ?? 0,\n ...(form.anchor === undefined ? {} : { anchor: form.anchor }),\n };\n case \"at\":\n return { at: form.at ?? 0 };\n case \"none\":\n return null;\n }\n}\n\n/** The field a mode's main value lives in (where a whole-schedule error goes). */\nexport function mainField(mode: ScheduleMode): ScheduleField | null {\n switch (mode) {\n case \"cron\":\n return \"cron\";\n case \"every\":\n return \"every\";\n case \"at\":\n return \"at\";\n case \"none\":\n return null;\n }\n}\n\n/**\n * A 400 `INVALID_SCHEDULE`'s issues as field errors (path → message, as\n * `useApiMutation` builds them for `VALIDATION`). The API names the part at\n * fault in `issues[].path`: `schedule.cron`, `schedule.tz`, or `schedule`\n * itself for a bare cron string. A bad interval, anchor or time\n * (`schedule.every`, `schedule.anchor`, `schedule.at`) comes as `VALIDATION`\n * instead; {@link scheduleFieldErrors} maps both the same way, and still\n * accepts those paths here, from an API that sent them as `INVALID_SCHEDULE`.\n * `{}` for any other error, and for an `INVALID_SCHEDULE` without issues\n * (an older API), which the editor shows as a banner instead.\n */\nexport function invalidScheduleErrors(\n error: unknown,\n): Readonly<Record<string, string>> {\n return isApiError(error) && error.code === \"INVALID_SCHEDULE\"\n ? issuesToFieldErrors(error.issues)\n : {};\n}\n\n/**\n * `VALIDATION` field errors (keys are paths within the body, e.g.\n * `schedule.tz`) mapped onto the editor's fields. A path the editor has no\n * input for (`schedule` itself, a union mismatch) goes to the mode's main\n * field; with no field to show it on (mode none), it is returned as `other`.\n */\nexport function scheduleFieldErrors(\n fieldErrors: Readonly<Record<string, string>>,\n mode: ScheduleMode,\n): { fields: ScheduleErrors; other: string | undefined } {\n const fields: ScheduleErrors = {};\n let other: string | undefined;\n for (const [path, message] of Object.entries(fieldErrors)) {\n const leaf = /^schedule\\.(cron|tz|every|anchor|at)\\b/.exec(path)?.[1] as\n ScheduleField | undefined;\n const field = leaf ?? mainField(mode);\n if (field) {\n fields[field] ??= message;\n } else {\n other ??= message;\n }\n }\n return { fields, other };\n}\n",
12
+ "import type { FormEvent } from \"react\";\nimport type { ScheduleRunnerBody } from \"../../../api/types\";\nimport type {\n IntervalUnit,\n ScheduleErrors,\n ScheduleField,\n ScheduleForm,\n ScheduleMode,\n} from \"./schedule\";\nimport type { RunnerDialogProps } from \"./TriggerDialog\";\nimport { useId, useMemo, useState } from \"react\";\nimport { rescheduleRunner } from \"../../../api/runnerActions\";\nimport { runnerInvalidations } from \"../../../api/runners\";\nimport { Button } from \"../../../components/Button\";\nimport { Dialog } from \"../../../components/Dialog\";\nimport { Field } from \"../../../components/Field\";\nimport {\n DateTimeInput,\n NumberInput,\n Select,\n TextInput,\n} from \"../../../components/inputs\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../../components/RelativeTime\";\nimport { useToast } from \"../../../components/toast\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { useFieldProblems } from \"../../../hooks/useFieldProblems\";\nimport { useNow } from \"../../../hooks/useNow\";\nimport { explained } from \"./explain\";\nimport {\n formFromSchedule,\n invalidScheduleErrors,\n MAX_CRON_LENGTH,\n MAX_TZ_LENGTH,\n scheduleBody,\n scheduleFieldErrors,\n supportedTimeZones,\n validateScheduleForm,\n} from \"./schedule\";\n\n/** The modes, as the radio group offers them. */\nconst MODES: readonly { value: ScheduleMode; label: string }[] = [\n { value: \"cron\", label: \"Cron\" },\n { value: \"every\", label: \"Every\" },\n { value: \"at\", label: \"Once\" },\n { value: \"none\", label: \"None\" },\n];\n\n/** Units of the interval. */\nconst UNITS: readonly { value: IntervalUnit; label: string }[] = [\n { value: \"seconds\", label: \"seconds\" },\n { value: \"minutes\", label: \"minutes\" },\n { value: \"hours\", label: \"hours\" },\n];\n\n/**\n * `PUT /runners/:runner/schedule`: a cron expression (+ time zone), an\n * interval (+ anchor), a one-off time, or none (manual only). Prefilled from\n * the stored schedule; the success toast shows when it next fires, from the\n * response.\n */\nexport function ScheduleEditorDialog({ runner, onClose }: RunnerDialogProps) {\n const api = useApiClient();\n const toast = useToast();\n const formId = useId();\n const zonesId = useId();\n const zones = useMemo(() => supportedTimeZones(), []);\n const now = useNow();\n const [form, setForm] = useState<ScheduleForm>(() =>\n formFromSchedule(runner.schedule),\n );\n const [submitted, setSubmitted] = useState(false);\n // A field's problem clears as the field unmounts, so only the shown mode's\n // times can hold the form back.\n const times = useFieldProblems<\"anchor\" | \"at\">();\n const patch = (next: Partial<ScheduleForm>) =>\n setForm((current) => ({ ...current, ...next }));\n\n const save = useApiMutation({\n mutationFn: (schedule: ScheduleRunnerBody[\"schedule\"]) =>\n rescheduleRunner(api, runner.id, schedule),\n onSuccess: (result) => {\n toast.success(\n result.schedule === null\n ? `${runner.id} is unscheduled`\n : `Rescheduled ${runner.id}`,\n {\n description:\n result.nextRunAt === null ? (\n \"No run is scheduled; it runs only when triggered.\"\n ) : (\n <span data-testid=\"schedule-next-run\">\n Next run <RelativeTime value={result.nextRunAt} />\n </span>\n ),\n },\n );\n onClose();\n },\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n });\n\n const clientErrors: ScheduleErrors = submitted\n ? validateScheduleForm(form, zones)\n : {};\n const server = scheduleFieldErrors(\n { ...save.fieldErrors, ...invalidScheduleErrors(save.error) },\n form.mode,\n );\n const fieldError = (field: ScheduleField): string | undefined =>\n (field === \"anchor\" || field === \"at\"\n ? times.problems[field]\n : undefined) ??\n clientErrors[field] ??\n server.fields[field];\n const hasServerFieldError = Object.keys(server.fields).length > 0;\n // An INVALID_SCHEDULE without issues (an API from before they were sent)\n // names no field, so it falls through to the banner. So does an issue no\n // field shows (`unplacedIssues`), as the whole problem, which lists it.\n const unplaced = save.unplacedIssues.length > 0;\n const bannerError =\n save.error && (!hasServerFieldError || unplaced)\n ? ((unplaced ? undefined : server.other) ??\n explained(save.error, \"reschedule\"))\n : null;\n\n const onSubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setSubmitted(true);\n if (\n times.blocked ||\n Object.keys(validateScheduleForm(form, zones)).length > 0\n ) {\n return;\n }\n save.mutate(scheduleBody(form));\n };\n\n const close = () => {\n if (!save.isPending) {\n onClose();\n }\n };\n\n const atInPast = form.at !== undefined && form.at <= now;\n\n return (\n <Dialog\n open\n onClose={close}\n title={\n <>\n Reschedule <code>{runner.id}</code>\n </>\n }\n description=\"Replaces the stored schedule for every process running this runner.\"\n size=\"md\"\n closeOnEscape={!save.isPending}\n closeOnBackdrop={!save.isPending}\n footer={\n <>\n <Button\n onClick={close}\n disabled={save.isPending}\n >\n Cancel\n </Button>\n <Button\n variant=\"primary\"\n type=\"submit\"\n form={formId}\n disabled={save.isPending || times.blocked}\n aria-busy={save.isPending || undefined}\n >\n {save.isPending ? \"Saving…\" : \"Save schedule\"}\n </Button>\n </>\n }\n >\n <form\n id={formId}\n className=\"runner-form\"\n onSubmit={onSubmit}\n noValidate\n >\n <fieldset className=\"schedule-modes\">\n <legend>Schedule</legend>\n {MODES.map((mode) => (\n <label\n key={mode.value}\n className=\"schedule-mode\"\n >\n <input\n type=\"radio\"\n name={`${formId}-mode`}\n value={mode.value}\n checked={form.mode === mode.value}\n onChange={() => patch({ mode: mode.value })}\n />\n {mode.label}\n </label>\n ))}\n </fieldset>\n\n {form.mode === \"cron\" && (\n <>\n <Field\n label=\"Cron expression\"\n required\n hint=\"5 fields (minute hour day month weekday), 6 with seconds first, or a nickname such as @daily.\"\n error={fieldError(\"cron\")}\n >\n <TextInput\n value={form.cron}\n onChange={(cron) => patch({ cron })}\n maxLength={MAX_CRON_LENGTH}\n autoComplete=\"off\"\n spellCheck={false}\n placeholder=\"0 9 * * MON-FRI\"\n />\n </Field>\n <Field\n label=\"Time zone\"\n hint=\"Optional IANA name, e.g. Europe/London. Empty: the owner's local time (the zone of the process that runs it).\"\n error={fieldError(\"tz\")}\n >\n <TextInput\n value={form.tz}\n onChange={(tz) => patch({ tz })}\n maxLength={MAX_TZ_LENGTH}\n autoComplete=\"off\"\n spellCheck={false}\n list={zones ? zonesId : undefined}\n />\n </Field>\n {zones && (\n <datalist id={zonesId}>\n {zones.map((zone) => (\n <option\n key={zone}\n value={zone}\n />\n ))}\n </datalist>\n )}\n </>\n )}\n\n {form.mode === \"every\" && (\n <>\n <div className=\"runner-form-row\">\n <Field\n label=\"Interval\"\n required\n error={fieldError(\"every\")}\n >\n <NumberInput\n value={form.everyAmount}\n onChange={(everyAmount) => patch({ everyAmount })}\n min={0}\n step=\"any\"\n />\n </Field>\n <Field label=\"Unit\">\n <Select\n options={UNITS}\n value={form.everyUnit}\n onChange={(everyUnit) => patch({ everyUnit })}\n />\n </Field>\n </div>\n <Field\n label=\"Anchor\"\n hint=\"Optional: runs stay on this time's grid. Without one, each interval counts from when the schedule is applied.\"\n error={fieldError(\"anchor\")}\n >\n <DateTimeInput\n value={form.anchor}\n onChange={(anchor) => patch({ anchor })}\n onProblem={(problem) => times.report(\"anchor\", problem)}\n withSeconds\n />\n </Field>\n </>\n )}\n\n {form.mode === \"at\" && (\n <Field\n label=\"Run at\"\n required\n hint={\n atInPast\n ? \"This time has passed: the runner will not fire.\"\n : \"One run at this time, then none.\"\n }\n error={fieldError(\"at\")}\n >\n <DateTimeInput\n value={form.at}\n onChange={(at) => patch({ at })}\n onProblem={(problem) => times.report(\"at\", problem)}\n withSeconds\n />\n </Field>\n )}\n\n {form.mode === \"none\" && (\n <p\n className=\"runner-note\"\n role=\"note\"\n >\n No schedule: the runner runs only when triggered.\n </p>\n )}\n\n {bannerError !== null && <ProblemBanner error={bannerError} />}\n </form>\n </Dialog>\n );\n}\n",
13
+ "import type { RunnerInfoDto, TriggerOutcomeDto } from \"../../../api/types\";\nimport type { ToastApi } from \"../../../components/toast\";\nimport { CopyButton } from \"../../../components/CopyButton\";\nimport { SKIP_REASONS } from \"./explain\";\n\n/**\n * Toasts a trigger's outcome: started (with the run id to copy), queued\n * (with its position, and for a runner registered in another process who\n * starts it), or skipped (with the reason in words).\n */\nexport function toastTriggerOutcome(\n toast: ToastApi,\n runner: RunnerInfoDto,\n outcome: TriggerOutcomeDto,\n): void {\n switch (outcome.outcome) {\n case \"started\":\n toast.success(`Run started on ${runner.id}`, {\n description: (\n <span className=\"run-id\">\n <span>\n Run <code>{outcome.runId}</code>\n </span>\n <CopyButton\n text={outcome.runId}\n ariaLabel=\"Copy run id\"\n />\n </span>\n ),\n });\n return;\n case \"queued\":\n toast.success(`Run queued on ${runner.id}`, {\n description: `Position ${outcome.position} in the trigger queue.${\n runner.isLocal\n ? \" It starts when the runner is free.\"\n : \" The process that owns the runner starts it at its next sync.\"\n }`,\n });\n return;\n case \"skipped\":\n toast.info(`Run skipped on ${runner.id}`, {\n description: SKIP_REASONS[outcome.reason] ?? outcome.reason,\n });\n }\n}\n",
14
+ "import type { RunnerInfoDto, TriggerRunnerBody } from \"../../../api/types\";\nimport type { JsonEditorState } from \"../../../components/jsonParse\";\nimport { useState } from \"react\";\nimport { triggerRunner } from \"../../../api/runnerActions\";\nimport { runnerInvalidations } from \"../../../api/runners\";\nimport { ConfirmDialog } from \"../../../components/ConfirmDialog\";\nimport { Field } from \"../../../components/Field\";\nimport { Checkbox } from \"../../../components/inputs\";\nimport { JsonEditor } from \"../../../components/JsonEditor\";\nimport { jsonEditorState } from \"../../../components/jsonParse\";\nimport { useToast } from \"../../../components/toast\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { useMeta } from \"../../../meta/hooks\";\nimport { explained } from \"./explain\";\nimport { toastTriggerOutcome } from \"./triggerOutcome\";\n\n/** Props of one runner action dialog. */\nexport interface RunnerDialogProps {\n /** The runner acted on. */\n runner: RunnerInfoDto;\n /** Closes the dialog. */\n onClose: () => void;\n}\n\n/** `POST /runners/:runner/trigger`: force, and args only when the API accepts them. */\nexport function TriggerDialog({ runner, onClose }: RunnerDialogProps) {\n const api = useApiClient();\n const meta = useMeta();\n const toast = useToast();\n const maxBytes = meta.limits.maxJobDataBytes;\n const [force, setForce] = useState(false);\n const [args, setArgs] = useState<JsonEditorState>(() =>\n jsonEditorState(\"\", { maxBytes, allowEmpty: true }),\n );\n const trigger = useApiMutation({\n mutationFn: (body: TriggerRunnerBody) =>\n triggerRunner(api, runner.id, body),\n onSuccess: (outcome) => toastTriggerOutcome(toast, runner, outcome),\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n });\n const argsAllowed = meta.runnerTriggerArgs;\n const argsInvalid = argsAllowed && (!args.valid || args.overLimit);\n\n const confirm = async () => {\n const body: TriggerRunnerBody = {\n ...(force ? { force: true } : {}),\n ...(argsAllowed && args.value !== undefined ? { args: args.value } : {}),\n };\n try {\n await trigger.mutateAsync(body);\n } catch (error) {\n throw explained(error, \"trigger\");\n }\n };\n\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Trigger ${runner.id}?`}\n description=\"Asks for a run now. It starts at once, waits in the trigger queue, or is skipped, depending on the runner's state and options.\"\n confirmLabel=\"Trigger\"\n pendingLabel=\"Triggering…\"\n confirmDisabled={argsInvalid}\n onConfirm={confirm}\n >\n <Checkbox\n checked={force}\n onChange={setForce}\n label=\"Run even while paused\"\n hint=\"Force skips only the pause check. A busy runner, a lock held elsewhere, the concurrency cap and a full trigger queue still apply.\"\n />\n {argsAllowed && (\n <Field\n label=\"Arguments\"\n hint=\"Optional JSON handed to this run; leave empty for the runner's own args.\"\n >\n <JsonEditor\n value={args.text}\n onChange={setArgs}\n maxBytes={maxBytes}\n allowEmpty\n rows={6}\n />\n </Field>\n )}\n {!runner.isLocal && (\n <p\n className=\"runner-note\"\n role=\"note\"\n data-testid=\"trigger-non-local-note\"\n >\n This runner is registered in another process, so the run is queued for\n it: its owner starts it at its next sync.\n </p>\n )}\n </ConfirmDialog>\n );\n}\n",
15
+ "/**\n * A runner's actions: trigger, pause/resume, reschedule, edit its settings,\n * kill, reset stats and clear the run history, with their dialogs. The runner screen mounts\n * {@link RunnerActions} in its header; this props interface is the contract\n * between the two, so keep it.\n */\nimport type { RunnerInfoDto } from \"../../../api/types\";\nimport { useState } from \"react\";\nimport { isApiError } from \"../../../api/errors\";\nimport { pauseRunner } from \"../../../api/runnerActions\";\nimport { runnerInvalidations } from \"../../../api/runners\";\nimport { Button } from \"../../../components/Button\";\nimport { useToast } from \"../../../components/toast\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { useCanMutate } from \"../../queues/gating\";\nimport { ConfigEditorDialog } from \"./ConfigEditorDialog\";\nimport { explainRunnerError } from \"./explain\";\nimport { runnerActionGates } from \"./gating\";\nimport { KillDialog, ResetStatsDialog, ResumeDialog } from \"./RunnerDialogs\";\nimport { ScheduleEditorDialog } from \"./ScheduleEditorDialog\";\nimport { TriggerDialog } from \"./TriggerDialog\";\nimport \"./actions.css\";\n\n/** Props of {@link RunnerActions}. */\nexport interface RunnerActionsProps {\n /** The runner as `GET /runners/:runner` last returned it. */\n runner: RunnerInfoDto;\n}\n\n/** Which dialog is open. */\ntype OpenDialog =\n \"trigger\" | \"resume\" | \"schedule\" | \"config\" | \"kill\" | \"reset\" | null;\n\n/**\n * The action buttons (and their dialogs) for one runner, each present only\n * when the runner-scoped permission is held and the API is not read-only.\n * Kill and reset stats exist only for a runner registered in the API's\n * process; for one registered in another process a hint says why they are\n * absent. Clearing the history works on any runner.\n */\nexport function RunnerActions({ runner }: RunnerActionsProps) {\n const api = useApiClient();\n const toast = useToast();\n const canMutate = useCanMutate();\n const [open, setOpen] = useState<OpenDialog>(null);\n const close = () => setOpen(null);\n const gates = runnerActionGates(runner, canMutate);\n\n const pause = useApiMutation({\n mutationFn: () => pauseRunner(api, runner.id),\n successMessage: runner.isLocal\n ? `Paused ${runner.id}`\n : `Paused ${runner.id}; its owner stops at its next sync`,\n invalidate: runnerInvalidations(runner.id),\n toastErrors: false,\n onError: (error) => {\n toast.error(`Could not pause ${runner.id}`, {\n description: isApiError(error)\n ? (explainRunnerError(error, \"pause\") ?? error.detail ?? error.title)\n : error.message,\n });\n },\n });\n\n const any =\n gates.trigger ||\n gates.pause ||\n gates.resume ||\n gates.reschedule ||\n gates.configure ||\n gates.kill ||\n gates.resetStats;\n if (!any && !gates.nonLocalOnly) {\n return null;\n }\n\n return (\n <div\n className=\"runner-actions\"\n role=\"group\"\n aria-label=\"Runner actions\"\n >\n {gates.trigger && (\n <Button\n variant=\"primary\"\n onClick={() => setOpen(\"trigger\")}\n >\n Trigger…\n </Button>\n )}\n {gates.pause && (\n <Button\n onClick={() => pause.mutate()}\n disabled={pause.isPending}\n aria-busy={pause.isPending || undefined}\n >\n Pause\n </Button>\n )}\n {gates.resume && (\n <Button onClick={() => setOpen(\"resume\")}>Resume…</Button>\n )}\n {gates.reschedule && (\n <Button onClick={() => setOpen(\"schedule\")}>Reschedule…</Button>\n )}\n {gates.configure && (\n <Button onClick={() => setOpen(\"config\")}>Settings…</Button>\n )}\n {gates.resetStats && (\n <Button\n variant=\"danger\"\n onClick={() => setOpen(\"reset\")}\n >\n Reset stats…\n </Button>\n )}\n {gates.kill && (\n <Button\n variant=\"danger\"\n onClick={() => setOpen(\"kill\")}\n >\n Kill…\n </Button>\n )}\n {gates.nonLocalOnly && (\n <p\n className=\"runner-actions-hint\"\n data-testid=\"runner-non-local-hint\"\n >\n Registered in another process: kill and reset stats are only available\n from the API of the process that runs it, and other changes reach it\n at its next sync.\n </p>\n )}\n {open === \"trigger\" && (\n <TriggerDialog\n runner={runner}\n onClose={close}\n />\n )}\n {open === \"resume\" && (\n <ResumeDialog\n runner={runner}\n onClose={close}\n />\n )}\n {open === \"schedule\" && (\n <ScheduleEditorDialog\n runner={runner}\n onClose={close}\n />\n )}\n {open === \"config\" && runner.config !== undefined && (\n <ConfigEditorDialog\n runner={runner}\n config={runner.config}\n onClose={close}\n />\n )}\n {open === \"kill\" && (\n <KillDialog\n runner={runner}\n onClose={close}\n />\n )}\n {open === \"reset\" && (\n <ResetStatsDialog\n runner={runner}\n onClose={close}\n />\n )}\n </div>\n );\n}\n",
16
+ "import type { QueryKey } from \"@tanstack/react-query\";\nimport type { ApiClient } from \"./client\";\nimport type { ApiError } from \"./errors\";\nimport type {\n EventWire,\n RunLogLineDto,\n RunLogPageDto,\n RunLogsQuery,\n RunLogStream,\n RunRecordDto,\n} from \"./types\";\nimport { segment } from \"./client\";\nimport { isApiError } from \"./errors\";\nimport { runnerKeys } from \"./runners\";\n\n/**\n * One run's captured output: `GET /runners/:runner/runs/:runId/logs`\n * (operation `getRunLogs`, action `runners.logs`), and the tail state the\n * view keeps.\n *\n * Organised like `api/jobs.ts`' log reads — the keys sit under the runner's\n * own prefix ({@link runnerKeys.runner}), so one runner invalidation\n * refreshes its run logs too — but the read is a **tail**, not a pager: the\n * page's `lastSeq` goes back as the next request's exclusive `since`, and\n * {@link mergeRunLogPage} folds each page into the lines already read. That\n * accumulation lives in the query cache (the merge takes the cached tail and\n * returns the next one), not in component state, so a view that unmounts and\n * comes back resumes where it was instead of re-reading from the start.\n */\n\n/**\n * How often a live run's log is re-read, ms, while live updates are off.\n *\n * With them on, the runner's `logs` event (see {@link runLogHint}) says when\n * the log grew and a read follows it, so this is relaxed the way every other\n * poll in the app is (`usePollInterval`) and becomes the fallback: a missed\n * hint only delays a line until the next read, and `since` means that read\n * still returns everything after what the view holds.\n */\nexport const RUN_LOG_POLL_MS = 2_000;\n\n/**\n * How long after a page that reported `hasMore` the next one is asked for,\n * ms: short, because this is catching up on a backlog rather than waiting\n * for new output.\n */\nexport const RUN_LOG_CATCHUP_MS = 100;\n\n/**\n * Most lines one tail keeps in memory. A run's log is capped by the backend\n * (1,000 lines and 1 MiB by default), so this only bites on a deployment\n * with much larger caps; when it does, the oldest lines go and\n * {@link RunLogTail.trimmed} counts them, so the view can say so.\n */\nexport const RUN_LOG_TAIL_MAX = 2_000;\n\n/** The page size a tail asks for: the API's own default, `min(100, limits.maxLogPage)`. */\nexport function runLogPageLimit(maxLogPage: number): number {\n return Math.max(1, Math.min(100, maxLogPage));\n}\n\n/** Query keys of one run's log, all under `[\"runner\", id]`. */\nexport const runLogKeys = {\n /** Everything about one run: `[\"runner\", runner, \"run\", runId]`. */\n run: (runner: string, runId: string) =>\n [...runnerKeys.runner(runner), \"run\", runId] as const,\n /** One run's log tail, per stream filter (`null` = every stream). */\n logs: (runner: string, runId: string, stream?: RunLogStream) =>\n [\n ...runLogKeys.run(runner, runId),\n \"logs\",\n { stream: stream ?? null },\n ] as const,\n};\n\n/**\n * What a read of one run's log refreshes. Nothing the UI sends writes run\n * output, so this is only for a caller that knows the log moved (a trigger,\n * a kill); the runner's own invalidation ({@link runnerKeys.runner}) already\n * covers it, and the merge keeps the lines already read either way.\n */\nexport function runLogInvalidations(runner: string, runId: string): QueryKey[] {\n return [runLogKeys.run(runner, runId)];\n}\n\n/** `/runners/:runner/runs/:runId/logs`, each part percent-encoded. */\nexport function runLogsPath(runner: string, runId: string): string {\n return `/runners/${segment(runner)}/runs/${segment(runId)}/logs`;\n}\n\n/** `GET /runners/:runner/runs/:runId/logs`. */\nexport function getRunLogs(\n api: ApiClient,\n runner: string,\n runId: string,\n query: RunLogsQuery = {},\n signal?: AbortSignal,\n): Promise<RunLogPageDto> {\n return api.request<RunLogPageDto>(\"GET\", runLogsPath(runner, runId), {\n query: {\n since: query.since,\n offset: query.offset,\n limit: query.limit,\n order: query.order,\n stream: query.stream,\n },\n signal,\n });\n}\n\n/** The lines read so far, and what the last page said about the run. */\nexport interface RunLogTail {\n /** Every line read so far, oldest first, at most {@link RUN_LOG_TAIL_MAX}. */\n lines: RunLogLineDto[];\n /** The exclusive `since` of the next read: the highest `seq` this tail has accounted for. */\n cursor: number;\n /** How many of the run's lines the backend's caps have dropped, oldest first (the run's own number, unfiltered). */\n dropped: number;\n /** Whether a cap is trimming this run's log right now, so what is here is a tail of it. */\n capped: boolean;\n /** Whether the run is still going, so more lines may follow. */\n live: boolean;\n /** The highest `seq` the run's log holds (the run's own, unfiltered by the stream filter). */\n lastSeq: number;\n /** Whether the last page left more to read at once, so the next read is a catch-up rather than a poll. */\n hasMore: boolean;\n /** How many lines this view itself let go to stay within {@link RUN_LOG_TAIL_MAX}. */\n trimmed: number;\n}\n\n/**\n * Folds one page into the tail read so far: new lines are appended (a line\n * whose `seq` is already there is ignored, so a retried request cannot\n * duplicate one), and the cursor advances.\n *\n * **The cursor is the page's `lastSeq` only when the page holds everything up\n * to it.** With `hasMore` the page stopped at its `limit`, so the cursor is\n * the last line actually received; resuming from `lastSeq` there would skip\n * the rest. `lastSeq` is the run's own, unfiltered by `stream`, which is what\n * makes it the right cursor for a filtered tail once the page is complete.\n */\nexport function mergeRunLogPage(\n previous: RunLogTail | undefined,\n page: RunLogPageDto,\n): RunLogTail {\n const seen = new Set(previous?.lines.map((line) => line.seq) ?? []);\n const fresh = page.items.filter((line) => !seen.has(line.seq));\n const merged = [...(previous?.lines ?? []), ...fresh].sort(\n (left, right) => left.seq - right.seq,\n );\n const overflow = Math.max(0, merged.length - RUN_LOG_TAIL_MAX);\n const lastReceived = page.items.at(-1)?.seq ?? 0;\n const hasMore = page.page.hasMore;\n const reached = hasMore\n ? lastReceived\n : Math.max(page.lastSeq, lastReceived, previous?.cursor ?? 0);\n return {\n lines: overflow > 0 ? merged.slice(overflow) : merged,\n cursor: Math.max(previous?.cursor ?? 0, reached),\n dropped: page.dropped,\n capped: page.capped,\n live: page.live,\n lastSeq: page.lastSeq,\n hasMore,\n trimmed: (previous?.trimmed ?? 0) + overflow,\n };\n}\n\n/**\n * The tail's refetch interval: a short catch-up while a page reported more\n * to read, `poll` while the run is live, and no polling at all once a\n * finished run's log has been read to its end.\n *\n * `poll` defaults to {@link RUN_LOG_POLL_MS}; the view passes it relaxed by\n * `usePollInterval` while live updates are on, because the `logs` hint then\n * drives the reads and the poll is only the fallback. The catch-up is never\n * relaxed: it is draining a backlog the last page already reported.\n */\nexport function runLogsRefetchInterval(\n tail: RunLogTail | undefined,\n poll: number | false = RUN_LOG_POLL_MS,\n): number | false {\n if (!tail) {\n return false;\n }\n if (tail.hasMore) {\n return RUN_LOG_CATCHUP_MS;\n }\n return tail.live ? poll : false;\n}\n\n/**\n * The `lastSeq` a runner's `logs` event announces for one run, or\n * `undefined` when the event is not that: another type, another runner, or\n * another run.\n *\n * The event is a **hint, never the lines**: it carries `{ runId, lastSeq }`\n * and nothing of what was logged, so the only thing to do with it is read\n * the log again from the view's own cursor.\n */\nexport function runLogHint(\n event: EventWire,\n runner: string,\n runId: string,\n): number | undefined {\n if (\n event.kind !== \"runner\" ||\n event.type !== \"logs\" ||\n event.target !== runner ||\n event.payload.runId !== runId\n ) {\n return undefined;\n }\n return event.payload.lastSeq;\n}\n\n/**\n * Whether a hint announcing `lastSeq` is worth a read: the view has not read\n * anything yet, or its cursor is behind. The cursor is the run's own\n * `lastSeq` once a page is complete (unfiltered by the stream filter), so a\n * hint the view has already caught up with — the one sent as a run settles,\n * typically — costs nothing.\n */\nexport function runLogHintNeedsRead(\n tail: RunLogTail | undefined,\n lastSeq: number,\n): boolean {\n return tail === undefined || tail.cursor < lastSeq;\n}\n\n/**\n * Whether a run's log is worth offering: it has lines, it is still going and\n * may yet write some, or its record does not say.\n *\n * Three cases, and only one of them hides the log:\n *\n * - `logLines > 0`, or the run is `running` — offered. The count lags by up\n * to 250 ms while a run is live (capture flushes on a timer), so a live\n * run qualifies whatever it reports.\n * - `logLines` **absent** — offered. The field is optional on the wire, and a\n * record that omits it says nothing about the run; the read itself is then\n * the honest answer (a 200 with no lines means the run was quiet). Whether\n * the backend stores run logs at all is `meta.features.runnerLogs`, which\n * gates the whole feature, and the API states it again as 409\n * `LOGS_NOT_RETAINED`.\n * - `logLines === 0` on a finished run — not offered: the run was quiet, and\n * the caller says so instead.\n */\nexport function hasRunLogs(\n run: Pick<RunRecordDto, \"logLines\" | \"status\">,\n): boolean {\n return (\n run.logLines === undefined || run.logLines > 0 || run.status === \"running\"\n );\n}\n\n/**\n * Whether the API answered 409 `LOGS_NOT_RETAINED`: this backend keeps no\n * run logs at all, which is exactly `meta.features.runnerLogs === false`. A\n * 200 with no lines is a different thing — the log is kept, and the run\n * logged nothing.\n */\nexport function isLogsNotRetained(error: unknown): error is ApiError {\n return isApiError(error) && error.code === \"LOGS_NOT_RETAINED\";\n}\n\n/**\n * Whether the API answered 404 `RUN_NOT_FOUND`. The route answers it for a\n * run neither the history nor the log store has heard of — which today also\n * covers a run that has aged out of what the runner keeps, since nothing\n * tells the two apart. The copy has to be true of both.\n */\nexport function isRunNotFound(error: unknown): error is ApiError {\n return isApiError(error) && error.code === \"RUN_NOT_FOUND\";\n}\n",
17
+ "import type { RunnerEventName } from \"../../api/types\";\nimport { useCallback } from \"react\";\nimport { RUNNER_EVENT_TYPES } from \"../../api/contract\";\nimport { runLogHint } from \"../../api/runnerLogs\";\nimport { runnerKeys } from \"../../api/runners\";\nimport {\n liveChannels,\n useLiveInvalidation,\n useLiveSubscription,\n usePollInterval,\n} from \"../../live\";\n\n/**\n * How the runner screens stay current: runner events invalidate the reads\n * they change ({@link useRunnerListLive}, {@link useRunnerLive}), and every\n * read also polls — at the base intervals below while live updates are off,\n * much slower while they are live (`usePollInterval`).\n */\nexport const RUNNER_REFRESH = {\n /** The runner list: statuses change rarely. */\n list: 10_000,\n /** One runner's detail and stats while a run is in flight. */\n busy: 5_000,\n /** One runner's detail and stats while nothing runs. */\n idle: 15_000,\n /** The run history. */\n history: 15_000,\n} as const satisfies Record<string, number | false>;\n\n/** What decides how often a runner's detail and stats are re-read. */\nexport interface RunnerActivity {\n /** `RunnerInfoDto.isRunning`: a run is in flight anywhere. */\n isRunning?: boolean;\n /** `RunnerInfoDto.local`, when the runner is registered in the API's process. */\n local?: {\n /** Runs in flight in that process. */\n activeRuns: readonly unknown[];\n };\n}\n\n/**\n * Whether a runner has a run in flight: `isRunning`, or an active run in this\n * process. Not `local.status === \"running\"`, which only means started.\n */\nexport function isBusy(runner: RunnerActivity | undefined): boolean {\n if (!runner) {\n return false;\n }\n return (\n runner.isRunning === true || (runner.local?.activeRuns.length ?? 0) > 0\n );\n}\n\n/** The refetch interval of a runner's detail and stats: {@link RUNNER_REFRESH.busy} while running, else {@link RUNNER_REFRESH.idle}. */\nexport function runnerRefetchInterval(\n runner: RunnerActivity | undefined,\n): number | false {\n return isBusy(runner) ? RUNNER_REFRESH.busy : RUNNER_REFRESH.idle;\n}\n\n/** The refetch interval of the runner list. */\nexport function listRefetchInterval(): number | false {\n return RUNNER_REFRESH.list;\n}\n\n/** The refetch interval of a runner's history. */\nexport function historyRefetchInterval(): number | false {\n return RUNNER_REFRESH.history;\n}\n\n/** {@link runnerRefetchInterval}, relaxed while live updates are on. */\nexport function useRunnerRefetchInterval(): (\n runner: RunnerActivity | undefined,\n) => number | false {\n const busy = usePollInterval(RUNNER_REFRESH.busy);\n const idle = usePollInterval(RUNNER_REFRESH.idle);\n return useCallback((runner) => (isBusy(runner) ? busy : idle), [busy, idle]);\n}\n\n/** {@link listRefetchInterval}, relaxed while live updates are on. */\nexport function useListRefetchInterval(): number | false {\n return usePollInterval(listRefetchInterval());\n}\n\n/** {@link historyRefetchInterval}, relaxed while live updates are on. */\nexport function useHistoryRefetchInterval(): number | false {\n return usePollInterval(historyRefetchInterval());\n}\n\n/**\n * The runner events that change what the runner screens read: every one the\n * contract declares except `logs`.\n *\n * `logs` is left out on purpose. It says one run's stored log grew — at most\n * every half second per run while a chatty run is going — and changes no\n * runner detail, stat or history row, so letting it invalidate\n * {@link runnerKeys.runner} would re-read all three that often, and every\n * open run log with them, whichever run the hint was for. The log view\n * follows it itself ({@link useRunLogHints}), for its own run only.\n */\nexport const RUNNER_STATE_EVENTS: readonly RunnerEventName[] =\n RUNNER_EVENT_TYPES.filter((type) => type !== \"logs\");\n\n/** The Runners list's live updates: any event on `runners` but a log hint refreshes every runner list. */\nexport function useRunnerListLive(enabled: boolean): void {\n useLiveInvalidation([liveChannels.runners], [runnerKeys.all], {\n events: RUNNER_STATE_EVENTS,\n enabled,\n });\n}\n\n/**\n * One runner's live updates, on `runner/<id>`: any of its events but a log\n * hint (a run started or ended, a control) refreshes its detail, stats and\n * history, which all sit under its key.\n */\nexport function useRunnerLive(id: string, enabled: boolean): void {\n useLiveInvalidation([liveChannels.runner(id)], [runnerKeys.runner(id)], {\n events: RUNNER_STATE_EVENTS,\n enabled,\n });\n}\n\n/** What {@link useRunLogHints} calls. */\nexport interface RunLogHintHandlers {\n /** A hint about the run arrived, announcing the `seq` of the last line its log now holds. */\n onHint: (lastSeq: number) => void;\n /** The server reported a gap on the runner's channel: hints may have been lost, so read anyway. */\n onGap: () => void;\n}\n\n/**\n * Follows one run's `logs` hints on `runner/<runner>`: calls `onHint` with\n * the announced `lastSeq` for each hint about `runId`, and ignores every\n * other run's. Only `logs` is asked for, so a busy runner's other traffic\n * never reaches it.\n */\nexport function useRunLogHints(\n runner: string,\n runId: string,\n enabled: boolean,\n handlers: RunLogHintHandlers,\n): void {\n useLiveSubscription({\n channels: [liveChannels.runner(runner)],\n events: [\"logs\"],\n enabled,\n onEvent: (event) => {\n const lastSeq = runLogHint(event, runner, runId);\n if (lastSeq !== undefined) {\n handlers.onHint(lastSeq);\n }\n },\n onGap: () => handlers.onGap(),\n });\n}\n",
18
+ "import type { ApiError } from \"../../api/errors\";\nimport type {\n RunnerInfoDto,\n RunnerListItemDto,\n RunnerScheduleDto,\n RunnerStatusDto,\n RunRecordDto,\n} from \"../../api/types\";\nimport type { BadgeTone } from \"../../components/Badge\";\nimport { isApiError } from \"../../api/errors\";\nimport { formatNumber } from \"../../format\";\nimport { formatMs } from \"../queues/duration\";\n\n/** How a runner status is shown. */\nexport interface RunnerBadge {\n /** The text. */\n label: string;\n /** Its colour. */\n tone: BadgeTone;\n /** What it means, as a tooltip. */\n hint: string;\n}\n\n/**\n * Display of each runner status. It is the INSTANCE's lifecycle, not whether\n * a run is in flight: `running` means started (its schedule armed in that\n * process), `idle` registered but not started (bun-jobs runner/BunRunner.ts\n * `start()`/`stop()`). A run in flight is `isRunning`, shown separately.\n */\nexport const RUNNER_STATUS: Readonly<Record<RunnerStatusDto, RunnerBadge>> = {\n idle: {\n label: \"Idle\",\n tone: \"neutral\",\n hint: \"Registered but not started in its process: nothing is scheduled there.\",\n },\n running: {\n label: \"Running\",\n tone: \"success\",\n hint: \"Started: its schedule is armed and triggers are accepted.\",\n },\n paused: {\n label: \"Paused\",\n tone: \"warning\",\n hint: \"Paused everywhere: scheduled and manual runs do not start.\",\n },\n stopped: {\n label: \"Stopped\",\n tone: \"danger\",\n hint: \"Stopped in its process: it takes no more triggers.\",\n },\n};\n\n/** The badge of a run in flight. */\nexport const RUN_IN_FLIGHT: RunnerBadge = {\n label: \"Run in flight\",\n tone: \"info\",\n hint: \"A run is executing somewhere right now (the runner's lock or active runs say so).\",\n};\n\n/** The badge of an unpaused runner registered in another process. */\nexport const NON_LOCAL_ACTIVE: RunnerBadge = {\n label: \"Active\",\n tone: \"neutral\",\n hint: \"Not paused. Its owner's own status shows only on the owner's API.\",\n};\n\n/** Display label and colour of each run status. */\nexport const RUN_STATUS: Readonly<\n Record<RunRecordDto[\"status\"], { label: string; tone: BadgeTone }>\n> = {\n running: { label: \"Running\", tone: \"info\" },\n success: { label: \"Success\", tone: \"success\" },\n failed: { label: \"Failed\", tone: \"danger\" },\n timeout: { label: \"Timed out\", tone: \"warning\" },\n killed: { label: \"Killed\", tone: \"danger\" },\n};\n\n/** Display labels of what started a run. */\nexport const RUN_SOURCE: Readonly<Record<RunRecordDto[\"source\"], string>> = {\n schedule: \"Schedule\",\n manual: \"Manual\",\n queued: \"Queued trigger\",\n resume: \"Resume\",\n};\n\n/** An instant as a stable, readable UTC string: `2026-09-18 03:00:00 UTC`. */\nexport function formatInstant(ms: number): string {\n return `${new Date(ms).toISOString().slice(0, 19).replace(\"T\", \" \")} UTC`;\n}\n\n/**\n * A schedule in words:\n * - `null` → \"None: runs only when triggered\"\n * - `{ cron, tz? }` → \"Cron 0 3 * * * in Europe/London\" (or \"in the owner's local time\")\n * - `{ every, anchor? }` → \"Every 30s, aligned to …\" (or \"from when it started\")\n * - `{ at }` → \"Once, at …\"\n */\nexport function describeSchedule(schedule: RunnerScheduleDto): string {\n if (schedule === null) {\n return \"None: runs only when triggered\";\n }\n if (\"cron\" in schedule) {\n return `Cron ${schedule.cron} in ${schedule.tz ?? \"the owner's local time\"}`;\n }\n if (\"every\" in schedule) {\n const anchor =\n schedule.anchor === undefined\n ? \"from when it started\"\n : `aligned to ${formatInstant(schedule.anchor)}`;\n return `Every ${formatMs(schedule.every)}, ${anchor}`;\n }\n return `Once, at ${formatInstant(schedule.at)}`;\n}\n\n/** A runner's concurrency cap: absent means unlimited (JSON cannot carry `Infinity`). */\nexport function describeConcurrency(\n maxConcurrency: number | undefined,\n): string {\n return maxConcurrency === undefined\n ? \"unlimited\"\n : formatNumber(maxConcurrency);\n}\n\n/** Whether triggers queue while busy, and how many may: \"Yes, up to 100\" / \"No\". */\nexport function describeQueueing(runner: RunnerInfoDto): string | null {\n if (runner.queueRuns === undefined) {\n return runner.maxQueuedRuns === undefined\n ? null\n : `up to ${formatNumber(runner.maxQueuedRuns)}`;\n }\n if (!runner.queueRuns) {\n return \"No\";\n }\n return runner.maxQueuedRuns === undefined\n ? \"Yes\"\n : `Yes, up to ${formatNumber(runner.maxQueuedRuns)}`;\n}\n\n/**\n * The header's badges: the local instance's status (or, for a runner\n * registered in another process, paused or active from the shared flag), plus\n * \"Run in flight\" while a run executes anywhere.\n */\nexport function runnerBadges(runner: RunnerInfoDto): RunnerBadge[] {\n const badges: RunnerBadge[] = [\n runner.local\n ? RUNNER_STATUS[runner.local.status]\n : runner.isPaused\n ? RUNNER_STATUS.paused\n : NON_LOCAL_ACTIVE,\n ];\n if (runner.isRunning || (runner.local?.activeRuns.length ?? 0) > 0) {\n badges.push(RUN_IN_FLIGHT);\n }\n return badges;\n}\n\n/** Page sizes offered for history; those above `maxHistory` are dropped. */\nexport const HISTORY_LIMITS: readonly number[] = [10, 25, 50, 100, 200];\n\n/** The API's default history size: `min(50, maxHistory)`. */\nexport function defaultHistoryLimit(maxHistory: number): number {\n return Math.max(1, Math.min(50, maxHistory));\n}\n\n/** The history sizes to offer: {@link HISTORY_LIMITS} up to `maxHistory`, plus the default and the cap itself. */\nexport function historyLimitOptions(maxHistory: number): number[] {\n const max = Math.max(1, maxHistory);\n return [\n ...new Set([\n ...HISTORY_LIMITS.filter((limit) => limit <= max),\n defaultHistoryLimit(max),\n max,\n ]),\n ].sort((a, b) => a - b);\n}\n\n/** Whether an error means the runner does not exist. */\nexport function isRunnerNotFound(error: unknown): boolean {\n return isApiError(error) && error.code === \"RUNNER_NOT_FOUND\";\n}\n\n/** Whether an error means the caller may not read this runner (401/403). */\nexport function isRunnerDenied(error: unknown): error is ApiError {\n return isApiError(error) && error.isAuth;\n}\n\n/** A run's duration: its own `durationMs`, else the span it recorded. */\nexport function runDuration(run: RunRecordDto): string | null {\n const ms =\n run.durationMs ??\n (run.finishedAt === undefined ? undefined : run.finishedAt - run.startedAt);\n return ms === undefined ? null : formatMs(Math.max(0, ms));\n}\n\n/** The sentence on the screen of a runner registered in another process. */\nexport const NON_LOCAL_RUNNER_NOTE =\n \"This runner is registered by another process; changes are adopted at its owner's next sync.\";\n\n/**\n * Orders the list: local runners first, then those registered in other\n * processes, each group in the order the API gave (local by registration,\n * the others sorted by id).\n */\nexport function orderRunners(\n items: readonly RunnerListItemDto[],\n): RunnerListItemDto[] {\n return [\n ...items.filter((item) => item.isLocal),\n ...items.filter((item) => !item.isLocal),\n ];\n}\n\n/** The runners whose id or name contains `filter` (case-insensitive). */\nexport function filterRunners(\n items: readonly RunnerListItemDto[],\n filter: string,\n): RunnerListItemDto[] {\n const needle = filter.trim().toLowerCase();\n if (!needle) {\n return [...items];\n }\n return items.filter(\n (item) =>\n item.id.toLowerCase().includes(needle) ||\n (item.name?.toLowerCase().includes(needle) ?? false),\n );\n}\n\n/** The runs newest first (the API's order already; a stable sort keeps ties as given). */\nexport function newestFirst(runs: readonly RunRecordDto[]): RunRecordDto[] {\n return [...runs].sort((a, b) => b.startedAt - a.startedAt);\n}\n",
19
+ "import type { UseQueryResult } from \"@tanstack/react-query\";\nimport type { HistoryWindow } from \"../../api/runners\";\nimport type { RunnerHistoryDto, RunRecordDto } from \"../../api/types\";\nimport type { PageWindow } from \"../../components/pagerState\";\nimport { keepPreviousData, useQuery } from \"@tanstack/react-query\";\nimport { getRunnerHistory, runnerKeys } from \"../../api/runners\";\nimport { useApiClient } from \"../../context\";\nimport { clampLimit, intParam, useUrlParams } from \"../queues/urlState\";\nimport { useHistoryRefetchInterval } from \"./live\";\nimport { defaultHistoryLimit, newestFirst } from \"./runnerFormat\";\n\n/**\n * Reading a page of the run history — **the one seam** between the history\n * table and where its pages come from.\n *\n * `GET /runners/:runner/history` takes a window (`offset`, `limit`, `order`)\n * and reports `page.total`, so the table pages the runner's **whole** stored\n * history rather than a fetched prefix of it: the \"Runs shown\" select is the\n * page size, and turning a page re-reads with a new offset. `limits.maxHistory`\n * caps a page, never how deep `offset` may read, so every run `keepHistory`\n * holds is reachable.\n *\n * The window lives in the URL (`offset`, beside `history` and `logs`), as the\n * jobs table's and the runners list's do. That is what keeps `?logs=` working:\n * a link made from a Log button carries the page its run is on, since\n * `update()` merges into the query string it was pressed on. A `?logs=` that\n * names a run **not** on the page shown cannot be located — no route gives a\n * run's position, and a page that is not there cannot be computed from rows we\n * do not have — so {@link RunnerHistoryPage.openRunOffPage} says so and the\n * card tells the reader, rather than pretending.\n *\n * The table takes everything it needs from this module's return value and\n * never does the arithmetic itself, so a future cursor-paged route replaces\n * this function alone.\n */\n\n/** Options of {@link useRunnerHistory}. */\nexport interface RunnerHistoryPageOptions {\n /** The runner's id. */\n runner: string;\n /** The API's largest page (`limits.maxHistory`), which bounds the size select. */\n maxHistory: number;\n /** Whether to read (and poll) the history at all. */\n enabled: boolean;\n /**\n * The run whose log the URL has open (`?logs=`), or `null` — including\n * `null` when the caller may not read run logs, since nothing is open then.\n */\n openRunId: string | null;\n}\n\n/** The page of runs on screen, and what its card and pager need. */\nexport interface RunnerHistoryPage {\n /** The read behind the page; the card renders its pending, error and data states. */\n query: UseQueryResult<RunnerHistoryDto, Error>;\n /** The runs on the page, newest first. */\n rows: RunRecordDto[];\n /** Runs skipped before the page, as the URL asked for them. */\n offset: number;\n /** Runs per page — the \"Runs shown\" select, clamped to `1…maxHistory`. */\n limit: number;\n /**\n * Every run the runner has stored, from the route's `page.total`, which is\n * always present and exact here. `0` until the first answer arrives.\n */\n total: number;\n /** Whether the history outgrows one page, and so deserves a pager. */\n paged: boolean;\n /**\n * Whether a new page is being read to replace the one on screen. The pager\n * is disabled while it is true, so one click is one page.\n */\n turning: boolean;\n /**\n * Whether `openRunId` names a run that is not on this page. The card says so\n * rather than silently opening nothing; `false` while the page is empty or\n * still loading, when there is nothing yet to be missing from.\n */\n openRunOffPage: boolean;\n /** Takes the window the `Pager` asks for, and writes it to the URL. */\n onChange: (next: PageWindow) => void;\n}\n\n/**\n * The page of the runner's history the URL asks for, read from the server:\n * which runner, how large a page may be, whether to read at all, and which\n * run's log the URL has open ({@link RunnerHistoryPageOptions}).\n */\nexport function useRunnerHistory({\n runner,\n maxHistory,\n enabled,\n openRunId,\n}: RunnerHistoryPageOptions): RunnerHistoryPage {\n const api = useApiClient();\n const [params, update] = useUrlParams();\n const fallback = defaultHistoryLimit(maxHistory);\n const limit = clampLimit(intParam(params, \"history\", fallback), maxHistory);\n // Uncapped on purpose, as the route is: a `keepHistory` far above\n // `maxHistory` is exactly what deep offsets exist to reach.\n const offset = intParam(params, \"offset\", 0);\n const asked: HistoryWindow = { offset, limit, order: \"desc\" };\n const refetchInterval = useHistoryRefetchInterval();\n const query = useQuery({\n queryKey: runnerKeys.history(runner, asked),\n queryFn: ({ signal }) => getRunnerHistory(api, runner, asked, signal),\n refetchInterval,\n enabled,\n // Turning a page swaps the rows in place instead of dropping the table\n // back to its spinner.\n placeholderData: keepPreviousData,\n });\n const rows = query.data ? newestFirst(query.data.items) : [];\n const total = query.data?.page.total ?? 0;\n return {\n query,\n rows,\n offset,\n limit,\n total,\n paged: total > limit,\n turning: query.isFetching && query.isPlaceholderData,\n openRunOffPage:\n openRunId !== null &&\n rows.length > 0 &&\n !rows.some((run) => run.runId === openRunId),\n onChange: (next) => {\n // A new page size restarts paging: offsets of the old size name\n // different runs, and \"page 3 of 25\" is not \"page 3 of 10\". Doing it\n // here is what keeps the card's \"Runs shown\" select and the pager's own\n // \"Rows per page\" — two views of one value — behaving identically.\n const moved = next.limit === limit ? next.offset : 0;\n update({\n offset: moved > 0 ? String(moved) : null,\n // The default is spelled by leaving the parameter out, as the rest of\n // the screen's parameters are.\n history: next.limit === fallback ? null : String(next.limit),\n });\n },\n };\n}\n",
20
+ "import type { RunLogLevel, RunLogStream } from \"../../api/types\";\nimport type { BadgeTone } from \"../../components/Badge\";\nimport {\n DEFAULT_REDACT_REPLACEMENT,\n RUN_LOG_HINT_MS,\n RUN_LOG_LEVELS,\n RUN_LOG_STREAMS,\n} from \"../../api/contract\";\n\n/**\n * How a run's captured output is labelled: the streams a filter offers, the\n * severity colours, and a line's clock time.\n *\n * Every list here is built from the contract's own (`RUN_LOG_STREAMS`,\n * `RUN_LOG_LEVELS`), so a stream or a level the API adds appears with no\n * change on this side.\n */\n\n/**\n * What capture sees and what it never does, stated wherever a run's log is\n * shown. It is a documented limit of how bun-jobs captures output, not a gap\n * in this view: a quiet log does not prove a quiet run.\n *\n * Every mode captures its console now: a spawned run through its pipes, a\n * `worker` or `in-process` run by attributing each `console` call to the run\n * that made it (so two runs at once each keep their own). What escapes is\n * what bypasses both; the note names the gaps a user will actually hit, and\n * the README carries the full list.\n */\nexport const CAPTURE_GAP_NOTE =\n \"A run's console output is captured whichever way it runs: console.log, info and debug as stdout, warn and error as stderr, beside its ctx.log() lines. Output written by native code, by a program the run launched with its own output, or, in a worker or in-process run, straight to process.stdout or process.stderr is not captured here.\";\n\n/**\n * That a stored line may not read exactly as the run printed it: capture\n * redacts values that look like secrets before storing a line, by default.\n * Deliberately no list of what it catches — the defaults are the backend's\n * and may change, and a list here would go stale.\n */\nexport const REDACTION_NOTE = `Values that look like secrets are replaced with ${DEFAULT_REDACT_REPLACEMENT} before a line is stored, by default, so some lines may not read exactly as the run printed them.`;\n\n/**\n * The note on a live run's log saying how it is followed: by the runner's\n * `logs` hint, with `poll` as the fallback, while live updates have relaxed\n * the poll beyond `base`; by polling every `base` otherwise.\n */\nexport function followingNote(poll: number | false, base: number): string {\n // A live run's record carries no line count until the run settles, so while\n // it runs the log itself is the count — nothing on screen lags behind it.\n const count =\n \"While the run is going, its history row shows no line count yet: the lines here are the count.\";\n if (poll === false || poll <= base) {\n return `Following this run: re-read every ${base / 1000} s. Live updates are not reaching this view, so this is a poll. ${count}`;\n }\n return `Following this run: each time its log grows the live socket says so (at most every ${RUN_LOG_HINT_MS / 1000} s), and the new lines are read at once. A re-read every ${poll / 1000} s is the fallback, in case a notice goes missing. ${count}`;\n}\n\n/** What the stream filter is called in the URL. */\nexport const STREAM_PARAM = \"logStream\";\n\n/** What the `logs` URL parameter holds: the run id whose log is open. */\nexport const LOGS_PARAM = \"logs\";\n\n/** The label each stream is shown under; an unlisted one shows its own name. */\nconst STREAM_LABELS: Readonly<Record<string, string>> = {\n stdout: \"stdout\",\n stderr: \"stderr\",\n log: \"logger\",\n};\n\n/** How a stream is labelled in the filter and beside a line. */\nexport function streamLabel(stream: RunLogStream): string {\n return STREAM_LABELS[stream] ?? stream;\n}\n\n/** The stream filter's choices: every stream, then one per `RUN_LOG_STREAMS` in the order it offers them. */\nexport function streamOptions(): { value: string; label: string }[] {\n return [\n { value: \"\", label: \"Every stream\" },\n ...RUN_LOG_STREAMS.map((stream) => ({\n value: stream,\n label: streamLabel(stream),\n })),\n ];\n}\n\n/** Whether `value` names a stream the API knows; anything else means no filter. */\nexport function isRunLogStream(\n value: string | null | undefined,\n): value is RunLogStream {\n return (\n value !== null &&\n value !== undefined &&\n (RUN_LOG_STREAMS as readonly string[]).includes(value)\n );\n}\n\n/**\n * The badge colour of a level, by its place in `RUN_LOG_LEVELS` (increasing\n * severity): `error` and above are danger, `warn` warning, `info` info, the\n * quieter ones neutral. A level the list does not hold is neutral, so a\n * logger that grows a level still renders.\n */\nexport function levelTone(level: RunLogLevel): BadgeTone {\n const rank = (RUN_LOG_LEVELS as readonly string[]).indexOf(level);\n if (rank < 0) {\n return \"neutral\";\n }\n if (rank >= RUN_LOG_LEVELS.indexOf(\"error\")) {\n return \"danger\";\n }\n if (rank >= RUN_LOG_LEVELS.indexOf(\"warn\")) {\n return \"warning\";\n }\n return rank >= RUN_LOG_LEVELS.indexOf(\"info\") ? \"info\" : \"neutral\";\n}\n\n/** Two digits, for {@link formatClockTime}. */\nfunction pad(value: number, length = 2): string {\n return String(value).padStart(length, \"0\");\n}\n\n/**\n * A line's local clock time, `HH:MM:SS.mmm` — the time of day, not a\n * relative age: consecutive lines are milliseconds apart, and \"now\" would\n * say nothing about their order. The ISO instant goes in the `<time>`'s\n * `dateTime` and `title` beside it.\n */\nexport function formatClockTime(at: number): string {\n const when = new Date(at);\n if (Number.isNaN(when.getTime())) {\n return \"--:--:--.---\";\n }\n return `${pad(when.getHours())}:${pad(when.getMinutes())}:${pad(when.getSeconds())}.${pad(when.getMilliseconds(), 3)}`;\n}\n\n/** The ISO instant of a line, or `undefined` when its `at` is not a time. */\nexport function isoTime(at: number): string | undefined {\n const when = new Date(at);\n return Number.isNaN(when.getTime()) ? undefined : when.toISOString();\n}\n",
21
+ "import { useCan, useFeature } from \"../../meta/hooks\";\n\n/**\n * Whether this caller gets run logs at all: the backend stores them\n * (`meta.features.runnerLogs`) and the caller may read them\n * (`runners.logs`, a default-on read like `jobs.logs`).\n *\n * Deliberately **not** `logLines !== undefined`: that field says what one\n * run recorded, and a run recorded before logs were kept would switch the\n * whole feature off for the screen.\n */\nexport function useCanReadRunLogs(): boolean {\n const feature = useFeature(\"runnerLogs\");\n const allowed = useCan(\"runners.logs\");\n return feature && allowed;\n}\n",
22
+ "import type { RunLogTail } from \"../../api/runnerLogs\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useId } from \"react\";\nimport {\n getRunLogs,\n isLogsNotRetained,\n isRunNotFound,\n mergeRunLogPage,\n RUN_LOG_POLL_MS,\n RUN_LOG_TAIL_MAX,\n runLogHintNeedsRead,\n runLogKeys,\n runLogPageLimit,\n runLogsRefetchInterval,\n} from \"../../api/runnerLogs\";\nimport { Badge } from \"../../components/Badge\";\nimport { cx } from \"../../components/classNames\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { Select } from \"../../components/inputs\";\nimport { ProblemBanner } from \"../../components/ProblemBanner\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { useApiClient } from \"../../context\";\nimport { formatNumber, plural } from \"../../format\";\nimport { usePollInterval } from \"../../live\";\nimport { useMeta } from \"../../meta/hooks\";\nimport { LogLines } from \"../logs/LogLines\";\nimport { useUrlParams } from \"../queues/urlState\";\nimport { useRunLogHints } from \"./live\";\nimport {\n CAPTURE_GAP_NOTE,\n followingNote,\n formatClockTime,\n isoTime,\n isRunLogStream,\n levelTone,\n REDACTION_NOTE,\n STREAM_PARAM,\n streamLabel,\n streamOptions,\n} from \"./runLogFormat\";\n\n/** What the per-line cut marker says, and what it means. */\nconst TRUNCATED_HINT =\n \"Capture cut this line at its 8 KiB limit, so it is shorter than what the run printed.\";\n\n/** Props of {@link RunLogs}. */\nexport interface RunLogsProps {\n /** The runner's id. */\n runner: string;\n /** The run's id. */\n runId: string;\n /** Whether to read the log and follow it. Defaults to `true`. */\n enabled?: boolean;\n}\n\n/**\n * One run's captured output, tailed.\n *\n * Each read asks for the lines above the last `seq` this view has\n * accounted for (`?since=`, an exclusive bound), and the page is folded into\n * the ones already read.\n *\n * **What makes it move.** While the run is live, the runner's `logs` event\n * on `runner/<runner>` — a hint carrying `{ runId, lastSeq }`, never a line —\n * triggers a read for this run whenever its `lastSeq` is past the cursor;\n * hints for the runner's other runs are ignored. A poll stays underneath as\n * the fallback: every {@link RUN_LOG_POLL_MS} while live updates are off,\n * relaxed by `usePollInterval` while they are on, because then a missed hint\n * only delays a line until the next read (which, being `since` the cursor,\n * still returns it). Once the run has finished and its log has been read to\n * the end, both stop.\n *\n * The stream filter lives in the URL (`logStream`), and each filter keeps\n * its own tail: `since` is a raw sequence number, so switching the filter\n * re-reads from the start of what is kept rather than resuming mid-stream.\n *\n * **A line is a line, not an event.** A spawned run's pretty-printed logger\n * arrives as several `stdout` lines and is shown as several; nothing here\n * tries to join them back together, which is what makes the stream filter\n * worth having (`log` alone is the handler's own narrative). A `log` line's\n * fields are already rendered into its text at capture, and a line may carry\n * no level at all, so neither is treated as structure to unpack.\n */\nexport function RunLogs({ runner, runId, enabled = true }: RunLogsProps) {\n const api = useApiClient();\n const { limits } = useMeta();\n const queryClient = useQueryClient();\n const [params, update] = useUrlParams();\n const filterId = useId();\n const raw = params.get(STREAM_PARAM);\n const stream = isRunLogStream(raw) ? raw : undefined;\n const limit = runLogPageLimit(limits.maxLogPage);\n const queryKey = runLogKeys.logs(runner, runId, stream);\n const poll = usePollInterval(RUN_LOG_POLL_MS);\n\n const logs = useQuery({\n queryKey,\n enabled,\n queryFn: async ({ signal }) => {\n // The tail accumulates in the cache, so a view that unmounts and comes\n // back resumes from its cursor instead of re-reading the whole log.\n const previous = queryClient.getQueryData<RunLogTail>(queryKey);\n const page = await getRunLogs(\n api,\n runner,\n runId,\n {\n since: previous?.cursor === 0 ? undefined : previous?.cursor,\n limit,\n order: \"asc\",\n stream,\n },\n signal,\n );\n return mergeRunLogPage(previous, page);\n },\n refetchInterval: (query) =>\n isLogsNotRetained(query.state.error) || isRunNotFound(query.state.error)\n ? false\n : runLogsRefetchInterval(query.state.data, poll),\n });\n\n const tail = logs.data;\n /** Re-reads this filter's tail from its cursor, if the query is mounted and enabled. */\n const read = () =>\n void queryClient.refetchQueries({ queryKey, exact: true, type: \"active\" });\n useRunLogHints(runner, runId, enabled && tail?.live !== false, {\n onHint: (lastSeq) => {\n if (\n runLogHintNeedsRead(\n queryClient.getQueryData<RunLogTail>(queryKey),\n lastSeq,\n )\n ) {\n read();\n }\n },\n onGap: read,\n });\n const filter = (\n <span className=\"run-log-filter\">\n <label htmlFor={filterId}>Stream</label>\n <Select\n id={filterId}\n options={streamOptions()}\n value={stream ?? \"\"}\n onChange={(value) => update({ [STREAM_PARAM]: value || null })}\n />\n </span>\n );\n\n return (\n <section\n className=\"run-logs\"\n aria-label={`Log of run ${runId}`}\n data-testid={`run-logs-${runId}`}\n >\n <div className=\"run-log-header\">{filter}</div>\n {isLogsNotRetained(logs.error) ? (\n <EmptyState\n title=\"Run logs are not retained\"\n description={\n logs.error.detail ??\n \"This API keeps no run logs at all, so there is none for this run to read. That is not the same as a run that logged nothing.\"\n }\n />\n ) : isRunNotFound(logs.error) ? (\n <EmptyState\n title=\"No log for this run\"\n description={\n // The API cannot yet tell an unknown run from one whose record\n // has aged out, so neither can this.\n \"This runner has no record of the run: either it never ran here, or it has aged out of the runs this runner keeps, and its log went with it.\"\n }\n />\n ) : !tail ? (\n logs.isError ? (\n <ProblemBanner\n error={logs.error}\n title=\"Could not load the run log\"\n onRetry={() => void logs.refetch()}\n />\n ) : (\n <Spinner\n label=\"Loading the run log\"\n showLabel\n />\n )\n ) : (\n <>\n {logs.isError && (\n <ProblemBanner\n error={logs.error}\n title=\"Could not refresh the run log\"\n onRetry={() => void logs.refetch()}\n />\n )}\n <RunLogNotes\n tail={tail}\n poll={poll}\n />\n {tail.lines.length === 0 ? (\n <p\n className=\"muted\"\n data-testid=\"run-logs-empty\"\n >\n {emptyText(tail, stream !== undefined)}\n </p>\n ) : (\n <LogLines\n label={`Log lines of run ${runId}`}\n items={tail.lines.map((line) => ({\n // `seq` is the line's identity in its run, and a gap in the\n // gutter is where the cap dropped lines.\n id: line.seq,\n number: line.seq,\n text: line.message,\n className: cx(\"run-log-line\", `run-log-line-${line.stream}`),\n prefix: (\n <>\n <time\n className=\"run-log-time\"\n dateTime={isoTime(line.at)}\n title={isoTime(line.at)}\n >\n {formatClockTime(line.at)}\n </time>\n <span className=\"run-log-stream\">\n {streamLabel(line.stream)}\n </span>\n {line.level !== undefined && (\n <Badge\n className=\"run-log-level\"\n tone={levelTone(line.level)}\n >\n {line.level}\n </Badge>\n )}\n </>\n ),\n suffix: line.truncated ? (\n <Badge\n className=\"run-log-cut\"\n tone=\"warning\"\n title={TRUNCATED_HINT}\n >\n cut at 8 KiB\n </Badge>\n ) : undefined,\n }))}\n />\n )}\n <p className=\"muted run-log-gap\">{CAPTURE_GAP_NOTE}</p>\n <p className=\"muted run-log-redaction\">{REDACTION_NOTE}</p>\n </>\n )}\n </section>\n );\n}\n\n/** What an empty tail means: nothing on this stream, nothing yet, or a run that logged nothing at all. */\nfunction emptyText(tail: RunLogTail, filtered: boolean): string {\n if (filtered) {\n return tail.live\n ? \"Nothing on this stream yet.\"\n : \"This run logged nothing on this stream.\";\n }\n return tail.live\n ? \"Nothing logged yet.\"\n : \"This run logged nothing. A 200 with no lines means the log is kept and empty, not that it is missing.\";\n}\n\n/** Props of {@link RunLogNotes}. */\ninterface RunLogNotesProps {\n /** The tail the notes describe. */\n tail: RunLogTail;\n /** The fallback poll in force, ms: {@link RUN_LOG_POLL_MS} while live updates are off, relaxed while they are on. */\n poll: number | false;\n}\n\n/** What this log is not showing, and why: the caps, this view's own ceiling, and whether it is still moving. */\nfunction RunLogNotes({ tail, poll }: RunLogNotesProps) {\n const notes: string[] = [];\n if (tail.dropped > 0) {\n notes.push(\n `${plural(tail.dropped, \"earlier line\")} dropped: this run's log keeps its most recent lines only (1,000 lines and 1 MiB per run by default). The count is the run's own, whatever this filter shows.`,\n );\n }\n if (tail.capped) {\n notes.push(\n \"A cap is trimming this log now: its oldest lines go as new ones arrive.\",\n );\n }\n if (tail.trimmed > 0) {\n notes.push(\n `${plural(tail.trimmed, \"earlier line\")} left this view, which keeps the last ${formatNumber(RUN_LOG_TAIL_MAX)}. Reopen the log to read from the start of what is kept.`,\n );\n }\n if (tail.live) {\n notes.push(followingNote(poll, RUN_LOG_POLL_MS));\n }\n if (notes.length === 0) {\n return null;\n }\n return (\n <div\n className=\"run-log-notes\"\n data-testid=\"run-log-notes\"\n >\n {notes.map((note) => (\n <p\n key={note}\n className=\"muted\"\n >\n {note}\n </p>\n ))}\n </div>\n );\n}\n",
23
+ "import type { RunRecordDto } from \"../../api/types\";\nimport { hasRunLogs } from \"../../api/runnerLogs\";\nimport { Button } from \"../../components/Button\";\nimport { useUrlParams } from \"../queues/urlState\";\nimport { LOGS_PARAM } from \"./runLogFormat\";\nimport { useCanReadRunLogs } from \"./runLogGates\";\nimport { RunLogs } from \"./RunLogs\";\n\n/** Props of {@link RunLogsSection}. */\nexport interface RunLogsSectionProps {\n /** The run whose log this is. */\n run: RunRecordDto;\n}\n\n/**\n * A run's log inside its details: a disclosure that reads nothing until it\n * is opened, and states what this run has instead when there is nothing to\n * open.\n *\n * Which run's log is open lives in the URL (`logs=<runId>`), so a link\n * reproduces the view and the run history's own Logs button opens the same\n * thing.\n */\nexport function RunLogsSection({ run }: RunLogsSectionProps) {\n const canRead = useCanReadRunLogs();\n const [params, update] = useUrlParams();\n const open = params.get(LOGS_PARAM) === run.runId;\n if (!canRead) {\n return null;\n }\n if (!hasRunLogs(run)) {\n // The only way here is a finished run whose record says `logLines: 0`:\n // its log is kept and empty. A record with no `logLines` at all says\n // nothing, so its log is offered and the read answers for it.\n return (\n <p\n className=\"muted run-log-section\"\n data-testid={`run-no-logs-${run.runId}`}\n >\n This run logged nothing.\n </p>\n );\n }\n return (\n <div className=\"run-log-section\">\n <Button\n size=\"sm\"\n aria-expanded={open}\n onClick={() => update({ [LOGS_PARAM]: open ? null : run.runId })}\n >\n {open ? \"Hide log\" : \"Show log\"}\n </Button>\n {open && (\n <RunLogs\n runner={run.runnerId}\n runId={run.runId}\n />\n )}\n </div>\n );\n}\n",
24
+ "import type { RunRecordDto } from \"../../api/types\";\nimport { Badge } from \"../../components/Badge\";\nimport { CopyButton } from \"../../components/CopyButton\";\nimport { JsonView } from \"../../components/JsonView\";\nimport { KeyValue } from \"../../components/KeyValue\";\nimport { RelativeTime } from \"../../components/RelativeTime\";\nimport { formatNumber } from \"../../format\";\nimport { ErrorDetails } from \"../job/ErrorDetails\";\nimport { RUN_SOURCE, RUN_STATUS, runDuration } from \"./runnerFormat\";\n\n/** Props of {@link RunStatusBadge}. */\nexport interface RunStatusBadgeProps {\n /** The run's status. */\n status: RunRecordDto[\"status\"];\n}\n\n/** A run's outcome, colour-coded. */\nexport function RunStatusBadge({ status }: RunStatusBadgeProps) {\n const { label, tone } = RUN_STATUS[status];\n return <Badge tone={tone}>{label}</Badge>;\n}\n\n/** Props of {@link RunRecordDetails}. */\nexport interface RunRecordDetailsProps {\n /** The run. */\n run: RunRecordDto;\n}\n\n/** Everything a run recorded: ids, timings, the process's exit, the error and the result. */\nexport function RunRecordDetails({ run }: RunRecordDetailsProps) {\n const where =\n run.host === undefined && run.pid === undefined\n ? null\n : [run.host, run.pid === undefined ? undefined : `pid ${run.pid}`]\n .filter(Boolean)\n .join(\", \");\n return (\n <div\n className=\"run-record\"\n data-testid={`run-${run.runId}`}\n >\n <KeyValue\n className=\"run-summary\"\n items={[\n {\n label: \"Run id\",\n value: (\n <span className=\"run-id-line\">\n <code>{run.runId}</code>\n <CopyButton\n text={run.runId}\n ariaLabel={`Copy run id ${run.runId}`}\n />\n </span>\n ),\n },\n { label: \"Status\", value: <RunStatusBadge status={run.status} /> },\n { label: \"Attempt\", value: formatNumber(run.attempt) },\n { label: \"Source\", value: RUN_SOURCE[run.source] },\n { label: \"Execution\", value: run.mode },\n where !== null && { label: \"Where\", value: where },\n { label: \"Started\", value: <RelativeTime value={run.startedAt} /> },\n {\n label: \"Finished\",\n value:\n run.finishedAt === undefined ? null : (\n <RelativeTime value={run.finishedAt} />\n ),\n },\n { label: \"Duration\", value: runDuration(run) },\n run.exitCode !== undefined && {\n label: \"Exit code\",\n value: run.exitCode === null ? null : String(run.exitCode),\n },\n run.signal !== undefined && {\n label: \"Signal\",\n value: run.signal,\n },\n run.detached !== undefined && {\n label: \"Detached\",\n value: run.detached ? \"Yes: it outlived its owner\" : \"No\",\n },\n ]}\n />\n {run.error && (\n <ErrorDetails\n error={run.error}\n label={`Error of run ${run.runId}`}\n />\n )}\n {run.result !== undefined && (\n <JsonView\n value={run.result}\n label={`Result of run ${run.runId}`}\n expandDepth={1}\n />\n )}\n </div>\n );\n}\n",
25
+ "import type { RunnerInfoDto, RunRecordDto } from \"../../api/types\";\nimport { Fragment, useId, useState } from \"react\";\nimport { hasRunLogs } from \"../../api/runnerLogs\";\nimport { Badge } from \"../../components/Badge\";\nimport { Button } from \"../../components/Button\";\nimport { Card } from \"../../components/Card\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { Select } from \"../../components/inputs\";\nimport { Pager } from \"../../components/Pager\";\nimport { ProblemBanner } from \"../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../components/RelativeTime\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { Table } from \"../../components/Table\";\nimport { formatNumber, plural } from \"../../format\";\nimport { useMeta } from \"../../meta/hooks\";\nimport { useCanMutate } from \"../queues/gating\";\nimport { useUrlParams } from \"../queues/urlState\";\nimport { runnerActionGates } from \"./actions/gating\";\nimport { ClearHistoryDialog } from \"./actions/RunnerDialogs\";\nimport { useRunnerHistory } from \"./historyPage\";\nimport { LOGS_PARAM } from \"./runLogFormat\";\nimport { useCanReadRunLogs } from \"./runLogGates\";\nimport { RunLogsSection } from \"./RunLogsSection\";\nimport { historyLimitOptions, RUN_SOURCE, runDuration } from \"./runnerFormat\";\nimport { RunRecordDetails, RunStatusBadge } from \"./RunRecord\";\n\n/** Props of {@link RunnerHistory}. */\nexport interface RunnerHistoryProps {\n /** The runner's id. */\n runner: string;\n /** Whether to read (and poll) the history. Defaults to `true`. */\n enabled?: boolean;\n /**\n * The runner as `GET /runners/:runner` returned it, which Clear history…\n * needs (its gate and its confirmation read the runner's mode and whether\n * it is local). Absent, the card shows no Clear history….\n */\n info?: RunnerInfoDto;\n}\n\n/** Props of {@link HistoryRow}. */\ninterface HistoryRowProps {\n /** The run this row shows. */\n run: RunRecordDto;\n /** Whether the table carries its Logs column (the caller may read run logs). */\n showLogs: boolean;\n}\n\n/** One run, and its details once expanded. */\nfunction HistoryRow({ run, showLogs }: HistoryRowProps) {\n const [params, update] = useUrlParams();\n const [expanded, setExpanded] = useState(false);\n const detailsId = useId();\n // A row whose log the URL opens is expanded, so the log is on screen; the\n // disclosure closes both.\n const logsOpen = showLogs && params.get(LOGS_PARAM) === run.runId;\n const open = expanded || logsOpen;\n const toggle = () => {\n if (open) {\n setExpanded(false);\n if (logsOpen) {\n update({ [LOGS_PARAM]: null });\n }\n return;\n }\n setExpanded(true);\n };\n const dropped = run.logsDropped ?? 0;\n return (\n <Fragment>\n <tr data-testid={`history-row-${run.runId}`}>\n <td>\n <button\n type=\"button\"\n className=\"run-toggle\"\n aria-expanded={open}\n aria-controls={open ? detailsId : undefined}\n aria-label={`${open ? \"Hide\" : \"Show\"} run ${run.runId}`}\n onClick={toggle}\n >\n <span aria-hidden=\"true\">{open ? \"▾\" : \"▸\"}</span>\n </button>\n </td>\n <td>\n <RelativeTime value={run.startedAt} />\n </td>\n <td>\n <RunStatusBadge status={run.status} />\n </td>\n <td>{RUN_SOURCE[run.source]}</td>\n <td className=\"numeric\">{formatNumber(run.attempt)}</td>\n <td className=\"numeric\">{runDuration(run) ?? \"—\"}</td>\n <td>\n <code className=\"run-id\">{run.runId}</code>\n </td>\n {showLogs && (\n <td className=\"run-logs-cell\">\n {hasRunLogs(run) && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-expanded={logsOpen}\n onClick={() =>\n update({ [LOGS_PARAM]: logsOpen ? null : run.runId })\n }\n >\n {logsOpen ? \"Hide log\" : \"Log\"}\n </Button>\n )}\n {dropped > 0 && (\n <Badge\n tone=\"warning\"\n title={`${plural(dropped, \"line\")} of this run's log went to the log's cap, oldest first.`}\n >\n {plural(dropped, \"line\")} dropped\n </Badge>\n )}\n </td>\n )}\n </tr>\n {open && (\n <tr\n id={detailsId}\n className=\"run-details-row\"\n >\n <td colSpan={showLogs ? 8 : 7}>\n <RunRecordDetails run={run} />\n {/* The one place a run's log is hosted: the runner screen shows\n the run in flight and the last run again in their own cards,\n and both are rows of this table too, so hosting it here keeps\n one log per run on the screen. */}\n <RunLogsSection run={run} />\n </td>\n </tr>\n )}\n </Fragment>\n );\n}\n\n/**\n * `GET /runners/:runner/history`: the runner's whole stored history, newest\n * first, one server page at a time. The \"Runs shown\" select is the page size,\n * capped at `limits.maxHistory` (kept in the URL as `history`), and the pager\n * moves the window (`offset`). Where a page comes from is\n * {@link useRunnerHistory}'s business, not this component's.\n */\nexport function RunnerHistory({\n runner,\n enabled = true,\n info,\n}: RunnerHistoryProps) {\n const canMutate = useCanMutate();\n // Clear history… lives here, above the runs it clears, rather than with the\n // runner's other actions in the page header.\n const canClear =\n info !== undefined && runnerActionGates(info, canMutate).clearHistory;\n const [clearing, setClearing] = useState(false);\n const { limits } = useMeta();\n const [params, update] = useUrlParams();\n const selectId = useId();\n // Like the workers table's actions column: a caller who cannot read run\n // logs sees the table without the column, not a column of blanks.\n const showLogs = useCanReadRunLogs();\n const options = historyLimitOptions(limits.maxHistory);\n const openRunId = showLogs ? params.get(LOGS_PARAM) : null;\n\n // The page on screen, read from the server: `historyPage.ts` is the only\n // place that knows where a page comes from.\n const page = useRunnerHistory({\n runner,\n maxHistory: limits.maxHistory,\n enabled,\n openRunId,\n });\n const history = page.query;\n // The whole history, not this page: an empty page past the end is not an\n // empty history, and Clear history… clears every run there is.\n const noRuns = history.data !== undefined && page.total === 0;\n\n return (\n <Card\n title=\"History\"\n actions={\n <div className=\"history-actions\">\n {canClear && (\n <Button\n size=\"sm\"\n variant=\"danger\"\n // Nothing to clear yet: say so rather than open an empty dialog.\n disabled={noRuns}\n title={noRuns ? \"No runs to clear.\" : undefined}\n onClick={() => setClearing(true)}\n >\n Clear history…\n </Button>\n )}\n <div className=\"history-limit\">\n <label htmlFor={selectId}>Runs shown</label>\n <Select\n id={selectId}\n options={options.map((value) => ({ value: String(value) }))}\n value={String(page.limit)}\n onChange={(value) =>\n page.onChange({ offset: page.offset, limit: Number(value) })\n }\n />\n </div>\n </div>\n }\n >\n {clearing && info && (\n <ClearHistoryDialog\n runner={info}\n onClose={() => setClearing(false)}\n />\n )}\n {history.isPending ? (\n <Spinner\n label=\"Loading the history\"\n showLabel\n />\n ) : history.isError ? (\n <ProblemBanner\n error={history.error}\n title=\"Could not load the history\"\n onRetry={() => void history.refetch()}\n />\n ) : noRuns ? (\n <EmptyState\n title=\"No runs yet\"\n description=\"Runs show here, newest first, once this runner has run.\"\n />\n ) : page.rows.length === 0 ? (\n // A window past the end of the history — a stale link, or runs that\n // went while it was open. Not \"no runs yet\": there are runs, behind us.\n <EmptyState\n title=\"No runs on this page\"\n description=\"This page starts past the end of the history. Go back for the runs it holds.\"\n />\n ) : (\n <Table\n label=\"Run history\"\n className=\"history-table\"\n >\n <thead>\n <tr>\n <th scope=\"col\">\n <span className=\"visually-hidden\">Details</span>\n </th>\n <th scope=\"col\">Started</th>\n <th scope=\"col\">Status</th>\n <th scope=\"col\">Source</th>\n <th\n scope=\"col\"\n className=\"numeric\"\n >\n Attempt\n </th>\n <th\n scope=\"col\"\n className=\"numeric\"\n >\n Duration\n </th>\n <th scope=\"col\">Run id</th>\n {showLogs && <th scope=\"col\">Log</th>}\n </tr>\n </thead>\n <tbody>\n {page.rows.map((run) => (\n <HistoryRow\n key={run.runId}\n run={run}\n showLogs={showLogs}\n />\n ))}\n </tbody>\n </Table>\n )}\n {page.openRunOffPage && (\n // The URL opens a run's log, and that run is not among these rows.\n // Its page cannot be worked out — no route reports where a run sits,\n // and these rows do not hold it — so say so rather than open nothing.\n <p\n className=\"muted history-open-log-note\"\n data-testid=\"history-open-log-note\"\n >\n {`The log this link opens belongs to run ${openRunId}, which is not on this page — it is further back in the history, or gone. `}\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => update({ [LOGS_PARAM]: null })}\n >\n Close log\n </Button>\n </p>\n )}\n {page.paged && (\n <Pager\n label=\"History pages\"\n offset={page.offset}\n limit={page.limit}\n total={page.total}\n itemCount={page.rows.length}\n pageSizes={options}\n maxPageSize={limits.maxHistory}\n disabled={page.turning}\n onChange={page.onChange}\n />\n )}\n </Card>\n );\n}\n",
26
+ "import type { RunnerInfoDto, RunnerStatsDto } from \"../../api/types\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { getRunner, getRunnerStats, runnerKeys } from \"../../api/runners\";\nimport { Badge } from \"../../components/Badge\";\nimport { Card } from \"../../components/Card\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { ErrorView } from \"../../components/ErrorView\";\nimport { KeyValue } from \"../../components/KeyValue\";\nimport { ProblemBanner } from \"../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../components/RelativeTime\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { useApiClient } from \"../../context\";\nimport { displayText, formatNumber } from \"../../format\";\nimport { useCan, usePermissionsSettled } from \"../../meta/hooks\";\nimport { Link } from \"../../router\";\nimport { useParams } from \"../../routing\";\nimport { RunnerActions } from \"./actions\";\nimport {\n describeAdoption,\n describeOverridden,\n describeOverride,\n} from \"./actions/config\";\nimport { useRunnerLive, useRunnerRefetchInterval } from \"./live\";\nimport {\n describeConcurrency,\n describeQueueing,\n describeSchedule,\n isRunnerDenied,\n isRunnerNotFound,\n NON_LOCAL_RUNNER_NOTE,\n runnerBadges,\n} from \"./runnerFormat\";\nimport { RunnerHistory } from \"./RunnerHistory\";\nimport { RunRecordDetails } from \"./RunRecord\";\nimport \"./runners.css\";\n\n/** The counters, in the order the tiles show them, with their tile colour. */\nconst STAT_TILES: readonly {\n key: keyof RunnerStatsDto;\n label: string;\n state?: string;\n}[] = [\n { key: \"success\", label: \"Success\", state: \"completed\" },\n { key: \"failed\", label: \"Failed\", state: \"failed\" },\n { key: \"timeout\", label: \"Timed out\", state: \"delayed\" },\n { key: \"killed\", label: \"Killed\", state: \"dead\" },\n { key: \"skipped\", label: \"Skipped\" },\n { key: \"queued\", label: \"Queued\", state: \"waiting\" },\n { key: \"total\", label: \"Total\" },\n];\n\n/** Props of {@link RunnerHidden}. */\nexport interface RunnerHiddenProps {\n /** The API's own explanation (a 401/403 problem's `detail`), shown instead of the default sentence. */\n detail?: string;\n}\n\n/** Shown when the caller may not read this runner (`runners.read`), or the API refused. */\nexport function RunnerHidden({ detail }: RunnerHiddenProps) {\n return (\n <div\n className=\"screen\"\n data-testid=\"runner-hidden\"\n >\n <EmptyState\n headingLevel={1}\n title=\"Runner hidden\"\n description={detail ?? \"You may not read this runner.\"}\n action={<Link to=\"/runners\">Back to runners</Link>}\n />\n </div>\n );\n}\n\n/** Shown when no runner has this id. */\nexport function RunnerNotFound({ id }: { id: string }) {\n return (\n <div\n className=\"screen\"\n data-testid=\"runner-not-found\"\n >\n <EmptyState\n headingLevel={1}\n title=\"Runner not found\"\n description={\n <>\n There is no runner <code>{id}</code> in this namespace. It may have\n been unregistered, or its process has not started yet.\n </>\n }\n action={<Link to=\"/runners\">Back to runners</Link>}\n />\n </div>\n );\n}\n\n/** Props of {@link RunnerSummary}. */\nexport interface RunnerSummaryProps {\n /** The runner. */\n runner: RunnerInfoDto;\n}\n\n/** The runner's settings and state as label/value rows. */\nexport function RunnerSummary({ runner }: RunnerSummaryProps) {\n return (\n <KeyValue\n className=\"runner-summary\"\n items={[\n { label: \"Namespace\", value: <code>{runner.namespace}</code> },\n {\n label: \"Registered here\",\n value: runner.isLocal ? \"Yes (local)\" : \"No (another process)\",\n key: \"isLocal\",\n },\n runner.file !== undefined && {\n label: \"File\",\n value: <code>{runner.file}</code>,\n },\n {\n label: \"Schedule\",\n value: (\n <span data-testid=\"runner-schedule\">\n {describeSchedule(runner.schedule)}\n </span>\n ),\n },\n {\n label: \"Next run\",\n value:\n runner.nextRunAt === null ? (\n \"Not scheduled\"\n ) : (\n <RelativeTime value={runner.nextRunAt} />\n ),\n hint:\n runner.local &&\n runner.local.nextRunAt !== null &&\n runner.local.nextRunAt !== runner.nextRunAt ? (\n <>\n This process's ticker:{\" \"}\n <RelativeTime value={runner.local.nextRunAt} />\n </>\n ) : undefined,\n },\n {\n label: \"Execution mode\",\n value: runner.executionMode,\n hint: describeOverride(runner.config, \"executionMode\"),\n },\n {\n label: \"Run mode\",\n value: runner.runMode,\n hint: describeOverride(runner.config, \"runMode\"),\n },\n { label: \"Queues triggers\", value: describeQueueing(runner) },\n {\n label: \"Max concurrency\",\n value: (\n <span data-testid=\"runner-concurrency\">\n {describeConcurrency(runner.maxConcurrency)}\n </span>\n ),\n hint: describeOverride(runner.config, \"maxConcurrency\"),\n },\n runner.config !== undefined && {\n label: \"Settings override\",\n key: \"configOverride\",\n value: (\n <span data-testid=\"runner-config-override\">\n {describeOverridden(runner.config)}\n </span>\n ),\n hint: describeAdoption(runner.config),\n },\n {\n label: \"Queued triggers\",\n value: formatNumber(runner.queuedTriggers),\n },\n {\n label: \"Running on\",\n value: runner.runningOn ? (\n <>\n <code>{runner.runningOn.runId}</code>\n {runner.runningOn.host !== undefined &&\n ` on ${runner.runningOn.host}`}\n {runner.runningOn.pid !== undefined &&\n ` (pid ${runner.runningOn.pid})`}\n {\", since \"}\n <RelativeTime value={runner.runningOn.since} />\n </>\n ) : null,\n },\n { label: \"Updated\", value: <RelativeTime value={runner.updatedAt} /> },\n {\n label: \"Last error\",\n value: runner.lastError ? (\n <span className=\"runner-last-error\">\n <strong>{runner.lastError.name}</strong>:{\" \"}\n {runner.lastError.message}\n </span>\n ) : null,\n },\n ]}\n />\n );\n}\n\n/** Props of {@link RunnerStatsTiles}. */\nexport interface RunnerStatsTilesProps {\n /** The counters. */\n stats: RunnerStatsDto;\n}\n\n/** The lifetime counters as tiles. */\nexport function RunnerStatsTiles({ stats }: RunnerStatsTilesProps) {\n return (\n <dl\n className=\"stat-grid\"\n aria-label=\"Runs by outcome\"\n data-testid=\"runner-stats\"\n >\n {STAT_TILES.map(({ key, label, state }) => (\n <div\n key={key}\n className={state ? `stat stat-state state-${state}` : \"stat\"}\n >\n <dt className=\"stat-label\">{label}</dt>\n <dd className=\"stat-value\">{formatNumber(stats[key])}</dd>\n </div>\n ))}\n </dl>\n );\n}\n\n/** The loaded runner: header, summary, stats, runs and history. */\nfunction RunnerDetail({\n runner,\n stats,\n historyEnabled,\n}: {\n /** The runner. */\n runner: RunnerInfoDto;\n /** Its counters: `GET /stats`, else the detail's own until that loads. */\n stats: RunnerStatsDto;\n /** Whether the history may be read (the scoped `runners.read`, settled). */\n historyEnabled: boolean;\n}) {\n const activeRuns = runner.local?.activeRuns ?? [];\n return (\n <>\n <header className=\"runner-header\">\n <div className=\"runner-heading\">\n <h1 className=\"screen-title runner-title\">\n {/* Defence in depth: `getRunner`'s shape guard already refuses a\n runner whose name is not a string, so the \"(invalid)\" fallback\n is unreachable here. In practice only the unguarded list cells\n show it. */}\n {displayText(runner.name)}\n </h1>\n <span\n className=\"runner-badges\"\n data-testid=\"runner-status\"\n >\n {runnerBadges(runner).map((badge) => (\n <Badge\n key={badge.label}\n tone={badge.tone}\n title={badge.hint}\n >\n {badge.label}\n </Badge>\n ))}\n {!runner.isLocal && <Badge tone=\"neutral\">Other process</Badge>}\n </span>\n {runner.name !== runner.id && (\n <code\n className=\"runner-id muted\"\n data-testid=\"runner-id\"\n >\n {runner.id}\n </code>\n )}\n </div>\n <RunnerActions runner={runner} />\n </header>\n\n {!runner.isLocal && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"non-local-note\"\n >\n {NON_LOCAL_RUNNER_NOTE}\n </p>\n )}\n\n <Card title=\"Summary\">\n <RunnerSummary runner={runner} />\n </Card>\n\n <Card title=\"Stats\">\n <RunnerStatsTiles stats={stats} />\n </Card>\n\n {runner.local && (\n <Card title=\"Active runs\">\n {activeRuns.length === 0 ? (\n <p\n className=\"muted\"\n data-testid=\"no-active-runs\"\n >\n Nothing is running in this process.\n </p>\n ) : (\n <ul\n className=\"run-list\"\n aria-label=\"Active runs\"\n >\n {activeRuns.map((run) => (\n <li key={run.runId}>\n <RunRecordDetails run={run} />\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n <Card title=\"Last run\">\n {runner.lastRun ? (\n <RunRecordDetails run={runner.lastRun} />\n ) : (\n <p className=\"muted\">This runner has not run yet.</p>\n )}\n </Card>\n\n <RunnerHistory\n runner={runner.id}\n enabled={historyEnabled}\n info={runner}\n />\n </>\n );\n}\n\n/**\n * `/runners/:runner`: one runner, polled faster while it runs. Needs\n * `runners.read` for the runner (scoped by the route's\n * `<RunnerPermissionScope>`); without it, or on a 401/403, the runner is\n * hidden and never fetched again.\n */\nexport function RunnerScreen() {\n const { runner: id = \"\" } = useParams<{ runner: string }>();\n const api = useApiClient();\n // Scoped to the runner by the route's <RunnerPermissionScope>. Every read\n // waits for that map (`settled`), so an untargeted grant cannot send a read\n // the runner's own answer refuses; a later `false` also stops the polling.\n const canRead = useCan(\"runners.read\");\n const settled = usePermissionsSettled();\n const allowed = canRead && settled;\n const refetchInterval = useRunnerRefetchInterval();\n\n const detail = useQuery({\n queryKey: runnerKeys.detail(id),\n queryFn: ({ signal }) => getRunner(api, id, signal),\n enabled: allowed,\n refetchInterval: (query) =>\n isRunnerNotFound(query.state.error) || isRunnerDenied(query.state.error)\n ? false\n : refetchInterval(query.state.data),\n });\n const stats = useQuery({\n queryKey: runnerKeys.stats(id),\n queryFn: ({ signal }) => getRunnerStats(api, id, signal),\n enabled: allowed && detail.isSuccess,\n refetchInterval: (query) =>\n isRunnerNotFound(query.state.error) || isRunnerDenied(query.state.error)\n ? false\n : refetchInterval(detail.data),\n });\n // Subscribes once the runner's map allows the read, and stops when the\n // runner turns out to be refused or gone.\n useRunnerLive(\n id,\n allowed && !isRunnerDenied(detail.error) && !isRunnerNotFound(detail.error),\n );\n\n if (settled && !canRead) {\n return <RunnerHidden />;\n }\n if (isRunnerDenied(detail.error)) {\n return <RunnerHidden detail={detail.error.detail} />;\n }\n if (isRunnerNotFound(detail.error)) {\n return <RunnerNotFound id={id} />;\n }\n return (\n <div\n className=\"screen runner-screen\"\n data-testid=\"runner-screen\"\n >\n <nav\n className=\"breadcrumb\"\n aria-label=\"Breadcrumb\"\n >\n <ol>\n <li>\n <Link to=\"/runners\">Runners</Link>\n </li>\n <li aria-current=\"page\">{id}</li>\n </ol>\n </nav>\n {detail.data ? (\n <>\n {detail.isError && (\n <ProblemBanner\n error={detail.error}\n title=\"Could not refresh this runner\"\n onRetry={() => void detail.refetch()}\n />\n )}\n <RunnerDetail\n runner={detail.data}\n stats={stats.data ?? detail.data.stats}\n historyEnabled={allowed}\n />\n </>\n ) : detail.isError ? (\n <ErrorView\n headingLevel={1}\n error={detail.error}\n title=\"Could not load the runner\"\n onRetry={() => void detail.refetch()}\n />\n ) : (\n <Spinner\n label=\"Loading the runner\"\n showLabel\n />\n )}\n </div>\n );\n}\n",
27
+ "import type { RunnerListItemDto } from \"../../api/types\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useDeferredValue, useId } from \"react\";\nimport { listRunners, runnerKeys, runnerPath } from \"../../api/runners\";\nimport { Badge } from \"../../components/Badge\";\nimport { Card } from \"../../components/Card\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { ErrorView } from \"../../components/ErrorView\";\nimport { Pager } from \"../../components/Pager\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { Table } from \"../../components/Table\";\nimport { clientWindow } from \"../../components/useClientPage\";\nimport { useApiClient } from \"../../context\";\nimport { plural } from \"../../format\";\nimport { SEARCH_SHORTCUT } from \"../../layout/shortcuts\";\nimport { useCan } from \"../../meta/hooks\";\nimport { Link } from \"../../router\";\nimport { intParam, useUrlParams } from \"../queues/urlState\";\nimport { useListRefetchInterval, useRunnerListLive } from \"./live\";\nimport { filterRunners, orderRunners, RUNNER_STATUS } from \"./runnerFormat\";\nimport \"./runners.css\";\n\n/**\n * Runners per page.\n *\n * Twenty-five: a runner row is three short cells, so a page holds more than a\n * queue's, and a namespace registering one runner per report or sweep reaches\n * a few dozen. The window lives in the URL, like `/queues`, since the whole\n * screen is this one list and a link should reproduce the page being read.\n */\nconst RUNNER_PAGE_SIZE = 25;\n\n/**\n * One row: a local runner with its name and lifecycle status, or the id of one\n * registered in another process, with its shared paused flag; either one's\n * run in flight is marked.\n */\nfunction RunnerRow({ item }: { item: RunnerListItemDto }) {\n const status = item.status ? RUNNER_STATUS[item.status] : null;\n return (\n <tr data-testid={`runner-row-${item.id}`}>\n <td>\n <Link\n to={runnerPath(item.id)}\n className=\"runner-link\"\n >\n {item.isLocal && item.name && item.name !== item.id\n ? item.name\n : item.id}\n </Link>\n {item.isLocal && item.name && item.name !== item.id && (\n <code className=\"runner-id muted\">{item.id}</code>\n )}\n </td>\n <td className=\"runners-where\">\n {item.isLocal ? (\n <Badge tone=\"accent\">Local</Badge>\n ) : (\n <Badge\n tone=\"neutral\"\n title=\"Registered by another process: its name shows on its own screen.\"\n >\n Other process\n </Badge>\n )}\n </td>\n <td className=\"runners-status\">\n {status ? (\n <Badge\n tone={status.tone}\n title={status.hint}\n >\n {status.label}\n </Badge>\n ) : item.isPaused ? (\n <Badge\n tone=\"warning\"\n title=\"Paused through the shared state: its owner skips scheduled runs.\"\n >\n Paused\n </Badge>\n ) : (\n <Badge\n tone=\"neutral\"\n title=\"Not paused. Its lifecycle status shows only in its own process.\"\n >\n Active\n </Badge>\n )}{\" \"}\n {item.isRunning && (\n <Badge\n tone=\"accent\"\n title=\"A run holds this runner's lock right now, in whichever process.\"\n >\n Run in flight\n </Badge>\n )}\n </td>\n </tr>\n );\n}\n\n/** `/runners`: every runner in the namespace, local first; filtered in the browser. */\nexport function RunnersListScreen() {\n const api = useApiClient();\n const canList = useCan(\"runners.list\");\n const [params, update] = useUrlParams();\n const filterId = useId();\n const filter = params.get(\"search\") ?? \"\";\n const deferredFilter = useDeferredValue(filter);\n\n const refetchInterval = useListRefetchInterval();\n useRunnerListLive(canList);\n const runners = useQuery({\n queryKey: runnerKeys.list(),\n queryFn: ({ signal }) => listRunners(api, signal),\n refetchInterval,\n enabled: canList,\n });\n\n if (!canList) {\n return (\n <div className=\"screen\">\n <h1 className=\"screen-title\">Runners</h1>\n <EmptyState\n title=\"Nothing to show\"\n description=\"You may not list runners on this API.\"\n />\n </div>\n );\n }\n\n const all = runners.data ? orderRunners(runners.data.items) : [];\n const shown = filterRunners(all, deferredFilter);\n const localCount = all.filter((item) => item.isLocal).length;\n // The list is read whole and filtered in the browser, so the page is cut\n // here too; the window is in the URL, as on `/queues`.\n const page = clientWindow(shown.length, {\n offset: intParam(params, \"offset\", 0),\n limit: intParam(params, \"limit\", RUNNER_PAGE_SIZE),\n });\n const rows = shown.slice(page.offset, page.offset + page.limit);\n\n return (\n <div\n className=\"screen\"\n data-testid=\"runners-list\"\n >\n <h1 className=\"screen-title\">Runners</h1>\n <Card\n title=\"Runners\"\n actions={\n <div className=\"search\">\n <label\n htmlFor={filterId}\n className=\"visually-hidden\"\n >\n Filter runners by id or name\n </label>\n <input\n id={filterId}\n type=\"search\"\n className=\"input\"\n placeholder=\"Filter runners\"\n {...SEARCH_SHORTCUT}\n value={filter}\n maxLength={200}\n onChange={(event) =>\n update({ search: event.target.value, offset: null })\n }\n />\n </div>\n }\n >\n {runners.isPending ? (\n <Spinner\n label=\"Loading runners\"\n showLabel\n />\n ) : runners.isError ? (\n <ErrorView\n error={runners.error}\n title=\"Could not load runners\"\n onRetry={() => void runners.refetch()}\n />\n ) : all.length === 0 ? (\n <EmptyState\n title=\"No runners yet\"\n description=\"Runners appear here once a process registers one in this namespace.\"\n />\n ) : shown.length === 0 ? (\n <EmptyState\n title=\"No runners match\"\n description={`No runner id or name contains “${deferredFilter.trim()}”.`}\n />\n ) : (\n <>\n <Table label=\"Runners\">\n <thead>\n <tr>\n <th scope=\"col\">Runner</th>\n <th\n scope=\"col\"\n className=\"runners-where\"\n >\n Where\n </th>\n <th\n scope=\"col\"\n className=\"runners-status\"\n >\n Status\n </th>\n </tr>\n </thead>\n <tbody>\n {rows.map((item) => (\n <RunnerRow\n key={item.id}\n item={item}\n />\n ))}\n </tbody>\n </Table>\n {page.paged && (\n <Pager\n label=\"Runner pages\"\n offset={page.offset}\n limit={page.limit}\n total={shown.length}\n itemCount={rows.length}\n onChange={(next) =>\n update({\n offset: next.offset > 0 ? String(next.offset) : null,\n limit:\n next.limit === RUNNER_PAGE_SIZE\n ? null\n : String(next.limit),\n })\n }\n />\n )}\n <p\n className=\"muted runners-count\"\n data-testid=\"runners-count\"\n >\n {plural(localCount, \"local runner\")},{\" \"}\n {plural(all.length - localCount, \"runner\")} in other processes\n {shown.length < all.length && ` (${shown.length} shown)`}\n </p>\n </>\n )}\n </Card>\n </div>\n );\n}\n"
28
+ ],
29
+ "mappings": "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",
30
+ "debugId": "A4F2192C724764E064756E2164756E21",
31
+ "names": []
32
+ }
@@ -0,0 +1 @@
1
+ .breadcrumb{font-size:var(--text-sm);color:var(--text-muted)}.queue-header{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap}.queue-heading{display:flex;align-items:baseline;gap:var(--space-2)var(--space-3);flex-wrap:wrap;min-width:0}.queue-heading .screen-title{overflow-wrap:anywhere}.queue-total{font-variant-numeric:tabular-nums;color:var(--text-muted)}.queue-updated{font-size:var(--text-sm)}.queue-toolbar,.queue-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.jobs-section{display:flex;gap:var(--space-3);flex-direction:column;min-width:0}.jobs-filters{display:flex;align-items:flex-end;gap:var(--space-2)var(--space-3);flex-wrap:wrap}.jobs-filter{display:flex;gap:var(--space-1);font-size:var(--text-sm);flex-direction:column;min-width:0}.jobs-total{align-self:center}.bulk-bar{display:flex;align-items:center;gap:var(--space-2)var(--space-3);padding:var(--space-2)var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-2);flex-wrap:wrap}.bulk-count{font-size:var(--text-sm);font-variant-numeric:tabular-nums}.select-cell{text-align:center;width:2rem}.job-id{display:flex;align-items:center;gap:var(--space-2);font-family:var(--font-mono);max-width:22rem;font-weight:400}.job-id a{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.job-processed-by-col{white-space:nowrap;width:12rem}.job-processed-by-text{display:inline-block;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap;max-width:12rem}.job-failure{color:var(--text-muted);overflow-wrap:anywhere;max-width:24rem}tr[aria-selected=true]{background:var(--info-bg)}.queue-panels{min-width:0}.field-row{display:flex;gap:var(--space-3);flex-wrap:wrap}.field-row>.field{flex:10rem;min-width:0}.limits-editor{display:flex;gap:var(--space-3);flex-direction:column}.limits-names{display:flex;gap:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-3);flex-direction:column}.limits-name{border-top:1px solid var(--border);padding-top:var(--space-3)}.form-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.limits-summary{display:grid;grid-template-columns:max-content 1fr;gap:var(--space-1)var(--space-3);margin:0}.limits-summary>div{display:contents}.limits-summary dd{margin:0}.throughput-controls{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)}.throughput-chart{margin:0}.throughput-legend{display:flex;gap:var(--space-3);font-size:var(--text-sm);margin-bottom:var(--space-2);flex-wrap:wrap}.swatch{display:inline-block;vertical-align:middle;border-radius:2px;width:10px;height:10px}.swatch-completed,.bar-completed{background:var(--state-completed);fill:var(--state-completed)}.swatch-failed,.bar-failed{background:var(--state-failed);fill:var(--state-failed)}.throughput-svg{display:block;width:100%;height:160px}.throughput-baseline{stroke:var(--border);stroke-width:1;vector-effect:non-scaling-stroke}.throughput-hit{fill:transparent}.throughput-hit:hover{fill:var(--surface-2)}.throughput-axis{display:flex;font-size:var(--text-xs);color:var(--text-muted);justify-content:space-between}.repeatable-data{max-width:24rem}.repeatable-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.demand-panel{display:grid;gap:var(--space-3)}.demand-urls{display:grid;gap:var(--space-2)}.demand-urls p{margin:0}.demand-url{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0}.demand-url-label{min-width:6rem;font-weight:600}.demand-url code{overflow-wrap:anywhere}.summon-panel{display:grid;gap:var(--space-3)}.summon-heading{margin:0 0 var(--space-2);font-size:1rem}.summon-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.summoning-unit{display:block;font-size:var(--text-sm);font-weight:400}.summon-budget{display:inline-grid;gap:var(--space-1)}.summon-budget-window{display:flex;align-items:center;gap:var(--space-2);font-variant-numeric:tabular-nums;flex-wrap:wrap}.summon-budget-window.is-exhausted{color:var(--warning-text);font-weight:600}.summon-budget-meter{flex:none;width:80px;height:8px}.summon-group{display:grid;gap:var(--space-2);padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}.summon-subheading{font-size:var(--text-sm);margin:0;font-weight:600}.summon-group-circuit.is-open{color:var(--warning-text)}.summon-group tr.is-current,.summon-groups tr.is-current{background:var(--surface-2);box-shadow:inset 3px 0 0 var(--accent)}.summon-groups{display:grid;gap:var(--space-2);margin-top:var(--space-5)}.summon-groups-title{font-size:var(--text-lg);margin:0}.summon-group-list{list-style:none;margin:0;padding:0}.summon-group-count{font-variant-numeric:tabular-nums;color:var(--text-muted)}.summon-coverage{display:flex;gap:var(--space-1)var(--space-2);list-style:none;flex-wrap:wrap;margin:0;padding:0}.summon-last-missing{display:block;color:var(--danger-text)}.summon-group-note{display:block;font-size:var(--text-sm);margin:0}.summon-policy-tag{font-size:var(--text-xs);font-style:italic}.summon-policy-names{display:inline-flex;gap:var(--space-1);flex-wrap:wrap}.summon-policy-names code{padding:0 var(--space-1);border:1px solid var(--border);border-radius:var(--radius-sm);overflow-wrap:anywhere}