docs(website): the browser pass on the engagement screens (Phase 4b)

Six defects that only driving the two screens in a browser could find, plus two
environment facts worth not re-deriving:

  - rebuilding client/dist while the server is running blanks the whole SPA. The
    HTML shell resolves core's hashed bundle filename at boot, so after a
    rebuild it points at a file that no longer exists; core's bundle 404s,
    window.__rg is never published, and modules/uo/entry.js throws MODULE_API.md
    §3.1's "core did not publish its shared dependencies" into a blank page. The
    error names core, and core is not at fault. Restart after every build.
  - a window.confirm blocks CDP entirely, so the two destructive actions cannot
    be driven from a script and a session that opens one is stuck until a human
    dismisses it. Exercise those paths over the API.

- [x] AI-assisted: written with Claude Code (Opus)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-29 12:26:41 -05:00
parent 97b652fed3
commit 73e1669a5c

View File

@@ -1991,6 +1991,45 @@ would have caught, and both of them in Phase 4a's code rather than 4b's:
2. **"1 rule still use this segment."** The delete refusal pluralised the noun and not the verb. It is
a one-word fix and it is in the sentence an operator reads at the moment they are being told no.
**Six more defects came out of driving the two screens in a browser**, after the API walk above had
already found the two in Phase 4a's code. None of them is visible from a test or from `curl`, and two
of them cost an operator something real:
1. **The Audience dropdown rendered empty before a trigger was chosen.** `audienceChoicesFor(null)`
answers `[]` — correctly, because without a trigger there is no ceiling and therefore nothing it
may legitimately offer. But a `<select>` with zero options reads as a control that is *broken*,
not one that is waiting. It now carries "Choose a trigger first…" and is disabled.
2. **A `members` audience with no saved audience reaches nobody, and only the preview button said
so.** This is the design (§5.1a: `members` is the ceiling for "a module-declared list", and
without a list core cannot guess) — but it is also the **default** the instant an operator picks
any `members`-ceiling trigger, which turns a documented property into a trap: the rule saves, gets
switched on, and mails nobody, with nothing on the screen saying so unless the operator happens to
press Preview. The editor now says it inline, before the save, and stands down once a preview has
answered the same question more precisely.
3. **The composer offered "exclude" on the only row**, which builds an `and` whose every child is a
complement. The server refuses that correctly — "has nothing but complements, there is no set to
exclude from" — but only after a save. It is one checkbox away at all times, so the composer now
refuses it inline, in the operator's words.
4. **The template-key input truncated its own placeholder** and the text said "optional until Phase
5", which is a sentence about this document rather than about the operator's deployment.
5. **"segment" leaked into a screen that says "saved audience" everywhere else.** The API, the schema
and this document should keep saying *segment* — one word for one table — but an operator meets
the concept under a heading that reads "Audiences", and a sentence that switches vocabulary
mid-screen reads as a sentence about something else. Translated at the point of display only.
6. The composer repeated its "AUDIENCE" heading above every row; the heading belongs to the group.
**And one thing that is worth knowing but is not a defect in this phase:** rebuilding
`client/dist` while the server is running blanks the whole SPA. The HTML shell resolves core's hashed
bundle filename at boot, so after a rebuild it still points at a filename that no longer exists;
core's bundle 404s, `window.__rg` is never published, and `modules/uo/entry.js` throws the
"core did not publish its shared dependencies" error from `MODULE_API.md` §3.1 into a blank page.
**Restart the server after every client build.** The error names core, and core is not at fault.
**A `window.confirm` freezes browser automation.** The two destructive actions here use it, matching
`InvitesAdmin` and `ModulesAdmin`, and that is the right call for the product — but a native dialog
blocks CDP entirely, so a delete cannot be driven from a script and a session that opens one is stuck
until a human dismisses it. Exercise those two paths over the API instead.
**Three smaller things the screens decided:**
- **A rule's trigger is fixed once the rule exists.** `engagementRules.db.update` never carried