M4 — the product UI and the container delivery

Provenance: the frames below are the user-verification layer's own PNGs, and the raw output is the run's own artifact — nothing here is retyped. Source: mihail812-org/polymarket_wiki, milestones/M4-report.md and milestones/M4-user-verification/.

Milestone: M4 — the product UI and the container delivery (Kind: feature)
Change: m4-ui-and-container-delivery
Branch: feat/m4-ui-and-container-delivery (poly a2faf7b; wiki 7f2d5e8)
Layer: SBHOST — the real binary against a live ClickHouse store, driven by a real headless chromium
Verdict: the UI is delivered and its user stories are captured and checked; the container image's build/run is not run on this host — the podman-kind runner carries no engine client (preinstall#114).

What was done, in short

User story 1 — "I open the product and see what has been collected"

An owner opens the UI and wants to know, at a glance, which events the collector has recorded and how much of each. The page lists the collected events with their market and token counts, the number of order-book and trade rows, and the recorded time span; a filter narrows the list to a slug.

the list — the collected events, their markets, tokens, both counts and the recorded span (header: events 2/2)
the list — the collected events, their markets, tokens, both counts and the recorded span (header: events 2/2)
the counts table — Shakhtar 360 order-book / 179 trade rows; Stefanik 4 / 1
the counts table — Shakhtar 360 order-book / 179 trade rows; Stefanik 4 / 1
the recorded span — the min and max recv_time across both tables
the recorded span — the min and max recv_time across both tables
the filter narrows the list — filtering by stefanik drops the Shakhtar event (header: events 1/2)
the filter narrows the list — filtering by stefanik drops the Shakhtar event (header: events 1/2)

Against the store: list.order_book rows == store → UI=360 store=360; list.trade rows == store → UI=179 store=179. match.

User story 2 — "I pick an event and see what the book and the trades looked like at a moment"

The owner picks an event and a moment T and wants the order book that stood then and the trades that executed around it. The detail names the event, the moment and the window, so the moment is recountable by hand.

the detail at T — the event, the moment T (2026-10-07T21:34:39.828Z) and the window (±10.0s), then the two tables
the detail at T — the event, the moment T (2026-10-07T21:34:39.828Z) and the window (±10.0s), then the two tables
the order book at T — best_bid 0.001, best_ask 0.002, at 2026-10-07T21:34:39.828Z
the order book at T — best_bid 0.001, best_ask 0.002, at 2026-10-07T21:34:39.828Z
the trades around T — the ±10 s window, in receive-time order (20 rows)
the trades around T — the ±10 s window, in receive-time order (20 rows)
the default moment — opening an event with no moment shows the book at the recorded end
the default moment — opening an event with no moment shows the book at the recorded end
a second moment — the same event at the recorded end (2026-10-07T21:36:09.447Z)
a second moment — the same event at the recorded end (2026-10-07T21:36:09.447Z)
the first recorded instant — the event at 2026-10-07T21:33:10.552Z, the very start of the recording
the first recorded instant — the event at 2026-10-07T21:33:10.552Z, the very start of the recording

Against the store: orderbook.at_T best_bid == store → UI=0.001 store=0.001; best_ask → UI=0.002 store=0.002; trades.around_T count == store → UI=20 store=20. match.

User story 3 — "I can trust what the UI shows me"

An owner has to be able to tell "nothing was recorded" from "the store did not answer" from "this event has no rows here". The UI renders each as its own state; the layer captures each and asserts the page says so.

the store unreachable — an explicit page, not an empty list
the store unreachable — an explicit page, not an empty list
a reachable but empty store — events 0/0, recorded span: n/a
a reachable but empty store — events 0/0, recorded span: n/a
the empty store's detail — the moment and window print, both tables shown empty
the empty store's detail — the moment and window print, both tables shown empty
an event with no rows — both tables empty with their headers, not an invented book
an event with no rows — both tables empty with their headers, not an invented book

Goal → case → what was observed → result

