docs(website): the authoring UI proper, and the one route it needed (Phase 13) #223

Merged
whitlocktech merged 1 commits from docs/events-p13-authoring-ui into edge 2026-09-07 22:12:28 +00:00
3 changed files with 139 additions and 5 deletions
Showing only changes of commit 303a52a2a0 - Show all commits

View File

@@ -1709,6 +1709,14 @@ file a route sits in — that is the property the route manifest freezes.
| GET | `/events/series` | the arcs a definition may belong to. Read-only in this phase |
| — | `/shard/*` · `/uo-link/*` | **Served by `module-uo`, not by core** (33 routes). Documented in [`../modules/uo/API.md`](../modules/uo/API.md) |
> **The `/events/*` rows above are the surface as of Phase 1**, and they are not the whole of it.
> The live run controls (Phase 3), the calendar and series writes (Phase 4), `advance` (Phase 5),
> the dry run and the action switchboard (Phase 6), the option-source route (Phase 7), `cleanup`
> (Phase 8) and the cap meter (Phase 13) are not listed here. **[`EVENTS.md`](EVENTS.md) § API
> surface is the canonical table** and carries every one of them with the reasoning for its gate;
> re-listing them here would be a second copy of a contract that file owns, and the copy that
> drifts is always the second one.
Every admin write logs to `activity_log`.
### The SPA HTML shell (`app.js` → `utils/htmlShell.js`)

View File

