portal-mojo 0.2.8 → 0.2.9

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 (149) hide show
  1. package/README.md +59 -11
  2. package/docs/DetailView.md +3 -2
  3. package/docs/README.md +6 -2
  4. package/docs/account.md +4 -3
  5. package/docs/admin-bouncer.md +3 -3
  6. package/docs/admin-credentials.md +4 -3
  7. package/docs/admin-devices-geoip.md +1 -1
  8. package/docs/admin-dns-certificates.md +2 -2
  9. package/docs/admin-dns-records.md +1 -1
  10. package/docs/admin-edge.md +210 -0
  11. package/docs/admin-identity-directories-sources.json +2 -2
  12. package/docs/admin-identity-directories.md +9 -0
  13. package/docs/admin-identity-users.md +2 -2
  14. package/docs/admin-jobs.md +4 -4
  15. package/docs/admin-media-rendering.md +142 -0
  16. package/docs/admin-modals.md +7 -0
  17. package/docs/admin-monitoring.md +4 -4
  18. package/docs/admin-network-security.md +2 -2
  19. package/docs/admin-phonehub.md +4 -1
  20. package/docs/admin-storage.md +5 -2
  21. package/docs/calendar.md +1 -1
  22. package/docs/client.md +159 -26
  23. package/docs/collection-multiselect.md +2 -2
  24. package/docs/combobox.md +2 -2
  25. package/docs/dataview.md +1 -1
  26. package/docs/datepicker.md +3 -3
  27. package/docs/daterange.md +1 -1
  28. package/docs/datetimepicker.md +1 -1
  29. package/docs/defineModel.md +40 -7
  30. package/docs/detail-primitives.md +1 -1
  31. package/docs/feedback.md +9 -8
  32. package/docs/file-upload-ux.md +2 -2
  33. package/docs/idioms.md +1 -1
  34. package/docs/loading.md +2 -2
  35. package/docs/location-address.md +2 -1
  36. package/docs/markdown.md +3 -3
  37. package/docs/menus-and-access.md +21 -1
  38. package/docs/multiselect-dropdown.md +1 -1
  39. package/docs/popover.md +1 -1
  40. package/docs/record-feed.md +2 -2
  41. package/docs/taginput.md +2 -1
  42. package/docs/timepicker.md +1 -1
  43. package/docs/timezone-select.md +4 -3
  44. package/docs/user-menu.md +4 -3
  45. package/docs/worldmap.md +2 -2
  46. package/package.json +3 -2
  47. package/src/account/AccountModal.tsx +3 -2
  48. package/src/account/index.ts +2 -2
  49. package/src/admin/core/index.ts +1 -1
  50. package/src/admin/dns/models.ts +1 -1
  51. package/src/admin/domains/infrastructure.ts +12 -2
  52. package/src/admin/domains/security.ts +3 -1
  53. package/src/admin/edge/BlocklistPage.tsx +110 -0
  54. package/src/admin/edge/UpstreamsPage.tsx +113 -0
  55. package/src/admin/edge/VhostDetail.tsx +166 -0
  56. package/src/admin/edge/VhostWizard.tsx +273 -0
  57. package/src/admin/edge/VhostsPage.tsx +47 -0
  58. package/src/admin/edge/api.ts +56 -0
  59. package/src/admin/edge/index.ts +15 -0
  60. package/src/admin/edge/models.ts +395 -0
  61. package/src/admin/edge/permissions.ts +13 -0
  62. package/src/admin/identity/users/PersonalApiKeysPage.tsx +10 -0
  63. package/src/admin/identity/users/sections/actions.tsx +3 -3
  64. package/src/admin/index.ts +1 -0
  65. package/src/admin/phonehub/api.ts +5 -3
  66. package/src/admin/public/infrastructure.ts +1 -0
  67. package/src/admin/registry.ts +2 -1
  68. package/src/admin/storage/MediaRenderingPage.tsx +258 -0
  69. package/src/admin/storage/file-renditions.ts +12 -0
  70. package/src/admin/storage/index.ts +2 -0
  71. package/src/admin/storage/rendition-settings.ts +264 -0
  72. package/src/charts/exportChart.ts +1 -1
  73. package/src/client/action-result.ts +23 -61
  74. package/src/client/active-group.ts +9 -3
  75. package/src/client/auth.ts +18 -4
  76. package/src/client/client.ts +49 -10
  77. package/src/client/errors.ts +57 -2
  78. package/src/client/group-context.ts +2 -0
  79. package/src/client/group.tsx +112 -20
  80. package/src/client/hooks.ts +7 -3
  81. package/src/client/mock.ts +958 -14
  82. package/src/client/model.ts +15 -3
  83. package/src/client/renditions-mock.ts +212 -0
  84. package/src/client/scoped.ts +19 -5
  85. package/src/styles/components/account.css +304 -0
  86. package/src/styles/components/admin-assistant.css +28 -0
  87. package/src/styles/components/admin-bouncer.css +53 -0
  88. package/src/styles/components/admin-cloudwatch.css +23 -0
  89. package/src/styles/components/admin-dashboard.css +16 -0
  90. package/src/styles/components/admin-devices.css +260 -0
  91. package/src/styles/components/admin-dns-certificates.css +28 -0
  92. package/src/styles/components/admin-dns-registrar.css +1 -0
  93. package/src/styles/components/admin-dns.css +43 -0
  94. package/src/styles/components/admin-edge.css +1 -0
  95. package/src/styles/components/admin-incidents.css +14 -0
  96. package/src/styles/components/admin-jobs.css +237 -0
  97. package/src/styles/components/admin-media-rendering.css +38 -0
  98. package/src/styles/components/admin-messaging.css +3 -0
  99. package/src/styles/components/admin-monitoring.css +152 -0
  100. package/src/styles/components/admin-network.css +406 -0
  101. package/src/styles/components/admin-phonehub.css +61 -0
  102. package/src/styles/components/admin-push.css +30 -0
  103. package/src/styles/components/admin-rules.css +23 -0
  104. package/src/styles/components/admin-security.css +37 -0
  105. package/src/styles/components/admin-shortlinks.css +8 -0
  106. package/src/styles/components/admin-signin.css +64 -0
  107. package/src/styles/components/admin-storage.css +4 -0
  108. package/src/styles/components/auth.css +162 -0
  109. package/src/styles/components/calendar.css +228 -0
  110. package/src/styles/components/charts-c2.css +249 -0
  111. package/src/styles/components/collection-multiselect.css +114 -0
  112. package/src/styles/components/collection-select.css +63 -0
  113. package/src/styles/components/combobox.css +42 -0
  114. package/src/styles/components/dataview.css +139 -0
  115. package/src/styles/components/datepicker.css +106 -0
  116. package/src/styles/components/daterange.css +119 -0
  117. package/src/styles/components/datetimepicker.css +230 -0
  118. package/src/styles/components/detail-primitives.css +246 -0
  119. package/src/styles/components/detailview.css +69 -0
  120. package/src/styles/components/drawer.css +111 -0
  121. package/src/styles/components/file-upload.css +112 -0
  122. package/src/styles/components/form-wizard.css +11 -0
  123. package/src/styles/components/formview.css +74 -0
  124. package/src/styles/components/group-admin.css +302 -0
  125. package/src/styles/components/guardrail.css +29 -0
  126. package/src/styles/components/idioms.css +84 -0
  127. package/src/styles/components/image-editor.css +80 -0
  128. package/src/styles/components/loading.css +200 -0
  129. package/src/styles/components/location-address.css +32 -0
  130. package/src/styles/components/markdown.css +192 -0
  131. package/src/styles/components/multiselect-dropdown.css +92 -0
  132. package/src/styles/components/popover.css +35 -0
  133. package/src/styles/components/record-feed.css +161 -0
  134. package/src/styles/components/right-panel.css +37 -0
  135. package/src/styles/components/tabs.css +26 -0
  136. package/src/styles/components/taginput.css +132 -0
  137. package/src/styles/components/timepicker.css +282 -0
  138. package/src/styles/components/user-admin.css +189 -0
  139. package/src/styles/components/user-menu.css +98 -0
  140. package/src/styles/components/worldmap.css +97 -0
  141. package/src/styles/core.css +807 -0
  142. package/src/styles/index.css +69 -0
  143. package/src/styles/required-tokens.json +32 -0
  144. package/src/ui/FormWizard.tsx +9 -3
  145. package/src/ui/ThemeProvider.tsx +1 -1
  146. package/src/ui/detail/index.ts +2 -2
  147. package/src/ui/index.ts +3 -2
  148. package/src/ui/loading.tsx +1 -1
  149. package/src/ui/toast.tsx +1 -1
