portal-mojo 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -11
- package/docs/DetailView.md +3 -2
- package/docs/README.md +6 -2
- package/docs/account.md +4 -3
- package/docs/admin-bouncer.md +3 -3
- package/docs/admin-credentials.md +4 -3
- package/docs/admin-devices-geoip.md +1 -1
- package/docs/admin-dns-certificates.md +2 -2
- package/docs/admin-dns-records.md +1 -1
- package/docs/admin-edge.md +210 -0
- package/docs/admin-identity-directories-sources.json +2 -2
- package/docs/admin-identity-directories.md +9 -0
- package/docs/admin-identity-users.md +2 -2
- package/docs/admin-jobs.md +4 -4
- package/docs/admin-media-rendering.md +142 -0
- package/docs/admin-modals.md +7 -0
- package/docs/admin-monitoring.md +4 -4
- package/docs/admin-network-security.md +2 -2
- package/docs/admin-phonehub.md +4 -1
- package/docs/admin-storage.md +5 -2
- package/docs/auth.md +6 -0
- package/docs/calendar.md +1 -1
- package/docs/client.md +159 -26
- package/docs/collection-multiselect.md +2 -2
- package/docs/combobox.md +2 -2
- package/docs/dataview.md +1 -1
- package/docs/datepicker.md +3 -3
- package/docs/daterange.md +1 -1
- package/docs/datetimepicker.md +1 -1
- package/docs/defineModel.md +40 -7
- package/docs/detail-primitives.md +1 -1
- package/docs/feedback.md +9 -8
- package/docs/file-upload-ux.md +2 -2
- package/docs/idioms.md +1 -1
- package/docs/loading.md +2 -2
- package/docs/location-address.md +2 -1
- package/docs/markdown.md +3 -3
- package/docs/menus-and-access.md +21 -1
- package/docs/multiselect-dropdown.md +1 -1
- package/docs/popover.md +1 -1
- package/docs/record-feed.md +2 -2
- package/docs/taginput.md +2 -1
- package/docs/timepicker.md +1 -1
- package/docs/timezone-select.md +4 -3
- package/docs/user-menu.md +4 -3
- package/docs/worldmap.md +2 -2
- package/package.json +3 -2
- package/src/account/AccountModal.tsx +3 -2
- package/src/account/index.ts +2 -2
- package/src/admin/core/index.ts +1 -1
- package/src/admin/dns/models.ts +1 -1
- package/src/admin/domains/infrastructure.ts +12 -2
- package/src/admin/domains/security.ts +3 -1
- package/src/admin/edge/BlocklistPage.tsx +110 -0
- package/src/admin/edge/UpstreamsPage.tsx +113 -0
- package/src/admin/edge/VhostDetail.tsx +166 -0
- package/src/admin/edge/VhostWizard.tsx +273 -0
- package/src/admin/edge/VhostsPage.tsx +47 -0
- package/src/admin/edge/api.ts +56 -0
- package/src/admin/edge/index.ts +15 -0
- package/src/admin/edge/models.ts +395 -0
- package/src/admin/edge/permissions.ts +13 -0
- package/src/admin/identity/users/PersonalApiKeysPage.tsx +10 -0
- package/src/admin/identity/users/sections/actions.tsx +3 -3
- package/src/admin/index.ts +1 -0
- package/src/admin/phonehub/api.ts +5 -3
- package/src/admin/public/infrastructure.ts +1 -0
- package/src/admin/registry.ts +2 -1
- package/src/admin/storage/MediaRenderingPage.tsx +258 -0
- package/src/admin/storage/file-renditions.ts +12 -0
- package/src/admin/storage/index.ts +2 -0
- package/src/admin/storage/rendition-settings.ts +264 -0
- package/src/charts/exportChart.ts +1 -1
- package/src/client/action-result.ts +23 -61
- package/src/client/active-group.ts +9 -3
- package/src/client/auth.ts +38 -15
- package/src/client/client.ts +49 -10
- package/src/client/errors.ts +57 -2
- package/src/client/group-context.ts +2 -0
- package/src/client/group.tsx +112 -20
- package/src/client/hooks.ts +7 -3
- package/src/client/mock.ts +958 -14
- package/src/client/model.ts +15 -3
- package/src/client/renditions-mock.ts +212 -0
- package/src/client/scoped.ts +19 -5
- package/src/styles/components/account.css +304 -0
- package/src/styles/components/admin-assistant.css +28 -0
- package/src/styles/components/admin-bouncer.css +53 -0
- package/src/styles/components/admin-cloudwatch.css +23 -0
- package/src/styles/components/admin-dashboard.css +16 -0
- package/src/styles/components/admin-devices.css +260 -0
- package/src/styles/components/admin-dns-certificates.css +28 -0
- package/src/styles/components/admin-dns-registrar.css +1 -0
- package/src/styles/components/admin-dns.css +43 -0
- package/src/styles/components/admin-edge.css +1 -0
- package/src/styles/components/admin-incidents.css +14 -0
- package/src/styles/components/admin-jobs.css +237 -0
- package/src/styles/components/admin-media-rendering.css +38 -0
- package/src/styles/components/admin-messaging.css +3 -0
- package/src/styles/components/admin-monitoring.css +152 -0
- package/src/styles/components/admin-network.css +406 -0
- package/src/styles/components/admin-phonehub.css +61 -0
- package/src/styles/components/admin-push.css +30 -0
- package/src/styles/components/admin-rules.css +23 -0
- package/src/styles/components/admin-security.css +37 -0
- package/src/styles/components/admin-shortlinks.css +8 -0
- package/src/styles/components/admin-signin.css +64 -0
- package/src/styles/components/admin-storage.css +4 -0
- package/src/styles/components/auth.css +162 -0
- package/src/styles/components/calendar.css +228 -0
- package/src/styles/components/charts-c2.css +249 -0
- package/src/styles/components/collection-multiselect.css +114 -0
- package/src/styles/components/collection-select.css +63 -0
- package/src/styles/components/combobox.css +42 -0
- package/src/styles/components/dataview.css +139 -0
- package/src/styles/components/datepicker.css +106 -0
- package/src/styles/components/daterange.css +119 -0
- package/src/styles/components/datetimepicker.css +230 -0
- package/src/styles/components/detail-primitives.css +246 -0
- package/src/styles/components/detailview.css +69 -0
- package/src/styles/components/drawer.css +111 -0
- package/src/styles/components/file-upload.css +112 -0
- package/src/styles/components/form-wizard.css +11 -0
- package/src/styles/components/formview.css +74 -0
- package/src/styles/components/group-admin.css +302 -0
- package/src/styles/components/guardrail.css +29 -0
- package/src/styles/components/idioms.css +84 -0
- package/src/styles/components/image-editor.css +80 -0
- package/src/styles/components/loading.css +200 -0
- package/src/styles/components/location-address.css +32 -0
- package/src/styles/components/markdown.css +192 -0
- package/src/styles/components/multiselect-dropdown.css +92 -0
- package/src/styles/components/popover.css +35 -0
- package/src/styles/components/record-feed.css +161 -0
- package/src/styles/components/right-panel.css +37 -0
- package/src/styles/components/tabs.css +26 -0
- package/src/styles/components/taginput.css +132 -0
- package/src/styles/components/timepicker.css +282 -0
- package/src/styles/components/user-admin.css +189 -0
- package/src/styles/components/user-menu.css +98 -0
- package/src/styles/components/worldmap.css +97 -0
- package/src/styles/core.css +807 -0
- package/src/styles/index.css +69 -0
- package/src/styles/required-tokens.json +32 -0
- package/src/ui/FormWizard.tsx +9 -3
- package/src/ui/ThemeProvider.tsx +1 -1
- package/src/ui/detail/index.ts +2 -2
- package/src/ui/index.ts +3 -2
- package/src/ui/loading.tsx +1 -1
- 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
|
-
- **
|
|
67
|
-
(`panel`, `chip`, `tbl`, …)
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
package/docs/DetailView.md
CHANGED
|
@@ -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 `
|
|
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 `
|
|
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.**
|
|
92
|
-
|
|
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.
|
|
225
|
-
`
|
|
226
|
-
(`.ga-secret-*` token reveal)
|
|
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
|
package/docs/admin-bouncer.md
CHANGED
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
182
|
-
|
|
183
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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.
|
|
4
|
-
|
|
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 {
|
package/docs/admin-jobs.md
CHANGED
|
@@ -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
|
-
`
|
|
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
|
-
`
|
|
428
|
-
`
|
|
429
|
-
|
|
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`.
|