Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  app.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { defaultQaModelForMode, isQaFastModeEnabled } from "../../model-selection.js";
import { formatErrorMessage } from "./errors.js";
import {
  type Bootstrap,
  type OutcomesEnvelope,
  type ReportEnvelope,
  type RunnerSelection,
  type Snapshot,
  type TabId,
  type CaptureEventsEnvelope,
  type CaptureCoverageEnvelope,
  type CaptureQueryEnvelope,
  type CaptureSessionsEnvelope,
  type CaptureStartupStatusEnvelope,
  type CaptureSavedView,
  type UiState,
  renderQaLabUi,
} from "./ui-render.js";

async function getJson<T>(path: string): Promise<T> {
  const response = await fetch(path);
  if (!response.ok) {
    throw new Error(`${response.status} ${response.statusText}`);
  }
  return (await response.json()) as T;
}

async function getJsonNoStore<T>(path: string): Promise<T> {
  const response = await fetch(path, { cache: "no-store" });
  if (!response.ok) {
    throw new Error(`${response.status} ${response.statusText}`);
  }
  return (await response.json()) as T;
}

async function postJson<T>(path: string, body: unknown): Promise<T> {
  const response = await fetch(path, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    const payload = (await response.json().catch(() => ({}))) as { error?: string };
    throw new Error(payload.error || `${response.status} ${response.statusText}`);
  }
  return (await response.json()) as T;
}

function countCaptureDimension(
  events: UiState["captureEvents"],
  pick: (event: UiState["captureEvents"][number]) => string | undefined,
) {
  const counts = new Map<string, number>();
  for (const event of events) {
    const value = pick(event)?.trim();
    if (!value) {
      continue;
    }
    counts.set(value, (counts.get(value) ?? 0) + 1);
  }
  return [...counts.entries()]
    .map(([value, count]) => ({ value, count }))
    .toSorted((left, right) => right.count - left.count || left.value.localeCompare(right.value));
}

function summarizeCaptureCoverageFromEvents(
  sessionIds: string[],
  events: UiState["captureEvents"],
): UiState["captureCoverage"] {
  const unlabeledEventCount = events.filter(
    (event) => !event.provider?.trim() && !event.api?.trim() && !event.model?.trim(),
  ).length;
  return {
    sessionId: sessionIds.join(","),
    totalEvents: events.length,
    unlabeledEventCount,
    providers: countCaptureDimension(events, (event) => event.provider),
    apis: countCaptureDimension(events, (event) => event.api),
    models: countCaptureDimension(events, (event) => event.model),
    hosts: countCaptureDimension(events, (event) => event.host),
    localPeers: countCaptureDimension(events, (event) => {
      const host = event.host?.trim();
      return host && /^(127\.0\.0\.1|localhost)(:\d+)?$/i.test(host) ? host : undefined;
    }),
  };
}

function defaultModelsForProviderMode(
  mode: RunnerSelection["providerMode"],
  bootstrap?: Bootstrap | null,
): Pick<RunnerSelection, "primaryModel" | "alternateModel" | "fastMode"> {
  const preferredLiveModel = bootstrap?.runnerCatalog.real[0]?.key;
  if (mode === "live-frontier") {
    const primaryModel = defaultQaModelForMode(mode, { preferredLiveModel });
    const alternateModel = defaultQaModelForMode(mode, { alternate: true, preferredLiveModel });
    return {
      primaryModel,
      alternateModel,
      fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }),
    };
  }
  const primaryModel = defaultQaModelForMode(mode);
  const alternateModel = defaultQaModelForMode(mode, { alternate: true });
  return {
    primaryModel,
    alternateModel,
    fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }),
  };
}

function detectTheme(): "light" | "dark" {
  const stored = localStorage.getItem("qa-lab-theme");
  if (stored === "light" || stored === "dark") {
    return stored;
  }
  return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}

function detectSidebarCollapsed(): boolean {
  return localStorage.getItem("qa-lab-sidebar-collapsed") === "1";
}

function detectSidebarPanel(): UiState["sidebarPanel"] {
  const stored = localStorage.getItem("qa-lab-sidebar-panel");
  return stored === "config" || stored === "run" ? stored : "scenarios";
}

const CAPTURE_SAVED_VIEWS_KEY = "qa-lab-capture-saved-views";

function loadCaptureSavedViews(): CaptureSavedView[] {
  try {
    const raw = localStorage.getItem(CAPTURE_SAVED_VIEWS_KEY);
    if (!raw) {
      return [];
    }
    const parsed = JSON.parse(raw) as unknown;
    return Array.isArray(parsed) ? (parsed as CaptureSavedView[]) : [];
  } catch {
    return [];
  }
}

function persistCaptureSavedViews(savedViews: CaptureSavedView[]) {
  localStorage.setItem(CAPTURE_SAVED_VIEWS_KEY, JSON.stringify(savedViews));
}

function isEditableElement(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) {
    return false;
  }
  return (
    target.isContentEditable ||
    target instanceof HTMLInputElement ||
    target instanceof HTMLTextAreaElement ||
    target instanceof HTMLSelectElement
  );
}

