feat(events): the authoring UI proper (Phase 13)
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:
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user