Repository navigation
feat(settings,plugin-email): state the live mail transport on Setup → Email - #25448
Conversation
… Email (WIP) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4863eec2ce048a95edb21bbd3d44f088d4f2acd8 && git checkout 4863eec2ce048a95edb21bbd3d44f088d4f2acd8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d0a1361bdabc153b2fb927b8f7ef6f024ecc6891 39af42e75abdfef4f6be187cc0836925fa2d253c && git checkout -B drift-repro d0a1361bdabc153b2fb927b8f7ef6f024ecc6891 && git merge --no-ff 39af42e75abdfef4f6be187cc0836925fa2d253c
node scripts/docs-audit/affected-docs.mjs --json d0a1361bdabc153b2fb927b8f7ef6f024ecc6891
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
Contract reviewServed-tier: The face is the declaration face. This review is owed only by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT · 2026-10-23T14:38Z
Generated by Claude Code |
Requested by Sales · project thread
Part of #25335 — the server half. The card stays open for the console half (see What is left).
Clause-②: yes (widening: service-settings exports SettingsService.registerLiveStatus / liveStatusOf, SettingsLiveStatus, SettingsLiveStatusProvider and SETTINGS_LIVE_STATUS_BANNER_ID; nothing is refused)
Metadata-protocol: none
What changed
Setup → Email showed what is stored, not what is in use. Out of the box the
mailnamespace stores providersmtpwith an empty host, so the form reads as SMTP while every message goes to the development log and is recorded insys_emailas sent. The page now states the live transport at the top.@objectstack/service-settings:SettingsService.registerLiveStatus(namespace, provider)andliveStatusOf(namespace, ctx).GET /api/settings/:namespaceasks the provider on every read and puts its answer first inmanifest.specifiers, as a spec-validinfo_bannerwith the reserved idlive_status(SETTINGS_LIVE_STATUS_BANNER_ID). The registered manifest and in-processgetNamespace()are unchanged. The banner is added in the route, not ingetNamespace(), because the mail plugin callsgetNamespace('mail')while rebuilding the transport the statement waits on. A provider that throws gives an error-severity "Live status unavailable" banner and onewarnline; the statement is never silently dropped.@objectstack/plugin-email: registers themailstatement (src/live-transport-status.ts). The branch isEmailService.isConfigured():false(log transport): "No mail leaves this machine", warning severity. The body says messages are recorded insys_emailas sent and none is delivered.true: "Mail is delivered through SMTP (host:port)", or Resend, Postmark, or a host-supplied transport, at info severity.mailre-apply still running. A read made just after a save then names the transport that save built.Landing: the claim expected
plugin-emailandservice-settings, and that is where the change landed. The page is built fromGET /api/settings/:namespace(manifest plus values). The descriptor lives inservice-settings/src/manifests/mail.manifest.ts, and the live fact lives inplugin-email. Nopackages/specchange:info_banner(bannerText,bannerSeverity) already exists, and the console already renders it.One pattern: the SMS page does not state its live sender today, so there was no reader shape to copy.
registerLiveStatusis namespace-generic;service-smscan register the same statement fromISmsService.isConfigured().What is left — the console half (not done here)
The statement follows a swap on the next read of the page, not yet in place after Save.
@object-ui/console18.0.0 (SettingsView, read from the npm tarball because this session has no objectui access) merges only the PUT response'svaluesinto the page after a save. It keeps the manifest from its first GET and never refetches, so the banner keeps the pre-save statement until the page is reloaded. The fix is a console change: refetchGET /api/settings/:namespaceafter a successful save, or after any action. The server already answers the new statement on that read.Seam: runtime:service-settings GET /api/settings/:namespace manifest.specifiers[0] (info_banner id live_status) → renderer:@object-ui/console SettingsView save handler (merges PUT values only; manifest never re-read)Tests
plugin-email/src/live-transport-status.pin.test.ts(5 tests): pins the reading ofisConfigured(). It covers the out-of-the-box state, a swap saved from the page (read before the re-apply settles), and that the branch isisConfigured(), not the transport class.service-settings/src/settings-routes.live-status.pin.test.ts(6 tests): the banner heads the GET payload and parses underSettingsNamespacePayloadSchema. The provider is asked on every read, the registered manifest stays untouched, nothing is added without a provider, a throwing provider gets an error banner, and an unknown namespace is refused.liveStatusOf: async () => undefined(the read route now calls it).pnpm --filter @objectstack/plugin-email test: 37 files / 676 tests passed. Typecheck passed (tsc --noEmit && check:test-typecheck).pnpm --filter @objectstack/service-settings test: 60 files / 995 tests passed. Typecheck passed.Ablations (
scripts/ablation-replace.mjs; every leg restored to the HEAD blob with an emptygit diff HEAD):if (!source.isConfigured())replaced byif (!(source.options.transport instanceof LogTransport)): 5 tests went red, but withReferenceError: LogTransport is not defined. That is a crash, not the mutation, so this run does not count; it is reported here, not hidden.source.options.transport.constructor.name === 'LogTransport': 1 failed, 4 passed. The red test is "the branch is isConfigured(), not the transport class".if (false): 4 failed, 1 passed.await Promise.allSettled([...applying]);deleted from the plugin: 1 failed, the swap test (expected 'warning' to be 'info').Gates (local, at 4789209)
node scripts/pm/dispatch-gates.mjs --commandsderived 70 commands, and all 70 exited 0.--ranreconciliation: "70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED (a DERIVED zero — all 70 recorded an exit code and none of them is 3)". One gate needed a fix along the way: on the first sweep,check:engine-double-contractflagged the new pin's fake engineupdate(). The fake now declares onlyfind(), which is all this boot reads. Everything CI runs beyond the derived set is CI's.Acceptance notes
live_statusbanner has no zh-CN, ja-JP or es-ES text. Its text is computed per request, and the console's banner lookup by id would replace it with a fixed translation. A translated statement needs per-state ids or a parameterised lookup.mail/testfallback already says no email service is mounted.origin/mainmoved 7 commits since the basef0fdd45bbc. None touchplugin-emailorservice-settings, so no merge was taken.🤖 Generated with Claude Code
https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy