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,393 @@
1
+ # Admin: network security — blocks, IP sets, geofencing
2
+
3
+ ```ts
4
+ import {
5
+ // the section (registered in ADMIN_SECTIONS — no app wiring needed)
6
+ NETWORK_SECURITY_ADMIN_SECTION,
7
+ // pages
8
+ BlockedIPsPage, FirewallLogPage, IPSetsPage, GeofencingPage,
9
+ // KISS detail modals (#1425)
10
+ showBlockedIpDetail, showIPSetDetail,
11
+ // the SHARED rule editor — platform page and group panel render this one
12
+ GeofenceRuleEditor, makeRuleEditorValue, ruleFromEditorValue, toggleRuleEditorMode,
13
+ // models
14
+ IPSetModel, GeofenceEventModel,
15
+ // permission clauses
16
+ IPSET_VIEW_PERMS, IPSET_MANAGE_PERMS, IPSET_DELETE_PERMS,
17
+ GEOFENCE_VIEW_PERMS, GEOFENCE_MANAGE_PERMS, SECURITY_EVENTS_PERMS,
18
+ METRICS_GLOBAL_VIEW_PERMS, GROUP_GEOFENCE_EDIT_PERMS,
19
+ // the geofence projection (one module, two scopes)
20
+ isAdvancedRule, ruleToForm, formToRule, describeRule, diffRules,
21
+ describeDecision, describeWouldBlock, buildSimulateBody, buildGroupRulePayload,
22
+ coerceRuleInput, collectScopes, scopeLabel, regionName,
23
+ ABUSE_FLAGS, US_STATES, COUNTRY_OPTIONS, countryName,
24
+ // config-plane hooks (all `enabled`-gated — a denied surface issues NO request)
25
+ useGeoConfig, useGeoAllowlist, useBypassHolders, useGeoSimulate,
26
+ useSaveGeoRules, useRemoveGeoRulesOverride, useSaveGeoAllowlist,
27
+ useGeofenceCountryTotals, useIPSetCidrData,
28
+ isGeofenceApiMissing, GEOFENCE_API_MISSING_MESSAGE,
29
+ } from 'portal-mojo/admin';
30
+ ```
31
+
32
+ Perimeter control: block an abusive IP right now, whitelist a customer's NAT,
33
+ maintain country and datacenter CIDR sets synced to the firewall fleet, set
34
+ geographic policy — and prove the policy does what you think with a built-in
35
+ simulator before it bites real traffic. Demo: showcase → Admin → **Network
36
+ security**. Styles: `apps/{portal,showcase}/src/theme/admin-network.css`
37
+ (byte-identical).
38
+
39
+ ## Surfaces
40
+
41
+ One section, `network-security` at `security/network`, under the **Security**
42
+ navigation group beside Security Operations, Bouncer, Devices & Logins and IP
43
+ Intelligence.
44
+
45
+ | Route | Presentation | Gate |
46
+ |---|---|---|
47
+ | `blocked-ips` | page + `modal.detail` | `GEOIP_VIEW_PERMS` |
48
+ | `firewall-log` | page + the shipped `LogInspector` | `FIREWALL_LOG_PERMS` |
49
+ | `ip-sets` | page + a two-tab `modal.detail` | `IPSET_VIEW_PERMS` |
50
+ | `geofencing` | **page**: posture header over 4 tabs | `GEOFENCE_VIEW_PERMS` |
51
+
52
+ Geofencing is the one surface that stays a page. That is the explicitly
53
+ requested presentation: a config page that states what is in force and lets you
54
+ test it before it bites.
55
+
56
+ The section gate is the ANY-of **union** of the four route gates (the
57
+ `MONITORING_ADMIN_SECTION` pattern), and each route then carries its exact
58
+ backend gate. A geofence-only operator (`sys.view_geofence` alone) reaches
59
+ Geofencing and is denied everywhere else; the engine's landing redirect resolves
60
+ to the first route they can see, so nobody lands on a denial.
61
+
62
+ ## Permissions — `sys.`-pinned global grants only, fail-closed
63
+
64
+ `@md.requires_global_perms` authorizes against global `User.permissions` or
65
+ superuser **only** — no group fallback, and a `GroupScopedToken` is refused
66
+ outright. Every clause here is therefore `sys.`-pinned: a member grant must
67
+ never open platform enforcement config.
68
+
69
+ | Constant | Value | Backend source |
70
+ |---|---|---|
71
+ | `GEOIP_VIEW_PERMS` | (imported from #1291) | `GeoLocatedIP.RestMeta.VIEW_PERMS` |
72
+ | `GEOIP_MANAGE_PERMS` | (imported from #1291) | `GeoLocatedIP.RestMeta.SAVE_PERMS` |
73
+ | `IPSET_VIEW_PERMS` | `sys.view_security`, `sys.security` | `IPSet.RestMeta.VIEW_PERMS` |
74
+ | `IPSET_MANAGE_PERMS` | `sys.manage_security`, `sys.security` | `IPSet.RestMeta.SAVE_PERMS` |
75
+ | `IPSET_DELETE_PERMS` | `sys.manage_security` **only** | `IPSet.RestMeta.DELETE_PERMS` |
76
+ | `FIREWALL_LOG_PERMS` | `LOGS_ADMIN_PERMISSIONS` (reused) | `logit.Log.RestMeta.VIEW_PERMS` |
77
+ | `GEOFENCE_VIEW_PERMS` | `sys.view_geofence`, `sys.manage_geofence`, `sys.security` | `@requires_global_perms` on `GET /api/geo/rules` |
78
+ | `GEOFENCE_MANAGE_PERMS` | `sys.manage_geofence`, `sys.security` | `POST`/`DELETE /api/geo/rules` |
79
+ | `SECURITY_EVENTS_PERMS` | `SECURITY_VIEW_PERMS` (reused) | `incident.Event.RestMeta.VIEW_PERMS` |
80
+ | `METRICS_GLOBAL_VIEW_PERMS` | `sys.view_metrics`, `sys.metrics` | `metrics/rest/helpers.check_view_permissions` for `account="global"` |
81
+
82
+ Note the two traps the table encodes:
83
+
84
+ - **`IPSET_DELETE_PERMS` is narrower than manage.** The broad `security` grant
85
+ saves an IP set but cannot delete one.
86
+ - **`manage_metrics` does NOT imply `view_metrics`.** An operator holding only
87
+ the manage grant is denied `account="global"` reads, so the blocks tab's
88
+ metrics strip gates on the view clause rather than firing and catching a 403.
89
+
90
+ A permission-disabled query passes `enabled: false`. **No denied surface in this
91
+ module issues a request** — not the metrics strip, not the blocks table, not the
92
+ change history, not the posture header's Last-change chip.
93
+
94
+ ## Endpoints
95
+
96
+ | Endpoint | Gate | Notes |
97
+ |---|---|---|
98
+ | `GET/POST /api/incident/ipset` `(+/<pk>)` | model `VIEW`/`SAVE`/`DELETE_PERMS` | graphs `default` · `detailed`; actions `sync`, `enable`, `disable`, `refresh_source` |
99
+ | `GET /api/system/geoip` `(+/<pk>)` | #1291's model | Blocked IPs is the `is_blocked=true` projection |
100
+ | `GET /api/logs` | `logit.Log` | filtered `kind__startswith=firewall:` |
101
+ | `GET/POST/DELETE /api/geo/rules` | global geofence grants | `POST` body is `{rule}`, a FULL REPLACE |
102
+ | `POST /api/geo/simulate` | global geofence view | body keys `ip`, `geo`, `scope`, `group_uuid` |
103
+ | `GET/POST /api/geo/allowlist` | view / manage | `POST` body is `{entries}`, a FULL REPLACE |
104
+ | `GET /api/geo/bypass_holders` | global geofence view | read-only, capped at 200 |
105
+ | `GET /api/incident/event` | `SECURITY_EVENTS_PERMS` | `category=geofence_block\|geofence_exempt\|geofence_config` |
106
+ | `GET /api/metrics/category_slugs` | `check_view_permissions` | **flat envelope — no `data` wrapper** |
107
+ | `GET /api/metrics/fetch` | same | `account="global"` |
108
+
109
+ ### `GET /api/geo/rules` — the config payload, key for key
110
+
111
+ ```jsonc
112
+ {
113
+ "system": { "rule": {…}, "source": "setting|conf|none", "modified": "ISO|null" },
114
+ "group": { "id", "uuid", "is_active", "rule", "strict_posture", "strict_posture_effective" },
115
+ "posture": {
116
+ "enabled", "fail_closed", "fail_closed_scopes",
117
+ "allow_private_ips", "strict_posture", "cache_ttl"
118
+ },
119
+ "allowlist_summary": { "setting_entries", "geoip_active" },
120
+ "evaluation_order": ["system", "group"],
121
+ "enforced_endpoints": [{ "endpoint", "scope", "after_auth?" }]
122
+ }
123
+ ```
124
+
125
+ `group` is present **only** when the request carried `group_uuid`; an unknown
126
+ uuid is a 400, and an *inactive* group is deliberately returned and evaluated.
127
+ `after_auth` appears only when truthy.
128
+
129
+ ### The decision shape
130
+
131
+ `_build_decision` writes `allowed`, `reason`, `detail`, `ip`, `country`,
132
+ `country_code`, `region`, `region_code`, `abuse`, `checked_at`, `rule_level`,
133
+ `strict_posture`. `country` and `country_code` carry the **same** value, as do
134
+ `region` and `region_code`.
135
+
136
+ **`enabled` sits at the TOP level of a simulate decision** (`engine.py:473`) and
137
+ is absent from `check()` decisions entirely. `would_block`, `would_block_reason`,
138
+ `allowlist_source`, `allowlist_reason` and `allowlist_until` appear **only** when
139
+ the IP matched the allowlist — and `would_block` is `null` (not `false`) when the
140
+ shadow decision was itself a `lookup_failed`, because the engine genuinely does
141
+ not know. `_allowlisted_decision` never copies `rule_level`, so an exempt
142
+ decision's rule level is always `null`.
143
+
144
+ ### `/api/metrics/category_slugs` — the flat-envelope exception
145
+
146
+ This route returns a raw `JsonResponse`, so the payload is **not** nested under
147
+ `data`:
148
+
149
+ ```jsonc
150
+ { "slugs": [...], "category": "geofence", "account": "global", "status": true }
151
+ ```
152
+
153
+ The `account` default is `"public"` while `metrics.record()` defaults to
154
+ `"global"` — so a geofence slug is invisible unless `account=global` is asked
155
+ for explicitly. `get_category_slugs` returns a **set**, so slug order is
156
+ nondeterministic and no consumer may depend on it.
157
+
158
+ ## The rule DSL and the guided↔advanced projection
159
+
160
+ Top-level keys `{country, region, abuse}`; operators `{in, not_in, eq}`; abuse
161
+ flags `{tor, vpn, datacenter, proxy}`. Abuse semantics: `false` blocks when the
162
+ flag is detected, `true` REQUIRES the flag (rare), absent/`null` means don't
163
+ care.
164
+
165
+ `geofence-data.ts` is the ONE definition of the lossy projection between that
166
+ DSL and the friendly form. `apps/portal/src/pages/group-sections/geofence-data.ts`
167
+ is a re-export shim over it, so the platform page and the group panel cannot
168
+ drift. `GeofenceRuleEditor` is the ONE editor both render.
169
+
170
+ The projection's contract:
171
+
172
+ - `isAdvancedRule(rule)` — true for anything the guided form cannot represent:
173
+ unknown top keys, non-dict bodies, `country.eq`, multiple operators, an
174
+ allow+block mix, any `region` shape other than a single `not_in` of US state
175
+ codes, and any abuse `true` or unknown flag.
176
+ - An advanced-forced rule **opens in JSON with no toggle** — flipping to guided
177
+ would silently drop clauses.
178
+ - The toggle back to guided **refuses** invalid JSON and unrepresentable shapes;
179
+ the guided→JSON direction carries unsaved edits across.
180
+ - `buildGroupRulePayload(old, new)` — GROUP layer only. django deep-merges
181
+ JSONFields (`null` deletes a key; nested `__replace` is NOT supported), so the
182
+ payload is the new value for every kept key plus explicit `null`s for anything
183
+ removed. Sub-keys are **allowlisted**, so a `__replace` typed into the advanced
184
+ editor is dropped rather than PATCHed into stored metadata.
185
+ - The platform layer never merges: `POST /api/geo/rules` is a full replace, and
186
+ the two surfaces share no save path. **The group surface can only tighten and
187
+ can never write platform rules; the global surface never writes group
188
+ metadata.**
189
+
190
+ Every reason code in `engine._DETAIL_MAP` has plain-language copy, including
191
+ **`no_rules_strict`** — the strict-posture denial web-mojo had no text for.
192
+ An unknown code degrades to a readable fallback **and warns once**.
193
+
194
+ ## Event metadata keys
195
+
196
+ `services/geofence/evidence.py` writes, for `geofence_block`: `reason`,
197
+ `rule_level`, **`geofence_scope`**, `country_code`, `region_code`, `abuse`,
198
+ `detail`, and `username` when the request was authenticated. For
199
+ `geofence_exempt`: `reason`, `geofence_scope`, `allowlist_source`,
200
+ `allowlist_reason`, `would_block_reason`, `country_code`, `region_code`. For
201
+ `geofence_config`: `target`, `old`, `new`, **`changed_by`**, `changed_by_id`.
202
+
203
+ Two keys this module reads that web-mojo got wrong:
204
+
205
+ - It is **`metadata.geofence_scope`**, never `metadata.scope`. The reporter
206
+ never passes the reporter's `scope=` argument, so the top-level `Event.scope`
207
+ column stays `"global"` for every geofence row.
208
+ - The config "who" is **`metadata.changed_by`** (a username), falling back to
209
+ `metadata.user_name` (the display name, written by
210
+ `reporter._create_event_dict`). `metadata.username` is written by neither.
211
+
212
+ `Event.country_code` is a real indexed column — `Event.sync_metadata` fills it
213
+ from the geolocated source IP — so country filtering uses the column, not the
214
+ metadata copy.
215
+
216
+ `_block_level`: **7** rule_invalid (a broken rule is denying traffic; the default
217
+ incident threshold auto-creates an Incident) · **6** lookup failure while failing
218
+ open (enforcement silently is not happening) · **5** abuse-flag block, a block on
219
+ a fail-closed scope, or any block under strict posture · **3** ordinary
220
+ jurisdiction block, exemption used, or config change.
221
+
222
+ ## IP sets
223
+
224
+ `KIND_CHOICES` are `country · datacenter · abuse · custom`. `SOURCE_CHOICES` has
225
+ **five** members — `ipdeny`, `abuseipdb`, `tor`, `blocklist_de`, `manual`.
226
+ web-mojo listed three, so the two cache-only rows showed a raw slug and could not
227
+ be filtered.
228
+
229
+ ### Records are created disabled, and `is_enabled` is never a plain field
230
+
231
+ `on_action_enable` is the **only** path that runs the cache-only rejection.
232
+ Writing `is_enabled` as a plain field produces a row that reads "Enabled" and
233
+ silently never syncs, because `sync()` no-ops for a cache-only set without
234
+ raising. So:
235
+
236
+ - the create form has no enable switch and the POST body carries no
237
+ `is_enabled` — a set is created **staged**;
238
+ - the edit form has no `is_enabled` either;
239
+ - enabling and disabling are armed **actions**.
240
+
241
+ Direct #1097 lineage; the verifier asserts it against source.
242
+
243
+ ### `tor_exits` and `blocklist_de` are cache-only
244
+
245
+ `IPSet.THREAT_CACHE_SETS`. They exist to feed GeoIP threat *detection*, not the
246
+ kernel firewall. `enable` answers a 400 that names the set:
247
+
248
+ > `'tor_exits' is a cache-only threat list for geoip detection — enabling it
249
+ > would kernel-block every listed IP fleet-wide and is not permitted`
250
+
251
+ The UI surfaces that message verbatim, chips the rows **CACHE-ONLY**, and
252
+ excludes them from the batch Enable and Sync eligibility so a mixed selection
253
+ still succeeds for the rest.
254
+
255
+ ### `data` must be posted as a LIST
256
+
257
+ `IPSet` defines `set_data(cidr_list)` = `"\n".join(cidr_list)`, and
258
+ `on_rest_save_field` prefers a `set_<key>` method over a plain assignment. So a
259
+ posted `data` runs through the setter:
260
+
261
+ - a **JSON list** is correct, and `cidr_count` is recomputed from it;
262
+ - a **string** gets a newline interleaved between every *character* and
263
+ `cidr_count` becomes the character count.
264
+
265
+ web-mojo posted the raw textarea string. `parseCidrLines()` is the boundary here;
266
+ the mock reproduces both behaviours so the trap stays executable.
267
+
268
+ This also corrects a claim recorded during scoping: `cidr_count` is **not**
269
+ frozen at 0 for manual sets forever. It is stale only until the list is next
270
+ *written* — by a source refresh, or by saving the list from the editor.
271
+ `refresh_from_source()` really does return `False` immediately for
272
+ `source == 'manual'`, so the *refresh* path alone can never update it. The CIDR
273
+ Data tab shows the parsed line count beside the stored one whenever they differ.
274
+
275
+ ## Firewall log — column honesty
276
+
277
+ `GeoLocatedIP` writes these rows through `MojoModel.log`, which fills `path`,
278
+ `ip`, `username` and `uid` from the **ambient request** — the admin's endpoint
279
+ and the admin's address. The blocked address lives only in `payload.ip`.
280
+ web-mojo labelled `path` as "IP / Path", which was never either.
281
+
282
+ The four `kind` values are exact, and the payload shape is **not** uniform:
283
+
284
+ | kind | payload keys |
285
+ |---|---|
286
+ | `firewall:block` | `ip, reason, ttl, blocked_until, block_count, trigger` |
287
+ | `firewall:unblock` | `ip, reason, trigger` |
288
+ | `firewall:whitelist` | `ip, reason, until, was_blocked, trigger` |
289
+ | `firewall:unwhitelist` | `ip, trigger` |
290
+
291
+ ## Blocked IPs
292
+
293
+ The `is_blocked=true` projection over #1291's `GeoLocatedIPModel`. This module
294
+ defines **no** model for `/api/system/geoip` — one endpoint, one `defineModel`,
295
+ one cache key.
296
+
297
+ - **Enforcement is computed.** `block_active` and `whitelist_active` are Python
298
+ properties serialized only on the `basic` graph, so `blockActive(row)` /
299
+ `whitelistActive(row)` (#1291's helpers) decide the State column: a whitelist
300
+ beats a block, an expired block stops enforcing, and `is_blocked` stays set
301
+ either way.
302
+ - **`blocked_until = null` means PERMANENT** and renders "Never", not blank.
303
+ - The threat filter is a multiselect over the four real values plus an explicit
304
+ `threat_level__isnull` filter. web-mojo offered a literal `none` value that
305
+ matched no row on any deployment.
306
+ - The search placeholder names the model's real `SEARCH_FIELDS`
307
+ (ip / city / country name / ASN org / ISP). There is no rule text on this model.
308
+ - Batch **Unblock** and **Whitelist** collect one reason per batch through
309
+ `prepare`; whitelist sends the DICT form `{reason, until}` so the audit trail
310
+ and `whitelisted_until` are both real (the source sent a bare string).
311
+
312
+ ## Exemptions and the full-replace race
313
+
314
+ `POST /api/geo/allowlist` replaces the whole `GEOFENCE_ALLOWLIST` setting, so a
315
+ stale local copy silently deletes a concurrent editor's entry. Every write here
316
+ **refetches and compares first** and aborts with an explanatory message when the
317
+ server list has moved (`AllowlistRaceError`). An empty list is a legitimate
318
+ CLEAR, not "no change" — removing the last entry asks explicitly.
319
+
320
+ Expired entries are listed with `active: false` and rendered inactive, never
321
+ hidden: an auditor needs to see that an exemption existed.
322
+
323
+ Removing an individual IP whitelist resolves the record through a **cached read**
324
+ (`?ip_address=`), not `/geoip/lookup` — the row exists by definition, so removal
325
+ costs no provider call and no rate-limit budget. Adding one does use `/lookup`,
326
+ because the address may never have been seen.
327
+
328
+ ## Degrading on a backend without the config plane
329
+
330
+ `/api/geo/*` is simply **not registered** on an older django-mojo, so the wire
331
+ answer is a **404**, not an error envelope. `isGeofenceApiMissing(error)` is the
332
+ branch; the copy names the documented floor (v1.2.42) as prose and asserts no
333
+ version the client can verify — there is no version endpoint. Any other status
334
+ shows the server's own message.
335
+
336
+ ## Armed confirmation
337
+
338
+ Anything that reaches the fleet is an `ArmedButton` whose armed label names the
339
+ blast radius: the rules replace (with a plain-language clause diff), override
340
+ removal, ipset enable / disable / sync / refresh / delete, allowlist removal and
341
+ whitelist removal. Operations that need input (a reason, an expiry) use
342
+ `formModal` instead — an armed button cannot collect input.
343
+
344
+ ## Absent by design
345
+
346
+ Do not add these back.
347
+
348
+ 1. **Batch delete of IP sets.** Multi-selecting kernel firewall sets and deleting
349
+ them in one irreversible action is exactly the shape #1097 warned about.
350
+ Delete is single-record and armed, from the detail modal.
351
+ 2. **Creating a block from the Blocked IPs table.** The backend supports it and
352
+ web-mojo did not expose it either. Unblock and whitelist — both *relaxing* —
353
+ are exposed.
354
+ 3. **Editing geofence posture.** `GEOFENCE_ENABLED`, `GEOFENCE_FAIL_CLOSED`,
355
+ `GEOFENCE_FAIL_CLOSED_SCOPES`, `GEOFENCE_CACHE_TTL` and
356
+ `GEOFENCE_STRICT_POSTURE` are plain `Setting` rows with no validated
357
+ geofence-specific endpoint. The header is read-only and points at Runtime
358
+ Settings.
359
+ 4. **Granting or revoking `bypass_geofence`.** The backend's own comment says
360
+ grants are managed on user records, and the endpoint returns id/username only
361
+ to avoid leaking user PII through a geofence-only grant.
362
+ 5. **A world map / choropleth.** Owned by #1426 and reserved. The ranked country
363
+ list ships instead and is the same server-derived dataset.
364
+ 6. **A general GeoLocatedIP administration table.** That is #1291's IP
365
+ Intelligence section; the Blocked IP modal links to its dossier.
366
+ 7. **`geoip_sync` / federation controls.** Fleet-peer plumbing, not perimeter
367
+ operation.
368
+ 8. **Event deletion or editing from the blocks log.** The evidence plane is
369
+ append-only by design.
370
+ 9. **A client-side "top blocked countries" computed from the visible page.** It
371
+ would lie about totals whenever the page did not cover the window.
372
+
373
+ ## Verification
374
+
375
+ ```bash
376
+ node scripts/verify-admin-network.mjs # or: npm run verify:admin-network
377
+ ```
378
+
379
+ Headless assertions over the nine permission clauses, section registration and
380
+ route generation for both mounts, the full `geofence-data` mapping table
381
+ (`isAdvancedRule` truth table, form round trip, `buildGroupRulePayload` null-out
382
+ and sub-key allowlist, `coerceRuleInput`, `collectScopes`, `describeDecision`
383
+ over every `_DETAIL_MAP` code, `describeWouldBlock` including the nullable
384
+ shadow, `buildSimulateBody`, `diffRules`), the editor's two refusals, the app
385
+ shim proving ONE module by identity, the source-shape rules (no route/right-panel
386
+ inspection, `modal.detail` present, no `is_enabled` write, no batch delete, no
387
+ block creation, a read-only posture header), the persona 403 matrix across
388
+ `/api/geo/*`, `/api/incident/ipset` and `/api/system/geoip`, and the mock wire:
389
+ graph exclusions, the cache-only 400, the create-disabled default, the
390
+ list-vs-string `data` trap, name uniqueness, delete-perm narrowness, DSL and
391
+ allowlist validation, the empty-list clear, top-level `enabled`, the exempt
392
+ shadow fields, the firewall Log trail with its per-kind payload shape, and the
393
+ flat `category_slugs` envelope. Plus theme byte-identity and tokens-only CSS.
@@ -0,0 +1,27 @@
1
+ # Phone Hub admin (#1295)
2
+
3
+ Import `PhoneHubPage`, the three individual pages, models, permission clauses, and API helpers from `portal-mojo/admin`. `PHONE_HUB_ADMIN_SECTION` registers one global `/phonehub` route in Communications; the page exposes only the Phone Numbers, SMS, and Provider Config tabs the caller may view. Admin global scope masks any outer product group.
4
+
5
+ ## Wire contracts
6
+
7
+ - `POST /api/phonehub/number/normalize` is public. `normalizePhoneNumber()` accepts the backend result only when it is strict E.164.
8
+ - `POST /api/phonehub/number/lookup` requires authentication and is offered only under `PHONE_LOOKUP_UI_PERMISSIONS`. The helper always normalizes first. For a new or expired record it omits `force_refresh` so the backend's normal lookup path refreshes once. For a known-fresh record it requires confirmation, then sends `force_refresh: true`. A successful response must provide a first lookup timestamp/count or advance one of them relative to the prior row; a 2xx stale row is rejected as provider failure evidence.
9
+ - `PhoneNumberModel` requests the default graph but positively sanitizes every row before Query. Raw `lookup_data` is never retained or exported.
10
+ - `SmsModel` is an audit-only surface. It requests the default graph and positively projects known display fields before Query; raw metadata, provider payloads, and provider message ids are discarded. Its client exporter is bounded and omits message bodies, errors, user/group details, and raw fields. There is no send UI.
11
+ - `PhoneConfigModel` reads sanitized scalar/default-graph rows. Saves and `test_connection` use imperative, cache-free calls. Secret inputs are DOM refs, not React or Query state: blank means untouched and is omitted; replacement must be non-empty; clear sends `null` only after a separate confirmation. Responses are allowlisted before cache reconciliation. This surface edits existing provider credentials; it does not provision Django user/group API keys.
12
+
13
+ ## Exact permissions
14
+
15
+ Every exported array is an ANY-of clause and retains the `sys.` UI namespace. Number view is `view_phone_numbers | manage_phone_numbers | comms | manage_users`; save is `manage_phone_numbers | comms | manage_users`; delete is only `manage_phone_numbers`. SMS view is `view_sms | manage_sms | comms | owner | manage_notifications`; save is `manage_sms | comms | manage_notifications`; delete is `manage_sms | manage_notifications`. Config view/save is `manage_phone_config | manage_groups | comms`; delete is `manage_phone_config | manage_groups`. The server remains authoritative.
16
+
17
+ Group choices are fetched only with a separate global group-directory clause and are capped at 100 basic rows. Without that grant the editor does not issue a group-directory request and preserves the existing scope (or creates a system default).
18
+
19
+ ## Provider and deletion reality
20
+
21
+ Phone lookup is always the global Twilio Lookup integration; it is not group-configured. Stored Twilio and AWS credentials can be tested, but the current SMS send implementation only consults a per-group configuration for `provider="mojo"`. Twilio/AWS configurations otherwise fall through to global Twilio send settings, and AWS sending is not wired. `test_mode` only short-circuits connection testing; it does not block sending.
22
+
23
+ An active group config wins; otherwise `PhoneConfig.get_for_group` falls back to the first active system default. Deleting or deactivating a group config restores that fallback. Deleting a group cascades its config and SMS rows. Deleting a user cascades their SMS rows. Deleting a config does not delete SMS audit rows. Deleting an SMS row affects only local audit storage and cannot recall provider delivery.
24
+
25
+ ## Reuse rules
26
+
27
+ Use KISS detail modals, not `:id` routes. Never add provider payloads to a model graph, cache, error, export, or request history. Never reuse `PhoneConfigModel.useSave()` for credentials. Do not add SMS sending or API-key provisioning to this admin area without a new reviewed contract.
@@ -0,0 +1,36 @@
1
+ # Push Notifications admin (#1296)
2
+
3
+ Import `PushPage`, its individual panels/pages, the canonical push models, API helpers, and `PUSH_ADMIN_SECTION` from `portal-mojo/admin`. The section contributes one global/no-group Communications route at `/push`; there are no record routes. KISS detail modals own record inspection.
4
+
5
+ ## Tabs and URL state
6
+
7
+ The page has six possible tabs: Your stats, Global metrics, Devices, Deliveries, Templates, and Config. Stats are authenticated caller data. Every other tab is independently filtered by its exact permission clause, and only the active authorized panel mounts, so hidden/inactive surfaces issue no requests.
8
+
9
+ `push_surface` is registered with `registerNonFilterParams` at module load. It survives table writes but never becomes a Django lookup or filter pill. Changing tabs replaces the flat URL params with only `push_surface`, intentionally clearing search, sort, paging, and filters that belong to the prior model.
10
+
11
+ ## Exact permissions
12
+
13
+ All UI clauses carry the `sys.` namespace and are ANY-of:
14
+
15
+ - Devices view: `view_devices | manage_devices | comms | owner | manage_users`; save: `manage_devices | comms | owner`.
16
+ - Deliveries view: `view_notifications | manage_notifications | comms | owner | manage_users`; save: `manage_notifications | comms`.
17
+ - Templates view: `manage_notifications | manage_groups | comms | owner | manage_users`; save: `manage_notifications | manage_groups | comms`.
18
+ - Config view/save: `manage_push_config | manage_groups | comms`.
19
+ - Config connection test: `manage_push_config | comms`.
20
+ - Global metrics: `view_metrics | metrics`, with `account="global"`.
21
+
22
+ `sys.users` is deliberately absent: it is not in `RegisteredDevice.RestMeta.VIEW_PERMS`. The Stats endpoint requires authentication only and always scopes all five keys to `request.user`.
23
+
24
+ ## Projection and ownership boundaries
25
+
26
+ Every model pins a `basic` or `default` graph, allowlists list params, and positively projects rows before Query or Mutation cache reconciliation. RegisteredDevice never retains `device_token`; Delivery never retains `platform_data`, and its arbitrary `data_payload` is not retained by the UI projection. Related users are reduced to id plus a short display label—never email, phone, metadata, or an expanded user graph. Template JSON objects accept only a bounded shallow scalar projection on reads.
27
+
28
+ `PushDeviceModel` now lives canonically in `admin/messaging/push`. The former `admin/identity/users/models.ts` definition is a compatibility re-export of the same object, so User Detail and the global Push page share one endpoint definition and Query key.
29
+
30
+ ## FCM credentials and actions
31
+
32
+ FCM writes are imperative and cache-free. A service account textarea is an uncontrolled DOM ref and never prefills. Blank means untouched and is omitted. Replacement must parse to an object with `type="service_account"`, `project_id`, `client_email`, and a PEM `private_key`. Clear sends `null` only after explicit confirmation. Empty replacement is impossible, and server responses are sanitized before cache reconciliation.
33
+
34
+ Connection testing calls only `POST /api/account/devices/push/config/<id>/test` with an empty body. The backend uses its dummy token to validate credentials. The portal never accepts a device token. It also does not expose `/send`, caller `/test`, delivery retry, device registration/unregistration, or any delete action.
35
+
36
+ All four REST models have live `CAN_DELETE = false`; the mock returns 403 without removing or cascading anything. Templates/configs can be created and edited under their save clauses. Devices and deliveries remain investigative here.
@@ -0,0 +1,87 @@
1
+ # Rule Engine admin
2
+
3
+ Import models, routes, transforms, and the builder from `portal-mojo/admin`:
4
+
5
+ ```tsx
6
+ import {
7
+ RuleSetModel, RuleModel, HandlerChainBuilder,
8
+ parseHandlerChain, runtimeEffectiveHandlerChain,
9
+ } from 'portal-mojo/admin';
10
+ ```
11
+
12
+ The shared Security Operations section mounts the `rules` table after Tickets,
13
+ Incidents, and Events. Selecting a RuleSet opens its DetailView through the
14
+ shared KISS `modal.detail` surface; there is no `rules/:id` child route, so the
15
+ table URL and query state remain unchanged. The mount-relative table works at
16
+ `#/security/rules` in the standalone Admin and `#/system/security/rules` when
17
+ embedded. Reads require
18
+ `sys.view_security|sys.security`; mutations require
19
+ `sys.manage_security|sys.security`. These are system-pinned clauses: active
20
+ group grants do not satisfy them, and the server remains authoritative.
21
+
22
+ ## Wire models and safety boundary
23
+
24
+ `RuleSetModel` uses `/api/incident/event/ruleset`; `RuleModel` uses
25
+ `/api/incident/event/ruleset/rule`. Row types intentionally remain open and
26
+ fetched rows are not reconstructed, so future top-level fields, metadata,
27
+ enum values, comparators, and value types remain in TanStack Query unchanged.
28
+
29
+ Rule-set creation always sends `is_active:false`, regardless of form input.
30
+ An empty active RuleSet is a category catch-all and can preempt later
31
+ priorities, so enabling a zero-condition set is a separate warning that names
32
+ its category and priority. The create defaults are priority 10, ALL matching,
33
+ SOURCE_IP bundling for 30 minutes, and an empty handler. `bundle_minutes=0`
34
+ disables time bundling; `null` means the current backend's unbounded window.
35
+ Count thresholds require effective bundling, trigger/retrigger windows require
36
+ a count, and a finite trigger window cannot exceed the finite bundle window.
37
+
38
+ Rule transforms reject leading-underscore fields and validate integer/float
39
+ comparison values. Existing `bool` rules are visible and preservable, but
40
+ creating or changing a bool comparison is blocked: the backend currently
41
+ applies Python `bool()` to stored strings, so the string `"false"` is true.
42
+
43
+ ## Handler grammar and preservation
44
+
45
+ Supported runtime schemes are `job`, `email`, `sms`, `notify`, `block`,
46
+ `ticket`, `maestro`, `llm`, and `resolve`. Notification targets may contain
47
+ commas (`perm@…`, `protected@…`, usernames). The editor recognizes a chain
48
+ boundary only at comma + URI scheme, preserving target commas.
49
+
50
+ There are deliberately two projections:
51
+
52
+ - The editor projection exposes any URI scheme so unsupported content is
53
+ visible and movable/removable only by explicit operator action.
54
+ - The runtime-effective projection exactly mirrors django-mojo's split:
55
+ comma immediately followed by a *known* scheme. Leading unknown segments
56
+ are skipped; unknown or whitespace-prefixed segments after a known handler
57
+ can be swallowed into that handler's spec. The builder warns about both.
58
+
59
+ Every step retains its raw bytes and preceding separator. Parsing then
60
+ serializing an untouched chain is byte-for-byte exact. Structured changes are
61
+ surgical: ordered query entries and unknown parameters remain, duplicate keys
62
+ remain, and the first duplicate is the runtime-effective value that edits
63
+ target. Moving/editing/removing skipped or swallowed content requires an
64
+ explicit high-severity behavior-change confirmation. An empty string is valid:
65
+ the incident is recorded and no action is dispatched.
66
+
67
+ Handler order is publication/dispatch order. Jobs run asynchronously, so the
68
+ UI does not promise completion order. Structural validation covers URI shape,
69
+ percent encoding, required targets/module paths, integer/range values, blank
70
+ parameter keys, and resolve statuses. It cannot prove target or permission
71
+ existence, job importability, Maestro board existence, runtime settings, or
72
+ Python regex semantics; failed server mutations reject visibly and leave the
73
+ editor open.
74
+
75
+ ## Reordering limits
76
+
77
+ The whole handler chain is one RuleSet text field, so a chain edit/reorder is
78
+ one atomic save. RuleSet priorities and child Rule indexes are separate rows;
79
+ django-mojo exposes no atomic multi-row reorder action and `CAN_BATCH` is
80
+ false. Drag reorder is therefore absent. Operators edit one numeric value at
81
+ a time, and duplicate priorities/indexes are flagged because tie order is
82
+ undefined.
83
+
84
+ The showcase demo is under **Admin → Rule Engine** and includes supported
85
+ steps, notification target commas, duplicate/extra parameters, unsupported
86
+ content, structural errors, move/remove actions, exact raw serialization, and
87
+ the empty-chain case.
@@ -0,0 +1,40 @@
1
+ # Admin Security Tickets
2
+
3
+ `SECURITY_OPERATIONS_ADMIN_SECTION` adds the mount-relative `security/tickets` route to the shared admin registry. Consumers normally mount it through `ADMIN_SECTIONS`; direct imports are available from `portal-mojo/admin`.
4
+
5
+ ## Permissions
6
+
7
+ The section and route use `sys.view_security` or `sys.security`. Mutating controls require `sys.manage_security` or `sys.security`; delete uses `sys.manage_security`. Assignee lookup is offered only with `sys.view_users`, `sys.manage_users`, or `sys.users`. These are UI gates only—the server remains authoritative for every request.
8
+
9
+ ## Wire contracts
10
+
11
+ - Ticket collection/detail: `/api/incident/ticket`. List state uses the standard Django lookups, graph relations, paging, sorting, search, and `download_format=csv|json`. Saves accept title, description, arbitrary status/category strings, integer priority 1–10, and a nullable assignee ID.
12
+ - Notes: `/api/incident/ticket/note`, always scoped with the ticket `parent` and, for grouped tickets, the ticket's own `group`. Operator notes require text and may atomically add one completed existing File id as `media`. Responses expose only File's reference graph. A status save creates a structured note with `metadata.type=status_change`, `old_status`, and `new_status`.
13
+ - Ticket actions: detail POST bodies use `enable_llm`, `disable_llm`, or `push_to_maestro`. Push success means queued, not linked.
14
+ - Maestro links: `/api/incident/maestro/item-link?ticket=<id>`. The detail modal polls for at most 30 seconds after a queued push and always offers a manual check.
15
+
16
+ Unknown status/category values are appended to the known UI options and are never coerced away. Relation values may be a graph object, a bare ID, or null.
17
+
18
+ ## Cache and approval boundaries
19
+
20
+ Every ticket mutation invalidates the Ticket model root, the record-scoped TicketNote feed prefix, and Maestro link lists. Approval and denial are new TicketNote rows with `metadata.action_response`. The response copies the pending action's `handler` and `context` without reconstructing or exposing editable JSON. Unknown handlers render a generic informational action card; the client never executes handler-specific navigation or code.
21
+
22
+ Resolved actions, terminal tickets, missing manage permission, and in-flight responses disable approval controls. The server still owns replay protection and handler authorization.
23
+
24
+ ## Detail modal behavior
25
+
26
+ Ticket titles are real buttons and both title clicks and row clicks open the
27
+ shared KISS `modal.detail` surface without changing the current route or table
28
+ params. The modal fetches the authoritative detail row and uses `DetailView`
29
+ sections for Overview, permission-gated Manage controls, and Activity.
30
+ Nested edit and confirmation modals use the native dialog stack, and closing
31
+ the detail returns to the unchanged queue. `RecordFeed` uses compact mode and
32
+ receives `showInput={canManage}`.
33
+
34
+ Activity notes offer the shared capacity-one upload queue only while manage
35
+ permission and an authoritative ticket/group are current. Grouped uploads use
36
+ the ticket's group, groupless uploads use personal storage, and the completed
37
+ manager/group lifecycle response must match before `media` can be sent. Queue
38
+ removal never deletes File. Assistant chat and incident-detail navigation
39
+ remain separate, and a remote Maestro URL becomes a link only when it parses as
40
+ HTTP or HTTPS.