@@ -1190,8 +1190,8 @@ already fails core's build on a UO identifier, so it is enforced in CI rather th
| Screen | Pattern it reuses | What is new |
| --- | --- | --- |
| **Calendar** — month and list view, filtered by state, scope and series | New, but this is the deliverable that replaces a WordPress plugin | Series, recurrence, local timezone rendering. |
| **Definition editor** — basics, storyline, venue, schedule, phases | Sectioned admin form; schedule as a closed shape | The phase timeline. |
| **Step editor** — pick an action, fill its params | **The condition builder, exactly**: core serves a catalog, the module declared the schema, core renders a form it does not understand | Option sources; a live cap meter. |
| **Definition editor** — basics, storyline, venue, schedule, phases | Sectioned admin form; schedule as a closed shape | The phase timeline. **Finished in Phase 13**, which added the per-phase cap draw, the advance-condition builder and the start dialog — `rehearsal`, `scope` and `params` have been on the start route since Phase 10 and the screen posted none of them. An event whose concurrency key names a `{placeholder}` therefore could not be started correctly by hand at all: the key is rendered from the run's own params, so every manual run rendered the same one and the second was refused as an overlap with the first. |
| **Step editor** — pick an action, fill its params | **The condition builder, exactly**: core serves a catalog, the module declared the schema, core renders a form it does not understand | Option sources; a live cap meter. **Built in Phase 13**, with the JSON box kept as a per-step escape hatch — see below. |
| **Actions** — which are enabled on this deployment, and their per-run caps | `SettingsAdmin.jsx` shape, one row per registered action | The cap editor. **Built in Phase 6**, `admin` only. Each row says whether it is showing a stored decision or a risk-class default, because "an admin turned this on" and "this has always been on" are different facts and only one is a decision somebody made. A cap may only name a dimension the action actually spends — a cap on one it never names would be a number an operator believes is protecting them while it bounds nothing. |
| **Run console** — live status, steps, caps, failures, cleanup | Shaped like `EngagementSendLog.jsx` | The "why didn't phase 3 start?" panel. **Built through Phase 6**, caps included: a meter per dimension rather than a sentence, because unlike a gate a cap is two numbers and a name and needs no grammar rendered to be read. Shown on finished runs too — *"how much did last night's invasion actually spawn"* is the same question asked the morning after. |
@@ -1232,13 +1232,63 @@ advance condition, its cap draw and its failure policy — says exactly what the
the registration: the action's `label`, its params' names, its option sources' labels, its budget
dimensions' labels. `check:modules` already fails core's build on a UO identifier.
### What Phase 13 settled, replacing the two placeholders
Phase 3 shipped a step's params and a phase's `where` as raw JSON boxes, each captioned as a
placeholder for this phase. Both are forms now, and three things about how they give way are
decisions rather than mechanics.
**A form gives way to JSON on the condition builder's own rule.** A value the editor cannot
round-trip is SHOWN rather than silently rewritten — which is why the two placeholders were always
the same problem. Flattening `A and (B or C)` into `A and B and C` changes which firings release a
phase; dropping a param the action does not declare changes what a step does. Neither is visible in
a save that otherwise looks clean. Three things force a step into JSON, and the screen names which:
the action is dormant, so there is no declaration to draw; the step carries a param the action does
not declare, which the save refuses BY NAME and which is exactly what the author needs to read; or a
value no single control can hold. Otherwise the author's own choice of editor stands.
**A `where` the builder cannot draw is posted back unchanged.** The builder offers the flat half of
the grammar — one `and`/`or` over a list of comparisons — and a nested tree opens read-only with its
JSON showing and one honest choice: leave it, or clear it and start again. The alternative to posting
it back verbatim is deleting an author's predicate because this screen could not render it.
**It is the engagement builder, not a second one shaped like it.** The grammar behind a phase gate
IS the engagement condition grammar: `engagement/conditions.js` validates it at save, and the
diagnosis panel's sentence is rendered on the server from the same labels. `conditionRowsFrom`,
`conditionsFromRows` and `coerceLiteral` are imported, not reimplemented. The coercion is the half
worth naming: every value in an HTML input is a string, and `{ cmp: 'gt', value: "5" }` against an
`int` variable is refused — rightly — by a server whose error the author would then be reading as a
complaint about JSON rather than about what they typed.
**A boolean param is a three-value select and an empty field removes its key.** A checkbox cannot
say *"not set"*, which for an optional boolean is a real third state — the action's own default —
and would post `false` for every param nobody touched. And `checkParams` reads `undefined`, `null`
and `''` alike, so clearing a required field is answered *"is required"* rather than with a type
complaint about an empty string.
**The searchable source got its consumer here.** Phase 12b shipped `q` and `searchable` and nothing
sent a term — so the one screen that picks a spawner was picking from a 2,000-entry truncation of a
6,707-entry world, with nothing on it saying so. Which control is drawn follows the source's own
`searchable`, never the length of the answer: inferring it from a short list reads correctly right
up until the same source is a dropdown on one shard and a search box on another.
### Four affordances worth building in from the start
- **Dry run.** *(Built, Phase 6.)* Materialise the steps, dispatch each with `verify: true`, report what *would* happen
and what it *would* cost against the caps. The project's own idiom, and what turns an unattended
world write from alarming into routine.
- **Rehearsal.** Run for real with announcements ceilinged to `staff`. Free from the ceilings lattice
— the run is real, the world changes are real, no player is told.
- **The live cap meter.** *(Built, Phase 13.)* What the plan in the author's hands would spend, and
what this deployment allows, updated while they type. **Deliberately not a lighter dry run**: it
dispatches nothing, so it knows nothing a module knows — whether the landmark exists, whether the
shard is up — and the screen says so under the numbers. What it answers is the half core can
answer alone, which is the half a debounce may ask on every keystroke. It says what it does not
know, too: a step core could not price makes every total an under-count, and an author reading a
number smaller than what will happen is worse off than one reading no number at all.
- **Rehearsal.** *(Built, Phase 10; reachable, Phase 13.)* Run for real with announcements ceilinged
to `staff`. Free from the ceilings lattice — the run is real, the world changes are real, no player
is told. The narrowing ceiling on the emit envelope arrived in Phase 10 and the checkbox that asks
for it in Phase 13, which is longer than it should have been: an affordance nothing can reach is an
affordance nobody has.
- **A GM cue step.** A core action whose implementation is "post the instruction, wait for a human to
confirm they did it in-client, then advance". No module, no protocol, no world write — and it makes
the entire system useful on day one, with a GM doing the target-driven parts in-client exactly as
@@ -1618,7 +1668,8 @@ no URL moved.
| `DELETE /admin/events/:id` | admin | archive — never a hard delete while runs reference it |
| `GET /admin/events/:id/versions` | staff | version history |
| `POST /admin/events/:id/verify` | admin, editor | **dry run** (Phase 6) — dispatch every step with `verify: true`, report the findings and the whole-plan cost against the caps. Verifies the PUBLISHED version of a `ready` definition and the working spec of a draft; a pass against a version is recorded and is what unlocks scheduled starts (§K) |
| `POST /admin/events/:id/runs` | admin | start now; optional `rehearsal: true`, optional `scope` |
| `POST /admin/events/price` | admin, editor | **the live cap meter** (Phase 13) — price the spec IN THE BODY against the per-run caps. Dispatches nothing and records nothing, which is what separates it from the dry run and what makes it safe to call on a debounce while an author types; the spec is in the body because the plan being priced is unsaved between keystrokes. It therefore knows nothing a module knows — whether the landmark exists, whether the shard is up — and the screen says so under the numbers. A step it cannot price is reported in `unpriced` rather than counted as free; an **undeclared dimension is still counted**, because the action really will try to spend it and it is the enforcement rather than the amount that is missing. Over the caps is a `200`, for `verify`'s reason |
| `POST /admin/events/:id/runs` | admin | start now; optional `rehearsal: true`, optional `scope` — and optional `params`, which **Phase 13 was the first thing to send**: the concurrency key is a `{placeholder}` template rendered from the run's own params, so a UI posting an empty body rendered the same key for every manual run and had the second refused as an overlap with the first |
| `GET /admin/events/runs` | staff | run history across definitions |
| `GET /admin/events/runs/:runId` | staff | status, phase, steps, caps, resources, cleanup |
| `GET /admin/events/runs/:runId/log` | staff | the diagnostic log |
@@ -1710,6 +1761,14 @@ asks for 45 and you allow 30" indistinguishable from "you sent a bad event id".
Nothing is absent from this list any more: `advance` left it in Phase 5, `cleanup` and cancel's
`cleanup` flag in Phase 8.
**Phase 13 added the one route this table did not anticipate**, and it is worth saying why it is a
route rather than arithmetic in the browser. A module's `cost()` is a function of params evaluated on
the server and nowhere else (§F), so a meter has nothing to add up until something asks. The dry run
is the wrong thing to ask twice over: it dispatches every step through the module — and through it to
a sidecar and a game tick — and a pass against a published version is RECORDED, which is the stamp
§K's unattended-start gate reads. Neither belongs behind a keystroke. `POST /admin/events/price`
does neither, which is the whole of its design.
> **The whole-plan cost check is the one thing no other path makes.** Every per-step check the dry run
> performs is also made at save or at dispatch; the TOTAL is not. Three steps each spawning 15 under a
> cap of 30 pass every individual check and breach the cap on the third — at two in the morning,

