portal-mojo 0.2.20 → 0.2.21

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 (131) hide show
  1. package/docs/README.md +7 -1
  2. package/docs/admin-deployments.md +201 -0
  3. package/docs/admin-dns-certificates.md +3 -3
  4. package/docs/admin-dns-domains.md +92 -0
  5. package/docs/admin-dns.md +4 -2
  6. package/docs/admin-edge.md +200 -196
  7. package/docs/admin-geolocation.md +100 -0
  8. package/docs/admin-maintenance.md +133 -0
  9. package/docs/admin-modals.md +17 -2
  10. package/docs/admin-operational-fleet.md +206 -0
  11. package/docs/admin-presentation.md +51 -0
  12. package/docs/admin-signin.md +12 -2
  13. package/docs/client.md +7 -0
  14. package/docs/combobox.md +18 -0
  15. package/docs/popover.md +5 -4
  16. package/package.json +1 -1
  17. package/src/admin/core/OperationsHelp.tsx +10 -0
  18. package/src/admin/core/index.ts +6 -4
  19. package/src/admin/core/route-context.ts +6 -0
  20. package/src/admin/deployments/ApiDeploymentDetail.tsx +58 -0
  21. package/src/admin/deployments/DeploymentsPage.tsx +73 -0
  22. package/src/admin/deployments/WebAppDeploySetup.tsx +49 -0
  23. package/src/admin/deployments/WebAppDeploymentDetail.tsx +59 -0
  24. package/src/admin/deployments/WebAppDetail.tsx +104 -0
  25. package/src/admin/deployments/WebAppEditDialog.tsx +70 -0
  26. package/src/admin/deployments/WebAppKeyDialog.tsx +25 -0
  27. package/src/admin/deployments/WebAppOnboardingDialog.tsx +217 -0
  28. package/src/admin/deployments/WebAppRollbackDialog.tsx +82 -0
  29. package/src/admin/deployments/WebAppUploadPanel.tsx +132 -0
  30. package/src/admin/deployments/api.ts +158 -0
  31. package/src/admin/deployments/authority-state.ts +9 -0
  32. package/src/admin/deployments/deployment-state.ts +13 -0
  33. package/src/admin/deployments/index.ts +4 -0
  34. package/src/admin/deployments/onboarding-state.ts +11 -0
  35. package/src/admin/deployments/permissions.ts +37 -0
  36. package/src/admin/deployments/receipts.ts +78 -0
  37. package/src/admin/deployments/sanitize.ts +73 -0
  38. package/src/admin/deployments/shared.tsx +39 -0
  39. package/src/admin/deployments/types.ts +238 -0
  40. package/src/admin/deployments/upload.ts +53 -0
  41. package/src/admin/deployments/useDeploymentObservation.ts +104 -0
  42. package/src/admin/dns/DomainDetail.tsx +10 -6
  43. package/src/admin/dns/DomainOnboardingContent.tsx +44 -0
  44. package/src/admin/dns/DomainOnboardingDialog.tsx +5 -24
  45. package/src/admin/dns/DomainPurchaseWizard.tsx +1 -1
  46. package/src/admin/dns/DomainRegisterExisting.tsx +63 -0
  47. package/src/admin/dns/DomainRegistrarSettings.tsx +52 -0
  48. package/src/admin/dns/DomainsPage.tsx +22 -13
  49. package/src/admin/dns/SharedValidationDialog.tsx +1 -1
  50. package/src/admin/dns/api.ts +4 -2
  51. package/src/admin/dns/domain-navigation.ts +16 -0
  52. package/src/admin/dns/registrar-api.ts +13 -0
  53. package/src/admin/domains/assistant.ts +5 -4
  54. package/src/admin/domains/communications.ts +10 -9
  55. package/src/admin/domains/identity.ts +8 -7
  56. package/src/admin/domains/infrastructure.ts +19 -13
  57. package/src/admin/domains/lazy-page.ts +6 -0
  58. package/src/admin/domains/observability.ts +6 -5
  59. package/src/admin/domains/operations.ts +22 -7
  60. package/src/admin/domains/security.ts +16 -15
  61. package/src/admin/edge/EdgeHelp.tsx +10 -0
  62. package/src/admin/edge/ServingReadiness.tsx +54 -0
  63. package/src/admin/edge/VhostDetail.tsx +145 -139
  64. package/src/admin/edge/VhostWizard.tsx +204 -115
  65. package/src/admin/edge/VhostsPage.tsx +27 -28
  66. package/src/admin/edge/api.ts +24 -10
  67. package/src/admin/edge/models.ts +4 -4
  68. package/src/admin/edge/serving-data.ts +78 -0
  69. package/src/admin/fleet/CapacityEditor.tsx +81 -0
  70. package/src/admin/fleet/CapacityInspector.tsx +19 -0
  71. package/src/admin/fleet/CapacityProgress.tsx +9 -0
  72. package/src/admin/fleet/FleetHelp.tsx +7 -0
  73. package/src/admin/fleet/FleetOverview.tsx +150 -0
  74. package/src/admin/fleet/FleetPage.tsx +11 -0
  75. package/src/admin/fleet/FleetWorkspace.tsx +82 -0
  76. package/src/admin/fleet/ScaleFleetDialog.tsx +169 -0
  77. package/src/admin/fleet/access.ts +22 -0
  78. package/src/admin/fleet/api.ts +77 -0
  79. package/src/admin/fleet/data.ts +148 -0
  80. package/src/admin/fleet/index.ts +8 -0
  81. package/src/admin/fleet/operations.ts +179 -0
  82. package/src/admin/fleet/standalone-data.ts +87 -0
  83. package/src/admin/fleet/types.ts +208 -0
  84. package/src/admin/geolocation/GeoLocationPage.tsx +171 -0
  85. package/src/admin/geolocation/data.ts +138 -0
  86. package/src/admin/geolocation/index.ts +3 -0
  87. package/src/admin/identity/members/GroupMembersPanel.tsx +2 -8
  88. package/src/admin/index.ts +8 -0
  89. package/src/admin/maintenance/MaintenanceDialogs.tsx +161 -0
  90. package/src/admin/maintenance/MaintenancePage.tsx +442 -0
  91. package/src/admin/maintenance/data.ts +486 -0
  92. package/src/admin/maintenance/index.ts +3 -0
  93. package/src/admin/maintenance/operations.ts +208 -0
  94. package/src/admin/public/infrastructure.ts +1 -0
  95. package/src/admin/public/operations.ts +3 -0
  96. package/src/admin/registry.ts +6 -2
  97. package/src/admin/settings/SettingDetail.tsx +7 -2
  98. package/src/admin/settings/SettingEditor.tsx +2 -1
  99. package/src/admin/settings/SettingsPage.tsx +3 -1
  100. package/src/admin/settings/model.ts +1 -0
  101. package/src/admin/signin/SigninPage.tsx +17 -3
  102. package/src/admin/signin/api.ts +2 -0
  103. package/src/charts/MetricsChartView.tsx +4 -11
  104. package/src/charts/MetricsMiniWidget.tsx +3 -15
  105. package/src/client/client.ts +6 -0
  106. package/src/client/deployments-mock.ts +375 -0
  107. package/src/client/domain-operations-mock.ts +37 -0
  108. package/src/client/geo-provider-mock.ts +82 -0
  109. package/src/client/maintenance-mock.ts +285 -0
  110. package/src/client/mock.ts +52 -10
  111. package/src/client/operational-fleet-mock.ts +279 -0
  112. package/src/client/operations-mock.ts +85 -0
  113. package/src/client/serving-mock.ts +22 -0
  114. package/src/styles/components/admin-deployments.css +35 -0
  115. package/src/styles/components/admin-domains.css +12 -0
  116. package/src/styles/components/admin-edge.css +125 -1
  117. package/src/styles/components/admin-geolocation.css +40 -0
  118. package/src/styles/components/admin-maintenance.css +127 -0
  119. package/src/styles/components/admin-operational-fleet.css +38 -0
  120. package/src/styles/components/admin-operations.css +9 -0
  121. package/src/styles/components/admin-signin.css +2 -0
  122. package/src/styles/components/combobox.css +12 -2
  123. package/src/styles/components/fleet-page.css +9 -0
  124. package/src/styles/index.css +7 -0
  125. package/src/ui/ComboBox.tsx +39 -8
  126. package/src/ui/FilterBar.tsx +3 -5
  127. package/src/ui/ModelTable.tsx +2 -8
  128. package/src/ui/Popover.tsx +15 -7
  129. package/src/ui/date/TimePicker.tsx +4 -14
  130. package/src/ui/image-editor/ImageEditor.tsx +4 -5
  131. package/src/ui/schema-form-core.tsx +3 -5
