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
package/docs/charts.md ADDED
@@ -0,0 +1,303 @@
1
+ # charts — dependency-free SVG
2
+
3
+ ```ts
4
+ import {
5
+ SeriesChart, MetricsChart, MiniChart, MetricsMiniWidget,
6
+ KPITile, KPIStrip, CircularProgress, PieChart,
7
+ showSeriesStats, showSeriesData, exportChartPng,
8
+ computeSeriesStats, computeTrend, deltaBadge, // pure math
9
+ } from 'portal-mojo/charts';
10
+ ```
11
+
12
+ No chart library — web-mojo's SVG charts ported whole; they match the
13
+ design tokens in both themes. Demos: Develop → Components → Display.
14
+
15
+ ## The metrics wire (all metrics-driven components speak it)
16
+
17
+ `GET /api/metrics/fetch` params:
18
+
19
+ | param | meaning |
20
+ |---|---|
21
+ | `slugs` | comma list, **REQUIRED** (the server 400s without; singular `slug` exists but never send it — prod once rejected it) |
22
+ | `granularity` | `minutes/hours/days/weeks/months` (server default `hours`) |
23
+ | `account` | `'public'` (server default) / `'global'` / `'group-<id>'` / `'user-<id>'` — components default to `'global'` (web-mojo parity) |
24
+ | `dt_start` / `dt_end` | window bounds, **epoch SECONDS** (rule 6) |
25
+ | `category` | optional category passthrough via `apiParams` |
26
+ | `child_kind`, `breakdown` | typed `MetricsChart` fan-out props (`childKind`, `breakdown`) |
27
+
28
+ Response is a slug-keyed map `{data: {slug: number[]}, labels: string[]}` —
29
+ normalized ONLY by `mojoMetrics` in the client into `{labels, datasets}`.
30
+ Malformed feeds degrade to the empty state WITH a console.warn — never a
31
+ crash.
32
+
33
+ **Two windowing params ride together, deliberately:** the real backend uses
34
+ `dt_*` and ignores `range`; components send both for quick ranges and only
35
+ `dt_*` for custom ranges. The mock now gives `dt_start`/`dt_end` precedence,
36
+ accepts either bound alone, and mirrors the backend's inclusive buckets.
37
+ Reverse ranges return an empty series; malformed non-epoch bounds reject.
38
+ Month/year buckets advance by calendar boundaries, not 30/365-day arithmetic.
39
+ The mock caps a pathological request at 400 buckets to keep the showcase
40
+ responsive. It also salts values by `account` so entity-scope demos visibly
41
+ change; that salt is a mock-only presentation affordance, not a server promise.
42
+
43
+ **Trap (fixed here, do not regress):** `dr_start/dr_end` is the model-LIST
44
+ daterange triple — `/api/metrics/fetch` ignores it silently. web-mojo sent
45
+ `dr_*` to metrics for years; its custom ranges never reached the backend.
46
+
47
+ ## SeriesChart (presentational)
48
+
49
+ ```tsx
50
+ <SeriesChart
51
+ labels={['Mon', 'Tue']}
52
+ datasets={[{ label: 'API Calls', data: [420, 380] }]}
53
+ chartType="line" // 'line' | 'bar' | 'area'
54
+ stacked="auto" // bars stack by default
55
+ height={240}
56
+ showLegend showGrid showXLabels showYLabels
57
+ valueFormatter={(n) => ...}
58
+ />
59
+ ```
60
+
61
+ Legend entries toggle series; hover shows the crosshair index tooltip;
62
+ ticks are "nice"-rounded; container width is measured (no viewBox
63
+ smearing).
64
+
65
+ ## MetricsChart (the django-mojo metrics widget)
66
+
67
+ ```tsx
68
+ <MetricsChart
69
+ title="Platform activity"
70
+ slugs={['api_calls', 'user_activity_day', 'api_errors']}
71
+ seriesLabels={{ api_calls: 'API Calls' }} // wire carries raw slugs
72
+ account="global"
73
+ defaultRange="24h" // 1H / 24H / 7D / 30D
74
+ defaultGranularity="hours" // gated per range (MIN HR DAY WK MO)
75
+ defaultType="line"
76
+ height={280}
77
+ valueFormatter={(n) => ...} // tooltips, ticks, dialogs
78
+ childKind="store" breakdown={false} // typed fan-out; overwrites apiParams
79
+ loadSeries={exactLoader} // optional; default is mojoMetrics
80
+ seriesCacheKey="caller-42:exact-history" // required stable namespace for custom loaders
81
+ preserveSeriesLabels // keep full-slug/child identity verbatim
82
+ allowedGranularities={['minutes','hours','days']} // optional backend subset; unsupported controls omitted
83
+ refreshSignal={refreshVersion} // changed by a parent refresh owner; exactly one fetch
84
+ // toggles, all default true:
85
+ showGranularity showDateRange showTypeSwitch showRefresh showStats showDataTable
86
+ />
87
+ ```
88
+
89
+ - **Stats (ⓘ)** — per-series Latest/Min/Max/Avg/Median/Sum dialog.
90
+ **View data (table)** — the series as a table + Download CSV. Both are
91
+ native `<dialog>`s via the modal manager; both render the DISPLAY names
92
+ (`seriesLabels`-mapped).
93
+ - **Custom range** — the calendar button in the range segment opens a
94
+ DateRangePicker (presets rail) dialog with an explicit Apply. An applied
95
+ range shows as a chip beside the title, sends `dt_start/dt_end` (local
96
+ day bounds 00:00:00–23:59:59 → epoch seconds), and re-points granularity
97
+ at a bucket that fits the span (`granularitiesForSpanMs`).
98
+ - Quick ranges anchor their window at pick time; **Refresh re-anchors at
99
+ now** (web-mojo kept the stale window — deliberate fix). A quick-window
100
+ refresh changes the query key and does not also call `refetch`, preventing
101
+ the former duplicate request. `refreshSignal` follows the same one-request
102
+ path for a parent-owned refresh control. Range switches
103
+ re-point granularity at the nearest sensible bucket.
104
+ - `allowedGranularities` additively narrows the control vocabulary for a
105
+ backend such as CloudWatch. Values outside the supported subset are not
106
+ rendered; normal metrics callers retain all five granularities.
107
+ - `loadSeries(params)` receives typed `slugs`, `account`, `granularity`,
108
+ `dt_start`, `dt_end`, and optional fan-out fields. The loader namespace is
109
+ part of the TanStack key, preventing a custom exact-identity loader from
110
+ reusing a cached default response with the same wire params.
111
+ - The default loader remaps live tail-only response keys back to configured
112
+ full slugs when that mapping is unique. Duplicate tails remain an explicit
113
+ error and require an exact loader that can split/reassemble requests.
114
+ - CSV data export neutralizes formula-looking **string** cells before RFC
115
+ quoting. Negative numeric metrics remain numeric.
116
+ - Deliberately NOT carried: `compactHeader` (MetricsMiniWidget IS the
117
+ compact card), `.export()` (removed in web-mojo too — use
118
+ `exportChartPng`), `withDelta` endpoint switching (KPIStrip covers the
119
+ KPI use-case).
120
+
121
+ ## MiniChart (sparkline primitive)
122
+
123
+ ```tsx
124
+ <MiniChart data={[4, 9, 6]} labels={['Mon','Tue','Wed']} chartType="line"
125
+ height={48} color="var(--accent)" fill smoothing={0.3}
126
+ showTooltip showCrosshair showXAxis={false}
127
+ minValue={} maxValue={} softMin={} softMax={} />
128
+ ```
129
+
130
+ Single series, line or bar. Hard crops (`minValue/maxValue`) win over bar
131
+ soft bounds (`softMin/softMax` — normalize-to targets that expand if data
132
+ exceeds). Bars always include zero; the x-axis sits at zero when data
133
+ crosses it. All-zero bar data renders the dashed "alive, just zero"
134
+ baseline instead of blank. Draw-in animation is pathLength-normalized CSS
135
+ (no measuring); `prefers-reduced-motion` kills it. Colors accept `var()`
136
+ tokens — the default fill is the stroke at 0.12 opacity, so one token
137
+ drives both.
138
+
139
+ ## MetricsMiniWidget (compact card + entity search)
140
+
141
+ ```tsx
142
+ <MetricsMiniWidget
143
+ title="Logins" icon="bi bi-box-arrow-in-right"
144
+ slugs={['user_activity_day']} // FIRST slug drives the card
145
+ granularity="hours" defaultRange="24h" chartType="line"
146
+ subtitle={(ctx) => <><b>{ctx.total}</b> in 24h · {ctx.nowLabel}: {ctx.nowValue}</>}
147
+ showTrending trendRange={6}
148
+ tone="accent" // token-tinted card variant
149
+ showSettings showDateRange settingsKey="dash-activity"
150
+ search={{ // the C2 addition
151
+ model: GroupModel, // or endpoint: '/api/group'
152
+ toAccount: (id) => `group-${id}`, // picked row → account param
153
+ placeholder: 'All groups (global)',
154
+ }}
155
+ onScopeChange={(account, row) => ...}
156
+ />
157
+ ```
158
+
159
+ - **Entity search**: a CollectionSelect bound to any model list — shared
160
+ cache keys, NEVER a parallel fetch path. Picking a record re-fetches
161
+ with `account=toAccount(id, row)`; clearing returns to the base
162
+ `account` prop.
163
+ - **subtitle** is a ReactNode or `(ctx) => ReactNode`; `ctx` is the typed
164
+ twin of web-mojo's Mustache tokens: `{total, nowValue, lastValue,
165
+ prevValue, trendingPercent, nowLabel, totalLabel, granularity}`
166
+ (`nowValue` is ALWAYS the latest bucket; `lastValue` is the
167
+ offset-shifted trend-window sum — source distinction preserved).
168
+ - **Trending** is the source's windowed math (`computeTrend`):
169
+ `trendRange` (k = floor(range/2) buckets per window), `trendOffset`
170
+ (skip trailing partial buckets), `prevTrendOffset` (compare N earlier).
171
+ Up renders green / down red (source parity — no tone flip here; KPITile
172
+ has `tone`).
173
+ - **Settings** (gear, off by default): granularity / chart type / date
174
+ range dialog; persisted under localStorage `metrics-chart-<settingsKey>`
175
+ (source key format). A granularity change with a custom window in play
176
+ auto-adjusts the window (hours→24h … years→5y, source table); it never
177
+ sets dates for the first time.
178
+ - `defaultRange={null}` omits window params entirely (backend default
179
+ window — the source MiniChart's behavior when no range was given).
180
+ - Card is single-series by design (source parity); extra slugs still feed
181
+ the stats/view-data dialogs. A response without the first slug warns
182
+ once and shows the empty state.
183
+
184
+ ## KPITile + KPIStrip
185
+
186
+ ```tsx
187
+ <KPIStrip
188
+ tiles={[
189
+ { slug: 'api_calls', label: 'API Calls', tone: 'good' },
190
+ { slug: 'api_errors', label: 'API errors', tone: 'bad', severity: 'warn' },
191
+ { key: 'open', label: 'Open Tickets', value: openCount, sparklineSlug: 'api_calls' },
192
+ ]}
193
+ granularity="days" range="7d" account="global"
194
+ onTileClick={({ slug, key }) => ...}
195
+ />
196
+ ```
197
+
198
+ ONE batched `/api/metrics/fetch` covers values, deltas and sparklines:
199
+ value = latest bucket, delta = latest − previous, `deltaPct` omitted when
200
+ the previous bucket was 0 — the same convention
201
+ `/api/metrics/series?with_delta=true` uses, so a server-delta variant is a
202
+ drop-in later. A spec with `value` set is a caller-value tile (the
203
+ source's REST-count tiles — pages already have `size:0` count queries);
204
+ `sparklineSlug` lets it borrow a metric trail + delta. Missing slugs warn
205
+ once and render '—'.
206
+
207
+ `KPITile` alone is pure presentation: `label`, `value`, `delta`,
208
+ `deltaPct`, `tone` ('bad' = rising is bad), `severity`
209
+ (critical/high/warn/info/good left stripe; high shares the warn token —
210
+ no orange in the palette), `sparkline`, `onClick`, `loading`. Badge rules
211
+ (source verbatim, via `deltaBadge`): finite pct → `+12%` (one decimal
212
+ under 10), else absolute delta → `+4`, else no badge; ±0 renders flat.
213
+ **Never Infinity%.**
214
+
215
+ ## CircularProgress
216
+
217
+ ```tsx
218
+ <CircularProgress value={64} size="md" /> // xs sm md lg xl | px
219
+ <CircularProgress value={64} gap={90} rotation={135} /> // gauge
220
+ <CircularProgress value={7} max={10} valueFormat="fraction" /> // 7/10
221
+ <CircularProgress segments={[{ value: 38, color: 'var(--accent)' }, ...]} max={100} />
222
+ <CircularProgress gradientColors={['var(--accent)', 'var(--ok)']} />
223
+ ```
224
+
225
+ Ring progress with size presets (stroke auto-scales), variants mapped to
226
+ tokens (`success/danger/warning/info` → ok/bad/warn/info), partial-arc
227
+ gauges, rounded caps, center value (`percentage | fraction | value` or a
228
+ `valueFormatter(value, min, max)`), `label`, `icon`, and multi-segment
229
+ mode with `segmentGap` degrees. Value changes glide via the CSS
230
+ dash-offset transition (the imperative `animateTo/pulse` API from the
231
+ source is just… setting the prop). Dropped from source: the Bootstrap
232
+ popover tooltip (pass `title` or wrap it) and the hardcoded 'dark'/'light'
233
+ theme presets (tokens make them meaningless).
234
+
235
+ ## PieChart
236
+
237
+ ```tsx
238
+ <PieChart data={[{ label: 'Chrome', value: 61, color: 'var(--info)' }]} /> // shape 1
239
+ <PieChart data={{ labels: ['a'], datasets: [{ data: [1] }] }} /> // shape 2 (Chart.js)
240
+ <PieChart data={{ US: 42, DE: 21 }} /> // shape 3 (map)
241
+ <PieChart endpoint="/api/some/summary" params={{ days: 7 }} /> // fetched
242
+ <PieChart data={d} cutout={0.62}
243
+ centerLabel={(ctx) => ctx.total.toLocaleString()} centerSubLabel="users" />
244
+ ```
245
+
246
+ Native SVG pie/doughnut. All three web-mojo input shapes; unknown shapes
247
+ warn + empty state. Colors: per-item `color` → `colors[i]` (default
248
+ CHART_PALETTE) → `colorGenerator(i)` (default golden angle — distinct hues
249
+ forever). `legendPosition` right/bottom/none; `showLabels` puts labels at
250
+ the slice edges; tooltip clamps inside the svg area; `onSliceClick` gets
251
+ `{label, value, pct, index}`. Data changes tween label-keyed (existing
252
+ slices slide, new ones grow from a 0-arc; rAF, cancelled on unmount).
253
+ Center labels are strings (SVG `<text>` — not a ReactNode slot). The
254
+ single-full-circle case renders a circle element (SVG arcs can't draw
255
+ 360°). `endpoint` fetches through `mojoCall` (the one boundary) with the
256
+ source's `data.data ?? data` nesting heuristic; fetch errors warn + empty.
257
+ Pure helpers (`parsePieInput`, `buildPieGeometry`, `goldenAngleColor`,
258
+ `interpolateSegments`, `arcPath`) are exported from `pie-math`.
259
+
260
+ ## Stats + view-data dialogs (shared)
261
+
262
+ ```ts
263
+ showSeriesStats({ title, labels, datasets, granularity, formatter });
264
+ showSeriesData({ title, labels, datasets, granularity, formatter }); // + Download CSV
265
+ ```
266
+
267
+ Awaitable native-`<dialog>`s (modal manager). Stats: per-series
268
+ Latest/Min/Max/Avg/Median/Sum with the "Hourly · N points" header. Data:
269
+ one row per label, one column per series; CSV downloads as
270
+ `<title-slug>-<date>.csv` (RFC-safe quoting via `buildCsv`). Empty data
271
+ renders "No data to display." — never a blank dialog.
272
+
273
+ ## exportChartPng
274
+
275
+ ```ts
276
+ exportChartPng(refOrElement, { filename: 'chart.png', background: null, scale: 2 });
277
+ ```
278
+
279
+ Framework-free SVG→PNG: finds the `<svg>` in the target (element or ref),
280
+ clones it, **inlines computed styles** (token-driven fills/strokes/fonts
281
+ would otherwise vanish — theme classes don't serialize), rasterizes at
282
+ `scale` (default 2) over the nearest ancestor's real background (dark
283
+ exports stay dark; pass a color or `null` for transparent), and downloads.
284
+ ⚠️ Tainted-canvas caveat (from the source, still true): an SVG referencing
285
+ cross-origin resources (external `<image href>`, remote fonts) taints the
286
+ canvas and `toDataURL` throws a SecurityError. These charts reference
287
+ none; inline such resources before exporting anything that does.
288
+
289
+ ## Pure math (headless-tested)
290
+
291
+ `computeSeriesStats`, `defaultStatFormat`, `computeTrend`, `deltaBadge`,
292
+ `buildCsv`/`csvEscape`/`csvFilename`, `quickRangeWindow`,
293
+ `granularitiesForSpanMs`, `ymdRangeToEpochSeconds`, `toNumber` — all in
294
+ `charts/stats.ts`, no DOM/React, shared by every surface above (web-mojo
295
+ carried two diverging copies of the stats math; here there is one).
296
+
297
+ ## WorldMap
298
+
299
+ `portal-mojo/charts` also exports `WorldMap` — the dependency-free geo map
300
+ (markers, routes, drill-down, the country centroid table) plus its projection
301
+ math. It has its own page: **[worldmap.md](worldmap.md)**. Note that
302
+ `charts/pie-math` exports `arcPath` and the map's arc helper is therefore
303
+ `geoArcPath`.
package/docs/client.md ADDED
@@ -0,0 +1,206 @@
1
+ # client — the typed django-mojo protocol layer
2
+
3
+ ```ts
4
+ import {
5
+ mojoList, mojoGet, mojoSave, mojoDelete, mojoDownload, mojoMetrics,
6
+ mojoCall, mojoQueryDefaults, usingMockTransport, apiOrigin,
7
+ MojoError, AuthRequiredError,
8
+ } from 'portal-mojo/client';
9
+ ```
10
+
11
+ ## Transport
12
+
13
+ - `VITE_MOJO_API` unset → the in-memory mock answers every call
14
+ (`npm run dev`). Set to a django-mojo origin → real backend
15
+ (`npm run dev:live`, configured in `apps/portal/.env.live`).
16
+ - Every request carries `X-Mojo-UID` (device id) and, once `initAuth()` ran
17
+ and a session exists, `Authorization: Bearer <jwt>`.
18
+ - CORS note (dev, cross-origin): every request shows an `OPTIONS` preflight
19
+ twin in devtools. Not a duplicate fetch.
20
+
21
+ ## Envelope — the ONE unwrap boundary
22
+
23
+ django-mojo wraps everything: `{status, data, …}`. `status: false` (or a
24
+ non-2xx with an envelope body) becomes a **thrown `MojoError(message,
25
+ status, errorCode, data)`** carrying the server's real message, numeric HTTP
26
+ status, semantic `error_code`, and structured safe failure evidence. Nothing outside `client.ts`
27
+ parses envelopes; `mojoCall(path, {method, params, body})` is the typed
28
+ escape hatch for protocol modules and returns the unwrapped `Envelope`.
29
+
30
+ Live non-2xx responses and mock `status:false` envelopes preserve the same
31
+ shape. Modern envelopes use numeric top-level `code` plus a string semantic
32
+ `error_code`; numeric legacy `error_code` remains a status fallback. Failed
33
+ envelopes always reject. Structured `data` can contain deliberately safe
34
+ recovery evidence, but callers must still avoid logging it indiscriminately.
35
+
36
+ ## Functions
37
+
38
+ | Fn | Wire | Returns |
39
+ |---|---|---|
40
+ | `mojoList<T>(endpoint, params)` | `GET endpoint?start&size&sort&search&<lookups>` | `{rows: T[], count, start, size}` |
41
+ | `mojoGet<T>(endpoint, id)` | `GET endpoint/<id>` | `T` |
42
+ | `mojoSave<T>(endpoint, id\|null, changes)` | `POST endpoint[/<id>]` | `T` (server-authoritative row). REJECTS on failure |
43
+ | `mojoDelete(endpoint, id)` | `DELETE endpoint/<id>` | `void` — server answers `{status:"deleted"}` (string; deliberate) |
44
+ | `mojoDownload(endpoint, params, 'csv'\|'json')` | `GET endpoint?download_format&filename&<filters>` (start/size stripped) | triggers a browser download of the WHOLE filtered set |
45
+ | `mojoMetrics(params)` | `GET /api/metrics/fetch?slugs&range&granularity` | `{labels, datasets:[{label: slug, data}]}` — normalized from the wire's `{data:{slug: number[]}, labels}` |
46
+
47
+ Wire facts (measured against live django-mojo): list sort is `'-field'`
48
+ desc; filters are Django lookups (`field`, `field__in=a,b`,
49
+ `field__icontains`, `field__gte`, `field__isnull=true`); the daterange
50
+ triple is `dr_field/dr_start/dr_end`; datetimes serialize as epoch seconds;
51
+ unknown params are silently ignored by the server.
52
+
53
+ The executable mock coerces comparisons narrowly: both operands must be
54
+ fully numeric for numeric comparison; temporal comparison only recognizes
55
+ canonical `YYYY[-MM[-DD]]` or full ISO datetime shapes; everything else is
56
+ text. This keeps numeric IDs (`10 > 2`) and `User.dob` DateFields correct
57
+ without turning arbitrary date-looking labels into timestamps. The showcase
58
+ Filters demo exercises both `dob__gte=YYYY-MM-DD` and epoch-backed `dr_*`.
59
+
60
+ Mock-only contract controls are exported in development: call
61
+ `armMockReauth(method, path)` to make one authenticated method+path match
62
+ answer 440. Ordinary requests and real transports are unaffected.
63
+
64
+ ## Mock admin contracts
65
+
66
+ The mock carries measured GroupView/admin transports, not UI-shaped
67
+ shortcuts:
68
+
69
+ - group API keys use `/api/group/apikey`; ordinary/default rows omit the raw
70
+ token, create returns it, and only explicit `graph=token` reads it later;
71
+ - webhook subscriptions validate HTTPS URLs, reject URL credentials, and
72
+ keep metadata detail-only; `/api/group/webhook_secret` is the deliberate
73
+ read/rotate secret endpoint;
74
+ - member invites stamp the requested group and member edits preserve the
75
+ backend permission-dict behavior; group incident rows filter through
76
+ `/api/incident/event?group_id=`;
77
+ - `/api/auth/config` resolves defaults → deployment → root → child, replaces
78
+ lists, preserves nested dict keys, ignores unknown/inactive UUIDs, and
79
+ returns only the public whitelist;
80
+ - settings keep plaintext for secret rows in a private mock slot, while every
81
+ wire response carries `value:''` + `display_value:'******'`. Payload order
82
+ deliberately mirrors live `set_value`: serialize `is_secret` before
83
+ `value`. Non-null group scopes enforce `(key, group)` uniqueness; repeated
84
+ global `(key, null)` rows remain possible because PostgreSQL NULL uniqueness
85
+ does. REST delete is denied;
86
+ - metrics permissions are top-level `/api/metrics/permissions` responses, not
87
+ model envelopes. Single permission reads may be strings, multiple are
88
+ lists, and cleared values read as null. Clearing/deleting retains the
89
+ account row in the list, matching the separate Redis account index.
90
+
91
+ Stable mock permission identities (password `mojo`) make gates reproducible:
92
+ `ian@mojoverify.com` is only an odd-group admin;
93
+ `security.viewer@nativemojo.com` has global security/geofence view;
94
+ `security.manager@nativemojo.com` has security/geofence/settings/metrics
95
+ management; `groups.manager@nativemojo.com` has global groups/users
96
+ management. `/api/geo/check?__mock_country=CN` is the deterministic public
97
+ deny case; the default US case allows.
98
+
99
+ The record-feed mock follows the shared Django feed shape rather than a
100
+ component-specific adapter. `/api/incident/ticket/note` and
101
+ `/api/incident/incident/history` return the newest 100 rows, support parent
102
+ and group filtering, and explicitly use `graph=default`. Posts reject unknown
103
+ parents, inherit the parent's group, stamp the authenticated user, embed that
104
+ user in the response, and keep `media` null.
105
+
106
+ Bouncer operator data is available to the stable global security identities:
107
+ signals and devices provide seeded list/detail graphs, while bot signatures
108
+ exercise create, update, uniqueness validation, and enable/disable; DELETE
109
+ matches the live endpoint's denial. The security
110
+ viewer is read-only; the security manager can mutate supported resources.
111
+ Signal detail deliberately omits `token_nonce`: it is neither needed by the
112
+ admin portal nor safe to type, store, or cache as operator-facing data. Block
113
+ and monitor decisions are also seeded into the incident-event feed. The
114
+ read-only `/api/incident/incident` mock follows the real reporter contract:
115
+ Bouncer device investigations filter the security category and search the
116
+ incident details for the device MUID, without invented model linkage.
117
+
118
+ The mock-only component showcase signs in as
119
+ `showcase.operator@nativemojo.com`, an explicit broad demo identity. The
120
+ narrow `security.viewer`, `security.manager`, `groups.manager`, and Ian
121
+ fixtures remain unchanged for permission-boundary verification.
122
+
123
+ Communications mock parity includes `/api/aws/email/{domain,mailbox,sent,template,send}`
124
+ and `/api/account/public_message`. Credential plaintext is reduced to masks
125
+ and discarded, managed onboarding writes through the registered DNS adapter,
126
+ audit persists domain status, send may return a 2xx `failed` row, and mailbox
127
+ and domain deletes reproduce local audit cascades. Stable email/support
128
+ operator identities are listed in [admin-messaging.md](admin-messaging.md).
129
+
130
+ DNSMan's mock contract is central-state-backed and capability-first. Stable
131
+ identities `dns.viewer@nativemojo.com`, `dns.manager@nativemojo.com`, the
132
+ group-1-only member `dns.tenant@nativemojo.com`, and the house-only superuser
133
+ `dns.platform@nativemojo.com` (password `mojo`) exercise the exact global
134
+ view/manage, tenant, and platform split. Credential
135
+ assignment searches only `/api/dnsman/credential/group-choice`; an exact
136
+ selection hydrates with `?id=…`, and inactive or over-depth groups never
137
+ appear. First-link verification failure creates no row. Failed rotation marks
138
+ the existing row unverified while retaining its old masks. Successful raw
139
+ keys are used only by the imperative call and are never stored in mock state
140
+ or returned. Registrar confirmation tokens, legal-contact PII, WHOIS PII, and
141
+ certificate material likewise stay outside model/query state.
142
+
143
+ ## Imperative file uploads
144
+
145
+ `startFileUpload(file, options?)` implements django-mojo's three stages:
146
+ initiate the File, transfer bytes to the returned capability, then reconcile
147
+ and mark the File completed. It returns a `FileUploadTask` immediately; its
148
+ `result` always resolves to a fixed `completed`, `failed`, or `uncertain`
149
+ outcome.
150
+
151
+ ```ts
152
+ const task = startFileUpload(file, { fileManagerId: 12, groupId: 4, use: 'attachments' });
153
+ const unsubscribe = task.subscribe(({ phase, loadedBytes, totalBytes }) => {
154
+ console.log(phase, loadedBytes, totalBytes);
155
+ });
156
+ const outcome = await task.result;
157
+ unsubscribe();
158
+ ```
159
+
160
+ Progress is the count reported by the actual byte transport. Multipart POST
161
+ progress therefore includes wire overhead. Provider fields are appended
162
+ before `file`, and the browser owns the multipart boundary. The #1485 response
163
+ keeps `upload_url`, `method`, `fields`, and `headers` flat beside the safe File
164
+ lifecycle fields. PUT and POST are supported only. The server-echoed MIME type
165
+ governs the byte request.
166
+ Root-relative direct-upload paths missing the deployment's `/api` prefix are
167
+ repaired before transfer. API Bearer and DUID headers are never forwarded;
168
+ an unsafe backend-provided Bearer transfer header produces only a fixed safe
169
+ outcome.
170
+
171
+ Snapshots and outcomes never contain capability URLs, provider fields or
172
+ headers, API bearer/DUID values, raw response bodies, or File URLs. A completed
173
+ outcome exposes only an `UploadedFileRef`: authoritative id, name, MIME, size,
174
+ category, manager id, and group id.
175
+
176
+ Every task generates one private strict `idempotency_key`. `cancel()` aborts
177
+ current work. Even an initiation abort is `uncertain` with a
178
+ nullable File id: the server may have committed before the response became
179
+ observable. `recover()` cannot act without an id and returns the same outcome;
180
+ calling `retry()` replays the same key so a committed-but-lost initiation
181
+ returns the same File. Cancellation after a known initiation, dropped provider responses,
182
+ and ambiguous completion responses are likewise `uncertain`, since remote
183
+ bytes or a completed File may already exist. `recover()` reconciles and
184
+ attempts completion without replaying bytes. `retry()` reconciles first and
185
+ replays the retained private capability only while the File is still
186
+ uploading. If reconciliation proves failed/expired, that same Retry rotates to
187
+ a fresh key and attempt; completed replays need no target. Both are single-flight and generation guarded. A successful
188
+ completion POST is never authoritative by itself; a following File GET must
189
+ confirm `completed` before the task returns success. Completed scalar
190
+ `file_manager_id` and `group_id` must match an explicitly selected destination.
191
+
192
+ ## `mojoQueryDefaults()`
193
+
194
+ Spread into the app's `QueryClient` defaults. Provides: no retry on 4xx
195
+ `MojoError`s (deterministic failures), `networkMode: 'always'` under the
196
+ mock ('online' live), and `refetchOnWindowFocus: false` (freshness comes
197
+ from explicit refresh + opt-in autoRefresh — focus storms read as phantom
198
+ fetches).
199
+
200
+ ## Errors
201
+
202
+ - `MojoError{message, status, errorCode, data}` — server-reported failure;
203
+ `message` is the server's text, `status` is numeric, `errorCode` preserves
204
+ the semantic wire code, and `data` preserves safe structured evidence.
205
+ - `AuthRequiredError` — thrown by the pre-request gate when a request needs
206
+ a session that doesn't exist (synthetic 401; no network was touched).
@@ -0,0 +1,117 @@
1
+ # CollectionMultiSelect — server-backed multi-pick dropdown
2
+
3
+ ```tsx
4
+ import { CollectionMultiSelect } from 'portal-mojo/ui';
5
+ ```
6
+
7
+ A checkbox list over live model data — server search, `SELECT (n)` /
8
+ `DESELECT (n)` with live counts, shift-click range selection — presented by
9
+ default as a **dropdown**: a summary trigger + `<Popover>` menu sharing
10
+ `MultiSelectDropdown`'s shell. The menu stays open while ticking; **Done**,
11
+ Escape or an outside click closes it. `variant="panel"` keeps the
12
+ always-visible box (web-mojo's shape) for settings-page contexts. The value
13
+ is a **controlled id array** in both directions. Ported from web-mojo
14
+ `CollectionMultiSelect.js`; sibling of `CollectionSelect` in the server-data
15
+ selects family (epic #1275).
16
+
17
+ ```tsx
18
+ const [ids, setIds] = useState<Array<string | number>>([]);
19
+
20
+ <CollectionMultiSelect<GroupRow>
21
+ model={GroupModel} // or endpoint="/api/group"
22
+ value={ids}
23
+ onChange={setIds}
24
+ label="Groups"
25
+ required
26
+ />
27
+ ```
28
+
29
+ ## Props
30
+
31
+ | Prop | Behavior |
32
+ |---|---|
33
+ | `model` / `endpoint` | One is required. `model` is a `defineModel` def (supplies endpoint + row type); `endpoint` is the bare path. |
34
+ | `value` / `onChange` | Controlled id array. Every toggle commits — `onChange` fires with the full next array. Ids keep their caller-side types; all comparisons are normalized (`String()`), so string vs number ids work by construction. |
35
+ | `variant` | `'dropdown'` (default): summary trigger + Popover menu — the form-embedded presentation. `'panel'`: the always-visible box. Internals (search, counts, ranges, states) are identical. |
36
+ | `placeholder` | Trigger text while nothing is selected (dropdown). Default `'Select...'`. |
37
+ | `showSelectedLabels` / `maxLabelsToShow` | Trigger summary: up to `maxLabelsToShow` (default 3) comma-joined labels **when every pick's label is known** (labels accumulate from rows the control has seen — across searches), else `"N selected"`. Hydrated initial ids the control has never seen summarize as `"N selected"` — no per-id fetches, no warn. |
38
+ | `placement` | Menu placement against the trigger. Default `'bottom-start'`. |
39
+ | `labelField` / `valueField` | Row fields for label and id, **dot notation** reaches nested fields (`'user.email'`). Defaults `'name'` / `'id'`. A missing field warns once and falls back (label → the id; missing id → row dropped) — never renders nothing silently. |
40
+ | `size` | VISIBLE rows before the list scrolls (max-height = `size × 42px`). Default 8. **Not the fetch size** — the wire page size defaults to 50; override it via `defaultParams: { size: 100 }`. |
41
+ | `maxHeight` | Explicit px override for the list height. |
42
+ | `enableSearch` | Search input above the list. **Defaults ON in the dropdown variant, OFF in the panel.** 400ms debounce (source parity — deliberately not ModelTable's 300) into the `search` wire param; trimmed; empty term drops the param. |
43
+ | `defaultParams` | Extra wire params — a dict, or a **callback re-evaluated per render** (each fetch uses the freshest result; a changed result is a new query key → refetch). Merge order: `{size: 50} → defaultParams → group → search` (later wins). |
44
+ | `requiresActiveGroup` | Folds the active group id in as `group`. With NO active group the fetch is **held** (empty state shows) — never an unscoped list where a scoped one was demanded. Needs a `<GroupProvider>` above; missing one warns. |
45
+ | `ignoreIds` | Ids hidden client-side — the "already added" pattern. Compared normalized. This is the documented exception to the no-client-filtering rule; it exists so a picker can subtract rows already chosen elsewhere without a server round-trip. |
46
+ | `renderItem` | `(row) => ReactNode` — replaces the label span; the checkbox stays component-owned. This replaces web-mojo's mustache `itemTemplate` (trusted-HTML slots end here). |
47
+ | `isRowDisabled` | `(row) => boolean` — per-row disabled: unclickable, skipped by SELECT and by shift-click ranges. |
48
+ | `showSelectAll` | The SELECT/DESELECT header row. Default true. |
49
+ | `label` / `required` / `help` / `error` | Field wrapper (house `.field-label` / `.field-help` / `.field-error`; `error` replaces `help`). |
50
+ | `disabled` | Whole control: trigger, search, buttons, every row. |
51
+ | `id` | id for the trigger button (a `<label for>` can point at it). |
52
+
53
+ ## Selection semantics (the contract)
54
+
55
+ - **Selection is an id set, never row references** — it survives searches,
56
+ refetches and paging of the visible list. Picking under one search term
57
+ and then searching another keeps the first picks in `value`.
58
+ - **SELECT (n)**: n = visible rows that are enabled and not yet selected.
59
+ Clicking it ADDS those ids to the value (**union** — deliberate fix over
60
+ the source, which replaced the value and dropped off-screen selections).
61
+ Disabled rows are skipped. Disabled at n = 0.
62
+ - **DESELECT (n)**: n = `value.length` — the WHOLE selection, including ids
63
+ not on screen under the current search. Clicking it clears the entire
64
+ value (source parity: it is the "empty the value" affordance). Disabled
65
+ at 0.
66
+ - **Shift-click range**: anchor = the last clicked row's index; the clicked
67
+ row's NEW state applies across the span; disabled rows skipped; the
68
+ anchor moves to the clicked row afterwards. The anchor resets whenever
69
+ the row set changes (new data / `ignoreIds` change) — stale indices into
70
+ a replaced list are meaningless (the source kept them and could walk off
71
+ the array).
72
+ - Counts render only when > 0 (`SELECT`, not `SELECT (0)`) — source parity.
73
+
74
+ ## Wire contract
75
+
76
+ One GET to the list endpoint through `useModelList`, so the cache key is the
77
+ shared `[endpoint, params]` — this picker, `ModelTable` and model hooks read
78
+ one cache and one invalidation root (`[endpoint]`). There is no parallel
79
+ fetch path; `model.invalidate(qc)` refreshes open pickers too.
80
+
81
+ Params sent: `size: 50` (default page size) merged with `defaultParams`,
82
+ plus `group` (when `requiresActiveGroup`) and `search` (when searching).
83
+ Sorting is the server's default unless `defaultParams` carries `sort`.
84
+
85
+ ## Invariants
86
+
87
+ 1. Controlled value, one pipeline: the checkboxes can never display a state
88
+ `value` doesn't hold (the visual checkbox is `pointer-events: none`; all
89
+ interaction — including `shiftKey` — lands on the row element).
90
+ 2. Every id comparison goes through one `String()` normalization. Never
91
+ compare raw ids anywhere in or around this component.
92
+ 3. Rows the server returns are shown as-is except the `ignoreIds` subtraction
93
+ — every other narrowing (kind, group, search…) must be a wire param.
94
+ 4. Loading shows skeleton rows (house silhouette) whenever the current key's
95
+ rows aren't on screen yet — cold load AND search/param changes. Empty is
96
+ `"No items available"`; fetch errors show the server's message + Retry.
97
+
98
+ ## Pitfalls
99
+
100
+ - `size` is visible rows, not the fetch size. A list of 300 candidates with
101
+ the default wire size shows only the first 50 — raise it via
102
+ `defaultParams: { size: 300 }` (or make the user search; search is server-side).
103
+ - The legacy `excludeIds` option is **not ported**: despite its "server-side
104
+ filtering" comment it was a second client-side filter identical to
105
+ `ignoreIds` (web-mojo filtered both in `buildItems`). Use `ignoreIds`.
106
+ - DESELECT clears ids that are hidden by `ignoreIds` or the current search
107
+ too — it empties the value, not the visible page.
108
+ - An inline `defaultParams` object/callback re-evaluates per render; that is
109
+ fine (TanStack hashes keys structurally — equal content, same key). Only a
110
+ changed RESULT refetches.
111
+ - `requiresActiveGroup` holds the fetch until a group is active; pair the
112
+ control with `RequiresGroup` or group-scoped screens so users aren't shown
113
+ a permanently empty picker in global context.
114
+ - The dropdown's trigger/footer classes (`.multiselect-trigger`,
115
+ `.multiselect-footer`, `.multiselect-done`) live in
116
+ `multiselect-dropdown.css` — consuming apps must import it alongside
117
+ `collection-multiselect.css` (the reference `theme.css` imports both).