(function () {
  const root = document.querySelector(".workbench");
  const fields = new Map(
    Array.from(document.querySelectorAll("[data-field]")).map((item) => [item.dataset.field, item])
  );
  const tabButtons = Array.from(document.querySelectorAll("[data-tab]"));
  const programSelect = document.querySelector("[data-program-select]");
  const frameSlider = document.querySelector("[data-frame-slider]");
  const frameTable = document.querySelector("[data-frame-table]");
  const stepTable = document.querySelector("[data-step-table]");
  const objectTree = document.querySelector("[data-object-tree]");
  const pathCurrent = document.querySelector("[data-path-current]");
  const parserProgress = document.querySelector("[data-parser-progress]");

  const fallbackManifest = {
    release_id: "local",
    suite_id: "working2-abb120-spec",
    job_id: "W2-JOB-latest",
    robot_id: "abb_irb120_3_58",
    report_html: "test-results/abb120-spec/W2-JOB-latest/report.html",
    programs: [
      {
        program_id: "W2_60_IOWaitPulse",
        file: "runtime/W2_60_IOWaitPulse.grl",
        source: "",
        coverage_level: "Runtime",
        status: "pass",
        spec_sections: [13],
        diagnostics: [],
        artifacts: {}
      }
    ]
  };

  const fallbackProgramData = {
    sourceText: [
      "language grl 0.1",
      "module W2IOWaitPulse",
      "  const speed v = joint(35 %)",
      "  const zone zf = fine",
      "  target home = joint_target { joints: [0 deg, 0 deg, 0 deg, 0 deg, 0 deg, 0 deg] }",
      "  path io_path {",
      "    defaults { speed: v, zone: zf }",
      "    point p0 movej home",
      "    event at p0 distance 5 + 5 mm pulse io.do[20] duration 50 + 50 ms",
      "  }",
      "  proc main()",
      "    io.do[1] = true",
      "    io.ao[1] = 0.75",
      "    io.go[1] = 3",
      "    wait all(io.di[1] == true, io.di[2] == false) timeout 1 s on_timeout alarm \"all timeout\"",
      "    wait any(io.di[3] == true, io.di[4] == true) timeout 1 s",
      "    wait rising(io.di[5]) timeout 1 s",
      "    wait falling(io.di[6]) timeout 1 s",
      "    wait changed(io.di[7]) timeout 1 s",
      "    pulse io.do[2] duration 50 + 50 ms",
      "    run_path io_path",
      "  end",
      "end"
    ].join("\n"),
    trajectory: {
      sampleCount: 4,
      duration: 0.24,
      startJoints: [0, 0, 0, 0, 0, 0],
      endJoints: [0.12, 0.03, -0.08, 0, 0.02, 0],
      frames: [
        frame(0, 0, "MOVEJ", "home", [0, 0, 0, 0, 0, 0], [0.374, 0, 0.63], { line: 8, column: 14 }),
        frame(1, 0.08, "MOVEJ", "home", [0.04, 0.01, -0.026667, 0, 0.006667, 0], [0.377333, 0, 0.63], { line: 8, column: 14 }),
        frame(2, 0.16, "MOVEJ", "home", [0.08, 0.02, -0.053333, 0, 0.013333, 0], [0.380667, 0, 0.63], { line: 8, column: 14 }),
        frame(3, 0.24, "MOVEJ", "home", [0.12, 0.03, -0.08, 0, 0.02, 0], [0.384, 0, 0.63], { line: 8, column: 14 })
      ],
      diagnostics: []
    },
    controller: {
      commands: [],
      snapshot: {
        runtime: {
          pc: 0,
          trace: []
        },
        state: {
          state: "stopped",
          diagnostics: []
        }
      }
    },
    motionQueue: {
      items: [
        {
          id: "mq_local",
          source: { kind: "path", pathId: "io_path", sourceMap: { line: 21, column: 5 } },
          request: {
            pathId: "io_path",
            segments: [{ id: "p0", motion: "MOVEJ", targetId: "home", sourceMap: { line: 8, column: 14 } }],
            events: [{ id: "event_0", timing: "at", pointId: "p0", kind: "pulse", sourceMap: { line: 9, column: 35 } }]
          }
        }
      ]
    },
    trace: [
      traceEntry(0, "io", "IO_WRITE", 12, 0),
      traceEntry(0, "io", "IO_WRITE", 13, 1),
      traceEntry(0, "io", "IO_WRITE", 14, 2),
      traceEntry(0, "wait", "WAIT", 15, 3, { status: "satisfied" }),
      traceEntry(0, "wait", "WAIT", 16, 4, { status: "satisfied" }),
      traceEntry(0.1, "wait", "WAIT", 17, 5, { status: "satisfied" }),
      traceEntry(0.1, "wait", "WAIT", 18, 6, { status: "satisfied" }),
      traceEntry(0.1, "wait", "WAIT", 19, 7, { status: "satisfied" }),
      traceEntry(0.1, "io", "PULSE", 20, 8),
      traceEntry(0.1, "motion", "RUN_PATH", 21, 9, { queued: true })
    ],
    io: { image: { "io.di[1]": false, "io.do[1]": false }, events: [] },
    compile: { moduleName: "W2IOWaitPulse", pathCount: 1, operationCount: 0 },
    postReport: { filenames: ["W2IOWaitPulse.mod", "W2IOWaitPulse.ls", "W2IOWaitPulse.src"], report: [] },
    roundtrip: {},
    isFallback: true,
    releaseId: "local",
    errors: []
  };

  const app = {
    manifest: fallbackManifest,
    programs: fallbackManifest.programs,
    selectedProgramId: "W2_60_IOWaitPulse",
    programCache: new Map([["W2_60_IOWaitPulse", fallbackProgramData]]),
    command: "ready",
    mode: "auto",
    motors: "on",
    tab: "grl",
    currentFrameIndex: 0,
    currentSampleIndex: 0,
    playbackTime: 0,
    playbackStartedAt: 0,
    playbackTimeStart: 0,
    playbackSampleIndex: 0,
    samplePeriodSec: 0.05,
    samplePeriodMs: 50,
    di1: false,
    do1: false,
    wait: "none",
    report: "W2-REPORT working2-abb120-spec",
    localTrace: ["W2-JOB-latest ready"],
    playbackTimer: 0,
    programLoads: new Map(),
    assetTextCache: new Map(),
    assetRefreshes: new Set(),
    parseStream: {
      active: false,
      timer: 0,
      programId: "",
      cursorLine: 0,
      totalLines: 0,
      linesPerTick: 1,
      startedAt: 0,
      completedProgramId: "",
      lastSummary: "idle"
    },
    storage: {
      mode: "checking",
      hits: 0,
      network: 0,
      writes: 0,
      errors: 0,
      last: "checking browser storage"
    }
  };

  window.__virtualController = app;
  window.__vcErrors = [];

  function frame(index, time, motion, targetId, joints, position, sourceMap) {
    return {
      index,
      time,
      motion,
      targetId,
      joints,
      tcp: {
        position,
        quaternion: [0, 0.707107, 0, 0.707107]
      },
      sourceMap,
      diagnostics: []
    };
  }

  function traceEntry(time, kind, instructionKind, line, pc, data) {
    return {
      time,
      kind,
      procedure: "main",
      pc,
      instructionKind,
      source: {
        kind: instructionKind,
        id: `main:${instructionKind}:${line}:5`,
        sourceMap: { line, column: 5 },
        procedureId: "main"
      },
      data,
      id: pc + 1
    };
  }

  function selectedProgram() {
    return app.programs.find((program) => program.program_id === app.selectedProgramId) ?? app.programs[0];
  }

  function cachedProgramData(programId = app.selectedProgramId) {
    return app.programCache.get(programId) ?? fallbackProgramData;
  }

  function selectedData() {
    const data = cachedProgramData();
    if (app.parseStream.active && app.parseStream.programId === app.selectedProgramId) {
      return projectedProgramData(data, app.parseStream.cursorLine);
    }
    return data;
  }

  function projectedProgramData(data, cursorLine) {
    const lines = (data.sourceText || fallbackProgramData.sourceText).split(/\r?\n/);
    const visibleLineCount = Math.min(Math.max(Math.round(numberOr(cursorLine, 0)), 0), lines.length);
    const sourceText = [
      ...lines.slice(0, visibleLineCount),
      ...(visibleLineCount < lines.length ? [`... parsing ${visibleLineCount}/${lines.length}`] : [])
    ].join("\n");

    return {
      ...data,
      sourceText,
      trajectory: projectedTrajectory(data.trajectory, visibleLineCount),
      controller: projectedController(data.controller, visibleLineCount),
      motionQueue: projectedMotionQueue(data.motionQueue, visibleLineCount),
      trace: projectedTrace(data.trace, visibleLineCount),
      postReport: visibleLineCount >= lines.length ? data.postReport : { filenames: data.postReport?.filenames ?? [], report: [] },
      isStreaming: true
    };
  }

  function projectedTrajectory(trajectory, cursorLine) {
    const sourceFrames = Array.isArray(trajectory?.frames) ? trajectory.frames : [];
    const visibleFrames = sourceFrames.filter((item) => numberOr(item?.sourceMap?.line, 0) <= cursorLine);
    const lastTime = numberOr(visibleFrames.at(-1)?.time, 0);
    return {
      sampleCount: visibleFrames.length,
      duration: lastTime,
      frames: visibleFrames,
      diagnostics: Array.isArray(trajectory?.diagnostics)
        ? trajectory.diagnostics.filter((item) => numberOr(item?.sourceMap?.line, 0) <= cursorLine)
        : []
    };
  }

  function projectedController(controller, cursorLine) {
    const trace = Array.isArray(controller?.snapshot?.runtime?.trace)
      ? controller.snapshot.runtime.trace.filter((item) => numberOr(item?.source?.sourceMap?.line, 0) <= cursorLine)
      : [];
    return {
      ...controller,
      snapshot: {
        ...(controller?.snapshot ?? {}),
        runtime: {
          ...(controller?.snapshot?.runtime ?? {}),
          trace
        }
      }
    };
  }

  function projectedTrace(trace, cursorLine) {
    return Array.isArray(trace)
      ? trace.filter((item) => numberOr(item?.source?.sourceMap?.line, 0) <= cursorLine)
      : [];
  }

  function projectedMotionQueue(motionQueue, cursorLine) {
    const sourceItems = Array.isArray(motionQueue?.items) ? motionQueue.items : [];
    const items = [];
    for (const item of sourceItems) {
      const parentLine = numberOr(item?.source?.sourceMap?.line ?? item?.source?.instruction?.sourceMap?.line, 0);
      const request = item.request ?? {};
      const segments = Array.isArray(request.segments)
        ? request.segments.filter((segment) => numberOr(segment?.sourceMap?.line, 0) <= cursorLine)
        : [];
      const events = Array.isArray(request.events)
        ? request.events.filter((event) => numberOr(event?.sourceMap?.line, 0) <= cursorLine)
        : [];
      if (parentLine > cursorLine && segments.length === 0 && events.length === 0) continue;
      items.push({
        ...item,
        request: {
          ...request,
          segments,
          events
        }
      });
    }
    return {
      ...motionQueue,
      items
    };
  }

  function frames() {
    const data = selectedData();
    return normalizeFrames(data.trajectory, data.isFallback === true && data.isStreaming !== true);
  }

  function trajectoryDuration(items = frames()) {
    const duration = Number(selectedData().trajectory?.duration);
    const lastTime = Number(items.at(-1)?.time ?? 0);
    const trace = selectedData().trace;
    const traceTime = Array.isArray(trace)
      ? trace.reduce((max, item) => Math.max(max, numberOr(item?.time, 0)), 0)
      : 0;
    return Math.max(
      0,
      Number.isFinite(duration) ? duration : 0,
      Number.isFinite(lastTime) ? lastTime : 0,
      traceTime
    );
  }

  function executionSamples() {
    const items = frames();
    const duration = trajectoryDuration(items);
    const step = app.samplePeriodSec;
    const times = [];

    if (duration <= 0) {
      times.push(0);
    } else {
      for (let time = 0; time < duration - 1e-9; time += step) {
        times.push(Number(time.toFixed(6)));
      }
      if (times.length === 0 || Math.abs(times.at(-1) - duration) > 1e-9) {
        times.push(Number(duration.toFixed(6)));
      }
    }

    return times.map((time, index) => interpolateSample(items, time, index));
  }

  function hasTrajectoryData(data) {
    const trajectory = data?.trajectory;
    return Array.isArray(trajectory?.frames) && trajectory.frames.length > 0
      || Array.isArray(trajectory?.startJoints)
      || Array.isArray(trajectory?.endJoints);
  }

  function currentFrame() {
    const samples = executionSamples();
    return samples[Math.min(app.currentSampleIndex, Math.max(samples.length - 1, 0))]
      ?? fallbackProgramData.trajectory.frames[0];
  }

  function currentSamples() {
    return executionSamples();
  }

  function syncFrameIndexFromTime(time = app.playbackTime) {
    const items = frames();
    if (items.length === 0) {
      app.currentFrameIndex = 0;
      return;
    }
    let index = 0;
    for (let i = 0; i < items.length; i += 1) {
      if (items[i].time <= time + 1e-9) index = i;
      else break;
    }
    app.currentFrameIndex = index;
  }

  function setSampleIndex(index) {
    const samples = currentSamples();
    const total = samples.length;
    app.currentSampleIndex = Math.min(Math.max(Math.round(numberOr(index, 0)), 0), Math.max(total - 1, 0));
    app.playbackTime = samples[app.currentSampleIndex]?.time ?? 0;
    syncFrameIndexFromTime(app.playbackTime);
  }

  function setPlaybackTime(time) {
    const samples = currentSamples();
    if (samples.length === 0) {
      app.currentSampleIndex = 0;
      app.playbackTime = 0;
      syncFrameIndexFromTime(0);
      return;
    }
    const duration = samples.at(-1).time;
    const clamped = Math.min(Math.max(numberOr(time, 0), 0), duration);
    const sampleIndex = Math.min(Math.round(clamped / app.samplePeriodSec), samples.length - 1);
    app.currentSampleIndex = sampleIndex;
    app.playbackTime = samples[sampleIndex]?.time ?? clamped;
    syncFrameIndexFromTime(app.playbackTime);
  }

  function resetExecutionClock() {
    app.currentSampleIndex = 0;
    app.currentFrameIndex = 0;
    app.playbackTime = 0;
    app.playbackStartedAt = 0;
    app.playbackTimeStart = 0;
    app.playbackSampleIndex = 0;
  }

  function syncExecutionClockToLatestSample() {
    const samples = currentSamples();
    app.currentSampleIndex = Math.max(samples.length - 1, 0);
    app.playbackTime = samples.at(-1)?.time ?? 0;
    syncFrameIndexFromTime(app.playbackTime);
  }

  function interpolateSample(items, time, sampleIndex) {
    if (!Array.isArray(items) || items.length === 0) {
      return frame(sampleIndex, time, "NO_MOTION", "no planned segment", [0, 0, 0, 0, 0, 0], [0, 0, 0]);
    }

    const first = items[0];
    if (time <= first.time) return sampleFromFrame(first, time, sampleIndex);

    const last = items.at(-1);
    if (time >= last.time) return sampleFromFrame(last, time, sampleIndex);

    let nextIndex = items.findIndex((item) => item.time >= time);
    if (nextIndex <= 0) nextIndex = 1;
    const previous = items[nextIndex - 1];
    const next = items[nextIndex];
    const span = Math.max(next.time - previous.time, 1e-9);
    const ratio = Math.min(Math.max((time - previous.time) / span, 0), 1);
    const source = ratio < 1 ? previous : next;

    return {
      index: sampleIndex,
      time,
      motion: source.motion || next.motion || previous.motion || "MOVEJ",
      segmentId: source.segmentId ?? next.segmentId ?? previous.segmentId,
      targetId: source.targetId ?? next.targetId ?? previous.targetId,
      joints: interpolateArray(previous.joints, next.joints, ratio),
      tcp: {
        position: interpolateArray(previous.tcp.position, next.tcp.position, ratio),
        quaternion: normalizeQuaternion(interpolateArray(previous.tcp.quaternion, next.tcp.quaternion, ratio))
      },
      sourceMap: source.sourceMap,
      trajectoryFrameIndex: source.index,
      diagnostics: [...(previous.diagnostics ?? []), ...(next.diagnostics ?? [])]
    };
  }

  function sampleFromFrame(item, time, sampleIndex) {
    return {
      ...item,
      index: sampleIndex,
      time,
      joints: [...item.joints],
      tcp: {
        position: [...item.tcp.position],
        quaternion: [...item.tcp.quaternion]
      },
      trajectoryFrameIndex: item.index
    };
  }

  function interpolateArray(start, end, ratio) {
    const a = Array.isArray(start) ? start : [];
    const b = Array.isArray(end) ? end : [];
    return Array.from({ length: Math.max(a.length, b.length) }, (_item, index) => {
      const left = numberOr(a[index], 0);
      const right = numberOr(b[index], left);
      return left + (right - left) * ratio;
    });
  }

  function normalizeQuaternion(values) {
    const q = padNumbers(values, 4, [0, 0.707107, 0, 0.707107]);
    const length = Math.hypot(q[0], q[1], q[2], q[3]);
    if (length <= 1e-9) return [0, 0.707107, 0, 0.707107];
    return q.map((value) => value / length);
  }

  function normalizeFrames(trajectory, allowFallback) {
    if (Array.isArray(trajectory?.frames) && trajectory.frames.length > 0) {
      return trajectory.frames.map((item, index) => ({
        index: numberOr(item.index, index),
        time: numberOr(item.time, index * 0.02),
        motion: item.motion || "MOVEJ",
        segmentId: item.segmentId,
        targetId: item.targetId,
        joints: padNumbers(item.joints, 6),
        tcp: {
          position: padNumbers(item.tcp?.position, 3),
          quaternion: padNumbers(item.tcp?.quaternion, 4, [0, 0.707107, 0, 0.707107])
        },
        sourceMap: item.sourceMap,
        diagnostics: Array.isArray(item.diagnostics) ? item.diagnostics : []
      }));
    }

    if (Array.isArray(trajectory?.startJoints) || Array.isArray(trajectory?.endJoints)) {
      return [
        frame(0, 0, "MOVEJ", "start", padNumbers(trajectory.startJoints, 6), [0.374, 0, 0.63]),
        frame(1, numberOr(trajectory.duration, 0), "MOVEJ", "end", padNumbers(trajectory.endJoints, 6), [0.384, 0, 0.63])
      ];
    }

    if (allowFallback) return fallbackProgramData.trajectory.frames;
    return [frame(0, 0, "NO_MOTION", "no planned segment", [0, 0, 0, 0, 0, 0], [0, 0, 0])];
  }

  function padNumbers(values, length, fallback) {
    const source = Array.isArray(values) ? values : fallback ?? [];
    return Array.from({ length }, (_item, index) => numberOr(source[index], 0));
  }

  function numberOr(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) ? number : fallback;
  }

  function sourceMapLine(value) {
    return numberOr(value?.sourceMap?.line ?? value?.source?.sourceMap?.line, 0);
  }

  function formatNumber(value, digits) {
    const fixed = Number(value).toFixed(digits);
    return fixed.replace(/\.?0+$/, "");
  }

  function formatArray(values, digits) {
    return `[${values.map((value) => formatNumber(value, digits)).join(", ")}]`;
  }

  function setField(name, value) {
    const item = fields.get(name);
    if (item) item.textContent = value;
  }

  function resolvePackageUrl(relativePath) {
    if (!relativePath) return "";
    return new URL(`../${relativePath.replace(/^\/+/, "")}`, window.location.href).href;
  }

  function packageCacheKey(relativePath) {
    if (!relativePath) return "";
    try {
      const url = new URL(resolvePackageUrl(relativePath));
      return `package:${url.origin}${url.pathname}`;
    } catch (_error) {
      return `package:${relativePath}`;
    }
  }

  function cacheFileName(key) {
    try {
      return window.btoa(key).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
    } catch (_error) {
      return key.replace(/[^a-z0-9_.-]/gi, "_").slice(0, 180);
    }
  }

  function shortName(value) {
    const name = String(value || "").split(/[?#]/)[0].split("/").pop() || String(value || "asset");
    return name.length > 48 ? `${name.slice(0, 45)}...` : name;
  }

  const opfs = {
    directory: undefined
  };

  async function opfsDirectory() {
    if (opfs.directory) return opfs.directory;
    opfs.directory = (async () => {
      if (!navigator.storage?.getDirectory) {
        app.storage.mode = "localStorage";
        app.storage.last = "OPFS unavailable";
        return null;
      }
      try {
        const rootDirectory = await navigator.storage.getDirectory();
        const directory = await rootDirectory.getDirectoryHandle("kdl-virtual-controller", { create: true });
        app.storage.mode = "OPFS";
        app.storage.last = "OPFS ready";
        return directory;
      } catch (error) {
        app.storage.mode = "localStorage";
        app.storage.errors += 1;
        app.storage.last = `OPFS blocked: ${error instanceof Error ? error.name : "error"}`;
        return null;
      }
    })();
    return opfs.directory;
  }

  async function readOpfsText(key) {
    const directory = await opfsDirectory();
    if (!directory) return undefined;
    try {
      const handle = await directory.getFileHandle(cacheFileName(key));
      const file = await handle.getFile();
      return await file.text();
    } catch (_error) {
      return undefined;
    }
  }

  async function writeOpfsText(key, text) {
    const directory = await opfsDirectory();
    if (!directory) return false;
    try {
      const handle = await directory.getFileHandle(cacheFileName(key), { create: true });
      const writable = await handle.createWritable();
      await writable.write(text);
      await writable.close();
      return true;
    } catch (error) {
      app.storage.errors += 1;
      app.storage.last = `OPFS write failed: ${error instanceof Error ? error.name : "error"}`;
      return false;
    }
  }

  function readLocalStorageText(key) {
    try {
      const value = window.localStorage?.getItem(`kdl-vc-cache:${key}`);
      return value ?? undefined;
    } catch (_error) {
      return undefined;
    }
  }

  function writeLocalStorageText(key, text) {
    try {
      if (text.length > 2_000_000) return false;
      window.localStorage?.setItem(`kdl-vc-cache:${key}`, text);
      return true;
    } catch (_error) {
      return false;
    }
  }

  async function readCachedTextByKey(key, label) {
    if (!key) return undefined;
    if (app.assetTextCache.has(key)) {
      app.storage.hits += 1;
      app.storage.last = `memory ${shortName(label ?? key)}`;
      return app.assetTextCache.get(key);
    }

    const opfsText = await readOpfsText(key);
    if (typeof opfsText === "string") {
      app.assetTextCache.set(key, opfsText);
      app.storage.hits += 1;
      app.storage.last = `OPFS ${shortName(label ?? key)}`;
      return opfsText;
    }

    const localStorageText = readLocalStorageText(key);
    if (typeof localStorageText === "string") {
      app.assetTextCache.set(key, localStorageText);
      app.storage.hits += 1;
      app.storage.last = `localStorage ${shortName(label ?? key)}`;
      return localStorageText;
    }

    return undefined;
  }

  async function writeCachedTextByKey(key, text, label) {
    if (!key || typeof text !== "string") return;
    app.assetTextCache.set(key, text);
    const opfsWritten = await writeOpfsText(key, text);
    const localStorageWritten = writeLocalStorageText(key, text);
    if (opfsWritten || localStorageWritten) {
      app.storage.writes += 1;
      app.storage.last = `${opfsWritten ? "OPFS" : "localStorage"} saved ${shortName(label ?? key)}`;
    }
  }

  async function fetchTextFromNetwork(relativePath, options = {}) {
    if (!relativePath) return undefined;
    const controller = typeof AbortController === "function" ? new AbortController() : undefined;
    const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 3500;
    const timeout = controller
      ? window.setTimeout(() => controller.abort(), timeoutMs)
      : 0;
    try {
      const response = await fetch(resolvePackageUrl(relativePath), {
        cache: "no-store",
        signal: controller?.signal
      });
      if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
      const text = await response.text();
      app.storage.network += 1;
      app.storage.last = `network ${shortName(relativePath)}`;
      void writeCachedTextByKey(packageCacheKey(relativePath), text, relativePath);
      return text;
    } catch (error) {
      app.storage.errors += 1;
      app.storage.last = `network ${shortName(relativePath)} ${error instanceof Error ? error.name : "error"}`;
      return undefined;
    } finally {
      if (timeout) window.clearTimeout(timeout);
    }
  }

  function refreshPackageAsset(relativePath, options = {}) {
    const key = packageCacheKey(relativePath);
    if (!key || app.assetRefreshes.has(key)) return;
    app.assetRefreshes.add(key);
    void fetchTextFromNetwork(relativePath, options).finally(() => {
      app.assetRefreshes.delete(key);
    });
  }

  async function optionalText(relativePath, options = {}) {
    if (!relativePath) return "";
    let triedNetwork = false;
    if (options.networkFirst) {
      triedNetwork = true;
      const text = await fetchTextFromNetwork(relativePath, options);
      if (typeof text === "string") return text;
    }

    const key = packageCacheKey(relativePath);
    const cached = await readCachedTextByKey(key, relativePath);
    if (typeof cached === "string") {
      if (!options.networkFirst && options.refresh !== false) refreshPackageAsset(relativePath, options);
      return cached;
    }

    if (triedNetwork) return "";
    const text = await fetchTextFromNetwork(relativePath, options);
    return text ?? "";
  }

  async function optionalJson(relativePath, options = {}) {
    const text = await optionalText(relativePath, options);
    if (!text) return undefined;
    try {
      return JSON.parse(text);
    } catch (error) {
      app.storage.errors += 1;
      app.storage.last = `JSON ${shortName(relativePath)} ${error instanceof Error ? error.name : "error"}`;
      return undefined;
    }
  }

  function applyManifest(manifest) {
    app.manifest = manifest;
    if (manifest.release_id !== "local") {
      for (const [programId, data] of app.programCache) {
        if (data.releaseId && data.releaseId !== manifest.release_id && data.releaseId !== "local") {
          app.programCache.delete(programId);
        }
      }
    }
    app.programs = Array.isArray(manifest.programs) && manifest.programs.length > 0
      ? manifest.programs
      : fallbackManifest.programs;
    app.selectedProgramId = chooseDefaultProgram(app.selectedProgramId);
    app.report = manifest.report_html ? `${manifest.job_id} report` : app.report;
    root?.setAttribute("data-robot", manifest.robot_id ?? "abb_irb120_3_58");
    root?.setAttribute("data-suite", manifest.suite_id ?? "working2-abb120-spec");
    root?.setAttribute("data-job", manifest.job_id ?? "W2-JOB-latest");
    populateProgramSelect();
  }

  function chooseDefaultProgram(current) {
    const ids = app.programs.map((program) => program.program_id);
    if (current && ids.includes(current)) return current;
    if (ids.includes("W2_60_IOWaitPulse")) return "W2_60_IOWaitPulse";
    return ids[0] ?? "W2_60_IOWaitPulse";
  }

  function populateProgramSelect() {
    if (!programSelect) return;
    programSelect.textContent = "";
    for (const program of app.programs) {
      const option = document.createElement("option");
      option.value = program.program_id;
      option.textContent = `${program.program_id} (${program.coverage_level}/${program.status})`;
      programSelect.append(option);
    }
    programSelect.value = app.selectedProgramId;
  }

  async function loadManifest() {
    const manifest = await optionalJson("demo-manifest.json", {
      networkFirst: true,
      timeoutMs: 2500,
      refresh: false
    });
    if (!manifest) return;
    applyManifest(manifest);
    void ensureProgramLoaded(app.selectedProgramId).then((data) => {
      if (!data.isFallback) {
        if (!["ready", "loading"].includes(app.command)) return;
        startParseStream(app.selectedProgramId, "manifest");
      }
    });
    resetExecutionClock();
    render();
  }

  async function ensureProgramLoaded(programId) {
    const cachedData = app.programCache.get(programId);
    if (cachedData && cachedData.releaseId === app.manifest.release_id && (cachedData.isFallback || hasTrajectoryData(cachedData))) {
      return cachedData;
    }
    if (app.programLoads.has(programId)) return app.programLoads.get(programId);

    const load = loadProgramData(programId).finally(() => {
      app.programLoads.delete(programId);
    });
    app.programLoads.set(programId, load);
    return load;
  }

  async function loadProgramData(programId) {
    const program = app.programs.find((item) => item.program_id === programId);
    if (!program) return fallbackProgramData;

    const artifacts = program.artifacts ?? {};
    const [
      sourceText,
      trajectory,
      controller,
      motionQueue,
      trace,
      io,
      compile,
      postReport,
      roundtrip
    ] = await Promise.all([
      optionalText(program.source),
      optionalJson(artifacts.trajectory),
      optionalJson(artifacts.controller),
      optionalJson(artifacts.motion_queue),
      optionalJson(artifacts.trace),
      optionalJson(artifacts.io),
      optionalJson(artifacts.compile),
      optionalJson(artifacts.post_report),
      optionalJson(artifacts.roundtrip)
    ]);

    const data = {
      sourceText: sourceText || fallbackProgramData.sourceText,
      trajectory: trajectory ?? {},
      controller: controller ?? {},
      motionQueue: motionQueue ?? {},
      trace: Array.isArray(trace) ? trace : [],
      io: io ?? {},
      compile: compile ?? {},
      postReport: postReport ?? {},
      roundtrip: roundtrip ?? {},
      isFallback: false,
      releaseId: app.manifest.release_id ?? "local",
      errors: []
    };
    if (hasTrajectoryData(data) || !app.programCache.has(programId)) {
      app.programCache.set(programId, data);
    }
    return data;
  }

  function sourceLineCount(data) {
    return (data.sourceText || fallbackProgramData.sourceText).split(/\r?\n/).length;
  }

  function sourceFrameCount(data) {
    return Array.isArray(data?.trajectory?.frames) ? data.trajectory.frames.length : 0;
  }

  function visibleSourceFrameCount() {
    const data = selectedData();
    return Array.isArray(data?.trajectory?.frames) ? data.trajectory.frames.length : 0;
  }

  function stopParseStream() {
    if (app.parseStream.timer) {
      window.clearInterval(app.parseStream.timer);
      app.parseStream.timer = 0;
    }
    app.parseStream.active = false;
  }

  function resetParseStreamSummary(message = "idle") {
    stopParseStream();
    app.parseStream.programId = "";
    app.parseStream.cursorLine = 0;
    app.parseStream.totalLines = 0;
    app.parseStream.linesPerTick = 1;
    app.parseStream.startedAt = 0;
    app.parseStream.lastSummary = message;
  }

  function startParseStream(programId, reason) {
    const data = cachedProgramData(programId);
    const totalLines = sourceLineCount(data);
    stopParseStream();
    resetExecutionClock();
    app.command = "parsing";
    app.parseStream.active = true;
    app.parseStream.programId = programId;
    app.parseStream.cursorLine = 0;
    app.parseStream.totalLines = totalLines;
    app.parseStream.linesPerTick = Math.max(1, Math.ceil(totalLines / 24));
    app.parseStream.startedAt = performance.now();
    app.parseStream.completedProgramId = "";
    app.parseStream.lastSummary = `parsing 0/${totalLines} lines`;
    pushTrace(`${programId} parsing ${reason} from ${storageModeLabel()}`);
    render();
    app.parseStream.timer = window.setInterval(advanceParseStream, app.samplePeriodMs);
    advanceParseStream();
  }

  function advanceParseStream() {
    if (!app.parseStream.active) return;
    const data = cachedProgramData(app.parseStream.programId);
    app.parseStream.totalLines = sourceLineCount(data);
    app.parseStream.cursorLine = Math.min(
      app.parseStream.totalLines,
      app.parseStream.cursorLine + app.parseStream.linesPerTick
    );
    syncExecutionClockToLatestSample();
    const parsedFrames = visibleSourceFrameCount();
    const totalFrames = sourceFrameCount(data);
    app.parseStream.lastSummary = [
      `parsing ${app.parseStream.cursorLine}/${app.parseStream.totalLines} lines`,
      `frames ${Math.min(parsedFrames, totalFrames)}/${totalFrames}`,
      `t=${formatNumber(app.playbackTime, 3)} s`
    ].join(", ");
    if (app.parseStream.cursorLine >= app.parseStream.totalLines) {
      finishParseStream("ready");
      return;
    }
    render();
  }

  function finishParseStream(nextCommand = "ready") {
    const programId = app.parseStream.programId || app.selectedProgramId;
    const data = cachedProgramData(programId);
    stopParseStream();
    app.parseStream.cursorLine = sourceLineCount(data);
    app.parseStream.totalLines = app.parseStream.cursorLine;
    app.parseStream.completedProgramId = programId;
    app.parseStream.lastSummary = `parsed ${app.parseStream.totalLines} lines, ${sourceFrameCount(data)} frames`;
    app.command = nextCommand;
    resetExecutionClock();
    pushTrace(`${programId} parsed (${sourceFrameCount(data)} frames, ${executionSamples().length} samples @50ms)`);
    render();
  }

  function finishParseStreamForCommand() {
    if (!app.parseStream.active) return;
    finishParseStream("ready");
  }

  function parserText() {
    return app.parseStream.active ? app.parseStream.lastSummary : app.parseStream.lastSummary || "idle";
  }

  function parserProgressValue() {
    if (app.parseStream.totalLines <= 0) return app.parseStream.active ? 0 : 1;
    return Math.min(Math.max(app.parseStream.cursorLine / app.parseStream.totalLines, 0), 1);
  }

  async function selectProgram(programId) {
    stopPlayback();
    stopParseStream();
    app.selectedProgramId = programId;
    app.command = "loading";
    resetExecutionClock();
    app.di1 = false;
    app.do1 = false;
    app.wait = "none";
    pushTrace(`${programId} loading from ${storageModeLabel()}`);
    render();
    try {
      await ensureProgramLoaded(programId);
      if (app.selectedProgramId !== programId) return;
      startParseStream(programId, "selection");
    } catch (error) {
      if (app.selectedProgramId === programId) reportRuntimeError(`${programId} select`, error);
    }
  }

  function commandTrace() {
    const trace = selectedData().trace;
    if (Array.isArray(trace) && trace.length > 0) return trace;
    return [];
  }

  function diagnosticText() {
    const data = selectedData();
    const diagnostics = [
      ...((data.controller?.snapshot?.state?.diagnostics) ?? []),
      ...((data.trajectory?.diagnostics) ?? []),
      ...((data.postReport?.report) ?? [])
    ];
    const latest = diagnostics.at(-1);
    if (!latest) {
      const program = selectedProgram();
      return `${program?.status ?? "ready"} / ${program?.coverage_level ?? "Runtime"}`;
    }
    return `${latest.code ?? latest.severity}: ${latest.message ?? ""}`.trim();
  }

  function queueItems() {
    const items = selectedData().motionQueue?.items;
    return Array.isArray(items) ? items : [];
  }

  function queueText() {
    const items = queueItems();
    if (items.length === 0) return "empty";
    return items.map((item) => {
      const pathId = item.request?.pathId ?? item.source?.pathId ?? item.id;
      const motions = Array.isArray(item.request?.segments)
        ? item.request.segments.map((segment) => segment.motion).join("/")
        : item.kind ?? "motion";
      return `${pathId}:${motions}`;
    }).join(" -> ");
  }

  function ioText() {
    const image = selectedData().io?.image ?? {};
    const do1 = app.do1 || image["io.do[1]"] === true;
    return `do[1]=${do1}, di[1]=${app.di1}, io=${Object.keys(image).length}`;
  }

  function storageModeLabel() {
    const mode = app.storage.mode === "checking" ? "cache" : app.storage.mode;
    return `${mode} h${app.storage.hits}/n${app.storage.network}/w${app.storage.writes}/e${app.storage.errors}`;
  }

  function storageText() {
    return `${storageModeLabel()} ${app.storage.last}`;
  }

  function reportRuntimeError(context, error) {
    const message = `${context}: ${error instanceof Error ? error.message : String(error)}`;
    window.__vcErrors.push(message);
    app.storage.errors += 1;
    app.storage.last = message;
    pushTrace(message);
    render();
  }

  function sourceTextForTab(tab) {
    const program = selectedProgram();
    const data = selectedData();
    if (tab === "grl") return data.sourceText || fallbackProgramData.sourceText;

    const moduleName = data.compile?.moduleName ?? (program?.program_id ?? "W2Program").replace(/^W2_/, "W2");
    const filenames = Array.isArray(data.postReport?.filenames) ? data.postReport.filenames : [];
    if (tab === "abb") {
      return [
        `MODULE ${moduleName}`,
        `  ! ${filenames.find((name) => name.endsWith(".mod")) ?? `${moduleName}.mod`}`,
        "  PROC main()",
        "    ! See post-report.json for generated brand diagnostics.",
        "  ENDPROC",
        "ENDMODULE"
      ].join("\n");
    }
    if (tab === "fanuc") {
      return [
        `/PROG ${moduleName.toUpperCase()}`,
        `/ATTR`,
        `COMMENT = "${filenames.find((name) => name.endsWith(".ls")) ?? `${moduleName}.ls`}";`,
        "/MN",
        "  1: ! See post-report.json ;",
        "/END"
      ].join("\n");
    }
    return [
      `DEF ${moduleName}()`,
      `  ; ${filenames.find((name) => name.endsWith(".src")) ?? `${moduleName}.src`}`,
      "  ; See post-report.json",
      "END"
    ].join("\n");
  }

  function updateObjectTree() {
    if (!objectTree) return;
    const program = selectedProgram();
    const items = queueItems();
    const pathNames = [...new Set(items.map((item) => item.request?.pathId ?? item.source?.pathId).filter(Boolean))];
    const entries = [
      `Robot: ${app.manifest.robot_id ?? "abb_irb120_3_58"}`,
      "Tool: tool0",
      "Frame: world",
      `Path: ${pathNames.join(", ") || "pick_place"}`,
      `Program: ${program?.program_id ?? "W2_60_IOWaitPulse"}`,
      `Coverage: ${program?.coverage_level ?? "Runtime"}`,
      `Status: ${program?.status ?? "pass"}`,
      `Frames: ${frames().length}`,
      `Suite: ${app.manifest.suite_id ?? "working2-abb120-spec"}`,
      `Job: ${app.manifest.job_id ?? "W2-JOB-latest"}`,
      "Reports"
    ];
    objectTree.textContent = "";
    for (const entry of entries) {
      const item = document.createElement("li");
      item.textContent = entry;
      objectTree.append(item);
    }
  }

  function updateFrameSlider() {
    if (!frameSlider) return;
    const samples = currentSamples();
    frameSlider.max = String(Math.max(samples.length - 1, 0));
    frameSlider.value = String(Math.min(app.currentSampleIndex, Math.max(samples.length - 1, 0)));
  }

  function updatePathMarker() {
    if (!pathCurrent) return;
    const duration = trajectoryDuration();
    const ratio = duration > 0 ? Math.min(Math.max(app.playbackTime / duration, 0), 1) : 0;
    const x = 70 + ratio * 340;
    const y = 170 - Math.sin(ratio * Math.PI) * 80 - ratio * 20;
    pathCurrent.setAttribute("cx", String(formatNumber(x, 2)));
    pathCurrent.setAttribute("cy", String(formatNumber(y, 2)));
  }

  function sourceLines() {
    return (selectedData().sourceText || fallbackProgramData.sourceText).split(/\r?\n/);
  }

  function sourceLineText(line) {
    const index = Math.round(numberOr(line, 0)) - 1;
    return sourceLines()[index]?.trim() ?? "";
  }

  function mainProcedureRange() {
    const lines = sourceLines();
    const startIndex = lines.findIndex((line) => /^\s*proc\s+main\s*\(/i.test(line));
    if (startIndex < 0) return { start: 0, end: 0 };
    const startIndent = leadingSpaces(lines[startIndex]);
    for (let index = startIndex + 1; index < lines.length; index += 1) {
      if (/^\s*end\b/i.test(lines[index]) && leadingSpaces(lines[index]) <= startIndent) {
        return { start: startIndex + 1, end: index + 1 };
      }
    }
    return { start: startIndex + 1, end: lines.length };
  }

  function leadingSpaces(text) {
    return String(text ?? "").match(/^\s*/)?.[0].length ?? 0;
  }

  function setupStepsBefore(firstLine) {
    const range = mainProcedureRange();
    if (range.start <= 0) return [];
    const steps = [];
    const lines = sourceLines();
    const limit = Math.max(numberOr(firstLine, range.end + 1), range.start + 1);
    for (let line = range.start + 1; line < Math.min(limit, range.end); line += 1) {
      const text = lines[line - 1]?.trim() ?? "";
      if (!/^(set_tool|set_frame)\b/i.test(text)) continue;
      steps.push({
        kind: "setup",
        label: "Setup",
        line,
        text,
        timeStart: 0,
        timeEnd: 0,
        sampleStart: 0,
        sampleEnd: 0,
        detail: text.replace(/\s+/g, " ")
      });
    }
    return steps;
  }

  function traceInstructionSteps() {
    const steps = [];
    let lastKey = "";
    for (const entry of commandTrace()) {
      const line = numberOr(entry?.source?.sourceMap?.line, 0);
      if (line <= 0 || entry?.kind === "load") continue;
      const kind = entry.instructionKind ?? entry.source?.kind ?? entry.kind ?? "instruction";
      const key = `${line}:${kind}`;
      const time = numberOr(entry.time, 0);
      if (key === lastKey && steps.length > 0) {
        const previous = steps.at(-1);
        previous.timeEnd = Math.max(numberOr(previous.timeEnd, time), time);
        previous.detail = mergeDetails(previous.detail, traceDetail(entry));
        continue;
      }
      const text = sourceLineText(line);
      steps.push({
        kind: String(kind).toLowerCase(),
        label: stepLabel(kind, text),
        line,
        text,
        timeStart: time,
        timeEnd: time,
        pc: numberOr(entry.pc, 0),
        traceKind: entry.kind,
        detail: traceDetail(entry)
      });
      lastKey = key;
    }
    return steps;
  }

  function traceDetail(entry) {
    const data = entry?.data ?? {};
    if (Array.isArray(data.result) && data.result.length > 0) {
      return data.result.map((item) => {
        const target = item.target?.raw ?? item.target?.domain ?? item.kind ?? "io";
        return `${target}=${String(item.value)}`;
      }).join(", ");
    }
    if (data.status) return `status=${data.status}`;
    if (data.queued === true) return "queued";
    if (data.alarmId) return `alarm=${data.alarmId}`;
    if (entry?.diagnostic?.code) return entry.diagnostic.code;
    if (entry?.message) return entry.message;
    return "";
  }

  function mergeDetails(left, right) {
    if (!right) return left ?? "";
    if (!left) return right;
    return left.includes(right) ? left : `${left}; ${right}`;
  }

  function stepLabel(kind, text) {
    const source = String(text ?? "").trim().toLowerCase();
    const normalized = String(kind ?? "").toUpperCase();
    if (/^set_(tool|frame)\b/.test(source)) return "Setup";
    if (/^run_operation\b/.test(source) || normalized === "RUN_OPERATION") return "Operation";
    if (/^run_path\b/.test(source) || normalized === "RUN_PATH") return "Path";
    if (/^point\b/.test(source)) return "Path Point";
    if (/^event\b/.test(source)) return "Path Event";
    if (/^(movej|movel|movec|moveabsj)\b/.test(source) || /^MOVE/.test(normalized)) return "Motion";
    if (/^wait\b/.test(source) || normalized === "WAIT") return "Wait";
    if (/^pulse\b/.test(source) || normalized === "PULSE") return "Pulse";
    if (/^io\./.test(source) || normalized === "IO_WRITE") return "IO";
    if (/^(if|elseif|else|while|for|switch|case|break|continue)\b/.test(source) || normalized.startsWith("EXEC_")) return "Control";
    if (/^call\b/.test(source)) return "Call";
    if (/^return\b/.test(source)) return "Return";
    if (normalized === "ALARM") return "Alarm";
    return normalized.replace(/^RAW_/, "").replace(/_/g, " ").toLowerCase();
  }

  function sampleGroups(samples = currentSamples()) {
    const groups = [];
    for (const sample of samples) {
      const line = numberOr(sample?.sourceMap?.line, 0);
      if (line <= 0) continue;
      const key = [
        line,
        sample.motion ?? "",
        sample.targetId ?? sample.segmentId ?? ""
      ].join(":");
      const previous = groups.at(-1);
      if (previous?.key === key && previous.sampleEnd + 1 === sample.index) {
        previous.sampleEnd = sample.index;
        previous.timeEnd = numberOr(sample.time, previous.timeEnd);
      } else {
        groups.push({
          id: `sample:${groups.length}`,
          key,
          line,
          motion: sample.motion ?? "",
          target: sample.targetId ?? sample.segmentId ?? "",
          sampleStart: sample.index,
          sampleEnd: sample.index,
          timeStart: numberOr(sample.time, 0),
          timeEnd: numberOr(sample.time, 0)
        });
      }
    }
    return groups;
  }

  function applySampleGroup(step, group) {
    if (!group) return step;
    step.motion = group.motion || step.motion;
    step.target = group.target || step.target;
    step.sampleStart = group.sampleStart;
    step.sampleEnd = group.sampleEnd;
    step.timeStart = Math.min(numberOr(step.timeStart, group.timeStart), group.timeStart);
    step.timeEnd = Math.max(numberOr(step.timeEnd, group.timeEnd), group.timeEnd);
    return step;
  }

  function nearestSampleIndex(time, samples = currentSamples()) {
    if (samples.length === 0) return 0;
    let bestIndex = 0;
    let bestDistance = Infinity;
    for (const sample of samples) {
      const distance = Math.abs(numberOr(sample.time, 0) - numberOr(time, 0));
      if (distance < bestDistance) {
        bestDistance = distance;
        bestIndex = sample.index;
      }
    }
    return bestIndex;
  }

  function findUnusedGroup(line, groups, usedGroupIds) {
    const group = groups.find((item) => item.line === line && !usedGroupIds.has(item.id))
      ?? groups.find((item) => item.line === line);
    if (group) usedGroupIds.add(group.id);
    return group;
  }

  function queueChildrenForStep(step, groups, usedGroupIds) {
    const children = [];
    for (const item of queueItems()) {
      const parentLine = numberOr(item?.source?.sourceMap?.line ?? item?.source?.instruction?.sourceMap?.line, 0);
      if (parentLine !== step.line) continue;
      const request = item.request ?? {};
      const segments = Array.isArray(request.segments) ? request.segments : [];
      const events = Array.isArray(request.events) ? request.events : [];
      const eventsByPoint = new Map();
      const looseEvents = [];
      for (const event of events) {
        const pointId = event.pointId ?? "";
        if (!pointId) {
          looseEvents.push(event);
          continue;
        }
        if (!eventsByPoint.has(pointId)) eventsByPoint.set(pointId, []);
        eventsByPoint.get(pointId).push(event);
      }

      for (const segment of segments) {
        const beforeEvents = (eventsByPoint.get(segment.id) ?? []).filter((event) => event.timing === "before");
        for (const event of beforeEvents) children.push(queueEventStep(event, step));

        children.push(queueSegmentStep(segment, step, groups, usedGroupIds));

        const afterEvents = (eventsByPoint.get(segment.id) ?? []).filter((event) => event.timing !== "before");
        for (const event of afterEvents) children.push(queueEventStep(event, step, segment, groups));
      }

      for (const event of looseEvents) children.push(queueEventStep(event, step));
    }

    return children.filter((child) => child.line > 0 && child.line !== step.line);
  }

  function queueSegmentStep(segment, parent, groups, usedGroupIds) {
    const line = numberOr(segment?.sourceMap?.line, 0);
    const text = sourceLineText(line);
    const step = {
      kind: "path-point",
      label: stepLabel(segment?.motion ?? "MOVE", text),
      line,
      text,
      timeStart: numberOr(parent.timeStart, 0),
      timeEnd: numberOr(parent.timeEnd, 0),
      motion: segment?.motion ?? "",
      target: segment?.targetId ?? segment?.target?.id ?? segment?.id ?? "",
      detail: `${segment?.motion ?? "MOVE"} ${segment?.targetId ?? segment?.target?.id ?? segment?.id ?? ""}`.trim()
    };
    return applySampleGroup(step, findUnusedGroup(line, groups, usedGroupIds));
  }

  function queueEventStep(event, parent, segment, groups = []) {
    const line = numberOr(event?.sourceMap?.line, 0);
    const text = sourceLineText(line);
    const segmentGroup = segment
      ? groups.find((group) => group.line === numberOr(segment?.sourceMap?.line, 0))
      : undefined;
    const time = event?.timing === "before"
      ? numberOr(segmentGroup?.timeStart, numberOr(parent.timeStart, 0))
      : numberOr(segmentGroup?.timeEnd, numberOr(parent.timeEnd, numberOr(parent.timeStart, 0)));
    const sample = event?.timing === "before"
      ? numberOr(segmentGroup?.sampleStart, parent.sampleStart)
      : numberOr(segmentGroup?.sampleEnd, parent.sampleEnd);
    return {
      kind: "path-event",
      label: stepLabel("EVENT", text),
      line,
      text,
      timeStart: time,
      timeEnd: time,
      sampleStart: Number.isFinite(sample) ? sample : undefined,
      sampleEnd: Number.isFinite(sample) ? sample : undefined,
      detail: `${event?.timing ?? "event"} ${event?.pointId ?? ""} ${event?.kind ?? ""}`.trim()
    };
  }

  function buildProgramSteps(samples = currentSamples()) {
    const groups = sampleGroups(samples);
    const usedGroupIds = new Set();
    const traceSteps = traceInstructionSteps();
    const firstTraceLine = traceSteps.find((step) => step.line > 0)?.line ?? 0;
    const steps = setupStepsBefore(firstTraceLine);
    const seenSourceRows = new Set(steps.map((step) => `${step.line}:${step.kind}`));

    for (const traceStep of traceSteps) {
      const step = { ...traceStep };
      applySampleGroup(step, findUnusedGroup(step.line, groups, usedGroupIds));
      if (step.sampleStart === undefined && step.timeStart !== undefined) {
        const sample = nearestSampleIndex(step.timeStart, samples);
        step.sampleStart = sample;
        step.sampleEnd = sample;
      }
      const childSteps = queueChildrenForStep(step, groups, usedGroupIds);
      const childGroups = childSteps.filter((child) => child.sampleStart !== undefined);
      if (childGroups.length > 0 && !step.motion) {
        step.sampleStart = Math.min(...childGroups.map((child) => child.sampleStart));
        step.sampleEnd = Math.max(...childGroups.map((child) => child.sampleEnd));
        step.timeStart = Math.min(...childGroups.map((child) => child.timeStart));
        step.timeEnd = Math.max(...childGroups.map((child) => child.timeEnd));
      }
      const key = `${step.line}:${step.kind}:${step.pc ?? ""}`;
      if (!seenSourceRows.has(key)) {
        steps.push(step);
        seenSourceRows.add(key);
      }
      for (const child of childSteps) {
        const childKey = `${child.line}:${child.kind}:${child.detail ?? ""}`;
        if (seenSourceRows.has(childKey)) continue;
        steps.push(child);
        seenSourceRows.add(childKey);
      }
    }

    for (const group of groups) {
      if (usedGroupIds.has(group.id)) continue;
      const text = sourceLineText(group.line);
      steps.push({
        kind: "motion",
        label: stepLabel(group.motion, text),
        line: group.line,
        text,
        timeStart: group.timeStart,
        timeEnd: group.timeEnd,
        sampleStart: group.sampleStart,
        sampleEnd: group.sampleEnd,
        motion: group.motion,
        target: group.target,
        detail: `${group.motion} ${group.target}`.trim()
      });
      usedGroupIds.add(group.id);
    }

    if (steps.length === 0) {
      const item = currentFrame();
      steps.push({
        kind: "sample",
        label: "Sample",
        line: numberOr(item?.sourceMap?.line, 0),
        text: item?.sourceMap ? sourceLineText(item.sourceMap.line) : "sample trajectory",
        timeStart: app.playbackTime,
        timeEnd: app.playbackTime,
        sampleStart: app.currentSampleIndex,
        sampleEnd: app.currentSampleIndex,
        motion: item?.motion ?? "",
        target: item?.targetId ?? item?.segmentId ?? ""
      });
    }

    return steps.map((step, index) => ({
      ...step,
      index: index + 1,
      id: `step:${index + 1}:${step.line}:${step.kind}`
    }));
  }

  function activeProgramStep(steps) {
    if (steps.length === 0) return undefined;
    const item = currentFrame();
    const line = numberOr(item?.sourceMap?.line, 0);
    if (line > 0) {
      const matchingMotion = steps.find((step) => {
        return step.line === line
          && step.sampleStart !== undefined
          && step.sampleEnd !== undefined
          && step.sampleStart <= app.currentSampleIndex
          && step.sampleEnd >= app.currentSampleIndex;
      });
      if (matchingMotion) return matchingMotion;
    }

    const matchingSample = steps.find((step) => {
      return step.sampleStart !== undefined
        && step.sampleEnd !== undefined
        && step.sampleStart <= app.currentSampleIndex
        && step.sampleEnd >= app.currentSampleIndex;
    });
    if (matchingSample) return matchingSample;

    const elapsedSteps = steps.filter((step) => numberOr(step.timeStart, 0) <= app.playbackTime + 1e-9);
    return elapsedSteps.at(-1) ?? steps[0];
  }

  function stepState(step, active) {
    if (!active) return "pending";
    if (step.id === active.id) return "active";
    if (step.sampleEnd !== undefined && active.sampleStart !== undefined && step.sampleEnd < active.sampleStart) {
      return "complete";
    }
    if (step.index < active.index && numberOr(step.timeEnd, 0) <= app.playbackTime + 1e-9) {
      return "complete";
    }
    return "pending";
  }

  function renderStepTable(steps, active) {
    if (!stepTable) return;
    stepTable.textContent = "";
    for (const step of steps) {
      const row = document.createElement("tr");
      row.dataset.stepState = stepState(step, active);
      if (step.id === active?.id) row.setAttribute("aria-current", "true");
      const cells = [
        step.index,
        stepTimeText(step),
        step.line > 0 ? step.line : "",
        step.label,
        step.text,
        stepMotionText(step),
        stepSampleText(step)
      ];
      for (const cell of cells) {
        const td = document.createElement("td");
        td.textContent = String(cell);
        row.append(td);
      }
      stepTable.append(row);
    }
  }

  function stepTimeText(step) {
    const start = numberOr(step.timeStart, 0);
    const end = numberOr(step.timeEnd, start);
    if (Math.abs(end - start) > 1e-9) {
      return `${formatNumber(start, 3)}-${formatNumber(end, 3)} s`;
    }
    return `${formatNumber(start, 3)} s`;
  }

  function stepMotionText(step) {
    if (step.motion || step.target) {
      return `${step.motion ?? ""}${step.target ? ` / ${step.target}` : ""}`.trim();
    }
    return step.detail ?? "";
  }

  function stepSampleText(step) {
    if (step.sampleStart === undefined || step.sampleEnd === undefined) return "-";
    const start = numberOr(step.sampleStart, 0) + 1;
    const end = numberOr(step.sampleEnd, step.sampleStart) + 1;
    return start === end ? `${start} @50ms` : `${start}-${end} @50ms`;
  }

  function renderFrameTable() {
    if (!frameTable) return;
    const items = frames();
    frameTable.textContent = "";
    for (const item of items) {
      const row = document.createElement("tr");
      if (item.index === app.currentFrameIndex) row.setAttribute("aria-current", "true");
      const cells = [
        item.index,
        formatNumber(item.time, 4),
        item.motion,
        item.targetId ?? item.segmentId ?? "",
        formatArray(item.tcp.position, 6),
        formatArray(item.tcp.quaternion, 6),
        ...item.joints.map((joint) => formatNumber(joint, 6))
      ];
      for (const cell of cells) {
        const td = document.createElement("td");
        td.textContent = String(cell);
        row.append(td);
      }
      frameTable.append(row);
    }
  }

  function render() {
    const program = selectedProgram();
    const data = selectedData();
    const item = currentFrame();
    const trace = commandTrace();
    const traceLast = trace.at(-1);
    const totalFrames = frames().length;
    const samples = currentSamples();
    const programSteps = buildProgramSteps(samples);
    const activeStep = activeProgramStep(programSteps);
    const totalSamples = samples.length;
    const duration = samples.at(-1)?.time ?? trajectoryDuration();
    const sourceMap = item.sourceMap ? `:${item.sourceMap.line ?? 0}` : "";

    root?.setAttribute("data-last-command", app.command);
    root?.setAttribute("data-running", String(app.command === "running"));
    if (programSelect && programSelect.value !== app.selectedProgramId) {
      programSelect.value = app.selectedProgramId;
    }

    setField("job", app.manifest.job_id ?? "W2-JOB-latest");
    setField("state", app.command);
    setField("program", program?.program_id ?? "W2_60_IOWaitPulse");
    setField("mode", app.mode);
    setField("motors", app.motors);
    setField("pc", `sample:${app.currentSampleIndex}${sourceMap}`);
    setField(
      "runtime",
      `${formatNumber(app.playbackTime, 3)} / ${formatNumber(duration, 3)} s @ ${app.samplePeriodMs} ms`
    );
    setField("joints", formatArray(item.joints, 6));
    setField("tcp", `pos ${formatArray(item.tcp.position, 6)} q ${formatArray(item.tcp.quaternion, 6)}`);
    setField("log", app.localTrace.at(-1) ?? traceLast?.message ?? "ready");
    setField("diagnostics", diagnosticText());
    setField("io", ioText());
    setField("wait", app.wait);
    setField("queue", queueText());
    setField("trace", `${trace.length + app.localTrace.length} events`);
    setField("reports", app.report);
    setField("parser", parserText());
    setField("storage", storageText());
    setField("editor", sourceTextForTab(app.tab));
    setField("frame", totalSamples > 0 ? `${app.currentSampleIndex + 1} / ${totalSamples}` : "0 / 0");
    setField(
      "step-status",
      activeStep
        ? `${activeStep.index}/${programSteps.length} ${activeStep.label} line ${activeStep.line || "-"} t=${formatNumber(app.playbackTime, 3)} s`
        : "0 / 0"
    );
    const sampleCount = Number.isFinite(Number(data.trajectory?.sampleCount))
      ? Number(data.trajectory.sampleCount)
      : (data.isFallback ? totalFrames : 0);
    setField(
      "frame-time",
      `t=${formatNumber(app.playbackTime, 3)} / ${formatNumber(duration, 3)} s, step=${formatNumber(app.samplePeriodSec, 2)} s, source ${app.currentFrameIndex + 1}/${totalFrames} (${sampleCount} trajectory frames)`
    );

    for (const button of tabButtons) {
      button.setAttribute("aria-pressed", String(button.dataset.tab === app.tab));
    }
    if (parserProgress) {
      parserProgress.value = parserProgressValue();
    }

    updateObjectTree();
    updateFrameSlider();
    updatePathMarker();
    renderStepTable(programSteps, activeStep);
    renderFrameTable();
  }

  function pushTrace(message) {
    app.localTrace.push(message);
    if (app.localTrace.length > 100) app.localTrace.shift();
  }

  function advanceSample(stepSize) {
    setSampleIndex(app.currentSampleIndex + stepSize);
  }

  function startPlayback() {
    finishParseStreamForCommand();
    stopPlayback();
    app.playbackStartedAt = performance.now();
    app.playbackTimeStart = app.playbackTime;
    app.playbackSampleIndex = app.currentSampleIndex;
    app.playbackTimer = window.setInterval(() => {
      if (app.command !== "running") {
        stopPlayback();
        return;
      }
      const elapsed = (performance.now() - app.playbackStartedAt) / 1000;
      const before = app.currentSampleIndex;
      setPlaybackTime(app.playbackTimeStart + elapsed);
      if (before !== app.currentSampleIndex) {
        app.playbackSampleIndex = app.currentSampleIndex;
      }
      if (app.currentSampleIndex >= Math.max(currentSamples().length - 1, 0)) {
        stopPlayback();
        app.command = "ready";
        pushTrace(`${app.selectedProgramId} completed t=${formatNumber(app.playbackTime, 3)}s`);
      }
      render();
    }, app.samplePeriodMs);
  }

  function stopPlayback() {
    if (app.playbackTimer) {
      window.clearInterval(app.playbackTimer);
      app.playbackTimer = 0;
    }
  }

  async function handleCommand(command) {
    root?.setAttribute("data-last-command", command);
    if (command === "load") {
      const programId = app.selectedProgramId;
      stopPlayback();
      stopParseStream();
      app.command = "load";
      resetExecutionClock();
      app.di1 = false;
      app.do1 = false;
      app.wait = "none";
      pushTrace(`${programId} loading from ${storageModeLabel()}`);
      render();
      void ensureProgramLoaded(programId).then(() => {
        if (app.selectedProgramId !== programId) return;
        if (!["load", "loading"].includes(app.command)) return;
        startParseStream(programId, "load");
      }).catch((error) => reportRuntimeError(`${programId} load`, error));
      return;
    } else if (command === "run") {
      const programId = app.selectedProgramId;
      finishParseStreamForCommand();
      app.command = "running";
      app.do1 = true;
      if (app.currentSampleIndex >= Math.max(currentSamples().length - 1, 0)) {
        resetExecutionClock();
      }
      advanceSample(1);
      app.wait = app.di1 ? "satisfied" : "waiting di[1]";
      pushTrace(`${programId} running sample ${app.currentSampleIndex + 1} t=${formatNumber(app.playbackTime, 3)}s`);
      startPlayback();
      render();
      void ensureProgramLoaded(programId).then(() => {
        if (app.selectedProgramId === programId && app.command === "running") render();
      }).catch((error) => reportRuntimeError(`${programId} run load`, error));
      return;
    } else if (command === "pause") {
      stopPlayback();
      app.command = "paused";
      pushTrace(`${app.selectedProgramId} paused`);
    } else if (command === "step") {
      stopPlayback();
      finishParseStreamForCommand();
      app.command = "paused";
      advanceSample(1);
      pushTrace(`${app.selectedProgramId} stepped sample ${app.currentSampleIndex + 1} t=${formatNumber(app.playbackTime, 3)}s`);
    } else if (command === "stop") {
      stopPlayback();
      stopParseStream();
      app.command = "stopped";
      app.wait = "none";
      pushTrace(`${app.selectedProgramId} stopped`);
    } else if (command === "reset") {
      stopPlayback();
      stopParseStream();
      app.command = "ready";
      resetExecutionClock();
      app.di1 = false;
      app.do1 = false;
      app.wait = "none";
      pushTrace("controller reset");
    } else if (command === "export") {
      app.report = `${app.manifest.job_id ?? "W2-JOB-latest"} report exported`;
      pushTrace("delivery package exported");
    }
    render();
  }

  for (const button of document.querySelectorAll("[data-command]")) {
    button.addEventListener("click", () => {
      const command = button.dataset.command ?? "ready";
      void handleCommand(command).catch((error) => reportRuntimeError(command, error));
    });
  }

  for (const button of tabButtons) {
    button.addEventListener("click", () => {
      app.tab = button.dataset.tab ?? "grl";
      pushTrace(`${app.tab.toUpperCase()} source selected`);
      render();
    });
  }

  programSelect?.addEventListener("change", () => {
    void selectProgram(programSelect.value).catch((error) => reportRuntimeError("program select", error));
  });

  frameSlider?.addEventListener("input", () => {
    stopPlayback();
    setSampleIndex(numberOr(frameSlider.value, 0));
    app.command = app.command === "running" ? "paused" : app.command;
    pushTrace(`${app.selectedProgramId} sample ${app.currentSampleIndex + 1} t=${formatNumber(app.playbackTime, 3)}s`);
    render();
  });

  document.querySelector("[data-io='di1']")?.addEventListener("click", () => {
    app.di1 = !app.di1;
    app.wait = app.di1 ? "satisfied" : "waiting di[1]";
    pushTrace(`di[1]=${app.di1}`);
    render();
  });

  applyManifest(fallbackManifest);
  render();
  void loadManifest();
})();