View File

@@ -1621,6 +1621,73 @@ rather than a silent revert of their change.
### Phase 13 — The authoring UI proper (`website`)
> **Complete.** `edge` in `website` and `docs`. The two raw JSON boxes Phase 3 shipped as explicit
> placeholders are forms; the meter, the searchable dropdown and rehearsal are reachable. **One
> route added** — `POST /admin/events/price` — and `MODULE_API_VERSION` is untouched: every schema
> the form renders was already in the catalog.
>
> **Four decisions the org lead settled (2026-09-07), all as recommended:**
>
> - **The meter gets a route of its own rather than riding the dry run.** A module's `cost()` is a
> server function of params, so nothing can be totted up in the browser — and the dry run is the
> wrong call to make on a debounce twice over: it dispatches every step through the module (and
> through it to a sidecar and a game tick), and a pass against a published version is RECORDED,
> which is the stamp §K's unattended-start gate reads. `POST /admin/events/price` dispatches
> nothing and records nothing, takes the spec in the body because the plan being priced is unsaved
> between keystrokes, and is `admin, editor` for `verify`'s reason.
> - **The advance-condition builder is in scope**, though the phase sentence did not name it. P13 is
> the last authoring phase, so *"a later phase"* was this one or none. It is the engagement builder
> — the same `conditionRowsFrom`/`conditionsFromRows`, not a second one shaped like it — because
> the grammar behind a phase gate IS the engagement grammar, validated on the server by
> `engagement/conditions.js` and rendered into the diagnosis panel's sentence by the same labels.
> - **Start now opens a dialog carrying `rehearsal`, `scope` and `params`.** All three have been on
> the route since Phase 10 and this screen posted `{}`.
> - **The searchable option source gets its consumer**, the JSON box stays as a per-step escape
> hatch, and the timeline gains the per-phase cap draw §I asks it for.
>
> **Two defects this phase closes in already-merged code, and neither was visible from a test:**
>
> - **12b's searchable sources had no consumer.** The server half shipped — `q` on
> `/catalog/options/:sourceId`, `searchable` on the answer — and the only UI that reads a source
> never sent a term. The spawner list is 6,707 entries against `MAX_OPTIONS`' 2,000, so the one
> screen that picks a spawner was picking from a truncation of two thirds of the world, with
> nothing on it saying so.
> - **An event whose concurrency key names a `{placeholder}` could not be started correctly by
> hand.** The key is rendered from the run's own params; a start posting `{}` rendered the same key
> every time, so the second manual run was refused as an overlap with the first — the failure
> looking exactly like the safety feature working.
>
> **What the build settled.**
>
> - **A form gives way to the JSON box on the CONDITION BUILDER's rule, not on a new one.** A value
> the editor cannot round-trip is shown rather than silently rewritten: dropping a param the action
> does not declare and flattening `A and (B or C)` into `A and B and C` are the same mistake, a
> save that looks clean and means something else. Three things force it — a dormant action, an
> undeclared param, a value no single control can hold — and the screen names which.
> - **A step core cannot price is reported, never counted as free.** The three ways that happens all
> make the total an under-count, and an author trusting a number smaller than what will happen is
> worse off than one with no number. An undeclared dimension is the one that is still *counted*:
> the action really will try to spend it — the step is refused at dispatch for exactly that — so
> the amount is true and it is the enforcement that is missing.
> - **A boolean param is a three-value select, not a checkbox.** A checkbox cannot say *"not set"*,
> and for an optional boolean that is a real third state: the action's own default. A checkbox
> would have posted `false` for every param nobody touched.
> - **An empty field removes the key rather than posting `""`.** `checkParams` reads `undefined`,
> `null` and `''` alike, so a required param left blank comes back as *"is required"* — the error
> the author needs — instead of a type complaint about an empty string.
> - **A half-typed number is kept as typed.** `coerceLiteral` is borrowed from the engagement builder
> unchanged, and its rule matters here for the same reason: turning `-` into `NaN` mid-keystroke
> either posts a value nobody wrote or makes a negative impossible to enter.
> - **The meter debounces AND counts generations.** Requests 400ms apart do not necessarily answer in
> that order, and an older answer landing last leaves the meter describing a plan that has already
> changed — stale in the one direction that matters, with nothing on screen to say so. A failure
> leaves the last answer standing, dimmed, rather than blanking it: the plan is still saveable,
> dry-runnable and publishable without a meter.
>
> **Verify, as run.** Server `npm test` (the new `eventPrice.test.js`, 13 tests, plus the 403 walk's
> new row); client `npm test` — **380 pass, 0 fail**, 18 of them new; the client builds.
> `routes:manifest` and `swagger` regenerated — one route added, none moved.
Replaces P3's placeholders. The phase timeline (**not** a node graph — the condition grammar has no
branching and a canvas would advertise power the engine does not have); the step editor rendering each
action's declared params, with option-source dropdowns; the live cap meter; dry run; rehearsal.