feat(events): the authoring UI proper (Phase 13)
Some checks failed
PR Checks / client-build (pull_request) Successful in 31s
PR Checks / bot-tests (pull_request) Successful in 31s
PR Checks / server-tests (pull_request) Failing after 9m1s

Replaces the two raw JSON boxes Phase 3 shipped as explicit placeholders: a
step's params are a form rendered from the action's own declaration, and a
phase's advance condition is the engagement condition builder. Adds the live cap
meter, the searchable option source's first consumer, and a start dialog
carrying the three fields the route has taken since Phase 10.

One route: POST /admin/events/price, admin+editor. A module's cost() runs on the
server and only there, so a meter has nothing to add up until something asks --
and the dry run is the wrong thing to ask on a debounce twice over: it dispatches
every step through the module and a pass against a version is RECORDED, which is
the stamp K's unattended-start gate reads. This dispatches nothing and records
nothing, and takes the spec in the body because the plan being priced is unsaved
between keystrokes.

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. Dropping a param the
action does not declare and flattening `A and (B or C)` are the same mistake.

Two defects fixed in already-merged code:

  * Creating an event has been impossible since Phase 6. `events/new` was added
    beside `events/:id` and binds no param, and React Router ranks a static
    segment above a dynamic one whatever the order -- so the editor was handed no
    id and fetched /admin/events/undefined. Worse, the failure was invisible:
    `!form` is true for every failed load, so the error state sat behind a
    spinner that never stopped.
  * 12b's searchable sources had no consumer. The server half shipped and the
    only UI that reads a source never sent a term, so the 6,707-entry spawner
    list was picked from a 2,000-entry truncation with nothing saying so.

Server: 2113 tests, 2024 pass, 0 fail (89 DB-skipped). Client: 380 pass, 0 fail.
routes:manifest and swagger regenerated -- one route added, none moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-07 16:34:03 -05:00
parent db8e01e868
commit 8453762e3b
13 changed files with 1913 additions and 149 deletions

View File

@@ -33,6 +33,7 @@ const logDb = require('../../../model/events/eventRunLog.db')
const activity = require('../../../model/activity/activity.model')
const settingsDb = require('../../../model/events/eventActionSettings.db')
const authorize = require('../../../events/authorize')
const price = require('../../../events/price')
const asId = (raw) => {
const n = Number(raw)
@@ -466,6 +467,44 @@ exports.verify = async (req, res) => {
})
}
/**
* POST /api/v1/admin/events/price — the live cap meter (Phase 13).
*
* `admin, editor`, exactly as the dry run is and for the same reason: an author
* should be able to find out what their plan would cost before asking an admin
* to commit the deployment to it.
*
* **The spec is in the BODY, not looked up by id**, and that is the whole point
* of the route. The meter answers a question about the plan in the author's
* hands — half-typed, unsaved, and quite possibly not publishable yet — so a
* route that read the stored draft would be answering about a spec the author is
* no longer looking at.
*
* It dispatches nothing, unlike `verify`, and it records nothing, unlike a dry
* run that passes against a version — which is the stamp §K's unattended-start
* gate reads. Those two absences are exactly what make it safe to call while
* somebody is still typing.
*
* A body core cannot make sense of is a `400`; a plan that is over the caps is a
* **200**, for the dry run's reason — *"this asks for 45 and you allow 30"* is
* an answer, not a failed request.
*
* Not logged to the activity trail. It is a read that changes nothing and it
* fires on a debounce while a form is edited; an audit line per keystroke would
* bury the acts that matter under the act of looking.
*/
exports.price = async (req, res) => {
const result = await price.priceSpec(req.body || {})
if (!result.ok) return res.status(400).json({ error: result.error })
return res.json({
steps: result.steps,
priced: result.priced,
cost: result.cost,
phases: result.phases,
unpriced: result.unpriced,
})
}
/**
* GET /api/v1/admin/events/actions — the deployment's switchboard.
*