docs(website): the authoring UI proper, and the one route it needed (Phase 13)
EVENTS.md I gains what the phase settled -- the rule by which a form gives way to the JSON box, why the condition builder is the engagement one rather than a second one shaped like it, and the live cap meter as a fifth affordance beside the dry run it must not be mistaken for. The screens table's two unfinished rows are marked built; rehearsal is marked reachable, which it had not been since Phase 10 shipped it. The API surface table gains POST /admin/events/price, and a paragraph on why it is a route rather than arithmetic in the browser: cost() is a server function of params, and the dry run both dispatches and records, neither of which belongs behind a keystroke. EVENTS_PLAN.md carries Phase 13 as built, with the four decisions, the two defects it closes in already-merged code, and the six things the build settled. BACKEND_DESIGN.md's /events/* rows are Phase 1's surface and have not moved since; a note says so and points at EVENTS.md as canonical rather than re-listing eleven routes that file already owns. Checks: docs has no CI. Every relative link and in-page anchor in the new sections was resolved by hand; the files are CRLF in the working tree and each diff is content-sized (8/0, 64/5, 67/0), so none carries the \r\r\n full-file rewrite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -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 |
|
| 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) |
|
| — | `/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`.
|
Every admin write logs to `activity_log`.
|
||||||
|
|
||||||
### The SPA HTML shell (`app.js` → `utils/htmlShell.js`)
|
### The SPA HTML shell (`app.js` → `utils/htmlShell.js`)
|
||||||
|
|||||||
@@ -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 |
|
| 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. |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **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
|
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.
|
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
|
### 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
|
- **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
|
and what it *would* cost against the caps. The project's own idiom, and what turns an unattended
|
||||||
world write from alarming into routine.
|
world write from alarming into routine.
|
||||||
- **Rehearsal.** Run for real with announcements ceilinged to `staff`. Free from the ceilings lattice
|
- **The live cap meter.** *(Built, Phase 13.)* What the plan in the author's hands would spend, and
|
||||||
— the run is real, the world changes are real, no player is told.
|
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
|
- **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
|
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
|
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 |
|
| `DELETE /admin/events/:id` | admin | archive — never a hard delete while runs reference it |
|
||||||
| `GET /admin/events/:id/versions` | staff | version history |
|
| `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/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` | staff | run history across definitions |
|
||||||
| `GET /admin/events/runs/:runId` | staff | status, phase, steps, caps, resources, cleanup |
|
| `GET /admin/events/runs/:runId` | staff | status, phase, steps, caps, resources, cleanup |
|
||||||
| `GET /admin/events/runs/:runId/log` | staff | the diagnostic log |
|
| `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
|
Nothing is absent from this list any more: `advance` left it in Phase 5, `cleanup` and cancel's
|
||||||
`cleanup` flag in Phase 8.
|
`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
|
> **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
|
> 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,
|
> cap of 30 pass every individual check and breach the cap on the third — at two in the morning,
|
||||||
|
|||||||
@@ -1621,6 +1621,73 @@ rather than a silent revert of their change.
|
|||||||
|
|
||||||
### Phase 13 — The authoring UI proper (`website`)
|
### 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
|
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
|
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.
|
action's declared params, with option-source dropdowns; the live cap meter; dry run; rehearsal.
|
||||||
|
|||||||
Reference in New Issue
Block a user