adispatchd 0.0.0-stage → 0.1.0

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 (396) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/api/errors.d.ts +27 -0
  4. package/dist/api/errors.js +80 -0
  5. package/dist/api/errors.js.map +1 -0
  6. package/dist/api/index.d.ts +10 -0
  7. package/dist/api/index.js +11 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/logs.d.ts +30 -0
  10. package/dist/api/logs.js +88 -0
  11. package/dist/api/logs.js.map +1 -0
  12. package/dist/api/serialize.d.ts +65 -0
  13. package/dist/api/serialize.js +135 -0
  14. package/dist/api/serialize.js.map +1 -0
  15. package/dist/api/server.d.ts +89 -0
  16. package/dist/api/server.js +751 -0
  17. package/dist/api/server.js.map +1 -0
  18. package/dist/api/sse.d.ts +90 -0
  19. package/dist/api/sse.js +225 -0
  20. package/dist/api/sse.js.map +1 -0
  21. package/dist/app.d.ts +173 -0
  22. package/dist/app.js +488 -0
  23. package/dist/app.js.map +1 -0
  24. package/dist/cli/config-validate.d.ts +6 -0
  25. package/dist/cli/config-validate.js +39 -0
  26. package/dist/cli/config-validate.js.map +1 -0
  27. package/dist/cli/control.d.ts +31 -0
  28. package/dist/cli/control.js +186 -0
  29. package/dist/cli/control.js.map +1 -0
  30. package/dist/cli/http-client.d.ts +57 -0
  31. package/dist/cli/http-client.js +174 -0
  32. package/dist/cli/http-client.js.map +1 -0
  33. package/dist/cli/index.d.ts +14 -0
  34. package/dist/cli/index.js +14 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/cli/init-templates.d.ts +20 -0
  37. package/dist/cli/init-templates.js +275 -0
  38. package/dist/cli/init-templates.js.map +1 -0
  39. package/dist/cli/init.d.ts +16 -0
  40. package/dist/cli/init.js +107 -0
  41. package/dist/cli/init.js.map +1 -0
  42. package/dist/cli/main.d.ts +2 -0
  43. package/dist/cli/main.js +25 -0
  44. package/dist/cli/main.js.map +1 -0
  45. package/dist/cli/program.d.ts +102 -0
  46. package/dist/cli/program.js +133 -0
  47. package/dist/cli/program.js.map +1 -0
  48. package/dist/cli/service.d.ts +27 -0
  49. package/dist/cli/service.js +252 -0
  50. package/dist/cli/service.js.map +1 -0
  51. package/dist/cli/start.d.ts +22 -0
  52. package/dist/cli/start.js +133 -0
  53. package/dist/cli/start.js.map +1 -0
  54. package/dist/cli/workflow-dry-run.d.ts +13 -0
  55. package/dist/cli/workflow-dry-run.js +144 -0
  56. package/dist/cli/workflow-dry-run.js.map +1 -0
  57. package/dist/config/field-catalog.d.ts +33 -0
  58. package/dist/config/field-catalog.js +172 -0
  59. package/dist/config/field-catalog.js.map +1 -0
  60. package/dist/config/filter-schema.d.ts +15 -0
  61. package/dist/config/filter-schema.js +110 -0
  62. package/dist/config/filter-schema.js.map +1 -0
  63. package/dist/config/index.d.ts +15 -0
  64. package/dist/config/index.js +16 -0
  65. package/dist/config/index.js.map +1 -0
  66. package/dist/config/issues.d.ts +27 -0
  67. package/dist/config/issues.js +37 -0
  68. package/dist/config/issues.js.map +1 -0
  69. package/dist/config/loader.d.ts +32 -0
  70. package/dist/config/loader.js +250 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/manager.d.ts +153 -0
  73. package/dist/config/manager.js +276 -0
  74. package/dist/config/manager.js.map +1 -0
  75. package/dist/config/model.d.ts +334 -0
  76. package/dist/config/model.js +2 -0
  77. package/dist/config/model.js.map +1 -0
  78. package/dist/config/normalize.d.ts +20 -0
  79. package/dist/config/normalize.js +200 -0
  80. package/dist/config/normalize.js.map +1 -0
  81. package/dist/config/parameters.d.ts +37 -0
  82. package/dist/config/parameters.js +71 -0
  83. package/dist/config/parameters.js.map +1 -0
  84. package/dist/config/public.d.ts +6 -0
  85. package/dist/config/public.js +18 -0
  86. package/dist/config/public.js.map +1 -0
  87. package/dist/config/schemas.d.ts +538 -0
  88. package/dist/config/schemas.js +321 -0
  89. package/dist/config/schemas.js.map +1 -0
  90. package/dist/config/snapshot.d.ts +38 -0
  91. package/dist/config/snapshot.js +57 -0
  92. package/dist/config/snapshot.js.map +1 -0
  93. package/dist/config/validate.d.ts +33 -0
  94. package/dist/config/validate.js +346 -0
  95. package/dist/config/validate.js.map +1 -0
  96. package/dist/core/clock.d.ts +59 -0
  97. package/dist/core/clock.js +133 -0
  98. package/dist/core/clock.js.map +1 -0
  99. package/dist/core/duration.d.ts +19 -0
  100. package/dist/core/duration.js +93 -0
  101. package/dist/core/duration.js.map +1 -0
  102. package/dist/core/errors.d.ts +62 -0
  103. package/dist/core/errors.js +72 -0
  104. package/dist/core/errors.js.map +1 -0
  105. package/dist/core/ids.d.ts +57 -0
  106. package/dist/core/ids.js +88 -0
  107. package/dist/core/ids.js.map +1 -0
  108. package/dist/core/index.d.ts +13 -0
  109. package/dist/core/index.js +14 -0
  110. package/dist/core/index.js.map +1 -0
  111. package/dist/core/logger.d.ts +100 -0
  112. package/dist/core/logger.js +207 -0
  113. package/dist/core/logger.js.map +1 -0
  114. package/dist/core/paths.d.ts +84 -0
  115. package/dist/core/paths.js +138 -0
  116. package/dist/core/paths.js.map +1 -0
  117. package/dist/core/template.d.ts +136 -0
  118. package/dist/core/template.js +261 -0
  119. package/dist/core/template.js.map +1 -0
  120. package/dist/core/types.d.ts +544 -0
  121. package/dist/core/types.js +172 -0
  122. package/dist/core/types.js.map +1 -0
  123. package/dist/core/worker-protocol.d.ts +102 -0
  124. package/dist/core/worker-protocol.js +75 -0
  125. package/dist/core/worker-protocol.js.map +1 -0
  126. package/dist/executors/classify.d.ts +25 -0
  127. package/dist/executors/classify.js +54 -0
  128. package/dist/executors/classify.js.map +1 -0
  129. package/dist/executors/dispatcher.d.ts +35 -0
  130. package/dist/executors/dispatcher.js +68 -0
  131. package/dist/executors/dispatcher.js.map +1 -0
  132. package/dist/executors/env.d.ts +43 -0
  133. package/dist/executors/env.js +66 -0
  134. package/dist/executors/env.js.map +1 -0
  135. package/dist/executors/event-parser.d.ts +41 -0
  136. package/dist/executors/event-parser.js +106 -0
  137. package/dist/executors/event-parser.js.map +1 -0
  138. package/dist/executors/index.d.ts +12 -0
  139. package/dist/executors/index.js +13 -0
  140. package/dist/executors/index.js.map +1 -0
  141. package/dist/executors/log-capture.d.ts +57 -0
  142. package/dist/executors/log-capture.js +155 -0
  143. package/dist/executors/log-capture.js.map +1 -0
  144. package/dist/executors/process-identity.d.ts +124 -0
  145. package/dist/executors/process-identity.js +283 -0
  146. package/dist/executors/process-identity.js.map +1 -0
  147. package/dist/executors/shell-executor.d.ts +35 -0
  148. package/dist/executors/shell-executor.js +478 -0
  149. package/dist/executors/shell-executor.js.map +1 -0
  150. package/dist/executors/stream-redactor.d.ts +40 -0
  151. package/dist/executors/stream-redactor.js +89 -0
  152. package/dist/executors/stream-redactor.js.map +1 -0
  153. package/dist/executors/workspace.d.ts +10 -0
  154. package/dist/executors/workspace.js +47 -0
  155. package/dist/executors/workspace.js.map +1 -0
  156. package/dist/filters/evaluate.d.ts +77 -0
  157. package/dist/filters/evaluate.js +187 -0
  158. package/dist/filters/evaluate.js.map +1 -0
  159. package/dist/filters/fields.d.ts +35 -0
  160. package/dist/filters/fields.js +75 -0
  161. package/dist/filters/fields.js.map +1 -0
  162. package/dist/filters/index.d.ts +11 -0
  163. package/dist/filters/index.js +12 -0
  164. package/dist/filters/index.js.map +1 -0
  165. package/dist/filters/iso-date.d.ts +10 -0
  166. package/dist/filters/iso-date.js +42 -0
  167. package/dist/filters/iso-date.js.map +1 -0
  168. package/dist/filters/predicate.d.ts +24 -0
  169. package/dist/filters/predicate.js +178 -0
  170. package/dist/filters/predicate.js.map +1 -0
  171. package/dist/filters/priority.d.ts +51 -0
  172. package/dist/filters/priority.js +91 -0
  173. package/dist/filters/priority.js.map +1 -0
  174. package/dist/filters/pushdown.d.ts +12 -0
  175. package/dist/filters/pushdown.js +52 -0
  176. package/dist/filters/pushdown.js.map +1 -0
  177. package/dist/filters/shell-check.d.ts +110 -0
  178. package/dist/filters/shell-check.js +304 -0
  179. package/dist/filters/shell-check.js.map +1 -0
  180. package/dist/identities/github-app.d.ts +55 -0
  181. package/dist/identities/github-app.js +160 -0
  182. package/dist/identities/github-app.js.map +1 -0
  183. package/dist/identities/index.d.ts +6 -0
  184. package/dist/identities/index.js +7 -0
  185. package/dist/identities/index.js.map +1 -0
  186. package/dist/identities/jwt.d.ts +24 -0
  187. package/dist/identities/jwt.js +56 -0
  188. package/dist/identities/jwt.js.map +1 -0
  189. package/dist/lifecycle/context.d.ts +106 -0
  190. package/dist/lifecycle/context.js +170 -0
  191. package/dist/lifecycle/context.js.map +1 -0
  192. package/dist/lifecycle/engine.d.ts +185 -0
  193. package/dist/lifecycle/engine.js +551 -0
  194. package/dist/lifecycle/engine.js.map +1 -0
  195. package/dist/lifecycle/errors.d.ts +48 -0
  196. package/dist/lifecycle/errors.js +71 -0
  197. package/dist/lifecycle/errors.js.map +1 -0
  198. package/dist/lifecycle/index.d.ts +9 -0
  199. package/dist/lifecycle/index.js +9 -0
  200. package/dist/lifecycle/index.js.map +1 -0
  201. package/dist/lifecycle/performers.d.ts +66 -0
  202. package/dist/lifecycle/performers.js +179 -0
  203. package/dist/lifecycle/performers.js.map +1 -0
  204. package/dist/notifications/index.d.ts +6 -0
  205. package/dist/notifications/index.js +7 -0
  206. package/dist/notifications/index.js.map +1 -0
  207. package/dist/notifications/service.d.ts +115 -0
  208. package/dist/notifications/service.js +364 -0
  209. package/dist/notifications/service.js.map +1 -0
  210. package/dist/persistence/actions.d.ts +101 -0
  211. package/dist/persistence/actions.js +196 -0
  212. package/dist/persistence/actions.js.map +1 -0
  213. package/dist/persistence/blocks.d.ts +85 -0
  214. package/dist/persistence/blocks.js +140 -0
  215. package/dist/persistence/blocks.js.map +1 -0
  216. package/dist/persistence/cron-firings.d.ts +55 -0
  217. package/dist/persistence/cron-firings.js +70 -0
  218. package/dist/persistence/cron-firings.js.map +1 -0
  219. package/dist/persistence/database.d.ts +60 -0
  220. package/dist/persistence/database.js +124 -0
  221. package/dist/persistence/database.js.map +1 -0
  222. package/dist/persistence/errors.d.ts +39 -0
  223. package/dist/persistence/errors.js +67 -0
  224. package/dist/persistence/errors.js.map +1 -0
  225. package/dist/persistence/events.d.ts +72 -0
  226. package/dist/persistence/events.js +83 -0
  227. package/dist/persistence/events.js.map +1 -0
  228. package/dist/persistence/index.d.ts +18 -0
  229. package/dist/persistence/index.js +19 -0
  230. package/dist/persistence/index.js.map +1 -0
  231. package/dist/persistence/instance-lock.d.ts +26 -0
  232. package/dist/persistence/instance-lock.js +117 -0
  233. package/dist/persistence/instance-lock.js.map +1 -0
  234. package/dist/persistence/lock-keys.d.ts +16 -0
  235. package/dist/persistence/lock-keys.js +41 -0
  236. package/dist/persistence/lock-keys.js.map +1 -0
  237. package/dist/persistence/notifications.d.ts +113 -0
  238. package/dist/persistence/notifications.js +213 -0
  239. package/dist/persistence/notifications.js.map +1 -0
  240. package/dist/persistence/persistence.d.ts +46 -0
  241. package/dist/persistence/persistence.js +42 -0
  242. package/dist/persistence/persistence.js.map +1 -0
  243. package/dist/persistence/retention.d.ts +46 -0
  244. package/dist/persistence/retention.js +68 -0
  245. package/dist/persistence/retention.js.map +1 -0
  246. package/dist/persistence/runs.d.ts +215 -0
  247. package/dist/persistence/runs.js +417 -0
  248. package/dist/persistence/runs.js.map +1 -0
  249. package/dist/persistence/schema.d.ts +16 -0
  250. package/dist/persistence/schema.js +183 -0
  251. package/dist/persistence/schema.js.map +1 -0
  252. package/dist/persistence/sqlite.d.ts +49 -0
  253. package/dist/persistence/sqlite.js +131 -0
  254. package/dist/persistence/sqlite.js.map +1 -0
  255. package/dist/runs/controller.d.ts +307 -0
  256. package/dist/runs/controller.js +1078 -0
  257. package/dist/runs/controller.js.map +1 -0
  258. package/dist/runs/errors.d.ts +20 -0
  259. package/dist/runs/errors.js +26 -0
  260. package/dist/runs/errors.js.map +1 -0
  261. package/dist/runs/event-bus.d.ts +106 -0
  262. package/dist/runs/event-bus.js +105 -0
  263. package/dist/runs/event-bus.js.map +1 -0
  264. package/dist/runs/execution.d.ts +39 -0
  265. package/dist/runs/execution.js +146 -0
  266. package/dist/runs/execution.js.map +1 -0
  267. package/dist/runs/index.d.ts +12 -0
  268. package/dist/runs/index.js +13 -0
  269. package/dist/runs/index.js.map +1 -0
  270. package/dist/runs/retention.d.ts +64 -0
  271. package/dist/runs/retention.js +232 -0
  272. package/dist/runs/retention.js.map +1 -0
  273. package/dist/runs/workspace.d.ts +20 -0
  274. package/dist/runs/workspace.js +51 -0
  275. package/dist/runs/workspace.js.map +1 -0
  276. package/dist/scheduler/checks.d.ts +41 -0
  277. package/dist/scheduler/checks.js +111 -0
  278. package/dist/scheduler/checks.js.map +1 -0
  279. package/dist/scheduler/cron.d.ts +32 -0
  280. package/dist/scheduler/cron.js +83 -0
  281. package/dist/scheduler/cron.js.map +1 -0
  282. package/dist/scheduler/dry-run.d.ts +42 -0
  283. package/dist/scheduler/dry-run.js +167 -0
  284. package/dist/scheduler/dry-run.js.map +1 -0
  285. package/dist/scheduler/errors.d.ts +22 -0
  286. package/dist/scheduler/errors.js +22 -0
  287. package/dist/scheduler/errors.js.map +1 -0
  288. package/dist/scheduler/format.d.ts +3 -0
  289. package/dist/scheduler/format.js +73 -0
  290. package/dist/scheduler/format.js.map +1 -0
  291. package/dist/scheduler/index.d.ts +13 -0
  292. package/dist/scheduler/index.js +14 -0
  293. package/dist/scheduler/index.js.map +1 -0
  294. package/dist/scheduler/planner.d.ts +88 -0
  295. package/dist/scheduler/planner.js +255 -0
  296. package/dist/scheduler/planner.js.map +1 -0
  297. package/dist/scheduler/scheduler.d.ts +160 -0
  298. package/dist/scheduler/scheduler.js +680 -0
  299. package/dist/scheduler/scheduler.js.map +1 -0
  300. package/dist/scheduler/state.d.ts +26 -0
  301. package/dist/scheduler/state.js +25 -0
  302. package/dist/scheduler/state.js.map +1 -0
  303. package/dist/scheduler/types.d.ts +197 -0
  304. package/dist/scheduler/types.js +3 -0
  305. package/dist/scheduler/types.js.map +1 -0
  306. package/dist/service/index.d.ts +6 -0
  307. package/dist/service/index.js +7 -0
  308. package/dist/service/index.js.map +1 -0
  309. package/dist/service/install.d.ts +93 -0
  310. package/dist/service/install.js +250 -0
  311. package/dist/service/install.js.map +1 -0
  312. package/dist/service/unit.d.ts +50 -0
  313. package/dist/service/unit.js +171 -0
  314. package/dist/service/unit.js.map +1 -0
  315. package/dist/sources/github/adapter.d.ts +99 -0
  316. package/dist/sources/github/adapter.js +336 -0
  317. package/dist/sources/github/adapter.js.map +1 -0
  318. package/dist/sources/github/fields.d.ts +8 -0
  319. package/dist/sources/github/fields.js +72 -0
  320. package/dist/sources/github/fields.js.map +1 -0
  321. package/dist/sources/github/http.d.ts +100 -0
  322. package/dist/sources/github/http.js +288 -0
  323. package/dist/sources/github/http.js.map +1 -0
  324. package/dist/sources/github/index.d.ts +6 -0
  325. package/dist/sources/github/index.js +7 -0
  326. package/dist/sources/github/index.js.map +1 -0
  327. package/dist/sources/github/pushdown.d.ts +14 -0
  328. package/dist/sources/github/pushdown.js +47 -0
  329. package/dist/sources/github/pushdown.js.map +1 -0
  330. package/dist/sources/github/refs.d.ts +17 -0
  331. package/dist/sources/github/refs.js +61 -0
  332. package/dist/sources/github/refs.js.map +1 -0
  333. package/dist/sources/index.d.ts +6 -0
  334. package/dist/sources/index.js +7 -0
  335. package/dist/sources/index.js.map +1 -0
  336. package/dist/ui/index.d.ts +12 -0
  337. package/dist/ui/index.js +28 -0
  338. package/dist/ui/index.js.map +1 -0
  339. package/dist/ui/public/app.css +239 -0
  340. package/dist/ui/public/favicon.svg +1 -0
  341. package/dist/ui/public/index.html +27 -0
  342. package/dist/ui/public/js/client/api.js +68 -0
  343. package/dist/ui/public/js/client/app.js +146 -0
  344. package/dist/ui/public/js/client/dom.js +34 -0
  345. package/dist/ui/public/js/client/page.js +9 -0
  346. package/dist/ui/public/js/client/pages/actions.js +35 -0
  347. package/dist/ui/public/js/client/pages/blocked.js +58 -0
  348. package/dist/ui/public/js/client/pages/config.js +126 -0
  349. package/dist/ui/public/js/client/pages/dashboard.js +175 -0
  350. package/dist/ui/public/js/client/pages/run.js +367 -0
  351. package/dist/ui/public/js/client/pages/runs.js +72 -0
  352. package/dist/ui/public/js/client/pages/views.js +80 -0
  353. package/dist/ui/public/js/client/pages/workflows.js +291 -0
  354. package/dist/ui/public/js/client/types.js +1 -0
  355. package/dist/ui/public/js/client/ui.js +152 -0
  356. package/dist/ui/public/js/shared/dom.js +77 -0
  357. package/dist/ui/public/js/shared/events.js +88 -0
  358. package/dist/ui/public/js/shared/format.js +213 -0
  359. package/dist/ui/public/js/shared/logs.js +136 -0
  360. package/dist/ui/public/js/shared/params.js +90 -0
  361. package/dist/ui/public/js/shared/route.js +36 -0
  362. package/dist/ui/public/js/shared/sort.js +70 -0
  363. package/dist/ui/public/js/shared/sse.js +193 -0
  364. package/dist/ui/public/js/shared/throttle.js +65 -0
  365. package/dist/views/index.d.ts +6 -0
  366. package/dist/views/index.js +7 -0
  367. package/dist/views/index.js.map +1 -0
  368. package/dist/views/service.d.ts +131 -0
  369. package/dist/views/service.js +264 -0
  370. package/dist/views/service.js.map +1 -0
  371. package/docs/api.md +239 -0
  372. package/docs/configuration-reference.md +1785 -0
  373. package/docs/cron-semantics.md +96 -0
  374. package/docs/manual-tests/github-fixture/config.yaml +30 -0
  375. package/docs/manual-tests/github-fixture/helpers.sh +93 -0
  376. package/docs/manual-tests/github-fixture/identities.yaml +18 -0
  377. package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
  378. package/docs/manual-tests/github-fixture/sources.yaml +13 -0
  379. package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
  380. package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
  381. package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
  382. package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
  383. package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
  384. package/docs/manual-tests/github.md +1308 -0
  385. package/docs/manual-tests/process-supervision.md +64 -0
  386. package/docs/manual-tests/systemd.md +68 -0
  387. package/docs/operations.md +271 -0
  388. package/docs/worker-protocol.md +145 -0
  389. package/examples/config/config.yaml +35 -0
  390. package/examples/config/identities.yaml +18 -0
  391. package/examples/config/sources.yaml +13 -0
  392. package/examples/config/views/needs-maintainer.yaml +15 -0
  393. package/examples/config/workflows/coder-project-a.yaml +86 -0
  394. package/examples/config/workflows/deploy.yaml +15 -0
  395. package/examples/config/workflows/nightly-maintenance.yaml +13 -0
  396. package/package.json +75 -4
