Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/stack/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ static HTML file with a `<script>` tag, no framework, no build step. It gets
its data from a live WebSocket the relay pushes to, renders a handful of
tables, and that's it. A second file, `infra/status-relay/scrubber.js`
(copy-and-adapted from the paper demo's `docs/javascripts/scrubber-demo.js`,
deliberately not shared with it), adds a rewind view over one Run's pushed
history, fetched with a plain `fetch()` against the relay's own two REST
reads rather than the live socket. Still no framework, no build step, no
bundler: two plain scripts.
deliberately not shared with it), renders a rewind view fetched with a
plain `fetch()` against the relay's own REST reads rather than the live
socket. Still no framework, no build step, no bundler: two plain scripts.

`scrubber.js` itself knows nothing about Runs or any other CORA domain: it
renders a subject-neutral "timeline document" (a time domain plus a list of
marker/series lanes), so a later lens over a different kind of subject is a
server-side change, not a frontend one. Today the only document producer is
`page.html`'s own `runHistoryToTimelineDocument`, a small adapter bridging
the still-Run-shaped `run_history` wire payload into that document shape;
it is expected to move server-side and disappear once the producer emits
documents directly.

Next.js was picked below for a real multi-page application with routing,
shared component state, and a build pipeline; this page has none of those.
Expand Down
76 changes: 75 additions & 1 deletion infra/status-relay/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,74 @@ <h2>Rewind</h2>
rewindEmptyEl.style.display = runs.length === 0 ? "block" : "none";
}

// Maps a RunHistoryEvent's event_type to the state it puts the Run into,
// for the scrubber's generic fold: a state-less event (e.g. campaign
// membership) rides the lifecycle lane as a marker but does not move
// the "current status" the readout shows. Bridge only: once the
// producer emits timeline documents directly (see the status-relay
// design notes), this mapping and the function below move server-side
// and this adapter is deleted.
var RUN_EVENT_STATE = {
RunStarted: "active",
RunResumed: "active",
RunHeld: "paused",
RunCompleted: "ended",
RunAborted: "ended",
RunStopped: "ended",
RunTruncated: "ended",
};

function runHistoryToTimelineDocument(history) {
var events = history.events || [];
var observations = history.observations || [];

var domainFrom = events.length ? events[0].occurred_at : null;
var domainTo = events.length ? events[events.length - 1].occurred_at : null;
observations.forEach(function (o) {
if (domainFrom === null || o.sampled_at < domainFrom) domainFrom = o.sampled_at;
if (domainTo === null || o.sampled_at > domainTo) domainTo = o.sampled_at;
});

var lifecycleLane = {
lane_id: "lifecycle",
label: "Lifecycle",
render: "markers",
points: events.map(function (e) {
return { t: e.occurred_at, label: e.event_type, state: RUN_EVENT_STATE[e.event_type] || null };
}),
};

var byChannel = {};
var channelOrder = [];
observations.forEach(function (o) {
if (!byChannel[o.channel_name]) {
byChannel[o.channel_name] = [];
channelOrder.push(o.channel_name);
}
byChannel[o.channel_name].push(o);
});

var channelLanes = channelOrder.map(function (name) {
return {
lane_id: "channel:" + name,
label: name,
render: "series",
points: byChannel[name].map(function (o) {
return { t: o.sampled_at, value: o.value, text: o.categorical_value };
}),
};
});

return {
subject_lane_id: "lifecycle",
title: history.name,
subtitle: history.status,
domain: { from: domainFrom, to: domainTo },
lanes: [lifecycleLane].concat(channelLanes),
truncated: { observations: !!history.observations_truncated },
};
}

function showRewind(runId) {
fetch("/run-history/" + encodeURIComponent(runId))
.then(function (resp) {
Expand All @@ -503,7 +571,10 @@ <h2>Rewind</h2>
})
.then(function (history) {
rewindStageEl.textContent = "";
window.CoraScrubber.mount(rewindStageEl, history);
window.CoraScrubber.mount(rewindStageEl, runHistoryToTimelineDocument(history), {
chromeTitle: "Rewind",
sliderLabel: "Fold cursor: time within the run",
});
rewindBackEl.style.display = "inline-block";
})
.catch(function (err) {
Expand All @@ -512,6 +583,9 @@ <h2>Rewind</h2>
}

function backToLive() {
if (rewindStageEl._coraScrubberCleanup) {
rewindStageEl._coraScrubberCleanup();
}
rewindPickerEl.value = "";
rewindStageEl.textContent = "";
rewindBackEl.style.display = "none";
Expand Down
Loading
Loading