Skip to content

feat(mockup): live JSON mockups rendered by the real block preview - #858

Open
Upd4ting wants to merge 17 commits into
mainfrom
feat/mockup-engine
Open

Upd4ting wants to merge 17 commits into
mainfrom
feat/mockup-engine

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

⚠ sensitive: more than 400 non-test lines, and dev-only routes without a login that write under mockups/.

Why

Iterating on a visual must be much faster as a mockup than through the builder, whose save typechecks and reloads (several seconds). A mockup is a JSON spec drawn by the real block renderer (DmsBlockPreview on /dms-preview), so it always matches what the DMS can draw. Task: ANT-2041.

For the dms-mockup skills (ANT-2042)

What changes

All of it is development only: nothing is registered outside ajs project run.

  • Spec. mockups/<name>/spec.json holds pages in the DmsBlockPreview tree format, page options (theme, locale, padding), fixtures per page, and sketch nodes.

  • Validation runs against ListBlockTypes() at runtime, so blocks from installed modules count:

    • options through ValidateBlockOptions
    • children against container and allowedChildren
    • duplicate sibling ids
    • sketch markup
    • a 100-level depth limit

    Each issue is a JSON pointer into spec.json. Compilation is lenient: an invalid node is still drawn, and its issues are listed.

  • Fixtures. A string option fixture:<key> points at a page fixture through /api/mockup/:name/pages/:page/fixtures/:key. TableView is built with the real TableView.fromSource over a fixture array, so blocks that need a backend render from fixtures instead of the needs_backend notice.

  • Sketches. A sketch node is static HTML and CSS. It renders in <iframe sandbox=""> with a CSP that loads only data: URLs, drawn hatched and outlined with a "Custom" label.

  • Live preview. The store watches each subscribed mockup directory and sends the compiled state over SSE. /mockup (DmsMockupViewer) posts each state to the /dms-preview iframe with the existing render message: no typecheck and no reload. It has page tabs, desktop and mobile widths, and the issues list.

  • Annotations. In Annotate mode, a click on a block writes { page, path, text, status } to mockups/<name>/annotations.json through GET/POST/PATCH /api/mockup/:name/annotations. The preview gains inspect and select messages for that click.

  • Verify map. A new mockups feature has a journey proving that every mockup route answers 404 on a built backend.

flowchart LR
  edit[Save spec.json] --> watch[fs.watch on mockups/name]
  watch -->|40 ms debounce| compile[parseSpec + compileSpec vs ListBlockTypes]
  compile --> sse["SSE state: pages, paths, issues"]
  sse --> viewer["/mockup DmsMockupViewer"]
  viewer -->|postMessage render| frame["/dms-preview iframe: DmsBlockPreview"]
  frame -->|fetchUrl| fixtures["/api/mockup/.../fixtures/key"]
  frame -->|select layout path| viewer
  viewer -->|POST note on spec pointer| ann[annotations.json]
Loading