@@ -1,210 +1,214 @@
1
- # Edge Admin
1
+ # Edge Admin · Serving
2
2
 
3
3
  ```ts
4
4
  import {
5
5
  EDGE_ADMIN_SECTION, VhostsPage, VhostDetail, VhostWizard, openVhostWizard,
6
6
  UpstreamsPage, BlocklistPage, EdgeVhostModel, EdgeRouteModel,
7
7
  EdgeUpstreamModel, EdgeBlocklistModel, buildVhostPayload,
8
- } from 'portal-mojo/admin';
8
+ } from 'portal-mojo/admin'; // also portal-mojo/admin/infrastructure
9
9
  ```
10
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.
11
+ `EDGE_ADMIN_SECTION` contributes Infrastructure routes `edge/vhosts` and
12
+ `edge/upstreams`, relative to the standalone or embedded `/system` mount.
13
+ Network Security owns `security/network/edge-blocklist`. Records open in
14
+ native modals, not record routes. Consumer source: django-mojo `mojo/apps/edge/`;
15
+ `client/mock.ts` mirrors its graphs and refusals, with wording limits below.
16
+
17
+ ## Permissions and ownership
18
+
19
+ All clauses are global; section and route gates both apply.
20
+
21
+ | Resource/action | Authority |
22
+ |---|---|
23
+ | Vhost, route, upstream view | `sys.view_dns OR sys.manage_dns OR sys.security` |
24
+ | Vhost/route change or delete; upstream enabled switch | `sys.manage_dns OR sys.security` |
25
+ | Upstream declare/retire | Literal `me.is_superuser`; server `require_platform_admin` also refuses API keys |
26
+ | Blocklist view | `sys.view_security OR sys.manage_security OR sys.security` |
27
+ | Blocklist change/delete | `sys.manage_security OR sys.security` |
28
+
29
+ `EDGE_BLOCKLIST_VIEW_PERMS` deliberately includes `manage_security`: manage does
30
+ not imply view in the client. Security-only operators cannot enter DNS-gated
31
+ Edge, hence the separate blocklist route. Member grants and `?group=` do not
32
+ open the fleet-wide blocklist.
33
+
34
+ A vhost on a group-less domain is a **house row**: non-superusers cannot list,
35
+ inspect or create it or its routes, even with global DNS grants. Vhost graphs'
36
+ `domain: basic` omits group, so never infer ownership there. Non-superuser domain
37
+ choices use `group__isnull: false`. Resolve the domain's own graph before offering
38
+ enabled upstreams: `group=<owner>` includes owned and shared rows; platform domains
39
+ use `group__isnull: true`. Unresolved ownership disables selection.
47
40
 
