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.
- package/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- 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
|
+
};
|