Proof

  • Latency. On a playground run with a dev backend, a spec edit appeared in the preview iframe 83 to 93 ms after the file write, over 10 timed trials (5 before the review fixes, 5 after). The trial script edits a KPI title, then records when the new text appears in the iframe DOM. The 40 ms debounce is about half of that. The rest is compile, SSE, postMessage and the Vue render.
  • Tests on the committed head, after a clean build:
    • pnpm run checks: Checks: 11 passed, 0 failed.
    • Mockup backend tests (compile, store, routes): 35 passing.
    • Frontend preview and viewer tests: 50 passing.
    • Full frontend suite: 452 files passing.
  • Review. Two adversarial reviewers each reviewed the change, then two more rounds checked only the fix diffs. Each verified finding has a test that fails without its fix:
    • a missing list route (@Get() → @Get("/"))
    • unchecked children
    • SSE streams left behind by closed tabs
    • a recreated directory that froze its viewers
    • a sibling directory that could not be read
    • text sort order
    • sketch filter bypasses: slash-separated attributes, escaped url(, image-set candidates, entities in style
    • a note that could land on a shifted node
  • Not covered by a test. A client that disconnects while its spec is still loading now ends its stream. That leak did not reproduce on this host, so no test can fail without the fix.

Attention

  • No login. The /api/mockup/* routes and the /mockup page are development only and need no login, like /dms/dev. That includes the annotation writes. Every name, page id, fixture key and annotation id is checked against its pattern before a path is built, so no request reaches outside mockups/<name>/.
  • Whole folder deleted. Deleting the whole mockups/ directory while a viewer is open stops its updates until a reload. The docs say so.

A spec under mockups/<name>/spec.json is parsed at its boundary, validated
against the live block registry with JSON-pointer issues, its fixture:
references rewritten, TableView built through TableView.fromSource and
sketches checked like dms-ai's. A watched mockup streams its state and
annotations over SSE. /mockup renders DmsMockupViewer, dev only.
buildMockupSchema describes spec.json with one node branch per block
type, plus the sketch and componentName nodes. Each preview adapter now
carries both directions, so the schema accepts the format the compiler
validates: a block option given as a child, a form's section fieldIds.
scripts/mockup-schema.mjs writes data/mockup-spec.schema.json, and its
--check mode runs in CI next to docs:catalog:check.
The example spec and every non-controller docs example compile with no
issue; a bad option, an unknown type, a missing fixture and a forbidden
sketch are reported at their pointers; a block registered at runtime
validates. The store keeps the last good pages on a JSON error, reaches a
subscriber within a second of a save, and keeps every annotation of
concurrent creates. The rows route sorts, searches and pages, and stopping
the engine ends its event streams. src/mockup ranks with routes and dev.
A reference page for skill authors: the file layout, the spec envelope
and page options, nodes and their preview-format exceptions, fixtures,
controller blocks, sketches and their refused markup, the compiled
state, the HTTP API and its events, annotations, and the viewer.
…dren

GET /api/mockup answered 404: a bare @get() does not match a controller's
root, @get("/") does. The compiler now reports a child its parent does not
accept (Grid takes only GridRow; a block that is no container takes only the
children its block options name), the example spec wraps its cards in a
GridRow, and the name 'schema' is reserved for the static schema route.
…eview

The render message carries an optional padding flag, kept from the previous
state when absent; the mockup viewer sends each page's, padded by default.
…docs example

dms-control map failed on the unmapped src/mockup files. The mockups feature
covers them with a journey a run's built backend can drive: every mockup
route answers 404 outside development. The docs example of MockupState
declares the PreviewNode it names, which the docs check compiles.
…tory, refuse slash-separated URLs

- The events route ends its stream when the request's socket closes: the API
  only pipes the stream into the response, so a closed tab left its stream,
  listener and watcher behind.
- One recursive watcher covers mockups/ while anyone subscribes, so a mockup
  directory deleted and made again keeps reaching its subscribers instead of
  freezing on its last state.
- The sketch check refuses src and href written after a slash or a quote
  (<a/href=...>), which HTML parses as an attribute.
- A spec nested deeper than 100 JSON levels is refused with an issue instead
  of overflowing the compiler's stack.
…ng nodes, 404 a missing mockup's events

- The rows route sorts strings with a locale comparison, so "apple" comes
  before "apple pie", and rows missing the field come last both ways.
- Two siblings with one id are an issue at the second id: the preview gives
  both the same layout path, so one could not be traced back to the spec.
- GET :name/events answers 404 for a missing spec instead of an empty 200
  stream, and POST :name/annotations answers 404 unless page and path name a
  node of the spec now.
- The sketch check refuses image() and image-set() of a remote URL and an
  at-rule written with an escape.
…d read sketch CSS with escapes decoded

- The events route listens for the request's close before it reads the
  spec, and serves nothing to a client already gone.
- The sketch check decodes CSS escapes before matching, refuses any quoted
  remote URL in CSS (every image-set candidate), checks the CSS of style
  elements and attributes, and skips quoted attribute values when it looks
  for a tag's attributes (<a title=">" href=...>).
…s in one total order, refuse entities in style attributes

- The store watches mockups/ for its own entries and each subscribed mockup
  directory on its own, re-arming one that is made again: a recursive watch
  read every sibling, and one unreadable directory failed every stream.
  A directory that cannot be watched is logged, not thrown.
- Numbers and numeric text sort by value before other text, so mixed rows
  sort in one order whatever their input order, and "1.25" < "1.5".
- A sketch's style attribute may not hold character references (&#40;).
…er references

&quot; &apos; &amp; &lt; and &gt; are ordinary HTML escaping: they are read as
the characters they stand for before the CSS checks, so image-set(&quot;https://...&quot;)
is still refused, and any other reference (&#40;, &lpar;) still is.
@Upd4ting
Upd4ting marked this pull request as ready for review November 11, 2026 22:48

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant