portal-mojo 0.2.21 → 0.2.23
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/docs/ModelTable.md +11 -0
- package/docs/admin-assistant.md +75 -23
- package/docs/admin-dns-domains.md +96 -90
- package/docs/admin-dns-records.md +1 -1
- package/docs/admin-dns-registrar.md +29 -4
- package/docs/admin-dns.md +2 -3
- package/docs/admin-modals.md +21 -0
- package/docs/menus-and-access.md +24 -0
- package/package.json +1 -1
- package/src/admin/assistant/AssistantFeed.tsx +7 -4
- package/src/admin/assistant/AssistantSetupPage.tsx +141 -64
- package/src/admin/assistant/api.ts +9 -7
- package/src/admin/assistant/pages.tsx +125 -109
- package/src/admin/assistant/setup.ts +1 -1
- package/src/admin/assistant/skill-lifecycle.ts +5 -4
- package/src/admin/core/index.ts +10 -2
- package/src/admin/dns/DnsRecordsPanel.tsx +45 -9
- package/src/admin/dns/DomainAssignment.tsx +55 -0
- package/src/admin/dns/DomainDetail.tsx +55 -38
- package/src/admin/dns/DomainOnboardingContent.tsx +9 -4
- package/src/admin/dns/DomainPurchaseDetail.tsx +42 -7
- package/src/admin/dns/DomainPurchaseWizard.tsx +102 -72
- package/src/admin/dns/DomainPurchasesPage.tsx +1 -1
- package/src/admin/dns/DomainRegisterExisting.tsx +21 -6
- package/src/admin/dns/DomainRegistrarSettings.tsx +28 -48
- package/src/admin/dns/DomainRegistrationPanel.tsx +127 -0
- package/src/admin/dns/DomainsPage.tsx +25 -24
- package/src/admin/dns/ProviderCredentialsPage.tsx +32 -18
- package/src/admin/dns/RegistrantContactPage.tsx +73 -27
- package/src/admin/dns/api.ts +26 -25
- package/src/admin/dns/domain-authority.ts +11 -0
- package/src/admin/dns/index.ts +2 -2
- package/src/admin/dns/purchase-data.ts +5 -3
- package/src/admin/dns/registrar-api.ts +57 -11
- package/src/admin/domains/infrastructure.ts +8 -8
- package/src/admin/domains/operations.ts +6 -6
- package/src/admin/edge/VhostsPage.tsx +6 -1
- package/src/admin/jobs/JobDashboardPage.tsx +14 -4
- package/src/client/mock.ts +14 -8
- package/src/client/params.ts +3 -2
- package/src/styles/components/admin-assistant.css +83 -20
- package/src/styles/components/admin-domains.css +67 -11
- package/src/styles/components/admin-edge.css +1 -0
- package/src/styles/components/admin-jobs.css +1 -0
- package/src/ui/FilterBar.tsx +16 -10
- package/src/ui/ModelTable.tsx +7 -3
- package/src/ui/SidebarNav.tsx +10 -1
- package/src/ui/menu-registry.ts +2 -0
- package/src/ui/preset-owned.ts +39 -0
package/docs/ModelTable.md
CHANGED
|
@@ -174,6 +174,17 @@ The table retains native row/cell semantics.
|
|
|
174
174
|
(`__gte`), `daterange` (the `dr_*` triple). Applied filters render as
|
|
175
175
|
editable pills; presets are param bundles with DERIVED active state.
|
|
176
176
|
|
|
177
|
+
**The selected preset owns its keys.** While a preset matches, its params
|
|
178
|
+
render no pill and don't count on the Filter button, and every Clear (the
|
|
179
|
+
pill row's Clear all, the Filter menu's, the empty state's) keeps them —
|
|
180
|
+
the segment is the one control for them. So a `defaultParams` default that
|
|
181
|
+
matches a preset (`{ status: 'submitted' }` beside a "Submitted" preset)
|
|
182
|
+
shows as the highlighted segment, never as a removable pill. Filters added
|
|
183
|
+
on top still pill. The Filter menu keeps its check on an owned key's
|
|
184
|
+
definition; editing it there changes the value, the preset stops matching,
|
|
185
|
+
and the pill returns. A default with NO matching preset is still a plain
|
|
186
|
+
removable pill; scope that must never be removable is `fixedParams`.
|
|
187
|
+
|
|
177
188
|
## Selection + batch
|
|
178
189
|
|
|
179
190
|
`selectable` renders checkboxes independently of `batchActions` (decoupled
|
package/docs/admin-assistant.md
CHANGED
|
@@ -15,24 +15,44 @@ Assistant permissions and member/group grants cannot satisfy it. The server
|
|
|
15
15
|
also rejects confined ApiKey/GroupScopedToken sessions and enforces person
|
|
16
16
|
ownership; the client does not invent a broader per-user-key ban.
|
|
17
17
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
an
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
18
|
+
The page separates **AI Assistant** from **MCP connections** with two task
|
|
19
|
+
tabs. AI Assistant is the built-in provider-backed chat and incident processing;
|
|
20
|
+
MCP serves tools to external agents that bring their own AI. MCP does not
|
|
21
|
+
require an Assistant provider key, model selection, policy acknowledgement or
|
|
22
|
+
enabled chat switch. The MCP tab shows its address, independent access switch,
|
|
23
|
+
public discovery check and connections directly; AI readiness warnings remain
|
|
24
|
+
in the AI tab. Ordinary reads probe neither provider nor public discovery.
|
|
25
|
+
|
|
26
|
+
The AI tab starts with a compact status overview: the enabled switch, current
|
|
27
|
+
provider/model and selected connection, followed by Safety & usage and Incident
|
|
28
|
+
triage. Configure model and Manage keys open focused native dialogs. The API
|
|
29
|
+
keys dialog separates Assistant and Platform keys with two task tabs; Safety &
|
|
30
|
+
usage separates Policy & stops from Usage & recovery. Incident triage is a
|
|
31
|
+
direct dialog. Keyboard-accessible info icons hold catalogue metadata,
|
|
32
|
+
credential explanations, policy/admission diagnostics, connection dates and
|
|
33
|
+
the triage watermark. Errors, failed checks, emergency stops and operation
|
|
34
|
+
consequences stay visible beside their actions. No expandable sections or
|
|
35
|
+
single-item side navigation are used.
|
|
36
|
+
|
|
37
|
+
A configured key and enabled switch do not prove readiness. `route.ready`
|
|
38
|
+
describes configuration, not current provider health. Unknown routes remain
|
|
39
|
+
unknown: a missing credential can cause the backend to omit the entire route.
|
|
40
|
+
A handler route opens Manage keys on Platform key. Bedrock uses deployment AWS
|
|
41
|
+
identity; the key dialog explains that browser keys cannot configure it.
|
|
42
|
+
Assistant, Platform and AWS credentials never substitute for one another.
|
|
43
|
+
Missing Assistant/realtime applications are reported. Runtime model identifiers
|
|
44
|
+
preserve provider punctuation, including Bedrock colons; only a manual pin uses
|
|
45
|
+
the backend's restricted pin syntax.
|
|
46
|
+
|
|
47
|
+
Policy activation acknowledges the deployed policy on this node, does not edit
|
|
48
|
+
it and never establishes fleet-wide agreement. Static, database and effective
|
|
49
|
+
emergency stops are separate; only the database switch is writable. Activation
|
|
50
|
+
and breaker reset cannot release a stop. Destructive and consequential actions
|
|
51
|
+
use a nested confirmation, initially focused on Cancel. During an operation,
|
|
52
|
+
repeat submission, task tabs and native Escape/backdrop/close are locked. Cancel
|
|
53
|
+
returns to the task without making a request. Closing a task or changing key
|
|
54
|
+
types clears unsaved secret drafts; reopening model selection restores the
|
|
55
|
+
saved value. All dialogs unmount with the owner/session boundary.
|
|
36
56
|
|
|
37
57
|
Model selection controls the protected runtime selection only when the backend
|
|
38
58
|
returns a valid capability for the explicit Anthropic `auto:sonnet` policy.
|
|
@@ -42,8 +62,7 @@ unavailable until deployment removes that pin. Mode is authoritative even when
|
|
|
42
62
|
a fixed route retains an inert legacy `selected` value.
|
|
43
63
|
|
|
44
64
|
The exact resolved runtime identifier (`model.effective`) and source are
|
|
45
|
-
separate from the selection draft and `route.ready`.
|
|
46
|
-
and safety contains the raw policy model and admission diagnostic. The additive
|
|
65
|
+
separate from the selection draft and `route.ready`. Safety & usage → Policy & stops → the Deployed policy info icon contains the raw policy model and admission diagnostic. The additive
|
|
47
66
|
`route.can_invoke` reports admission to the guarded request path: a cold
|
|
48
67
|
automatic catalogue can admit a request while configuration readiness remains
|
|
49
68
|
false. It does not prove that a usable model or healthy provider is ready.
|
|
@@ -78,7 +97,7 @@ changes conservatively discard it: the shared auth API cannot distinguish
|
|
|
78
97
|
renewal from same-person replacement across tabs. A 440 uses the existing
|
|
79
98
|
fresh-auth UI after clearing credential drafts; even successful reauthentication
|
|
80
99
|
requires explicit re-entry/retry. No secret is silently replayed. Confirmation
|
|
81
|
-
is page-
|
|
100
|
+
is a page-owned native dialog, focuses Cancel, and ends with the page/session scope.
|
|
82
101
|
|
|
83
102
|
Protocol helpers are exported from `portal-mojo/admin/assistant`:
|
|
84
103
|
`getAssistantSetup(options, probe?)`, `mutateAssistantSetup(action, options)`,
|
|
@@ -153,7 +172,7 @@ fake password `mojo`. No real credentials or deployment changes belong in demos.
|
|
|
153
172
|
`verify:admin-assistant-setup` covers wire contracts, owner navigation/admission,
|
|
154
173
|
projections and mounted auth/draft/confirmation/fresh-auth/transport/cache
|
|
155
174
|
lifecycle; it is wired into `verify:release`. Browser checks separately establish
|
|
156
|
-
both-theme layout, native
|
|
175
|
+
both-theme layout, native dialogs, help popovers, keyboard controls and console behavior.
|
|
157
176
|
|
|
158
177
|
## Conversation transport
|
|
159
178
|
|
|
@@ -218,9 +237,42 @@ per-type schemas/caps; unknown/malformed blocks drop. Context references are
|
|
|
218
237
|
inert and allowlisted. Action choices require confirmation, then their bounded
|
|
219
238
|
value is sent attachment-free through the ordinary REST POST.
|
|
220
239
|
|
|
240
|
+
## Management pages
|
|
241
|
+
|
|
242
|
+
Conversations and Skills use a clean searchable list with URL-backed pagination
|
|
243
|
+
and Refresh. Searches target conversation titles or skill names. Record details
|
|
244
|
+
open in compact native dialogs without tabs or a side rail. Conversations label
|
|
245
|
+
messages with their actual owner and compare the viewer separately; foreign
|
|
246
|
+
history shows an explicit viewing-only notice and no composer. Owned history
|
|
247
|
+
retains realtime/REST transport selection, completed attachments and no automatic
|
|
248
|
+
resend after an unknown outcome. New conversations start from the app's Assistant
|
|
249
|
+
button.
|
|
250
|
+
|
|
251
|
+
Skill details show trigger phrases and readable ordered steps. Tool identifiers,
|
|
252
|
+
dates and secondary explanations use info popovers. Automatic execution remains
|
|
253
|
+
visibly called out: read-only steps can run automatically; changes still require approval. The sole lifecycle action deactivates/reactivates through the
|
|
254
|
+
existing endpoint, then reloads authoritative state; no create/delete UI is added.
|
|
255
|
+
|
|
256
|
+
The page lifetime is keyed to person, auth-session revision and global permissions.
|
|
257
|
+
Late reads cannot replace newer results. Writes check live authority after auth
|
|
258
|
+
preflight, use synchronous single-flight guards and lock native dismissal while
|
|
259
|
+
pending. Private history/memory stays component-local. The Dashboard is unchanged.
|
|
260
|
+
|
|
221
261
|
## Memory
|
|
222
262
|
|
|
223
|
-
Global and user tiers require the global Assistant grant. Group memory first chooses from a
|
|
263
|
+
Global and user tiers require the global Assistant grant. Group memory first chooses from a searchable normal `/api/group` picker and supplies that id as normal request context (`?group=<id>`). A group id is never put in the memory body and ambient group state is never consulted. Keys start with a lowercase letter or number and are limited to 64 lowercase alphanumeric/colon/underscore/hyphen characters; values are limited to 500 characters.
|
|
264
|
+
|
|
265
|
+
Memory uses Personal, Global and Group tabs because they select different
|
|
266
|
+
Assistant audiences. Lists show saved values with Edit and Remove actions; Add
|
|
267
|
+
opens a focused Key/Value dialog. Existing keys are immutable, duplicate new keys
|
|
268
|
+
cannot overwrite a saved value, and changing audience/group discards unsaved
|
|
269
|
+
drafts. The searchable group picker replaces the previous first-50 limit. Group
|
|
270
|
+
membership checks remain explicit and inherited; missing/refused checks are
|
|
271
|
+
visible. Members with the global Assistant grant can inspect group memory;
|
|
272
|
+
editing additionally requires inherited member Assistant authority or superuser. Shared edits name their audience, removal names its key and audience and
|
|
273
|
+
initially focuses Cancel. Writes keep errors in the dialog, retain drafts on
|
|
274
|
+
refusal and never replay automatically. Refresh failures are distinct from empty
|
|
275
|
+
memory. Saved entries are not silently truncated to the first 100.
|
|
224
276
|
|
|
225
277
|
## Shell ownership
|
|
226
278
|
|
|
@@ -1,92 +1,98 @@
|
|
|
1
1
|
# Domains
|
|
2
2
|
|
|
3
|
-
`DomainsPage`
|
|
4
|
-
host
|
|
5
|
-
`DomainModel
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
3
|
+
Import `DomainsPage` from `portal-mojo/admin/infrastructure`. The global DNS
|
|
4
|
+
inventory lives at the host's `/dns/domains` mount. Server search, filters,
|
|
5
|
+
sort and paging stay on `DomainModel` with `admin:dns:domains` persistence.
|
|
6
|
+
Scope comes from each row, never the active product group. An absent expiry
|
|
7
|
+
means **Unknown**, not Never. Narrow layouts combine provider, scope and status
|
|
8
|
+
under the name so the Records action stays visible.
|
|
9
|
+
|
|
10
|
+
## Everyday tasks
|
|
11
|
+
|
|
12
|
+
**Add domain** opens three explicit choices: **Connect GoDaddy domain**,
|
|
13
|
+
**Buy domain**, and literal-superuser **Adopt Route 53 zone**. Purchasing has its
|
|
14
|
+
own read-only ledger shortcut. **More domain actions → Shared validation setup**
|
|
15
|
+
keeps external-domain/controller setup available without an existing domain.
|
|
16
|
+
Viewers get inspection and Purchases; writes require global
|
|
17
|
+
`sys.manage_dns | sys.security`. Platform adoption/assignment additionally require
|
|
18
|
+
literal interactive `is_superuser`; backend gates remain authoritative.
|
|
19
|
+
|
|
20
|
+
A row opens a compact native dialog with Overview, provider-applicable DNS
|
|
21
|
+
records, Certificates, and manager Registration. It has no side rail or nested
|
|
22
|
+
registration disclosure. Only the selected task mounts. The same record panel
|
|
23
|
+
serves the standalone Records page; its type filter is controlled by that page's
|
|
24
|
+
URL or locally when embedded. Serving is an explicit sibling route under the
|
|
25
|
+
observed Admin mount. Extra registered domain links retain their own gates.
|
|
26
|
+
|
|
27
|
+
## Connect, repair and adopt
|
|
28
|
+
|
|
29
|
+
Connect selects one explicit authorized group through bounded
|
|
30
|
+
`/api/dnsman/credential/group-choice`, then an active, verified GoDaddy
|
|
31
|
+
credential in that exact group. Changing group clears credential state and
|
|
32
|
+
fences late replies. **Link credential** opens the existing write-only credential
|
|
33
|
+
editor locked to that group; a verified return resumes the same domain draft.
|
|
34
|
+
There is no generic group list, free-form group id or provider-account discovery.
|
|
35
|
+
The ownership probe sends exactly `{ group, domain, credential }` and spends no
|
|
36
|
+
money. Domain names are normalized before submission.
|
|
37
|
+
|
|
38
|
+
An existing GoDaddy domain with an unusable linked credential offers **Repair
|
|
39
|
+
linked credential**. Rotation verifies the same credential/group, clears raw
|
|
40
|
+
password refs, then refreshes safe credential and domain projections. This does
|
|
41
|
+
not expose an arbitrary credential-FK reassignment: the backend has no same-group
|
|
42
|
+
validation for that generic save. A domain with no linked credential cannot be
|
|
43
|
+
silently attached to a newly linked one.
|
|
44
|
+
|
|
45
|
+
Adoption reviews one exact existing public zone name and Platform scope before
|
|
46
|
+
posting `{ domain }`. It never supplies `group`/`create_zone` or discovers the AWS
|
|
47
|
+
account. Native dismissal is guarded during review and mutation. Cancel review
|
|
48
|
+
writes nothing. Unknown onboarding outcomes require inventory/provider review;
|
|
49
|
+
submitted forms never repeat their request.
|
|
50
|
+
|
|
51
|
+
A Platform domain offers **Assign to group** to literal superusers. Choose the
|
|
52
|
+
destination, review domain/from/to, and type the exact destination name. The UI
|
|
53
|
+
checks current authority, current domain ownership and the exact authorized group
|
|
54
|
+
again before `{ domain, group }`. Already-grouped rows never offer reassignment.
|
|
55
|
+
A lost receipt retains **Refresh owner** and refuses replay, even if the fresh
|
|
56
|
+
read still says Platform; a read cannot prove an older write has finished.
|
|
57
|
+
|
|
58
|
+
All onboarding writes check their owner at the transport's `beforeSend` boundary,
|
|
59
|
+
after any asynchronous auth refresh. Credential/purchase step-up continuations
|
|
60
|
+
perform that same check. A replacement session never inherits old consent.
|
|
61
|
+
|
|
62
|
+
## Registration
|
|
63
|
+
|
|
64
|
+
Registration starts with an explicit manager-only **Read registration**. WHOIS
|
|
65
|
+
contacts stay in the mounted dialog, outside Query, URLs, storage and logs.
|
|
66
|
+
Route 53 DNS hosting alone never admits registrar edits: the returned registrar
|
|
67
|
+
must identify Amazon Registrar/Route 53. Hosted-zone-only, unavailable or unknown
|
|
68
|
+
registrations remain read-only. GoDaddy registration tasks hand off to its account;
|
|
69
|
+
Mojo remains certificate-only. No manual Renew action is invented.
|
|
70
|
+
|
|
71
|
+
Confirmed Route 53 registration offers focused settings and contacts dialogs.
|
|
72
|
+
Auto-renew uses the existing best-effort boolean save; privacy uses its dedicated
|
|
73
|
+
all-contact action. Fresh owner/registrar/value preflight precedes writes. Only
|
|
74
|
+
live WHOIS readback can establish observed values; an accepted DB save is not
|
|
75
|
+
provider proof. Unknown outcomes lock the submitted editor and never replay.
|
|
76
|
+
An unchanged desired DB value may not re-run a failed model hook; the registrar
|
|
77
|
+
account handoff remains available.
|
|
78
|
+
|
|
79
|
+
Contact inspection groups identity, email/phone and postal address. Identical
|
|
80
|
+
Registrant/Admin/Technical contacts share one view; differing contacts get useful
|
|
81
|
+
role tabs. Editing keeps one draft across Contact/Address tabs. **Replace all
|
|
82
|
+
contacts** explicitly replaces all three roles, matching the backend contract.
|
|
83
|
+
Entering edit mode never submits. Registry extension fields are preserved locally.
|
|
84
|
+
Pending operations guard tab switches, native dismissal and repeated writes.
|
|
85
|
+
|
|
86
|
+
## Purchase and QA
|
|
87
|
+
|
|
88
|
+
Buying is restricted to one quoted year until the backend binds multi-year
|
|
89
|
+
payable totals. Privacy limits and acknowledged downgrades stay visible.
|
|
90
|
+
See [registrar flows](admin-dns-registrar.md) for exact confirmation and recovery.
|
|
91
|
+
|
|
92
|
+
`verify:admin-domains` includes delayed-auth transport fences.
|
|
93
|
+
`verify:admin-domains-mounted` runs onboarding, assignment/filter/credential,
|
|
94
|
+
registration and purchase/contact journey contracts, including StrictMode and
|
|
95
|
+
stale-session/scope cases. JSDOM establishes behavior only. Closing acceptance
|
|
96
|
+
uses actual Portal and built Showcase screenshots and interactions in light/dark,
|
|
97
|
+
desktop and 390px; see [visual QA](admin-presentation.md). No live registrar
|
|
98
|
+
purchase or infrastructure mutation is required for this mock acceptance.
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
The one canonical DNS section is `id: "dns"`, `basePath: "dns"`, in Infrastructure. Its routes are ordered `domains`, `records`, `certificates`, `purchases`, `registrant`, `credentials`. The read surfaces accept the global, system-pinned any-of view clause `sys.view_dns | sys.manage_dns | sys.security`; registrant and record mutation controls use `sys.manage_dns | sys.security`. Group/member grants never open Admin.
|
|
16
16
|
|
|
17
|
-
Domains uses the server-backed `DomainModel` list and
|
|
17
|
+
Domains uses the server-backed `DomainModel` list and compact native task dialog. Detail composes Overview, applicable DNS records, bounded Certificates and manager Registration; Records is the same `DnsRecordsPanel` used by the standalone page. That page owns only `domain` and `record_type` in the URL. An embedded panel owns a local type filter when no controlled value is supplied. It may select the first active domain only when `domain` is absent. Invalid, denied, missing, and inactive deep links are preserved and shown as errors.
|
|
18
18
|
|
|
19
19
|
## Wire contract
|
|
20
20
|
|
|
@@ -10,13 +10,28 @@ The wizard asks for one explicit group through bounded `/api/dnsman/credential/g
|
|
|
10
10
|
|
|
11
11
|
`POST /registrar/quote` returns `{purchase,name,price,currency,years,token,expires,privacy_supported}`. The raw token lives only in an immutable ref in the mounted wizard. It is never state, URL data, Query/Mutation cache data, mock request history, or durable mock data. The exact tuple binds purchase id, token, group, normalized domain, years, decimal price, currency, and expiry. Any upstream change or expiry clears it.
|
|
12
12
|
|
|
13
|
-
The
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
The portal purchases **one year** only. The backend currently keeps raw TLD
|
|
14
|
+
registration price separately from years, so the UI does not multiply it into an
|
|
15
|
+
allegedly authoritative multi-year payable total. `privacy_supported` is part of
|
|
16
|
+
the retained quote identity; an unsupported registry is explained before consent,
|
|
17
|
+
and an acknowledged `privacy_downgraded` result stays visible.
|
|
18
|
+
|
|
19
|
+
The operator types the exact normalized domain before the single-flight, nonretry `POST /registrar/purchase`. The token is removed from the ref before the request. Success and every exception reconcile through an imperative `GET /purchase/<id>?graph=default`; one non-overlapping bounded-backoff loop follows `submitted` to a durable terminal row. Timeout keeps exact **View purchase** and **Refresh** actions available. Each
|
|
20
|
+
refresh reads the retained purchase ID and never submits another purchase. A
|
|
21
|
+
completed receipt opens the acknowledged domain ID when available; the ledger
|
|
22
|
+
graph has no domain FK, so other completed receipts use a mount-relative
|
|
23
|
+
name-filtered inventory handoff.
|
|
24
|
+
|
|
25
|
+
After purchase is attempted, the wizard cannot retry or requote. A final
|
|
26
|
+
transport owner check runs after the asynchronous auth gate and before every
|
|
27
|
+
permitted 440 continuation. Session replacement stops that continuation. Only
|
|
28
|
+
the purchase ID/owner survives a same-user revision change, giving an exact-ID
|
|
29
|
+
recovery view; a different user receives none of the previous recovery state. A failed, ambiguous, or timed-out result always says that money may have moved and directs the operator to the AWS Route 53 Domains operation console and durable ledger first.
|
|
16
30
|
|
|
17
31
|
## Purchase ledger
|
|
18
32
|
|
|
19
|
-
`DomainPurchaseModel` lists only `graph=basic`: id, domain, status, price, and currency. Export/download and alternate graphs are rejected. Clicking a row performs an imperative default-graph read into a
|
|
33
|
+
`DomainPurchaseModel` lists only `graph=basic`: id, domain, status, price, and currency. Export/download and alternate graphs are rejected. Clicking a row performs an imperative default-graph read into a focused modal
|
|
34
|
+
with an exact-ID Refresh action; the detail response and redacted error are modal-local and never Query-cached.
|
|
20
35
|
|
|
21
36
|
## Registrant PII
|
|
22
37
|
|
|
@@ -24,6 +39,16 @@ The contact page starts with no scope and no contact. Operators explicitly choos
|
|
|
24
39
|
|
|
25
40
|
An inherited group response renders a blank form and only the backend-authored boolean status; inherited values are never disclosed. Normal editable values may seed from that scope's database row or the House settings-file response. `Fax` and opaque `ExtraParams` are preserved only when the original response is a direct database row for the exact same scope. They are never carried across scope, inheritance, or deployment-file override boundaries.
|
|
26
41
|
|
|
42
|
+
## Scoped purchase contact setup
|
|
43
|
+
|
|
44
|
+
A missing-contact search offers **Set up contact** without discarding the explicit
|
|
45
|
+
purchasing group or intended name. `RegistrantContactPage` accepts optional
|
|
46
|
+
`initialGroup`, `lockScope`, `onSaved`, `onClose`, and `onBusyChange` for that
|
|
47
|
+
focused native dialog. Reads are explicit manager-only, fenced by scope/session,
|
|
48
|
+
and never Query-cached. Contact/Address tabs keep one draft; save returns to the
|
|
49
|
+
same purchase search and rechecks scoped capabilities. There is no active-group
|
|
50
|
+
fallback. The standalone page retains its explicit scope chooser.
|
|
51
|
+
|
|
27
52
|
## House adoption
|
|
28
53
|
|
|
29
54
|
House adoption is a separate, literal-superuser-only menu option. The operator types one exact existing name and submits `/registrar/adopt` without `group`. The UI never calls `/registrar/discover`, auto-discovers account domains, or silently assigns the adopted asset to a tenant.
|
package/docs/admin-dns.md
CHANGED
|
@@ -89,9 +89,8 @@ rotation remains write-only and imperative. Built-in credential Delete is absent
|
|
|
89
89
|
|
|
90
90
|
## Shared wildcard validation
|
|
91
91
|
|
|
92
|
-
Domains offers **
|
|
93
|
-
managers even when server setup needs an operator. Domain
|
|
94
|
-
read-only **Advanced validation**; its context action and Certificate Delegation compose
|
|
92
|
+
Domains offers **More domain actions → Shared validation setup** to global DNS
|
|
93
|
+
managers even when server setup needs an operator. Domain Overview offers the focused **Shared validation** action; Certificate Delegation composes
|
|
95
94
|
the same focused flow. See [the human walkthrough and exact
|
|
96
95
|
contracts](admin-dns-certificates.md#shared-validation-setup).
|
|
97
96
|
|
package/docs/admin-modals.md
CHANGED
|
@@ -25,6 +25,27 @@ omit deployment controls, and attempt viewers get a direct overview when there
|
|
|
25
25
|
is no admitted node evidence. Workflow, integration, setup identity and profile
|
|
26
26
|
editing use focused child dialogs. No shared DetailView default is changed.
|
|
27
27
|
|
|
28
|
+
Domains uses a compact native task dialog with Overview, applicable DNS records,
|
|
29
|
+
Certificates and manager Registration. Focused assignment, credential, registrar,
|
|
30
|
+
contact and purchase-recovery dialogs guard their own pending operations. Read-only
|
|
31
|
+
contact roles use tabs only when the returned contacts differ. This is scoped to
|
|
32
|
+
Domains; the shared DetailView and Admin Dashboard remain unchanged.
|
|
33
|
+
|
|
34
|
+
Assistant Setup separates AI Assistant and MCP connections as page tabs. Model,
|
|
35
|
+
API keys, Safety & usage and Incident triage use focused page-owned native
|
|
36
|
+
dialogs, with a nested confirmation for consequential actions. The page's live
|
|
37
|
+
owner/session scope owns both layers, the single-flight operation lock guards
|
|
38
|
+
native dismissal, and closing a task clears its unsaved secret drafts. MCP
|
|
39
|
+
connection controls remain directly on their own page tab and do not depend on
|
|
40
|
+
AI readiness. This changes neither shared DetailView nor Admin Dashboard.
|
|
41
|
+
|
|
42
|
+
Assistant Conversations, Skills and Memory use focused page-owned native dialogs.
|
|
43
|
+
History and skill details have no rail or gratuitous tabs. Memory's audience tabs
|
|
44
|
+
remain on its page, with Add/Edit and named removal confirmation dialogs. The live
|
|
45
|
+
person/session/permission and selected memory audience own their lifetime; pending
|
|
46
|
+
writes block native dismissal. Foreign history has no composer and names the
|
|
47
|
+
actual owner. This changes neither shared DetailView nor Admin Dashboard.
|
|
48
|
+
|
|
28
49
|
Use `modal-pad`, `modal-title`, `modal-message`, grouped fields and `modal-actions`.
|
|
29
50
|
Cancel precedes the primary button. Pending operations disable repeat submission;
|
|
30
51
|
`canDismiss` also protects native Escape/backdrop dismissal during a write.
|
package/docs/menus-and-access.md
CHANGED
|
@@ -62,6 +62,30 @@ share one `/system` landing instead of emitting duplicate index routes.
|
|
|
62
62
|
Generated Admin destinations are exact matched, so an embedded mount’s root
|
|
63
63
|
Dashboard cannot remain selected on every nested page.
|
|
64
64
|
|
|
65
|
+
### Everyday Admin navigation
|
|
66
|
+
|
|
67
|
+
The built-in menu keeps the existing top-level Dashboard. Operations contains
|
|
68
|
+
Fleet, Maintenance, Deployments, Jobs Dashboard and Scheduled Tasks. Scheduled
|
|
69
|
+
Tasks stays primary because its audience can lack access to Jobs Dashboard. Domains & Hosting contains
|
|
70
|
+
Domains, Certificates and Serving. Storage has its own group. Configuration
|
|
71
|
+
contains GeoLocation and Settings. Purpose-specific dashboards are welcome;
|
|
72
|
+
their labels identify the task and their content should not duplicate Admin
|
|
73
|
+
Dashboard. These menu changes do not change Dashboard content.
|
|
74
|
+
|
|
75
|
+
`AdminRoute.searchOnly` is presentation metadata, carried to `MenuItem.searchOnly`.
|
|
76
|
+
Accordion menus show these secondary tools in navigation search and while that
|
|
77
|
+
exact destination is active. Static menus keep all entries. Permission filtering
|
|
78
|
+
happens before search; section/route clauses and literal-superuser gates still
|
|
79
|
+
apply. All routes remain registered, including embedded mounts and direct links.
|
|
80
|
+
|
|
81
|
+
Fleet exposes Configuration beside its capacity controls. Domains exposes
|
|
82
|
+
Purchases and a More menu for DNS records, provider credentials and registrant
|
|
83
|
+
contact; individual domain records retain their focused tasks. Serving links to
|
|
84
|
+
Upstreams. Jobs Dashboard links to Jobs, Runners and Scheduled tasks, each with
|
|
85
|
+
its own permission check. These contextual links are omitted when their target
|
|
86
|
+
is absent from an embedded Admin mount. Secondary tools remain searchable by
|
|
87
|
+
their full names without filling the ordinary sidebar.
|
|
88
|
+
|
|
65
89
|
Import the route/menu machinery from `portal-mojo/admin/core`, then import only
|
|
66
90
|
the registry domains the shell installs. The stable domains are `identity`,
|
|
67
91
|
`security`, `observability`, `operations`, `infrastructure`, `communications`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-mojo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "React portal toolkit for django-mojo — typed client + params store, mission-control UI, dependency-free SVG charts, admin section bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { FileReference, RecordFeedItem } from '../../client/record-feed';
|
|
3
|
-
import { AttachmentQueue,
|
|
3
|
+
import { AttachmentQueue, type AttachmentQueueState } from '../../ui/AttachmentQueue';
|
|
4
|
+
import { RecordFeed } from '../../ui/RecordFeed';
|
|
5
|
+
import { filesize } from '../../ui/format';
|
|
6
|
+
import { modal } from '../../ui/modal';
|
|
4
7
|
import { isSafeCapabilityUrl, openCapabilityUrl } from '../storage/models';
|
|
5
8
|
import type { AssistantBlock, AssistantMessage, AssistantUser } from './types';
|
|
6
9
|
|
|
@@ -9,12 +12,12 @@ function BlockView({ block, onAction, disabled }: { block: AssistantBlock; onAct
|
|
|
9
12
|
if (block.type === 'chart') return <section className="assistant-block"><h4>{block.title ?? 'Chart data'}</h4><div className="assistant-chart" role="img" aria-label={`${block.chart_type} chart: ${block.title ?? 'Assistant chart'}`}>{block.labels.map((label, index) => <div key={`${label}:${index}`}><strong>{label}</strong>{block.series.map((series) => <span key={series.name}>{series.name}: {series.values[index]}</span>)}</div>)}</div></section>;
|
|
10
13
|
if (block.type === 'stat' || block.type === 'list') return <section className={`assistant-block assistant-${block.type}`}><h4>{block.title ?? (block.type === 'stat' ? 'Summary' : 'Details')}</h4><dl>{block.items.map((item) => <div key={item.label}><dt>{item.label}</dt><dd>{String(item.value ?? '—')}</dd></div>)}</dl></section>;
|
|
11
14
|
if (block.type === 'alert') return <section className={`assistant-block assistant-alert assistant-alert-${block.level}`} role={block.level === 'error' ? 'alert' : undefined}><strong>{block.title ?? block.level}</strong><p>{block.message}</p></section>;
|
|
12
|
-
if (block.type === 'file') return <section className="assistant-block assistant-file"><i className="bi bi-file-earmark-arrow-down" aria-hidden="true" /><span><strong>{block.filename}</strong><small>{[block.format, block.size != null ?
|
|
15
|
+
if (block.type === 'file') return <section className="assistant-block assistant-file"><i className="bi bi-file-earmark-arrow-down" aria-hidden="true" /><span><strong>{block.filename}</strong><small>{[block.format, block.size != null ? filesize(block.size) : null, block.row_count != null ? `${block.row_count} rows` : null, block.expires_in].filter(Boolean).join(' · ')}</small></span>{isSafeCapabilityUrl(block.url) ? <button type="button" className="btn btn-compact" onClick={() => openCapabilityUrl(block.url, true)}>Download</button> : <span className="storage-unsafe">Unsafe URL withheld</span>}</section>;
|
|
13
16
|
if (block.type === 'attachment') return <section className="assistant-block assistant-attachments" aria-label="Message attachments">{block.files.map((file) => <span className="assistant-attachment-chip" key={file.id}><i className="bi bi-paperclip" aria-hidden="true" /> <strong>{file.filename}</strong><small>{file.content_type}</small></span>)}</section>;
|
|
14
17
|
if (block.type === 'context') return <section className="assistant-block"><h4>References</h4><ul>{block.references.map((reference) => <li key={`${reference.model}:${reference.pk}`}><code>{reference.model} #{reference.pk}</code>{reference.label ? ` — ${reference.label}` : ''}</li>)}</ul></section>;
|
|
15
18
|
return <section className="assistant-block assistant-action"><h4>{block.title ?? 'Confirm action'}</h4>{block.description && <p>{block.description}</p>}<div className="assistant-action-row">{block.actions.map((action) => <button key={`${block.action_id ?? block.title}:${action.value}`} type="button" className="btn btn-compact" disabled={disabled} onClick={async () => { const confirmed = await modal.confirm({ title: `${action.label}?`, message: `Send “${action.value}” to the Assistant as your response?`, confirmText: action.label }); if (confirmed) await onAction(action.value); }}>{action.label}</button>)}</div></section>;
|
|
16
19
|
}
|
|
17
|
-
export function AssistantFeed({ messages, user, onSend, responding, disabled = false, attachmentKey = 'new' }: { messages: AssistantMessage[]; user: AssistantUser; onSend(text: string, attachments?: readonly FileReference[]): Promise<void>; responding: boolean; disabled?: boolean; attachmentKey?: number | string }) {
|
|
20
|
+
export function AssistantFeed({ messages, user, onSend, responding, disabled = false, attachmentKey = 'new', viewerId = user.id, readOnly = false }: { messages: AssistantMessage[]; user: AssistantUser; onSend(text: string, attachments?: readonly FileReference[]): Promise<void>; responding: boolean; disabled?: boolean; attachmentKey?: number | string; viewerId?: number; readOnly?: boolean }) {
|
|
18
21
|
const actionFlight = useRef(false);
|
|
19
22
|
const [actionError, setActionError] = useState('');
|
|
20
23
|
const [attachmentState, setAttachmentState] = useState<AttachmentQueueState>({ files: [], busy: false });
|
|
@@ -25,5 +28,5 @@ export function AssistantFeed({ messages, user, onSend, responding, disabled = f
|
|
|
25
28
|
const sendAction = async (value: string) => { if (actionFlight.current || responding || disabled) return; actionFlight.current = true; setActionError(''); try { await onSend(value, []); } catch (error) { setActionError(error instanceof Error ? error.message : 'Action response failed'); } finally { actionFlight.current = false; } };
|
|
26
29
|
const send = async (text: string) => { await onSend(text, attachmentState.files); setAttachmentReset((value) => value + 1); };
|
|
27
30
|
const composerAddon = disabled ? null : <AttachmentQueue key={`assistant:${user.id}:${attachmentKey}:personal`} consumerKey={`assistant:${user.id}:${attachmentKey}:personal`} capacity={5} destination={{}} expectedGroupId={null} disabled={responding} resetKey={attachmentReset} onChange={attachmentChange} />;
|
|
28
|
-
return <RecordFeed items={items} onSend={send} isSending={responding} disabled={disabled} submitBlocked={attachmentState.busy} composerAddon={composerAddon} currentUserId={
|
|
31
|
+
return <RecordFeed items={items} onSend={send} isSending={responding} disabled={disabled} submitBlocked={attachmentState.busy} composerAddon={composerAddon} currentUserId={viewerId} showInput={!readOnly} variant="bubbles" placeholder={disabled ? 'Only the owner can continue this conversation' : 'Message Assistant…'} sendLabel="Send" emptyLabel="Ask the Assistant about this deployment." ariaLabel="Assistant conversation" pending={responding ? <>Responding…</> : null} error={actionError || undefined} renderAddon={(item) => { const blocks = Array.isArray(item.raw.blocks) ? item.raw.blocks as AssistantBlock[] : []; return blocks.length ? <div className="assistant-blocks">{blocks.map((entry, index) => <BlockView key={`${item.id}:${index}`} block={entry} onAction={sendAction} disabled={responding || disabled || actionFlight.current} />)}</div> : null; }} />;
|
|
29
32
|
}
|