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,103 @@
|
|
|
1
|
+
# Admin Users
|
|
2
|
+
|
|
3
|
+
Reusable global User administration for django-mojo. Consumers must include
|
|
4
|
+
the semantic `user-admin.css` stylesheet used by the portal and showcase.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import {
|
|
8
|
+
UsersPage, UserDetail, UserModel, ApiKeyModel,
|
|
9
|
+
USERS_ADMIN_SECTION, USER_VIEW_PERMISSIONS, USER_MANAGE_PERMISSIONS,
|
|
10
|
+
} from 'portal-mojo/admin';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`USERS_ADMIN_SECTION` is root-relative: `/users` in standalone Admin and
|
|
14
|
+
`/system/users` when embedded. Both the section and route are system-pinned:
|
|
15
|
+
view requires `sys.users | sys.view_users | sys.manage_users`; mutations
|
|
16
|
+
require `sys.users | sys.manage_users`. Active-group membership never satisfies
|
|
17
|
+
either clause, and Admin remains a no-group workspace.
|
|
18
|
+
|
|
19
|
+
## Table and detail inventory
|
|
20
|
+
|
|
21
|
+
`UsersPage` keeps server-owned URL params, search, filters, presets, sorting,
|
|
22
|
+
paging, chooser, persistence, export, expansion, recency grouping, selection,
|
|
23
|
+
create, and state-aware batch disable/reactivate. Disable collects one required
|
|
24
|
+
reason per batch; rows already in the target state are ineligible.
|
|
25
|
+
|
|
26
|
+
The detail preserves all 14 sections: Overview, Profile, Personal, Security,
|
|
27
|
+
OAuth, Groups, Sys Perms, conditional App Perms, API Keys, Devices, Logins,
|
|
28
|
+
Audit, Notifications, and Metadata. Subsidiary count/list queries are enabled
|
|
29
|
+
only when their exact system permission is present. Logs use
|
|
30
|
+
`sys.view_logs | sys.manage_logs | sys.security`; incident events use
|
|
31
|
+
`sys.view_security | sys.security`. Permission revocation therefore removes
|
|
32
|
+
the section and prevents background requests.
|
|
33
|
+
|
|
34
|
+
Pass `onOpenGroup(groupId)` to `UsersPage` or `UserDetail` to connect an
|
|
35
|
+
app-owned Group detail. Without it, organization and membership identities are
|
|
36
|
+
plain text—never false links.
|
|
37
|
+
|
|
38
|
+
**Logins ships a Map tab (#1291, shipped).** The section is `Map | Logins`;
|
|
39
|
+
the Map renders `<LoginLocationMap userId={user.id} height={280} />` over
|
|
40
|
+
`/api/account/logins/user?user_id=`, and it is present only when
|
|
41
|
+
`useCan(LOGIN_SUMMARY_PERMS)` passes — absent, it issues no request at all.
|
|
42
|
+
Login rows are clickable and open `showLoginEventDetail`. The dead
|
|
43
|
+
`loginTone(event_type)` map is gone: `UserLoginEvent` has never had an
|
|
44
|
+
`event_type` field, so every dot rendered muted. Tone now comes from
|
|
45
|
+
`loginRiskTone(row)` (`is_new_country` / `is_new_region`). Device rows are
|
|
46
|
+
clickable too and open the full device dossier. See
|
|
47
|
+
[admin-devices-geoip.md](admin-devices-geoip.md).
|
|
48
|
+
|
|
49
|
+
The device / device-location / login-event models are DEFINED in
|
|
50
|
+
`admin/security/devices/models.ts` and re-exported here — one `defineModel`
|
|
51
|
+
per endpoint, so `UserDetail` and the fleet-wide tables share one cache.
|
|
52
|
+
|
|
53
|
+
## Action contracts
|
|
54
|
+
|
|
55
|
+
- Username changes use the `change_username` POST_SAVE_ACTION, not a direct
|
|
56
|
+
field save. Model saves/actions retry one fresh-auth 440 automatically;
|
|
57
|
+
combined `disable_totp` writes explicitly use `withFreshAuth`.
|
|
58
|
+
- Send invite, password reset, magic link, and verification email controls all
|
|
59
|
+
require the User mutation tier even though public send responses
|
|
60
|
+
anti-enumerate recipients.
|
|
61
|
+
- Disable rejects inactive users; reactivate rejects active users. There is no
|
|
62
|
+
inactivity-warning reset action for an active account.
|
|
63
|
+
- API-key inventory/revoke uses `/api/account/api_keys`. Generation appears
|
|
64
|
+
only for the signed-in caller and calls `/api/auth/generate_api_key`.
|
|
65
|
+
`/api/auth/manage/generate_api_key` does not exist.
|
|
66
|
+
- Key generation splits the raw token inside the pending mutation, invokes a
|
|
67
|
+
transient reveal callback, deletes `token`, `jti`, `auth_key`, and secret
|
|
68
|
+
aliases, and resolves MutationCache with non-secret receipt metadata only.
|
|
69
|
+
- Notification preferences are interactive only for the signed-in caller;
|
|
70
|
+
django-mojo ignores a target `user` parameter. Other-user details render an
|
|
71
|
+
explicit unavailable state and make no request.
|
|
72
|
+
|
|
73
|
+
## Avatar uploads
|
|
74
|
+
|
|
75
|
+
Manage avatar uses the shared `ImageField`: it uploads into the acting admin's
|
|
76
|
+
personal File scope, then saves that completed positive numeric File id (or
|
|
77
|
+
null to clear) on the target User. This matches django-mojo #1488's
|
|
78
|
+
admin-on-behalf rule: the File remains owned by the uploader, and replace/clear
|
|
79
|
+
detach without deleting it. Success requires the authoritative returned avatar
|
|
80
|
+
relation to match the requested id/null exactly. A failed or mismatched owner
|
|
81
|
+
save retains the uploaded candidate for attachment-only retry and reports an
|
|
82
|
+
abandoned candidate without deleting it.
|
|
83
|
+
|
|
84
|
+
Avatar selection opts into the shared editor before that upload begins. It
|
|
85
|
+
starts on a square crop and emits an exact alpha-preserving 200×200 PNG. Cancel
|
|
86
|
+
or edit failure leaves the original local selection available for retry or
|
|
87
|
+
discard; this required editor path does not offer Use original. Only an editor save converts the Blob to the File passed
|
|
88
|
+
into the existing personal-scope queue, so editing cannot create a File id or
|
|
89
|
+
trigger owner attachment early.
|
|
90
|
+
|
|
91
|
+
UserModel and `useMe` independently reduce expanded avatar relations to `{id}`
|
|
92
|
+
before their separate Query caches. The modal resolves a stored preview only
|
|
93
|
+
imperatively while mounted; capability URLs do not enter Query/Mutation cache,
|
|
94
|
+
storage, logs, or errors. Transfer and attach-save intervals both block modal
|
|
95
|
+
dismissal, while auth/permission loss closes the modal and cancels its queue.
|
|
96
|
+
|
|
97
|
+
## Known seams
|
|
98
|
+
|
|
99
|
+
The login map and the richer device/GeoIP dossiers SHIPPED with #1291 and are documented
|
|
100
|
+
in [admin-devices-geoip.md](admin-devices-geoip.md). Arbitrary-user key
|
|
101
|
+
creation, arbitrary-user notification administration, and active-user
|
|
102
|
+
inactivity-warning clear require new backend contracts and are intentionally
|
|
103
|
+
absent.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Admin incidents and events
|
|
2
|
+
|
|
3
|
+
`IncidentsPage` and `EventsPage` extend the shared Security Operations section
|
|
4
|
+
at `security/incidents` and `security/events`; Tickets remain at
|
|
5
|
+
`security/tickets`.
|
|
6
|
+
|
|
7
|
+
## Permissions and contracts
|
|
8
|
+
|
|
9
|
+
Routes are global and system-pinned. View is `sys.view_security | sys.security`;
|
|
10
|
+
incident save, merge, protection, and history create use
|
|
11
|
+
`sys.manage_security | sys.security`; delete uses `sys.manage_security`.
|
|
12
|
+
Events remain immutable in this forensic UI, and event/history deletes are
|
|
13
|
+
denied by Django.
|
|
14
|
+
|
|
15
|
+
Incident comments require text and may atomically add one completed existing
|
|
16
|
+
File id as `media`. The upload queue is capacity one and derives its immutable
|
|
17
|
+
group from the loaded parent incident, never ambient or request-supplied group
|
|
18
|
+
context. The completed File and manager must match that parent group; groupless
|
|
19
|
+
parents use personal storage with no selector. Removing a candidate only
|
|
20
|
+
detaches it from the composer and never deletes the File or history row.
|
|
21
|
+
|
|
22
|
+
Lists allowlist parameters and force `graph=default`; graph-qualified detail
|
|
23
|
+
keys request `graph=detailed`. Both models search only `details`, hence “Search
|
|
24
|
+
incident details” and “Search event details.” Incident lifecycle is `pending`,
|
|
25
|
+
`new`, `open`, `investigating`, `resolved`, `closed`, `ignored`; `state` remains
|
|
26
|
+
separate and legacy `qa` is omitted. Events stay sorted by created time for
|
|
27
|
+
contiguous day groups and use the backend severity bands (8+ danger, 4+ warning).
|
|
28
|
+
|
|
29
|
+
## Merge and sanitization
|
|
30
|
+
|
|
31
|
+
Merge is one selection-wide call: choose one selected target and send every
|
|
32
|
+
other ID once. Django moves source events/tickets, writes target history, and
|
|
33
|
+
deletes sources. Source history is not reparented. Conflicting Maestro links
|
|
34
|
+
surface Django's 409 unchanged, and every non-cancelled attempt refetches.
|
|
35
|
+
|
|
36
|
+
Rows, history, optimistic notes, mutation variables, curated copy surfaces, and
|
|
37
|
+
exports pass through a recursive non-mutating sanitizer. It redacts normalized
|
|
38
|
+
secret keys, auth/header blocks, JWT/Bearer values, sensitive URL/form params,
|
|
39
|
+
and parsed bodies; evidence/traces are explicitly bounded. Heuristic redaction
|
|
40
|
+
cannot guarantee arbitrary opaque prose, so raw JSON is not rendered or
|
|
41
|
+
exported. `createSafeExporter` sanitizes before accumulation and projects only
|
|
42
|
+
declared fields.
|
|
43
|
+
|
|
44
|
+
Before that recursive sanitizer runs, every history `media` value is positively
|
|
45
|
+
rebuilt to exactly `id`, `filename`, `content_type`, and `category`; URLs,
|
|
46
|
+
tokens, provider/manager details, transfer state, and browser File objects are
|
|
47
|
+
discarded rather than redacted.
|
|
48
|
+
|
|
49
|
+
Incident and event rows open the shared KISS `modal.detail` surface, composing
|
|
50
|
+
DetailView, StatusPanel, KnownFieldsCard, StackTraceView, and RecordFeed.
|
|
51
|
+
Incident→event and event→incident drill-ins stack a child detail modal; closing
|
|
52
|
+
the child reveals the parent. Ticket/rule/AI/network-response/dashboard controls
|
|
53
|
+
remain excluded. Bouncer discovery stays category-prefix plus MUID search
|
|
54
|
+
because its reporter writes MUID into incident details.
|
|
55
|
+
|
|
56
|
+
## Backend evidence
|
|
57
|
+
|
|
58
|
+
The contract was checked against django-mojo source rather than inferred from
|
|
59
|
+
the mock:
|
|
60
|
+
|
|
61
|
+
- `mojo/apps/incident/models/incident.py`: `Incident.RestMeta` search,
|
|
62
|
+
permissions and graphs; `POST_SAVE_ACTIONS`; `Incident.on_action_merge`.
|
|
63
|
+
- `mojo/apps/incident/models/event.py`: `Event.RestMeta` search, permissions,
|
|
64
|
+
graph, severity definition, and unsafe CSV metadata fields.
|
|
65
|
+
- `mojo/apps/incident/models/history.py`: `IncidentHistory.RestMeta`, including
|
|
66
|
+
view/save clauses, JSON replacement and `CAN_DELETE = False`.
|
|
67
|
+
- `mojo/apps/incident/rest/event.py`: incident, history and event route mounts.
|
|
68
|
+
|
|
69
|
+
No disposable live Django records were available during this batch build, so
|
|
70
|
+
mutation evidence is source contract plus the focused mock verifier. A closing
|
|
71
|
+
live pass should remain read-only unless disposable incidents exist.
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Admin: jobs engine
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
// section (registered in ADMIN_SECTIONS — no app wiring needed)
|
|
6
|
+
JOBS_ADMIN_SECTION,
|
|
7
|
+
// pages
|
|
8
|
+
JobDashboardPage, JobRunnersPage, JobsTablePage, ScheduledTasksPage,
|
|
9
|
+
// dashboard sections
|
|
10
|
+
JobRunnersStrip, JobChannelsPanel, JobThroughputSection, JobOperationsSection,
|
|
11
|
+
// KISS detail modals (#1425)
|
|
12
|
+
JobDetail, showJobDetail,
|
|
13
|
+
RunnerDetail, showRunnerDetail,
|
|
14
|
+
ScheduledTaskDetail, showScheduledTaskDetail,
|
|
15
|
+
openScheduledTaskEditor,
|
|
16
|
+
// models — ONE defineModel per endpoint
|
|
17
|
+
JobModel, JobEventModel, JobLogModel, ScheduledTaskModel, TaskResultModel,
|
|
18
|
+
// permission clauses
|
|
19
|
+
JOBS_VIEW_PERMS, JOBS_MANAGE_PERMS,
|
|
20
|
+
SCHEDULED_TASK_VIEW_PERMS, SCHEDULED_TASK_MANAGE_PERMS,
|
|
21
|
+
// control plane (one typed function per route)
|
|
22
|
+
fetchJobStats, fetchRunners, fetchAllRunnerSysinfo, fetchRunnerSysinfo,
|
|
23
|
+
fetchChannels, fetchQueueSizes, fetchJobsConfig,
|
|
24
|
+
pingRunner, shutdownRunner, broadcastCommand, BROADCAST_COMMANDS,
|
|
25
|
+
clearStuck, manualReclaim, purgeJobs, isPurgeDryRun, resetFailed,
|
|
26
|
+
clearQueue, cleanupConsumers, rebuildScheduled, forceSchedulerLead,
|
|
27
|
+
publishTestJob, publishTestSuite,
|
|
28
|
+
forEachChannel, summarizeChannelOutcomes,
|
|
29
|
+
// query hooks (every one `enabled`-gated on the matching useCan)
|
|
30
|
+
jobsKeys, useJobStats, useRunners, useDiscoveredChannels, useChannelOptions,
|
|
31
|
+
useQueueSizes, useJobsConfig, useRunnerSysinfo,
|
|
32
|
+
useJobTimeline, useJobLogs, useRunnerActiveJobs, useRunnerJobHistory, useRunnerJobLogs,
|
|
33
|
+
useTaskResults,
|
|
34
|
+
// derived helpers
|
|
35
|
+
jobStatusTone, jobStatusIcon, isTerminalJob, isScheduledJob, isOverdueJob,
|
|
36
|
+
canCancelJob, canRetryJob,
|
|
37
|
+
runnerHealth, heartbeatAgeSeconds, formatHeartbeatAge, formatUptime, formatUptimeShort,
|
|
38
|
+
runnerUptimeSeconds, runnerFailureRate,
|
|
39
|
+
channelSeverity, channelSeverityTone,
|
|
40
|
+
formatRunDays, formatRunTimes, formatSchedule, taskTypeTone,
|
|
41
|
+
// scheduled-task form helpers (the client mirror of the model's _validate)
|
|
42
|
+
emptyScheduledTaskDraft, scheduledTaskDraft,
|
|
43
|
+
validateScheduledTaskDraft, scheduledTaskChanges,
|
|
44
|
+
// table pieces
|
|
45
|
+
JOB_SEGMENTS, jobSegmentOf, jobColumns, JOB_EXPORTER, sanitizeJobRowForExport,
|
|
46
|
+
} from 'portal-mojo/admin';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Queue operations: is the runner fleet alive, which channel is backed up and
|
|
50
|
+
why, what is this job doing and why did it fail, retry or cancel it, clear
|
|
51
|
+
stuck work, purge history — plus the cron-style scheduled tasks whose backend
|
|
52
|
+
django-mojo has always shipped and whose admin page web-mojo never wired up.
|
|
53
|
+
|
|
54
|
+
Demo: showcase → Admin → **Jobs engine**. Styles:
|
|
55
|
+
`apps/{portal,showcase}/src/theme/admin-jobs.css` (byte-identical).
|
|
56
|
+
Verifier: `npm run verify:admin-jobs`.
|
|
57
|
+
|
|
58
|
+
Wire facts below come from django-mojo `mojo/apps/jobs/` (`models/job.py`,
|
|
59
|
+
`models/scheduled_task.py`, `models/task_result.py`, `rest/jobs.py`,
|
|
60
|
+
`rest/control.py`, `manager.py`, `job_engine.py`), which is authoritative
|
|
61
|
+
wherever web-mojo disagreed — and it disagreed a lot. See
|
|
62
|
+
[Backend corrections](#backend-corrections).
|
|
63
|
+
|
|
64
|
+
## Registration
|
|
65
|
+
|
|
66
|
+
`JOBS_ADMIN_SECTION` is in `ADMIN_SECTIONS`, so routes and the sidebar entry
|
|
67
|
+
are generated. **A mounting app edits nothing** — neither
|
|
68
|
+
`apps/portal/src/menus.ts` nor `apps/portal/src/pages/admin-routes.tsx`.
|
|
69
|
+
|
|
70
|
+
| | |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `id` / `basePath` | `jobs` / `jobs` (basePath defaults to id) |
|
|
73
|
+
| `navigationGroup` | `operations` |
|
|
74
|
+
| `icon` | `bi-cpu` |
|
|
75
|
+
| `permissions` | `['sys.manage_jobs', 'sys.view_jobs', 'sys.jobs', 'sys.view_scheduled_tasks']` — the ANY-OF union of the route gates |
|
|
76
|
+
|
|
77
|
+
Standalone mount → `#/jobs`, `#/jobs/runners`, `#/jobs/list`,
|
|
78
|
+
`#/jobs/scheduled-tasks`. Embedded (`mount: '/system'`) → the same paths under
|
|
79
|
+
`#/system/…`. The dashboard registers `path: ''`, so the section gets no
|
|
80
|
+
generated landing redirect: `#/jobs` **is** the dashboard.
|
|
81
|
+
|
|
82
|
+
## Permissions — and why the gates are NOT uniform
|
|
83
|
+
|
|
84
|
+
Every clause is `sys.`-pinned, so an identically named ACTIVE-GROUP member
|
|
85
|
+
grant can never light an operator-global surface, and every clause is
|
|
86
|
+
fail-closed while `me` is loading.
|
|
87
|
+
|
|
88
|
+
| Route | Page | Route gate | Backend source |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `jobs` | `JobDashboardPage` | `JOBS_VIEW_PERMS` | control plane `requires_global_perms('view_jobs','manage_jobs','jobs')` |
|
|
91
|
+
| `jobs/runners` | `JobRunnersPage` (+ `RunnerDetail`) | `JOBS_VIEW_PERMS` | same |
|
|
92
|
+
| `jobs/list` | `JobsTablePage` (+ `JobDetail`) | `JOBS_VIEW_PERMS` | `Job.VIEW_PERMS = ["view_jobs","manage_jobs","jobs"]` |
|
|
93
|
+
| `jobs/scheduled-tasks` | `ScheduledTasksPage` (+ detail + editor) | **`SCHEDULED_TASK_VIEW_PERMS`** | `ScheduledTask.VIEW_PERMS = ["jobs","view_scheduled_tasks","owner"]` |
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
JOBS_VIEW_PERMS = ['sys.manage_jobs', 'sys.view_jobs', 'sys.jobs'];
|
|
97
|
+
JOBS_MANAGE_PERMS = ['sys.manage_jobs', 'sys.jobs'];
|
|
98
|
+
SCHEDULED_TASK_VIEW_PERMS = ['sys.jobs', 'sys.view_scheduled_tasks'];
|
|
99
|
+
SCHEDULED_TASK_MANAGE_PERMS= ['sys.jobs', 'sys.manage_scheduled_tasks'];
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**The scheduled-tasks difference is load-bearing, not tidiness.**
|
|
103
|
+
`ScheduledTask.VIEW_PERMS` contains *neither* manage grant and *not*
|
|
104
|
+
`view_jobs`. Three consequences:
|
|
105
|
+
|
|
106
|
+
1. A `jobs.viewer` (holding only `view_jobs`) **cannot** read the system task
|
|
107
|
+
list. If the sidenav entry and the route reused `JOBS_VIEW_PERMS`, that
|
|
108
|
+
operator would get a menu entry that opens a page rendering a permission
|
|
109
|
+
notice. The route gate matches the page's own gate, so the entry is simply
|
|
110
|
+
absent for them.
|
|
111
|
+
2. `manage_scheduled_tasks` does **not** imply view (correction 13 below), so
|
|
112
|
+
`SCHEDULED_TASK_MANAGE_PERMS` alone opens nothing either — the page checks
|
|
113
|
+
view first and says so explicitly.
|
|
114
|
+
3. `owner` is deliberately **dropped** from both clauses. Keeping it would be
|
|
115
|
+
worse than useless: see correction 10.
|
|
116
|
+
|
|
117
|
+
Mutations gate separately — `JOBS_MANAGE_PERMS` for the jobs control plane and
|
|
118
|
+
job actions, `SCHEDULED_TASK_MANAGE_PERMS` for task writes. Every
|
|
119
|
+
permission-disabled query is `enabled: false`, so a view-only operator issues
|
|
120
|
+
**zero** denied requests rather than firing and catching 403s.
|
|
121
|
+
|
|
122
|
+
## The wire
|
|
123
|
+
|
|
124
|
+
### Model endpoints
|
|
125
|
+
|
|
126
|
+
`uses_model_security`, global-only — Job/JobEvent/JobLog have no group field.
|
|
127
|
+
|
|
128
|
+
| Endpoint | Verbs | Request params this module sends | Response |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `/api/jobs/job` | GET list | `status`, `status__in`, `channel`, `func__icontains`, `runner_id`, `run_at__isnull`, `dr_field/dr_start/dr_end`, `search`, `start`, `size`, **always `sort`** | `{status, count, start, size, data: JobRow[], graph}` |
|
|
131
|
+
| `/api/jobs/job/<id>` | GET | `graph` (`default` · `detail` · `admin` · `status`) | `{status, data: JobRow, graph}` |
|
|
132
|
+
| `/api/jobs/job/<id>` | POST | `{cancel_request: true}` / `{retry_request: true}` / `{retry_request:{retry:true, delay:N}}` | the handler dict **verbatim** — see correction 4 |
|
|
133
|
+
| `/api/jobs/job/<id>` | DELETE | — | `{status:'deleted'}` (gate `manage_jobs\|jobs`) |
|
|
134
|
+
| `/api/jobs/event` | GET list | `job`, `job__in`, `event`, `graph=timeline`, `sort=at`, `size` | `{status, count, data: JobTimelineEntry[]}` |
|
|
135
|
+
| `/api/jobs/logs` | GET list | `job`, `job__in`, `kind`, `sort=-created`, `size` | `{status, count, data: JobLogRow[]}` |
|
|
136
|
+
| `/api/jobs/scheduled_task` | GET/POST/DELETE | `enabled`, `task_type`, `name__icontains`, `search`, paging, `sort=-created`; `graph` `list` \| `default` | `{status, count, data: ScheduledTaskRow[]}` |
|
|
137
|
+
| `/api/jobs/task_result` | GET (read-only), DELETE | `task`, `sort=-created`, `size` | `{status, count, data: TaskResultRow[]}` |
|
|
138
|
+
|
|
139
|
+
`JobEvent`/`JobLog` `SAVE_PERMS` is empty — rows are system-created; POST is a
|
|
140
|
+
403. `TaskResult` is read-only over REST; its DELETE_PERMS drops `owner`
|
|
141
|
+
(`jobs|manage_scheduled_tasks` only).
|
|
142
|
+
|
|
143
|
+
Graph field sets that matter:
|
|
144
|
+
|
|
145
|
+
- `event?graph=timeline` → exactly `{event, at, runner_id, details}` — **no
|
|
146
|
+
`id`**. That is why `JobTimelineEntry` is a separate type from
|
|
147
|
+
`JobEventRow`: a timeline entry cannot be fed to anything that keys on a row
|
|
148
|
+
id.
|
|
149
|
+
- `job?graph=admin` → `__all__` **minus** `stack_trace`, and no `duration_ms`
|
|
150
|
+
(the admin graph declares no `extra`).
|
|
151
|
+
- `scheduled_task?graph=list` carries neither `description` nor `job_config`;
|
|
152
|
+
the table must not read them.
|
|
153
|
+
- `task_result?graph=list` is `{id, task_id, status, created}` — `output` and
|
|
154
|
+
`error` only exist on `default`.
|
|
155
|
+
|
|
156
|
+
### Control plane
|
|
157
|
+
|
|
158
|
+
`requires_global_perms` — member grants never satisfy it. Read gate
|
|
159
|
+
`view_jobs|manage_jobs|jobs`; **every write** gate `manage_jobs|jobs`.
|
|
160
|
+
|
|
161
|
+
| Route | Body / params | Response shape |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `GET stats` | — | `{status, data:{channels, runners, totals, scheduler}}` |
|
|
164
|
+
| `GET runners` | `channel?` **only** | `{status, count, data:[…]}` — complete list, paging ignored |
|
|
165
|
+
| `GET runners/sysinfo` | `timeout` | `{status, count, data: RunnerSysinfo[]}` |
|
|
166
|
+
| `GET runners/sysinfo/<id>` | `timeout` | `{status, data: RunnerSysinfo}`; no reply → **404 envelope** |
|
|
167
|
+
| `GET control/channels` | — | `{status, data: string[]}` (Redis stream scan) |
|
|
168
|
+
| `GET control/queue-sizes` | — | `{status, data:{<channel>: {stream, scheduled, db_*}}}` |
|
|
169
|
+
| `GET control/config` | — | `{status, data: JobsConfig}` — **manage-gated**, unlike every other read |
|
|
170
|
+
| `POST runners/ping` | `{runner_id, timeout}` | **top-level** `{status, runner_id, responsive}` |
|
|
171
|
+
| `POST runners/shutdown` | `{runner_id, graceful}` | `{status, message}` — fire-and-forget |
|
|
172
|
+
| `POST runners/broadcast` | `{command, data, timeout}` | **top-level** `{status, command, responses_count, responses}` |
|
|
173
|
+
| `POST control/clear-stuck` | `{channel*, idle_threshold_ms}` | `{status, message, data:{channel, cleared, details[], errors[]}}` |
|
|
174
|
+
| `POST control/manual-reclaim` | `{channel*}` | same shape (`idle_threshold_ms: 0` — reclaims everything in flight) |
|
|
175
|
+
| `POST control/purge` | `{days_old*, status?, dry_run?}` | dry run `{dry_run:true, count, cutoff, status_filter}` · real run `{deleted, details, cutoff, status_filter}` |
|
|
176
|
+
| `POST control/reset-failed` | `{channel?, since?, limit}` | **top-level** `{status, message, reset_count, requeue[]}` |
|
|
177
|
+
| `POST control/clear-queue` | `{channel*, confirm:"yes"*}` | `{status, message, data:{channel, deleted{}, db_pending_canceled, errors[]}}` |
|
|
178
|
+
| `POST control/cleanup-consumers` | `{channel?, destroy_empty_groups}` | `{status, data}` |
|
|
179
|
+
| `POST control/rebuild-scheduled` | `{channel?, limit?}` | `{status, data}` |
|
|
180
|
+
| `POST control/force-scheduler-lead` | `{}` | **top-level** `{status, message, previous_holder}` |
|
|
181
|
+
| `POST control/test` | `{channel, delay?}` | **top-level** `{status, message, job_id, channel, delayed}` |
|
|
182
|
+
| `POST tests` | `{}` | `{status, message}` — publishes a whole sample suite (load-generating; armed) |
|
|
183
|
+
|
|
184
|
+
`*` = enforced server-side; omitting it is an HTTP 400.
|
|
185
|
+
|
|
186
|
+
Result keys are **not uniform**: some handlers nest under `data`, others answer
|
|
187
|
+
with top-level fields. `mojoCall` returns the unwrapped envelope and each
|
|
188
|
+
`control.ts` function reads from whichever level its endpoint actually uses —
|
|
189
|
+
never a `data ?? body` heuristic.
|
|
190
|
+
|
|
191
|
+
### Metrics slugs
|
|
192
|
+
|
|
193
|
+
`jobs.published`, `jobs.completed`, `jobs.failed`, `jobs.retried`,
|
|
194
|
+
`jobs.expired`, `jobs.local.completed|failed|duration_ms`.
|
|
195
|
+
|
|
196
|
+
Per-channel slugs are **asymmetric**, and the throughput switch honours it:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
jobs.published.<channel> # publishes
|
|
200
|
+
jobs.channel.<channel>.completed # terminal outcomes
|
|
201
|
+
jobs.channel.<channel>.failed
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Backend corrections
|
|
205
|
+
|
|
206
|
+
Thirteen places where the backend does not do what web-mojo (or a reasonable
|
|
207
|
+
reading of the model) assumes. The backend wins in all of them; the mock
|
|
208
|
+
encodes every one, which is why they are testable.
|
|
209
|
+
|
|
210
|
+
1. **`GET /api/jobs/health` and `health/<channel>` are DEAD at runtime.**
|
|
211
|
+
`JobManager.get_channel_health` reads `state['stream_length']` and
|
|
212
|
+
`state['pending_count']`, which the Plan-B `get_queue_state` no longer
|
|
213
|
+
returns → `KeyError` → HTTP 400 `{status:false,error:"'stream_length'"}`.
|
|
214
|
+
Nothing here calls them; every dashboard number comes from
|
|
215
|
+
`GET /api/jobs/stats`, which uses `.get()` fallbacks throughout and already
|
|
216
|
+
carries `channels`, `runners`, `totals` and `scheduler`.
|
|
217
|
+
2. **`clear-stuck` and `manual-reclaim` are `@requires_params('channel')`** —
|
|
218
|
+
there is **no** all-channel form. `purge` requires `days_old`.
|
|
219
|
+
`clear-queue` requires `channel` **and** `confirm:"yes"`. Sending
|
|
220
|
+
`channel: null` (web-mojo's "All Channels") is an instant 400.
|
|
221
|
+
3. **Result keys are the backend's, not the obvious ones.** `clear-stuck` →
|
|
222
|
+
`data:{cleared, details[], errors[], message}` — there is **no `count`**
|
|
223
|
+
(web-mojo read one and always reported 0). `purge` real run → `deleted`;
|
|
224
|
+
dry run → `count`. `reset-failed` → top-level `{reset_count, requeue[]}`.
|
|
225
|
+
`ping` → top-level `{runner_id, responsive}`. `broadcast` → top-level
|
|
226
|
+
`{command, responses_count, responses}`.
|
|
227
|
+
4. **`POST_SAVE_ACTIONS` return the handler dict VERBATIM as the response
|
|
228
|
+
body** (`rest.py on_rest_save_and_respond → JsonResponse(resp)`) — not the
|
|
229
|
+
row, not nested under `data`, and at **HTTP 200 even on failure**. The
|
|
230
|
+
client's `status === false` unwrap is what turns a refusal into a rejection,
|
|
231
|
+
so both actions declare `response: 'payload'`.
|
|
232
|
+
`cancel_request` must be sent as `true`: Python treats `{}` as falsy, so an
|
|
233
|
+
argument-less call answers `"cancel_request must be true"`.
|
|
234
|
+
`retry_request` is `true` or `{retry:true, delay:N}` and **always
|
|
235
|
+
republishes**: `{original_job_id, new_job_id, delayed}`. The UI must never
|
|
236
|
+
imply the same row resumed.
|
|
237
|
+
5. **Job has no `recent_events` in any graph** (only inside the `get_status`
|
|
238
|
+
action payload) and no `is_retriable`/`stack_trace` in any graph. The
|
|
239
|
+
lifecycle timeline is `/api/jobs/event?job=<id>&graph=timeline`;
|
|
240
|
+
retriability is computed client-side from the *service* rule
|
|
241
|
+
(`status ∈ {failed, canceled, expired}`), which is deliberately wider than
|
|
242
|
+
the model's `is_retriable` property.
|
|
243
|
+
6. **Unknown filter keys are silently dropped** by `build_rest_filters`
|
|
244
|
+
(`hasattr(cls, field_name)`), so a bad key returns the **unfiltered** list
|
|
245
|
+
rather than an error. JobLog has no `runner_id` field at all — web-mojo
|
|
246
|
+
shipped `/api/jobs/logs?runner_id=…` and presented the whole log table as
|
|
247
|
+
one runner's logs. Always filter by the **relation** name (`job=`,
|
|
248
|
+
`job__in=`). See correction 12 for the sharper version of this.
|
|
249
|
+
7. **The default list sort is `-id`**, and Job ids are 32-char uuid hex, so an
|
|
250
|
+
unsorted job list is lexicographic noise. `normalizeJobListParams` (and its
|
|
251
|
+
four siblings) therefore **always** emit an explicit `sort`. The `ordering`
|
|
252
|
+
param web-mojo sent is not read by the backend at all.
|
|
253
|
+
8. **The runner heartbeat payload is
|
|
254
|
+
`{runner_id, hostname, channels[], jobs_processed, jobs_failed, started,
|
|
255
|
+
last_heartbeat}`** plus `alive` and `id` injected by the view. There is **no
|
|
256
|
+
`version` field** (web-mojo rendered a chip for one). `GET runners` ignores
|
|
257
|
+
`start/size/sort/search` and returns the complete list — it is not a paged
|
|
258
|
+
model endpoint, which is why `JobRunnersPage` is a bespoke panel table.
|
|
259
|
+
Heartbeat/sysinfo timestamps are **ISO strings** while model rows carry
|
|
260
|
+
**epoch seconds**: a real asymmetry, preserved in the types.
|
|
261
|
+
9. **`/api/jobs/publish` does not exist.** web-mojo's `Job.publish` was dead
|
|
262
|
+
code. Republishing happens through `retry_request` (or `control/test` for a
|
|
263
|
+
sample job).
|
|
264
|
+
10. **ScheduledTask/TaskResult are not globally pinned.** `VIEW_PERMS`
|
|
265
|
+
includes `owner`, so a caller with **no** global jobs grant gets HTTP 200
|
|
266
|
+
with only their **own** tasks. An admin page that did not gate on the
|
|
267
|
+
global grant would silently render a personal list as if it were the
|
|
268
|
+
system's. Gating fail-closed on `sys.jobs | sys.view_scheduled_tasks`
|
|
269
|
+
closes that hole; `owner` is dropped from the client clause on purpose.
|
|
270
|
+
11. **ScheduledTask has no REST "run now"** — only the owner-scoped assistant
|
|
271
|
+
tool `_tool_run_scheduled_task_now`. See [The run-now gap](#the-run-now-gap).
|
|
272
|
+
|
|
273
|
+
Two more corrections found while building, refining 6 and the gate table:
|
|
274
|
+
|
|
275
|
+
12. **`/api/jobs/logs?job_id=` is an HTTP 500, not an unfiltered list.**
|
|
276
|
+
Django installs a descriptor for a FK's *attname*, so `hasattr(cls,
|
|
277
|
+
'job_id')` is `True` and `build_rest_filters` accepts the key — then
|
|
278
|
+
`get_model_field('job_id')` returns `None` and `normalize_rest_value`
|
|
279
|
+
dereferences it: `AttributeError` → 500
|
|
280
|
+
(`"'NoneType' object has no attribute 'get_internal_type'"`). By contrast
|
|
281
|
+
`?runner_id=` (a name JobLog does not have in any form) **is** silently
|
|
282
|
+
dropped and returns the whole table. Same-looking mistake, two completely
|
|
283
|
+
different failures — the mock reproduces both, and the same split applies
|
|
284
|
+
to `task_result?task_id=` vs the relation name `task=`.
|
|
285
|
+
13. **`manage_scheduled_tasks` does not imply view.**
|
|
286
|
+
`ScheduledTask.VIEW_PERMS` is `["jobs","view_scheduled_tasks","owner"]` and
|
|
287
|
+
`SAVE_PERMS` is `["jobs","manage_scheduled_tasks","owner"]` — the manage
|
|
288
|
+
grant appears in **neither** view clause. An operator holding only
|
|
289
|
+
`manage_scheduled_tasks` falls through to the `owner` branch and sees just
|
|
290
|
+
their own tasks. A real jobs operator needs the `jobs` catch-all, or
|
|
291
|
+
`view_scheduled_tasks` alongside the manage grant (the mock's
|
|
292
|
+
`jobs.operator` identity is seeded exactly that way, deliberately).
|
|
293
|
+
|
|
294
|
+
## Design decisions
|
|
295
|
+
|
|
296
|
+
- **Stats, not health.** Correction 1. web-mojo's `JobHealthView` — the only
|
|
297
|
+
consumer of `/health` — was never mounted, so nothing is lost.
|
|
298
|
+
- **Runners is not a ModelTable.** The endpoint has no paging/sort/search
|
|
299
|
+
contract; rendering a pager and sort headers that silently do nothing would
|
|
300
|
+
lie about the wire. Client-side sort/filter over the full list, the same
|
|
301
|
+
precedent as `MetricsPermissionsPage`.
|
|
302
|
+
- **Queued vs Scheduled by `run_at__isnull`,** not by comparing a datetime on
|
|
303
|
+
the wire. `__isnull` is an explicitly supported operator and needs no date
|
|
304
|
+
coercion. A pending job with a *past* `run_at` shows up under Scheduled
|
|
305
|
+
flagged **overdue**, which is its true state.
|
|
306
|
+
- **All-channel operations fan out client-side** (`forEachChannel` →
|
|
307
|
+
`Promise.allSettled`), because correction 2 leaves no server-side form.
|
|
308
|
+
Partial failures are reported per channel with a `warning` toast — never a
|
|
309
|
+
blanket success.
|
|
310
|
+
- **Purge is dry-run first.** The dialog runs `dry_run:true` and reports
|
|
311
|
+
"N jobs would be deleted before `<cutoff>`"; only the armed confirm performs
|
|
312
|
+
the real run, which is read from `deleted`. Fixes the source's wrong
|
|
313
|
+
`data.count` read.
|
|
314
|
+
- **Armed confirmations** for purge, clear-queue, clear-stuck,
|
|
315
|
+
cleanup-consumers, shutdown, broadcast `shutdown`, force-scheduler-lead and
|
|
316
|
+
task delete. `clear-queue` sends `confirm:"yes"` **only after** arming: the
|
|
317
|
+
token is a server-side safety gate, and pre-satisfying it (as web-mojo did on
|
|
318
|
+
every call) removes the safety rather than honouring it.
|
|
319
|
+
- **KISS modals.** Job, runner, scheduled-task and task-result inspection are
|
|
320
|
+
`modal.detail`; the editor and confirmations are stacked native `<dialog>`s.
|
|
321
|
+
No RightPanel, no record-detail routes — the jobs package imports neither.
|
|
322
|
+
- **Bounded export.** `JOB_EXPORTER` is a *client* projection (id, channel,
|
|
323
|
+
func, status, timing, attempt, runner). The server's `download_format` would
|
|
324
|
+
ship `payload`, `metadata` and `last_error`.
|
|
325
|
+
|
|
326
|
+
### ScheduledTask: PORT (decision recorded)
|
|
327
|
+
|
|
328
|
+
The backend is complete and live — model, REST, hourly cron dispatcher,
|
|
329
|
+
`TaskResult`, three task types including LLM. The only reason the page never
|
|
330
|
+
shipped is that web-mojo's `ScheduledTaskTablePage` was imported by
|
|
331
|
+
`src/admin.js` and then never `registerPage`d.
|
|
332
|
+
|
|
333
|
+
It is also the one jobs surface with a real security subtlety worth owning:
|
|
334
|
+
the `owner` fallback (correction 10) means an ungated page degrades into a
|
|
335
|
+
personal task list wearing an admin page's chrome. Porting it *with a
|
|
336
|
+
global-only gate* turns a dead page into a first-class admin feature and closes
|
|
337
|
+
that hole in one move — which is why the gate difference is called out at the
|
|
338
|
+
top of this page rather than buried.
|
|
339
|
+
|
|
340
|
+
### The run-now gap (django-mojo #1309)
|
|
341
|
+
|
|
342
|
+
There is **no REST route** to run a scheduled task immediately. The capability
|
|
343
|
+
exists only as the owner-scoped assistant tool `_tool_run_scheduled_task_now`,
|
|
344
|
+
and there is no `/api/jobs/publish` (correction 9) to synthesize one from.
|
|
345
|
+
|
|
346
|
+
Per the wave-7A precedent, an unsupported operation stays **absent** rather
|
|
347
|
+
than shipping disabled, so **no "Run now" control ships** — not greyed out, not
|
|
348
|
+
hidden behind a permission. When django-mojo **#1309** lands a REST route, the
|
|
349
|
+
control belongs in `ScheduledTaskDetail`'s action kebab and in the editor's
|
|
350
|
+
footer; nothing else needs to change.
|
|
351
|
+
|
|
352
|
+
### Immutable task type / the JSONField merge
|
|
353
|
+
|
|
354
|
+
`task_type` is **fixed after creation** — the select is disabled while editing.
|
|
355
|
+
django-mojo merges a JSON body into an existing `JSONField` rather than
|
|
356
|
+
replacing it, so switching a task's type would leave the previous type's keys
|
|
357
|
+
behind inside `job_config` (an LLM task carrying a stale `url`, a webhook task
|
|
358
|
+
carrying a stale `user_prompt`). The dispatcher reads by type and would ignore
|
|
359
|
+
them, but the record would be quietly wrong and the editor would show a config
|
|
360
|
+
the server does not use. Creating a new task is the honest operation; the field
|
|
361
|
+
help says so in place.
|
|
362
|
+
|
|
363
|
+
For the same reason the config editor is keyed on `task_type`
|
|
364
|
+
(`<ConfigEditor key={draft.task_type} …>`) and a type change on a *new* task
|
|
365
|
+
resets `job_config` to `{}`.
|
|
366
|
+
|
|
367
|
+
### Deliberately absent
|
|
368
|
+
|
|
369
|
+
Carried over from web-mojo only as *decisions not to carry*:
|
|
370
|
+
|
|
371
|
+
| Not shipped | Why |
|
|
372
|
+
|---|---|
|
|
373
|
+
| Per-runner **Drain** and **Restart** | The source's buttons only toasted "backend integration pending". No endpoint exists. |
|
|
374
|
+
| The runner **`version` chip** | No such field in the heartbeat payload (correction 8). |
|
|
375
|
+
| **`Job.publish`** | `/api/jobs/publish` does not exist (correction 9). |
|
|
376
|
+
| **`JobHealthView`** | Reads a broken endpoint (correction 1) and was never mounted anyway. |
|
|
377
|
+
| **Batch task delete** | Delete cascades over `TaskResult`; arming once for a whole selection is not a safety story. Enable/disable *is* offered as a batch — reversible, one field. |
|
|
378
|
+
| **Run Now** | django-mojo #1309, above. |
|
|
379
|
+
| `data.count` reads, `ordering=` params, `job_id=`/`runner_id=` log filters | Corrections 3, 7, 6/12. |
|
|
380
|
+
| HTML-string interpolation | ReactNode slots only (architecture rule 6). |
|
|
381
|
+
|
|
382
|
+
## Edge cases the pages handle
|
|
383
|
+
|
|
384
|
+
- Zero runners with queued work → critical strip **and** critical channel rows.
|
|
385
|
+
- Heartbeat tiers computed from the ISO `last_heartbeat`: ≥30s slow, ≥120s
|
|
386
|
+
stale. The roster row and the detail header always agree.
|
|
387
|
+
- Running job whose runner is dead → cancel force-cancels server-side and
|
|
388
|
+
returns `forced:true`; the toast says "canceled", not "cancellation
|
|
389
|
+
requested".
|
|
390
|
+
- Terminal job → the service refuses cancel, so the control is **not offered**.
|
|
391
|
+
- Retry of a canceled/expired job is allowed and always produces a new id.
|
|
392
|
+
- Sysinfo has three distinct states: a healthy host, a successful envelope with
|
|
393
|
+
an inner `status:'error'` (psutil missing), and a runner that never answers
|
|
394
|
+
(404 envelope). None of them renders blank.
|
|
395
|
+
- Empty/absent channel list → operations needing a channel are disabled with an
|
|
396
|
+
explanation, not a broken select.
|
|
397
|
+
- Huge `payload`/`last_error` → bounded rendering (`JsonBlock` + truncation with
|
|
398
|
+
full text on demand). Stack traces are unavailable by graph and are not
|
|
399
|
+
implied.
|
|
400
|
+
- Scheduled task at the per-user cap, or with a `run_times` value that slipped
|
|
401
|
+
past client validation → the server's message surfaces **verbatim** through
|
|
402
|
+
the rejecting save path.
|
|
403
|
+
- A viewer-only operator sees every read surface, zero mutation controls, and
|
|
404
|
+
issues no denied request.
|
|
405
|
+
|
|
406
|
+
## Showcase & styling
|
|
407
|
+
|
|
408
|
+
`apps/showcase/src/pages/components/demos-admin-jobs.tsx` reaches every state
|
|
409
|
+
the browser pass must see, in both themes: the dashboard (dead runner, a
|
|
410
|
+
channel with zero alive runners), the runner roster and its three sysinfo
|
|
411
|
+
states, the jobs table across all five segments, the job inspector (failed with
|
|
412
|
+
a multi-line error, running on a dead runner, overdue, canceled, expired), the
|
|
413
|
+
operations panel including a purge dry run, the scheduled-task table + detail +
|
|
414
|
+
editor, and the permission-gate story.
|
|
415
|
+
|
|
416
|
+
Two states cannot be produced by calling the mock, because the seeded fixture
|
|
417
|
+
is a *populated* deployment: **no runners registered at all** and **no channels
|
|
418
|
+
configured**. The demo's "Fleet edge cases" tab drives the shipped components
|
|
419
|
+
from hand-built `JobsStats` objects (and, for `JobRunnersStrip`, a demo-only
|
|
420
|
+
`QueryClient` pinned to an empty fleet). Nothing in the package is mocked or
|
|
421
|
+
branched for the demo.
|
|
422
|
+
|
|
423
|
+
Styles are semantic `jobs-*` classes, tokens only, both themes:
|
|
424
|
+
`apps/{portal,showcase}/src/theme/admin-jobs.css`, imported from each app's
|
|
425
|
+
`theme.css`. The verifier asserts the two copies stay byte-identical and that
|
|
426
|
+
neither contains a raw hex colour.
|