@@ -0,0 +1,239 @@
1
+ /* Agent Dispatch Daemon (adispatchd) web UI – plain CSS, light/dark via prefers-color-scheme. */
2
+ :root {
3
+ --bg: #f6f7f9;
4
+ --surface: #ffffff;
5
+ --surface-2: #f0f2f5;
6
+ --text: #1c2330;
7
+ --muted: #5d6878;
8
+ --border: #d9dee6;
9
+ --accent: #2563eb;
10
+ --accent-text: #ffffff;
11
+ --ok: #157f3b;
12
+ --ok-bg: #e3f5e9;
13
+ --warn: #8a5a00;
14
+ --warn-bg: #fff3d6;
15
+ --error: #b42318;
16
+ --error-bg: #fde8e7;
17
+ --info: #1d4ed8;
18
+ --info-bg: #e5edff;
19
+ --muted-bg: #eceff3;
20
+ --code-bg: #f3f4f6;
21
+ --shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
22
+ --radius: 8px;
23
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
24
+ color-scheme: light dark;
25
+ }
26
+
27
+ @media (prefers-color-scheme: dark) {
28
+ :root {
29
+ --bg: #0f1218;
30
+ --surface: #171b23;
31
+ --surface-2: #1f2430;
32
+ --text: #e5e9f0;
33
+ --muted: #9aa4b2;
34
+ --border: #2c3341;
35
+ --accent: #5b8cff;
36
+ --accent-text: #0b1020;
37
+ --ok: #5fd38a;
38
+ --ok-bg: #12301f;
39
+ --warn: #f5c062;
40
+ --warn-bg: #352812;
41
+ --error: #ff8a80;
42
+ --error-bg: #3a1716;
43
+ --info: #8fb0ff;
44
+ --info-bg: #19254a;
45
+ --muted-bg: #252b37;
46
+ --code-bg: #11151c;
47
+ --shadow: none;
48
+ }
49
+ }
50
+
51
+ * { box-sizing: border-box; }
52
+ html, body { margin: 0; padding: 0; }
53
+ body {
54
+ background: var(--bg);
55
+ color: var(--text);
56
+ font: 14px/1.45 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
57
+ }
58
+ a { color: var(--accent); text-decoration: none; }
59
+ a:hover { text-decoration: underline; }
60
+ code, pre, .mono { font-family: var(--mono); font-size: 12.5px; }
61
+ code { overflow-wrap: anywhere; }
62
+ small { font-size: 12px; }
63
+ h1 { font-size: 22px; margin: 4px 0 14px; overflow-wrap: anywhere; }
64
+ h1 small { font-weight: normal; }
65
+ h2 { font-size: 16px; margin: 0 0 10px; }
66
+ h3 { font-size: 14px; margin: 16px 0 6px; }
67
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
68
+
69
+ /* ---------- header / nav ---------- */
70
+ .top {
71
+ position: sticky; top: 0; z-index: 10;
72
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px;
73
+ padding: 8px 16px; background: var(--surface); border-bottom: 1px solid var(--border);
74
+ }
75
+ .brand { font-weight: 700; color: var(--text); white-space: nowrap; }
76
+ #nav { flex: 1 1 auto; min-width: 0; }
77
+ .menu { list-style: none; display: flex; flex-wrap: wrap; gap: 2px; margin: 0; padding: 0; }
78
+ .menu > li { position: relative; }
79
+ .menu a, .menu summary {
80
+ display: inline-flex; align-items: center; gap: 4px;
81
+ padding: 6px 10px; border-radius: 6px; color: var(--text); cursor: pointer; white-space: nowrap;
82
+ }
83
+ .menu summary { list-style: none; }
84
+ .menu summary::-webkit-details-marker { display: none; }
85
+ .menu summary::after { content: '▾'; font-size: 10px; color: var(--muted); }
86
+ .menu a:hover, .menu summary:hover { background: var(--surface-2); text-decoration: none; }
87
+ .menu a.active, .menu summary.active { background: var(--info-bg); color: var(--info); }
88
+ .submenu {
89
+ list-style: none; margin: 4px 0 0; padding: 6px; position: absolute; left: 0; top: 100%;
90
+ min-width: 200px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
91
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); z-index: 20;
92
+ }
93
+ .submenu a { display: block; }
94
+ .count .badge { margin-left: 2px; }
95
+ .live { font-size: 12px; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
96
+ .live::before { content: '●'; margin-right: 4px; }
97
+ .live-open { color: var(--ok); background: var(--ok-bg); }
98
+ .live-connecting, .live-reconnecting { color: var(--warn); background: var(--warn-bg); }
99
+ .live-closed { color: var(--error); background: var(--error-bg); }
100
+
101
+ /* ---------- layout ---------- */
102
+ main { padding: 16px; max-width: 1400px; margin: 0 auto; outline: none; }
103
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; align-items: start; }
104
+ .span-all { grid-column: 1 / -1; }
105
+ .card {
106
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
107
+ padding: 14px; box-shadow: var(--shadow); min-width: 0; margin-bottom: 14px;
108
+ }
109
+ .grid > .card { margin-bottom: 0; }
110
+ .crumbs { margin: 0 0 4px; }
111
+ .hint { color: var(--muted); font-size: 12.5px; margin: 8px 0; }
112
+ .muted { color: var(--muted); }
113
+ .empty { color: var(--muted); font-style: italic; }
114
+ .unknown { color: var(--muted); }
115
+ .error-text { color: var(--error); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12.5px; }
116
+ .wrap { white-space: pre-wrap; word-break: break-all; }
117
+
118
+ /* ---------- key/value ---------- */
119
+ .kv { display: grid; grid-template-columns: minmax(110px, max-content) 1fr; gap: 6px 14px; margin: 0; }
120
+ .kv dt { color: var(--muted); }
121
+ .kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
122
+
123
+ /* ---------- tables ---------- */
124
+ .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
125
+ table { width: 100%; border-collapse: collapse; }
126
+ th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
127
+ th { font-weight: 600; color: var(--muted); font-size: 12.5px; white-space: nowrap; }
128
+ tbody tr:hover { background: var(--surface-2); }
129
+ td { overflow-wrap: anywhere; }
130
+ .event-message { white-space: pre-wrap; }
131
+ button.sort { background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; font-weight: 600; }
132
+ button.sort:hover { color: var(--text); }
133
+ ul.plain, ul.issues, ul.checks { list-style: none; padding: 0; margin: 0; }
134
+ ul.issues li, ul.checks li { margin: 4px 0; }
135
+ ul.trace { list-style: none; margin: 2px 0 2px 8px; padding-left: 10px; border-left: 2px solid var(--border); }
136
+ ul.trace li { margin: 3px 0; }
137
+ ol.attempts { margin: 4px 0; padding-left: 18px; }
138
+
139
+ /* ---------- badges ---------- */
140
+ .badges { display: inline-flex; flex-wrap: wrap; gap: 4px; }
141
+ .badge {
142
+ display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 12px; font-weight: 600;
143
+ white-space: nowrap; border: 1px solid transparent; line-height: 18px; margin-right: 2px;
144
+ }
145
+ .badge-ok { color: var(--ok); background: var(--ok-bg); }
146
+ .badge-warn { color: var(--warn); background: var(--warn-bg); }
147
+ .badge-error { color: var(--error); background: var(--error-bg); }
148
+ .badge-info { color: var(--info); background: var(--info-bg); }
149
+ .badge-muted { color: var(--muted); background: var(--muted-bg); }
150
+
151
+ /* ---------- alerts / toasts ---------- */
152
+ .alert { padding: 10px 12px; border-radius: var(--radius); margin: 8px 0; border: 1px solid transparent; overflow-wrap: anywhere; }
153
+ .alert-ok { background: var(--ok-bg); color: var(--ok); }
154
+ .alert-warn { background: var(--warn-bg); color: var(--warn); }
155
+ .alert-error { background: var(--error-bg); color: var(--error); }
156
+ .alert-info { background: var(--info-bg); color: var(--info); }
157
+ .alert pre { color: var(--text); }
158
+ .toasts { position: fixed; right: 16px; bottom: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 100; max-width: min(420px, calc(100vw - 32px)); }
159
+ .toast { padding: 10px 14px; border-radius: var(--radius); background: var(--surface); border: 1px solid var(--border); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); overflow-wrap: anywhere; }
160
+ .toast-ok { border-left: 4px solid var(--ok); }
161
+ .toast-error { border-left: 4px solid var(--error); }
162
+ .toast-info, .toast-warn, .toast-muted { border-left: 4px solid var(--info); }
163
+
164
+ /* ---------- buttons / forms ---------- */
165
+ .actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 10px 0; }
166
+ .btn {
167
+ font: inherit; padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border);
168
+ background: var(--surface-2); color: var(--text); cursor: pointer; white-space: nowrap;
169
+ }
170
+ .btn:hover:not(:disabled) { filter: brightness(0.96); }
171
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
172
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
173
+ .btn-danger { background: var(--error-bg); border-color: var(--error); color: var(--error); }
174
+ .segmented { display: inline-flex; }
175
+ .segmented .btn { border-radius: 0; }
176
+ .segmented .btn:first-child { border-radius: 6px 0 0 6px; }
177
+ .segmented .btn:last-child { border-radius: 0 6px 6px 0; }
178
+ .input {
179
+ font: inherit; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--border);
180
+ background: var(--surface); color: var(--text); min-width: 0; max-width: 100%;
181
+ }
182
+ .input-narrow { width: 7em; }
183
+ .form { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
184
+ .field { display: flex; flex-direction: column; gap: 4px; }
185
+ .field > span { font-weight: 600; font-size: 12.5px; }
186
+ .check { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
187
+ .inline-form { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
188
+ .filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
189
+ .pager { display: flex; gap: 16px; align-items: center; margin: 10px 0; color: var(--muted); }
190
+ .tabs { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--border); margin: 10px 0 14px; }
191
+ .tabs a { padding: 7px 12px; border-bottom: 2px solid transparent; color: var(--muted); }
192
+ .tabs a.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
193
+ .tabs a:hover { text-decoration: none; color: var(--text); }
194
+ .meter-wrap { display: inline-flex; align-items: center; gap: 6px; }
195
+ meter { width: 120px; height: 12px; }
196
+
197
+ /* ---------- run ---------- */
198
+ .run-status { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
199
+ .status-message { color: var(--muted); font-style: italic; overflow-wrap: anywhere; }
200
+ pre.log, pre.json, pre.yaml {
201
+ background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px;
202
+ padding: 10px; margin: 6px 0; overflow: auto; white-space: pre-wrap; word-break: break-all;
203
+ }
204
+ pre.log { height: 60vh; min-height: 240px; white-space: pre; word-break: normal; }
205
+ pre.json { max-height: 420px; }
206
+ pre.yaml { max-height: 70vh; white-space: pre; word-break: normal; }
207
+ .log-info { font-size: 12px; }
208
+ details > summary { cursor: pointer; }
209
+ details.file { border-bottom: 1px solid var(--border); padding: 6px 0; }
210
+
211
+ /* ---------- dialog ---------- */
212
+ .dialog {
213
+ border: 1px solid var(--border); border-radius: 10px; padding: 18px; width: min(520px, calc(100vw - 24px));
214
+ background: var(--surface); color: var(--text); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
215
+ }
216
+ .dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
217
+ .dialog h2 { margin-top: 0; }
218
+ .dialog-body { margin: 6px 0 12px; overflow-wrap: anywhere; }
219
+ .dialog .field { margin: 10px 0; }
220
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
221
+
222
+ /* ---------- narrow screens ---------- */
223
+ @media (max-width: 640px) {
224
+ main { padding: 10px; }
225
+ .top { padding: 6px 10px; }
226
+ .grid { grid-template-columns: 1fr; }
227
+ .kv { grid-template-columns: 1fr; gap: 2px; }
228
+ .kv dt { margin-top: 6px; }
229
+ h1 { font-size: 19px; }
230
+ .menu a, .menu summary { padding: 5px 8px; }
231
+ .submenu { position: static; box-shadow: none; }
232
+ pre.log { height: 50vh; }
233
+ .table-wrap > table { min-width: 640px; }
234
+ .table-wrap td code { overflow-wrap: normal; white-space: nowrap; }
235
+ }
236
+
237
+ /* events table: keep type and payload columns compact */
238
+ table.events td:nth-child(4) code, table.events td:nth-child(2) { white-space: nowrap; overflow-wrap: normal; }
239
+ table.events td:last-child { white-space: nowrap; }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#2563eb"/><circle cx="10" cy="16" r="4" fill="#fff"/><circle cx="22" cy="9" r="3" fill="#fff"/><circle cx="22" cy="23" r="3" fill="#fff"/><path d="M13 14l6-4M13 18l6 4" stroke="#fff" stroke-width="2"/></svg>
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta
7
+ http-equiv="Content-Security-Policy"
8
+ content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; font-src 'self'; base-uri 'none'; form-action 'none'; object-src 'none'"
9
+ />
10
+ <meta name="referrer" content="no-referrer" />
11
+ <meta name="color-scheme" content="light dark" />
12
+ <title>Agent Dispatch Daemon</title>
13
+ <link rel="icon" href="favicon.svg" type="image/svg+xml" />
14
+ <link rel="stylesheet" href="app.css" />
15
+ <script type="module" src="js/client/app.js"></script>
16
+ </head>
17
+ <body>
18
+ <header class="top">
19
+ <a class="brand" href="#/" title="Agent Dispatch Daemon">adispatchd</a>
20
+ <nav id="nav" aria-label="Main"></nav>
21
+ <span id="live" class="live live-connecting" role="status" aria-live="polite">connecting…</span>
22
+ </header>
23
+ <main id="main" tabindex="-1">
24
+ <noscript><p>This interface needs JavaScript.</p></noscript>
25
+ </main>
26
+ </body>
27
+ </html>
@@ -0,0 +1,68 @@
1
+ /**
2
+ * HTTP client for `/api/v1` (docs/api.md). Mutating requests always carry
3
+ * the CSRF header `X-Adispatch-Request: 1` and a JSON body.
4
+ */
5
+ /** Error returned by the API (`{error:{code,message,details}}`) or the network. */
6
+ export class ApiError extends Error {
7
+ status;
8
+ code;
9
+ details;
10
+ constructor(status, code, message, details = null) {
11
+ super(message);
12
+ this.status = status;
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+ async function request(method, path, body) {
18
+ const headers = { accept: 'application/json' };
19
+ const mutating = method !== 'GET' && method !== 'HEAD';
20
+ if (mutating) {
21
+ headers['x-adispatch-request'] = '1';
22
+ headers['content-type'] = 'application/json';
23
+ }
24
+ let res;
25
+ try {
26
+ res = await fetch(path, {
27
+ method,
28
+ headers,
29
+ credentials: 'same-origin',
30
+ cache: 'no-store',
31
+ ...(mutating ? { body: JSON.stringify(body ?? {}) } : {}),
32
+ });
33
+ }
34
+ catch (err) {
35
+ throw new ApiError(0, 'network_error', `Network error: ${err instanceof Error ? err.message : String(err)}`);
36
+ }
37
+ let json = null;
38
+ const text = await res.text();
39
+ if (text) {
40
+ try {
41
+ json = JSON.parse(text);
42
+ }
43
+ catch {
44
+ json = null;
45
+ }
46
+ }
47
+ if (!res.ok) {
48
+ const e = json?.error;
49
+ throw new ApiError(res.status, typeof e?.code === 'string' ? e.code : `http_${res.status}`, typeof e?.message === 'string' ? e.message : `HTTP ${res.status}`, e?.details ?? null);
50
+ }
51
+ return json;
52
+ }
53
+ /** Query string from an object (empty values omitted). */
54
+ export function qs(params) {
55
+ const p = new URLSearchParams();
56
+ for (const [k, v] of Object.entries(params))
57
+ if (v !== null && v !== undefined && v !== '')
58
+ p.set(k, String(v));
59
+ const s = p.toString();
60
+ return s ? `?${s}` : '';
61
+ }
62
+ export const api = {
63
+ get: (path) => request('GET', path),
64
+ post: (path, body) => request('POST', path, body),
65
+ patch: (path, body) => request('PATCH', path, body),
66
+ };
67
+ /** Encode one path segment. */
68
+ export const seg = (s) => encodeURIComponent(String(s));
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Entry point of the web UI (WI-11, Spec §16): navigation, hash router, one
3
+ * shared SSE connection with reconnect + REST resync.
4
+ */
5
+ import { api } from './api.js';
6
+ import { h, replaceChildren, $ } from './dom.js';
7
+ import { parseHash } from '../shared/route.js';
8
+ import { LiveConnection } from '../shared/sse.js';
9
+ import { createRefresher } from '../shared/throttle.js';
10
+ import { badge, errorBox } from './ui.js';
11
+ import { dashboardPage } from './pages/dashboard.js';
12
+ import { runsPage } from './pages/runs.js';
13
+ import { runPage } from './pages/run.js';
14
+ import { workflowPage, workflowsPage } from './pages/workflows.js';
15
+ import { blockedPage } from './pages/blocked.js';
16
+ import { failedActionsPage } from './pages/actions.js';
17
+ import { viewPage, viewsListPage } from './pages/views.js';
18
+ import { configPage } from './pages/config.js';
19
+ /** Views listed directly in the menu up to this count (more → only "All Views"). */
20
+ const MAX_DIRECT_VIEWS = 8;
21
+ const main = $('#main');
22
+ const nav = $('#nav');
23
+ const liveIndicator = $('#live');
24
+ let current = null;
25
+ // ------------------------------------------------------------------ nav
26
+ const counters = { blocks: h('span', { class: 'count' }), actions: h('span', { class: 'count' }) };
27
+ const viewsMenu = h('ul', { class: 'submenu' });
28
+ const navItem = (key, href, label, extra) => h('li', null, h('a', { href, 'data-nav': key }, label, extra ?? null));
29
+ replaceChildren(nav, h('ul', { class: 'menu' }, navItem('', '#/', 'Dashboard'), navItem('runs', '#/runs', 'Runs'), navItem('workflows', '#/workflows', 'Workflows'), navItem('blocked', '#/blocked', 'Blocked', counters.blocks), navItem('actions', '#/actions', 'Failed Actions', counters.actions), h('li', { class: 'has-sub' }, h('details', { class: 'views-menu' }, h('summary', { 'data-nav': 'views' }, 'Views'), viewsMenu)), navItem('config', '#/config', 'Configuration')));
30
+ const renderCount = (el, n, tone) => {
31
+ replaceChildren(el, n > 0 ? badge(String(n), tone) : null);
32
+ };
33
+ const loadNav = async () => {
34
+ try {
35
+ const [status, views] = await Promise.all([api.get('/api/v1/status'), api.get('/api/v1/views')]);
36
+ renderCount(counters.blocks, status.counts.activeBlocks, 'warn');
37
+ renderCount(counters.actions, status.counts.failedActions, 'error');
38
+ const direct = views.views.length <= MAX_DIRECT_VIEWS ? views.views : [];
39
+ replaceChildren(viewsMenu, direct.map((v) => h('li', null, h('a', { href: `#/views/${encodeURIComponent(v.id)}`, 'data-nav': `views/${v.id}` }, v.title))), h('li', null, h('a', { href: '#/views', 'data-nav': 'views' }, 'All Views')));
40
+ markActiveNav();
41
+ }
42
+ catch {
43
+ // shown by the live indicator / pages
44
+ }
45
+ };
46
+ const navRefresher = createRefresher(loadNav, 2000);
47
+ const markActiveNav = () => {
48
+ const r = parseHash(location.hash);
49
+ const top = r.segments[0] ?? '';
50
+ const full = r.segments.slice(0, 2).join('/');
51
+ nav.querySelectorAll('[data-nav]').forEach((a) => {
52
+ const key = a.getAttribute('data-nav') ?? '';
53
+ const active = key === full || (key === top && !(top === 'views' && r.segments.length > 1 && a.tagName === 'A'));
54
+ a.classList.toggle('active', active);
55
+ if (a.tagName === 'A') {
56
+ if (active)
57
+ a.setAttribute('aria-current', 'page');
58
+ else
59
+ a.removeAttribute('aria-current');
60
+ }
61
+ });
62
+ };
63
+ // --------------------------------------------------------------- router
64
+ const ctxFor = (route) => ({
65
+ route,
66
+ navigate: (hash) => {
67
+ if (location.hash === hash)
68
+ render();
69
+ else
70
+ location.hash = hash;
71
+ },
72
+ refreshNav: () => navRefresher.trigger(),
73
+ setTitle: (title) => {
74
+ document.title = `${title} · adispatchd`;
75
+ },
76
+ });
77
+ function pageFor(route, ctx) {
78
+ const [a, b] = route.segments;
79
+ switch (a) {
80
+ case undefined:
81
+ return dashboardPage(ctx);
82
+ case 'runs':
83
+ return b ? runPage(ctx, b) : runsPage(ctx);
84
+ case 'workflows':
85
+ return b ? workflowPage(ctx, b) : workflowsPage(ctx);
86
+ case 'blocked':
87
+ return blockedPage(ctx);
88
+ case 'actions':
89
+ return failedActionsPage(ctx);
90
+ case 'views':
91
+ return b ? viewPage(ctx, b) : viewsListPage(ctx);
92
+ case 'config':
93
+ return configPage(ctx);
94
+ default:
95
+ return { el: h('div', { class: 'page' }, h('h1', null, 'Not found'), h('p', null, 'Unknown page. ', h('a', { href: '#/' }, 'Go to the dashboard'))) };
96
+ }
97
+ }
98
+ function render() {
99
+ current?.dispose?.();
100
+ current = null;
101
+ const route = parseHash(location.hash);
102
+ try {
103
+ current = pageFor(route, ctxFor(route));
104
+ replaceChildren(main, current.el);
105
+ }
106
+ catch (err) {
107
+ replaceChildren(main, errorBox(err));
108
+ }
109
+ nav.querySelectorAll('details').forEach((d) => {
110
+ d.open = false;
111
+ });
112
+ markActiveNav();
113
+ main.focus({ preventScroll: true });
114
+ window.scrollTo(0, 0);
115
+ }
116
+ window.addEventListener('hashchange', render);
117
+ // ----------------------------------------------------------------- live
118
+ const STATE_LABEL = { connecting: 'connecting…', open: 'live', reconnecting: 'reconnecting…', closed: 'offline' };
119
+ const live = new LiveConnection({
120
+ url: '/api/v1/events',
121
+ // The DOM EventSource is structurally compatible (handler parameter types differ only nominally).
122
+ create: (url) => new EventSource(url),
123
+ onMessage: (msg) => {
124
+ if (msg.type === 'block.created' || msg.type === 'block.cleared' || msg.type === 'run.action' || msg.type === 'config.reloaded' || msg.type === 'system.status')
125
+ navRefresher.trigger();
126
+ try {
127
+ current?.onLive?.(msg);
128
+ }
129
+ catch (err) {
130
+ console.error('live handler failed', err);
131
+ }
132
+ },
133
+ onResync: () => {
134
+ navRefresher.trigger();
135
+ current?.onResync?.();
136
+ },
137
+ onState: (state) => {
138
+ liveIndicator.textContent = STATE_LABEL[state];
139
+ liveIndicator.className = `live live-${state}`;
140
+ liveIndicator.title = state === 'open' ? 'Live updates via SSE' : 'Live updates interrupted; data is reloaded after reconnecting';
141
+ },
142
+ });
143
+ window.addEventListener('online', () => live.reconnectNow());
144
+ render();
145
+ void loadNav();
146
+ live.start();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Browser binding of the safe element builder (see shared/dom.ts). All
3
+ * rendering goes through `h()`; untrusted strings therefore always end up in
4
+ * text nodes. `innerHTML` is never used in the UI.
5
+ */
6
+ import { createH } from '../shared/dom.js';
7
+ /** Element builder bound to `document`. */
8
+ export const h = createH(document);
9
+ /** Remove all children of `el` and append `children`. */
10
+ export function replaceChildren(el, ...children) {
11
+ el.replaceChildren();
12
+ const tmp = h('div', null, ...children);
13
+ while (tmp.firstChild)
14
+ el.appendChild(tmp.firstChild);
15
+ }
16
+ /**
17
+ * Like {@link replaceChildren}, but keeps the open state of `<details>`
18
+ * elements (matched by position) across live re-renders.
19
+ */
20
+ export function rerender(el, ...children) {
21
+ const open = [...el.querySelectorAll('details')].map((d) => d.open);
22
+ replaceChildren(el, ...children);
23
+ el.querySelectorAll('details').forEach((d, i) => {
24
+ if (open[i])
25
+ d.open = true;
26
+ });
27
+ }
28
+ /** Typed `querySelector` that throws if missing. */
29
+ export function $(selector, root = document) {
30
+ const el = root.querySelector(selector);
31
+ if (!el)
32
+ throw new Error(`element ${selector} not found`);
33
+ return el;
34
+ }
@@ -0,0 +1,9 @@
1
+ /** True for live messages that change run/system state (used for coalesced refreshes). */
2
+ export function isStateChange(msg) {
3
+ return msg.type !== 'run.output' && msg.type !== 'stream.open' && msg.type !== 'retention.finished';
4
+ }
5
+ /** Run ID a live message refers to, if any. */
6
+ export function messageRunId(msg) {
7
+ const p = msg.payload;
8
+ return typeof p.runId === 'string' ? p.runId : null;
9
+ }
@@ -0,0 +1,35 @@
1
+ /** Failed Actions (Spec §10, §16): errors, attempts, manual retry / skip. */
2
+ import { api } from '../api.js';
3
+ import { h, replaceChildren, rerender } from '../dom.js';
4
+ import { createRefresher } from '../../shared/throttle.js';
5
+ import { errorBox, loading } from '../ui.js';
6
+ import { actionsTable } from './run.js';
7
+ export const failedActionsPage = (ctx) => {
8
+ ctx.setTitle('Failed Actions');
9
+ const body = h('div', null, loading());
10
+ const el = h('div', { class: 'page' }, h('h1', null, 'Failed Actions'), h('p', { class: 'hint' }, 'Lifecycle actions whose attempts are exhausted ("lifecycle failed"). Retry starts a fresh attempt budget; the hook chain continues afterwards in the background.'), body);
11
+ const after = () => {
12
+ ctx.refreshNav();
13
+ refresher.trigger();
14
+ };
15
+ const load = async () => {
16
+ try {
17
+ const res = await api.get('/api/v1/actions/failed');
18
+ rerender(body, res.actions.length ? actionsTable(res.actions, after, { showRun: true }) : h('p', { class: 'empty' }, 'No failed lifecycle actions.'));
19
+ }
20
+ catch (err) {
21
+ replaceChildren(body, errorBox(err));
22
+ }
23
+ };
24
+ const refresher = createRefresher(load, 1000);
25
+ void load();
26
+ return {
27
+ el,
28
+ onLive: (msg) => {
29
+ if (msg.type === 'run.action' || (msg.type === 'run.event' && String(msg.payload.type).startsWith('lifecycle.')))
30
+ refresher.trigger();
31
+ },
32
+ onResync: () => void refresher.now(),
33
+ dispose: () => refresher.dispose(),
34
+ };
35
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Blocked (Spec §11, §16): internal retry blocks of the orchestrator after
3
+ * repeated technical failures – explicitly NOT GitHub "blocked by" or labels.
4
+ */
5
+ import { api } from '../api.js';
6
+ import { h, replaceChildren, rerender } from '../dom.js';
7
+ import { createRefresher } from '../../shared/throttle.js';
8
+ import { act, button, confirmDialog, errorBox, externalLink, loading, routeLink, runLink, table, time } from '../ui.js';
9
+ export const blockedPage = (ctx) => {
10
+ ctx.setTitle('Blocked');
11
+ const body = h('div', null, loading());
12
+ const el = h('div', { class: 'page' }, h('h1', null, 'Blocked'), h('div', { class: 'alert alert-info' }, h('strong', null, 'Internal retry blocks only. '), 'A block is created by the orchestrator after too many consecutive technical failures (crashed, start error, timeout) of a workflow/item. ', 'This is unrelated to GitHub "blocked by" dependencies or GitHub labels. Removing a block starts a new counting period; history is kept.'), body);
13
+ const clear = async (b, btn) => {
14
+ const r = await confirmDialog({
15
+ title: 'Remove retry block?',
16
+ message: h('div', null, h('p', null, `Block #${b.id} for workflow ${b.workflowId}${b.itemKey ? `, item ${b.itemKey}` : ' (whole workflow)'} is removed.`), h('p', null, 'The scheduler may start the item again in the next planning step. The failure counter starts a new period.')),
17
+ confirmLabel: 'Remove block',
18
+ danger: true,
19
+ });
20
+ if (!r.ok)
21
+ return;
22
+ await act(btn, () => api.post(`/api/v1/blocks/${b.id}/clear`, { clearedBy: 'ui' }), 'Block removed');
23
+ ctx.refreshNav();
24
+ await refresher.now();
25
+ };
26
+ const load = async () => {
27
+ try {
28
+ const res = await api.get('/api/v1/blocks');
29
+ rerender(body, table(['Block key', 'Reason', 'Failures', 'Created', 'Runs', 'Source item', ''], res.blocks.map((b) => {
30
+ const btn = button('Remove Block', () => void clear(b, btn), { kind: 'danger' });
31
+ const runs = [...new Set([...(b.runId ? [b.runId] : []), ...b.affectedRunIds])];
32
+ return [
33
+ h('span', null, routeLink(['workflows', b.workflowId], b.workflowName ?? b.workflowId), h('br'), h('code', null, b.itemKey ?? '(workflow scope)')),
34
+ b.reason,
35
+ String(b.failureCount),
36
+ time(b.createdAt),
37
+ runs.length ? h('ul', { class: 'plain' }, runs.slice(0, 10).map((id) => h('li', null, runLink(id))), runs.length > 10 ? h('li', { class: 'muted' }, `+${runs.length - 10} more`) : null) : '—',
38
+ externalLink(b.sourceUrl),
39
+ btn,
40
+ ];
41
+ }), { empty: 'No active retry blocks.' }));
42
+ }
43
+ catch (err) {
44
+ replaceChildren(body, errorBox(err));
45
+ }
46
+ };
47
+ const refresher = createRefresher(load, 1000);
48
+ void load();
49
+ return {
50
+ el,
51
+ onLive: (msg) => {
52
+ if (msg.type === 'block.created' || msg.type === 'block.cleared')
53
+ refresher.trigger();
54
+ },
55
+ onResync: () => void refresher.now(),
56
+ dispose: () => refresher.dispose(),
57
+ };
58
+ };