export async function createQaLabApp(root: HTMLDivElement) {
  const state: UiState = {
    theme: detectTheme(),
    bootstrap: null,
    snapshot: null,
    latestReport: null,
    scenarioRun: null,
    captureSessions: [],
    captureEvents: [],
    captureQueryPreset: "none",
    captureQueryRows: [],
    captureKindFilter: [],
    captureProviderFilter: [],
    captureHostFilter: [],
    captureSearchText: "",
    captureHeaderMode: "key",
    captureViewMode: "list",
    captureGroupMode: "none",
    captureTimelineLaneMode: "domain",
    captureTimelineLaneSort: "most-events",
    captureTimelinePreviousLaneSort: null,
    captureTimelineLaneSearch: "",
    captureTimelineZoom: 100,
    captureTimelineSparklineMode: "session-relative",
    captureTimelineWindowStartPct: null,
    captureTimelineWindowEndPct: null,
    captureTimelineBrushAnchorPct: null,
    captureTimelineBrushCurrentPct: null,
    captureTimelineFocusSelectedFlow: false,
    captureTimelineFocusedLaneMode: "all",
    captureTimelineFocusedLaneThreshold: "any",
    captureDetailPlacement: "right",
    captureDetailSplitPct: 34,
    captureDetailSplitDragging: false,
    captureDetailView: "overview",
    capturePreferredDetailView: null,
    captureFlowDetailLayout: null,
    capturePayloadDetailLayout: null,
    capturePayloadExtent: "preview",
    capturePayloadEventSort: "stream",
    capturePayloadEventFilter: "",
    captureErrorsOnly: false,
    captureCoverage: null,
    captureStartupStatus: null,
    captureControlsExpanded: false,
    captureSummaryExpanded: false,
    captureSavedViews: loadCaptureSavedViews(),
    captureSelectedSessionsExpanded: false,
    sidebarCollapsed: detectSidebarCollapsed(),
    sidebarPanel: detectSidebarPanel(),
    captureCollapsedLaneIds: [],
    capturePinnedLaneIds: [],
    selectedCaptureSessionIds: [],
    selectedCaptureEventKey: null,
    selectedConversationId: null,
    selectedThreadId: null,
    selectedScenarioId: null,
    activeTab: "chat",
    runnerDraft: null,
    runnerDraftDirty: false,
    composer: {
      conversationKind: "direct",
      conversationId: "alice",
      senderId: "alice",
      senderName: "Alice",
      text: "",
    },
    busy: false,
    error: null,
  };

  /* Track whether user has scrolled up in the chat */
  let chatScrollLocked = true;
  let previousMessageCount = 0;

  /* ---------- Render guards (avoid DOM churn during polling) ---------- */

  let lastFingerprint = "";
  let renderDeferred = false;
  let previousRunnerStatus: string | null = null;
  let currentUiVersion: string | null = null;
  let syncingCaptureTimelineScroll = false;
  let sparklineSweepActive = false;
  let sparklineSweepAnchorStartPct: number | null = null;
  let sparklineSweepAnchorEndPct: number | null = null;
  let sparklineSweepCurrentStartPct: number | null = null;
  let sparklineSweepCurrentEndPct: number | null = null;
  let captureGlobalListenersBound = false;

  function stateFingerprint(): string {
    const msgs = state.snapshot?.messages;
    const ev = state.snapshot?.events;
    return JSON.stringify({
      mc: msgs?.length ?? 0,
      lm: msgs && msgs.length > 0 ? msgs[msgs.length - 1].id : null,
      cc: state.snapshot?.conversations.length ?? 0,
      tc: state.snapshot?.threads.length ?? 0,
      ec: ev?.length ?? 0,
      lc: ev && ev.length > 0 ? ev[ev.length - 1].cursor : -1,
      rs: state.bootstrap?.runner.status,
      ra: state.bootstrap?.runner.startedAt,
      rf: state.bootstrap?.runner.finishedAt,
      re: state.bootstrap?.runner.error,
      ss: state.scenarioRun?.status,
      sc: state.scenarioRun?.counts,
      so: state.scenarioRun?.scenarios.map((o) => o.status).join(","),
      rp: state.latestReport?.generatedAt,
      cs: state.bootstrap?.runnerCatalog.status,
      cl: state.bootstrap?.runnerCatalog.real.length ?? 0,
      cps: state.captureSessions.length,
      cse: state.captureSessions[0]?.eventCount ?? 0,
      cei: state.selectedCaptureSessionIds.join(","),
      cec: state.captureEvents.length,
      ceh: state.captureEvents[0]?.host ?? null,
      ccp: state.captureQueryPreset,
      ccq: state.captureQueryRows.length,
      ccv: state.captureCoverage?.totalEvents ?? 0,
      ccpv: state.captureCoverage?.providers[0]?.value ?? null,
      ccss: state.captureStartupStatus?.proxy.ok ?? null,
      ccsg: state.captureStartupStatus?.gateway.ok ?? null,
      ccce: state.captureControlsExpanded,
      ccse: state.captureSummaryExpanded,
      ccsx: state.captureSelectedSessionsExpanded,
      ccsv: state.captureSavedViews.map((view) => `${view.id}:${view.name}`).join("|"),
      scc: state.sidebarCollapsed,
      scp: state.sidebarPanel,
      cck: state.captureKindFilter.join(","),
      ccpf: state.captureProviderFilter.join(","),
      cchf: state.captureHostFilter.join(","),
      cchm: state.captureHeaderMode,
      ccgm: state.captureGroupMode,
      cctl: state.captureTimelineLaneMode,
      ccts: state.captureTimelineLaneSort,
      cctps: state.captureTimelinePreviousLaneSort,
      cctq: state.captureTimelineLaneSearch,
      cctz: state.captureTimelineZoom,
      cctsm: state.captureTimelineSparklineMode,
      cctws: state.captureTimelineWindowStartPct,
      cctwe: state.captureTimelineWindowEndPct,
      cctba: state.captureTimelineBrushAnchorPct,
      cctbc: state.captureTimelineBrushCurrentPct,
      cctff: state.captureTimelineFocusSelectedFlow,
      cctfm: state.captureTimelineFocusedLaneMode,
      cctft: state.captureTimelineFocusedLaneThreshold,
      ccdp: state.captureDetailPlacement,
      ccds: state.captureDetailSplitPct,
      ccdsd: state.captureDetailSplitDragging,
      ccdv: state.captureDetailView,
      ccpdv: state.capturePreferredDetailView,
      ccdfl: state.captureFlowDetailLayout,
      ccdpl: state.capturePayloadDetailLayout,
      ccdpe: state.capturePayloadExtent,
      ccpes: state.capturePayloadEventSort,
      ccpef: state.capturePayloadEventFilter,
      ccli: state.captureCollapsedLaneIds.join(","),
      ccpi: state.capturePinnedLaneIds.join(","),
      er: state.error,
    });
  }

  function isSelectOpen(): boolean {
    const active = document.activeElement;
    return !!active && root.contains(active) && active.tagName === "SELECT";
  }

  /* ---------- Data fetching ---------- */

  async function refresh() {
    try {
      const [bootstrap, snapshot, report, outcomes] = await Promise.all([
        getJson<Bootstrap>("/api/bootstrap"),
        getJson<Snapshot>("/api/state"),
        getJson<ReportEnvelope>("/api/report"),
        getJson<OutcomesEnvelope>("/api/outcomes"),
      ]);
      state.bootstrap = bootstrap;
      state.snapshot = snapshot;
      state.latestReport = report.report ?? bootstrap.latestReport;
      state.scenarioRun = outcomes.run;
      if (!state.runnerDraft || !state.runnerDraftDirty) {
        state.runnerDraft = {
          ...bootstrap.runner.selection,
          scenarioIds: [...bootstrap.runner.selection.scenarioIds],
        };
        state.runnerDraftDirty = false;
      }
      if (!state.selectedConversationId) {
        state.selectedConversationId = snapshot.conversations[0]?.id ?? null;
      }
      if (!state.selectedScenarioId) {
        state.selectedScenarioId = bootstrap.scenarios[0]?.id ?? null;
      }
      if (!state.composer.conversationId) {
        state.composer = {
          ...state.composer,
          conversationKind: bootstrap.defaults.conversationKind,
          conversationId: bootstrap.defaults.conversationId,
          senderId: bootstrap.defaults.senderId,
          senderName: bootstrap.defaults.senderName,
        };
      }
      state.error = null;
    } catch (error) {
      state.error = formatErrorMessage(error);
    }

    try {
      const sessions = await getJson<CaptureSessionsEnvelope>("/api/capture/sessions");
      const startupStatusPromise = getJson<CaptureStartupStatusEnvelope>(
        "/api/capture/startup-status",
      );
      state.captureSessions = sessions.sessions;
      const availableSessionIds = new Set(sessions.sessions.map((session) => session.id));
      state.selectedCaptureSessionIds = state.selectedCaptureSessionIds.filter((id) =>
        availableSessionIds.has(id),
      );
      if (state.selectedCaptureSessionIds.length === 0) {
        state.selectedCaptureSessionIds = sessions.sessions[0]?.id ? [sessions.sessions[0].id] : [];
      }
      const startupStatusResult = await Promise.allSettled([startupStatusPromise]);
      state.captureStartupStatus =
        startupStatusResult[0]?.status === "fulfilled" ? startupStatusResult[0].value.status : null;
      if (state.selectedCaptureSessionIds.length > 0) {
        const eventsPromises = state.selectedCaptureSessionIds.map((sessionId) =>
          getJson<CaptureEventsEnvelope>(
            `/api/capture/events?sessionId=${encodeURIComponent(sessionId)}`,
          ),
        );
        const singleSessionId =
          state.selectedCaptureSessionIds.length === 1 ? state.selectedCaptureSessionIds[0] : null;
        const coveragePromise = singleSessionId
          ? getJson<CaptureCoverageEnvelope>(
              `/api/capture/coverage?sessionId=${encodeURIComponent(singleSessionId)}`,
            )
          : Promise.resolve<CaptureCoverageEnvelope | null>(null);
        const queryPromise =
          state.captureQueryPreset === "none"
            ? Promise.resolve<CaptureQueryEnvelope>({ rows: [] })
            : singleSessionId
              ? getJson<CaptureQueryEnvelope>(
                  `/api/capture/query?sessionId=${encodeURIComponent(
                    singleSessionId,
                  )}&preset=${encodeURIComponent(state.captureQueryPreset)}`,
                )
              : Promise.resolve<CaptureQueryEnvelope>({ rows: [] });
        const [eventsResult, coverageResult, queryResult] = await Promise.allSettled([
          Promise.all(eventsPromises),
          coveragePromise,
          queryPromise,
        ]);
        if (eventsResult.status !== "fulfilled") {
          throw eventsResult.reason;
        }
        state.captureEvents = eventsResult.value
          .flatMap((envelope) => envelope.events)
          .toSorted(
            (left, right) =>
              right.ts - left.ts || String(right.id ?? "").localeCompare(String(left.id ?? "")),
          );
        state.captureCoverage =
          coverageResult.status === "fulfilled" && coverageResult.value
            ? coverageResult.value.coverage
            : summarizeCaptureCoverageFromEvents(
                state.selectedCaptureSessionIds,
                state.captureEvents,
              );
        state.captureQueryRows = queryResult.status === "fulfilled" ? queryResult.value.rows : [];
        if (
          !state.selectedCaptureEventKey ||
          !state.captureEvents.some(
            (event) =>
              `${event.id ?? "no-id"}:${event.flowId}:${event.ts}:${event.kind}` ===
              state.selectedCaptureEventKey,
          )
        ) {
          const first = state.captureEvents[0];
          state.selectedCaptureEventKey = first
            ? `${first.id ?? "no-id"}:${first.flowId}:${first.ts}:${first.kind}`
            : null;
        }
      } else {
        state.captureEvents = [];
        state.captureCoverage = null;
        state.captureQueryRows = [];
        state.selectedCaptureEventKey = null;
      }
    } catch (error) {
      state.error = formatErrorMessage(error);
    }

    /* Auto-switch to chat when a run starts so user can watch live */
    const currentRunnerStatus = state.bootstrap?.runner.status ?? null;
    if (currentRunnerStatus === "running" && previousRunnerStatus !== "running") {
      state.activeTab = "chat";
      chatScrollLocked = true;
    }
    previousRunnerStatus = currentRunnerStatus;

    /* Only re-render when data actually changed; defer if a <select> is open */
    const fp = stateFingerprint();
    if (fp !== lastFingerprint) {
      lastFingerprint = fp;
      renderDeferred = true;
    }
    if (renderDeferred && !isSelectOpen()) {
      renderDeferred = false;
      render();
    }
  }

  async function pollUiVersion() {
    if (document.visibilityState === "hidden") {
      return;
    }
    try {
      const payload = await getJsonNoStore<{ version: string | null }>("/api/ui-version");
      if (!currentUiVersion) {
        currentUiVersion = payload.version;
        return;
      }
      if (payload.version && payload.version !== currentUiVersion) {
        window.location.reload();
      }
    } catch {
      // Ignore transient rebuild windows while the dist dir is being rewritten.
    }
  }

  /* ---------- Draft mutations ---------- */

  function updateRunnerDraft(mutator: (draft: RunnerSelection) => RunnerSelection) {
    const fallback = state.bootstrap?.runner.selection;
    if (!state.runnerDraft && fallback) {
      state.runnerDraft = { ...fallback, scenarioIds: [...fallback.scenarioIds] };
    }
    if (!state.runnerDraft) {
      return;
    }
    state.runnerDraft = mutator(state.runnerDraft);
    state.runnerDraftDirty = true;
    render();
  }

  /* ---------- Actions ---------- */

  async function runSelfCheck() {
    state.busy = true;
    state.error = null;
    render();
    try {
      const result = await postJson<{ report: string; outputPath: string }>(
        "/api/scenario/self-check",
        {},
      );
      state.latestReport = {
        outputPath: result.outputPath,
        markdown: result.report,
        generatedAt: new Date().toISOString(),
      };
      state.activeTab = "report";
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function resetState() {
    state.busy = true;
    render();
    try {
      await postJson("/api/reset", {});
      state.latestReport = null;
      state.selectedThreadId = null;
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function sendInbound() {
    const conversationId = state.composer.conversationId.trim();
    const text = state.composer.text.trim();
    if (!conversationId || !text) {
      state.error = "Conversation id and text are required.";
      render();
      return;
    }
    state.busy = true;
    state.error = null;
    render();
    try {
      await postJson("/api/inbound/message", {
        conversation: {
          id: conversationId,
          kind: state.composer.conversationKind,
          ...(state.composer.conversationKind === "channel" ? { title: conversationId } : {}),
        },
        senderId: state.composer.senderId.trim() || "alice",
        senderName: state.composer.senderName.trim() || undefined,
        text,
        ...(state.selectedThreadId ? { threadId: state.selectedThreadId } : {}),
      });
      state.selectedConversationId = conversationId;
      state.composer.text = "";
      chatScrollLocked = true;
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function runSuite() {
    if (!state.runnerDraft) {
      state.error = "Runner selection not ready yet.";
      render();
      return;
    }
    state.busy = true;
    state.error = null;
    render();
    try {
      const result = await postJson<{ runner: { selection: RunnerSelection } }>(
        "/api/scenario/suite",
        {
          providerMode: state.runnerDraft.providerMode,
          primaryModel: state.runnerDraft.primaryModel,
          alternateModel: state.runnerDraft.alternateModel,
          scenarioIds: state.runnerDraft.scenarioIds,
        },
      );
      state.runnerDraft = {
        ...result.runner.selection,
        scenarioIds: [...result.runner.selection.scenarioIds],
      };
      state.runnerDraftDirty = false;
      state.activeTab = "chat";
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function sendKickoff() {
    state.busy = true;
    state.error = null;
    render();
    try {
      await postJson("/api/kickoff", {});
      state.activeTab = "chat";
      chatScrollLocked = true;
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  function downloadReport() {
    if (!state.latestReport?.markdown) {
      return;
    }
    const blob = new Blob([state.latestReport.markdown], { type: "text/markdown;charset=utf-8" });
    const href = URL.createObjectURL(blob);
    const anchor = document.createElement("a");
    anchor.href = href;
    anchor.download = "qa-report.md";
    anchor.click();
    URL.revokeObjectURL(href);
  }

  function toggleTheme() {
    state.theme = state.theme === "dark" ? "light" : "dark";
    localStorage.setItem("qa-lab-theme", state.theme);
    render();
  }

  function toggleSidebar() {
    state.sidebarCollapsed = !state.sidebarCollapsed;
    localStorage.setItem("qa-lab-sidebar-collapsed", state.sidebarCollapsed ? "1" : "0");
    render();
  }

  function setSidebarPanel(panel: UiState["sidebarPanel"]) {
    state.sidebarPanel = panel;
    localStorage.setItem("qa-lab-sidebar-panel", panel);
    if (state.sidebarCollapsed) {
      state.sidebarCollapsed = false;
      localStorage.setItem("qa-lab-sidebar-collapsed", "0");
    }
    render();
  }

  function applyCaptureSavedView(view: CaptureSavedView) {
    state.selectedCaptureSessionIds = [...view.sessionIds];
    state.captureKindFilter = [...view.kindFilter];
    state.captureProviderFilter = [...view.providerFilter];
    state.captureHostFilter = [...view.hostFilter];
    state.captureSearchText = view.searchText;
    state.captureHeaderMode = view.headerMode;
    state.captureViewMode = view.viewMode;
    state.captureGroupMode = view.groupMode;
    state.captureTimelineLaneMode = view.timelineLaneMode;
    state.captureTimelineLaneSort = view.timelineLaneSort;
    state.captureTimelineZoom = view.timelineZoom;
    state.captureTimelineSparklineMode = view.timelineSparklineMode;
    state.captureErrorsOnly = view.errorsOnly;
    state.captureDetailPlacement = view.detailPlacement;
    state.capturePayloadDetailLayout = view.payloadLayout;
    state.capturePayloadExtent = view.payloadExtent;
    state.selectedCaptureEventKey = null;
  }

  function buildCaptureSavedView(name: string): CaptureSavedView {
    return {
      id: crypto.randomUUID(),
      name,
      sessionIds: [...state.selectedCaptureSessionIds],
      kindFilter: [...state.captureKindFilter],
      providerFilter: [...state.captureProviderFilter],
      hostFilter: [...state.captureHostFilter],
      searchText: state.captureSearchText,
      headerMode: state.captureHeaderMode,
      viewMode: state.captureViewMode,
      groupMode: state.captureGroupMode,
      timelineLaneMode: state.captureTimelineLaneMode,
      timelineLaneSort: state.captureTimelineLaneSort,
      timelineZoom: state.captureTimelineZoom,
      timelineSparklineMode: state.captureTimelineSparklineMode,
      errorsOnly: state.captureErrorsOnly,
      detailPlacement: state.captureDetailPlacement,
      payloadLayout: state.capturePayloadDetailLayout,
      payloadExtent: state.capturePayloadExtent,
    };
  }

  /* ---------- Chat scroll tracking ---------- */

  function trackChatScroll() {
    const el = root.querySelector<HTMLElement>("#chat-messages");
    if (!el) {
      return;
    }
    el.addEventListener("scroll", () => {
      const threshold = 40;
      chatScrollLocked = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
    });
  }

  function scrollChatToBottom(force?: boolean) {
    const el = root.querySelector<HTMLElement>("#chat-messages");
    if (!el) {
      return;
    }
    const newCount = state.snapshot?.messages.length ?? 0;
    if (force || (chatScrollLocked && newCount !== previousMessageCount)) {
      el.scrollTop = el.scrollHeight;
    }
    previousMessageCount = newCount;
  }

  /* ---------- Event binding ---------- */

  function bindEvents() {
    /* Tabs */
    root.querySelectorAll<HTMLElement>("[data-tab]").forEach((node) => {
      node.addEventListener("click", () => {
        const nextTab = node.dataset.tab as TabId | undefined;
        if (nextTab) {
          state.activeTab = nextTab;
          render();
        }
      });
    });

    /* Conversation chips */
    root.querySelectorAll<HTMLElement>("[data-conversation-id]").forEach((node) => {
      node.addEventListener("click", () => {
        state.selectedConversationId = node.dataset.conversationId ?? null;
        state.selectedThreadId = null;
        if (state.activeTab !== "chat") {
          state.activeTab = "chat";
        }
        render();
      });
    });

    /* Thread chips */
    root.querySelectorAll<HTMLElement>("[data-thread-select]").forEach((node) => {
      node.addEventListener("click", () => {
        const val = node.dataset.threadSelect;
        if (val === "root") {
          state.selectedThreadId = null;
        } else {
          state.selectedThreadId = val ?? null;
          const conv = node.dataset.threadConv;
          if (conv) {
            state.selectedConversationId = conv;
          }
        }
        render();
      });
    });

    /* Scenario selection (results tab + sidebar) */
    root.querySelectorAll<HTMLElement>("[data-scenario-id]").forEach((node) => {
      node.addEventListener("click", () => {
        state.selectedScenarioId = node.dataset.scenarioId ?? null;
        if (state.activeTab !== "results") {
          state.activeTab = "results";
        }
        render();
      });
    });

    /* Header / sidebar buttons */
    root
      .querySelector<HTMLElement>("[data-action='refresh']")
      ?.addEventListener("click", () => void refresh());
    root
      .querySelector<HTMLElement>("[data-action='reset']")
      ?.addEventListener("click", () => void resetState());
    root
      .querySelector<HTMLElement>("[data-action='toggle-theme']")
      ?.addEventListener("click", toggleTheme);
    root
      .querySelector<HTMLElement>("[data-action='toggle-sidebar']")
      ?.addEventListener("click", toggleSidebar);
    root.querySelectorAll<HTMLElement>("[data-sidebar-panel]").forEach((node) => {
      node.addEventListener("click", () => {
        const panel = node.dataset.sidebarPanel;
        if (panel === "config" || panel === "run" || panel === "scenarios") {
          setSidebarPanel(panel);
        }
      });
    });
    root
      .querySelector<HTMLElement>("[data-action='self-check']")
      ?.addEventListener("click", () => void runSelfCheck());
    root
      .querySelector<HTMLElement>("[data-action='run-suite']")
      ?.addEventListener("click", () => void runSuite());
    root
      .querySelector<HTMLElement>("[data-action='kickoff']")
      ?.addEventListener("click", () => void sendKickoff());
    root
      .querySelector<HTMLElement>("[data-action='send']")
      ?.addEventListener("click", () => void sendInbound());
    root
      .querySelector<HTMLElement>("[data-action='download-report']")
      ?.addEventListener("click", downloadReport);

    /* Scenario All/None */
    root
      .querySelector<HTMLElement>("[data-action='select-all-scenarios']")
      ?.addEventListener("click", () => {
        updateRunnerDraft((d) => ({
          ...d,
          scenarioIds: state.bootstrap?.scenarios.map((s) => s.id) ?? d.scenarioIds,
        }));
      });
    root
      .querySelector<HTMLElement>("[data-action='clear-scenarios']")
      ?.addEventListener("click", () => {
        updateRunnerDraft((d) => ({ ...d, scenarioIds: [] }));
      });

    /* Scenario toggles */
    root.querySelectorAll<HTMLInputElement>("[data-scenario-toggle-id]").forEach((node) => {
      node.addEventListener("change", () => {
        const scenarioId = node.dataset.scenarioToggleId;
        if (!scenarioId) {
          return;
        }
        updateRunnerDraft((draft) => {
          const selected = new Set(draft.scenarioIds);
          if (node.checked) {
            selected.add(scenarioId);
          } else {
            selected.delete(scenarioId);
          }
          const orderedIds = state.bootstrap?.scenarios
            .map((s) => s.id)
            .filter((id) => selected.has(id)) ?? [...selected];
          return { ...draft, scenarioIds: orderedIds };
        });
      });
    });

    /* Config form */
    root.querySelector<HTMLSelectElement>("#provider-mode")?.addEventListener("change", (e) => {
      const mode =
        (e.currentTarget as HTMLSelectElement).value === "live-frontier"
          ? "live-frontier"
          : "mock-openai";
      updateRunnerDraft((d) => ({
        ...d,
        providerMode: mode,
        ...defaultModelsForProviderMode(mode, state.bootstrap),
      }));
    });
    root.querySelector<HTMLSelectElement>("#primary-model")?.addEventListener("change", (e) => {
      const primaryModel = (e.currentTarget as HTMLSelectElement).value;
      updateRunnerDraft((d) => ({
        ...d,
        primaryModel,
        fastMode: isQaFastModeEnabled({ primaryModel, alternateModel: d.alternateModel }),
      }));
    });
    root.querySelector<HTMLSelectElement>("#alternate-model")?.addEventListener("change", (e) => {
      const alternateModel = (e.currentTarget as HTMLSelectElement).value;
      updateRunnerDraft((d) => ({
        ...d,
        alternateModel,
        fastMode: isQaFastModeEnabled({ primaryModel: d.primaryModel, alternateModel }),
      }));
    });

    root.querySelector<HTMLSelectElement>("#capture-session")?.addEventListener("change", (e) => {
      state.selectedCaptureSessionIds = readMultiSelect(e.currentTarget as HTMLSelectElement);
      state.selectedCaptureEventKey = null;
      void refresh();
    });
    root.querySelector<HTMLButtonElement>("#capture-save-view")?.addEventListener("click", () => {
      const name = window.prompt("Saved view name");
      const trimmed = name?.trim();
      if (!trimmed) {
        return;
      }
      state.captureSavedViews = [buildCaptureSavedView(trimmed), ...state.captureSavedViews].slice(
        0,
        12,
      );
      persistCaptureSavedViews(state.captureSavedViews);
      render();
    });
    root
      .querySelector<HTMLSelectElement>("#capture-saved-view")
      ?.addEventListener("change", (e) => {
        const id = (e.currentTarget as HTMLSelectElement).value;
        const view = state.captureSavedViews.find((candidate) => candidate.id === id);
        if (!view) {
          return;
        }
        applyCaptureSavedView(view);
        void refresh();
      });
    root.querySelector<HTMLButtonElement>("#capture-delete-view")?.addEventListener("click", () => {
      const select = root.querySelector<HTMLSelectElement>("#capture-saved-view");
      const id = select?.value?.trim();
      if (!id) {
        return;
      }
      state.captureSavedViews = state.captureSavedViews.filter((view) => view.id !== id);
      persistCaptureSavedViews(state.captureSavedViews);
      render();
    });
    root.querySelectorAll<HTMLButtonElement>("[data-capture-session-remove]").forEach((node) => {
      node.addEventListener("click", () => {
        const sessionId = node.dataset.captureSessionRemove?.trim();
        if (!sessionId) {
          return;
        }
        state.selectedCaptureSessionIds = state.selectedCaptureSessionIds.filter(
          (id) => id !== sessionId,
        );
        state.selectedCaptureEventKey = null;
        void refresh();
      });
    });
    root
      .querySelector<HTMLButtonElement>("#capture-toggle-selected-sessions")
      ?.addEventListener("click", () => {
        state.captureSelectedSessionsExpanded = !state.captureSelectedSessionsExpanded;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-delete-selected-sessions")
      ?.addEventListener("click", async () => {
        if (state.selectedCaptureSessionIds.length === 0) {
          return;
        }
        const confirmed = window.confirm(
          `Delete ${state.selectedCaptureSessionIds.length} selected capture session${
            state.selectedCaptureSessionIds.length === 1 ? "" : "s"
          }?`,
        );
        if (!confirmed) {
          return;
        }
        await postJson("/api/capture/delete-sessions", {
          sessionIds: state.selectedCaptureSessionIds,
        });
        state.selectedCaptureSessionIds = [];
        state.selectedCaptureEventKey = null;
        await refresh();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-purge-all")
      ?.addEventListener("click", async () => {
        const confirmed = window.confirm("Purge all captured sessions, events, and blobs?");
        if (!confirmed) {
          return;
        }
        await postJson("/api/capture/purge", {});
        state.selectedCaptureSessionIds = [];
        state.selectedCaptureEventKey = null;
        await refresh();
      });
    root.querySelector<HTMLSelectElement>("#capture-preset")?.addEventListener("change", (e) => {
      state.captureQueryPreset = (e.currentTarget as HTMLSelectElement)
        .value as UiState["captureQueryPreset"];
      void refresh();
    });
    const readMultiSelect = (select: HTMLSelectElement) =>
      [...select.selectedOptions].map((option) => option.value).filter(Boolean);
    root
      .querySelector<HTMLSelectElement>("#capture-kind-filter")
      ?.addEventListener("change", (e) => {
        state.captureKindFilter = readMultiSelect(e.currentTarget as HTMLSelectElement);
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-provider-filter")
      ?.addEventListener("change", (e) => {
        state.captureProviderFilter = readMultiSelect(e.currentTarget as HTMLSelectElement);
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-host-filter")
      ?.addEventListener("change", (e) => {
        state.captureHostFilter = readMultiSelect(e.currentTarget as HTMLSelectElement);
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-header-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureHeaderMode = value === "all" || value === "hidden" ? value : "key";
        render();
      });
    root.querySelector<HTMLSelectElement>("#capture-view-mode")?.addEventListener("change", (e) => {
      state.captureViewMode =
        (e.currentTarget as HTMLSelectElement).value === "timeline" ? "timeline" : "list";
      state.captureCollapsedLaneIds = [];
      state.capturePinnedLaneIds = [];
      state.captureTimelineWindowStartPct = null;
      state.captureTimelineWindowEndPct = null;
      state.captureTimelineBrushAnchorPct = null;
      state.captureTimelineBrushCurrentPct = null;
      state.selectedCaptureEventKey = null;
      render();
    });
    root
      .querySelector<HTMLSelectElement>("#capture-group-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureGroupMode =
          value === "flow" || value === "host-path" || value === "burst" ? value : "none";
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-lane-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureTimelineLaneMode = value === "provider" || value === "flow" ? value : "domain";
        state.captureTimelinePreviousLaneSort = null;
        state.captureCollapsedLaneIds = [];
        state.capturePinnedLaneIds = [];
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-lane-sort")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        const nextSort =
          value === "most-errors" || value === "severity" || value === "alphabetical"
            ? value
            : "most-events";
        if (nextSort !== state.captureTimelineLaneSort) {
          state.captureTimelinePreviousLaneSort = state.captureTimelineLaneSort;
        }
        state.captureTimelineLaneSort = nextSort;
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-timeline-lane-search")
      ?.addEventListener("input", (e) => {
        state.captureTimelineLaneSearch = (e.currentTarget as HTMLInputElement).value ?? "";
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-zoom")
      ?.addEventListener("change", (e) => {
        const value = Number((e.currentTarget as HTMLSelectElement).value);
        state.captureTimelineZoom =
          value === 75 || value === 150 || value === 200 || value === 300 ? value : 100;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-sparkline-mode")
      ?.addEventListener("change", (e) => {
        state.captureTimelineSparklineMode =
          (e.currentTarget as HTMLSelectElement).value === "lane-relative"
            ? "lane-relative"
            : "session-relative";
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-timeline-clear-window")
      ?.addEventListener("click", () => {
        state.captureTimelineWindowStartPct = null;
        state.captureTimelineWindowEndPct = null;
        state.captureTimelineBrushAnchorPct = null;
        state.captureTimelineBrushCurrentPct = null;
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-timeline-focus-flow")
      ?.addEventListener("change", (e) => {
        state.captureTimelineFocusSelectedFlow = (e.currentTarget as HTMLInputElement).checked;
        if (!state.captureTimelineFocusSelectedFlow) {
          state.captureTimelineFocusedLaneMode = "all";
          state.captureTimelineFocusedLaneThreshold = "any";
        }
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-focused-lane-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureTimelineFocusedLaneMode =
          value === "only-matching" || value === "collapse-background" ? value : "all";
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-focused-lane-threshold")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureTimelineFocusedLaneThreshold =
          value === "events-2" || value === "percent-10" || value === "percent-25" ? value : "any";
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-detail-placement")
      ?.addEventListener("change", (e) => {
        state.captureDetailPlacement =
          (e.currentTarget as HTMLSelectElement).value === "bottom" ? "bottom" : "right";
        render();
      });
    root
      .querySelector<HTMLElement>("[data-capture-detail-splitter]")
      ?.addEventListener("mousedown", (event) => {
        if (event.button !== 0 || state.captureDetailPlacement !== "right") {
          return;
        }
        const splitRoot = root.querySelector<HTMLElement>("[data-capture-detail-split-root]");
        if (!splitRoot) {
          return;
        }
        const rect = splitRoot.getBoundingClientRect();
        state.captureDetailSplitDragging = true;
        render();
        const handleMove = (moveEvent: MouseEvent) => {
          const localX = moveEvent.clientX - rect.left;
          const nextPct = ((rect.width - localX) / rect.width) * 100;
          state.captureDetailSplitPct = Math.max(22, Math.min(55, Number(nextPct.toFixed(2))));
          render();
        };
        const handleUp = () => {
          state.captureDetailSplitDragging = false;
          window.removeEventListener("mousemove", handleMove);
          window.removeEventListener("mouseup", handleUp);
          render();
        };
        window.addEventListener("mousemove", handleMove);
        window.addEventListener("mouseup", handleUp);
        event.preventDefault();
      });
    root
      .querySelector<HTMLElement>("[data-capture-detail-splitter]")
      ?.addEventListener("dblclick", () => {
        state.captureDetailSplitPct = 34;
        state.captureDetailSplitDragging = false;
        render();
      });
    root.querySelectorAll<HTMLInputElement>('input[name="capture-detail-view"]').forEach((node) => {
      node.addEventListener("change", () => {
        if (!node.checked) {
          return;
        }
        const value = node.value;
        state.captureDetailView =
          value === "flow" || value === "payload" || value === "headers" ? value : "overview";
        state.capturePreferredDetailView = state.captureDetailView;
        render();
      });
    });
    root.querySelectorAll<HTMLInputElement>('input[name="capture-flow-layout"]').forEach((node) => {
      node.addEventListener("change", () => {
        if (!node.checked) {
          return;
        }
        state.captureFlowDetailLayout = node.value === "pair-first" ? "pair-first" : "nav-first";
        render();
      });
    });
    root
      .querySelectorAll<HTMLInputElement>('input[name="capture-payload-layout"]')
      .forEach((node) => {
        node.addEventListener("change", () => {
          if (!node.checked) {
            return;
          }
          state.capturePayloadDetailLayout = node.value === "raw" ? "raw" : "formatted";
          render();
        });
      });
    root
      .querySelectorAll<HTMLInputElement>('input[name="capture-payload-extent"]')
      .forEach((node) => {
        node.addEventListener("change", () => {
          if (!node.checked) {
            return;
          }
          state.capturePayloadExtent = node.value === "full" ? "full" : "preview";
          render();
        });
      });
    root
      .querySelectorAll<HTMLInputElement>('input[name="capture-payload-event-sort"]')
      .forEach((node) => {
        node.addEventListener("change", () => {
          if (!node.checked) {
            return;
          }
          state.capturePayloadEventSort =
            node.value === "name" || node.value === "size" ? node.value : "stream";
          render();
        });
      });
    root
      .querySelector<HTMLInputElement>("#capture-payload-event-filter")
      ?.addEventListener("input", (e) => {
        state.capturePayloadEventFilter = (e.currentTarget as HTMLInputElement).value ?? "";
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-search-filter")
      ?.addEventListener("input", (e) => {
        state.captureSearchText = (e.currentTarget as HTMLInputElement).value ?? "";
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-errors-only")
      ?.addEventListener("change", (e) => {
        state.captureErrorsOnly = (e.currentTarget as HTMLInputElement).checked;
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-summary-toggle")
      ?.addEventListener("click", () => {
        state.captureSummaryExpanded = !state.captureSummaryExpanded;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-controls-toggle")
      ?.addEventListener("click", () => {
        state.captureControlsExpanded = !state.captureControlsExpanded;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-clear-filters")
      ?.addEventListener("click", () => {
        state.captureKindFilter = [];
        state.captureProviderFilter = [];
        state.captureHostFilter = [];
        state.captureSearchText = "";
        state.captureHeaderMode = "key";
        state.captureViewMode = "list";
        state.captureGroupMode = "none";
        state.captureTimelineLaneMode = "domain";
        state.captureTimelineLaneSort = "most-events";
        state.captureTimelinePreviousLaneSort = null;
        state.captureTimelineLaneSearch = "";
        state.captureTimelineZoom = 100;
        state.captureTimelineSparklineMode = "session-relative";
        state.captureTimelineWindowStartPct = null;
        state.captureTimelineWindowEndPct = null;
        state.captureTimelineBrushAnchorPct = null;
        state.captureTimelineBrushCurrentPct = null;
        state.captureTimelineFocusSelectedFlow = false;
        state.captureTimelineFocusedLaneMode = "all";
        state.captureTimelineFocusedLaneThreshold = "any";
        state.captureErrorsOnly = false;
        state.captureCollapsedLaneIds = [];
        state.capturePinnedLaneIds = [];
        state.selectedCaptureEventKey = null;
        render();
      });
    root.querySelectorAll<HTMLElement>("[data-capture-lane-toggle]").forEach((node) => {
      node.addEventListener("click", () => {
        const laneId = node.dataset.captureLaneToggle;
        if (!laneId) {
          return;
        }
        const collapsed = new Set(state.captureCollapsedLaneIds);
        if (collapsed.has(laneId)) {
          collapsed.delete(laneId);
        } else {
          collapsed.add(laneId);
        }
        state.captureCollapsedLaneIds = [...collapsed];
        render();
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-lane-pin]").forEach((node) => {
      node.addEventListener("click", () => {
        const laneId = node.dataset.captureLanePin;
        if (!laneId) {
          return;
        }
        const pinned = new Set(state.capturePinnedLaneIds);
        if (pinned.has(laneId)) {
          pinned.delete(laneId);
        } else {
          pinned.add(laneId);
        }
        state.capturePinnedLaneIds = [...pinned];
        render();
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-event]").forEach((node) => {
      node.addEventListener("click", () => {
        state.selectedCaptureEventKey = node.dataset.captureEvent ?? null;
        render();
      });
    });
    root.querySelectorAll<HTMLButtonElement>("[data-copy-text]").forEach((node) => {
      node.addEventListener("click", async () => {
        const text = node.dataset.copyText ?? "";
        if (!text) {
          return;
        }
        await navigator.clipboard.writeText(text).catch(() => undefined);
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-sparkline-window]").forEach((node) => {
      const readWindow = () => {
        const start = Number(node.dataset.captureWindowStart ?? "NaN");
        const end = Number(node.dataset.captureWindowEnd ?? "NaN");
        return Number.isFinite(start) && Number.isFinite(end) ? { start, end } : null;
      };
      node.addEventListener("mousedown", (event) => {
        if (event.button !== 0) {
          return;
        }
        const windowRange = readWindow();
        if (!windowRange) {
          return;
        }
        sparklineSweepActive = true;
        sparklineSweepAnchorStartPct = windowRange.start;
        sparklineSweepAnchorEndPct = windowRange.end;
        sparklineSweepCurrentStartPct = windowRange.start;
        sparklineSweepCurrentEndPct = windowRange.end;
        state.captureTimelineBrushAnchorPct = windowRange.start;
        state.captureTimelineBrushCurrentPct = windowRange.end;
        render();
      });
      node.addEventListener("mouseenter", () => {
        if (!sparklineSweepActive) {
          return;
        }
        const windowRange = readWindow();
        if (!windowRange) {
          return;
        }
        sparklineSweepCurrentStartPct = windowRange.start;
        sparklineSweepCurrentEndPct = windowRange.end;
        const previewStart = Math.min(
          sparklineSweepAnchorStartPct ?? windowRange.start,
          windowRange.start,
        );
        const previewEnd = Math.max(sparklineSweepAnchorEndPct ?? windowRange.end, windowRange.end);
        state.captureTimelineBrushAnchorPct = previewStart;
        state.captureTimelineBrushCurrentPct = previewEnd;
        render();
      });
    });
    const timelineViewports = [...root.querySelectorAll<HTMLElement>(".capture-timeline-viewport")];
    timelineViewports.forEach((node) => {
      node.addEventListener("scroll", () => {
        if (syncingCaptureTimelineScroll) {
          return;
        }
        syncingCaptureTimelineScroll = true;
        const nextLeft = node.scrollLeft;
        for (const other of timelineViewports) {
          if (other !== node && other.scrollLeft !== nextLeft) {
            other.scrollLeft = nextLeft;
          }
        }
        syncingCaptureTimelineScroll = false;
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-timeline-brush-surface]").forEach((node) => {
      node.addEventListener("mousedown", (event) => {
        if (event.button !== 0) {
          return;
        }
        const viewport = node;
        const trackWidth = Number(viewport.dataset.captureTimelineTrackWidth ?? "0");
        if (!Number.isFinite(trackWidth) || trackWidth <= 0) {
          return;
        }
        const percentFromEvent = (clientX: number) => {
          const rect = viewport.getBoundingClientRect();
          const localX = clientX - rect.left + viewport.scrollLeft;
          return Math.min(100, Math.max(0, (localX / trackWidth) * 100));
        };
        const anchorPct = percentFromEvent(event.clientX);
        state.captureTimelineBrushAnchorPct = anchorPct;
        state.captureTimelineBrushCurrentPct = anchorPct;
        render();
        const handleMove = (moveEvent: MouseEvent) => {
          state.captureTimelineBrushCurrentPct = percentFromEvent(moveEvent.clientX);
          render();
        };
        const handleUp = () => {
          const anchor = state.captureTimelineBrushAnchorPct;
          const current = state.captureTimelineBrushCurrentPct;
          if (anchor != null && current != null) {
            const start = Math.min(anchor, current);
            const end = Math.max(anchor, current);
            if (end - start >= 1) {
              state.captureTimelineWindowStartPct = start;
              state.captureTimelineWindowEndPct = end;
              state.selectedCaptureEventKey = null;
            }
          }
          state.captureTimelineBrushAnchorPct = null;
          state.captureTimelineBrushCurrentPct = null;
          window.removeEventListener("mousemove", handleMove);
          window.removeEventListener("mouseup", handleUp);
          render();
        };
        window.addEventListener("mousemove", handleMove);
        window.addEventListener("mouseup", handleUp);
      });
    });
    if (!captureGlobalListenersBound) {
      captureGlobalListenersBound = true;
      window.addEventListener("mouseup", (event) => {
        if (!sparklineSweepActive) {
          return;
        }
        const anchorStart = sparklineSweepAnchorStartPct;
        const anchorEnd = sparklineSweepAnchorEndPct;
        const currentStart = sparklineSweepCurrentStartPct;
        const currentEnd = sparklineSweepCurrentEndPct;
        sparklineSweepActive = false;
        sparklineSweepAnchorStartPct = null;
        sparklineSweepAnchorEndPct = null;
        sparklineSweepCurrentStartPct = null;
        sparklineSweepCurrentEndPct = null;
        if (
          anchorStart == null ||
          anchorEnd == null ||
          currentStart == null ||
          currentEnd == null
        ) {
          state.captureTimelineBrushAnchorPct = null;
          state.captureTimelineBrushCurrentPct = null;
          render();
          return;
        }
        const start = Math.min(anchorStart, currentStart);
        const end = Math.max(anchorEnd, currentEnd);
        const width = Math.max(0.01, end - start);
        const expand = event.shiftKey ? width : 0;
        state.captureTimelineWindowStartPct = Math.max(0, Math.min(100, start - expand));
        state.captureTimelineWindowEndPct = Math.max(0, Math.min(100, end + expand));
        state.captureTimelineBrushAnchorPct = null;
        state.captureTimelineBrushCurrentPct = null;
        state.selectedCaptureEventKey = null;
        render();
      });
      root.addEventListener("keydown", (event) => {
        if (state.activeTab !== "capture") {
          return;
        }
        if (isEditableElement(event.target)) {
          return;
        }
        if (event.key === "1" || event.key === "2" || event.key === "3" || event.key === "4") {
          const radios = [
            ...root.querySelectorAll<HTMLInputElement>('input[name="capture-detail-view"]'),
          ].filter((node) => !node.disabled);
          const index = Number(event.key) - 1;
          const target = radios[index];
          if (target) {
            event.preventDefault();
            target.checked = true;
            state.captureDetailView =
              target.value === "flow" || target.value === "payload" || target.value === "headers"
                ? target.value
                : "overview";
            state.capturePreferredDetailView = state.captureDetailView;
            render();
          }
          return;
        }
        if (state.captureViewMode !== "timeline") {
          return;
        }
        if (
          event.key !== "ArrowLeft" &&
          event.key !== "ArrowRight" &&
          event.key !== "Home" &&
          event.key !== "End" &&
          event.key !== "Escape"
        ) {
          return;
        }
        if (event.key === "Escape") {
          if (
            state.captureTimelineWindowStartPct != null ||
            state.captureTimelineBrushAnchorPct != null
          ) {
            event.preventDefault();
            state.captureTimelineWindowStartPct = null;
            state.captureTimelineWindowEndPct = null;
            state.captureTimelineBrushAnchorPct = null;
            state.captureTimelineBrushCurrentPct = null;
            state.selectedCaptureEventKey = null;
            render();
          }
          return;
        }
        const markers = [
          ...root.querySelectorAll<HTMLElement>(".capture-timeline [data-capture-event]"),
        ];
        if (markers.length === 0) {
          return;
        }
        const currentIndex = markers.findIndex(
          (node) => (node.dataset.captureEvent ?? null) === state.selectedCaptureEventKey,
        );
        let nextIndex = Math.max(currentIndex, 0);
        if (event.key === "Home") {
          nextIndex = 0;
        } else if (event.key === "End") {
          nextIndex = markers.length - 1;
        } else if (event.key === "ArrowLeft") {
          nextIndex = currentIndex <= 0 ? 0 : currentIndex - 1;
        } else if (event.key === "ArrowRight") {
          nextIndex = currentIndex < 0 ? 0 : Math.min(markers.length - 1, currentIndex + 1);
        }
        const next = markers[nextIndex];
        if (!next) {
          return;
        }
        event.preventDefault();
        state.selectedCaptureEventKey = next.dataset.captureEvent ?? null;
        render();
      });
    }

    /* Composer form */
    root.querySelector<HTMLSelectElement>("#conversation-kind")?.addEventListener("change", (e) => {
      state.composer.conversationKind =
        (e.currentTarget as HTMLSelectElement).value === "channel" ? "channel" : "direct";
    });
    root.querySelector<HTMLInputElement>("#conversation-id")?.addEventListener("input", (e) => {
      state.composer.conversationId = (e.currentTarget as HTMLInputElement).value;
    });
    root.querySelector<HTMLInputElement>("#sender-id")?.addEventListener("input", (e) => {
      state.composer.senderId = (e.currentTarget as HTMLInputElement).value;
    });
    root.querySelector<HTMLInputElement>("#sender-name")?.addEventListener("input", (e) => {
      state.composer.senderName = (e.currentTarget as HTMLInputElement).value;
    });

    /* Composer textarea: capture input + Enter-to-send */
    const textarea = root.querySelector<HTMLTextAreaElement>("#composer-text");
    if (textarea) {
      textarea.addEventListener("input", (e) => {
        state.composer.text = (e.currentTarget as HTMLTextAreaElement).value;
        /* Auto-grow */
        textarea.style.height = "auto";
        textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
      });
      textarea.addEventListener("keydown", (e) => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();
          void sendInbound();
        }
      });
    }

    /* Chat scroll tracking */
    trackChatScroll();
  }

  /* ---------- Render ---------- */

  function render() {
    /* Preserve focused element id so we can restore focus after re-render */
    const focusedId = (document.activeElement as HTMLElement)?.id || null;
    const composerText = state.composer.text;

    root.innerHTML = renderQaLabUi(state);
    bindEvents();

    /* Restore composer text (since we re-rendered) */
    const textEl = root.querySelector<HTMLTextAreaElement>("#composer-text");
    if (textEl && composerText) {
      textEl.value = composerText;
      textEl.style.height = "auto";
      textEl.style.height = `${Math.min(textEl.scrollHeight, 120)}px`;
    }

    /* Restore focus */
    if (focusedId) {
      const el = root.querySelector<HTMLElement>(`#${CSS.escape(focusedId)}`);
      if (el && "focus" in el) {
        el.focus();
      }
    }

    if (
      state.activeTab === "capture" &&
      state.captureViewMode === "timeline" &&
      state.selectedCaptureEventKey
    ) {
      const selectedTimelineMarker = root.querySelector<HTMLElement>(
        `.capture-timeline [data-capture-event="${CSS.escape(state.selectedCaptureEventKey)}"]`,
      );
      if (selectedTimelineMarker) {
        selectedTimelineMarker.scrollIntoView({ block: "nearest", inline: "center" });
      }
    }

    /* Auto-scroll chat */
    requestAnimationFrame(() => scrollChatToBottom());
  }

  /* ---------- Bootstrap ---------- */

  render();
  await refresh();
  void pollUiVersion();
  setInterval(() => void refresh(), 1_000);
  setInterval(() => void pollUiVersion(), 1_000);
}

¤ Dauer der Verarbeitung: 0.40 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge