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,599 @@
|
|
|
1
|
+
// Admin jobs — typed row shapes, model definitions and the derived helpers
|
|
2
|
+
// every jobs surface shares. Wire facts here come from django-mojo
|
|
3
|
+
// `mojo/apps/jobs/` (models/job.py, models/scheduled_task.py,
|
|
4
|
+
// models/task_result.py, rest/jobs.py, rest/control.py, manager.py,
|
|
5
|
+
// job_engine.py), which is authoritative wherever web-mojo disagreed.
|
|
6
|
+
//
|
|
7
|
+
// Two asymmetries are load-bearing and deliberately preserved in the types:
|
|
8
|
+
// · MODEL rows carry epoch-SECOND datetimes (the mojo serializer), while
|
|
9
|
+
// runner heartbeats and sysinfo replies carry ISO strings (they are raw
|
|
10
|
+
// Redis/JSON payloads that never pass through the serializer);
|
|
11
|
+
// · `GET /api/jobs/runners` stamps `id = runner_id` on every row
|
|
12
|
+
// (rest/jobs.py on_list_runners) but `GET /api/jobs/stats` does NOT —
|
|
13
|
+
// its `runners` array is manager.get_runners() verbatim. Hence
|
|
14
|
+
// `RunnerHeartbeat` (stats) vs `RunnerRow` (the runners endpoint).
|
|
15
|
+
import { defineModel, type Params } from '../../client';
|
|
16
|
+
import type { Tone } from '../../ui';
|
|
17
|
+
|
|
18
|
+
// ── Permission constants ──────────────────────────────────────────────
|
|
19
|
+
// Exact backend lists, `sys.`-pinned so a similarly named ACTIVE-GROUP
|
|
20
|
+
// permission can never light an operator-global surface. `owner` is
|
|
21
|
+
// deliberately dropped from the scheduled-task gates: ScheduledTask and
|
|
22
|
+
// TaskResult declare `owner` in VIEW_PERMS, so an ungated page would return
|
|
23
|
+
// HTTP 200 with only the caller's OWN rows and render a personal task list as
|
|
24
|
+
// if it were the system's. Gating on the global grant is fail-closed.
|
|
25
|
+
|
|
26
|
+
/** jobs.Job / JobEvent / JobLog VIEW_PERMS + the control plane's read gate. */
|
|
27
|
+
export const JOBS_VIEW_PERMS = ['sys.manage_jobs', 'sys.view_jobs', 'sys.jobs'];
|
|
28
|
+
/** jobs.Job SAVE/DELETE_PERMS + every `requires_global_perms` write control. */
|
|
29
|
+
export const JOBS_MANAGE_PERMS = ['sys.manage_jobs', 'sys.jobs'];
|
|
30
|
+
/** ScheduledTask/TaskResult VIEW_PERMS minus `owner` (see note above). */
|
|
31
|
+
export const SCHEDULED_TASK_VIEW_PERMS = ['sys.jobs', 'sys.view_scheduled_tasks'];
|
|
32
|
+
/** ScheduledTask SAVE/DELETE_PERMS minus `owner`. */
|
|
33
|
+
export const SCHEDULED_TASK_MANAGE_PERMS = ['sys.jobs', 'sys.manage_scheduled_tasks'];
|
|
34
|
+
|
|
35
|
+
// ── Row types ─────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export type JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'canceled' | 'expired';
|
|
38
|
+
|
|
39
|
+
export const JOB_STATUSES: readonly JobStatus[] = [
|
|
40
|
+
'pending', 'running', 'completed', 'failed', 'canceled', 'expired',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const JOB_STATUS_OPTIONS = JOB_STATUSES.map((value) => ({
|
|
44
|
+
value,
|
|
45
|
+
label: value.charAt(0).toUpperCase() + value.slice(1),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
/** jobs.Job `default`/`detail` graph + the `duration_ms` extra. */
|
|
49
|
+
export interface JobRow {
|
|
50
|
+
id: string;
|
|
51
|
+
channel: string;
|
|
52
|
+
func: string;
|
|
53
|
+
payload: Record<string, unknown>;
|
|
54
|
+
status: JobStatus;
|
|
55
|
+
run_at: number | null;
|
|
56
|
+
expires_at: number | null;
|
|
57
|
+
attempt: number;
|
|
58
|
+
max_retries: number;
|
|
59
|
+
broadcast: boolean;
|
|
60
|
+
cancel_requested: boolean;
|
|
61
|
+
max_exec_seconds: number | null;
|
|
62
|
+
runner_id: string | null;
|
|
63
|
+
last_error: string;
|
|
64
|
+
metadata: Record<string, unknown>;
|
|
65
|
+
created: number;
|
|
66
|
+
modified: number;
|
|
67
|
+
started_at: number | null;
|
|
68
|
+
finished_at: number | null;
|
|
69
|
+
duration_ms: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type JobEventKind =
|
|
73
|
+
| 'created' | 'queued' | 'scheduled' | 'running' | 'retry'
|
|
74
|
+
| 'canceled' | 'completed' | 'failed' | 'expired' | 'claimed' | 'released';
|
|
75
|
+
|
|
76
|
+
export const JOB_EVENT_KINDS: readonly JobEventKind[] = [
|
|
77
|
+
'created', 'queued', 'scheduled', 'running', 'retry',
|
|
78
|
+
'canceled', 'completed', 'failed', 'expired', 'claimed', 'released',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** jobs.JobEvent `default` graph (`detail` additionally carries job_id/channel). */
|
|
82
|
+
export interface JobEventRow {
|
|
83
|
+
id: number;
|
|
84
|
+
event: JobEventKind;
|
|
85
|
+
at: number;
|
|
86
|
+
runner_id: string | null;
|
|
87
|
+
attempt: number;
|
|
88
|
+
details: Record<string, unknown>;
|
|
89
|
+
job_id?: string;
|
|
90
|
+
channel?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* jobs.JobEvent `timeline` graph — {event, at, runner_id, details}. It has NO
|
|
95
|
+
* `id`, which is exactly why it is a separate type: a timeline entry cannot be
|
|
96
|
+
* fed to anything that keys on a row id.
|
|
97
|
+
*/
|
|
98
|
+
export interface JobTimelineEntry {
|
|
99
|
+
event: JobEventKind;
|
|
100
|
+
at: number;
|
|
101
|
+
runner_id: string | null;
|
|
102
|
+
details: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type JobLogKind = 'debug' | 'info' | 'warn' | 'error';
|
|
106
|
+
|
|
107
|
+
export const JOB_LOG_KIND_OPTIONS: { value: JobLogKind; label: string }[] = [
|
|
108
|
+
{ value: 'debug', label: 'Debug' },
|
|
109
|
+
{ value: 'info', label: 'Info' },
|
|
110
|
+
{ value: 'warn', label: 'Warn' },
|
|
111
|
+
{ value: 'error', label: 'Error' },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/** jobs.JobLog `default` graph (`detail` adds channel + meta). */
|
|
115
|
+
export interface JobLogRow {
|
|
116
|
+
id: number;
|
|
117
|
+
job_id: string;
|
|
118
|
+
created: number;
|
|
119
|
+
kind: JobLogKind;
|
|
120
|
+
message: string;
|
|
121
|
+
channel?: string;
|
|
122
|
+
meta?: Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A runner heartbeat as JobEngine._heartbeat_loop writes it into Redis, plus
|
|
127
|
+
* the `alive` flag manager.get_runners() derives from the heartbeat age. ISO
|
|
128
|
+
* strings, not epoch seconds — this payload never touches the serializer.
|
|
129
|
+
* There is no `version` field (web-mojo rendered a chip for one anyway).
|
|
130
|
+
*/
|
|
131
|
+
export interface RunnerHeartbeat {
|
|
132
|
+
runner_id: string;
|
|
133
|
+
hostname: string;
|
|
134
|
+
channels: string[];
|
|
135
|
+
jobs_processed: number;
|
|
136
|
+
jobs_failed: number;
|
|
137
|
+
started: string | null;
|
|
138
|
+
last_heartbeat: string | null;
|
|
139
|
+
alive: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `GET /api/jobs/runners` rows — the heartbeat with `id` stamped on. */
|
|
143
|
+
export interface RunnerRow extends RunnerHeartbeat {
|
|
144
|
+
id: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `mojo/helpers/sysinfo.get_host_info()` — every key optional; psutil may be absent. */
|
|
148
|
+
export interface RunnerHostInfo {
|
|
149
|
+
time?: number;
|
|
150
|
+
datetime?: string;
|
|
151
|
+
os?: {
|
|
152
|
+
system?: string;
|
|
153
|
+
version?: string;
|
|
154
|
+
hostname?: string;
|
|
155
|
+
release?: string;
|
|
156
|
+
processor?: string;
|
|
157
|
+
machine?: string;
|
|
158
|
+
};
|
|
159
|
+
boot_time?: number;
|
|
160
|
+
cpu_load?: number;
|
|
161
|
+
cpus_load?: number[];
|
|
162
|
+
memory?: { total?: number; used?: number; available?: number; percent?: number };
|
|
163
|
+
disk?: { total?: number; used?: number; free?: number; percent?: number };
|
|
164
|
+
cpu?: { count?: number; freq?: { current?: number; min?: number; max?: number } | null };
|
|
165
|
+
network?: {
|
|
166
|
+
tcp_cons?: number;
|
|
167
|
+
bytes_sent?: number;
|
|
168
|
+
bytes_recv?: number;
|
|
169
|
+
packets_sent?: number;
|
|
170
|
+
packets_recv?: number;
|
|
171
|
+
errin?: number;
|
|
172
|
+
errout?: number;
|
|
173
|
+
dropin?: number;
|
|
174
|
+
dropout?: number;
|
|
175
|
+
};
|
|
176
|
+
users?: unknown[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One runner's reply to the sysinfo broadcast-execute. The INNER `status` is
|
|
181
|
+
* the runner's own success/failure — a reply with `status: 'error'` arrives
|
|
182
|
+
* inside a successful HTTP envelope, so it must be handled as a distinct
|
|
183
|
+
* state rather than as a request failure.
|
|
184
|
+
*/
|
|
185
|
+
export interface RunnerSysinfo {
|
|
186
|
+
runner_id: string;
|
|
187
|
+
func?: string;
|
|
188
|
+
status: 'success' | 'error';
|
|
189
|
+
timestamp?: string;
|
|
190
|
+
result?: RunnerHostInfo;
|
|
191
|
+
error?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** One channel's slice of `GET /api/jobs/stats` (manager.get_queue_state + db_running). */
|
|
195
|
+
export interface JobChannelState {
|
|
196
|
+
channel: string;
|
|
197
|
+
queued_count: number;
|
|
198
|
+
inflight_count: number;
|
|
199
|
+
scheduled_count: number;
|
|
200
|
+
/** ALIVE runners serving this channel. */
|
|
201
|
+
runners: number;
|
|
202
|
+
db_running: number;
|
|
203
|
+
metrics?: {
|
|
204
|
+
jobs_per_minute?: number;
|
|
205
|
+
success_rate?: number;
|
|
206
|
+
avg_duration_ms?: number;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface JobsStatsTotals {
|
|
211
|
+
/** Backwards-compatible alias the backend keeps for `queued`. */
|
|
212
|
+
pending: number;
|
|
213
|
+
queued: number;
|
|
214
|
+
inflight: number;
|
|
215
|
+
running: number;
|
|
216
|
+
running_active: number;
|
|
217
|
+
running_stale: number;
|
|
218
|
+
completed: number;
|
|
219
|
+
failed: number;
|
|
220
|
+
scheduled: number;
|
|
221
|
+
runners_active: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** `GET /api/jobs/stats` — the whole dashboard reads from this one call. */
|
|
225
|
+
export interface JobsStats {
|
|
226
|
+
channels: Record<string, JobChannelState>;
|
|
227
|
+
runners: RunnerHeartbeat[];
|
|
228
|
+
totals: JobsStatsTotals;
|
|
229
|
+
scheduler: { active: boolean; lock_holder: string | null };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** `GET /api/jobs/control/queue-sizes` — per-channel Redis + DB status counts. */
|
|
233
|
+
export interface JobQueueSize {
|
|
234
|
+
stream: number;
|
|
235
|
+
scheduled: number;
|
|
236
|
+
db_pending: number;
|
|
237
|
+
db_running: number;
|
|
238
|
+
db_completed: number;
|
|
239
|
+
db_failed: number;
|
|
240
|
+
db_canceled: number;
|
|
241
|
+
db_expired: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `GET /api/jobs/control/config` (manage-gated) — the engine's runtime settings. */
|
|
245
|
+
export interface JobsConfig {
|
|
246
|
+
redis_url: string;
|
|
247
|
+
redis_prefix: string;
|
|
248
|
+
engine: Record<string, number>;
|
|
249
|
+
defaults: Record<string, string | number>;
|
|
250
|
+
limits: Record<string, number>;
|
|
251
|
+
timeouts: Record<string, number>;
|
|
252
|
+
channels: string[];
|
|
253
|
+
allowed_channels: string[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type ScheduledTaskType = 'job' | 'webhook' | 'llm';
|
|
257
|
+
|
|
258
|
+
export const SCHEDULED_TASK_TYPE_OPTIONS: { value: ScheduledTaskType; label: string }[] = [
|
|
259
|
+
{ value: 'job', label: 'Job' },
|
|
260
|
+
{ value: 'webhook', label: 'Webhook' },
|
|
261
|
+
{ value: 'llm', label: 'LLM' },
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
/** Mon=0 — ScheduledTask.run_days uses Python's weekday(), not Django's. */
|
|
265
|
+
export const WEEKDAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const;
|
|
266
|
+
|
|
267
|
+
/** ScheduledTask._validate's exact notify allowlist. */
|
|
268
|
+
export const NOTIFY_CHANNELS = ['email', 'in_app', 'sms', 'push'] as const;
|
|
269
|
+
|
|
270
|
+
/** jobs.ScheduledTask `default` graph (`list` is a narrower subset of it). */
|
|
271
|
+
export interface ScheduledTaskRow {
|
|
272
|
+
id: string;
|
|
273
|
+
name: string;
|
|
274
|
+
description: string;
|
|
275
|
+
enabled: boolean;
|
|
276
|
+
run_once: boolean;
|
|
277
|
+
task_type: ScheduledTaskType;
|
|
278
|
+
run_times: string[];
|
|
279
|
+
run_days: number[];
|
|
280
|
+
job_config: Record<string, unknown>;
|
|
281
|
+
notify: string[];
|
|
282
|
+
channel: string;
|
|
283
|
+
max_retries: number;
|
|
284
|
+
last_run: number | null;
|
|
285
|
+
run_count: number;
|
|
286
|
+
last_error: string;
|
|
287
|
+
created: number;
|
|
288
|
+
modified: number;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** jobs.TaskResult `default` graph. Read-only over REST (SAVE_PERMS is empty). */
|
|
292
|
+
export interface TaskResultRow {
|
|
293
|
+
id: string;
|
|
294
|
+
task_id: string;
|
|
295
|
+
job_id: string | null;
|
|
296
|
+
status: 'success' | 'error';
|
|
297
|
+
output: string;
|
|
298
|
+
error: string;
|
|
299
|
+
created: number;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── List-param normalization ──────────────────────────────────────────
|
|
303
|
+
// Two jobs invariants live here:
|
|
304
|
+
// 1. Every list ALWAYS sends an explicit `sort`. The backend's default is
|
|
305
|
+
// `-id`, and Job/ScheduledTask ids are uuid hex — an unsorted job list is
|
|
306
|
+
// lexicographic noise, not "newest first".
|
|
307
|
+
// 2. `graph` and `download_format` never survive from the URL. A graph
|
|
308
|
+
// override could widen the serialized field set past what a surface is
|
|
309
|
+
// meant to show; export format is chosen by the export path, not by a
|
|
310
|
+
// link someone pasted.
|
|
311
|
+
|
|
312
|
+
const COMMON_PARAMS = new Set(['start', 'size', 'sort', 'search', 'dr_field', 'dr_start', 'dr_end']);
|
|
313
|
+
|
|
314
|
+
const JOB_FILTERS = new Set([
|
|
315
|
+
'id', 'id__in', 'channel', 'channel__in', 'func', 'func__icontains', 'func__startswith',
|
|
316
|
+
'status', 'status__in', 'status__not', 'runner_id', 'runner_id__in', 'runner_id__isnull',
|
|
317
|
+
'run_at__isnull', 'run_at__gte', 'run_at__lte', 'broadcast', 'cancel_requested',
|
|
318
|
+
'attempt', 'attempt__gte', 'created__gte', 'created__lte',
|
|
319
|
+
'started_at__gte', 'started_at__lte', 'finished_at__gte', 'finished_at__lte',
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const JOB_EVENT_FILTERS = new Set([
|
|
323
|
+
'id', 'job', 'job__in', 'channel', 'event', 'event__in', 'event__not',
|
|
324
|
+
'runner_id', 'attempt', 'at__gte', 'at__lte',
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
// JobLog has NO `runner_id` field, and `build_rest_filters` SILENTLY DROPS
|
|
328
|
+
// unknown keys (`hasattr(cls, field_name)`) — `?job_id=…` and `?runner_id=…`
|
|
329
|
+
// return the UNFILTERED list rather than an error. Only the relation name
|
|
330
|
+
// works, so only the relation name is allowed through.
|
|
331
|
+
const JOB_LOG_FILTERS = new Set([
|
|
332
|
+
'id', 'job', 'job__in', 'channel', 'kind', 'kind__in', 'created__gte', 'created__lte',
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const SCHEDULED_TASK_FILTERS = new Set([
|
|
336
|
+
'id', 'name__icontains', 'enabled', 'run_once', 'task_type', 'task_type__in',
|
|
337
|
+
'channel', 'run_count__gte', 'last_run__gte', 'last_run__isnull', 'created__gte', 'created__lte',
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
const TASK_RESULT_FILTERS = new Set([
|
|
341
|
+
'id', 'task', 'task__in', 'job', 'status', 'status__in', 'created__gte', 'created__lte',
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
function normalizeJobsParams(params: Params, allowed: Set<string>, fallbackSort: string): Params {
|
|
345
|
+
const out: Params = {};
|
|
346
|
+
for (const [key, value] of Object.entries(params)) {
|
|
347
|
+
if (key === 'graph' || key === 'download_format') continue;
|
|
348
|
+
if (!COMMON_PARAMS.has(key) && !allowed.has(key)) continue;
|
|
349
|
+
if (value == null || value === '') continue;
|
|
350
|
+
out[key] = value;
|
|
351
|
+
}
|
|
352
|
+
out.sort = String(out.sort ?? fallbackSort);
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const normalizeJobListParams = (params: Params): Params =>
|
|
357
|
+
normalizeJobsParams(params, JOB_FILTERS, '-created');
|
|
358
|
+
export const normalizeJobEventListParams = (params: Params): Params =>
|
|
359
|
+
normalizeJobsParams(params, JOB_EVENT_FILTERS, '-at');
|
|
360
|
+
export const normalizeJobLogListParams = (params: Params): Params =>
|
|
361
|
+
normalizeJobsParams(params, JOB_LOG_FILTERS, '-created');
|
|
362
|
+
export const normalizeScheduledTaskListParams = (params: Params): Params =>
|
|
363
|
+
normalizeJobsParams(params, SCHEDULED_TASK_FILTERS, '-created');
|
|
364
|
+
export const normalizeTaskResultListParams = (params: Params): Params =>
|
|
365
|
+
normalizeJobsParams(params, TASK_RESULT_FILTERS, '-created');
|
|
366
|
+
|
|
367
|
+
// ── Model definitions ─────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* `cancel_request` and `retry_request` are POST_SAVE_ACTIONS, and django-mojo
|
|
371
|
+
* returns a handler's dict VERBATIM as the response body (rest.py
|
|
372
|
+
* on_rest_save_and_respond → `JsonResponse(resp)`) — not the row, not nested
|
|
373
|
+
* under `data`, and at HTTP 200 even when the handler failed. The client's
|
|
374
|
+
* `status === false` unwrap is what turns a refusal into a rejection, so both
|
|
375
|
+
* actions declare `response: 'payload'`.
|
|
376
|
+
*/
|
|
377
|
+
export const JobModel = defineModel<JobRow>({
|
|
378
|
+
name: 'job',
|
|
379
|
+
endpoint: '/api/jobs/job',
|
|
380
|
+
permissions: {
|
|
381
|
+
view: JOBS_VIEW_PERMS,
|
|
382
|
+
manage: JOBS_MANAGE_PERMS,
|
|
383
|
+
delete: JOBS_MANAGE_PERMS,
|
|
384
|
+
},
|
|
385
|
+
actions: {
|
|
386
|
+
// Python treats `{}` as falsy, so an argument-less call would fail with
|
|
387
|
+
// "cancel_request must be true" — callers MUST pass `payload: true`.
|
|
388
|
+
cancel_request: { response: 'payload', permissions: JOBS_MANAGE_PERMS },
|
|
389
|
+
// `true`, or `{retry: true, delay: N}`. ALWAYS republishes a new job.
|
|
390
|
+
retry_request: { response: 'payload', permissions: JOBS_MANAGE_PERMS },
|
|
391
|
+
},
|
|
392
|
+
normalizeListParams: normalizeJobListParams,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
export const JobEventModel = defineModel<JobEventRow>({
|
|
396
|
+
name: 'job_event',
|
|
397
|
+
endpoint: '/api/jobs/event',
|
|
398
|
+
// SAVE_PERMS is empty on the backend — events are system-created only.
|
|
399
|
+
permissions: { view: JOBS_VIEW_PERMS, delete: JOBS_MANAGE_PERMS },
|
|
400
|
+
normalizeListParams: normalizeJobEventListParams,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
export const JobLogModel = defineModel<JobLogRow>({
|
|
404
|
+
name: 'job_log',
|
|
405
|
+
endpoint: '/api/jobs/logs',
|
|
406
|
+
// SAVE_PERMS is empty — logs are written via Job.add_log.
|
|
407
|
+
permissions: { view: JOBS_VIEW_PERMS, delete: JOBS_MANAGE_PERMS },
|
|
408
|
+
normalizeListParams: normalizeJobLogListParams,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
export const ScheduledTaskModel = defineModel<ScheduledTaskRow>({
|
|
412
|
+
name: 'scheduled_task',
|
|
413
|
+
endpoint: '/api/jobs/scheduled_task',
|
|
414
|
+
permissions: {
|
|
415
|
+
view: SCHEDULED_TASK_VIEW_PERMS,
|
|
416
|
+
manage: SCHEDULED_TASK_MANAGE_PERMS,
|
|
417
|
+
delete: SCHEDULED_TASK_MANAGE_PERMS,
|
|
418
|
+
},
|
|
419
|
+
normalizeListParams: normalizeScheduledTaskListParams,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
export const TaskResultModel = defineModel<TaskResultRow>({
|
|
423
|
+
name: 'task_result',
|
|
424
|
+
endpoint: '/api/jobs/task_result',
|
|
425
|
+
permissions: { view: SCHEDULED_TASK_VIEW_PERMS, delete: SCHEDULED_TASK_MANAGE_PERMS },
|
|
426
|
+
normalizeListParams: normalizeTaskResultListParams,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ── Derived helpers ───────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
const JOB_STATUS_TONE: Record<JobStatus, Tone> = {
|
|
432
|
+
pending: 'info',
|
|
433
|
+
running: 'primary',
|
|
434
|
+
completed: 'success',
|
|
435
|
+
failed: 'danger',
|
|
436
|
+
canceled: 'muted',
|
|
437
|
+
expired: 'warning',
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const JOB_STATUS_ICON: Record<JobStatus, string> = {
|
|
441
|
+
pending: 'bi-hourglass',
|
|
442
|
+
running: 'bi-play-circle',
|
|
443
|
+
completed: 'bi-check-circle',
|
|
444
|
+
failed: 'bi-x-octagon',
|
|
445
|
+
canceled: 'bi-slash-circle',
|
|
446
|
+
expired: 'bi-clock-history',
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/** Unknown wire values fall back to muted WITH a warn — never to nothing. */
|
|
450
|
+
export function jobStatusTone(status: string): Tone {
|
|
451
|
+
const tone = JOB_STATUS_TONE[status as JobStatus];
|
|
452
|
+
if (tone) return tone;
|
|
453
|
+
console.warn(`[admin/jobs] unknown job status ${JSON.stringify(status)} — falling back to "muted". Valid: ${JOB_STATUSES.join(', ')}`);
|
|
454
|
+
return 'muted';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function jobStatusIcon(status: string): string {
|
|
458
|
+
const icon = JOB_STATUS_ICON[status as JobStatus];
|
|
459
|
+
if (icon) return icon;
|
|
460
|
+
console.warn(`[admin/jobs] unknown job status ${JSON.stringify(status)} — falling back to the generic icon.`);
|
|
461
|
+
return 'bi-question-circle';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Terminal states, per `Job.is_terminal`. */
|
|
465
|
+
export function isTerminalJob(job: Pick<JobRow, 'status'>): boolean {
|
|
466
|
+
return job.status === 'completed' || job.status === 'failed'
|
|
467
|
+
|| job.status === 'canceled' || job.status === 'expired';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Pending WITH a run_at — the Scheduled segment, not the Queued one. */
|
|
471
|
+
export function isScheduledJob(job: Pick<JobRow, 'status' | 'run_at'>): boolean {
|
|
472
|
+
return job.status === 'pending' && job.run_at != null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** A scheduled job whose run_at has passed and which still has not run. */
|
|
476
|
+
export function isOverdueJob(job: Pick<JobRow, 'status' | 'run_at'>, now = Date.now()): boolean {
|
|
477
|
+
return isScheduledJob(job) && job.run_at! * 1000 < now;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* JobActionsService.cancel_job refuses a terminal job, so the control is not
|
|
482
|
+
* offered for one. It is NOT `!cancel_requested` — re-requesting a cancel on a
|
|
483
|
+
* running job is legal and is how a cooperative cancel gets nudged again.
|
|
484
|
+
*/
|
|
485
|
+
export function canCancelJob(job: Pick<JobRow, 'status'>): boolean {
|
|
486
|
+
return !isTerminalJob(job);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The SERVICE rule (`status not in ('failed','canceled','expired')` → refuse),
|
|
491
|
+
* which is broader than the model's `is_retriable` property (failed AND
|
|
492
|
+
* attempt < max_retries). The service is what the endpoint runs.
|
|
493
|
+
*/
|
|
494
|
+
export function canRetryJob(job: Pick<JobRow, 'status'>): boolean {
|
|
495
|
+
return job.status === 'failed' || job.status === 'canceled' || job.status === 'expired';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export type RunnerHealthKey = 'healthy' | 'stale' | 'down';
|
|
499
|
+
|
|
500
|
+
export interface RunnerHealth {
|
|
501
|
+
key: RunnerHealthKey;
|
|
502
|
+
label: string;
|
|
503
|
+
tone: Tone;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Seconds since an ISO heartbeat, or null when there has never been one. */
|
|
507
|
+
export function heartbeatAgeSeconds(iso: string | null | undefined, now = Date.now()): number | null {
|
|
508
|
+
if (!iso) return null;
|
|
509
|
+
const parsed = Date.parse(iso);
|
|
510
|
+
if (!Number.isFinite(parsed)) return null;
|
|
511
|
+
return (now - parsed) / 1000;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function formatHeartbeatAge(ageSec: number | null): string {
|
|
515
|
+
if (ageSec == null) return 'never';
|
|
516
|
+
if (ageSec < 60) return `${Math.round(ageSec)}s ago`;
|
|
517
|
+
if (ageSec < 3600) return `${Math.round(ageSec / 60)}m ago`;
|
|
518
|
+
return `${Math.round(ageSec / 3600)}h ago`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Fleet health tiers, carried from web-mojo RunnerDetailsView: the backend's
|
|
523
|
+
* own `alive` flag first (heartbeat within 3× the interval), then a slow
|
|
524
|
+
* (≥30s) / stale (≥120s) split so a fading runner reads as a warning before
|
|
525
|
+
* Redis expires its key.
|
|
526
|
+
*/
|
|
527
|
+
export function runnerHealth(runner: Pick<RunnerHeartbeat, 'alive' | 'last_heartbeat'>, now = Date.now()): RunnerHealth {
|
|
528
|
+
if (!runner.alive) return { key: 'down', label: 'Down', tone: 'danger' };
|
|
529
|
+
const ageSec = heartbeatAgeSeconds(runner.last_heartbeat, now);
|
|
530
|
+
if (ageSec == null) return { key: 'stale', label: 'No heartbeat', tone: 'warning' };
|
|
531
|
+
if (ageSec >= 120) return { key: 'stale', label: 'Stale heartbeat', tone: 'warning' };
|
|
532
|
+
if (ageSec >= 30) return { key: 'stale', label: 'Slow heartbeat', tone: 'warning' };
|
|
533
|
+
return { key: 'healthy', label: 'Healthy', tone: 'success' };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function formatUptime(seconds: number): string {
|
|
537
|
+
const d = Math.floor(seconds / 86400);
|
|
538
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
539
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
540
|
+
if (d > 0) return `${d}d ${h}h ${m}m`;
|
|
541
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
542
|
+
return `${m}m`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function formatUptimeShort(seconds: number): string {
|
|
546
|
+
const d = Math.floor(seconds / 86400);
|
|
547
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
548
|
+
if (d > 0) return `${d}d`;
|
|
549
|
+
if (h > 0) return `${h}h`;
|
|
550
|
+
return `${Math.floor((seconds % 3600) / 60)}m`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Seconds since the runner's ISO `started`, or null when it is unknown. */
|
|
554
|
+
export function runnerUptimeSeconds(runner: Pick<RunnerHeartbeat, 'started'>, now = Date.now()): number | null {
|
|
555
|
+
if (!runner.started) return null;
|
|
556
|
+
const parsed = Date.parse(runner.started);
|
|
557
|
+
if (!Number.isFinite(parsed)) return null;
|
|
558
|
+
const seconds = (now - parsed) / 1000;
|
|
559
|
+
return seconds >= 0 ? seconds : null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** `failed / processed` as a percentage; null when nothing has been processed. */
|
|
563
|
+
export function runnerFailureRate(runner: Pick<RunnerHeartbeat, 'jobs_processed' | 'jobs_failed'>): number | null {
|
|
564
|
+
const processed = runner.jobs_processed || 0;
|
|
565
|
+
if (processed <= 0) return null;
|
|
566
|
+
return ((runner.jobs_failed || 0) / processed) * 100;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export type ChannelSeverity = 'healthy' | 'warning' | 'critical';
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Backlog thresholds carried verbatim from web-mojo JobHealthView: >50 queued
|
|
573
|
+
* plus in-flight warns, >100 is critical — and ZERO alive runners is critical
|
|
574
|
+
* regardless of depth, because nothing will drain it.
|
|
575
|
+
*/
|
|
576
|
+
export function channelSeverity(state: Pick<JobChannelState, 'queued_count' | 'inflight_count' | 'runners'>): ChannelSeverity {
|
|
577
|
+
const backlog = (state.queued_count || 0) + (state.inflight_count || 0);
|
|
578
|
+
if (backlog > 100 || state.runners === 0) return 'critical';
|
|
579
|
+
if (backlog > 50) return 'warning';
|
|
580
|
+
return 'healthy';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function channelSeverityTone(severity: ChannelSeverity): Tone {
|
|
584
|
+
return severity === 'critical' ? 'danger' : severity === 'warning' ? 'warning' : 'success';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Mon=0 weekday ints → "Mon · Wed · Fri", or "Every day" when empty. */
|
|
588
|
+
export function formatRunDays(days: readonly number[]): string {
|
|
589
|
+
if (!days.length) return 'Every day';
|
|
590
|
+
return [...days]
|
|
591
|
+
.filter((day) => Number.isInteger(day) && day >= 0 && day <= 6)
|
|
592
|
+
.sort((a, b) => a - b)
|
|
593
|
+
.map((day) => WEEKDAY_LABELS[day])
|
|
594
|
+
.join(' · ') || 'Every day';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function formatRunTimes(times: readonly string[]): string {
|
|
598
|
+
return times.length ? times.join(' · ') : 'No times set';
|
|
599
|
+
}
|