48
41
  ## Endpoints
49
42
 
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.
43
+ | `/api/edge/…` | Contract |
44
+ |---|---|
45
+ | `vhost`, `vhost/<id>` | list/read/create/update/delete; table graph `list`, record graph `default` |
46
+ | `route`, `route/<id>` | list/read/create/update/delete; `site_api` only; filter `vhost` |
47
+ | `upstream`, `upstream/<id>` | list/read/update `is_enabled`; no collection create/delete |
48
+ | `upstream/declare` | POST `{name, kind, host+port OR socket_path, group?}` |
49
+ | `upstream/retire` | POST `{upstream}`; disables and retains row |
50
+ | `blocklist`, `blocklist/<id>` | list/read/create/update/delete; fleet-wide |
51
+
52
+ Refused saves reject at the one client boundary. Public save/delete helpers
53
+ accept an optional `beforeWrite` guard, invoked before **every** fresh-auth attempt.
54
+
55
+ ## Serving tasks
56
+
57
+ The lean table shows hostname/domain, type and enabled intent. Pool and timestamps
58
+ belong in inspection. The Domain picker writes URL filter state; search supports
59
+ label/type/pool, not derived full hostnames. Persisted key remains
60
+ `admin:edge:vhosts`; public `Vhost*` exports and `/edge/vhosts` remain compatible.
61
+
62
+ - **Add hostname:** Hostname → Certificate → Review. Name, type and destination
63
+ belong to one Hostname task. Compact type choice,
64
+ only relevant destination controls, visible action footer. Other settings holds
65
+ pool/body/static/quiet paths without an accordion. Success opens the saved row.
66
+ - **Edit:** Hostname / Certificate / Settings share one controlled draft.
67
+ Validation selects the relevant tab and focuses the invalid input. Saving retains
68
+ the active panel. Domain is immutable. Info-icon popovers carry secondary help.
69
+ - **Inspect:** Overview / Routes / Settings; Routes only for Site + API.
70
+ Overview shows actual redirect/API destination first, followed by the certificate
71
+ name, stored status/expiry and direct Change certificate action. Settings holds other knobs, timestamps and
72
+ read-only platform policy. Header owns enable/disable, Edit and Delete.
73
+ - **Routes:** paginated local table, focused Add/Edit dialogs and explicit removal
74
+ confirmation. Before rename/removal, inspect current quiet paths and complete
75
+ bounded routes. Block orphaning a quiet path and identify what to repair in
76
+ Settings first; no atomic multi-row operation is implied.
77
+
78
+ Styles are scoped to these surfaces; shared Tabs/DetailView and Dashboard behavior
79
+ are unchanged. See [presentation QA](admin-presentation.md).
80
+
81
+ ## Kinds, payloads and validation
82
+
83
+ | Kind | Required | Supported options | Refused |
84
+ |---|---|---|---|
85
+ | `api` | `upstream` | quiet paths, static files | SPA, redirect |
86
+ | `site` | — | SPA | upstream, quiet paths, static, redirect |
87
+ | `site_api` | explicit routes on create | SPA, quiet paths, static | whole-host upstream, redirect |
88
+ | `redirect` | `redirect_to` | — | other kind-specific knobs |
89
+
90
+ `buildVhostPayload(draft, {create})` allowlists the chosen kind and sends empty
91
+ values for its unsupported knobs, clearing values from a former kind. It never
92
+ sends `mojosec_policy`. The server can refuse kind/SPA changes that conflict with
93
+ that policy's response class; show its refusal.
94
+
95
+ Client mirrors in `edge/models.ts`; server remains authoritative:
96
+
97
+ - Label: empty apex, `*` wildcard or one lowercase DNS label. Derive `server_name`.
98
+ - Body: integer 1–4096 MB, default 50; absent for redirect.
99
+ - Pool: deployment-declared `EDGE_POOLS`, default `["default"]`; no client catalog.
100
+ - Redirect: hostname only; no scheme/path/port/wildcard.
101
+ - Route/quiet path: leading `/`, letters/digits/`._-/`, at most 128 characters,
102
+ no `//` or `..` segment. A route cannot be `/`.
103
+
104
+ Only enabled rows require a certificate belonging to the domain and covering the
105
+ address, and a unique enabled domain/label. Stage a replacement disabled, disable
106
+ the old row, then enable the replacement. `certificateCovers` knows a wildcard
107
+ covers one label and never the apex. Coverage stays Unknown until the full
108
+ certificate graph (including SANs) is read; disabled staging remains available.
109
+
110
+ ## Staging and uncertain writes
111
+
112
+ `site_api` quiet paths require stored route prefixes, so create in this order:
113
+
114
+ 1. Vhost disabled, without quiet paths.
115
+ 2. Routes sequentially.
116
+ 3. Final vhost save with quiet paths and chosen enabled intent; always perform this
117
+ step so enable-time validation runs last.
118
+
119
+ The wizard requires at least one valid create route, though the server accepts
120
+ route-less enabled rows. Edits and Site → Site + API conversion may remain
121
+ route-less; author routes in detail. Existing routes block conversion away from
122
+ Site + API. Unread routes defer quiet-path validation to the server.
123
+
124
+ Retain acknowledged steps on explicit refusals. Transport loss, malformed receipts
125
+ and 5xx may follow committed writes and stay **unconfirmed**, never “refused” or
126
+ “still disabled”. An unacknowledged create cannot replay: close and inspect Serving.
127
+ An acknowledged partial create offers **Inspect saved hostname**.
128
+
129
+ **Observe outcome** reads only. Route continuation requires the matching stored
130
+ prefix/upstream; absence stays blocked. Edit/final-save observation compares the
131
+ exact normalized submitted payload, including FK IDs and arrays; unchanged or
132
+ conflicting fields retain the guard and show differences. Only an exact match
133
+ clears it. Continuing requires explicit **Add hostname** or **Save changes**;
134
+ observation never automatically replays a mutation.
135
+
136
+ Detail edits, route writes, enablement and deletion serialize through one pending
137
+ state. Lock Escape/backdrop/X during preflight and writes. Before each write and
138
+ fresh-auth attempt, check original session/current permission; owner disposal
139
+ closes nested dialogs and prevents later staged writes. Closing cannot cancel an
140
+ already submitted server operation or prove its outcome.
141
+
142
+ Route safety reads paginate to at most 2,000 rows and reject changed counts,
143
+ duplicate IDs, incomplete pages and excess inventory. Same-count concurrent edits
144
+ cannot all be detected; there is no atomic snapshot guarantee.
145
+
146
+ Delete confirms the address, stopped serving and route cascade, warns linked sites
147
+ can stop serving, retains domain/certificate and offers Disable as reversible.
148
+
149
+ ## Serving evidence
150
+
151
+ Read-only `ServingReadiness.tsx`/`serving-data.ts` keep positive projections outside
152
+ Query. `GET /api/account/admin/bootstrap` requires an authenticated matching global
153
+ identity with `view_admin OR manage_users OR manage_settings OR admin` (literal
154
+ superuser bypass); DNS grants alone never request it. Retain only matching user
155
+ ID/superuser, setup capability and edge available/HTTP/issuance fields.
156
+
157
+ `GET /api/account/admin/setup/readiness?section=hosting_vhosts` additionally requires
158
+ successful same-identity bootstrap with setup capability and literal superuser in
159
+ both Me and bootstrap. Server still refuses key-backed/noninteractive sessions;
160
+ client invents no provenance flag. Schema 1 validates status/time/count/coverage,
161
+ keeps the selected section only, caps checks at 64 and scalar details at 16 keys.
162
+
163
+ The unboxed summary sits within the page header and keeps timestamp and
164
+ partial-report warning visible. DNS-only sessions that cannot request bootstrap
165
+ see no global checks summary; admitted observation failures remain visible. Check
166
+ details holds technical evidence. HTTP is a file-rendering setting; DNS-01 is an
167
+ issuance method. Checks cover enabled rows' domain/certificate status, **not** live
168
+ serving, expiry/coverage/material installation, fleet convergence/nodes/generation.
169
+ An omitted hostname has no individual verdict. Enabled/saved is configuration intent.
170
+
171
+ Errors, denied attempted reads, old-backend 404s and malformed responses show Unavailable or
172
+ Unknown. Failed refresh clears old evidence. UID/session revision fence reads;
173
+ same-session loss of admission/bootstrap/superuser/setup authority disposes report
174
+ modals. Reads occur only while mounted/admitted, without polling; configuration
175
+ changes refresh the panel.
176
+
177
+ ## Upstreams and blocklist
178
+
179
+ An upstream's name/destination are fixed at declaration; only enabled is editable.
180
+ Lists include owned and shared rows. Retiring disables and retains it: reaching
181
+ vhosts/routes stop serving and are not repointed. Confirm that consequence.
182
+
183
+ Blocklist modes: `log` (default, still served), `enforce` (refused), `allow`
184
+ (exemption), `off` (retained inactive). Start in log, inspect the watch log, then
185
+ enforce. Propagation takes about ten minutes. Save IPs as canonical networks:
186
+ `10.1.2.3/8` becomes `10.0.0.0/8`; show the returned value. Declare and blocklist
187
+ editors guard native dismissal; blocklist save/delete share pending state.
188
+
189
+ ## Boundaries and pitfalls
190
+
191
+ - No claim-reserved action: django-mojo removed it in `bc2c57b1` (v1.7.1 onward).
192
+ Platform-owned `mojosec_policy` is read-only; never send it.
193
+ - No owning-WebApp/alias graph, pool catalog, route-disable flag, atomic multi-route
194
+ write or per-host live readiness. Aliases look like other rows; show refusals.
195
+ - Mock uniqueness wording (enabled hostname, route prefix, blocklist value,
196
+ upstream name) and missing-relation wording were not measured on the server.
197
+ - Blocklist parsing follows Python `ipaddress`/`re`, not JS RegExp: reject
198
+ `010.1.2.3` and `\q`; accept `10.0.0.0/255.0.0.0`, `(?i)bot`, `a*+`.
199
+ Compilation reason text is mock-specific; local validation is only a first pass.
200
+ - Client certificate checks are advisory; only the server validates a save.
199
201
 
