portal-mojo 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 (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,488 @@
1
+ // Admin jobs — the control plane. One typed function per django-mojo route
2
+ // (`mojo/apps/jobs/rest/jobs.py` + `rest/control.py`), each documenting the
3
+ // exact request params and response shape it speaks.
4
+ //
5
+ // Three wire facts shape this whole module:
6
+ //
7
+ // 1. RESULT KEYS ARE NOT UNIFORM. Some handlers nest under `data`
8
+ // (clear-stuck, purge, clear-queue, cleanup-consumers, queue-sizes,
9
+ // channels, config, stats), others answer with TOP-LEVEL fields
10
+ // (ping, broadcast, reset-failed, force-scheduler-lead, control/test).
11
+ // `mojoCall` returns the unwrapped envelope, so each function reads from
12
+ // whichever level its endpoint actually uses — never a `data ?? body`
13
+ // heuristic.
14
+ // 2. SOME OPERATIONS HAVE NO ALL-CHANNEL FORM. `clear-stuck` and
15
+ // `manual-reclaim` are `@requires_params('channel')`; sending
16
+ // `channel: null` (as web-mojo did) is an instant HTTP 400. "All
17
+ // channels" is a CLIENT fan-out — see `forEachChannel`.
18
+ // 3. `GET /api/jobs/health` AND `health/<channel>` ARE DEAD.
19
+ // `JobManager.get_channel_health` reads `state['stream_length']` and
20
+ // `state['pending_count']`, which the Plan-B `get_queue_state` no longer
21
+ // returns → KeyError → HTTP 400 `{status:false,error:"'stream_length'"}`.
22
+ // Nothing here calls them; every dashboard number comes from
23
+ // `GET /api/jobs/stats`, which uses `.get()` fallbacks throughout.
24
+ //
25
+ // `POST /api/jobs/publish` does not exist either — web-mojo's `Job.publish`
26
+ // was dead code. Republishing happens through the `retry_request` action.
27
+ import { mojoCall } from '../../client';
28
+ import type {
29
+ JobQueueSize,
30
+ JobsConfig,
31
+ JobsStats,
32
+ RunnerRow,
33
+ RunnerSysinfo,
34
+ } from './models';
35
+
36
+ const JOBS_API = '/api/jobs';
37
+
38
+ // ── Reads ─────────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * `GET /api/jobs/stats` → `{status, data: JobsStats}`.
42
+ * Gate: `view_jobs | manage_jobs | jobs`. The dashboard's single source.
43
+ */
44
+ export async function fetchJobStats(): Promise<JobsStats> {
45
+ const body = await mojoCall(`${JOBS_API}/stats`);
46
+ return body.data as JobsStats;
47
+ }
48
+
49
+ /**
50
+ * `GET /api/jobs/runners` → `{status, count, data: RunnerRow[]}`.
51
+ *
52
+ * NOT a paged model endpoint: `on_list_runners` reads exactly one param
53
+ * (`channel`) and returns the COMPLETE list, ignoring `start`/`size`/`sort`/
54
+ * `search`. Rows are heartbeats with `id = runner_id` stamped on, sorted by
55
+ * runner_id. Any paging or ordering is therefore a client concern.
56
+ */
57
+ export async function fetchRunners(channel?: string): Promise<RunnerRow[]> {
58
+ const body = await mojoCall(`${JOBS_API}/runners`, {
59
+ params: channel ? { channel } : {},
60
+ });
61
+ return (body.data ?? []) as RunnerRow[];
62
+ }
63
+
64
+ /**
65
+ * `GET /api/jobs/runners/sysinfo?timeout=` → `{status, count, data: RunnerSysinfo[]}`.
66
+ * A redis broadcast-execute that BLOCKS for up to `timeout` seconds — on
67
+ * demand only, never polled.
68
+ */
69
+ export async function fetchAllRunnerSysinfo(timeout = 5): Promise<RunnerSysinfo[]> {
70
+ const body = await mojoCall(`${JOBS_API}/runners/sysinfo`, { params: { timeout } });
71
+ return (body.data ?? []) as RunnerSysinfo[];
72
+ }
73
+
74
+ /**
75
+ * `GET /api/jobs/runners/sysinfo/<runner_id>?timeout=` → `{status, data: RunnerSysinfo}`.
76
+ *
77
+ * A runner that never answers is an HTTP 404 envelope (`"Runner … did not
78
+ * respond"`) and REJECTS here. A runner that answers with an internal failure
79
+ * resolves with `status: 'error'` inside the payload — a different state, and
80
+ * the caller must render it as such.
81
+ */
82
+ export async function fetchRunnerSysinfo(runnerId: string, timeout = 5): Promise<RunnerSysinfo> {
83
+ const body = await mojoCall(`${JOBS_API}/runners/sysinfo/${encodeURIComponent(runnerId)}`, {
84
+ params: { timeout },
85
+ });
86
+ return body.data as RunnerSysinfo;
87
+ }
88
+
89
+ /**
90
+ * `GET /api/jobs/control/channels` → `{status, data: string[]}`.
91
+ * Channels DISCOVERED by scanning Redis for registered streams — a channel
92
+ * declared in settings but never published to will not appear here.
93
+ */
94
+ export async function fetchChannels(): Promise<string[]> {
95
+ const body = await mojoCall(`${JOBS_API}/control/channels`);
96
+ return (body.data ?? []) as string[];
97
+ }
98
+
99
+ /**
100
+ * `GET /api/jobs/control/queue-sizes` → `{status, data: {<channel>: JobQueueSize}}`.
101
+ * Redis stream/scheduled counts plus the per-status DB tallies.
102
+ */
103
+ export async function fetchQueueSizes(): Promise<Record<string, JobQueueSize>> {
104
+ const body = await mojoCall(`${JOBS_API}/control/queue-sizes`);
105
+ return (body.data ?? {}) as Record<string, JobQueueSize>;
106
+ }
107
+
108
+ /**
109
+ * `GET /api/jobs/control/config` → `{status, data: JobsConfig}`.
110
+ * MANAGE-gated (`manage_jobs | jobs`) — unlike every other read here, a
111
+ * view-only operator gets a 403.
112
+ */
113
+ export async function fetchJobsConfig(): Promise<JobsConfig> {
114
+ const body = await mojoCall(`${JOBS_API}/control/config`);
115
+ return body.data as JobsConfig;
116
+ }
117
+
118
+ // ── Runner control ────────────────────────────────────────────────────
119
+
120
+ export interface PingResult {
121
+ runner_id: string;
122
+ /** False = the runner did not answer within `timeout`. Not an error. */
123
+ responsive: boolean;
124
+ }
125
+
126
+ /**
127
+ * `POST /api/jobs/runners/ping` `{runner_id, timeout}` → TOP-LEVEL
128
+ * `{status, runner_id, responsive}`.
129
+ */
130
+ export async function pingRunner(runnerId: string, timeout = 2): Promise<PingResult> {
131
+ const body = await mojoCall(`${JOBS_API}/runners/ping`, {
132
+ method: 'POST',
133
+ body: { runner_id: runnerId, timeout },
134
+ }) as { runner_id?: string; responsive?: boolean };
135
+ return { runner_id: body.runner_id ?? runnerId, responsive: Boolean(body.responsive) };
136
+ }
137
+
138
+ /**
139
+ * `POST /api/jobs/runners/shutdown` `{runner_id, graceful}` → `{status, message}`.
140
+ * Fire-and-forget: the command is pushed onto the runner's control channel and
141
+ * the response says nothing about whether the runner obeyed.
142
+ */
143
+ export async function shutdownRunner(runnerId: string, graceful = true): Promise<string> {
144
+ const body = await mojoCall(`${JOBS_API}/runners/shutdown`, {
145
+ method: 'POST',
146
+ body: { runner_id: runnerId, graceful },
147
+ });
148
+ return body.message ?? `Shutdown command sent to runner ${runnerId}`;
149
+ }
150
+
151
+ /** The only commands `on_broadcast_command` accepts; anything else is a 400. */
152
+ export const BROADCAST_COMMANDS = ['status', 'shutdown', 'pause', 'resume', 'reload'] as const;
153
+ export type BroadcastCommand = (typeof BROADCAST_COMMANDS)[number];
154
+
155
+ export interface BroadcastResult {
156
+ command: string;
157
+ responses_count: number;
158
+ responses: unknown[];
159
+ }
160
+
161
+ /**
162
+ * `POST /api/jobs/runners/broadcast` `{command, data, timeout}` → TOP-LEVEL
163
+ * `{status, command, responses_count, responses}`. Hits EVERY runner.
164
+ */
165
+ export async function broadcastCommand(
166
+ command: BroadcastCommand,
167
+ opts: { data?: Record<string, unknown>; timeout?: number } = {},
168
+ ): Promise<BroadcastResult> {
169
+ const body = await mojoCall(`${JOBS_API}/runners/broadcast`, {
170
+ method: 'POST',
171
+ body: { command, data: opts.data ?? {}, timeout: opts.timeout ?? 2 },
172
+ }) as { command?: string; responses_count?: number; responses?: unknown[] };
173
+ return {
174
+ command: body.command ?? command,
175
+ responses_count: body.responses_count ?? 0,
176
+ responses: body.responses ?? [],
177
+ };
178
+ }
179
+
180
+ // ── Queue control ─────────────────────────────────────────────────────
181
+
182
+ export interface ClearStuckResult {
183
+ channel: string;
184
+ /** How many in-flight entries were moved back onto the queue. */
185
+ cleared: number;
186
+ details: { job_id?: string; requeued?: boolean }[];
187
+ errors: string[];
188
+ message?: string;
189
+ }
190
+
191
+ /**
192
+ * `POST /api/jobs/control/clear-stuck` `{channel, idle_threshold_ms}` →
193
+ * `{status, message, data: ClearStuckResult}`.
194
+ *
195
+ * `channel` is REQUIRED (`@requires_params`). The result key is `cleared` —
196
+ * web-mojo read `data.count`, which never existed and always rendered 0.
197
+ */
198
+ export async function clearStuck(channel: string, idleThresholdMs = 60000): Promise<ClearStuckResult> {
199
+ const body = await mojoCall(`${JOBS_API}/control/clear-stuck`, {
200
+ method: 'POST',
201
+ body: { channel, idle_threshold_ms: idleThresholdMs },
202
+ });
203
+ return body.data as ClearStuckResult;
204
+ }
205
+
206
+ /**
207
+ * `POST /api/jobs/control/manual-reclaim` `{channel}` →
208
+ * `{status, message, data: ClearStuckResult}`.
209
+ *
210
+ * The same handler as clear-stuck with `idle_threshold_ms: 0` — it reclaims
211
+ * EVERY in-flight entry, not only the idle ones. `channel` is required.
212
+ */
213
+ export async function manualReclaim(channel: string): Promise<ClearStuckResult> {
214
+ const body = await mojoCall(`${JOBS_API}/control/manual-reclaim`, {
215
+ method: 'POST',
216
+ body: { channel },
217
+ });
218
+ return body.data as ClearStuckResult;
219
+ }
220
+
221
+ export interface PurgeDryRunResult {
222
+ dry_run: true;
223
+ /** Rows that WOULD be deleted. Only a dry run reports `count`. */
224
+ count: number;
225
+ cutoff: string;
226
+ status_filter: string | null;
227
+ }
228
+
229
+ export interface PurgeRunResult {
230
+ dry_run?: false;
231
+ /** Rows actually deleted (jobs + cascaded events/logs). */
232
+ deleted: number;
233
+ details: Record<string, number>;
234
+ cutoff: string;
235
+ status_filter: string | null;
236
+ }
237
+
238
+ export type PurgeResult = PurgeDryRunResult | PurgeRunResult;
239
+
240
+ export function isPurgeDryRun(result: PurgeResult): result is PurgeDryRunResult {
241
+ return result.dry_run === true;
242
+ }
243
+
244
+ /**
245
+ * `POST /api/jobs/control/purge` `{days_old, status?, dry_run?}` →
246
+ * `{status, data: PurgeResult}`.
247
+ *
248
+ * `days_old` is REQUIRED. The two runs report DIFFERENT keys: a dry run
249
+ * answers `{dry_run: true, count, …}` and a real run answers
250
+ * `{deleted, details, …}`. Reading `count` off a real run (web-mojo's bug)
251
+ * always shows 0 deletions after a successful purge.
252
+ */
253
+ export async function purgeJobs(opts: {
254
+ daysOld: number;
255
+ status?: string | null;
256
+ dryRun?: boolean;
257
+ }): Promise<PurgeResult> {
258
+ const body = await mojoCall(`${JOBS_API}/control/purge`, {
259
+ method: 'POST',
260
+ body: {
261
+ days_old: opts.daysOld,
262
+ ...(opts.status ? { status: opts.status } : {}),
263
+ ...(opts.dryRun ? { dry_run: true } : {}),
264
+ },
265
+ });
266
+ return body.data as PurgeResult;
267
+ }
268
+
269
+ export interface ResetFailedResult {
270
+ reset_count: number;
271
+ requeue: { status: boolean; requeued?: number; channel?: string; error?: string }[];
272
+ message?: string;
273
+ }
274
+
275
+ /**
276
+ * `POST /api/jobs/control/reset-failed` `{channel?, since?, limit?}` →
277
+ * TOP-LEVEL `{status, message, reset_count, requeue[]}`.
278
+ *
279
+ * Unlike clear-stuck this one DOES have an all-channel form: omitting
280
+ * `channel` resets across every channel that has failures. `since` is an ISO
281
+ * datetime string (`datetime.fromisoformat`), not an epoch.
282
+ */
283
+ export async function resetFailed(opts: {
284
+ channel?: string | null;
285
+ since?: string | null;
286
+ limit?: number;
287
+ } = {}): Promise<ResetFailedResult> {
288
+ const body = await mojoCall(`${JOBS_API}/control/reset-failed`, {
289
+ method: 'POST',
290
+ body: {
291
+ ...(opts.channel ? { channel: opts.channel } : {}),
292
+ ...(opts.since ? { since: opts.since } : {}),
293
+ limit: opts.limit ?? 100,
294
+ },
295
+ }) as { reset_count?: number; requeue?: ResetFailedResult['requeue']; message?: string };
296
+ return {
297
+ reset_count: body.reset_count ?? 0,
298
+ requeue: body.requeue ?? [],
299
+ message: body.message,
300
+ };
301
+ }
302
+
303
+ export interface ClearQueueResult {
304
+ channel: string;
305
+ deleted: Record<string, boolean>;
306
+ db_pending_canceled: number;
307
+ status: boolean;
308
+ errors: string[];
309
+ }
310
+
311
+ /**
312
+ * `POST /api/jobs/control/clear-queue` `{channel, confirm:"yes"}` →
313
+ * `{status, message, data: ClearQueueResult}`.
314
+ *
315
+ * DESTRUCTIVE: it deletes the channel's Redis keys and marks every DB-pending
316
+ * job `canceled`. Both `channel` and `confirm === "yes"` are enforced
317
+ * server-side; the confirm token is injected here only at the call site, i.e.
318
+ * only AFTER the caller's armed confirmation has fired.
319
+ */
320
+ export async function clearQueue(channel: string): Promise<ClearQueueResult> {
321
+ const body = await mojoCall(`${JOBS_API}/control/clear-queue`, {
322
+ method: 'POST',
323
+ body: { channel, confirm: 'yes' },
324
+ });
325
+ return body.data as ClearQueueResult;
326
+ }
327
+
328
+ export interface CleanupConsumersResult {
329
+ status?: boolean;
330
+ errors?: string[];
331
+ [key: string]: unknown;
332
+ }
333
+
334
+ /**
335
+ * `POST /api/jobs/control/cleanup-consumers` `{channel?, destroy_empty_groups?}`
336
+ * → `{status, data}`. Omitting `channel` cleans every channel.
337
+ */
338
+ export async function cleanupConsumers(opts: {
339
+ channel?: string | null;
340
+ destroyEmptyGroups?: boolean;
341
+ } = {}): Promise<CleanupConsumersResult> {
342
+ const body = await mojoCall(`${JOBS_API}/control/cleanup-consumers`, {
343
+ method: 'POST',
344
+ body: {
345
+ ...(opts.channel ? { channel: opts.channel } : {}),
346
+ destroy_empty_groups: opts.destroyEmptyGroups ?? true,
347
+ },
348
+ });
349
+ return (body.data ?? {}) as CleanupConsumersResult;
350
+ }
351
+
352
+ export interface RebuildScheduledResult {
353
+ status?: boolean;
354
+ errors?: string[];
355
+ [key: string]: unknown;
356
+ }
357
+
358
+ /**
359
+ * `POST /api/jobs/control/rebuild-scheduled` `{channel?, limit?}` →
360
+ * `{status, data}`. Rebuilds the Redis scheduled ZSETs from DB truth.
361
+ */
362
+ export async function rebuildScheduled(opts: {
363
+ channel?: string | null;
364
+ limit?: number | null;
365
+ } = {}): Promise<RebuildScheduledResult> {
366
+ const body = await mojoCall(`${JOBS_API}/control/rebuild-scheduled`, {
367
+ method: 'POST',
368
+ body: {
369
+ ...(opts.channel ? { channel: opts.channel } : {}),
370
+ ...(opts.limit != null ? { limit: opts.limit } : {}),
371
+ },
372
+ });
373
+ return (body.data ?? {}) as RebuildScheduledResult;
374
+ }
375
+
376
+ export interface ForceSchedulerLeadResult {
377
+ message: string;
378
+ /** The lock value that was released, or null when no lock existed. */
379
+ previous_holder: string | null;
380
+ }
381
+
382
+ /**
383
+ * `POST /api/jobs/control/force-scheduler-lead` → TOP-LEVEL
384
+ * `{status, message, previous_holder}`.
385
+ *
386
+ * Deletes the scheduler lock so another process can take leadership. Safe only
387
+ * when the current leader is genuinely stuck — otherwise two schedulers can
388
+ * briefly dispatch the same due jobs.
389
+ */
390
+ export async function forceSchedulerLead(): Promise<ForceSchedulerLeadResult> {
391
+ const body = await mojoCall(`${JOBS_API}/control/force-scheduler-lead`, {
392
+ method: 'POST',
393
+ body: {},
394
+ }) as { message?: string; previous_holder?: string | null };
395
+ return {
396
+ message: body.message ?? 'Scheduler lock released',
397
+ previous_holder: body.previous_holder ?? null,
398
+ };
399
+ }
400
+
401
+ export interface TestJobResult {
402
+ job_id: string;
403
+ channel: string;
404
+ delayed: boolean;
405
+ message: string;
406
+ }
407
+
408
+ /**
409
+ * `POST /api/jobs/control/test` `{channel?, delay?}` → TOP-LEVEL
410
+ * `{status, message, job_id, channel, delayed}`.
411
+ *
412
+ * Publishes ONE sample job (`examples.sample_jobs.generate_report`) and hands
413
+ * back its id, so the operator can follow it into the jobs table. This is the
414
+ * parameterized superset of the older fixed `POST /api/jobs/test` smoke test,
415
+ * which is deliberately not wired.
416
+ */
417
+ export async function publishTestJob(opts: {
418
+ channel?: string;
419
+ delay?: number | null;
420
+ } = {}): Promise<TestJobResult> {
421
+ const body = await mojoCall(`${JOBS_API}/control/test`, {
422
+ method: 'POST',
423
+ body: {
424
+ channel: opts.channel ?? 'default',
425
+ ...(opts.delay != null ? { delay: opts.delay } : {}),
426
+ },
427
+ }) as { job_id?: string; channel?: string; delayed?: boolean; message?: string };
428
+ return {
429
+ job_id: body.job_id ?? '',
430
+ channel: body.channel ?? opts.channel ?? 'default',
431
+ delayed: Boolean(body.delayed),
432
+ message: body.message ?? 'Test job published',
433
+ };
434
+ }
435
+
436
+ /**
437
+ * `POST /api/jobs/tests` → `{status, message}`.
438
+ *
439
+ * Publishes a whole sample SUITE — one job per configured channel, ten delayed
440
+ * jobs and fifty immediate ones. It returns no ids; the operator watches the
441
+ * dashboard. Load-generating: it is offered behind an armed confirmation.
442
+ */
443
+ export async function publishTestSuite(): Promise<string> {
444
+ const body = await mojoCall(`${JOBS_API}/tests`, { method: 'POST', body: {} });
445
+ return body.message ?? 'Test suite published';
446
+ }
447
+
448
+ // ── Client-side fan-out ───────────────────────────────────────────────
449
+
450
+ export interface ChannelOutcome<T> {
451
+ channel: string;
452
+ ok: boolean;
453
+ value?: T;
454
+ error?: string;
455
+ }
456
+
457
+ /**
458
+ * Run a per-channel operation across many channels and report EVERY outcome.
459
+ *
460
+ * `clear-stuck` and `manual-reclaim` have no all-channel form on the backend,
461
+ * so "All channels" is this: `Promise.allSettled` over the channel list, with
462
+ * partial failures surfaced per channel. Never a blanket success — a fan-out
463
+ * where three channels cleared and one 400'd must read as exactly that.
464
+ */
465
+ export async function forEachChannel<T>(
466
+ channels: readonly string[],
467
+ run: (channel: string) => Promise<T>,
468
+ ): Promise<ChannelOutcome<T>[]> {
469
+ const settled = await Promise.allSettled(channels.map((channel) => run(channel)));
470
+ return settled.map((outcome, index) => {
471
+ const channel = channels[index]!;
472
+ if (outcome.status === 'fulfilled') return { channel, ok: true, value: outcome.value };
473
+ const reason = outcome.reason as unknown;
474
+ return {
475
+ channel,
476
+ ok: false,
477
+ error: reason instanceof Error ? reason.message : String(reason),
478
+ };
479
+ });
480
+ }
481
+
482
+ /** "3 of 4 channels cleared · email failed" — the fan-out's one-line readout. */
483
+ export function summarizeChannelOutcomes<T>(outcomes: readonly ChannelOutcome<T>[], verb: string): string {
484
+ const ok = outcomes.filter((outcome) => outcome.ok);
485
+ const failed = outcomes.filter((outcome) => !outcome.ok);
486
+ if (failed.length === 0) return `${ok.length} of ${outcomes.length} channels ${verb}`;
487
+ return `${ok.length} of ${outcomes.length} channels ${verb} · failed: ${failed.map((outcome) => outcome.channel).join(', ')}`;
488
+ }
@@ -0,0 +1,85 @@
1
+ // portal-mojo/admin — jobs engine.
2
+ //
3
+ // Queue operations for a django-mojo deployment: is the runner fleet alive,
4
+ // which channel is backed up, why did this job fail, retry or cancel it, clear
5
+ // stuck work, purge history — plus the (cron-style) scheduled tasks the old
6
+ // portal's backend supported but never wired up.
7
+ import type { AdminSection } from '../index';
8
+ import { JobDashboardPage } from './JobDashboardPage';
9
+ import { JobRunnersPage } from './JobRunnersPage';
10
+ import { JobsTablePage } from './JobsTablePage';
11
+ import { ScheduledTasksPage } from './ScheduledTasksPage';
12
+ import { JOBS_VIEW_PERMS, SCHEDULED_TASK_VIEW_PERMS } from './models';
13
+
14
+ export * from './models';
15
+ export * from './control';
16
+ export * from './queries';
17
+ export * from './JobDashboardPage';
18
+ export * from './JobRunnersPage';
19
+ export * from './RunnerDetail';
20
+ export * from './sections/JobRunnersStrip';
21
+ export * from './sections/JobChannelsPanel';
22
+ export * from './sections/JobThroughputSection';
23
+ export * from './sections/JobOperationsSection';
24
+ // Stage C — scheduled tasks (plan step 10).
25
+ export * from './ScheduledTasksPage';
26
+ export * from './ScheduledTaskDetail';
27
+ export * from './scheduled-task-form';
28
+ // Stage B — the jobs table and the job inspector.
29
+ export * from './columns';
30
+ export * from './JobsTablePage';
31
+ export * from './JobDetail';
32
+
33
+ /**
34
+ * Registry contribution. The section gate is the ANY-OF UNION needed to see at
35
+ * least one child; each route then applies its own exact backend gate, and
36
+ * those gates are NOT uniform:
37
+ *
38
+ * · the dashboard, runners and the jobs table read Job/JobEvent/JobLog and
39
+ * the control plane, all of which answer to `view_jobs|manage_jobs|jobs`;
40
+ * · `ScheduledTask.VIEW_PERMS` is `["jobs","view_scheduled_tasks","owner"]`
41
+ * — it contains NEITHER manage grant and not `view_jobs`. A `view_jobs`
42
+ * operator therefore cannot read the system task list at all (the backend
43
+ * falls through to the `owner` branch and answers HTTP 200 with their own
44
+ * rows, which is exactly the hole SCHEDULED_TASK_VIEW_PERMS closes).
45
+ *
46
+ * Gating the scheduled-tasks route on SCHEDULED_TASK_VIEW_PERMS keeps the
47
+ * sidenav entry and the page's own gate in agreement: a `jobs.viewer` never
48
+ * sees a menu item that renders a permission notice.
49
+ *
50
+ * Routes and the sidebar entry are generated from this object — a mounting app
51
+ * edits nothing.
52
+ */
53
+ export const JOBS_ADMIN_SECTION: AdminSection = {
54
+ id: 'jobs',
55
+ title: 'Jobs',
56
+ icon: 'bi-cpu',
57
+ navigationGroup: 'operations',
58
+ permissions: [...new Set([...JOBS_VIEW_PERMS, ...SCHEDULED_TASK_VIEW_PERMS])],
59
+ routes: [
60
+ {
61
+ path: '',
62
+ label: 'Dashboard',
63
+ component: JobDashboardPage,
64
+ permissions: JOBS_VIEW_PERMS,
65
+ },
66
+ {
67
+ path: 'runners',
68
+ label: 'Runners',
69
+ component: JobRunnersPage,
70
+ permissions: JOBS_VIEW_PERMS,
71
+ },
72
+ {
73
+ path: 'list',
74
+ label: 'Jobs',
75
+ component: JobsTablePage,
76
+ permissions: JOBS_VIEW_PERMS,
77
+ },
78
+ {
79
+ path: 'scheduled-tasks',
80
+ label: 'Scheduled Tasks',
81
+ component: ScheduledTasksPage,
82
+ permissions: SCHEDULED_TASK_VIEW_PERMS,
83
+ },
84
+ ],
85
+ };