portal-mojo 0.2.2 → 0.2.3

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 (70) hide show
  1. package/docs/DetailView.md +19 -15
  2. package/docs/README.md +1 -0
  3. package/docs/admin-assistant.md +3 -1
  4. package/docs/admin-credentials.md +7 -0
  5. package/docs/admin-dashboard.md +1 -1
  6. package/docs/admin-dns.md +8 -2
  7. package/docs/admin-incidents.md +26 -3
  8. package/docs/admin-jobs.md +7 -4
  9. package/docs/admin-messaging.md +6 -5
  10. package/docs/admin-modals.md +122 -0
  11. package/docs/admin-network-security.md +4 -4
  12. package/docs/admin-phonehub.md +13 -5
  13. package/docs/admin-push.md +7 -0
  14. package/docs/admin-rules.md +8 -0
  15. package/docs/admin-shortlinks.md +2 -1
  16. package/docs/admin-storage.md +2 -1
  17. package/docs/auth.md +28 -0
  18. package/docs/client.md +12 -0
  19. package/docs/feedback.md +10 -0
  20. package/docs/forms.md +10 -0
  21. package/docs/popover.md +15 -0
  22. package/package.json +1 -1
  23. package/src/admin/assistant/api.ts +1 -1
  24. package/src/admin/assistant/pages.tsx +71 -17
  25. package/src/admin/assistant/skill-lifecycle.ts +9 -0
  26. package/src/admin/core/index.ts +7 -2
  27. package/src/admin/credentials/group-api-keys.tsx +47 -43
  28. package/src/admin/credentials/webhook-subscriptions.tsx +28 -40
  29. package/src/admin/dashboard/AdminDashboardPage.tsx +1 -1
  30. package/src/admin/dns/CertificateLifecyclePoller.tsx +15 -3
  31. package/src/admin/dns/ProviderCredentialsPage.tsx +33 -49
  32. package/src/admin/dns/setup.ts +1 -1
  33. package/src/admin/domains/observability.ts +1 -1
  34. package/src/admin/identity/users/UserDetail.tsx +36 -20
  35. package/src/admin/identity/users/sections/OAuthSection.tsx +13 -6
  36. package/src/admin/identity/users/sections/actions.tsx +20 -9
  37. package/src/admin/incidents/EventDetail.tsx +147 -5
  38. package/src/admin/incidents/IncidentDetail.tsx +181 -8
  39. package/src/admin/incidents/models.ts +1 -1
  40. package/src/admin/jobs/RunnerDetail.tsx +5 -8
  41. package/src/admin/jobs/ScheduledTaskDetail.tsx +5 -71
  42. package/src/admin/jobs/index.ts +1 -1
  43. package/src/admin/jobs/purge-preview.ts +25 -0
  44. package/src/admin/jobs/sections/JobOperationsSection.tsx +46 -35
  45. package/src/admin/messaging/EmailDomainsPage.tsx +3 -3
  46. package/src/admin/messaging/EmailTemplatesPage.tsx +2 -4
  47. package/src/admin/messaging/MailboxesPage.tsx +3 -3
  48. package/src/admin/messaging/PublicMessagesPage.tsx +3 -5
  49. package/src/admin/messaging/push/PushPage.tsx +20 -8
  50. package/src/admin/monitoring/LogInspector.tsx +3 -19
  51. package/src/admin/network/IPSetDetail.tsx +32 -63
  52. package/src/admin/phonehub/PhoneHubPage.tsx +274 -13
  53. package/src/admin/phonehub/SmsComposer.tsx +97 -0
  54. package/src/admin/phonehub/api.ts +6 -0
  55. package/src/admin/rules/RuleSetDetailPage.tsx +31 -7
  56. package/src/admin/rules/condition-removal.ts +7 -0
  57. package/src/admin/security/geoip/models.ts +5 -0
  58. package/src/admin/security/tickets.tsx +40 -17
  59. package/src/admin/shortlinks/ShortlinkDetail.tsx +18 -12
  60. package/src/admin/storage/BucketDetail.tsx +28 -9
  61. package/src/admin/storage/FileManagerDetail.tsx +23 -5
  62. package/src/admin/storage/FileView.tsx +28 -16
  63. package/src/admin/storage/FilesPage.tsx +4 -5
  64. package/src/client/auth.ts +10 -1
  65. package/src/client/client.ts +14 -6
  66. package/src/client/mock.ts +89 -5
  67. package/src/client/upload.ts +1 -0
  68. package/src/ui/DetailView.tsx +22 -5
  69. package/src/ui/Popover.tsx +13 -12
  70. package/src/ui/modal.tsx +8 -4
@@ -37,7 +37,7 @@ of the toolkit it must render inside the app's `QueryClientProvider`
37
37
  contextMenu={[
38
38
  { label: 'Resend invite', icon: 'bi-envelope-paper', when: (u) => u?.last_login == null, onSelect: ... },
39
39
  { divider: true },
40
- { label: 'Delete', icon: 'bi-trash', danger: true, permissions: ['manage_users', 'users'], onSelect: ... },
40
+ { label: 'Deactivate', icon: 'bi-pause-circle', permissions: ['manage_users', 'users'], disabled: pending, onSelect: ... },
41
41
  ]}