package/README.md CHANGED
@@ -9,6 +9,8 @@ npm install portal-mojo react react-dom react-router-dom @tanstack/react-query
9
9
  ```
10
10
 
11
11
  **Per-component reference docs (written for AI context): [`docs/`](docs/README.md).**
12
+ **Upgrading from 0.2:** raw calls now reject a flat HTTP-200 refusal — see
13
+ [Upgrading to 0.3](docs/client.md#upgrading-to-03).
12
14
  Live demos for everything: run `npm run dev:showcase` → **Develop → Components**.
13
15
 
14
16
  ## Subpath surfaces
@@ -63,18 +65,64 @@ are identical in both forms.
63
65
 
64
66
  ## What the consuming app must provide
65
67
 
66
- - **Theme tokens + component CSS.** Components render semantic classes
67
- (`panel`, `chip`, `tbl`, …) styled by the app's `theme.css` under
68
+ - **A CSS entry that imports the package stylesheet.** Components render
69
+ semantic classes (`panel`, `chip`, `tbl`, …). Their rules ship with the
70
+ package as `portal-mojo/styles.css`. In the app's Tailwind CSS entry, in
71
+ this order:
72
+
73
+ ```css
74
+ @import "tailwindcss";
75
+ @import "portal-mojo/styles.css";
76
+
77
+ :root { /* the app's tokens, light */ }
78
+ :root[data-theme="dark"] { /* the app's tokens, dark */ }
79
+ @theme inline { /* the app's own token-to-utility bridge, if it has one */ }
80
+
81
+ /* the app's own overrides, outside any layer */
82
+ ```
83
+
84
+ - **Import it from the CSS entry only**, never from TypeScript, and with
85
+ no `layer()` of your own. The file carries the package's Tailwind
86
+ `@source` and its own cascade layer. Both need the app's Tailwind pass,
87
+ and Tailwind rejects an `@source` inside a layered import.
88
+ - **No `@source` line for this package.** The stylesheet scans the
89
+ package's own markup, so utility classes used in package components are
90
+ generated without the app knowing the install path.
91
+ - **App rules always win.** Package rules sit in the `portal-mojo` cascade
92
+ layer, ordered after Tailwind's `utilities`. A rule the app writes
93
+ outside a layer beats a package rule whatever its specificity or order.
94
+ Package rules still beat Tailwind utilities.
95
+ - **`!important` is the exception.** For important declarations the layer
96
+ order runs backwards, so a package `!important` (a handful, in
97
+ `calendar`, `detailview`, `loading`, `admin-dns` and `core`) beats an
98
+ unlayered app `!important`. To override one, write the app rule with
99
+ `!important` inside one of Tailwind's layers, for example
100
+ `@layer components { … }`.
101
+ - **Theme tokens.** The package ships no token values. The app declares
102
+ every name in `src/styles/required-tokens.json`, under
68
103
  `data-theme="light"` **and** `"dark"` (reference set:
69
- `apps/portal/src/theme.css`).
70
- This includes the `.guardrail*` rules behind `confirmGuardrail`
71
- (reference: `apps/portal/src/theme/guardrail.css`) — the package ships no
72
- CSS, so an app that opens a guardrail must carry that block.
73
- - **Tailwind scan of this package.** In an installed app's CSS:
74
- `@source "../node_modules/portal-mojo/src";` (adjust the relative path from
75
- that CSS file). Utility classes used in package components are generated by
76
- the app's Tailwind pass. Monorepo development may scan
77
- `../../../packages/portal-mojo/src` instead.
104
+ `apps/portal/src/theme.css`). `npm run verify:package` keeps that list
105
+ equal to what the package styles read.
106
+
107
+ ### Migrating from copied theme files
108
+
109
+ Apps that copied the reference app's component CSS switch to the import:
110
+
111
+ 1. Delete the copied component files and the copied component body of
112
+ `theme.css` (everything that is not a token block or `@theme inline`).
113
+ 2. Keep the token blocks and `@theme inline`. Check them against
114
+ `src/styles/required-tokens.json`.
115
+ 3. Import `portal-mojo/styles.css` from the Tailwind entry, right after
116
+ `tailwindcss`, and drop any `@source` line that pointed at this package.
117
+ 4. Keep the app's own override files after it, outside any layer.
118
+ 5. Re-check every override. It now beats the package rule it targets
119
+ whatever its specificity or position, so an override that used to lose
120
+ now applies.
121
+
122
+ An app that keeps its old copies beside the import still renders: the
123
+ copies are unlayered and win. It just stops receiving package fixes for
124
+ those rules until the copies go.
125
+
78
126
  - **bootstrap-icons.** Components emit `bi bi-*` classes; the app imports the
79
127
  icon font once.
80
128
  - **Providers.** Every shell supplies `QueryClientProvider` (spread
@@ -73,7 +73,8 @@ receives a viewport-height limit; page/embedded bodies have no modal minimum
73
73
  or maximum. At narrow container widths an in-flow section button expands the
74
74
  same filtered rail entries, including rich, zero and dot badges. No native
75
75
  select stringification or second top-layer overlay is involved. The chrome
76
- lives in `theme/detailview.css` in both consuming apps.
76
+ lives in `packages/portal-mojo/src/styles/components/detailview.css` and ships in
77
+ `portal-mojo/styles.css`.
77
78
 
78
79
  ## Permission-gated sections (fail-closed)
79
80
 
@@ -191,7 +192,7 @@ get rows. See [Admin modal policy](admin-modals.md) for lifecycle exceptions.
191
192
  cache.
192
193
  - **Badges are state, not commands** — to "setBadge", change the value
193
194
  your `badges` prop is built from.
194
- - The styles for the C1 chrome live in `apps/portal/src/theme/detailview.css`
195
+ - The styles for the C1 chrome live in `packages/portal-mojo/src/styles/components/detailview.css`
195
196
  (`.rail-badge`, `.rail-dot`, `.dv-menu*`, `.dv-keep`) — tokens only,
196
197
  both themes.
197
198
 
package/docs/README.md CHANGED
@@ -35,6 +35,8 @@ everything: run the showcase (`npm run dev:showcase`) → **Develop → Componen
35
35
  | [admin-dns-certificates.md](admin-dns-certificates.md) | Certificate custody and ACME lifecycle — in-zone requests, delegation readiness, house gates, renewal-aware bounded polling, and the no-material boundary |
36
36
  | [admin-dns-registrar.md](admin-dns-registrar.md) | Domain search/quote/purchase, one-use token custody, durable-ledger reconciliation, scoped registrant PII, and manual House adoption |
37
37
  | [admin-storage.md](admin-storage.md) | Global S3 buckets, masked storage backends, explicit policy-backed File uploads, capability-safe FileView/shares, and finite rendition convergence |
38
+ | [admin-media-rendering.md](admin-media-rendering.md) | Storage → Media rendering: the three FILEMAN_RENDITIONS_* settings as a descriptor-driven form — delta-from-defaults writes, inline 400 routing, reset by writing `{}` |
39
+ | [admin-edge.md](admin-edge.md) | Edge vhosts by template kind with a shape-first wizard, `site_api` routes and their create order, declared upstreams, and the fleet blocklist with log-first modes. |
38
40
  | [admin-shortlinks.md](admin-shortlinks.md) | Global shortlink operations, destination-free caches, bounded click history, and reconciled one-shot mutations |
39
41
  | [admin-messaging.md](admin-messaging.md) | SES domains/mailboxes/sent/templates plus public contact requests, with write-only credentials and offline HTML previews |
40
42
  | [admin-phonehub.md](admin-phonehub.md) | Global Phone Hub: evidence-backed lookup, sanitized SMS audits, and write-only provider configuration |
@@ -88,8 +90,10 @@ everything: run the showcase (`npm run dev:showcase`) → **Develop → Componen
88
90
  4. **Controlled inputs, one value pipeline.** A control can never display a
89
91
  value its state doesn't hold. Unknown option values fall back to a
90
92
  default WITH a `console.warn` — never to rendering nothing.
91
- 5. **Both themes, day one.** Style with tokens from `apps/portal/src/theme.css`
92
- (`--surface`, `--ink`, `--mute`, `--accent`, …) and verify under
93
+ 5. **Both themes, day one.** Component CSS lives in
94
+ `packages/portal-mojo/src/styles/`. Style with the app's tokens
95
+ (`--surface`, `--ink`, `--mute`, `--accent`, …; reference values in
96
+ `apps/portal/src/theme.css`) and verify under
93
97
  `data-theme="light"` AND `"dark"`.
94
98
  6. **django-mojo datetimes are epoch SECONDS** on the wire. `fmt.*` accepts
95
99
  them directly; do not `new Date(epochSeconds)` without ×1000.
package/docs/account.md CHANGED
@@ -221,9 +221,10 @@ caller's preferences ▸ one "General" row.
221
221
  `onSignOut` both places). Lazy-load it if the shell entry is budgeted:
222
222
  `import('portal-mojo/account').then((m) => m.openAccountModal(props))`
223
223
  (apps/portal does this).
224
- 2. Copy the CSS (the package ships none): `theme/account.css`, plus
225
- `theme/user-admin.css` (API-key rows) and `theme/group-admin.css`
226
- (`.ga-secret-*` token reveal) if the app doesn't already ship them.
224
+ 2. Import `portal-mojo/styles.css` from the app's Tailwind CSS entry and
225
+ declare the colour tokens. `account.css`, `user-admin.css` (API-key rows)
226
+ and `group-admin.css` (`.ga-secret-*` token reveal) ship in it; there is
227
+ nothing to copy. The showcase still carries its own copies.
227
228
  3. Mount `ModalHost` + `ToastHost`, and a fresh-auth handler (apps/portal's
228
229
  `FreshAuthHost`, registered through `setFreshAuthHandler`) inside the
229
230
  authenticated tree. Without a handler a 440 just fails with the
@@ -110,9 +110,9 @@ accepted it. No batch or action endpoints are invented, so partial failures use
110
110
  ## Styling and showcase
111
111
 
112
112
  The package emits semantic `bouncer-*` classes and imports no application CSS.
113
- Consumers must include `theme/admin-bouncer.css` after the shared token
114
- definitions. The repository keeps matching leaf files in `apps/portal` and
115
- `apps/showcase`, consistent with the current intentional theme duplication.
113
+ Its styles live in `src/styles/components/admin-bouncer.css` and ship in
114
+ `portal-mojo/styles.css`; the app declares the tokens. The showcase still
115
+ carries its own copy until it adopts the package stylesheet.
116
116
 
117
117
  The showcase's `AdminBouncerDemo` switches among the three real package pages
118
118
  so only one URL-backed top-level table is mounted at a time. Open signal/device
@@ -178,9 +178,10 @@ and confirmed rotation are explicit button actions.
178
178
  ## Styling contract
179
179
 
180
180
  Credential components render the existing semantic `ga-*` classes and never
181
- import an application stylesheet. A consuming portal or showcase must include
182
- the `group-admin.css` semantic rules in its theme entry, as the repository's
183
- `apps/portal/src/theme.css` and `apps/showcase/src/theme.css` do today.
181
+ import an application stylesheet. The `ga-*` rules ship in
182
+ `portal-mojo/styles.css`
183
+ (`packages/portal-mojo/src/styles/components/group-admin.css`); a consuming
184
+ app imports that one stylesheet from its Tailwind entry.
184
185
 
185
186
  ## Personal keys are separate
186
187
 
@@ -34,7 +34,7 @@ import {
34
34
  Account-security triage: who logged in from a new country, what is this IP,
35
35
  and is this device shared across accounts. Demo: showcase → Admin →
36
36
  **Devices, logins & GeoIP**. Styles:
37
- `apps/{portal,showcase}/src/theme/admin-devices.css` (byte-identical).
37
+ `packages/portal-mojo/src/styles/components/admin-devices.css`.
38
38
 
39
39
  ## Sections
40
40
 
@@ -107,8 +107,8 @@ sticky delegation, staging, unconfigured, and malformed-response evidence.
107
107
  Its only custody fixture is a private `material_present` boolean; no PEM is
108
108
  stored or serialized.
109
109
 
110
- Portal and Showcase both import `theme/admin-dns-certificates.css`, which uses
111
- semantic tokens only. The executable demo is **Develop → Components → DNS
110
+ `src/styles/components/admin-dns-certificates.css` ships in
111
+ `portal-mojo/styles.css` and uses semantic tokens only. The executable demo is **Develop → Components → DNS
112
112
  certificates & ACME** with manager, viewer, platform-house, and unconfigured
113
113
  legs. Run:
114
114
 
@@ -62,4 +62,4 @@ Confirmation shows identity, TTL, unchanged/removed/added values, and contextual
62
62
 
63
63
  The central mock is the only DNS state owner and mirrors id-less envelopes, global-versus-tenant authorization, active/provider/credential gates, complete replacement, Route 53 delete, GoDaddy floor/refusal/spent placeholders, Mojo, unknown records, and structured types. Mutation verification is mock-only. Live verification may read a zone and open/cancel an editor, but must never POST or DELETE DNS records.
64
64
 
65
- Both Portal and Showcase import `theme/admin-dns.css`; all colors use theme tokens. Run `npm run verify:admin-dns-records` with the orchestration-owned typecheck/build/browser pass.
65
+ `src/styles/components/admin-dns.css` ships in `portal-mojo/styles.css`; all colors use theme tokens. Run `npm run verify:admin-dns-records` with the orchestration-owned typecheck/build/browser pass.
@@ -0,0 +1,210 @@
1
+ # Edge Admin
2
+
3
+ ```ts
4
+ import {
5
+ EDGE_ADMIN_SECTION, VhostsPage, VhostDetail, VhostWizard, openVhostWizard,
6
+ UpstreamsPage, BlocklistPage, EdgeVhostModel, EdgeRouteModel,
7
+ EdgeUpstreamModel, EdgeBlocklistModel, buildVhostPayload,
8
+ } from 'portal-mojo/admin';
9
+ ```
10
+
11
+ The same names are on `portal-mojo/admin/infrastructure`.
12
+
13
+ `EDGE_ADMIN_SECTION` contributes Infrastructure routes at `edge/vhosts` and
14
+ `edge/upstreams` (or the same paths under an embedded `/system` mount). The
15
+ fleet blocklist is a route of Network Security, at
16
+ `security/network/edge-blocklist`. There are no record routes: a vhost opens in
17
+ `modal.detail`.
18
+
19
+ Consumer contract: django-mojo `mojo/apps/edge/`. The mock in
20
+ `client/mock.ts` returns the same graphs and the same refusals, word for word
21
+ where the server has words.
22
+
23
+ ## Permissions
24
+
25
+ - Vhosts, routes, upstreams — view: `sys.view_dns | sys.manage_dns | sys.security`.
26
+ - Vhosts, routes — change or delete: `sys.manage_dns | sys.security`.
27
+ - Upstreams — switch on or off: `sys.manage_dns | sys.security`.
28
+ - Upstreams — declare or retire: `me.is_superuser === true`. The server gate
29
+ is `require_platform_admin`, which also refuses API keys.
30
+ - Blocklist — view: `sys.view_security | sys.manage_security | sys.security`
31
+ (`EDGE_BLOCKLIST_VIEW_PERMS`). This is wider than `SECURITY_VIEW_PERMS`: the
32
+ server lets `manage_security` view, and manage does not imply view on the
33
+ client, so the clause is spelled out.
34
+ - Blocklist — change or delete: `sys.manage_security | sys.security`.
35
+
36
+ A section gate and a route gate both apply. A security-only operator is denied
37
+ every DNS-gated section, which is why the blocklist does not live under Edge.
38
+ The blocklist has no group: a member-scoped grant and `?group=` open nothing.
39
+
40
+ **House rows.** A vhost on a group-less domain is platform property. It never
41
+ appears in a non-superuser's list, even with a global grant, and its detail,
42
+ its routes and creating one are refused to everyone else. The vhost graphs
43
+ carry `domain: basic`, which has no `group`, so house-ness cannot be read off a
44
+ vhost row. The wizard's domain picker passes `group__isnull: false` for
45
+ non-superusers so a house domain is never offered to someone who would be
46
+ refused at Finish.
47
+
48
+ ## Endpoints
49
+
50
+ | Address | Verbs | Notes |
51
+ |---|---|---|
52
+ | `/api/edge/vhost` (+`/<id>`) | list, read, create, update, delete | `list` graph for tables, `default` for one row |
53
+ | `/api/edge/route` (+`/<id>`) | list, read, create, update, delete | `site_api` vhosts only; filter `vhost` |
54
+ | `/api/edge/upstream` (+`/<id>`) | list, read, update | update writes `is_enabled` only; no create, no delete |
55
+ | `/api/edge/upstream/declare` | POST | `{name, kind, host+port \| socket_path, group?}`; platform administrators |
56
+ | `/api/edge/upstream/retire` | POST | `{upstream}`; disables, keeps the row; platform administrators |
57
+ | `/api/edge/blocklist` (+`/<id>`) | list, read, create, update, delete | fleet-wide |
58
+
59
+ ## The four kinds
60
+
61
+ The wizard never shows a kind dropdown over one long form. Its first step is
62
+ four cards in plain words; the following steps show only that shape's settings.
63
+
64
+ | Kind | Card | Needs | May carry | Refuses |
65
+ |---|---|---|---|---|
66
+ | `api` | API host | `upstream` | `quiet_paths`, `serve_static` | `spa`, `redirect_to` |
67
+ | `site` | Static site or SPA | — | `spa` | `upstream`, `quiet_paths`, `serve_static`, `redirect_to` |
68
+ | `site_api` | Site + API paths | routes | `spa`, `serve_static`, `quiet_paths` | whole-host `upstream`, `redirect_to` |
69
+ | `redirect` | Redirect | `redirect_to` | — | everything else |
70
+
71
+ `buildVhostPayload(draft, { create })` is the one place a draft becomes a
72
+ request. It sends each knob the chosen kind does not carry as its empty value,
73
+ so a draft that visited another shape cannot leak that shape's settings, and
74
+ an edit that changes kind clears what the old kind held. It never sends
75
+ `mojosec_policy`. A vhost that carries an edge evidence policy therefore cannot
76
+ change kind, or its single-page-app switch on a `site`, from here: the server
77
+ requires the policy's response class to match and refuses, and its message is
78
+ shown.
79
+
80
+ Shared rules, each mirrored by a validator in `edge/models.ts`:
81
+
82
+ - `label`: empty serves the bare domain, `*` the wildcard, otherwise one DNS
83
+ label. The address (`server_name`) is derived, never typed.
84
+ - `body_size_mb`: whole number, 1 to 4096, default 50. Not offered for a
85
+ redirect.
86
+ - `pool`: must be one the deployment declares (`EDGE_POOLS`, default
87
+ `["default"]`). The refusal names the declared set.
88
+ - `domain`: set once. An edit shows it read-only and never sends it.
89
+ - `redirect_to`: a host name. No scheme, path, port or wildcard.
90
+ - A route prefix or quiet path starts with `/`, uses letters, digits, `.`,
91
+ `_`, `-` and `/`, and has no `//` or `..`. A route prefix cannot be `/`.
92
+
93
+ ## What is checked when
94
+
95
+ The server checks most things on every save. Two things are checked **only
96
+ when the row is enabled**:
97
+
98
+ - the certificate belongs to the vhost's domain and covers its address;
99
+ - no other enabled row has the same domain and label.
100
+
101
+ So a disabled row may hold a certificate that does not cover it yet, and may
102
+ share a name with a live row. That is how a replacement is staged: create it
103
+ disabled, then disable the old one and enable the new one. The wizard warns
104
+ when the chosen certificate does not cover the address (`certificateCovers`,
105
+ which knows that a wildcard covers exactly one label and never the apex) and
106
+ still lets the row be saved disabled.
107
+
108
+ ## The create order for `site_api`
109
+
110
+ A quiet path on a `site_api` vhost must sit under one of its route prefixes,
111
+ and the server reads those prefixes from the stored row. A create that carries
112
+ quiet paths is therefore always refused with "none declared". The only order
113
+ that works is:
114
+
115
+ 1. create the vhost, **disabled** and without quiet paths;
116
+ 2. create its routes, one at a time;
117
+ 3. save the vhost once more with the quiet paths and the enablement the user
118
+ chose.
119
+
120
+ The wizard does exactly this. Step 3 always runs, so the enable-time checks
121
+ happen at the end and a failure part-way never leaves a live vhost with no
122
+ routes. If a step is refused the wizard stays open and says which step and
123
+ what already exists. Finishing again continues from there: it never posts the
124
+ vhost a second time and never repeats a route that landed.
125
+
126
+ The server will store an enabled `site_api` vhost with no routes. The wizard
127
+ does not: at least one valid route is required at the settings step and again
128
+ at review.
129
+
130
+ In edit mode routes are read-only, with a pointer to the detail view, which
131
+ owns adding, changing and deleting them. Changing a `site_api` vhost to
132
+ another kind while it has routes is blocked with the count; the server refuses
133
+ it too. Until the stored routes have been read, or if that read fails, the
134
+ wizard does not judge quiet paths against them and leaves that to the server.
135
+
136
+ ## Vhost detail
137
+
138
+ The detail follows the admin modal contract (`admin-modals.md`). The header
139
+ switch enables and disables. Edit and Delete are in the header menu, shown
140
+ only with the manage grant. Delete asks for confirmation, names the address,
141
+ says its routes go with it, and points at Disable as the choice that can be
142
+ undone. A refused enable shows the server's message in the overview. Routes
143
+ are content: the Routes section lists them and owns add, change and delete.
144
+
145
+ ## Upstreams
146
+
147
+ An upstream is a destination a vhost may proxy to. Only `is_enabled` can be
148
+ written on one; its name and destination are fixed when it is declared. The
149
+ list shows the active group's upstreams and the shared ones. Retiring disables
150
+ the row and keeps it: vhosts and routes that reach it stop being served and
151
+ are not repointed. The retire control names that consequence before it acts.
152
+ The wizard's upstream pickers list enabled upstreams only.
153
+
154
+ ## Blocklist
155
+
156
+ An entry matches an IP network or a user-agent pattern, in one of four modes:
157
+ `log` (recorded, still served — the default), `enforce` (refused), `allow` (an
158
+ exemption) and `off`. Create an entry in `log`, watch the edge watch log for
159
+ it, then switch it to `enforce`. A change reaches the fleet in about ten
160
+ minutes.
161
+
162
+ An `ip` value is stored as its network: `10.1.2.3/8` is saved as `10.0.0.0/8`.
163
+ The editor shows the saved value from the response.
164
+
165
+ ## Not built here
166
+
167
+ - **Claim-reserved.** The plan for this page asked for a superuser control to
168
+ claim a reserved name. django-mojo removed the reserved-name mechanism, its
169
+ field and its action (commit `bc2c57b1`, in every release since v1.7.1), so
170
+ there is nothing for such a control to call.
171
+ - **`mojosec_policy`.** Shown read-only on the vhost detail. It is set by the
172
+ platform and never sent from here.
173
+ - **Web apps, releases and alias addresses.** An alias address appears in the
174
+ vhost list like any other row; `alias_of` is in no graph, so it cannot be
175
+ told apart, and a refused edit shows the server's message.
176
+
177
+ ## Pitfalls
178
+
179
+ - Do not send `quiet_paths` on a `site_api` create. See the create order.
180
+ - Do not treat a saved, enabled row as proof the certificate covers it from
181
+ the client check alone; the server's answer on save is the proof.
182
+ - The mock's messages for database uniqueness (a second enabled vhost on one
183
+ name, a repeated route prefix, a repeated blocklist value, a repeated
184
+ upstream name) are the mock's own words. The server reports these from a
185
+ database constraint, and its wording was not measured.
186
+ - The server parses a blocklist address with Python's `ipaddress` and compiles
187
+ a user-agent pattern with Python's `re`, and the mock follows Python, not
188
+ JavaScript: `010.1.2.3` and `\q` are refused, `10.0.0.0/255.0.0.0`,
189
+ `(?i)bot` and `a*+` are accepted. The reason after "does not compile" is the
190
+ mock's own wording. The editor's own check is only a first pass and never
191
+ compiles the pattern; the server's answer on save is the verdict.
192
+ - A vhost update that names an upstream or certificate that does not exist is
193
+ refused by the mock with the same words as a missing one. The server's
194
+ wording for an unknown id was not measured.
195
+ - The declare-upstream dialog and the blocklist editor refuse Escape and the
196
+ backdrop while their write is in flight, and the editor's save and delete
197
+ share one pending state. `npm run verify:admin-edge` holds each write open
198
+ and checks this.
199
+
200
+ ## Themes and showcase
201
+
202
+ Styles live in `src/styles/components/admin-edge.css` and ship in
203
+ `portal-mojo/styles.css`; the app declares the tokens. They are built from
204
+ tokens only, so both themes render from one sheet. The showcase still carries
205
+ its own identical copy until it adopts the package stylesheet. The showcase
206
+ demo (Develop → Components → Admin → Edge) runs the shipped pages against the
207
+ mock under five identities: platform administrator, DNS manager, DNS viewer,
208
+ security manager and security viewer.
209
+
210
+ `npm run verify:admin-edge` is the executable contract.
@@ -25,8 +25,8 @@
25
25
  "source_path": "apps/portal/src/pages/ApiKeysPage.tsx",