200
202
  ## Themes and showcase
201
203
 
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.
204
+ Canonical token-only `src/styles/components/admin-edge.css` ships through
205
+ `portal-mojo/styles.css`, imported by Admin and Showcase. Demo: Components → Admin
206
+ → Edge, under platform administrator, DNS manager/viewer and security manager/viewer.
207
+ Verify both themes, desktop/390px, opened dialogs, keyboard/help, busy and error states.
208
+
209
+ Contracts: `npm run verify:admin-edge` (API plus held dialog writes),
210
+ `node scripts/verify-admin-serving-mounted.mjs` (all shapes, staging, draft/focus,
211
+ conversion, partial inspection, uncertainty, route guards/session races),
212
+ `node scripts/verify-admin-serving-mounted-evidence.mjs` (DTO bounds, exact gates,
213
+ unknown evidence, failed refresh, auth/permission replacement). Mounted tests do
214
+ not establish native layout; actual browser QA remains required.
@@ -0,0 +1,100 @@
1
+ # GeoLocation provider setup
2
+
3
+ `GeoLocationPage` (`portal-mojo/admin/operations`) is the global Operations →
4
+ GeoLocation surface. GeoIP Cache inspects IPs; Fleet Configuration applies
5
+ published settings. Dashboard is unchanged. Hosts may override the mount-derived Fleet link with
6
+ `fleetConfigurationPath`; Showcase opens the actual Fleet Configuration page with a return link.
7
+
8
+ Admission requires literal `is_superuser === true` and a current interactive
9
+ session. The backend also rejects key-backed sessions, requires same-origin
10
+ POSTs and authentication within 600 seconds. Global/group grants do not replace
11
+ these checks. `usePrivateOperator` remounts on session change, aborts requests and
12
+ owned dialogs, and rechecks authority before sending and accepting responses.
13
+
14
+ ## Tasks
15
+
16
+ Primary, fallback and ordered additional providers form one visible configuration task,
17
+ with add, remove and move controls. MojoVerify credentials and threat sharing sit
18
+ below the chain. One retained draft and Save/Discard footer cover the form; there
19
+ is no tab navigation for parts of the same task. The uncontrolled password element
20
+ stays mounted while editing, and shared Tabs is unchanged.
21
+
22
+ MojoVerify always uses `https://api.mojoverify.com`; no URL control is shown.
23
+ Older origins remain readable, but this page stages the hosted URL and warns
24
+ that switching requires a replacement key. Provider names are human-readable;
25
+ unknown configured identifiers are retained. New additional rows exclude primary,
26
+ fallback and existing rows. Runtime deduplicates the chain; existing overlaps are
27
+ not silently rewritten. Non-Mojo credentials are configured outside this endpoint.
28
+ The server's values are authoritative: setup defaults differ from runtime defaults.
29
+
30
+ No key shows **Add key**; a configured key shows **Replace key** and **Remove
31
+ key**. Test key sits with the credential. Blank replacement preserves the stored
32
+ key; confirmed removal is staged until Save changes and does not revoke remotely.
33
+ Save/Discard reflect actual draft changes. Refresh and the page-owned Open Fleet Configuration link confirm before discarding an
34
+ unsaved draft. Keep editing preserves both provider selections and the entered key;
35
+ explicit departure wipes the key before navigation. An in-flight operation cannot
36
+ be interrupted through that link. Info popovers hold secondary explanations; Configuration information
37
+ opens a focused native dialog for revision and publication facts. No disclosure
38
+ sections or sidebar modal are used.
39
+
40
+ Last stored-key check shows its historical outcome and timestamp separately from
41
+ the draft result. Pending activation and Open Fleet Configuration remain visible.
42
+ A false pending flag concerns only this request node, never proves fleet health.
43
+
44
+ ## Private protocol
45
+
46
+ `readGeoLocation()` positively projects only `provider_setup` from schema-v1
47
+ `/api/account/admin/settings`. Catalog, SMS, unknown fields and raw credentials
48
+ are discarded. Form state is imperative, outside Query/Mutation caches.
49
+
50
+ `buildGeoPayload(action, configuration_revision, draft, keyEdit)` sends exactly
51
+ `{action,topic:'geoip',providers:{expected_revision,geoip}}`. The revision is sent
52
+ even when null. The five static keys are primary, fallback, additional, Mojo URL
53
+ and sync. Untouched/blank replacement omits credentials; nonempty replacement
54
+ adds `GEOIP_API_KEY_MOJO`; confirmed removal adds `clear_api_key:true`.
55
+ Additional providers are unique and ordered, at most 16. Identifiers match
56
+ `^[a-z][a-z0-9_-]{0,31}$`. HTTPS origins permit no credentials, path, query,
57
+ fragment or alternate port (443 allowed); keys are at most 4096 characters.
58
+
59
+ Secrets stay only in the password DOM and short-lived request objects, whose
60
+ raw key is deleted in `finally`. They never enter React/Query/Mutation state,
61
+ URLs, storage, logs or mock observations. Editing retains the element and candidate;
62
+ Save attempts, explicit discard, fresh-auth refusal, session change and unmount
63
+ clear it. A stable ref callback wipes the password element before detachment;
64
+ removing the DOM node alone is insufficient. Writes are single-flight. A 440 invokes fresh authentication without
65
+ replaying; Refresh and a deliberate retry are required. Stale revisions
66
+ (400 “changed; reload before publishing”, or 409), unreadable state and uncertain
67
+ saves require authoritative Refresh before another write.
68
+
69
+ Test verifies **only MojoVerify credentials**, regardless of primary. It requires
70
+ the five delegated descriptor keys but not Fleet bucket/object/KMS/restart
71
+ availability; Save requires the publishing plane. Sharing requires the remote
72
+ key's `geoip_sync` permission. The current backend also verifies Mojo on every
73
+ Save except explicit removal: selecting another primary does not enable an
74
+ independent no-Mojo setup. This page does not invent provider-chain testing,
75
+ other-provider key editing, provisioning, revocation or an Apply endpoint.
76
+
77
+ A draft-key test does not rewrite stored verification. The backend's stored-test
78
+ predicate is no replacement plus the node-loaded origin and sync value; chain
79
+ changes do not affect it. Tests reread stored evidence without rebasing the
80
+ draft's revision. Revision changes or lost normalization authority block retry;
81
+ publishing-only unavailability does not block diagnostics. Loaded/desired origin
82
+ drift can prevent stored-key reuse until Fleet apply or explicit replacement.
83
+
84
+ Save rereads authoritative configuration. `published:true` means static
85
+ publication with restart pending; `unchanged:true` can still mean the database
86
+ credential changed. Neither result proves activation. Failed, stale and uncertain
87
+ requests keep errors actionable and never become successful saves.
88
+
89
+ ## Verification
90
+
91
+ `geo-provider-mock.ts` supplies isolated safe observations (action, key action,
92
+ stored-test predicate, expected revision), with no submitted key retained.
93
+ Scenarios include no-key setup, real registry names, unknown/empty registry,
94
+ publishing unavailable, stale/fresh-auth/uncertain outcomes and loaded-origin drift.
95
+ Showcase's GeoLocation demo mounts the actual page. Run
96
+ `npm run verify:admin-geolocation` and `npm run verify:admin-geolocation-mounted`;
97
+ JSDOM behavior does not establish native focus or layout. Inspect both apps,
98
+ light/dark, desktop/390px, opened dialogs and error recovery per
99
+ [Admin presentation](admin-presentation.md). The scoped stylesheet is included
100
+ through `portal-mojo/styles.css`.