GoalThe caseWhat the run observedResult
The UI serves the collected events with their counts and the recorded span, and narrows the list by a filterthe UI's list beside a query of the store for the same events and countsframes 1–4; list.order_book rows == store (360=360), list.trade rows == store (179=179); the filter narrows 2→1OK, captured and checked
For a chosen event the UI shows the order book at a moment T and the trades around T, read from the storethe UI's detail at T beside the store's own book at Tframes 5–10; best_bid 0.001=0.001, best_ask 0.002=0.002, trades around T 20=20OK, captured and checked
The UI reports its store honestly — a dead store is not an empty successthe UI against a dead store, an empty store, and an event with no rowsframes 11–14, each asserted for its own stateOK, captured and checked
The product ships as one container image carrying all of its dependencies, and is built and run as an image./run-m4.sh deploy on a clean environment (podman build → podman run)not run — the podman layer carries no engine client (have podman MISSING, preinstall#114); the Containerfile's static checks pass in ./gradlew testopen
The snap is retiredno snap/snapcraft.yaml, no snap scenario, no snap path in scripts or workflowsgrep for the snap paths is empty; the REMOVED delta sits in the changeOK

The reverse runs — a green result turned red

Each check must go red on its own broken subject. All were run from the same scripts and the same store as the green run.

Reverse runWhat was brokenWhat went red
reverse-athe layer pointed at a different UI (the empty store) while the store is fullframes miss the Shakhtar event, both counts and the span; no book — user-verify-m4: FAIL
reverse-bthe browser driver is absent from the environmentthe layer refuses to run: playwright not found — the browser driver belongs to the image (#105)
reverse-cthe store is not reachablecannot compare: the collection store is not reachable at http://127.0.0.1:9999
reverse-dthe UI serves a blank documentevery DOM assert reddens and each capture is 0.00% non-white — a blank capture is not evidence

Layer — the run's own record

Full m4-user-verify.txt (raw file):

== M4 user verification: the product UI beside the collection store, field for field ==
ui            : http://127.0.0.1:8899
store         : http://127.0.0.1:8123
event         : will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578
token         : 33339798372916037220786136133406478491116150628220441951753426683441228279665
moment T      : 2026-10-07T21:34:39.828Z
window        : ±10s
fonts         : /tmp/m4-fontconfig/fonts.conf
browser flags : --no-sandbox --disable-gpu

-- the store's own rows (read from ClickHouse, not from the page) --
  order_book rows (event token set) : 360
  trade rows (event token set)      : 179
  recorded span                     : 2026-10-07T21:33:10.552Z .. 2026-10-07T21:36:09.447Z
  book at T                         : best_bid=0.001 best_ask=0.002 @ 2026-10-07T21:34:39.828Z
  trades around T                   : 20

-- the frames (each asserted: DOM before the shot, non-blank after) --
  ok    us1-01-list.png                      2.94%  full page  http://127.0.0.1:8899/
  ok    us1-02-list-counts.png              14.77%  #events  http://127.0.0.1:8899/
  ok    us1-03-span.png                     11.24%  #span  http://127.0.0.1:8899/
  ok    us1-04-filter-narrows.png            1.99%  full page  http://127.0.0.1:8899/?filter=stefanik
  ok    us2-01-detail-T.png                  7.66%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us2-02-orderbook-at-T.png           24.86%  #orderbook  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us2-03-trades-around-T.png          25.28%  #trades  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us2-04-detail-default-moment.png      6.8%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578
  ok    us2-05-detail-second-moment.png       6.8%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408969447
  ok    us2-06-detail-first-instant.png      6.55%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408790552
  ok    us3-01-store-unreachable.png         0.66%  full page  http://127.0.0.1:8898/
  ok    us3-02-empty-store.png                1.1%  full page  http://127.0.0.1:8897/
  ok    us3-03-empty-store-detail.png        3.83%  full page  http://127.0.0.1:8897/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us3-04-event-with-no-rows.png        3.17%  full page  http://127.0.0.1:8899/?event=will-bernie-sanders-win-the-2028-democratic-presidential-nomination-879&moment=1791408879828

-- field for field: the UI against the store --
  match  [us1] us1-01-list.png painted            non-white 2.94%
  match  [us1] us1-02-list-counts.png painted     non-white 14.77%
  match  [us1] list.order_book rows == store      UI=360 store=360
  match  [us1] list.trade rows == store           UI=179 store=179
  match  [us1] us1-03-span.png painted            non-white 11.24%
  match  [us1] us1-04-filter-narrows.png painted  non-white 1.99%
  match  [us2] us2-01-detail-T.png painted        non-white 7.66%
  match  [us2] orderbook.at_T best_bid == store   UI=0.001 store=0.001
  match  [us2] orderbook.at_T best_ask == store   UI=0.002 store=0.002
  match  [us2] trades.around_T count == store     UI=20 store=20
  match  [us2] us2-02-orderbook-at-T.png painted  non-white 24.86%
  match  [us2] us2-03-trades-around-T.png painted non-white 25.28%
  match  [us2] us2-04-detail-default-moment.png painted non-white 6.8%
  match  [us2] us2-05-detail-second-moment.png painted non-white 6.8%
  match  [us2] us2-06-detail-first-instant.png painted non-white 6.55%
  match  [us3] us3-01-store-unreachable.png painted non-white 0.66%
  match  [us3] us3-02-empty-store.png painted     non-white 1.1%
  match  [us3] us3-03-empty-store-detail.png painted non-white 3.83%
  match  [us3] us3-04-event-with-no-rows.png painted non-white 3.17%
user-verify-m4: OK

The store's own answers

raw file:

# store side — the store's own answers (ClickHouse 24.8, direct), read by the UI's own reads #

select1                 : 1
order_book rows (total) : 364
trade rows (total)      : 180
span (min..max)         : 1791408790552..1791408969419

-- per event (UI token sets) --
  will-shakhtar-done   order_book=360  trade=179 
  will-elise-stefani   order_book=4    trade=1   

-- the main event's book at T=1791408879828 --
  best_bid=0.001 best_ask=0.002 at=1791408879828
  trades around T (±10s)=20

-- empty store (schema only, no rows): emptyui --
  emptyui.order_book=0  emptyui.trade=0

The raw console log

The layer's console output (raw file):

ok   us1-01-list.png                      2.94%  full page
ok   us1-02-list-counts.png              14.77%  #events
ok   us1-03-span.png                     11.24%  #span
ok   us1-04-filter-narrows.png            1.99%  full page
ok   us2-01-detail-T.png                  7.66%  full page
ok   us2-02-orderbook-at-T.png           24.86%  #orderbook
ok   us2-03-trades-around-T.png          25.28%  #trades
ok   us2-04-detail-default-moment.png      6.8%  full page
ok   us2-05-detail-second-moment.png       6.8%  full page
ok   us2-06-detail-first-instant.png      6.55%  full page
ok   us3-01-store-unreachable.png         0.66%  full page
ok   us3-02-empty-store.png                1.1%  full page
ok   us3-03-empty-store-detail.png        3.83%  full page
ok   us3-04-event-with-no-rows.png        3.17%  full page

== M4 user verification: the product UI beside the collection store, field for field ==
ui            : http://127.0.0.1:8899
store         : http://127.0.0.1:8123
event         : will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578
token         : 33339798372916037220786136133406478491116150628220441951753426683441228279665
moment T      : 2026-10-07T21:34:39.828Z
window        : ±10s
fonts         : /tmp/m4-fontconfig/fonts.conf
browser flags : --no-sandbox --disable-gpu

-- the store's own rows (read from ClickHouse, not from the page) --
  order_book rows (event token set) : 360
  trade rows (event token set)      : 179
  recorded span                     : 2026-10-07T21:33:10.552Z .. 2026-10-07T21:36:09.447Z
  book at T                         : best_bid=0.001 best_ask=0.002 @ 2026-10-07T21:34:39.828Z
  trades around T                   : 20

-- the frames (each asserted: DOM before the shot, non-blank after) --
  ok    us1-01-list.png                      2.94%  full page  http://127.0.0.1:8899/
  ok    us1-02-list-counts.png              14.77%  #events  http://127.0.0.1:8899/
  ok    us1-03-span.png                     11.24%  #span  http://127.0.0.1:8899/
  ok    us1-04-filter-narrows.png            1.99%  full page  http://127.0.0.1:8899/?filter=stefanik
  ok    us2-01-detail-T.png                  7.66%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us2-02-orderbook-at-T.png           24.86%  #orderbook  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us2-03-trades-around-T.png          25.28%  #trades  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us2-04-detail-default-moment.png      6.8%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578
  ok    us2-05-detail-second-moment.png       6.8%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408969447
  ok    us2-06-detail-first-instant.png      6.55%  full page  http://127.0.0.1:8899/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408790552
  ok    us3-01-store-unreachable.png         0.66%  full page  http://127.0.0.1:8898/
  ok    us3-02-empty-store.png                1.1%  full page  http://127.0.0.1:8897/
  ok    us3-03-empty-store-detail.png        3.83%  full page  http://127.0.0.1:8897/?event=will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578&moment=1791408879828
  ok    us3-04-event-with-no-rows.png        3.17%  full page  http://127.0.0.1:8899/?event=will-bernie-sanders-win-the-2028-democratic-presidential-nomination-879&moment=1791408879828

-- field for field: the UI against the store --
  match  [us1] us1-01-list.png painted            non-white 2.94%
  match  [us1] us1-02-list-counts.png painted     non-white 14.77%
  match  [us1] list.order_book rows == store      UI=360 store=360
  match  [us1] list.trade rows == store           UI=179 store=179
  match  [us1] us1-03-span.png painted            non-white 11.24%
  match  [us1] us1-04-filter-narrows.png painted  non-white 1.99%
  match  [us2] us2-01-detail-T.png painted        non-white 7.66%
  match  [us2] orderbook.at_T best_bid == store   UI=0.001 store=0.001
  match  [us2] orderbook.at_T best_ask == store   UI=0.002 store=0.002
  match  [us2] trades.around_T count == store     UI=20 store=20
  match  [us2] us2-02-orderbook-at-T.png painted  non-white 24.86%
  match  [us2] us2-03-trades-around-T.png painted non-white 25.28%
  match  [us2] us2-04-detail-default-moment.png painted non-white 6.8%
  match  [us2] us2-05-detail-second-moment.png painted non-white 6.8%
  match  [us2] us2-06-detail-first-instant.png painted non-white 6.55%
  match  [us3] us3-01-store-unreachable.png painted non-white 0.66%
  match  [us3] us3-02-empty-store.png painted     non-white 1.1%
  match  [us3] us3-03-empty-store-detail.png painted non-white 3.83%
  match  [us3] us3-04-event-with-no-rows.png painted non-white 3.17%
user-verify-m4: OK

Reverse runs — raw output

reverse-a.txt

FAIL us1-01-list.png                       1.1%  full page  missing: recorded span: 2026-10-07T21:33:10.552Z .. 2026-10-07T21:36:09.447Z; missing: will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578
FAIL us1-02-list-counts.png              32.54%  #events  missing: 360; missing: 179
FAIL us1-03-span.png                      4.25%  #span  missing: recorded span: 2026-10-07T21:33:10.552Z .. 2026-10-07T21:36:09.447Z
FAIL us1-04-filter-narrows.png            1.09%  full page  missing: events: 1/
ok   us2-01-detail-T.png                  3.83%  full page
user-verify-m4: FAIL — page.$eval: Failed to find element matching selector "#orderbook .best_bid"

reverse-b.txt

user-verify-m4: FAIL — playwright not found — the browser driver belongs to the image (mihail812-org/preinstall#105), not to this script; set PLAYWRIGHT_HOME to its install root

reverse-c.txt

user-verify-m4: FAIL — the collection store is not reachable at http://127.0.0.1:9999 — start it (deploy) first

reverse-d.txt

FAIL us1-01-list.png                         0%  full page  missing: Collected events; missing: order_book rows; missing: trade rows; missing: recorded span: 2026-10-07T21:33:10.552Z .. 2026-10-07T21:36:09.447Z; missing: will-shakhtar-donetsk-win-the-2026-27-uefa-champions-league-championship-20260701202025578; blank frame: 0.00% non-white
FAIL us1-02-list-counts.png                  0%  #events  missing: 360; missing: 179; missing selector #events; blank frame: 0.00% non-white
FAIL us1-03-span.png                         0%  #span  missing: recorded span: 2026-10-07T21:33:10.552Z .. 2026-10-07T21:36:09.447Z; missing selector #span; blank frame: 0.00% non-white
FAIL us1-04-filter-narrows.png               0%  full page  missing: events: 1/; blank frame: 0.00% non-white
FAIL us2-01-detail-T.png                     0%  full page  missing: Order book at a moment; missing: best_bid; missing: best_ask; missing: Trades around the moment; missing: moment T: 2026-10-07T21:34:39.828Z; missing: ±10.0s; blank frame: 0.00% non-white
user-verify-m4: FAIL — page.$eval: Failed to find element matching selector "#orderbook .best_bid"

Layers

What this run does not cover

Links