Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e0a328c
Merge branch 'feat/group-scopes' into integration/ndf-phase1
endrix Oct 22, 2026
4864c25
Merge branch 'feat/node-badges' into integration/ndf-phase1
endrix Oct 22, 2026
7844c7a
Merge branch 'feat/facts-section' into integration/ndf-phase1
endrix Oct 22, 2026
7eeda8c
Merge branch 'feat/read-only-profiles' into integration/ndf-phase1
endrix Oct 22, 2026
6b5b225
integration: restore the group constants dropped in the facts merge
endrix Oct 22, 2026
ca32b54
Merge branch 'feat/instance-name' into integration/ndf-phase1
endrix Oct 22, 2026
42006b4
Merge branch 'feat/edge-roles' into integration/ndf-phase1
endrix Oct 22, 2026
d32b20d
Merge branch 'feat/group-scopes' into integration/ndf-phase1
endrix Oct 23, 2026
5a16efa
Merge branch 'feat/group-scopes' into integration/ndf-phase1
endrix Oct 23, 2026
e0f135f
Merge branch 'feat/family-colours' into integration/ndf-phase1
endrix Oct 23, 2026
4776bf5
Merge branch 'feat/family-colours' into integration/ndf-phase1
endrix Oct 23, 2026
2b53a66
Merge branch 'feat/default-layout' into integration/ndf-phase1
endrix Oct 23, 2026
af91377
Merge branch 'feat/elk-server' into integration/ndf-phase1
endrix Oct 23, 2026
3640bbd
Merge branch 'feat/elk-server' into integration/ndf-phase1
endrix Oct 23, 2026
590404f
Merge branch 'feat/panel-links' into integration/ndf-phase1
endrix Oct 23, 2026
167c71a
Merge branch 'feat/panel-links' into integration/ndf-phase1
endrix Oct 23, 2026
3011686
Merge branch 'feat/facts-section' into integration/ndf-phase1
endrix Oct 23, 2026
13f655b
Merge branch 'feat/panel-links' into integration/ndf-phase1
endrix Oct 23, 2026
b2f0533
integration: a producer's Fields before its Facts
endrix Oct 23, 2026
a620415
Merge branch 'feat/status-overlay' into integration/coverage
endrix Oct 23, 2026
100a339
render-svg: --states, node states drawn through the client's own painter
endrix Oct 23, 2026
63c7e90
Merge branch 'feat/outline-rows' into integration/ndf-phase1
endrix Oct 23, 2026
7dadb25
Merge branch 'feat/outline-rows' into integration/ndf-phase1
endrix Oct 23, 2026
9d4a4d7
Merge branch 'feat/panel-links' into integration/ndf-phase1
endrix Oct 23, 2026
be82340
Merge branch 'feat/outline-rows' into integration/ndf-phase1
endrix Oct 23, 2026
a333b7d
Merge branch 'feat/outline-rows' into integration/ndf-phase1
endrix Oct 23, 2026
aeb17aa
Merge branch 'feat/read-only-views' into integration/ndf-phase1
endrix Oct 23, 2026
cfa07f0
Merge branch 'feat/captions-once' into integration/ndf-phase1
endrix Oct 23, 2026
11a2e52
Merge branch 'feat/client-stylesheets' into integration/ndf-phase1
endrix Oct 23, 2026
f356ad4
Merge branch 'feat/group-scopes' into integration/ndf-phase1
endrix Oct 23, 2026
094dcd0
Merge branch 'feat/edge-roles' into integration/ndf-phase1
endrix Oct 23, 2026
97229e4
Merge branch 'feat/elk-server' into integration/ndf-phase1
endrix Oct 23, 2026
ea298f8
Merge branch 'feat/panel-links' into integration/ndf-phase1
endrix Oct 23, 2026
df7c1da
Merge branch 'feat/status-overlay' into integration/ndf-phase1
endrix Oct 23, 2026
c02f6bf
Merge branch 'feat/outline-rows' into integration/ndf-phase1
endrix Oct 23, 2026
e179c5c
Merge branch 'feat/read-only-views' into integration/ndf-phase1
endrix Oct 23, 2026
7530d34
integration: the changelog as main will have it (the union merge kept…
endrix Oct 23, 2026
0e9ed5d
Merge remote-tracking branch 'origin/main' into integration/ndf-phase1
endrix Oct 24, 2026
6a9f766
Merge branch 'fix/export-run-ring' into integration/ndf-phase1
endrix Oct 24, 2026
d8d7b6a
Merge remote-tracking branch 'origin/main' into integration/ndf-phase1
endrix Oct 24, 2026
5c3c771
Merge remote-tracking branch 'origin/main' into integration/ndf-phase1
endrix Oct 24, 2026
be94844
Merge remote-tracking branch 'origin/release/0.8.0' into integration/…
endrix Oct 24, 2026
9bc0276
Merge origin/main (release 0.8.0, #108) into integration
endrix Oct 24, 2026
086e869
Merge origin/feat/selection-trace (#109) into integration
endrix Oct 30, 2026
ca72442
Merge origin/main (#109) into integration
endrix Oct 30, 2026
1c7dc4d
Merge origin/feat/elk-server-0.9 (#110) into integration
endrix Oct 30, 2026
fb1ffcc
Merge branch 'feat/elk-server-0.9' into integration/ndf-phase1
endrix Oct 30, 2026
eef6a24
Merge remote-tracking branch 'origin/main' into integration/ndf-phase1
endrix Oct 30, 2026
487c603
fix: the run overlay holds steady through a run; the eye takes the to…
endrix Nov 4, 2026
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ platform API version.
`elk-server-0.3.0-all.jar`**, and the server now needs **Java 17** (ELK 0.10
is built for it); without it layouts stay on elkjs, as before.

### Fixed

- **The running ring and the queue sizes no longer blink through a run.** The
runtime writes `run.wf-viewer.live.json` at every firing and the server
re-reads it every 400ms, so now and then it read the file halfway written,
took that for no run at all and fell through to the previous run: the ring
went off, the previous run's queue sizes came on, and the next read put
things back. A run-output file that does not parse is now read as it last
did (the queue trace too), and from the end of a run until its record is in
`run-log.jsonl` its last live overlay stands, as finished, where the
previous run showed for a moment.
- **The eye on the palette, off, takes the run's tokens and queue sizes off
the edges.** It took only the step counter away: the carried colour, the
token pills and the sizes stayed, so it looked as if it did nothing. The
nodes keep the run's state (the ring is the live run, with its own
setting). A queue size the trace no longer has is taken off the edge too.

### Tooling

- `scripts/render-svg --select <id>` selects an element (the diagram's id or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ function patchHeaderTools(headerTools: HTMLElement): void {
queueIcon.classList.toggle('codicon-eye', visible);
queueIcon.classList.toggle('codicon-eye-closed', !visible);
queueIcon.classList.toggle('cal-queue-hidden-icon', !visible);
queueIcon.title = visible ? 'Hide queue overlay' : 'Show queue overlay';
queueIcon.title = visible ? 'Hide what the run put on the edges (tokens, queue sizes)' : 'Show what the run put on the edges (tokens, queue sizes)';
queueIcon.setAttribute('aria-label', queueIcon.title);
};

Expand Down
143 changes: 102 additions & 41 deletions packages/diagram-server/src/server/source-model-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ export function countCoincidentNodes(positions: Iterable<{ x: number; y: number
* Source model storage for Workflow diagrams.
* Acquires the graph via the injected DiagramModelSource seam and transforms it to GModel.
*/
/** Take off an edge what a run put on it: the token it carried, its queue size at a step. */
function clearEdgeOverlay(edge: GEdge): void {
const args = edge.args as Record<string, unknown> | undefined;
if (args) {
delete args[WorkflowDiagramMetadata.VIEWER_LAST_TOKEN];
delete args[WorkflowDiagramMetadata.QUEUE_SIZE];
}
}

/** Call `fn` on every edge under `element`. */
function walkGEdges(element: { children?: unknown[] }, fn: (edge: GEdge) => void): void {
for (const child of element.children ?? []) {
if (child instanceof GEdge) {
fn(child);
}
walkGEdges(child as { children?: unknown[] }, fn);
}
}

@injectable()
export class WorkflowSourceModelStorage implements SourceModelStorage {

Expand Down Expand Up @@ -195,6 +214,24 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
*/
private readonly runLogEntryCache = new Map<string, { mtimeMs: number; size: number; entries: ReadonlyArray<Record<string, unknown>> }>();

/**
* What each run-output JSON file last parsed to, by path, keyed on the file's size and mtime
* (see {@link readJsonFileMemo}). A run writes `run.wf-viewer.live.json` at every firing and the
* diagram re-reads it every 400ms, so now and then it reads the file halfway written, which does
* not parse. That used to count as no file: the loader fell through to the previous run, the
* ring went off and the previous run's queue sizes came on, and the next read put things back.
* A file that does not parse is now read as it last did.
*/
private readonly jsonFileMemo = new Map<string, { mtimeMs: number; size: number; value: unknown }>();

/**
* The live overlay last read from each run-output dir, as the file held it. When a run ends
* its live file goes (or says it is not running) before the run's record is in `run-log.jsonl`,
* and in between the loader found only the previous run. While the log has no record of the
* run, its last live overlay stands, as finished.
*/
private readonly lastLiveOverlay = new Map<string, unknown>();

/**
* Short-TTL memo for the run-output *workspace* fallback scans (see
* {@link findRunOutputFallbackFiles}). The fallback only fires when the primary per-out-dir scan
Expand Down Expand Up @@ -1076,6 +1113,7 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
console.log(`[WorkflowSourceModelStorage] applyViewerOverlay: no overlay found for workflowFile=${workflowFilePath} workflow=${workflowName}`);
// Clear any stale glow state left from a previous cycle.
this.clearExecutionGlowState(root);
walkGEdges(root, clearEdgeOverlay);
return;
}
const activeNamesLog = overlay.active
Expand All @@ -1093,6 +1131,10 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
const queueTrace = queueTraceVisible === false
? undefined
: await this.tryLoadQueueTraceAtStep(overlay.outDir, queueTraceStep);
// The eye on the palette, off: nothing the run put on the edges is drawn, neither the
// token an edge carried nor the queue size at a step. The nodes keep the run's state;
// the ring is the live run, with its own setting.
const edgeOverlayHidden = queueTraceVisible === false;
if (queueTrace) {
(root as any).args['wf:queueTraceStep'] = queueTrace.selectedStep;
(root as any).args['wf:queueTraceStepCount'] = queueTrace.stepCount;
Expand Down Expand Up @@ -1219,7 +1261,9 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
if (!element) {
return;
}
if (element instanceof GEdge) {
if (element instanceof GEdge && edgeOverlayHidden) {
clearEdgeOverlay(element);
} else if (element instanceof GEdge) {
const args = element.args as Record<string, unknown> | undefined;
let queueSizeForEdge: number | undefined;
let queueTokenForEdge: unknown;
Expand Down Expand Up @@ -1277,6 +1321,8 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
if (queueSizeForEdge !== undefined) {
element.args = element.args || {};
(element.args as any)[WorkflowDiagramMetadata.QUEUE_SIZE] = queueSizeForEdge;
} else if (element.args) {
delete (element.args as any)[WorkflowDiagramMetadata.QUEUE_SIZE];
}
}
if (element instanceof GNode) {
Expand Down Expand Up @@ -2188,6 +2234,39 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
return runs;
}

/**
* The JSON a run-output file holds, through {@link jsonFileMemo}: `undefined` when there is no
* file. A file that fails to parse is one being written, and is answered with what it last
* parsed to, or `undefined` when it never did.
*/
private async readJsonFileMemo(filePath: string): Promise<unknown> {
const fs = await import('fs/promises');
let stat: import('fs').Stats;
try {
stat = await fs.stat(filePath);
} catch {
this.jsonFileMemo.delete(filePath);
return undefined;
}
const cached = this.jsonFileMemo.get(filePath);
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
return cached.value;
}
let value: unknown;
try {
value = JSON.parse(await fs.readFile(filePath, 'utf-8'));
} catch {
return cached?.value;
}
this.jsonFileMemo.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, value });
return value;
}

/** Whether `run-log.jsonl` at `runLogPath` records the run `runId`. */
private async runLogHasRun(runLogPath: string, runId: string): Promise<boolean> {
return (await this.readRunLogEntries(runLogPath)).some(entry => entry.runId === runId);
}

private async tryLoadQueueTraceAtStep(
outDir: string,
requestedStep?: number
Expand All @@ -2207,23 +2286,10 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
lastToken?: unknown;
}>;
} | undefined> {
const fs = await import('fs/promises');
const path = await import('path');

const queueTracePath = path.join(outDir, 'run.wf-queues.json');
let text: string;
try {
text = await fs.readFile(queueTracePath, 'utf-8');
} catch {
return undefined;
}

let trace: any;
try {
trace = JSON.parse(text);
} catch {
return undefined;
}
const trace: any = await this.readJsonFileMemo(queueTracePath);

if (!trace || typeof trace !== 'object' || trace.version !== 1 || !Array.isArray(trace.steps) || trace.steps.length === 0) {
return undefined;
Expand Down Expand Up @@ -2335,30 +2401,18 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
return typeof candidate === 'string' && wantWorkflowNames.has(candidate.trim());
};

const readOverlayFromPath = async (
overlayPath: string,
requireRunning = false
): Promise<{
type ViewerOverlay = {
runId: string;
outDir: string;
running: boolean;
edges: Record<string, { lastToken?: unknown }>;
active?: Array<{ entityInstanceName?: string; entityInstancePath?: string[] }>;
error?: { entityInstanceName?: string; entityInstancePath?: string[]; message?: string };
} | undefined> => {
let overlayText: string;
try {
overlayText = await fs.readFile(overlayPath, 'utf-8');
} catch {
return undefined;
}
};

let overlay: any;
try {
overlay = JSON.parse(overlayText);
} catch {
return undefined;
}
// The overlay `overlay` (a file's JSON) is, for this view: undefined when it is not one, or
// is another run's, another workflow's, or (`requireRunning`) a finished run's.
const acceptOverlay = (overlay: any, overlayPath: string, requireRunning = false): ViewerOverlay | undefined => {
if (!overlay || typeof overlay !== 'object') {
return undefined;
}
Expand Down Expand Up @@ -2425,14 +2479,10 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
};
};

const readOverlay = async (entry: { workflowName?: string; sourcePath?: string; outDir?: string; runId?: string }): Promise<{
runId: string;
outDir: string;
running: boolean;
edges: Record<string, { lastToken?: unknown }>;
active?: Array<{ entityInstanceName?: string; entityInstancePath?: string[] }>;
error?: { entityInstanceName?: string; entityInstancePath?: string[]; message?: string };
} | undefined> => {
const readOverlayFromPath = async (overlayPath: string, requireRunning = false): Promise<ViewerOverlay | undefined> =>
acceptOverlay(await this.readJsonFileMemo(overlayPath), overlayPath, requireRunning);

const readOverlay = async (entry: { workflowName?: string; sourcePath?: string; outDir?: string; runId?: string }): Promise<ViewerOverlay | undefined> => {
if (preferredRunId && entry.runId !== preferredRunId) {
return undefined;
}
Expand Down Expand Up @@ -2464,11 +2514,22 @@ export class WorkflowSourceModelStorage implements SourceModelStorage {
for (const baseOutDir of baseOutDirs) {
if (liveGlowEnabled) {
const liveOverlayPath = path.join(baseOutDir, 'run.wf-viewer.live.json');
const liveOverlay = await readOverlayFromPath(liveOverlayPath, true);
const liveJson = await this.readJsonFileMemo(liveOverlayPath);
const liveOverlay = acceptOverlay(liveJson, liveOverlayPath, true);
console.log(`[WorkflowSourceModelStorage] viewer live overlay: ${liveOverlayPath} found=${!!liveOverlay}`);
if (liveOverlay) {
this.lastLiveOverlay.set(baseOutDir, liveJson);
return liveOverlay;
}
// The run has ended: its live file says so, or is gone. Until the log records the
// run the loader would find the previous one, so the run's last live overlay
// stands, finished and with nothing active (what the run's final overlay says
// of it comes with the record).
const ended = acceptOverlay(liveJson ?? this.lastLiveOverlay.get(baseOutDir), liveOverlayPath);
if (ended && !(await this.runLogHasRun(path.join(baseOutDir, 'run-log.jsonl'), ended.runId))) {
console.log(`[WorkflowSourceModelStorage] viewer live overlay: run ${ended.runId} ended, not yet in the log`);
return { ...ended, running: false, active: undefined };
}
}
const runLogPath = path.join(baseOutDir, 'run-log.jsonl');
const entries = await this.readRunLogEntries(runLogPath);
Expand Down
Loading
Loading