Skip to content

DTaaS viz: live + replay dashboard (Explorer and standalone) and architecture figure - #3

Merged
andre-merzky merged 22 commits into
develfrom
feature/dtaas-viz
Aug 24, 2026
Merged

DTaaS viz: live + replay dashboard (Explorer and standalone) and architecture figure#3
andre-merzky merged 22 commits into
develfrom
feature/dtaas-viz

Conversation

@andre-merzky

Copy link
Copy Markdown
Member

Migrated from BenCarter44#7 as part of the move to this org repo.
Same branch, same commits. Now stacked properly: base is feature/dtaas-m3, so the diff below is
this milestone's own delta rather than the cumulative stack.

Note: these branches predate the recent merges/additions on upstream main. A rebase of the
full stack onto current main already exists and is tested (158 unit / 29 integration green):
branch feature/dtaas-rebased. It replaces this stack once review here settles.

Live + replay visualization dashboard for the DT service, plus a slide-ready architecture figure.

Stacked on #6 (M3) → #5#4#3 — review from commit 58d4be4 onward, or merge in order.

What's in here

  • One JS implementation, two hosts: src/digitaltwin/service/ui/dt_dash.js (vanilla, no build step, canvas idiom after the AmSC deck's flow diagrams) mounted by a standalone page (works from file://, self-demos with a bundled real 61 s recording) and by the ORBIT Explorer via PluginDT.ui_module (verified end-to-end in headless Chrome against a live broker).
  • What it shows: role lanes (client sessions | broker/dt-plugin | HPC task | HPC exsitu), twin cards with color-coded states and last errors, inferred create/destroy verb arcs, real task tiles from rhapsody SSE events, per-criterion convergence bars (value, target tick, sparkline), stream pulses under the orbit data plane, hover tooltips.
  • Live wiring: 1 Hz admin/sessions poll (chained, abort-bounded, ordered) + SSE with reconnect backoff and task TTLs; record button captures a bounded replay file (stream payloads stripped); replay uses the play/pause/speed controls.
  • Python side (small, read-only): per-twin metrics dict fed from ROSE's IterationState (significant-figure rounding — a 1e-8 threshold survives the wire), per-session endpoints map, plugin ui routes with an allow-list (encoded-traversal test-pinned).
  • Figure: docs/dtaas-architecture.svg — high-level architecture in the deck's idiom with consistent per-layer colors.
  • Tests: 164 (126 baseline + 38), including recording-schema pins that keep the JS and the bundled sample in sync.

Internally reviewed (request-changes round applied in full; security review of the new routes clean). Upstream notes recorded in the README: task→twin attribution needs a rhapsody payload field; ui_module is broker-hosted-only and ORBIT's ui_config/ui_module docs conflate the two; gateway sessions carry no x-orbit-src owner.

🤖 Generated with Claude Code

andre-merzky and others added 6 commits August 17, 2026 23:58
Four role lanes on one canvas: the client with a sub-lane per session,
the broker with one card per twin, and the task and ex-situ endpoints
grouped under an HPC-resources frame.  The lanes are roles rather than
hosts, so all four are drawn even when a deployment collapses them onto
one endpoint.

The data layer takes a stream of timestamped frames and folds them into
one world -- snapshots of `admin/sessions` and the gateway's SSE
notifications -- so it cannot tell a recording from a live stack.  This
commit brings only the recording half: `index.html` plays the bundled
`dt_sample.js` on load and loops it, and play/pause and the speed slider
act on the frames rather than on an animation drawn from them.

The sample is a real capture of a live stack (one broker hosting `dt`,
two rhapsody endpoints): a ROSE learner twin converging on its `rmse`
criterion, an in-situ twin created and closed mid-recording, and a twin
whose component crashes.  It is a .js file assigning one JSON object for
exactly one reason -- a classic <script src> loads from `file://` where
`fetch()` does not, which is what makes the page self-demo with nothing
running and no server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The data layer gains its second source: a 1 Hz `admin/sessions` poll and
the gateway's SSE feed, turned into the frames the replay half already
consumes -- so `rec` captures the live stream to a recording that plays
back through exactly the same model, and none of the renderer knows which
it is watching.

Live has to be same-origin with the broker, and that is not a choice.
The gateway's CORS allow-list holds a handful of localhost origins, and
the `orbit_broker_token` cookie an EventSource rides is SameSite=Strict:
a page opened from anywhere else cannot reach a live broker even holding
the right token.  So the plugin serves the page itself, at
`{namespace}/ui`, from an allow-list of assets -- `{asset}` is a
client-supplied path segment.

Two things the dashboard needs that nothing else did.  A learner now
mirrors its per-window criterion into a filtered, read-only `metrics`
dict -- value, threshold, the operator between them, whether the window
met it, the window count and a bounded history, and never the model,
which can be megabytes -- which `DTRuntime.metrics()` collects off the
graph by duck typing (the runtime must not have to know about ROSE) and
`twin_list` / `admin/sessions` carry per twin.  And a session summary now
names the endpoint behind each engine role, so the two HPC lanes can say
which hardware they are, and the ex-situ one can say when it aliases the
task one.

The create / destroy / state-change verbs on the arcs stay *inferred*
from the delta between two polls: in v1 nothing on the wire announces
them, and `twin_list` polling is the documented observation mechanism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ORBIT's plugin-UI machinery turns out to support arbitrary plugin JS: a
`Plugin.ui_module` path is read by `BrokerPluginHost.get_ui_modules()` and
served by the gateway at `/plugins/<plugin_name>.js`, which the Explorer
imports by convention and drops into a page of its own -- same origin,
same scope, no iframe and no CSP, so a canvas is entirely fine.  So the
`dt` plugin declares one.

`dt_explorer.js` is an adapter, not a second dashboard: it hands the
Explorer a template and then dynamically imports the very file the
standalone page loads with a <script src> (it has no imports and no
exports, so it is valid either way) and calls the same `mount()`, in
compact mode and live -- the Explorer page is already same-origin with
the broker, so the auth cookie is simply there.  It does not use the
Explorer's `onNotification` hook: that hook only delivers *this* plugin's
events, and the simulation tiles are rhapsody's, so the dashboard keeps
its own EventSource in both hosts.

One limitation to know about, since ORBIT's own docs present `ui_config`
and `ui_module` as interchangeable: `ui_module` is read on the broker
plugin host only.  Endpoint-hosted, the Explorer falls back to the
declarative `ui_config` tile and the page has to be opened at
`{namespace}/ui` instead.  The gateway also caches a plugin's JS for the
life of the broker process, which the plugin's own `ui/` route does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`round(x, 6)` counts decimal places, so a criterion threshold of 1e-8 --
an ordinary target -- reached the wire as 0.0, and the dashboard drew its
target tick at the bottom of the track.  Six *significant* figures
instead, in the value and the history alike, and infinities and NaN drop
out rather than becoming invalid JSON.  The history now stops at 24 to
match what the sparkline actually keeps.

Three things that had no ceiling:

- `rec` grew until the tab died.  Under the orbit data plane every stream
  message is an event on the same feed, so a recording left running on a
  chatty twin is unbounded by construction.  It now stops at a frame cap
  and hands over what it captured, and a stream frame keeps its topic but
  not its payload -- the topic is what draws the pulse; the payload is a
  cloudpickled blob no replay can read.
- A task whose terminal event fell into an SSE reconnect gap pulsed
  RUNNING forever, holding its tile and its place in the tally.  Tasks
  are now aged out on silence, which trades a task running longer than
  the TTL going undrawn for a slot that always comes back.
- The Explorer removes a page's node on disconnect without telling the
  module that drew it, so every reconnect cycle left another poll,
  EventSource and RAF loop running against a canvas nobody can see.  The
  frame loop notices it has been detached and tears itself down.  Checked
  against the real thing: one dashboard polls 6 times in 6 s, and so does
  one that has been through a disconnect/reconnect cycle.

The poll was a `setInterval` with nothing stopping two from overlapping;
out-of-order snapshots read as twins vanishing and coming straight back,
complete with inferred arcs.  It chains from completion now, carries a
request deadline, and drops a response that lands after its source was
replaced.  A CLOSED EventSource is re-opened with backoff -- the browser
only reconnects one that is still CONNECTING, and a dashboard that has
silently stopped seeing tasks looks like an idle service.

Also: the stream topic's terminator is a NUL, not the pipe the pulse
parser was stripping, so a dtype label kept a stray character; one
synthetic `dt_stream` frame in the sample (labelled as such -- the
recorded stack ran the zmq plane) now pins that path; percent-encoded
traversal cases on the asset route; a note against putting a broker token
in a URL; and a comment recording that a `Response` really is handled by
all three dispatch paths, since this is the one route here that does not
return JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deck-native dark idiom, level colors per the AmSC layer stack
(L5 DT / L4 ROSE / L3 AsyncFlow / L2.5 ORBIT / L2 Rhapsody / L1
resources), roles drawn as separate hosts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard used to guess which twin a task belonged to: the events
carry a uid, an endpoint and nothing else, so attribution went through
candidate narrowing, a session block and finally a drawn brace. It
could also lie -- a task whose twin stopped before its event arrived
was confidently credited to a sibling.

Ownership is now recorded where the task is submitted. The runtime
hooks its engine once and keeps a bounded ring of task uids per twin,
a ContextVar carries the owning twin into every child task, and an
instance wrapper on the learner's task registration covers the ROSE
side. twin_list reports the ring, and the dashboard joins events
against it: 99.6% of task arcs in the bundled trace, 100% in the
multi-session one, where every arc used to start at the broker frame.

That deletes the guessing subsystem entirely, and the dashboard
shrinks by about 550 lines. Arcs anchor two thirds down a card and bow
into the empty lane below the top-aligned grid, so a trail belongs
visibly to one twin. Lanes now resolve per session, which stops a
second session's ex-situ traffic from drawing on the task lane.

A stale copy of the dashboard cost a debugging round, so the header
carries its version and the page busts its own cache, with a test
keeping the two in step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
andre-merzky and others added 3 commits August 19, 2026 12:23
Every engine's `OrbitExecutionBackend` registers with the broker as a
participant, and the broker draws it in the topology -- until now as an
anonymous `rhapsody.<uuid>`, two of them per dual-engine session,
unexplained.  Now the session names them for what they are:
`rhapsody.<session>.<role>`.  Uniqueness holds by construction (one
engine per role per session; `engine` caches and `_lost` forbids
rebuilds), and the kwarg is signature-guarded so a rhapsody without the
new `participant_name` parameter keeps working unchanged.

The rhapsody half is fix/orbit-participant-name @ 6b00e14.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything a host needs to join the live demo, checked in where the
demo lives:

  deploy/install.sh      one pinned install for every role.  Python 3.12
                         on every host (the wire contract pins the
                         minor); asyncflow, rhapsody[telemetry], ROSE
                         pinned as git installs -- naive PyPI resolution
                         yields a rhapsody without the orbit backend, an
                         asyncflow without the non-main-thread fix, and
                         an unrelated package named `rose`; orbit rides
                         its 0.5.0 release.  A fail-fast sanity block
                         proves all of it at install time.
  deploy/run-broker.sh   broker + dt plugin; exports its own broker URL
                         (twin creation needs it) and the orbit data
                         plane (the dashboard's pulses need it).
  deploy/run-endpoint.sh one rhapsody endpoint per role, tagged so task
                         bodies can report where they ran.
  test/12-dtaas-live/    the narrated two-phase driver (build, walk
                         away, reattach with only the sid, tear down --
                         session included), its twin components, and the
                         operator runbook with the failure table the
                         rehearsals actually filled.

Rehearsed end to end against a remote broker: twin round trip, ex-situ
learning across two endpoints, reattach, teardown to an empty service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andre-merzky and others added 5 commits August 19, 2026 12:33
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andre-merzky and others added 6 commits August 19, 2026 14:49
Two topology-legibility fixes in one server round:

  * Stream clients on the orbit data plane registered as anonymous
    `dt_stream.<uuid>` consumers -- one per twin, unexplained in the
    Explorer.  Now they are `dt_stream.<twin-prefix>.<suffix>` with the
    advertised role `stream`: matchable to a dashboard card at a glance.
    The suffix stays random because a namespace can legitimately carry
    two clients (the twin's own, plus a consumer which opened the twin's
    reported config).

  * The dashboard page at `{namespace}/ui` (no trailing slash) resolved
    its relative script against the parent path and fell back to "did
    not load".  The assets now answer next to the page as well as under
    it, so both spellings work; the runbook links the canonical one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twin B moves from step 6 to step 5: its first training window (~15s
after start) then lands inside the ten-call query span, and one talking
window shows the task-lane arcs, the exsitu tiles and the convergence
bar together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first cut read the asset name off `request.url`, which exists on
the ASGI path and not on the broker-hosted one: `BrokerPluginHost`
hands a request shim, and the route 500ed exactly where the demo
serves it.  Caught on a live broker, not by the unit suite -- the shim
dispatch has no test double here yet.  The name is per-route anyway,
so the handler now closes over it and touches the request not at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… looks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BenCarter44

Copy link
Copy Markdown
Collaborator

Thanks Andre, looks good!

But, before merging, I created a new iteration of the UI. Before merging this, I would like to merge #6 first into this. It captures the abstractions better and is updated to reflect changes from #1.

@andre-merzky
andre-merzky merged commit 654e28c into devel Aug 24, 2026
2 checks passed
@andre-merzky
andre-merzky deleted the feature/dtaas-viz branch August 24, 2026 11:16
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.

2 participants