42
42
  sections={[
43
43
  { key: 'profile', label: 'Profile', icon: 'bi-person', render: () => <>…</> },
@@ -64,16 +64,16 @@ trap — those belong to the host dialog). The affordance follows the callback:
64
64
  - **Pass a history-back handler** when a routed record page should keep the
65
65
  ✕ as "back" (`onClose={() => navigate(-1)}` — linkable record routes read
66
66
  well this way).
67
- - **Modal hosts MUST pass it.** Inside `modal.detail` this ✕ is the modal's
68
- only visible dismiss — the envelope has no header X of its own, only
69
- Escape/backdrop — and TypeScript no longer enforces the prop. Omitting it
70
- in a modal yields an Escape-only dialog.
67
+ - **Modal hosts pass it** for the normal identity-header close. The flush
68
+ envelope supplies a fallback close during loading/error states without a
69
+ DetailView header; Escape/backdrop follow the live `canDismiss` gate.
71
70
 
72
- Sizing is the consuming app's concern: `.detail` carries
73
- `min-height`/`max-height: 82vh` in the app `theme.css` (base at
74
- `apps/portal/src/theme.css`) — page hosts relax it there. This is distinct
75
- from the C1 chrome (`.rail-badge`, `.dv-menu*`, `.dv-keep`) that lives in
76
- `theme/detailview.css` (see Pitfalls).
71
+ Sizing is the consuming app's concern. Only `.mojo-modal-flush > .detail`
72
+ receives a viewport-height limit; page/embedded bodies have no modal minimum
73
+ or maximum. At narrow container widths an in-flow section button expands the
74
+ same filtered rail entries, including rich, zero and dot badges. No native
75
+ select stringification or second top-layer overlay is involved. The chrome
76
+ lives in `theme/detailview.css` in both consuming apps.
77
77
 
78
78
  ## Permission-gated sections (fail-closed)
79
79
 
@@ -110,7 +110,7 @@ follows. There is no imperative handle.
110
110
 
111
111
  ## Header kebab menu
112
112
 
113
- `contextMenu?: Array<{ label, icon?, permissions?, when?, onSelect, danger? } | { divider: true }>`
113
+ `contextMenu?: Array<{ label, icon?, permissions?, when?, onSelect, danger?, disabled? } | { divider: true }>`
114
114
  renders a three-dot button in the header gutter (before the close X, when one renders) that
115
115
  opens a `Popover` menu (`placement: bottom-end`, top-layer — works inside
116
116
  `modal.detail`). Filtering is `ContextMenu.visibleItems` parity:
@@ -172,10 +172,12 @@ permission behavior, not misconfig.
172
172
  | `FlatRow` | label + value row; `action` adds the edit pencil (`actionIcon` to override, e.g. `bi-plus-lg` for empty values) |
173
173
  | `SecurityItem` | icon + title + description row with a trailing slot (badge/button) |
174
174
 
175
- Conventions from the Users proof: pencil actions open `formModal` with
176
- current values as `initial`; a danger action lives in a `.danger-zone`
177
- block at the bottom of a section; fields the wire doesn't carry for other
178
- users (me-graph only: `has_passkey`, `requires_mfa`) don't get rows.
175
+ Pencil actions open `formModal` with current values as `initial`. Current-record
176
+ operations live in the header menu or focused dialogs; content rails never
177
+ contain danger/action panes. Child-specific actions remain contextual. Fields
178
+ the wire does not carry for other users (`has_passkey`, `requires_mfa`) do not
179
+ get rows. See [Admin modal policy](admin-modals.md) for lifecycle exceptions.
180
+ `active.disabled` and menu-entry `disabled` block repeat submission while pending.
179
181
 
180
182
  ## Pitfalls
181
183
 
@@ -192,3 +194,5 @@ users (me-graph only: `has_passkey`, `requires_mfa`) don't get rows.
192
194
  - The styles for the C1 chrome live in `apps/portal/src/theme/detailview.css`
193
195
  (`.rail-badge`, `.rail-dot`, `.dv-menu*`, `.dv-keep`) — tokens only,
194
196
  both themes.
197
+
198
+ Keyboard selection in the compact section rail returns focus to the visible section toggle before the rail collapses. The desktop rail retains ordinary button focus.
package/docs/README.md CHANGED
@@ -41,6 +41,7 @@ everything: run the showcase (`npm run dev:showcase`) → **Develop → Componen
41
41
  | [ModelTable.md](ModelTable.md) | The server-driven table: columns, filters, selection/batch, chooser, persist, autoRefresh, expand, groupBy, export, skeleton |
42
42
  | [forms.md](forms.md) | The `Field` language, `SchemaForm`, `formModal`, `FormView` inline autosave + showWhen + tabsets |
43
43
  | [tabs-and-form-wizard.md](tabs-and-form-wizard.md) | Accessible Tabs variants and shared-state FormWizard, async finish, reset/roster semantics, modal helper |
44
+ | [admin-modals.md](admin-modals.md) | Admin modal inventory, presentation, resource lifecycle and retained exception policy |
44
45
  | [DetailView.md](DetailView.md) | The UserView-style detail surface + row/section primitives |
45
46
  | [feedback.md](feedback.md) | Awaitable `modal.*`, `toast.*` |
46
47
  | [idioms.md](idioms.md) | ArmedButton (two-step confirm), `undoToast` (grace-period undo), `progressToast` — the dangerous-action trio |
@@ -45,7 +45,9 @@ Import from `portal-mojo/admin`.
45
45
  the outcome is marked unknown, the list is refreshed, and the send is never
46
46
  retried automatically.
47
47
  - `POST /api/assistant/context` sends only `{model, pk}`. The returned conversation id is immediately fetched from `/api/assistant/conversation/<id>?graph=detail`; the client never synthesizes or reposts context text.
48
- - Conversation and Skill lists/details/deletes are imperative and component-local. They do not use `defineModel`, Query cache, `ModelTable`, a `RecordFeed` adapter, persistence, or exports.
48
+ - Conversation and Skill lists/details are imperative and component-local.
49
+ Built-in Delete controls are absent. Skill deactivation/reactivation requires
50
+ `sys.view_admin` and reloads authoritative state; conversations retain history. They do not use `defineModel`, Query cache, `ModelTable`, a `RecordFeed` adapter, persistence, or exports.
49
51
  - A foreign conversation visible to an administrator is inspect-only. Only `conversation.user.id === me.id` enables continuation.
50
52
 
51
53
  `AssistantFeed` uses controlled `RecordFeed` plus the shared attachment queue at
@@ -187,3 +187,10 @@ the `group-admin.css` semantic rules in its theme entry, as the repository's
187
187
  The personal `/api/account/api_keys` page and `/api/auth/generate_api_key` flow
188
188
  are intentionally unchanged. This package domain owns only group-scoped
189
189
  `/api/group/apikey` credentials and `/api/group/webhook_subscriptions`.
190
+
191
+ ## Modal lifecycle convention
192
+
193
+ See [Admin modal policy](admin-modals.md) for header actions, content-only
194
+ sections, supported lifecycle fields and retained irreversible exceptions.
195
+ Group API keys and webhook subscriptions deactivate/reactivate; their built-in
196
+ Delete controls are removed. Reveal, secret rotation and cache boundaries remain.
@@ -6,7 +6,7 @@ The page uses only authoritative django-mojo signals: time series `user_activity
6
6
 
7
7
  Each panel owns an independent permission gate. A caller without metrics access can still see authorized incident, job, email, or login-geography panels, and no denied panel issues a request. Every dashboard query cache key includes the authenticated caller UID. Login geography starts at the previous 30-day date and deliberately omits `dr_end`, matching the backend's instant-bound semantics.
8
8
 
9
- The dashboard section contributes the single Overview → Dashboard menu entry. If the caller has none of the aggregate dashboard grants, the root route redirects to their first visible Admin route; if no Admin route is visible it renders Access denied.
9
+ The dashboard section contributes a direct, top-level Dashboard menu entry instead of wrapping it in an Overview category. If the caller has none of the aggregate dashboard grants, the root route redirects to their first visible Admin route; if no Admin route is visible it renders Access denied.
10
10
 
11
11
  Attention links preserve the exact server filter in the URL:
12
12
 
package/docs/admin-dns.md CHANGED
@@ -69,8 +69,7 @@ Verification is transactional at the UI boundary: a failed first link adds
69
69
  nothing; a failed rotation rejects, preserves the previous masks/encrypted
70
70
  pair, and refetches the now-unverified row with its provider error. Successful
71
71
  verification replaces the masks and marks the row verified. Viewer-only users
72
- can inspect safe rows; managers can link, rotate, activate/retire, and arm
73
- deletion.
72
+ can inspect safe rows; managers can link, rotate and activate/retire; built-in deletion is absent.
74
73
 
75
74
  ## Themes and showcase
76
75
 
@@ -78,3 +77,10 @@ The page composes existing portal primitives and semantic tokens; it adds no
78
77
  fixed colors or theme-specific stylesheet. The executable three-leg showcase
79
78
  is **Develop → Components → Admin → DNS**: live manager lifecycle,
80
79
  viewer-only contract, and malformed-capability unavailable state.
80
+
81
+ ## Modal lifecycle convention
82
+
83
+ See [Admin modal policy](admin-modals.md) for header actions, content-only
84
+ sections, supported lifecycle fields and retained irreversible exceptions.
85
+ Provider credentials retire/reactivate through `is_active`, keeping history;
86
+ rotation remains write-only and imperative. Built-in credential Delete is absent.
@@ -37,9 +37,16 @@ Rows, history, optimistic notes, mutation variables, curated copy surfaces, and
37
37
  exports pass through a recursive non-mutating sanitizer. It redacts normalized
38
38
  secret keys, auth/header blocks, JWT/Bearer values, sensitive URL/form params,
39
39
  and parsed bodies; evidence/traces are explicitly bounded. Heuristic redaction
40
- cannot guarantee arbitrary opaque prose, so raw JSON is not rendered or
41
- exported. `createSafeExporter` sanitizes before accumulation and projects only
42
- declared fields.
40
+ cannot guarantee arbitrary opaque prose, so exports remain sanitized and
41
+ project only declared fields through `createSafeExporter`.
42
+
43
+ Incident and Event detail have one intentional operator-facing exception:
44
+ **Raw data** shows the exact record returned by Django's authenticated detailed
45
+ endpoint. That includes every top-level field, complete metadata, and expanded
46
+ relations such as an Incident's `ip_info`. Each uses a separate TanStack Query
47
+ cache key so the raw record cannot replace sanitized rows used by tables,
48
+ drill-ins, exports, activity, or formatted sections. Django remains the
49
+ authorization boundary for what those endpoints return.
43
50
 
44
51
  Before that recursive sanitizer runs, every history `media` value is positively
45
52
  rebuilt to exactly `id`, `filename`, `content_type`, and `category`; URLs,
@@ -53,6 +60,22 @@ the child reveals the parent. Ticket/rule/AI/network-response/dashboard controls
53
60
  remain excluded. Bouncer discovery stays category-prefix plus MUID search
54
61
  because its reporter writes MUID into incident details.
55
62
 
63
+ Incidents with a source address add a formatted **Source** section. It promotes
64
+ the detailed graph's `ip_info` into location/network, risk and reputation,
65
+ effective block/whitelist state, checked-firewall reconciliation, observation
66
+ times, and incident connection context. The address and action open the shared
67
+ GeoIP dossier from the existing cache table (never a billed provider lookup).
68
+ Missing enrichment stays visibly unknown; the exact `ip_info` object remains in
69
+ Raw data.
70
+
71
+ MojoSec events add a dedicated formatted section over the backend's fixed,
72
+ server-derived `metadata.mojosec` projection. It promotes the detection and
73
+ sensor identity, timing, policy revision, recommendation, and every currently
74
+ validated evidence field—including sudo command provenance and receipt
75
+ semantics. The separate Raw data section preserves the complete response for
76
+ operator investigation. The top-level `geo_ip` display string is shown in
77
+ Source when Django supplies it.
78
+
56
79
  ## Backend evidence
57
80
 
58
81
  The contract was checked against django-mojo source rather than inferred from
@@ -309,11 +309,14 @@ Two more corrections found while building, refining 6 and the gate table:
309
309
  blanket success.
310
310
  - **Purge is dry-run first.** The dialog runs `dry_run:true` and reports
311
311
  "N jobs would be deleted before `<cutoff>`"; only the armed confirm performs
312
- the real run, which is read from `deleted`. Fixes the source's wrong
313
- `data.count` read.
312
+ the real run, which is read from `deleted`. Preview parameters and generation
313
+ are captured; changing filters or replacing a preview resets arming. The
314
+ preview is an estimate: execution recalculates cutoff and includes cascaded
315
+ event/log rows. Inputs/dismissal lock during execution; failure requires a new
316
+ preview; actual cutoff and counts remain visible.
314
317
  - **Armed confirmations** for purge, clear-queue, clear-stuck,
315
- cleanup-consumers, shutdown, broadcast `shutdown`, force-scheduler-lead and
316
- task delete. `clear-queue` sends `confirm:"yes"` **only after** arming: the
318
+ cleanup-consumers, shutdown, broadcast `shutdown`, force-scheduler-lead. Scheduled-task Delete is absent; enabled state
319
+ preserves task-result history. `clear-queue` sends `confirm:"yes"` **only after** arming: the
317
320
  token is a server-side safety gate, and pre-satisfying it (as web-mojo did on
318
321
  every call) removes the safety rather than honouring it.
319
322
  - **KISS modals.** Job, runner, scheduled-task and task-result inspection are
@@ -25,9 +25,10 @@ Domain credentials are write-only. Secret-bearing saves, onboarding, audit,
25
25
  reconcile, and send are imperative and never use MutationCache. Audit is POST,
26
26
  has no automatic retry, and always refreshes because it persists readiness.
27
27
  Create reports only that the row was created: django-mojo's best-effort
28
- create-time audit/reconcile can fail after the row exists. Delete warns that
29
- local mailboxes and sent audit history disappear while SES/SNS/S3/DNS
30
- resources may remain.
28
+ create-time audit/reconcile can fail after the row exists. Built-in domain,
29
+ mailbox, template and contact deletion is absent so local audit history stays
30
+ available. No archive field is invented. Mailbox inbound/outbound flags remain
31
+ independent; support status changes use their existing status permission.
31
32
 
32
33
  Onboarding composes the shared `FormWizard`. Managed mode is available only
33
34
  when the optional DNS Admin integration resolves the normalized domain and
@@ -46,8 +47,8 @@ Stable mock identities (password `mojo`):
46
47
 
47
48
  - `email.operator@nativemojo.com` — email only (`comms`).
48
49
  - `support.viewer@nativemojo.com` — contact read only.
49
- - `support.manager@nativemojo.com` — contact read/update/delete.
50
- - `support.manage-only@nativemojo.com` — delete authority but intentionally no page view.
50
+ - `support.manager@nativemojo.com` — contact read/status update; the low-level delete gate is not a UI action.
51
+ - `support.manage-only@nativemojo.com` — low-level delete authority but intentionally no page view or status-write grant.
51
52
  - `showcase.operator@nativemojo.com` — executable showcase coverage.
52
53
 
53
54
  Live verification is read-only by default. Never create/edit/delete a domain,
@@ -0,0 +1,122 @@
1
+ # Admin modal presentation and lifecycle
2
+
3
+ Admin inspection uses `modal.detail` and `DetailView`: identity, useful subtitle and
4
+ chips, supported lifecycle state, a contextual header menu, and close. Navigation
5
+ contains record content. Editors and operations use focused `modal.open` or
6
+ `formModal` dialogs. Tables remain pages and retain their URL/query state.
7
+
8
+ A current-record action belongs in the header menu or a focused dialog. Do not
9
+ create Delete, Danger, Manage, Control, Operations or Actions navigation sections,
10
+ or rename an action pane to disguise it. Genuine autosave fields remain content.
11
+ A nested child may have its own contextual actions. Evidence such as a firewall
12
+ log's Action **column** describes the event and is not an operation section.
13
+
14
+ Use `modal-pad`, `modal-title`, `modal-message`, grouped fields and `modal-actions`.
15
+ Cancel precedes the primary button. Pending operations disable repeat submission;
16
+ `canDismiss` also protects native Escape/backdrop dismissal during a write.
17
+ Cancellation preserves data. Errors retain authoritative state and surface a
18
+ refusal. Fresh authentication, write-only secrets and resource-specific imperative
19
+ save/reconciliation helpers stay in force. Closing a view never cancels a server
20
+ operation that has already started.
21
+
22
+ The content rail becomes a collapsible in-flow button list at narrow **container**
23
+ widths. Both presentations use the same permission-filtered sections, active key,
24
+ rich ReactNode badges (including zero and dots), dividers and keep-alive state.
25
+ Revoked sections unmount. Modal height limits use available dynamic viewport
26
+ height; embedded/page hosts have no modal minimum or maximum and may omit
27
+ `onClose`. A flush modal has a fallback close control while its body loads or
28
+ fails; successful DetailView bodies supply the normal header close.
29
+
30
+ ## Executable inventory
31
+
32
+ `scripts/admin-modal-inventory.json` records file, owner, launcher/helper, body,
33
+ category, helper target, parent flow and expression fingerprint. The finite AST
34
+ scanner follows imported aliases, namespace/destructured/generic modal calls,
35
+ named forwarding helpers, literal dynamic imports and resolved section literals,
36
+ arrays, conditionals, spreads and explicit pushes. Unrelated table columns are
37
+ not section configurations. Assignment, unsupported mutation/property forwarding,
38
+ unknown computed members and unclassified modal references fail review.
39
+
40
+ `scripts/admin-modal-review.json` contains the reviewed semantics of generic
41
+ modal bodies and destructive exceptions. Editors are classified by their actual
42
+ body, not by whether the launcher happens to be `formModal`. Forwarders inherit
43
+ categories from their target; a flow with multiple categories is `nested-flow`.
44
+ Unknown syntax requires an exact expression/fingerprint mapping with a reason,
45
+ classification and runtime case; stale mappings fail. This is a finite syntax
46
+ contract, not a general JavaScript evaluator. When changing a surface, review the
47
+ body, permission/lifecycle behavior and matching runtime evidence before updating
48
+ the inventory using `node scripts/verify-admin-modals.mjs --write-inventory`.
49
+
50
+ ## Family map
51
+
52
+ | Family | Record bodies and focused flows | Lifecycle policy |
53
+ |---|---|---|
54
+ | Groups and members | Group, Member, nested parent/subgroup/user; admission/invite, disable reason, auth configuration/reset | Existing disable/reactivate and inherited configuration semantics |
55
+ | Users and credentials | User, group API key, webhook; profile/password/MFA/avatar/passkey/OAuth/personal keys, reveal/rate limits/rotation | Header lifecycle; deactivate group keys/webhooks; child credentials stay focused |
56
+ | Bouncer and device intelligence | Signal, device, signature, login/location/GeoIP dossiers; signature editor, lookup/threat/enforcement | Evidence retained; signature lifecycle and explicit enforcement remain |
57
+ | Security operations | Ticket, Incident, Event, RuleSet; feed/attachments, status/LLM/Assistant, conditions/handler editor | Header operations; properties remain content; RuleSet deactivation |
58
+ | Monitoring and CloudWatch | Log, resource/dashboard/metrics drills, chart/data/range/permission dialogs | Read-only evidence; explicit permission reset |
59
+ | Jobs | Job, runner, schedule, result; retry/cancel/payload/broadcast, schedule editor, queue maintenance | Schedule enabled state; runner controls focused; bounded purge only |
60
+ | Network/geofence | IPSet, blocked IP, GeoIP; expiry/editor/sync/source refresh, policy/reset/allowlist | IPSet disable/enable; no unsupported IPSet DELETE |
61
+ | DNS | Domain, certificate, credential, purchase; record set, credential rotation/link, revoke/onboard/contact | Retire credentials; certificate revoke; provider record-set removal only |
62
+ | Storage/shortlinks | Bucket, FileManager, File, shortlink; owner/backend/test/CORS/upload/rendition/share | FileManager/shortlink/share active state; no File disable promise; bucket empty only |
63
+ | Email/support | Domain, mailbox, sent message, template, contact; credentials/onboarding/audit/reconcile/test/template/status | Preserve evidence; separate mailbox directions and support status |
64
+ | Phone/Push | Number, SMS, config, device, delivery, template; lookup/test/config editors | Config/template lifecycle; number/SMS/device/delivery evidence retained |
65
+ | Assistant/settings | Conversation, Skill, Setting; feed continuation, scoped memory, typed settings editor | Owner-only continuation; Skill active selection; memory is scoped editing |
66
+
67
+ ## Resource lifecycle matrix
68
+
69
+ | Resource | Built-in Admin behavior |
70
+ |---|---|
71
+ | User / Group / Member | Existing deactivate/reactivate actions; preserve reason, fresh-auth and membership rules |
72
+ | Group API key / webhook | Toggle `is_active`; retain history; reveal and rotate remain credential-safe |
73
+ | RuleSet / schedule | Disable/reactivate; no parent/result cascade deletion. Disabling a schedule does not cancel already published jobs |
74
+ | IPSet | Existing enable/disable/sync/source refresh; server `CAN_DELETE=False` |
75
+ | DNS credential | Retire/reactivate and rotate with `sys.manage_dns OR sys.security` |
76
+ | FileManager | Existing active state and provider/owner operations via atomic imperative save |
77
+ | File | No File, filename-confirmation or batch Delete; no active toggle. Delivery URLs and file-backed shortlinks do not consistently enforce File active state |
78
+ | Shortlink / File share | Deactivate/reactivate using existing reconciler; preserve click history. Previously issued destinations cannot be recalled |
79
+ | Email domain / template | Retain rows; no delete or invented archive flag |
80
+ | Mailbox | Independent inbound/outbound flags; no delete or synthetic combined active flag |
81
+ | Support contact | Existing status transition using the status gate; delete permission does not grant status-write authority |
82
+ | Phone cache / SMS audit | Retain evidence; deleting a local row cannot recall a provider delivery |
83
+ | Phone config | Existing `is_active` through write-only-secret-safe save; disabling restores backend fallback. Test mode is not a disable switch |
84
+ | Push config / template | Existing active selection with separate manage/test grants |
85
+ | Assistant conversation | Retain history; no arbitrary REST save or invented archive |
86
+ | Assistant Skill | Existing `is_active` save requires `sys.view_admin`; authoritative reload after mutation |
87
+ | Other logs/devices/history | Read-only by default, even where low-level REST supports DELETE |
88
+
89
+ The public low-level exported delete APIs remain compatible. Built-in controls
90
+ are a narrower product contract. File delivery enforcement, email archival and
91
+ conversation archival require backend work before new lifecycle UI is added.
92
+
93
+ ## Retained irreversible operations
94
+
95
+ | Operation | Authority and confirmation contract |
96
+ |---|---|
97
+ | Rule condition removal | `sys.manage_security OR sys.security`; selected children of the exact parent, freshly read before confirmation. ALL removal broadens matching, ANY removal narrows it while conditions remain; last removal makes either mode catch-all. Warn when active. After partial refusal, refresh parent and children and report counts |
98
+ | OAuth unlink | Live owner or `sys.manage_users OR sys.users`, plus caller restriction; confirm provider and user. Removes association only; reconnect requires provider authorization. Preserve server lockout refusal |
99
+ | Passkey removal | Live owner or user-management grant; confirm friendly name, relying party and user. Disable remains reversible; removal requires reenrollment and never erases a physical device |
100
+ | Memory key removal | `sys.assistant`; selected group additionally requires inherited member `assistant` authority or superuser. Confirm tier/group/key, freeze scope, recheck authority, no optimistic deletion |
101
+ | DNS record-set removal | DNS manage gate, provider capability and record coordinator. Preview all values, apex/service impact and exact name/type; stale-state preflight and reconcile failures; no domain deletion |
102
+ | Empty bucket | `sys.manage_aws OR sys.files`, then fresh auth; header to focused armed confirmation and exact case-sensitive bucket name. Removes objects, versions, markers and multipart uploads; retains bucket. Display partial/unknown outcomes and always refresh |
103
+ | Jobs retention purge | `sys.manage_jobs OR sys.jobs`; dry-run estimate bound to normalized days/status plus generation. Editing/replacing preview resets arming. Ignore stale/unmounted responses; lock inputs/dismissal during execution. Report actual cutoff and cascade-inclusive counts; rejection requires another preview |
104
+ | Queue/consumer cleanup | Jobs manage grant, exact channel/scope and armed confirmation; queue clear sends `confirm:yes` only after consent and may cancel pending database jobs. Report partial outcomes |
105
+
106
+ Other non-row removals retain their scoped editor semantics: rate-limit override
107
+ clear, secret clear, inherited auth/geofence reset, metrics permission clear,
108
+ registrant clear, geofence exemption removal, unsaved handler entries, local
109
+ upload candidates and supported avatar/attachment detachment. Explain fallback
110
+ and inheritance rather than promising restoration of discarded values.
111
+
112
+ ## Verification
113
+
114
+ Run `verify:admin-modals`, `verify:admin-modal-scanner`,
115
+ `verify:admin-modal-lifecycle` and `verify:admin-modal-mounted`. The mounted Node
116
+ assertions use JSDOM and React StrictMode for asynchronous scope/effect behavior;
117
+ they do not establish native dialog stacking, focus or layout. Use actual mock
118
+ browser interactions for all family bodies, both themes, desktop/narrow,
119
+ 390px embedded hosts and short viewports, including nested dialogs, rich badges,
120
+ keyboard navigation and console checks. The lazy Showcase **Admin modal audit**
121
+ demo supplies long-title, rich/zero/dot badges, nested editor, lifecycle-cancel and
122
+ embedded-host cases. Existing ToastHost top-layer visibility work remains separate.
@@ -337,7 +337,7 @@ shows the server's own message.
337
337
 
338
338
  Anything that reaches the fleet is an `ArmedButton` whose armed label names the
339
339
  blast radius: the rules replace (with a plain-language clause diff), override
340
- removal, ipset enable / disable / sync / refresh / delete, allowlist removal and
340
+ removal, ipset enable / disable / sync / refresh, allowlist removal and
341
341
  whitelist removal. Operations that need input (a reason, an expiry) use
342
342
  `formModal` instead — an armed button cannot collect input.
343
343
 
@@ -345,9 +345,9 @@ whitelist removal. Operations that need input (a reason, an expiry) use
345
345
 
346
346
  Do not add these back.
347
347
 
348
- 1. **Batch delete of IP sets.** Multi-selecting kernel firewall sets and deleting
349
- them in one irreversible action is exactly the shape #1097 warned about.
350
- Delete is single-record and armed, from the detail modal.
348
+ 1. **IPSet deletion.** The backend has `CAN_DELETE=False`; both single-record
349
+ and batch Delete are absent. Header actions use the supported enabled state,
350
+ sync and source-refresh operations with focused confirmation.
351
351
  2. **Creating a block from the Blocked IPs table.** The backend supports it and
352
352
  web-mojo did not expose it either. Unblock and whitelist — both *relaxing* —
353
353
  are exposed.
@@ -7,12 +7,18 @@ Import `PhoneHubPage`, the three individual pages, models, permission clauses, a
7
7
  - `POST /api/phonehub/number/normalize` is public. `normalizePhoneNumber()` accepts the backend result only when it is strict E.164.
8
8
  - `POST /api/phonehub/number/lookup` requires authentication and is offered only under `PHONE_LOOKUP_UI_PERMISSIONS`. The helper always normalizes first. For a new or expired record it omits `force_refresh` so the backend's normal lookup path refreshes once. For a known-fresh record it requires confirmation, then sends `force_refresh: true`. A successful response must provide a first lookup timestamp/count or advance one of them relative to the prior row; a 2xx stale row is rejected as provider failure evidence.
9
9
  - `PhoneNumberModel` requests the default graph but positively sanitizes every row before Query. Raw `lookup_data` is never retained or exported.
10
- - `SmsModel` is an audit-only surface. It requests the default graph and positively projects known display fields before Query; raw metadata, provider payloads, and provider message ids are discarded. Its client exporter is bounded and omits message bodies, errors, user/group details, and raw fields. There is no send UI.
11
- - `PhoneConfigModel` reads sanitized scalar/default-graph rows. Saves and `test_connection` use imperative, cache-free calls. Secret inputs are DOM refs, not React or Query state: blank means untouched and is omitted; replacement must be non-empty; clear sends `null` only after a separate confirmation. Responses are allowlisted before cache reconciliation. This surface edits existing provider credentials; it does not provision Django user/group API keys.
10
+ - `SmsModel` reads SMS audit records. It requests the default graph and positively projects known display fields before Query; raw metadata, provider payloads, and provider message ids are discarded. Its client exporter is bounded and omits message bodies, errors, user/group details, and raw fields.
11
+ - The SMS table's **Send SMS** action opens a focused composer for one recipient and a custom message. It normalizes the recipient through the existing public endpoint, then calls `POST /api/phonehub/sms/send` with exactly `{to_number, body}`. The message body is preserved verbatim. This is global sending through the effective system configuration/default sender: no ambient group, provider override, metadata, or audit-creation call. Drafts and allowlisted receipts stay local to the dialog, outside MutationCache. Sending rechecks the original actor and current permissions after normalization and immediately before transport. A synchronous guard blocks repeat clicks; native Escape/backdrop and Cancel are locked while pending.
12
+ - A successful HTTP envelope can contain a `failed` or `undelivered` SMS record: render that status as failure. Other returned statuses remain literal; only `delivered` confirms delivery. The default graph does not expose `is_test`, so a `sent` receipt does not claim a real delivery. Transport/malformed-receipt failures retain the draft and ask the operator to check SMS Audit before retrying. Every attempted send invalidates the audit list; refresh failure cannot turn a completed send into a retry. Provider refusal is demonstrated in the mock with a recipient ending in `0000`; no mock request contacts a provider.
13
+ - `PhoneConfigModel` reads sanitized scalar/default-graph rows. Saves and `test_connection` use imperative, cache-free calls. The editor follows web-mojo's provider-conditional layout: selecting Twilio, AWS SNS, or Mojo Remote shows and submits only that provider's connection fields. Mojo Remote uses `https://api.mojoverify.com` when the saved URL is unset or the editor is switched to Mojo; an explicitly configured custom URL is preserved. Secret inputs are DOM refs, not React or Query state: blank means untouched and is omitted; replacement must be non-empty; clear sends `null` only after a separate confirmation. Switching providers resets pending clears, and hidden provider credentials remain untouched. Responses are allowlisted before cache reconciliation. This surface edits existing provider credentials; it does not provision Django user/group API keys.
12
14
 
13
15
  ## Exact permissions
14
16
 
15
- Every exported array is an ANY-of clause and retains the `sys.` UI namespace. Number view is `view_phone_numbers | manage_phone_numbers | comms | manage_users`; save is `manage_phone_numbers | comms | manage_users`; delete is only `manage_phone_numbers`. SMS view is `view_sms | manage_sms | comms | owner | manage_notifications`; save is `manage_sms | comms | manage_notifications`; delete is `manage_sms | manage_notifications`. Config view/save is `manage_phone_config | manage_groups | comms`; delete is `manage_phone_config | manage_groups`. The server remains authoritative.
17
+ Every exported array is an ANY-of clause and retains the `sys.` UI namespace. Number view is `view_phone_numbers | manage_phone_numbers | comms | manage_users`; save is `manage_phone_numbers | comms | manage_users`; delete is only `manage_phone_numbers`. SMS view is `view_sms | manage_sms | comms | owner | manage_notifications`; save is `manage_sms | comms | manage_notifications`; delete is `manage_sms | manage_notifications`. Config view/save is `manage_phone_config | manage_groups | comms`; delete is `manage_phone_config | manage_groups`. These delete clauses describe low-level API compatibility; built-in number,
18
+ SMS and config Delete controls are absent. The composer additionally requires
19
+ `sys.send_sms | sys.comms`; audit read/manage grants alone never allow sending.
20
+ It remains inside the existing SMS-view audience. Config deactivation uses the
21
+ credential-safe imperative save path. The server remains authoritative.
16
22
 
17
23
  Group choices are fetched only with a separate global group-directory clause and are capped at 100 basic rows. Without that grant the editor does not issue a group-directory request and preserves the existing scope (or creates a system default).
18
24
 
@@ -20,8 +26,10 @@ Group choices are fetched only with a separate global group-directory clause and
20
26
 
21
27
  Phone lookup is always the global Twilio Lookup integration; it is not group-configured. Stored Twilio and AWS credentials can be tested, but the current SMS send implementation only consults a per-group configuration for `provider="mojo"`. Twilio/AWS configurations otherwise fall through to global Twilio send settings, and AWS sending is not wired. `test_mode` only short-circuits connection testing; it does not block sending.
22
28
 
23
- An active group config wins; otherwise `PhoneConfig.get_for_group` falls back to the first active system default. Deleting or deactivating a group config restores that fallback. Deleting a group cascades its config and SMS rows. Deleting a user cascades their SMS rows. Deleting a config does not delete SMS audit rows. Deleting an SMS row affects only local audit storage and cannot recall provider delivery.
29
+ An active group config wins; otherwise `PhoneConfig.get_for_group` falls back to the first active system default. Deactivating a group config restores that fallback; Admin exposes this reversible lifecycle. Deleting a group cascades its config and SMS rows. Deleting a user cascades their SMS rows. Deleting a config does not delete SMS audit rows. Deleting an SMS row affects only local audit storage and cannot recall provider delivery.
24
30
 
25
31
  ## Reuse rules
26
32
 
27
- Use KISS detail modals, not `:id` routes. Never add provider payloads to a model graph, cache, error, export, or request history. Never reuse `PhoneConfigModel.useSave()` for credentials. Do not add SMS sending or API-key provisioning to this admin area without a new reviewed contract.
33
+ Use KISS detail modals, not `:id` routes. Never add provider payloads to a model graph, cache, error, export, or request history. Never reuse `PhoneConfigModel.useSave()` for credentials or `SmsModel.useSave()` to deliver messages. Sending follows the contract above; API-key provisioning remains separate.
34
+
35
+ Verify with `npm run verify:admin-sms-compose`, `npm run verify:admin-phonehub`, and the Admin modal inventory check. The mounted composer regression covers permissions, concurrent clicks, permission revocation, exact global payloads, uncertain results, failed receipts, cache isolation and audit refresh. Use the Phone Hub showcase to click the composer in both themes and at narrow widths.
@@ -34,3 +34,10 @@ FCM writes are imperative and cache-free. A service account textarea is an uncon
34
34
  Connection testing calls only `POST /api/account/devices/push/config/<id>/test` with an empty body. The backend uses its dummy token to validate credentials. The portal never accepts a device token. It also does not expose `/send`, caller `/test`, delivery retry, device registration/unregistration, or any delete action.
35
35
 
36
36
  All four REST models have live `CAN_DELETE = false`; the mock returns 403 without removing or cascading anything. Templates/configs can be created and edited under their save clauses. Devices and deliveries remain investigative here.
37
+
38
+ ## Modal lifecycle convention
39
+
40
+ See [Admin modal policy](admin-modals.md) for header actions, content-only
41
+ sections, supported lifecycle fields and retained irreversible exceptions.
42
+ Config/template active-state controls live in the detail header. Config saves
43
+ retain write-only credential handling, and test sends keep their separate gate.
@@ -85,3 +85,11 @@ The showcase demo is under **Admin → Rule Engine** and includes supported
85
85
  steps, notification target commas, duplicate/extra parameters, unsupported
86
86
  content, structural errors, move/remove actions, exact raw serialization, and
87
87
  the empty-chain case.
88
+
89
+ ## Modal lifecycle convention
90
+
91
+ See [Admin modal policy](admin-modals.md) for header actions, content-only
92
+ sections, supported lifecycle fields and retained irreversible exceptions.
93
+ RuleSet parents deactivate. Child removal confirms fresh parent/mode/selection;
94
+ ALL broadens, ANY narrows while nonempty, and the last removal is catch-all.
95
+ Partial deletion refreshes authoritative parent and condition state.
@@ -18,7 +18,8 @@ Neither table supports search or export, and list parameters are allowlisted.
18
18
 
19
19
  ## Mutations and short URLs
20
20
 
21
- Create, active-state changes, and delete are imperative one-shot requests. They
21
+ Create and active-state changes are imperative one-shot requests. Built-in
22
+ Admin deletion is absent; exported low-level delete helpers remain compatible. They
22
23
  do not claim fresh-auth protection the backend does not provide and do not use
23
24
  retrying mutation hooks. Every outcome runs an authoritative reconciliation in
24
25
  `finally` semantics. If reconciliation itself fails, the UI warns that the
@@ -65,7 +65,8 @@ FileManager DELETE and `check_public_access` are absent.
65
65
  `FileModel` forces the list graph and normal saves to `filename`, `is_public`,
66
66
  and explicit `group`. Group moves refetch and compare the authoritative FK.
67
67
  Selection supports public/private, explicit prepared group move, safe
68
- same-gesture download, and all-settled confirmed delete. Authorized operators
68
+ same-gesture download. File deletion is absent from detail and batch controls;
69
+ `is_active` is not presented as a safe revocation mechanism. Authorized operators
69
70
  also get **Add File** and a drag-only whole-page drop overlay. Both open one
70
71
  configuration modal; the bounded queue lives above it, so closing the modal
71
72
  does not cancel work or discard outcomes.
package/docs/auth.md CHANGED
@@ -30,6 +30,34 @@ onAuth('logout', () => queryClient.invalidateQueries()); // every cached answe
30
30
  - Events for `onAuth`: `'login' | 'logout' | 'refreshed' | 'refresh-failed'
31
31
  | 'unauthorized'`.
32
32
 
33
+ Both localStorage and sessionStorage keep the existing token keys. Refresh
34
+ preserves the chosen storage, including sessions with only a valid refresh token
35
+ at boot. Password login can replace malformed/expired credentials without the
36
+ old dead session blocking its own recovery request.
37
+
38
+ ## Packaged Django Admin
39
+
40
+ The generic auth client has no Admin side effects. The packaged app installs
41
+ its app-local source coordinator before `initAuth`, awaits hosted exchange and
42
+ source readiness before creating its router, and wraps all lazy loaders.
43
+ It uses the page's own API origin and hosted auth. Source grants renew at 70%
44
+ of their effective lifetime, independently of access-token refresh; focus and
45
+ suspended-tab recovery recheck the grant. Eager recovery preserves the route.
46
+
47
+ The app's `revokeAdminSourceSession()` first persists a nonsecret logout
48
+ tombstone, clears credentials, then waits for the shared exclusive Web Lock and
49
+ the completed `DELETE <admin>/_session` response. It must be awaited by app
50
+ sign-out callers; generic `logout()` retains its existing synchronous semantics
51
+ for ordinary toolkit consumers. All legacy/gate/Portal participants share
52
+ `mojo:admin-source-session:v1` (lock and channel) and
53
+ `mojo:admin-source-generation:v1` (authoritative localStorage record and per-tab
54
+ sessionStorage binding). No credentials appear in coordination messages/storage.
55
+
56
+ The complete app contract is in the producer repository's
57
+ `docs/admin-artifact.md`; the frozen source fixture is
58
+ `scripts/fixtures/admin-source-session-v1.json`. Django owns real cookie race
59
+ and protected-browser acceptance against the exact artifact manifest digest.
60
+
33
61
  ## Hosted auth pages (the django-mojo "bouncer" pages)
34
62
 
35
63
  The backend serves themed login/register pages at `<origin>/auth` (behind
package/docs/client.md CHANGED
@@ -10,6 +10,13 @@ import {
10
10
 
11
11
  ## Transport
12
12
 
13
+ - The controlled Django Admin artifact explicitly selects packaged mode:
14
+ `isPackagedAdmin()` is true, `apiOrigin()` is `window.location.origin`, and
15
+ `usingMockTransport()` is false. Every API path stays `/api/...`, independently
16
+ of the Admin asset mount. Upload/provider credentials retain their existing
17
+ isolation; realtime, hosted auth, exports and Shortlinks reuse `apiOrigin()`.
18
+ This mode is set by the Admin producer, not by ordinary deployment dotenv.
19
+
13
20
  - `VITE_MOJO_API` unset → the in-memory mock answers every call
14
21
  (`npm run dev`). Set to a django-mojo origin → real backend
15
22
  (`npm run dev:live`, configured in `apps/portal/.env.live`).
@@ -27,6 +34,11 @@ status, semantic `error_code`, and structured safe failure evidence. Nothing out
27
34
  parses envelopes; `mojoCall(path, {method, params, body})` is the typed
28
35
  escape hatch for protocol modules and returns the unwrapped `Envelope`.
29
36
 
37
+ `FetchOpts.beforeSend` is an optional synchronous final validity check, invoked
38
+ after the auth gate refreshes credentials and before headers/network activity.
39
+ The packaged app uses it to recheck its source-generation binding at the cookie
40
+ boundary. Throwing rejects the request. Ordinary consumers need no callback.
41
+
30
42
  Live non-2xx responses and mock `status:false` envelopes preserve the same
31
43
  shape. Modern envelopes use numeric top-level `code` plus a string semantic
32
44
  `error_code`; numeric legacy `error_code` remains a status fallback. Failed
package/docs/feedback.md CHANGED
@@ -83,3 +83,13 @@ bottom-right cards, max 5, 3.5s. Conventions:
83
83
 
84
84
  An open `<dialog>` pauses ModelTable's autoRefresh by design — don't build
85
85
  alternative modal systems that bypass `<dialog>` or that interlock breaks.
86
+
87
+ ## Admin focused dialogs
88
+
89
+ Use `modal-pad`, `modal-title`, `modal-message` and `modal-actions`; Cancel
90
+ precedes the primary action. Keep long form bodies scrollable and actions
91
+ reachable in short/narrow viewports. A pending mutation needs both disabled
92
+ buttons and a live `canDismiss` gate for Escape/backdrop. Content records use
93
+ DetailView header actions; never teach routine record deletion in a danger rail.
94
+ See [Admin modal policy](admin-modals.md) for supported lifecycle semantics,
95
+ permission checks and the complete retained-removal contract.
package/docs/forms.md CHANGED
@@ -433,3 +433,13 @@ orchestrator files follow-ups.
433
433
  | `combo` / `combobox` / `autocomplete` | — | ComboBox | OK (ComboInput feature spec, commit-only) |
434
434
  | `address` | — | AddressField | OK (private session token, details → atomic declared-field patch) |
435
435
  | `tabset` | FormView `tabs` prop | — | OK — tabsets are FORM STRUCTURE here (registry-of-tabs + permissions), not a field type |
436
+
437
+ ## Admin focused dialogs
438
+
439
+ Use `modal-pad`, `modal-title`, `modal-message` and `modal-actions`; Cancel
440
+ precedes the primary action. Keep long form bodies scrollable and actions
441
+ reachable in short/narrow viewports. A pending mutation needs both disabled
442
+ buttons and a live `canDismiss` gate for Escape/backdrop. Content records use
443
+ DetailView header actions; never teach routine record deletion in a danger rail.
444
+ See [Admin modal policy](admin-modals.md) for supported lifecycle semantics,
445
+ permission checks and the complete retained-removal contract.
package/docs/popover.md CHANGED
@@ -128,3 +128,18 @@ baked in 20px padding + 260px min-width for its one tenant, the calendar
128
128
  document state, remember listeners attach a tick later.
129
129
  - Placement flips only bottom→top (per source). If content can be taller
130
130
  than the viewport, cap and scroll it in YOUR content styles.
131
+
132
+ ## Modal ownership
133
+
134
+ A popover anchored inside a native modal portals into that dialog in both
135
+ native-Popover and fallback paths. Promoting a body-owned popover to the top
136
+ layer affects painting but does not escape the document's modal inertness; it
137
+ can appear visible while pointer/focus input lands underneath. Keeping the
138
+ portal in the owning dialog preserves menu and picker interaction. The first
139
+ Escape closes the popover; the next may dismiss the parent dialog.
140
+
141
+ Vertical placement clamps to the viewport when neither side fits. DetailView
142
+ menu content owns its height limit and scrolling; the general popover host
143
+ keeps overflow available for nested calendar/picker content. Dialog layout
144
+ cleanup restores focus while the native dialog is still connected. Header
145
+ menu selections focus their stable trigger before launching nested dialogs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portal-mojo",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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,7 +1,7 @@
1
+ import type { FileReference } from '../../client/record-feed';
1
2
  import { mojoCall, mojoDelete, mojoList, type Params } from '../../client/runtime';
2
3
  import { projectConversation, projectConversationSummary, projectReply, projectSkill } from './data';
3
4
  import type { AssistantConversation, AssistantConversationSummary, AssistantReply, AssistantSkill } from './types';
4
- import type { FileReference } from '../../client/record-feed';
5
5
 
6
6
  const malformed = (label: string): never => { throw new Error(`${label} returned an invalid response`); };
7
7
  export async function sendAssistantMessage(message: string, conversationId?: number, attachments: readonly FileReference[] = []): Promise<AssistantReply> { const ids = attachments.map((file) => file.id); if (ids.length > 5 || new Set(ids).size !== ids.length || ids.some((id) => !Number.isSafeInteger(id) || id <= 0)) throw new Error('Assistant attachments must be 1–5 unique completed File references'); const body = await mojoCall('/api/assistant', { method: 'POST', body: { message, ...(conversationId != null ? { conversation_id: conversationId } : {}), ...(ids.length ? { attachments: ids } : {}) } }); return projectReply(body.data) ?? malformed('Assistant'); }