26
26
  "source_sha256": "3d338cf77c3110b72a9361565c1cced10a7fccc52c486e3dafda119d66c676b2",
27
27
  "package_path": "packages/portal-mojo/src/admin/identity/users/PersonalApiKeysPage.tsx",
28
- "package_sha256": "f377d8a97b5c0ccbce745c333fdd7e4a5fd5f5a89b868b058677b31e3efe64ff",
29
- "adaptation": "Expanded row facts and actions are presented in a native DetailView modal that observes the current API key record."
28
+ "package_sha256": "0c1e1efd34f515e4b5dc994ce34be1446eb13d1486deea3e9641565215edb6ad",
29
+ "adaptation": "Expanded row facts and actions are presented in a native DetailView modal that observes the current API key record. Every list and export is pinned to the signed-in user with fixedParams, and the table is not mounted before that user is known."
30
30
  },
31
31
  {
32
32
  "source_path": "apps/portal/src/pages/GroupDetail.tsx",
@@ -54,6 +54,15 @@ revoke remains armed because it rotates the secret and cannot be undone. There
54
54
  is no delete action. This page does not use the separate group credential
55
55
  endpoint.
56
56
 
57
+ The page lists the signed-in user's own keys only. The endpoint serves every
58
+ user's keys to a caller holding `users` or `manage_users` who sends no `user`,
59
+ so the page passes `fixedParams={{ user: me.id }}`: every list and export
60
+ request carries it, and it cannot be cleared from the URL, a saved view or a
61
+ preset. Until the signed-in user is loaded the page shows a placeholder and
62
+ mounts no table, so no unscoped list or export can go out. An administrator
63
+ reads another user's keys per user, in that user's detail under Users. Plain
64
+ users reach their own keys in "My account".
65
+
57
66
  The Personal API Keys route was added to the package identity section registry
58
67
  alongside Users, Members, Groups, Credentials and Sign-in. Compatibility page
59
68
  paths in the reference app re-export the package-owned pages, and its menu and
@@ -1,7 +1,7 @@
1
1
  # Admin Users
2
2
 
3
- Reusable global User administration for django-mojo. Consumers must include
4
- the semantic `user-admin.css` stylesheet used by the portal and showcase.
3
+ Reusable global User administration for django-mojo. Its semantic
4
+ `user-admin.css` stylesheet ships in `portal-mojo/styles.css`.
5
5
 
6
6
  ```tsx
7
7
  import {
@@ -52,7 +52,7 @@ stuck work, purge history — plus the cron-style scheduled tasks whose backend
52
52
  django-mojo has always shipped and whose admin page web-mojo never wired up.
53
53
 
54
54
  Demo: showcase → Admin → **Jobs engine**. Styles:
55
- `apps/{portal,showcase}/src/theme/admin-jobs.css` (byte-identical).
55
+ `packages/portal-mojo/src/styles/components/admin-jobs.css`.
56
56
  Verifier: `npm run verify:admin-jobs`.
57
57
 
58
58
  Wire facts below come from django-mojo `mojo/apps/jobs/` (`models/job.py`,
@@ -424,6 +424,6 @@ from hand-built `JobsStats` objects (and, for `JobRunnersStrip`, a demo-only
424
424
  branched for the demo.
425
425
 
426
426
  Styles are semantic `jobs-*` classes, tokens only, both themes:
427
- `apps/{portal,showcase}/src/theme/admin-jobs.css`, imported from each app's
428
- `theme.css`. The verifier asserts the two copies stay byte-identical and that
429
- neither contains a raw hex colour.
427
+ `packages/portal-mojo/src/styles/components/admin-jobs.css`, imported by the
428
+ package's `src/styles/index.css`. The verifier asserts that import and that
429
+ the file contains no raw hex colour.
@@ -0,0 +1,142 @@
1
+ # Media rendering admin
2
+
3
+ ```ts
4
+ import {
5
+ MediaRenderingPage, readRenditionOptions, findCategoryRows, valueToDraft, draftToValue,
6
+ validateDraft, routeRenditionError, saveCategoryValue, resetCategory, fieldType,
7
+ renditionRoleLabel,
8
+ } from 'portal-mojo/admin/infrastructure';
9
+ ```
10
+
11
+ Storage → Media rendering edits how django-mojo's fileman builds thumbnails,
12
+ previews and transcodes, and which of those run after an upload. It is a form
13
+ over three ordinary `Setting` rows — `FILEMAN_RENDITIONS_IMAGE`, `_VIDEO`,
14
+ `_DOCUMENT` — built entirely from the backend's descriptor (django-mojo #7728,
15
+ `docs/web_developer/fileman/renditions.md`). Nothing about roles, defaults,
16
+ fields, choices or limits is hardcoded: a role the framework adds appears
17
+ without a portal release.
18
+
19
+ ## Route and access
20
+
21
+ `STORAGE_ADMIN_SECTION` registers `storage/rendering` (label "Media
22
+ rendering") with the Settings page's gate, `sys.manage_settings | sys.groups`
23
+ — the page writes Setting rows, so it carries the Setting write audience, not
24
+ the storage one. The section audience includes those two grants so a
25
+ settings-only operator can reach the page; the other storage routes keep their
26
+ own clauses. Member grants never satisfy the gate.
27
+
28
+ The descriptor itself (`GET /api/fileman/renditions/options`) is readable by
29
+ `manage_settings | manage_files | files | groups`; a `files`-only operator can
30
+ read it elsewhere but cannot open this editor.
31
+
32
+ Global scope only. There is no group selector; group-scoped overrides stay on
33
+ the generic Settings page.
34
+
35
+ ## Wire contract
36
+
37
+ **Read:** `GET /api/fileman/renditions/options` → `data.categories.<image|video|document>`:
38
+
39
+ | Field | Used for |
40
+ |---|---|
41
+ | `key` | the Setting key to write |
42
+ | `defaults` | one row per role, in served order; the greyed values |
43
+ | `automatic_default` | the "runs on upload" default set |
44
+ | `override` | the global **database** row, parsed, or `null`. A deployment-file value is applied but not reported here |
45
+ | `effective` | defaults with the override (and any file value) applied; shown only as a read-only "In effect: …" hint where it differs from the draft |
46
+ | `role_kinds`, `fields`, `choices`, `limits` | which fields a role renders, their enum values and inclusive numeric bounds |
47
+
48
+ `data.engine` is read-only on this page (`ffmpeg`; the key stays on Settings
49
+ until a second engine exists).
50
+
51
+ **Row lookup:** `findCategoryRows(key)` lists `/api/settings?key=<KEY>&group__isnull=true`
52
+ with raw `mojoList` — `SettingModel.normalizeListParams` strips `key`, so the
53
+ model's list hook cannot do this. More than one global row for a key disables
54
+ Save and Reset with a notice naming the ids: `Setting.Meta.ordering = ["key"]`
55
+ leaves the server's `.first()` undefined among duplicates, so editing one could
56
+ change a row the renderer never reads.
57
+
58
+ **Write:** the whole category as one object — `POST /api/settings` to create
59
+ (`{key, is_secret: false, group: null, value}`) or `POST /api/settings/<id>`
60
+ to update (`{value}`). The value is sent as an object; the server stores it as
61
+ JSON text. Imperative `mojoSave` under `withFreshAuth`; a failure rejects.
62
+
63
+ **Reset = write `{}`.** The live `Setting` model declares no `CAN_DELETE`, so
64
+ `DELETE /api/settings/<id>` answers 403 — the backend doc's DELETE instruction
65
+ does not work live. `{}` is the documented "defaults" value and the endpoint
66
+ reports it as `override: null`; the row stays, harmlessly. Reset is disabled
67
+ when there is no row or the override is already null.
68
+
69
+ ## Mapper invariants (`rendition-settings.ts`)
70
+
71
+ - **Draft seeds from `defaults ⊕ override`**, never from `effective`. Seeding
72
+ from `effective` would mark a deployment-file value as changed on an
73
+ untouched form and copy it into a DB row on the first Save.
74
+ - **`draftToValue` emits deltas against the defaults**, not against the
75
+ previous override: a field restored to its default disappears from the row.
76
+ Empty role objects are omitted; `{}` means defaults. Save is disabled when
77
+ the emitted value equals the current override.
78
+ - **One typing rule, `fieldType(category, kind, name)`**: `choice` when
79
+ `choices[kind][name]` exists, else `number` when the name is in `limits`
80
+ (checked in that order — the server lists a bound for every option name it
81
+ limits anywhere in the category, so document `quality`, a choice on pdf
82
+ roles, also appears under `limits`), `boolean`
83
+ when a default of that kind holds a boolean (today `audio`), else `string`.
84
+ Inputs coerce on change through `coerceFieldValue`; `<input type=number>`
85
+ strings never reach the payload (the server refuses `"300"`).
86
+ - **`_automatic`** is compared as a set and emitted only when it differs from
87
+ `automatic_default`, in default order with additions appended in `defaults`
88
+ key order (the renderer runs roles in list order). Roles the defaults do not
89
+ declare are dropped on read, as the server's `merge` does.
90
+ - **The mp4-only trio** (`codec`, `crf`, `preset`) is the one rule not in the
91
+ descriptor: it is hidden and stripped from the payload while a role's format
92
+ (draft, else default) is not `mp4`. Switching the format back restores the
93
+ draft values. Format wins over codec, exactly as the server validates.
94
+ - A field with no default (image `format`/`quality` — renderer class
95
+ attributes, not `default_renditions`) renders unset with a "renderer
96
+ default" placeholder; setting it emits it, clearing it omits it again.
97
+
98
+ ## Validation and the 400 slot
99
+
100
+ `validateDraft` mirrors the server's checks with the server's wording (whole
101
+ number / between min and max / one of … / `2000k or 2M` / `HH:MM:SS`). It is
102
+ **advisory**: inputs carry `min`/`max`/`step`, the hint shows inline as the
103
+ admin types, but Save stays enabled and the server decides. Gating on the
104
+ client copy would make the 400 path unreachable and drift from the server.
105
+
106
+ A refused save routes through `routeRenditionError(message, knownRoles)`:
107
+
108
+ | Message | Lands |
109
+ |---|---|
110
+ | `<role>.<option>: …` | under that field, same slot as the hint |
111
+ | `<role>: …` | under that role's row |
112
+ | `_automatic: …` | beside the "runs on upload" switches |
113
+ | anything else, or a role not on screen | the panel-level alert |
114
+
115
+ Never a generic toast.
116
+
117
+ ## Mock
118
+
119
+ `client/renditions-mock.ts` copies the renderers' `default_renditions`,
120
+ automatic roles, `OPTION_SCHEMA`, limits and the error grammar verbatim.
121
+ `describeRenditions(globalRows)` serves the descriptor from the mock's global
122
+ Setting rows; `validateRenditionSetting(key, value)` runs on every
123
+ `/api/settings` create/update whose key is a rendition key and answers
124
+ `{status: false, error, error_code: 400}` before the row mutates. DELETE stays
125
+ 403. The mock seeds `FILEMAN_RENDITIONS_VIDEO = {"video_hevc": {"crf": 24}}`
126
+ so a changed field sits beside greyed defaults on first load. Personas with
127
+ the gate: `security.manager@nativemojo.com`, `showcase.operator@nativemojo.com`
128
+ (password `mojo`).
129
+
130
+ ## Pitfalls
131
+
132
+ - `DocumentRenderer` on django-mojo `main` (2026-10-10) has no
133
+ `config_category`, so `FILEMAN_RENDITIONS_DOCUMENT` is validated and
134
+ described but not yet applied by the renderer. The Documents tab ships per
135
+ contract; the override takes effect once the renderer honours it.
136
+ - Changing options never touches existing files: open a file under Storage →
137
+ Files and use **Regenerate renditions** to see the result.
138
+ - `video_hevc` is never automatic unless `_automatic` names it; H.265 costs
139
+ several times the H.264 encode time and does not play in every browser, so
140
+ keep `video_mp4` as the compatibility rendition (the Video tab says so).
141
+
142
+ Verification: `npm run verify:admin-media-rendering`.