portal-mojo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,383 @@
1
+ // portal-mojo/admin — the base admin portal as self-registering section
2
+ // bundles (PLAN.md "Admin distribution: dual-mount", decided 2026-08-04).
3
+ // The same sections mount two ways:
4
+ // 1. standalone — apps/portal mounts ALL sections at the root and IS the
5
+ // dedicated back office for any django-mojo deployment;
6
+ // 2. embedded — a product's own portal registers them under a "System"
7
+ // area (#/system/…) beside its product pages, gated by `view_admin`.
8
+ // Constraints built in from day one:
9
+ // · routes are MOUNT-POINT RELATIVE — a section never knows where it lives;
10
+ // · sections CONTRIBUTE sidebar entries; the sidebar engine (Chunk A4
11
+ // registry) owns the menu — a section never renders the sidebar itself;
12
+ // · visibility = permissions (now) ∧ backend capabilities (Phase 2
13
+ // capabilities endpoint), evaluated by the mounting shell.
14
+ // Sections land here as Phase 2 admin pages stabilize; deployed portals then
15
+ // pick up admin updates via `npm update portal-mojo` — clone the shell,
16
+ // never the admin.
17
+ import { createElement, useContext, type ComponentType, type ReactNode } from 'react';
18
+ import { Navigate, type RouteObject } from 'react-router-dom';
19
+ import { GroupContext, hasPermission, useMe, type PermSpec } from '../client';
20
+ import { Guarded } from '../ui/Guarded';
21
+ import type { MenuConfig, MenuItem } from '../ui/menu-registry';
22
+ import {
23
+ GLOBAL_CREDENTIAL_PERMS,
24
+ GroupApiKeysPage,
25
+ WebhookSubscriptionsPage,
26
+ } from './credentials';
27
+ import { MONITORING_ADMIN_SECTION } from './monitoring';
28
+ import { CLOUDWATCH_ADMIN_SECTION } from './cloudwatch';
29
+ import { SETTINGS_ADMIN_SECTION } from './settings';
30
+ import { BOUNCER_ADMIN_SECTION } from './bouncer';
31
+ import {
32
+ DEVICE_INTEL_ADMIN_SECTION,
33
+ GEOIP_ADMIN_SECTION,
34
+ SECURITY_OPERATIONS_ADMIN_SECTION,
35
+ } from './security';
36
+ import { MEMBERS_ADMIN_SECTION, USERS_ADMIN_SECTION } from './identity';
37
+ import { JOBS_ADMIN_SECTION } from './jobs';
38
+ import { NETWORK_SECURITY_ADMIN_SECTION } from './network';
39
+ import { DNS_ADMIN_SECTION } from './dns';
40
+ import { STORAGE_ADMIN_SECTION } from './storage';
41
+ import { SHORTLINKS_ADMIN_SECTION } from './shortlinks';
42
+ import { EMAIL_ADMIN_SECTION, PUBLIC_MESSAGES_ADMIN_SECTION, PUSH_ADMIN_SECTION } from './messaging';
43
+ import { PHONE_HUB_ADMIN_SECTION } from './phonehub';
44
+ import { ASSISTANT_ADMIN_SECTION } from './assistant';
45
+ import { ADMIN_DASHBOARD_PERMISSIONS, AdminDashboardPage } from './dashboard';
46
+
47
+ export * from './credentials';
48
+ export * from './monitoring';
49
+ export * from './cloudwatch';
50
+ export * from './settings';
51
+ export * from './bouncer';
52
+ export * from './security-permissions';
53
+ export * from './security';
54
+ export * from './incidents';
55
+ export * from './rules';
56
+ export * from './identity';
57
+ export * from './jobs';
58
+ export * from './network';
59
+ export * from './dns';
60
+ export * from './storage';
61
+ export * from './shortlinks';
62
+ export * from './messaging';
63
+ export * from './phonehub';
64
+ export * from './dashboard';
65
+ export * from './assistant';
66
+
67
+ export type AdminNavigationGroup =
68
+ | 'overview'
69
+ | 'identity-access'
70
+ | 'security'
71
+ | 'observability'
72
+ | 'operations'
73
+ | 'infrastructure'
74
+ | 'communications'
75
+ | 'assistant'
76
+ | 'other';
77
+
78
+ export const ADMIN_NAVIGATION_GROUPS: Readonly<Record<AdminNavigationGroup, {
79
+ id: string;
80
+ label: string;
81
+ icon: string;
82
+ order: number;
83
+ }>> = {
84
+ overview: { id: 'admin:overview', label: 'Overview', icon: 'bi-grid-1x2', order: 0 },
85
+ 'identity-access': { id: 'admin:identity-access', label: 'Identity & Access', icon: 'bi-people', order: 10 },
86
+ security: { id: 'admin:security', label: 'Security', icon: 'bi-shield-check', order: 20 },
87
+ observability: { id: 'admin:observability', label: 'Observability', icon: 'bi-activity', order: 30 },
88
+ operations: { id: 'admin:operations', label: 'Operations', icon: 'bi-gear', order: 40 },
89
+ infrastructure: { id: 'admin:infrastructure', label: 'Infrastructure', icon: 'bi-cloud', order: 50 },
90
+ communications: { id: 'admin:communications', label: 'Communications', icon: 'bi-chat-dots', order: 60 },
91
+ assistant: { id: 'admin:assistant', label: 'Assistant', icon: 'bi-stars', order: 70 },
92
+ other: { id: 'admin:other', label: 'Other', icon: 'bi-grid', order: 80 },
93
+ };
94
+
95
+ /** One mountable admin area: pages + routes + sidebar contribution + gates. */
96
+ export interface AdminSection {
97
+ /** Stable id; the default route segment (`'users'` → `#/users` standalone, `#/system/users` embedded). */
98
+ id: string;
99
+ /**
100
+ * Route prefix relative to the admin mount. Defaults to `id`. Use `''`
101
+ * when sibling routes should mount directly at the admin root.
102
+ */
103
+ basePath?: string;
104
+ /** Sidebar entry label. */
105
+ title: string;
106
+ /** bootstrap-icons class for the sidebar entry (e.g. `'bi-people'`). */
107
+ icon: string;
108
+ /** Permissions that reveal the section (client errs permissive; the server stays authoritative). */
109
+ permissions: string[];
110
+ /** Backend capability key that must be enabled for the section to mount (Phase 2 handshake). */
111
+ capability?: string;
112
+ /** Operator-domain category. Omitted external sections fall under Other. */
113
+ navigationGroup?: AdminNavigationGroup;
114
+ /** Mount-point-relative routes (no leading slash). */
115
+ routes: AdminRoute[];
116
+ }
117
+
118
+ export interface AdminRoute {
119
+ /** Path relative to the section mount (`''` for the index, `':id'` for details). */
120
+ path: string;
121
+ component: ComponentType;
122
+ /** Additional route gate (ANY-of). It is ANDed with section permissions. */
123
+ permissions?: PermSpec;
124
+ /** Present → the route appears as a sidebar item under the section. */
125
+ label?: string;
126
+ /** A denied root landing redirects to the first visible admin route. */
127
+ fallbackToFirstVisible?: boolean;
128
+ }
129
+
130
+ /** Registry of every shipped admin section. */
131
+ export const ADMIN_SECTIONS: readonly AdminSection[] = [
132
+ {
133
+ id: 'dashboard',
134
+ basePath: '',
135
+ title: 'Overview',
136
+ icon: 'bi-grid-1x2',
137
+ navigationGroup: 'overview',
138
+ permissions: ADMIN_DASHBOARD_PERMISSIONS,
139
+ routes: [{ path: '', label: 'Dashboard', component: AdminDashboardPage, permissions: ADMIN_DASHBOARD_PERMISSIONS, fallbackToFirstVisible: true }],
140
+ },
141
+ USERS_ADMIN_SECTION,
142
+ MEMBERS_ADMIN_SECTION,
143
+ {
144
+ id: 'credentials',
145
+ basePath: '',
146
+ title: 'Credentials',
147
+ icon: 'bi-key',
148
+ navigationGroup: 'identity-access',
149
+ permissions: GLOBAL_CREDENTIAL_PERMS,
150
+ routes: [
151
+ {
152
+ path: 'api-keys', label: 'Group API Keys',
153
+ component: GroupApiKeysPage, permissions: GLOBAL_CREDENTIAL_PERMS,
154
+ },
155
+ {
156
+ path: 'webhook-subscriptions', label: 'Webhooks',
157
+ component: WebhookSubscriptionsPage, permissions: GLOBAL_CREDENTIAL_PERMS,
158
+ },
159
+ ],
160
+ },
161
+ MONITORING_ADMIN_SECTION,
162
+ CLOUDWATCH_ADMIN_SECTION,
163
+ SETTINGS_ADMIN_SECTION,
164
+ SECURITY_OPERATIONS_ADMIN_SECTION,
165
+ BOUNCER_ADMIN_SECTION,
166
+ DEVICE_INTEL_ADMIN_SECTION,
167
+ GEOIP_ADMIN_SECTION,
168
+ JOBS_ADMIN_SECTION,
169
+ NETWORK_SECURITY_ADMIN_SECTION,
170
+ DNS_ADMIN_SECTION,
171
+ STORAGE_ADMIN_SECTION,
172
+ SHORTLINKS_ADMIN_SECTION,
173
+ EMAIL_ADMIN_SECTION,
174
+ PUBLIC_MESSAGES_ADMIN_SECTION,
175
+ PUSH_ADMIN_SECTION,
176
+ PHONE_HUB_ADMIN_SECTION,
177
+ ASSISTANT_ADMIN_SECTION,
178
+ ];
179
+
180
+ function relativePath(...parts: Array<string | undefined>): string {
181
+ return parts.filter((part) => part != null && part !== '')
182
+ .join('/')
183
+ .replace(/^\/+|\/+$/g, '')
184
+ .replace(/\/+/g, '/');
185
+ }
186
+
187
+ function absolutePath(...parts: Array<string | undefined>): string {
188
+ const relative = relativePath(...parts);
189
+ return relative ? `/${relative}` : '/';
190
+ }
191
+
192
+ function AdminDenied() {
193
+ return createElement('div', { className: 'panel' },
194
+ createElement('div', { className: 'empty' },
195
+ createElement('i', { className: 'bi bi-shield-lock' }),
196
+ createElement('h2', null, 'Access denied'),
197
+ createElement('p', { className: 'dim' }, 'Your account does not have permission to open this admin page.'),
198
+ ));
199
+ }
200
+
201
+ function guardedElement(section: AdminSection, route: AdminRoute, sections: readonly AdminSection[], mount: string): ReactNode {
202
+ const page = createElement(route.component);
203
+ const fallback = route.fallbackToFirstVisible
204
+ ? createElement(AdminMountLanding, { sections: sections.filter((candidate) => candidate !== section), mount })
205
+ : createElement(AdminDenied);
206
+ const routeGuarded = route.permissions
207
+ ? createElement(Guarded, {
208
+ permission: route.permissions,
209
+ fallback,
210
+ children: page,
211
+ })
212
+ : page;
213
+ const guarded = createElement(Guarded, {
214
+ permission: section.permissions,
215
+ fallback,
216
+ children: routeGuarded,
217
+ });
218
+ return createElement(AdminGlobalScope, { children: guarded });
219
+ }
220
+
221
+ /** Admin is global even when embedded beneath a product portal's provider. */
222
+ function AdminGlobalScope({ children }: { children: ReactNode }) {
223
+ const outer = useContext(GroupContext);
224
+ if (!outer) return children;
225
+ return createElement(GroupContext.Provider, {
226
+ value: { ...outer, group: null, member: null, loading: false },
227
+ children,
228
+ });
229
+ }
230
+
231
+ function routeVisible(section: AdminSection, route: AdminRoute, me: ReturnType<typeof useMe>['data']): boolean {
232
+ return hasPermission(me ?? null, section.permissions, null)
233
+ && (!route.permissions || hasPermission(me ?? null, route.permissions, null));
234
+ }
235
+
236
+ function AdminLanding({ section, mount }: { section: AdminSection; mount: string }) {
237
+ const { data: me } = useMe();
238
+ const first = section.routes.find((route) => routeVisible(section, route, me));
239
+ if (!first) return createElement(AdminDenied);
240
+ const base = section.basePath ?? section.id;
241
+ return createElement(Navigate, {
242
+ replace: true,
243
+ to: absolutePath(mount, base, first.path),
244
+ });
245
+ }
246
+
247
+ function AdminMountLanding({ sections, mount }: { sections: readonly AdminSection[]; mount: string }) {
248
+ const { data: me } = useMe();
249
+ for (const section of sections) {
250
+ const first = section.routes.find((route) => routeVisible(section, route, me));
251
+ if (first) {
252
+ return createElement(Navigate, {
253
+ replace: true,
254
+ to: absolutePath(mount, section.basePath ?? section.id, first.path),
255
+ });
256
+ }
257
+ }
258
+ return createElement(AdminDenied);
259
+ }
260
+
261
+ /**
262
+ * Generate mount-relative React Router entries with section AND route gates.
263
+ * A non-root section receives a dynamic index redirect to its first visible
264
+ * route, so direct navigation never lands on a route the operator cannot see.
265
+ */
266
+ export function adminSectionRoutes(
267
+ sections: readonly AdminSection[],
268
+ opts: { mount?: string } = {},
269
+ ): RouteObject[] {
270
+ const mount = relativePath(opts.mount ?? '');
271
+ const routes: RouteObject[] = [];
272
+ const rootSections = sections.filter((section) => relativePath(section.basePath ?? section.id) === '');
273
+ const rootHasIndex = rootSections.some((section) =>
274
+ section.routes.some((route) => relativePath(route.path) === ''));
275
+ if (mount && rootSections.length > 0 && !rootHasIndex) {
276
+ routes.push({
277
+ path: mount,
278
+ element: createElement(AdminGlobalScope, {
279
+ children: createElement(AdminMountLanding, { sections: rootSections, mount }),
280
+ }),
281
+ });
282
+ }
283
+ for (const section of sections) {
284
+ const base = relativePath(section.basePath ?? section.id);
285
+ const landing = relativePath(mount, base);
286
+ const hasIndex = section.routes.some((route) => relativePath(route.path) === '');
287
+ if (base && landing && !hasIndex) {
288
+ routes.push({
289
+ path: landing,
290
+ element: createElement(AdminGlobalScope, {
291
+ children: createElement(AdminLanding, { section, mount }),
292
+ }),
293
+ });
294
+ }
295
+ for (const route of section.routes) {
296
+ routes.push({
297
+ path: relativePath(mount, base, route.path),
298
+ element: guardedElement(section, route, sections, mount),
299
+ });
300
+ }
301
+ }
302
+ return routes;
303
+ }
304
+
305
+ /**
306
+ * Bridge admin sections into the A4 sidebar registry — sections CONTRIBUTE
307
+ * menu entries; they never own the sidebar. `mount` prefixes every route:
308
+ * '' for the standalone back office (section routes at root), '/system' when
309
+ * embedded in a product portal. Permission gates carry over per item;
310
+ * capability filtering slots in here when the Phase 2 endpoint lands.
311
+ */
312
+ export function adminSectionsMenu(
313
+ sections: readonly AdminSection[],
314
+ opts: {
315
+ name?: string;
316
+ mount?: string;
317
+ divider?: string;
318
+ /** Category grouping is opt-in for compatibility with shipped menus. */
319
+ grouped?: boolean;
320
+ presentation?: MenuConfig['presentation'];
321
+ } = {},
322
+ ): MenuConfig {
323
+ const mount = relativePath(opts.mount ?? '');
324
+ const items: MenuItem[] = [];
325
+ if (opts.divider) items.push({ divider: opts.divider });
326
+
327
+ if (opts.grouped) {
328
+ const groups = new Map<AdminNavigationGroup, MenuItem[]>();
329
+ for (const section of sections) {
330
+ const group = section.navigationGroup ?? 'other';
331
+ const base = relativePath(mount, section.basePath ?? section.id);
332
+ const labeled = section.routes.filter((route) => route.label);
333
+ const destinations: MenuItem[] = labeled.length > 0
334
+ ? labeled.map((route) => ({
335
+ id: `admin:${section.id}:${route.path || 'index'}`,
336
+ label: route.label!,
337
+ keywords: [section.title],
338
+ route: absolutePath(base, route.path),
339
+ permissionClauses: [section.permissions, ...(route.permissions ? [route.permissions] : [])],
340
+ }))
341
+ : [{
342
+ id: `admin:${section.id}`,
343
+ label: section.title,
344
+ keywords: [section.title],
345
+ route: absolutePath(base),
346
+ permissionClauses: [section.permissions],
347
+ }];
348
+ groups.set(group, [...(groups.get(group) ?? []), ...destinations]);
349
+ }
350
+ for (const [group, children] of [...groups.entries()].sort((a, b) =>
351
+ ADMIN_NAVIGATION_GROUPS[a[0]].order - ADMIN_NAVIGATION_GROUPS[b[0]].order)) {
352
+ const meta = ADMIN_NAVIGATION_GROUPS[group];
353
+ items.push({ id: meta.id, label: meta.label, icon: meta.icon, children });
354
+ }
355
+ return {
356
+ name: opts.name ?? 'admin',
357
+ scope: 'admin',
358
+ presentation: opts.presentation ?? 'static',
359
+ items,
360
+ };
361
+ }
362
+
363
+ for (const section of sections) {
364
+ const labeled = section.routes.filter((r) => r.label);
365
+ const base = relativePath(mount, section.basePath ?? section.id);
366
+ const children = labeled.map((route) => ({
367
+ label: route.label!,
368
+ route: absolutePath(base, route.path),
369
+ permissions: route.permissions,
370
+ }));
371
+ items.push({
372
+ label: section.title,
373
+ icon: section.icon,
374
+ permissions: section.permissions,
375
+ // Root-mounted sections cannot own `/`; render a pure parent and
376
+ // let the first visible child be the landing. Prefixed sections
377
+ // use the generated dynamic index redirect above.
378
+ ...(base ? { route: absolutePath(base) } : {}),
379
+ children,
380
+ });
381
+ }
382
+ return { name: opts.name ?? 'admin', scope: 'admin', presentation: opts.presentation ?? 'static', items };
383
+ }
@@ -0,0 +1,111 @@
1
+ // JobDashboardPage — the jobs engine at a glance. Port of web-mojo
2
+ // JobDashboardPage.js, reading top to bottom the way an operator triages:
3
+ // is the fleet alive → which channel is backed up → what is the throughput →
4
+ // what can I do about it.
5
+ //
6
+ // EVERY number comes from `GET /api/jobs/stats`. `GET /api/jobs/health` raises
7
+ // server-side (get_channel_health reads a key get_queue_state stopped
8
+ // returning), and web-mojo's JobHealthView — the only consumer — was never
9
+ // mounted. Stats already carries channels, runners, totals and the scheduler.
10
+ import { useQueryClient } from '@tanstack/react-query';
11
+ import { useLocation, useNavigate } from 'react-router-dom';
12
+ import { KPITile } from '../../charts';
13
+ import { JobChannelsPanel } from './sections/JobChannelsPanel';
14
+ import { JobOperationsSection } from './sections/JobOperationsSection';
15
+ import { JobRunnersStrip } from './sections/JobRunnersStrip';
16
+ import { JobThroughputSection } from './sections/JobThroughputSection';
17
+ import { jobsKeys, useChannelOptions, useJobStats } from './queries';
18
+
19
+ export function JobDashboardPage() {
20
+ const queryClient = useQueryClient();
21
+ const stats = useJobStats();
22
+ const channelOptions = useChannelOptions();
23
+ const navigate = useNavigate();
24
+ const location = useLocation();
25
+
26
+ const totals = stats.data?.totals;
27
+ const refreshAll = () => { void queryClient.invalidateQueries({ queryKey: jobsKeys.root }); };
28
+
29
+ /**
30
+ * Open the jobs table already filtered to a channel. The path is derived
31
+ * from the CURRENT location rather than hardcoded, because an admin
32
+ * section never knows whether it is mounted at the root or under /system.
33
+ */
34
+ const openChannel = (channel: string) => {
35
+ const base = location.pathname.replace(/\/+$/, '');
36
+ navigate({ pathname: `${base}/list`, search: `?channel=${encodeURIComponent(channel)}` });
37
+ };
38
+
39
+ const loading = stats.isPending;
40
+ const tile = (label: string, value: number | undefined, severity: 'critical' | 'warn' | 'info' | 'good' | null, hint?: string) => (
41
+ <KPITile
42
+ label={label}
43
+ value={value ?? null}
44
+ severity={severity}
45
+ hint={hint}
46
+ loading={loading}
47
+ formatter={(v) => v.toLocaleString()}
48
+ />
49
+ );
50
+
51
+ return (
52
+ <div className="jobs-dashboard">
53
+ <header className="jobs-page-head">
54
+ <div>
55
+ <div className="eyebrow">Jobs</div>
56
+ <h1>Job engine</h1>
57
+ <p>Runner fleet, queue depth and the operations that unstick them.</p>
58
+ </div>
59
+ <button
60
+ type="button"
61
+ className="btn btn-compact"
62
+ disabled={stats.isFetching}
63
+ onClick={refreshAll}
64
+ >
65
+ <i className={`bi bi-arrow-clockwise${stats.isFetching ? ' spin' : ''}`} /> Refresh
66
+ </button>
67
+ </header>
68
+
69
+ {stats.isError && (
70
+ <div className="panel jobs-note jobs-note-bad">
71
+ <i className="bi bi-exclamation-triangle" />
72
+ {stats.error instanceof Error ? stats.error.message : 'Could not load job statistics.'}
73
+ <button type="button" className="btn btn-compact" onClick={() => void stats.refetch()}>Retry</button>
74
+ </div>
75
+ )}
76
+
77
+ <JobRunnersStrip />
78
+
79
+ <div className="jobs-kpi-strip">
80
+ {tile('Queued', totals?.queued, (totals?.queued ?? 0) > 100 ? 'critical' : (totals?.queued ?? 0) > 50 ? 'warn' : null)}
81
+ {tile('In flight', totals?.inflight, null)}
82
+ {/* Running splits active from stale: a "running" job whose
83
+ runner is no longer alive is not running at all. */}
84
+ {tile(
85
+ 'Running',
86
+ totals?.running,
87
+ (totals?.running_stale ?? 0) > 0 ? 'warn' : null,
88
+ totals ? `${totals.running_active} active · ${totals.running_stale} stale` : undefined,
89
+ )}
90
+ {tile('Scheduled', totals?.scheduled, null)}
91
+ {tile('Completed', totals?.completed, 'good')}
92
+ {tile('Failed', totals?.failed, (totals?.failed ?? 0) > 0 ? 'critical' : null)}
93
+ </div>
94
+
95
+ <JobChannelsPanel
96
+ stats={stats.data}
97
+ isPending={stats.isPending}
98
+ onSelectChannel={openChannel}
99
+ onSchedulerChanged={refreshAll}
100
+ />
101
+
102
+ <JobThroughputSection channels={channelOptions.channels} />
103
+
104
+ <JobOperationsSection
105
+ channels={channelOptions.channels}
106
+ channelsPending={channelOptions.isPending}
107
+ onChanged={refreshAll}
108
+ />
109
+ </div>
110
+ );
111
+ }