rails-profiler 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/profiler.css +24 -0
  3. data/app/assets/builds/profiler.js +994 -26
  4. data/app/controllers/profiler/api/console_controller.rb +46 -0
  5. data/app/controllers/profiler/api/profiles_controller.rb +1 -1
  6. data/app/controllers/profiler/api/test_runner_controller.rb +115 -0
  7. data/app/controllers/profiler/api/tests_controller.rb +46 -0
  8. data/app/controllers/profiler/test_runner_controller.rb +11 -0
  9. data/app/views/profiler/test_runner/index.html.erb +1 -0
  10. data/config/routes.rb +13 -0
  11. data/lib/profiler/collectors/console_collector.rb +57 -0
  12. data/lib/profiler/collectors/database_collector.rb +1 -1
  13. data/lib/profiler/collectors/test_collector.rb +75 -0
  14. data/lib/profiler/configuration.rb +14 -1
  15. data/lib/profiler/console_profiler.rb +102 -0
  16. data/lib/profiler/instrumentation/irb_instrumentation.rb +21 -0
  17. data/lib/profiler/mcp/resources/failing_tests.rb +40 -0
  18. data/lib/profiler/mcp/resources/slow_tests.rb +45 -0
  19. data/lib/profiler/mcp/server.rb +77 -6
  20. data/lib/profiler/mcp/tools/get_test_profile_detail.rb +126 -0
  21. data/lib/profiler/mcp/tools/query_test_profiles.rb +109 -0
  22. data/lib/profiler/mcp/tools/run_tests.rb +112 -0
  23. data/lib/profiler/railtie.rb +21 -1
  24. data/lib/profiler/test_helpers/minitest_support.rb +39 -0
  25. data/lib/profiler/test_helpers/reporter.rb +121 -0
  26. data/lib/profiler/test_helpers/rspec_support.rb +33 -0
  27. data/lib/profiler/test_profiler.rb +140 -0
  28. data/lib/profiler/test_runner/discovery.rb +57 -0
  29. data/lib/profiler/test_runner/run_store.rb +120 -0
  30. data/lib/profiler/test_runner/runner.rb +106 -0
  31. data/lib/profiler/version.rb +1 -1
  32. metadata +23 -2
@@ -323,6 +323,11 @@
323
323
  var u4 = p2(t2++, 7);
324
324
  return C2(u4.__H, r3) && (u4.__ = n2(), u4.__H = r3, u4.__h = n2), u4.__;
325
325
  }
326
+ function q2(n2, t3) {
327
+ return o2 = 8, T2(function() {
328
+ return n2;
329
+ }, t3);
330
+ }
326
331
  function j2() {
327
332
  for (var n2; n2 = f2.shift(); ) {
328
333
  var t3 = n2.__H;
@@ -1274,10 +1279,10 @@
1274
1279
  const overrideCount = Object.keys(overrides).length;
1275
1280
  const allEntries = Object.entries(variables);
1276
1281
  const filteredEntries = T2(() => {
1277
- const q2 = search.toLowerCase().trim();
1278
- if (!q2) return allEntries;
1282
+ const q3 = search.toLowerCase().trim();
1283
+ if (!q3) return allEntries;
1279
1284
  return allEntries.filter(
1280
- ([k3, v3]) => k3.toLowerCase().includes(q2) || v3.toLowerCase().includes(q2)
1285
+ ([k3, v3]) => k3.toLowerCase().includes(q3) || v3.toLowerCase().includes(q3)
1281
1286
  );
1282
1287
  }, [variables, search]);
1283
1288
  const groups = T2(() => {
@@ -1605,8 +1610,363 @@
1605
1610
  ] });
1606
1611
  }
1607
1612
 
1608
- // app/assets/typescript/profiler/components/ProfileList.tsx
1613
+ // app/assets/typescript/profiler/components/test-runner/TestFileTree.tsx
1614
+ function TestFileTree({ tree, selected, onToggleFile, onToggleDir }) {
1615
+ const [collapsed, setCollapsed] = d2(/* @__PURE__ */ new Set());
1616
+ const toggleCollapse = (dir) => {
1617
+ setCollapsed((prev) => {
1618
+ const next = new Set(prev);
1619
+ next.has(dir) ? next.delete(dir) : next.add(dir);
1620
+ return next;
1621
+ });
1622
+ };
1623
+ const dirSelected = (paths) => {
1624
+ const count = paths.filter((p3) => selected.has(p3)).length;
1625
+ if (count === 0) return "none";
1626
+ if (count === paths.length) return "all";
1627
+ return "partial";
1628
+ };
1629
+ return /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--mono", style: "overflow-y: auto; height: 100%", children: tree.map(({ directory, files }) => {
1630
+ const paths = files.map((f4) => f4.path);
1631
+ const sel = dirSelected(paths);
1632
+ const isCollapsed = collapsed.has(directory);
1633
+ return /* @__PURE__ */ u3("div", { style: "margin-bottom: 4px", children: [
1634
+ /* @__PURE__ */ u3(
1635
+ "div",
1636
+ {
1637
+ style: "display: flex; align-items: center; gap: 6px; padding: 3px 6px; cursor: pointer; border-radius: 4px; background: var(--profiler-bg-secondary, rgba(255,255,255,0.05))",
1638
+ onClick: () => toggleCollapse(directory),
1639
+ children: [
1640
+ /* @__PURE__ */ u3(
1641
+ "input",
1642
+ {
1643
+ type: "checkbox",
1644
+ checked: sel === "all",
1645
+ ref: (el) => {
1646
+ if (el) el.indeterminate = sel === "partial";
1647
+ },
1648
+ onClick: (e3) => {
1649
+ e3.stopPropagation();
1650
+ onToggleDir(directory, paths);
1651
+ },
1652
+ style: "cursor: pointer; flex-shrink: 0"
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ u3("span", { style: "color: var(--profiler-text-muted, #888); flex-shrink: 0", children: isCollapsed ? "\u25B6" : "\u25BC" }),
1656
+ /* @__PURE__ */ u3("span", { style: "color: var(--profiler-accent, #06b6d4); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap", title: directory, children: [
1657
+ directory,
1658
+ "/"
1659
+ ] }),
1660
+ /* @__PURE__ */ u3("span", { style: "color: var(--profiler-text-muted, #888); margin-left: auto; flex-shrink: 0", children: [
1661
+ "(",
1662
+ files.length,
1663
+ ")"
1664
+ ] })
1665
+ ]
1666
+ }
1667
+ ),
1668
+ !isCollapsed && files.map((file) => /* @__PURE__ */ u3(
1669
+ "div",
1670
+ {
1671
+ style: "display: flex; align-items: center; gap: 6px; padding: 2px 6px 2px 24px; cursor: pointer; border-radius: 4px",
1672
+ onClick: () => onToggleFile(file.path),
1673
+ children: [
1674
+ /* @__PURE__ */ u3(
1675
+ "input",
1676
+ {
1677
+ type: "checkbox",
1678
+ checked: selected.has(file.path),
1679
+ onClick: (e3) => {
1680
+ e3.stopPropagation();
1681
+ onToggleFile(file.path);
1682
+ },
1683
+ style: "cursor: pointer; flex-shrink: 0"
1684
+ }
1685
+ ),
1686
+ /* @__PURE__ */ u3("span", { style: "overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--profiler-text, #e2e8f0)", title: file.path, children: file.name })
1687
+ ]
1688
+ },
1689
+ file.path
1690
+ ))
1691
+ ] }, directory);
1692
+ }) });
1693
+ }
1694
+
1695
+ // app/assets/typescript/profiler/components/test-runner/RunOutput.tsx
1696
+ function statusColor(status) {
1697
+ if (status === "passed") return "var(--profiler-success, #10b981)";
1698
+ if (status === "failed") return "var(--profiler-error, #ef4444)";
1699
+ if (status === "running") return "var(--profiler-accent, #06b6d4)";
1700
+ if (status === "killed" || status === "error") return "var(--profiler-warning, #f59e0b)";
1701
+ return "var(--profiler-text-muted, #888)";
1702
+ }
1703
+ function statusLabel(status) {
1704
+ const map = {
1705
+ pending: "\u23F3 Pending",
1706
+ running: "\u25CF Running\u2026",
1707
+ passed: "\u2713 Passed",
1708
+ failed: "\u2717 Failed",
1709
+ killed: "\u25A0 Killed",
1710
+ error: "\u26A0 Error"
1711
+ };
1712
+ return map[status] || status;
1713
+ }
1714
+ function parseAnsi(raw) {
1715
+ const ANSI_STYLES = {
1716
+ "0": "",
1717
+ "1": "font-weight:bold",
1718
+ "31": "color:var(--ansi-red)",
1719
+ "32": "color:var(--ansi-green)",
1720
+ "33": "color:var(--ansi-yellow)",
1721
+ "34": "color:var(--ansi-blue)",
1722
+ "35": "color:var(--ansi-purple)",
1723
+ "36": "color:var(--ansi-cyan)"
1724
+ };
1725
+ const spans = [];
1726
+ let currentStyle = "";
1727
+ const parts = raw.split(/(\x1b\[[0-9;]*m)/);
1728
+ for (const part of parts) {
1729
+ if (part.startsWith("\x1B[") && part.endsWith("m")) {
1730
+ const codes = part.slice(2, -1).split(";");
1731
+ if (codes.includes("0")) {
1732
+ currentStyle = "";
1733
+ } else {
1734
+ const styles = codes.map((c3) => ANSI_STYLES[c3] ?? "").filter(Boolean);
1735
+ currentStyle = [...currentStyle ? [currentStyle] : [], ...styles].join(";");
1736
+ }
1737
+ } else if (part.length > 0) {
1738
+ spans.push({ text: part, style: currentStyle });
1739
+ }
1740
+ }
1741
+ return spans;
1742
+ }
1743
+ function AnsiOutput({ text }) {
1744
+ const spans = parseAnsi(text);
1745
+ if (spans.length === 0) {
1746
+ return /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "No output yet\u2026" });
1747
+ }
1748
+ return /* @__PURE__ */ u3(k, { children: spans.map(
1749
+ (s3, i3) => s3.style ? /* @__PURE__ */ u3("span", { style: s3.style, children: s3.text }, i3) : /* @__PURE__ */ u3("span", { children: s3.text }, i3)
1750
+ ) });
1751
+ }
1752
+ function RunOutput({ run }) {
1753
+ const outputRef = A2(null);
1754
+ y2(() => {
1755
+ if (outputRef.current && run?.status === "running") {
1756
+ outputRef.current.scrollTop = outputRef.current.scrollHeight;
1757
+ }
1758
+ }, [run?.output]);
1759
+ if (!run) {
1760
+ return /* @__PURE__ */ u3("div", { class: "profiler-empty", style: "height: 100%; display: flex; align-items: center; justify-content: center", children: /* @__PURE__ */ u3("div", { children: [
1761
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__title", style: "font-size: 1.1rem", children: "Select files and click Run" }),
1762
+ /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Test output will appear here in real time" })
1763
+ ] }) });
1764
+ }
1765
+ return /* @__PURE__ */ u3("div", { style: "display: flex; flex-direction: column; height: 100%; gap: 12px", children: [
1766
+ /* @__PURE__ */ u3("div", { style: "display: flex; align-items: center; gap: 12px; flex-shrink: 0", children: [
1767
+ /* @__PURE__ */ u3("span", { style: `font-weight: 600; color: ${statusColor(run.status)}`, children: statusLabel(run.status) }),
1768
+ run.duration != null && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
1769
+ (run.duration / 1e3).toFixed(1),
1770
+ "s"
1771
+ ] }),
1772
+ (run.status === "passed" || run.status === "failed") && /* @__PURE__ */ u3("a", { href: "/_profiler?section=tests", class: "profiler-text--xs", style: "color: var(--profiler-accent, #06b6d4); margin-left: auto", children: "View in Profiler \u2192" })
1773
+ ] }),
1774
+ /* @__PURE__ */ u3(
1775
+ "pre",
1776
+ {
1777
+ ref: outputRef,
1778
+ style: "flex: 1; overflow-y: auto; background: var(--profiler-terminal-bg); color: var(--profiler-terminal-text); padding: 14px 16px; border-radius: 6px; font-family: var(--profiler-font-mono); font-size: 12px; line-height: 1.65; margin: 0; white-space: pre-wrap; word-break: break-all; border: 1px solid var(--profiler-border)",
1779
+ children: /* @__PURE__ */ u3(AnsiOutput, { text: run.output || "" })
1780
+ }
1781
+ ),
1782
+ run.files.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "flex-shrink: 0", children: [
1783
+ run.files.length,
1784
+ " file",
1785
+ run.files.length !== 1 ? "s" : "",
1786
+ " \xB7 ",
1787
+ run.framework
1788
+ ] })
1789
+ ] });
1790
+ }
1791
+
1792
+ // app/assets/typescript/profiler/components/test-runner/TestRunnerContent.tsx
1609
1793
  var BASE = "/_profiler";
1794
+ function TestRunnerContent() {
1795
+ const [framework, setFramework] = d2("");
1796
+ const [frameworks, setFrameworks] = d2([]);
1797
+ const [tree, setTree] = d2([]);
1798
+ const [selected, setSelected] = d2(/* @__PURE__ */ new Set());
1799
+ const [loading, setLoading] = d2(true);
1800
+ const [currentRun, setCurrentRun] = d2(null);
1801
+ const [isRunning, setIsRunning] = d2(false);
1802
+ const [error, setError] = d2(null);
1803
+ const loadFiles = q2((fw) => {
1804
+ setLoading(true);
1805
+ fetch(`${BASE}/api/test_runner/files?framework=${fw}`).then((r3) => r3.json()).then((data) => {
1806
+ setFrameworks(data.frameworks || []);
1807
+ setTree(data.tree || []);
1808
+ setSelected(/* @__PURE__ */ new Set());
1809
+ setLoading(false);
1810
+ }).catch(() => {
1811
+ setError("Failed to load test files");
1812
+ setLoading(false);
1813
+ });
1814
+ }, []);
1815
+ y2(() => {
1816
+ fetch(`${BASE}/api/test_runner/files`).then((r3) => r3.json()).then((data) => {
1817
+ const fws = (data.frameworks || []).map(String);
1818
+ setFrameworks(fws);
1819
+ const defaultFw = fws[0] || "minitest";
1820
+ setFramework(defaultFw);
1821
+ return fetch(`${BASE}/api/test_runner/files?framework=${defaultFw}`);
1822
+ }).then((r3) => r3.json()).then((data) => {
1823
+ setTree(data.tree || []);
1824
+ setLoading(false);
1825
+ }).catch(() => {
1826
+ setError("Failed to load test files");
1827
+ setLoading(false);
1828
+ });
1829
+ }, []);
1830
+ y2(() => {
1831
+ if (!currentRun || !isRunning) return;
1832
+ if (["passed", "failed", "killed", "error"].includes(currentRun.status)) {
1833
+ setIsRunning(false);
1834
+ return;
1835
+ }
1836
+ const es = new EventSource(`${BASE}/api/test_runner/runs/${currentRun.id}/stream`);
1837
+ es.addEventListener("output", (e3) => {
1838
+ try {
1839
+ const data = JSON.parse(e3.data);
1840
+ setCurrentRun((prev) => prev ? { ...prev, output: (prev.output || "") + data.chunk } : prev);
1841
+ } catch {
1842
+ }
1843
+ });
1844
+ es.addEventListener("done", (e3) => {
1845
+ try {
1846
+ const data = JSON.parse(e3.data);
1847
+ setCurrentRun((prev) => prev ? { ...prev, status: data.status } : prev);
1848
+ } catch {
1849
+ }
1850
+ setIsRunning(false);
1851
+ es.close();
1852
+ });
1853
+ es.onerror = () => {
1854
+ es.close();
1855
+ fetch(`${BASE}/api/test_runner/runs/${currentRun.id}`).then((r3) => r3.json()).then((data) => {
1856
+ setCurrentRun(data);
1857
+ setIsRunning(false);
1858
+ }).catch(() => setIsRunning(false));
1859
+ };
1860
+ return () => es.close();
1861
+ }, [currentRun?.id, isRunning]);
1862
+ const handleFrameworkChange = (fw) => {
1863
+ setFramework(fw);
1864
+ loadFiles(fw);
1865
+ };
1866
+ const toggleFile = (path) => {
1867
+ setSelected((prev) => {
1868
+ const next = new Set(prev);
1869
+ next.has(path) ? next.delete(path) : next.add(path);
1870
+ return next;
1871
+ });
1872
+ };
1873
+ const toggleDir = (_dir, paths) => {
1874
+ setSelected((prev) => {
1875
+ const next = new Set(prev);
1876
+ const allSelected = paths.every((p3) => next.has(p3));
1877
+ if (allSelected) {
1878
+ paths.forEach((p3) => next.delete(p3));
1879
+ } else {
1880
+ paths.forEach((p3) => next.add(p3));
1881
+ }
1882
+ return next;
1883
+ });
1884
+ };
1885
+ const selectAll = () => {
1886
+ const all = tree.flatMap((d3) => d3.files.map((f4) => f4.path));
1887
+ setSelected(new Set(all));
1888
+ };
1889
+ const selectNone = () => setSelected(/* @__PURE__ */ new Set());
1890
+ const runTests = () => {
1891
+ if (selected.size === 0 || isRunning) return;
1892
+ setError(null);
1893
+ fetch(`${BASE}/api/test_runner/runs`, {
1894
+ method: "POST",
1895
+ headers: { "Content-Type": "application/json" },
1896
+ body: JSON.stringify({ files: Array.from(selected), framework })
1897
+ }).then((r3) => r3.json()).then((data) => {
1898
+ setCurrentRun(data);
1899
+ setIsRunning(true);
1900
+ }).catch(() => setError("Failed to start test run"));
1901
+ };
1902
+ const stopRun = () => {
1903
+ if (!currentRun) return;
1904
+ fetch(`${BASE}/api/test_runner/runs/${currentRun.id}`, { method: "DELETE" }).then(() => {
1905
+ setIsRunning(false);
1906
+ setCurrentRun((prev) => prev ? { ...prev, status: "killed" } : prev);
1907
+ }).catch(() => {
1908
+ });
1909
+ };
1910
+ const totalFiles = tree.reduce((n2, d3) => n2 + d3.files.length, 0);
1911
+ return /* @__PURE__ */ u3("div", { children: [
1912
+ error && /* @__PURE__ */ u3("div", { style: "color: var(--profiler-error, #ef4444); background: rgba(239,68,68,0.1); border: 1px solid var(--profiler-error, #ef4444); border-radius: 4px; padding: 8px 12px; margin-bottom: 12px; font-size: 13px", children: error }),
1913
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
1914
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: frameworks.length > 0 && frameworks.map((fw) => /* @__PURE__ */ u3(
1915
+ "button",
1916
+ {
1917
+ class: `profiler-preset-btn${framework === fw ? " profiler-preset-btn--active" : ""}`,
1918
+ onClick: () => handleFrameworkChange(fw),
1919
+ disabled: isRunning,
1920
+ children: fw === "rspec" ? "RSpec" : "Minitest"
1921
+ },
1922
+ fw
1923
+ )) }),
1924
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", style: "margin-left: auto", children: [
1925
+ /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
1926
+ selected.size,
1927
+ " / ",
1928
+ totalFiles,
1929
+ " selected"
1930
+ ] }),
1931
+ /* @__PURE__ */ u3("button", { class: "btn btn-secondary btn-sm", onClick: selectAll, disabled: isRunning || loading, children: "All" }),
1932
+ /* @__PURE__ */ u3("button", { class: "btn btn-secondary btn-sm", onClick: selectNone, disabled: isRunning, children: "None" }),
1933
+ isRunning ? /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: stopRun, children: "\u25A0 Stop" }) : /* @__PURE__ */ u3(
1934
+ "button",
1935
+ {
1936
+ class: "btn btn-sm",
1937
+ style: "background: var(--profiler-accent, #06b6d4); color: #000; font-weight: 600",
1938
+ onClick: runTests,
1939
+ disabled: selected.size === 0,
1940
+ children: [
1941
+ "\u25B6 Run Selected (",
1942
+ selected.size,
1943
+ ")"
1944
+ ]
1945
+ }
1946
+ )
1947
+ ] })
1948
+ ] }),
1949
+ /* @__PURE__ */ u3("div", { style: "display: grid; grid-template-columns: 260px 1fr; height: 460px; border: 1px solid var(--profiler-border, rgba(255,255,255,0.1)); border-radius: 6px; overflow: hidden", children: [
1950
+ /* @__PURE__ */ u3("div", { style: "border-right: 1px solid var(--profiler-border, rgba(255,255,255,0.1)); overflow-y: auto; padding: 8px", children: loading ? /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "padding: 8px", children: "Loading files\u2026" }) : tree.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "padding: 8px", children: [
1951
+ "No ",
1952
+ framework,
1953
+ " test files found"
1954
+ ] }) : /* @__PURE__ */ u3(
1955
+ TestFileTree,
1956
+ {
1957
+ tree,
1958
+ selected,
1959
+ onToggleFile: toggleFile,
1960
+ onToggleDir: toggleDir
1961
+ }
1962
+ ) }),
1963
+ /* @__PURE__ */ u3("div", { style: "overflow: hidden; display: flex; flex-direction: column", children: /* @__PURE__ */ u3(RunOutput, { run: currentRun }) })
1964
+ ] })
1965
+ ] });
1966
+ }
1967
+
1968
+ // app/assets/typescript/profiler/components/ProfileList.tsx
1969
+ var BASE2 = "/_profiler";
1610
1970
  function TableSkeleton({ cols, rows = 6 }) {
1611
1971
  return /* @__PURE__ */ u3("div", { children: Array.from({ length: rows }).map((_2, i3) => /* @__PURE__ */ u3("div", { class: "profiler-skeleton__row", children: cols.map((size, j3) => /* @__PURE__ */ u3("div", { class: `profiler-skeleton__cell profiler-skeleton__cell--${size}` }, j3)) }, i3)) });
1612
1972
  }
@@ -1644,7 +2004,7 @@
1644
2004
  const params = new URLSearchParams(window.location.search);
1645
2005
  const initialSection = () => {
1646
2006
  const s3 = params.get("section");
1647
- return s3 === "http" || s3 === "jobs" || s3 === "outbound" || s3 === "env" ? s3 : "http";
2007
+ return s3 === "http" || s3 === "jobs" || s3 === "console" || s3 === "tests" || s3 === "runner" || s3 === "outbound" || s3 === "env" ? s3 : "http";
1648
2008
  };
1649
2009
  const initialSort = () => {
1650
2010
  const col = params.get("sort");
@@ -1663,12 +2023,25 @@
1663
2023
  const [jobOffset, setJobOffset] = d2(0);
1664
2024
  const [jobHasMore, setJobHasMore] = d2(false);
1665
2025
  const [jobLoadingMore, setJobLoadingMore] = d2(false);
2026
+ const [consoles, setConsoles] = d2([]);
2027
+ const [consoleOffset, setConsoleOffset] = d2(0);
2028
+ const [consoleHasMore, setConsoleHasMore] = d2(false);
2029
+ const [consoleLoadingMore, setConsoleLoadingMore] = d2(false);
2030
+ const [tests, setTests] = d2([]);
2031
+ const [testOffset, setTestOffset] = d2(0);
2032
+ const [testHasMore, setTestHasMore] = d2(false);
2033
+ const [testLoadingMore, setTestLoadingMore] = d2(false);
2034
+ const [loadingTests, setLoadingTests] = d2(initialSection() === "tests");
2035
+ const [testsLoaded, setTestsLoaded] = d2(false);
2036
+ const [testsError, setTestsError] = d2(null);
1666
2037
  const [outboundRequests, setOutboundRequests] = d2([]);
1667
2038
  const [loadingHttp, setLoadingHttp] = d2(initialSection() === "http");
1668
2039
  const [loadingJobs, setLoadingJobs] = d2(initialSection() === "jobs");
2040
+ const [loadingConsole, setLoadingConsole] = d2(initialSection() === "console");
1669
2041
  const [loadingOutbound, setLoadingOutbound] = d2(initialSection() === "outbound");
1670
2042
  const [error, setError] = d2(null);
1671
2043
  const [jobsError, setJobsError] = d2(null);
2044
+ const [consoleError, setConsoleError] = d2(null);
1672
2045
  const [outboundError, setOutboundError] = d2(null);
1673
2046
  const [envData, setEnvData] = d2(void 0);
1674
2047
  const [loadingEnv, setLoadingEnv] = d2(initialSection() === "env");
@@ -1684,9 +2057,15 @@
1684
2057
  });
1685
2058
  const [httpSort, setHttpSort] = d2(initialSort);
1686
2059
  const [jobSort, setJobSort] = d2({ col: null, dir: "asc" });
2060
+ const [consoleSort, setConsoleSort] = d2({ col: null, dir: "asc" });
1687
2061
  const [jobSearch, setJobSearch] = d2("");
1688
2062
  const [jobStatus, setJobStatus] = d2("");
1689
2063
  const [jobDuration, setJobDuration] = d2("");
2064
+ const [consoleSearch, setConsoleSearch] = d2("");
2065
+ const [consoleStatus, setConsoleStatus] = d2("");
2066
+ const [testSearch, setTestSearch] = d2("");
2067
+ const [testStatus, setTestStatus] = d2("");
2068
+ const [testSort, setTestSort] = d2({ col: null, dir: "asc" });
1690
2069
  const [outboundSearch, setOutboundSearch] = d2("");
1691
2070
  const [outboundMethod, setOutboundMethod] = d2("");
1692
2071
  const [outboundStatus, setOutboundStatus] = d2("");
@@ -1724,7 +2103,7 @@
1724
2103
  };
1725
2104
  const loadMoreHttp = () => {
1726
2105
  setHttpLoadingMore(true);
1727
- fetch(`${BASE}/api/profiles?limit=50&offset=${httpOffset}`).then((res) => res.json()).then((data) => {
2106
+ fetch(`${BASE2}/api/profiles?limit=50&offset=${httpOffset}`).then((res) => res.json()).then((data) => {
1728
2107
  setProfiles((prev) => [...prev, ...data.profiles]);
1729
2108
  setHttpOffset((prev) => prev + data.profiles.length);
1730
2109
  setHttpHasMore(data.has_more);
@@ -1733,13 +2112,46 @@
1733
2112
  };
1734
2113
  const loadMoreJobs = () => {
1735
2114
  setJobLoadingMore(true);
1736
- fetch(`${BASE}/api/jobs?limit=50&offset=${jobOffset}`).then((res) => res.json()).then((data) => {
2115
+ fetch(`${BASE2}/api/jobs?limit=50&offset=${jobOffset}`).then((res) => res.json()).then((data) => {
1737
2116
  setJobs((prev) => [...prev, ...data.profiles]);
1738
2117
  setJobOffset((prev) => prev + data.profiles.length);
1739
2118
  setJobHasMore(data.has_more);
1740
2119
  setJobLoadingMore(false);
1741
2120
  }).catch(() => setJobLoadingMore(false));
1742
2121
  };
2122
+ const loadMoreConsole = () => {
2123
+ setConsoleLoadingMore(true);
2124
+ fetch(`${BASE2}/api/console?limit=50&offset=${consoleOffset}`).then((res) => res.json()).then((data) => {
2125
+ setConsoles((prev) => [...prev, ...data.profiles]);
2126
+ setConsoleOffset((prev) => prev + data.profiles.length);
2127
+ setConsoleHasMore(data.has_more);
2128
+ setConsoleLoadingMore(false);
2129
+ }).catch(() => setConsoleLoadingMore(false));
2130
+ };
2131
+ const loadTests = () => {
2132
+ if (testsLoaded) return;
2133
+ setLoadingTests(true);
2134
+ fetch(`${BASE2}/api/tests?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2135
+ setTests(data.profiles);
2136
+ setTestOffset(data.profiles.length);
2137
+ setTestHasMore(data.has_more);
2138
+ setLoadingTests(false);
2139
+ setTestsLoaded(true);
2140
+ }).catch(() => {
2141
+ setTestsError("Failed to load test profiles");
2142
+ setLoadingTests(false);
2143
+ setTestsLoaded(true);
2144
+ });
2145
+ };
2146
+ const loadMoreTests = () => {
2147
+ setTestLoadingMore(true);
2148
+ fetch(`${BASE2}/api/tests?limit=50&offset=${testOffset}`).then((res) => res.json()).then((data) => {
2149
+ setTests((prev) => [...prev, ...data.profiles]);
2150
+ setTestOffset((prev) => prev + data.profiles.length);
2151
+ setTestHasMore(data.has_more);
2152
+ setTestLoadingMore(false);
2153
+ }).catch(() => setTestLoadingMore(false));
2154
+ };
1743
2155
  const handleSectionChange = (s3) => {
1744
2156
  if (s3 === section) {
1745
2157
  refreshSection(s3);
@@ -1749,7 +2161,11 @@
1749
2161
  const url = new URL(window.location.href);
1750
2162
  url.searchParams.set("section", s3);
1751
2163
  history.pushState(null, "", url.toString());
1752
- refreshSection(s3);
2164
+ if (s3 === "tests") {
2165
+ loadTests();
2166
+ } else {
2167
+ refreshSection(s3);
2168
+ }
1753
2169
  setHttpSearch("");
1754
2170
  setHttpMethod("");
1755
2171
  setHttpStatus("");
@@ -1760,6 +2176,12 @@
1760
2176
  setJobStatus("");
1761
2177
  setJobDuration("");
1762
2178
  setJobSort({ col: null, dir: "asc" });
2179
+ setConsoleSearch("");
2180
+ setConsoleStatus("");
2181
+ setConsoleSort({ col: null, dir: "asc" });
2182
+ setTestSearch("");
2183
+ setTestStatus("");
2184
+ setTestSort({ col: null, dir: "asc" });
1763
2185
  setOutboundSearch("");
1764
2186
  setOutboundMethod("");
1765
2187
  setOutboundStatus("");
@@ -1771,41 +2193,65 @@
1771
2193
  });
1772
2194
  };
1773
2195
  const deleteProfile = (token) => {
1774
- fetch(`${BASE}/api/profiles/${token}`, { method: "DELETE" }).then(() => {
2196
+ fetch(`${BASE2}/api/profiles/${token}`, { method: "DELETE" }).then(() => {
1775
2197
  setProfiles((prev) => prev.filter((p3) => p3.token !== token));
1776
2198
  });
1777
2199
  };
1778
2200
  const deleteJob = (token) => {
1779
- fetch(`${BASE}/api/jobs/${token}`, { method: "DELETE" }).then(() => {
2201
+ fetch(`${BASE2}/api/jobs/${token}`, { method: "DELETE" }).then(() => {
1780
2202
  setJobs((prev) => prev.filter((p3) => p3.token !== token));
1781
2203
  });
1782
2204
  };
2205
+ const deleteConsole = (token) => {
2206
+ fetch(`${BASE2}/api/console/${token}`, { method: "DELETE" }).then(() => {
2207
+ setConsoles((prev) => prev.filter((p3) => p3.token !== token));
2208
+ });
2209
+ };
2210
+ const deleteTest = (token) => {
2211
+ fetch(`${BASE2}/api/tests/${token}`, { method: "DELETE" }).then(() => {
2212
+ setTests((prev) => prev.filter((p3) => p3.token !== token));
2213
+ });
2214
+ };
1783
2215
  const clearProfiles = () => {
1784
2216
  if (!window.confirm("Delete all HTTP profiles?")) return;
1785
- fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }).then(() => {
2217
+ fetch(`${BASE2}/api/profiles/clear`, { method: "DELETE" }).then(() => {
1786
2218
  setProfiles([]);
1787
2219
  });
1788
2220
  };
1789
2221
  const clearJobs = () => {
1790
2222
  if (!window.confirm("Delete all job profiles?")) return;
1791
- fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" }).then(() => {
2223
+ fetch(`${BASE2}/api/jobs/clear`, { method: "DELETE" }).then(() => {
1792
2224
  setJobs([]);
1793
2225
  });
1794
2226
  };
2227
+ const clearConsole = () => {
2228
+ if (!window.confirm("Delete all console profiles?")) return;
2229
+ fetch(`${BASE2}/api/console/clear`, { method: "DELETE" }).then(() => {
2230
+ setConsoles([]);
2231
+ });
2232
+ };
2233
+ const clearTests = () => {
2234
+ if (!window.confirm("Delete all test profiles?")) return;
2235
+ fetch(`${BASE2}/api/tests/clear`, { method: "DELETE" }).then(() => {
2236
+ setTests([]);
2237
+ });
2238
+ };
1795
2239
  const clearAll = () => {
1796
- if (!window.confirm("Delete all HTTP and job profiles?")) return;
2240
+ if (!window.confirm("Delete all HTTP, job and console profiles?")) return;
1797
2241
  Promise.all([
1798
- fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }),
1799
- fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" })
2242
+ fetch(`${BASE2}/api/profiles/clear`, { method: "DELETE" }),
2243
+ fetch(`${BASE2}/api/jobs/clear`, { method: "DELETE" }),
2244
+ fetch(`${BASE2}/api/console/clear`, { method: "DELETE" })
1800
2245
  ]).then(() => {
1801
2246
  setProfiles([]);
1802
2247
  setJobs([]);
2248
+ setConsoles([]);
1803
2249
  });
1804
2250
  };
1805
2251
  const refreshSection = (s3) => {
1806
2252
  if (s3 === "http") {
1807
2253
  setLoadingHttp(true);
1808
- fetch(`${BASE}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2254
+ fetch(`${BASE2}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
1809
2255
  setProfiles(data.profiles);
1810
2256
  setHttpOffset(data.profiles.length);
1811
2257
  setHttpHasMore(data.has_more);
@@ -1816,7 +2262,7 @@
1816
2262
  });
1817
2263
  } else if (s3 === "jobs") {
1818
2264
  setLoadingJobs(true);
1819
- fetch(`${BASE}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2265
+ fetch(`${BASE2}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
1820
2266
  setJobs(data.profiles);
1821
2267
  setJobOffset(data.profiles.length);
1822
2268
  setJobHasMore(data.has_more);
@@ -1825,18 +2271,43 @@
1825
2271
  setJobsError("Failed to load job profiles");
1826
2272
  setLoadingJobs(false);
1827
2273
  });
2274
+ } else if (s3 === "console") {
2275
+ setLoadingConsole(true);
2276
+ fetch(`${BASE2}/api/console?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2277
+ setConsoles(data.profiles);
2278
+ setConsoleOffset(data.profiles.length);
2279
+ setConsoleHasMore(data.has_more);
2280
+ setLoadingConsole(false);
2281
+ }).catch(() => {
2282
+ setConsoleError("Failed to load console profiles");
2283
+ setLoadingConsole(false);
2284
+ });
2285
+ } else if (s3 === "tests") {
2286
+ setLoadingTests(true);
2287
+ setTestsLoaded(false);
2288
+ fetch(`${BASE2}/api/tests?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2289
+ setTests(data.profiles);
2290
+ setTestOffset(data.profiles.length);
2291
+ setTestHasMore(data.has_more);
2292
+ setLoadingTests(false);
2293
+ setTestsLoaded(true);
2294
+ }).catch(() => {
2295
+ setTestsError("Failed to load test profiles");
2296
+ setLoadingTests(false);
2297
+ setTestsLoaded(true);
2298
+ });
1828
2299
  } else if (s3 === "outbound") {
1829
2300
  setLoadingOutbound(true);
1830
- fetch(`${BASE}/api/outbound_http`).then((res) => res.json()).then((data) => {
2301
+ fetch(`${BASE2}/api/outbound_http`).then((res) => res.json()).then((data) => {
1831
2302
  setOutboundRequests(data);
1832
2303
  setLoadingOutbound(false);
1833
2304
  }).catch(() => {
1834
2305
  setOutboundError("Failed to load outbound HTTP requests");
1835
2306
  setLoadingOutbound(false);
1836
2307
  });
1837
- } else {
2308
+ } else if (s3 === "env") {
1838
2309
  setLoadingEnv(true);
1839
- fetch(`${BASE}/api/env_vars`).then((res) => res.json()).then((data) => {
2310
+ fetch(`${BASE2}/api/env_vars`).then((res) => res.json()).then((data) => {
1840
2311
  setEnvData(data);
1841
2312
  setLoadingEnv(false);
1842
2313
  }).catch(() => {
@@ -1846,6 +2317,11 @@
1846
2317
  }
1847
2318
  };
1848
2319
  const refresh = () => refreshSection(section);
2320
+ const toggleConsoleSort = (col) => {
2321
+ setConsoleSort(
2322
+ (prev) => prev.col === col ? { col, dir: prev.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }
2323
+ );
2324
+ };
1849
2325
  const tabClass = (s3) => `tab${section === s3 ? " active" : ""}`;
1850
2326
  const filteredProfiles = profiles.filter((p3) => {
1851
2327
  if (httpSearch && !p3.path.toLowerCase().includes(httpSearch.toLowerCase())) return false;
@@ -1933,8 +2409,70 @@
1933
2409
  }
1934
2410
  return true;
1935
2411
  });
2412
+ const filteredConsoles = consoles.filter((p3) => {
2413
+ if (consoleSearch && !p3.path.toLowerCase().includes(consoleSearch.toLowerCase())) return false;
2414
+ if (consoleStatus === "error" && p3.status !== 500) return false;
2415
+ if (consoleStatus === "ok" && p3.status === 500) return false;
2416
+ return true;
2417
+ });
2418
+ const sortedConsoles = consoleSort.col ? [...filteredConsoles].sort((a3, b) => {
2419
+ if (consoleSort.col === "date") {
2420
+ const diff = new Date(a3.started_at).getTime() - new Date(b.started_at).getTime();
2421
+ return consoleSort.dir === "asc" ? diff : -diff;
2422
+ }
2423
+ let av, bv;
2424
+ switch (consoleSort.col) {
2425
+ case "duration":
2426
+ av = a3.duration;
2427
+ bv = b.duration;
2428
+ break;
2429
+ case "status":
2430
+ av = a3.status;
2431
+ bv = b.status;
2432
+ break;
2433
+ default:
2434
+ return 0;
2435
+ }
2436
+ return consoleSort.dir === "asc" ? av - bv : bv - av;
2437
+ }) : filteredConsoles;
2438
+ const filteredTests = tests.filter((p3) => {
2439
+ if (testSearch) {
2440
+ const testData = p3.collectors_data?.test;
2441
+ const name = (testData?.test_name || p3.path).toLowerCase();
2442
+ if (!name.includes(testSearch.toLowerCase())) return false;
2443
+ }
2444
+ if (testStatus === "failed" && p3.status !== 500) return false;
2445
+ if (testStatus === "passed" && p3.status === 500) return false;
2446
+ if (testStatus === "pending") {
2447
+ const testData = p3.collectors_data?.test;
2448
+ if (testData?.status !== "pending") return false;
2449
+ }
2450
+ return true;
2451
+ });
2452
+ const sortedTests = testSort.col ? [...filteredTests].sort((a3, b) => {
2453
+ let av, bv;
2454
+ if (testSort.col === "date") {
2455
+ const diff = new Date(a3.started_at).getTime() - new Date(b.started_at).getTime();
2456
+ return testSort.dir === "asc" ? diff : -diff;
2457
+ }
2458
+ switch (testSort.col) {
2459
+ case "duration":
2460
+ av = a3.duration;
2461
+ bv = b.duration;
2462
+ break;
2463
+ case "status":
2464
+ av = a3.status;
2465
+ bv = b.status;
2466
+ break;
2467
+ default:
2468
+ return 0;
2469
+ }
2470
+ return testSort.dir === "asc" ? av - bv : bv - av;
2471
+ }) : filteredTests;
1936
2472
  const httpFiltersActive = !!(httpSearch || httpMethod || httpStatus || httpDuration || httpPreset);
1937
2473
  const jobFiltersActive = !!(jobSearch || jobStatus || jobDuration);
2474
+ const consoleFiltersActive = !!(consoleSearch || consoleStatus);
2475
+ const testFiltersActive = !!(testSearch || testStatus);
1938
2476
  const outboundFiltersActive = !!(outboundSearch || outboundMethod || outboundStatus);
1939
2477
  const sortIcon = (activeCol, dir, col) => {
1940
2478
  if (activeCol !== col) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
@@ -1962,7 +2500,19 @@
1962
2500
  e3.preventDefault();
1963
2501
  handleSectionChange("jobs");
1964
2502
  }, children: "Background Jobs" }),
2503
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("console"), onClick: (e3) => {
2504
+ e3.preventDefault();
2505
+ handleSectionChange("console");
2506
+ }, children: "Console" }),
2507
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("tests"), onClick: (e3) => {
2508
+ e3.preventDefault();
2509
+ handleSectionChange("tests");
2510
+ }, children: "Tests" }),
1965
2511
  /* @__PURE__ */ u3("div", { style: "flex: 1" }),
2512
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("runner"), onClick: (e3) => {
2513
+ e3.preventDefault();
2514
+ handleSectionChange("runner");
2515
+ }, children: "Test Runner" }),
1966
2516
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("outbound"), onClick: (e3) => {
1967
2517
  e3.preventDefault();
1968
2518
  handleSectionChange("outbound");
@@ -2062,7 +2612,7 @@
2062
2612
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2063
2613
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: methodClass(p3.method), children: p3.method }) }),
2064
2614
  /* @__PURE__ */ u3("td", { children: [
2065
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2615
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, children: p3.path }),
2066
2616
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2067
2617
  ] }),
2068
2618
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
@@ -2143,7 +2693,7 @@
2143
2693
  return /* @__PURE__ */ u3("tr", { children: [
2144
2694
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2145
2695
  /* @__PURE__ */ u3("td", { children: [
2146
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2696
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, children: p3.path }),
2147
2697
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2148
2698
  ] }),
2149
2699
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
@@ -2160,6 +2710,167 @@
2160
2710
  ] }),
2161
2711
  jobHasMore && !jobFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreJobs, disabled: jobLoadingMore, children: jobLoadingMore ? "Loading\u2026" : "Load more" }) })
2162
2712
  ] })),
2713
+ section === "console" && (loadingConsole ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "flex", "sm", "sm", "xs", "sm"] }) : consoleError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: consoleError }) }) : consoles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2714
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No console profiles found" }),
2715
+ /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: [
2716
+ "Run expressions in ",
2717
+ /* @__PURE__ */ u3("code", { children: "rails console" }),
2718
+ " to see profiling data"
2719
+ ] })
2720
+ ] }) : /* @__PURE__ */ u3(k, { children: [
2721
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
2722
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2723
+ /* @__PURE__ */ u3(
2724
+ "input",
2725
+ {
2726
+ type: "text",
2727
+ class: "profiler-filter-input",
2728
+ placeholder: "Search expression\u2026",
2729
+ value: consoleSearch,
2730
+ onInput: (e3) => setConsoleSearch(e3.target.value)
2731
+ }
2732
+ ),
2733
+ /* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: consoleStatus, onChange: (e3) => setConsoleStatus(e3.target.value), children: [
2734
+ /* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
2735
+ /* @__PURE__ */ u3("option", { value: "ok", children: "OK" }),
2736
+ /* @__PURE__ */ u3("option", { value: "error", children: "Error" })
2737
+ ] })
2738
+ ] }),
2739
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2740
+ consoleFiltersActive && /* @__PURE__ */ u3("span", { class: "profiler-filter-count", children: [
2741
+ filteredConsoles.length,
2742
+ " / ",
2743
+ consoles.length
2744
+ ] }),
2745
+ /* @__PURE__ */ u3("button", { class: `btn-refresh${loadingConsole ? " btn-refresh--spinning" : ""}`, onClick: refresh, disabled: loadingConsole, title: "Refresh", children: "\u21BA" }),
2746
+ /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearConsole, title: "Delete console profiles", children: "Clear" }),
2747
+ /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearAll, title: "Delete all profiles", children: "Clear All" })
2748
+ ] })
2749
+ ] }),
2750
+ filteredConsoles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No results match filters" }) }) : /* @__PURE__ */ u3("table", { children: [
2751
+ /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
2752
+ /* @__PURE__ */ u3("th", { class: `sortable${consoleSort.col === "date" ? " sortable--active" : ""}`, onClick: () => toggleConsoleSort("date"), children: [
2753
+ "Time ",
2754
+ sortIcon(consoleSort.col, consoleSort.dir, "date")
2755
+ ] }),
2756
+ /* @__PURE__ */ u3("th", { children: "Expression" }),
2757
+ /* @__PURE__ */ u3("th", { class: `sortable${consoleSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => toggleConsoleSort("duration"), children: [
2758
+ "Duration ",
2759
+ sortIcon(consoleSort.col, consoleSort.dir, "duration")
2760
+ ] }),
2761
+ /* @__PURE__ */ u3("th", { children: "SQL" }),
2762
+ /* @__PURE__ */ u3("th", { class: `sortable${consoleSort.col === "status" ? " sortable--active" : ""}`, onClick: () => toggleConsoleSort("status"), children: [
2763
+ "Status ",
2764
+ sortIcon(consoleSort.col, consoleSort.dir, "status")
2765
+ ] }),
2766
+ /* @__PURE__ */ u3("th", { children: "Token" }),
2767
+ /* @__PURE__ */ u3("th", {})
2768
+ ] }) }),
2769
+ /* @__PURE__ */ u3("tbody", { children: sortedConsoles.map((p3) => {
2770
+ const isError = p3.status === 500;
2771
+ return /* @__PURE__ */ u3("tr", { children: [
2772
+ /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2773
+ /* @__PURE__ */ u3("td", { children: [
2774
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, class: "profiler-text--mono", style: "font-size:0.85em;", children: p3.path.length > 60 ? p3.path.slice(0, 60) + "\u2026" : p3.path }),
2775
+ p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2776
+ ] }),
2777
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2778
+ p3.duration.toFixed(2),
2779
+ " ms"
2780
+ ] }) }),
2781
+ /* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
2782
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: isError ? "badge-error" : "badge-success", children: isError ? "\u2717 Error" : "\u2713 OK" }) }),
2783
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
2784
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteConsole(p3.token), title: "Delete", children: "\xD7" }) })
2785
+ ] }, p3.token);
2786
+ }) })
2787
+ ] }),
2788
+ consoleHasMore && !consoleFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreConsole, disabled: consoleLoadingMore, children: consoleLoadingMore ? "Loading\u2026" : "Load more" }) })
2789
+ ] })),
2790
+ section === "tests" && (loadingTests ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "flex", "md", "sm", "xs", "sm"] }) : testsError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: testsError }) }) : tests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2791
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No test profiles found" }),
2792
+ /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: [
2793
+ "Add ",
2794
+ /* @__PURE__ */ u3("code", { children: "Profiler::TestHelpers::RSpecSupport.install(config)" }),
2795
+ " to your spec_helper.rb and run your tests. Or use the ",
2796
+ /* @__PURE__ */ u3("a", { href: "#", style: "color:var(--profiler-accent,#06b6d4)", onClick: (e3) => {
2797
+ e3.preventDefault();
2798
+ handleSectionChange("runner");
2799
+ }, children: "Test Runner" }),
2800
+ " tab to run tests from here."
2801
+ ] })
2802
+ ] }) : /* @__PURE__ */ u3(k, { children: [
2803
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
2804
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2805
+ /* @__PURE__ */ u3(
2806
+ "input",
2807
+ {
2808
+ type: "text",
2809
+ class: "profiler-filter-input",
2810
+ placeholder: "Search test name\u2026",
2811
+ value: testSearch,
2812
+ onInput: (e3) => setTestSearch(e3.target.value)
2813
+ }
2814
+ ),
2815
+ /* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: testStatus, onChange: (e3) => setTestStatus(e3.target.value), children: [
2816
+ /* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
2817
+ /* @__PURE__ */ u3("option", { value: "passed", children: "Passed" }),
2818
+ /* @__PURE__ */ u3("option", { value: "failed", children: "Failed" }),
2819
+ /* @__PURE__ */ u3("option", { value: "pending", children: "Pending" })
2820
+ ] })
2821
+ ] }),
2822
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2823
+ testFiltersActive && /* @__PURE__ */ u3("span", { class: "profiler-filter-count", children: [
2824
+ filteredTests.length,
2825
+ " / ",
2826
+ tests.length
2827
+ ] }),
2828
+ /* @__PURE__ */ u3("button", { class: `btn-refresh${loadingTests ? " btn-refresh--spinning" : ""}`, onClick: refresh, disabled: loadingTests, title: "Refresh", children: "\u21BA" }),
2829
+ /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearTests, title: "Delete test profiles", children: "Clear All" })
2830
+ ] })
2831
+ ] }),
2832
+ filteredTests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No results match filters" }) }) : /* @__PURE__ */ u3("table", { children: [
2833
+ /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
2834
+ /* @__PURE__ */ u3("th", { class: `sortable${testSort.col === "date" ? " sortable--active" : ""}`, onClick: () => setTestSort((prev) => ({ col: "date", dir: prev.col === "date" && prev.dir === "asc" ? "desc" : "asc" })), children: [
2835
+ "Time ",
2836
+ sortIcon(testSort.col, testSort.dir, "date")
2837
+ ] }),
2838
+ /* @__PURE__ */ u3("th", { children: "Test Name" }),
2839
+ /* @__PURE__ */ u3("th", { children: "File" }),
2840
+ /* @__PURE__ */ u3("th", { class: `sortable${testSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => setTestSort((prev) => ({ col: "duration", dir: prev.col === "duration" && prev.dir === "asc" ? "desc" : "asc" })), children: [
2841
+ "Duration ",
2842
+ sortIcon(testSort.col, testSort.dir, "duration")
2843
+ ] }),
2844
+ /* @__PURE__ */ u3("th", { children: "Queries" }),
2845
+ /* @__PURE__ */ u3("th", { class: `sortable${testSort.col === "status" ? " sortable--active" : ""}`, onClick: () => setTestSort((prev) => ({ col: "status", dir: prev.col === "status" && prev.dir === "asc" ? "desc" : "asc" })), children: [
2846
+ "Status ",
2847
+ sortIcon(testSort.col, testSort.dir, "status")
2848
+ ] }),
2849
+ /* @__PURE__ */ u3("th", { children: "Token" }),
2850
+ /* @__PURE__ */ u3("th", {})
2851
+ ] }) }),
2852
+ /* @__PURE__ */ u3("tbody", { children: sortedTests.map((p3) => {
2853
+ const testData = p3.collectors_data?.test;
2854
+ const isFailed = p3.status === 500;
2855
+ const status = testData?.status || (isFailed ? "failed" : "passed");
2856
+ return /* @__PURE__ */ u3("tr", { children: [
2857
+ /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2858
+ /* @__PURE__ */ u3("td", { style: "max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap", children: /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, title: testData?.test_name || p3.path, children: testData?.test_name || p3.path }) }),
2859
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: testData?.test_file || "-" }),
2860
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2861
+ p3.duration.toFixed(2),
2862
+ " ms"
2863
+ ] }) }),
2864
+ /* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
2865
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: status === "failed" ? "badge-error" : status === "pending" ? "badge-warning" : "badge-success", children: status === "failed" ? "\u2717 Failed" : status === "pending" ? "\u23F8 Pending" : "\u2713 Passed" }) }),
2866
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
2867
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteTest(p3.token), title: "Delete", children: "\xD7" }) })
2868
+ ] }, p3.token);
2869
+ }) })
2870
+ ] }),
2871
+ testHasMore && !testFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreTests, disabled: testLoadingMore, children: testLoadingMore ? "Loading\u2026" : "Load more" }) })
2872
+ ] })),
2873
+ section === "runner" && /* @__PURE__ */ u3(TestRunnerContent, {}),
2163
2874
  section === "outbound" && (loadingOutbound ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "xs", "flex", "sm", "xs"], rows: 4 }) : outboundError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: outboundError }) }) : outboundRequests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2164
2875
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No outbound HTTP requests found" }),
2165
2876
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Make requests to external services to see outbound HTTP data" })
@@ -2212,7 +2923,7 @@
2212
2923
  /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:2px", children: [
2213
2924
  formatTime(req.profile_started_at),
2214
2925
  " \xB7 Profile: ",
2215
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${req.profile_token}`, children: [
2926
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${req.profile_token}`, children: [
2216
2927
  req.profile_token.substring(0, 8),
2217
2928
  "\u2026"
2218
2929
  ] })
@@ -4228,8 +4939,8 @@
4228
4939
  const verbs = ["ALL", ...Array.from(new Set(routes.map((r3) => r3.verb))).sort()];
4229
4940
  const filtered = routes.filter((route) => {
4230
4941
  const matchesVerb = verbFilter === "ALL" || route.verb === verbFilter;
4231
- const q2 = filter.toLowerCase();
4232
- const matchesText = !q2 || (route.pattern ?? "").toLowerCase().includes(q2) || (route.name ?? "").toLowerCase().includes(q2) || (route.controller_action ?? "").toLowerCase().includes(q2);
4942
+ const q3 = filter.toLowerCase();
4943
+ const matchesText = !q3 || (route.pattern ?? "").toLowerCase().includes(q3) || (route.name ?? "").toLowerCase().includes(q3) || (route.controller_action ?? "").toLowerCase().includes(q3);
4233
4944
  return matchesVerb && matchesText;
4234
4945
  });
4235
4946
  return /* @__PURE__ */ u3(k, { children: [
@@ -4928,6 +5639,255 @@
4928
5639
  ] });
4929
5640
  }
4930
5641
 
5642
+ // app/assets/typescript/profiler/components/dashboard/tabs/ConsoleTab.tsx
5643
+ function ConsoleTab({ data }) {
5644
+ return /* @__PURE__ */ u3("div", { class: "profiler-p-4", children: [
5645
+ /* @__PURE__ */ u3("div", { class: "profiler-mb-6", children: [
5646
+ /* @__PURE__ */ u3("h3", { class: "profiler-section-title profiler-mb-2", children: "Expression" }),
5647
+ /* @__PURE__ */ u3("pre", { class: "profiler-code-block profiler-code-block--full", children: data.expression })
5648
+ ] }),
5649
+ data.return_value !== void 0 && /* @__PURE__ */ u3("div", { children: [
5650
+ /* @__PURE__ */ u3("h3", { class: "profiler-section-title profiler-mb-2", children: "Return value" }),
5651
+ /* @__PURE__ */ u3("pre", { class: "profiler-code-block profiler-code-block--full", children: data.return_value })
5652
+ ] })
5653
+ ] });
5654
+ }
5655
+
5656
+ // app/assets/typescript/profiler/components/dashboard/ConsoleProfileDashboard.tsx
5657
+ function ConsoleProfileDashboard({ profile, initialTab, embedded }) {
5658
+ const cd = profile.collectors_data || {};
5659
+ const hasHttp = cd["http"]?.total_requests > 0;
5660
+ const hasDumps = (cd["dump"]?.count ?? 0) > 0;
5661
+ const hasLogs = (cd["logs"]?.total ?? 0) > 0;
5662
+ const hasException = !!cd["exception"]?.exception_class;
5663
+ const consoleData = cd["console"];
5664
+ const validTabs = ["console", "database", "cache", "http", "dump", "logs", "exception", "env", "timeline"];
5665
+ const defaultTab = validTabs.includes(initialTab) ? initialTab : "console";
5666
+ const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
5667
+ const isFailed = profile.status === 500;
5668
+ const handleTabClick = (tab) => (e3) => {
5669
+ e3.preventDefault();
5670
+ setActiveTab(tab);
5671
+ const url = new URL(window.location.href);
5672
+ url.searchParams.set("tab", tab);
5673
+ history.pushState(null, "", url.toString());
5674
+ };
5675
+ const tabClass = (key) => `tab${activeTab === key ? " active" : ""}`;
5676
+ return /* @__PURE__ */ u3("div", { class: "container", children: [
5677
+ /* @__PURE__ */ u3("div", { class: "header", children: [
5678
+ /* @__PURE__ */ u3("h1", { children: /* @__PURE__ */ u3("a", { href: "/_profiler?section=console", children: [
5679
+ /* @__PURE__ */ u3("span", { class: "h1-emoji", children: ">_" }),
5680
+ " Console Profile"
5681
+ ] }) }),
5682
+ /* @__PURE__ */ u3("p", { style: "font-family: monospace; word-break: break-all", children: profile.path }),
5683
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
5684
+ /* @__PURE__ */ u3("span", { children: [
5685
+ "Duration: ",
5686
+ /* @__PURE__ */ u3("strong", { children: [
5687
+ profile.duration.toFixed(2),
5688
+ " ms"
5689
+ ] })
5690
+ ] }),
5691
+ /* @__PURE__ */ u3("span", { children: [
5692
+ "Status: ",
5693
+ /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Error" : "OK" }) })
5694
+ ] }),
5695
+ profile.memory != null && /* @__PURE__ */ u3("span", { children: [
5696
+ "Memory: ",
5697
+ /* @__PURE__ */ u3("strong", { children: [
5698
+ (profile.memory / 1024 / 1024).toFixed(2),
5699
+ " MB"
5700
+ ] })
5701
+ ] }),
5702
+ profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
5703
+ "v",
5704
+ profile.gem_version
5705
+ ] })
5706
+ ] }),
5707
+ profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
5708
+ "\u26A0\uFE0F Profil captur\xE9 avec la version ",
5709
+ /* @__PURE__ */ u3("strong", { children: profile.gem_version }),
5710
+ " \u2014 version actuelle : ",
5711
+ /* @__PURE__ */ u3("strong", { children: getGemVersion() })
5712
+ ] })
5713
+ ] }),
5714
+ /* @__PURE__ */ u3("div", { class: "profiler-panel", children: [
5715
+ /* @__PURE__ */ u3("div", { class: "tabs", children: [
5716
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("console"), onClick: handleTabClick("console"), children: "Console" }),
5717
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
5718
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
5719
+ hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "HTTP" }),
5720
+ hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: "Dumps" }),
5721
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
5722
+ hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), children: "Exception" }),
5723
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" }),
5724
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" })
5725
+ ] }),
5726
+ /* @__PURE__ */ u3("div", { class: "profiler-p-0 tab-content active", children: [
5727
+ activeTab === "console" && consoleData && /* @__PURE__ */ u3(ConsoleTab, { data: consoleData }),
5728
+ activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
5729
+ activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
5730
+ activeTab === "http" && hasHttp && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
5731
+ activeTab === "dump" && hasDumps && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
5732
+ activeTab === "logs" && hasLogs && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
5733
+ activeTab === "exception" && hasException && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
5734
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true }),
5735
+ activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"], functionProfileData: cd["function_profile"] })
5736
+ ] })
5737
+ ] }),
5738
+ !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
5739
+ ] });
5740
+ }
5741
+
5742
+ // app/assets/typescript/profiler/components/dashboard/tabs/TestTab.tsx
5743
+ function statusBadge2(status) {
5744
+ if (status === "passed") return /* @__PURE__ */ u3("span", { class: "badge-success", children: "\u2713 Passed" });
5745
+ if (status === "failed") return /* @__PURE__ */ u3("span", { class: "badge-error", children: "\u2717 Failed" });
5746
+ if (status === "pending") return /* @__PURE__ */ u3("span", { class: "badge-warning", children: "\u23F8 Pending" });
5747
+ return /* @__PURE__ */ u3("span", { class: "badge-default", children: status });
5748
+ }
5749
+ function TestTab({ testData }) {
5750
+ return /* @__PURE__ */ u3("div", { children: /* @__PURE__ */ u3("table", { class: "profiler-detail-table", children: /* @__PURE__ */ u3("tbody", { children: [
5751
+ /* @__PURE__ */ u3("tr", { children: [
5752
+ /* @__PURE__ */ u3("th", { children: "Test Name" }),
5753
+ /* @__PURE__ */ u3("td", { style: "word-break: break-word", children: testData.test_name })
5754
+ ] }),
5755
+ /* @__PURE__ */ u3("tr", { children: [
5756
+ /* @__PURE__ */ u3("th", { children: "Status" }),
5757
+ /* @__PURE__ */ u3("td", { children: statusBadge2(testData.status) })
5758
+ ] }),
5759
+ /* @__PURE__ */ u3("tr", { children: [
5760
+ /* @__PURE__ */ u3("th", { children: "File" }),
5761
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: [
5762
+ testData.test_file,
5763
+ ":",
5764
+ testData.test_line
5765
+ ] })
5766
+ ] }),
5767
+ /* @__PURE__ */ u3("tr", { children: [
5768
+ /* @__PURE__ */ u3("th", { children: "Framework" }),
5769
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: testData.framework })
5770
+ ] }),
5771
+ testData.assertions != null && /* @__PURE__ */ u3("tr", { children: [
5772
+ /* @__PURE__ */ u3("th", { children: "Assertions" }),
5773
+ /* @__PURE__ */ u3("td", { children: testData.assertions })
5774
+ ] }),
5775
+ testData.skip_reason && /* @__PURE__ */ u3("tr", { children: [
5776
+ /* @__PURE__ */ u3("th", { children: "Skip reason" }),
5777
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--muted", children: testData.skip_reason })
5778
+ ] }),
5779
+ testData.exception_message && /* @__PURE__ */ u3("tr", { children: [
5780
+ /* @__PURE__ */ u3("th", { children: "Exception" }),
5781
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("pre", { class: "profiler-text--xs", style: "white-space: pre-wrap; color: var(--profiler-error, #ef4444)", children: testData.exception_message }) })
5782
+ ] })
5783
+ ] }) }) });
5784
+ }
5785
+
5786
+ // app/assets/typescript/profiler/components/dashboard/TestProfileDashboard.tsx
5787
+ function TestProfileDashboard({ profile, initialTab, embedded }) {
5788
+ const cd = profile.collectors_data || {};
5789
+ const hasDumps = (cd["dump"]?.count ?? 0) > 0;
5790
+ const hasLogs = (cd["logs"]?.total ?? 0) > 0;
5791
+ const hasException = !!cd["exception"]?.exception_class;
5792
+ const testData = cd["test"];
5793
+ const validTabs = ["test", "database", "cache", "dump", "logs", "exception", "env", "timeline"];
5794
+ const defaultTab = validTabs.includes(initialTab) ? initialTab : "test";
5795
+ const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
5796
+ const isFailed = profile.status === 500;
5797
+ const testStatus = testData?.status || (isFailed ? "failed" : "passed");
5798
+ const handleTabClick = (tab) => (e3) => {
5799
+ e3.preventDefault();
5800
+ setActiveTab(tab);
5801
+ const url = new URL(window.location.href);
5802
+ url.searchParams.set("tab", tab);
5803
+ history.pushState(null, "", url.toString());
5804
+ };
5805
+ const tabClass = (key) => `tab${activeTab === key ? " active" : ""}`;
5806
+ return /* @__PURE__ */ u3("div", { class: "container", children: [
5807
+ /* @__PURE__ */ u3("div", { class: "header", children: [
5808
+ /* @__PURE__ */ u3("h1", { children: /* @__PURE__ */ u3("a", { href: "/_profiler?section=tests", children: [
5809
+ /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F9EA}" }),
5810
+ " Test Profile"
5811
+ ] }) }),
5812
+ /* @__PURE__ */ u3("p", { style: "word-break: break-word", children: testData?.test_name || profile.path }),
5813
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
5814
+ /* @__PURE__ */ u3("span", { children: [
5815
+ "Duration: ",
5816
+ /* @__PURE__ */ u3("strong", { children: [
5817
+ profile.duration.toFixed(2),
5818
+ " ms"
5819
+ ] })
5820
+ ] }),
5821
+ /* @__PURE__ */ u3("span", { children: [
5822
+ "Status: ",
5823
+ /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : testStatus === "pending" ? "warning" : "success"}`, children: testStatus === "failed" ? "\u2717 Failed" : testStatus === "pending" ? "\u23F8 Pending" : "\u2713 Passed" }) })
5824
+ ] }),
5825
+ profile.memory != null && /* @__PURE__ */ u3("span", { children: [
5826
+ "Memory: ",
5827
+ /* @__PURE__ */ u3("strong", { children: [
5828
+ (profile.memory / 1024 / 1024).toFixed(2),
5829
+ " MB"
5830
+ ] })
5831
+ ] })
5832
+ ] }),
5833
+ testData?.test_file && /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted profiler-mt-2", children: [
5834
+ /* @__PURE__ */ u3("span", { class: "profiler-text--mono", children: [
5835
+ testData.test_file,
5836
+ ":",
5837
+ testData.test_line
5838
+ ] }),
5839
+ /* @__PURE__ */ u3("span", { style: "margin-left: 8px", children: [
5840
+ "\xB7 ",
5841
+ testData.framework
5842
+ ] })
5843
+ ] })
5844
+ ] }),
5845
+ /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
5846
+ /* @__PURE__ */ u3("div", { class: "tabs", children: [
5847
+ hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), style: "color:var(--profiler-error,#ef4444);", children: "\u{1F4A5} Exception" }),
5848
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("test"), onClick: handleTabClick("test"), children: "Test" }),
5849
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
5850
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
5851
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
5852
+ hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: [
5853
+ "Dumps (",
5854
+ cd["dump"].count,
5855
+ ")"
5856
+ ] }),
5857
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
5858
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
5859
+ ] }),
5860
+ /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
5861
+ activeTab === "test" && testData && /* @__PURE__ */ u3(TestTab, { testData }),
5862
+ activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
5863
+ activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { data: cd["cache"] }),
5864
+ activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { data: cd["flamegraph"] }),
5865
+ activeTab === "dump" && hasDumps && /* @__PURE__ */ u3(DumpsTab, { data: cd["dump"] }),
5866
+ activeTab === "logs" && hasLogs && /* @__PURE__ */ u3(LogsTab, { data: cd["logs"] }),
5867
+ activeTab === "exception" && hasException && /* @__PURE__ */ u3(ExceptionTab, { data: cd["exception"] }),
5868
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
5869
+ ] })
5870
+ ] })
5871
+ ] });
5872
+ }
5873
+
5874
+ // app/assets/typescript/profiler/components/test-runner/TestRunnerPage.tsx
5875
+ var BASE3 = "/_profiler";
5876
+ function TestRunnerPage() {
5877
+ return /* @__PURE__ */ u3("div", { class: "container", children: [
5878
+ /* @__PURE__ */ u3("div", { class: "header", children: [
5879
+ /* @__PURE__ */ u3("h1", { children: [
5880
+ /* @__PURE__ */ u3("a", { href: BASE3, children: /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F50D}" }) }),
5881
+ " ",
5882
+ /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F9EA}" }),
5883
+ " Test Runner"
5884
+ ] }),
5885
+ /* @__PURE__ */ u3("p", { children: "Run tests from the profiler and capture a performance profile for each test." })
5886
+ ] }),
5887
+ /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: /* @__PURE__ */ u3("div", { class: "profiler-p-4", children: /* @__PURE__ */ u3(TestRunnerContent, {}) }) })
5888
+ ] });
5889
+ }
5890
+
4931
5891
  // app/assets/typescript/profiler/timeline.ts
4932
5892
  function initTimeline(container) {
4933
5893
  console.log("Initializing profiler timeline");
@@ -5189,12 +6149,20 @@ Duration: ${event.duration.toFixed(2)}ms`;
5189
6149
  const embedded = showEl.dataset.embedded === "true";
5190
6150
  if (profile.profile_type === "job") {
5191
6151
  J(/* @__PURE__ */ u3(JobProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
6152
+ } else if (profile.profile_type === "console") {
6153
+ J(/* @__PURE__ */ u3(ConsoleProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
6154
+ } else if (profile.profile_type === "test") {
6155
+ J(/* @__PURE__ */ u3(TestProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5192
6156
  } else {
5193
6157
  J(/* @__PURE__ */ u3(ProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5194
6158
  }
5195
6159
  }
5196
6160
  }
5197
- if (!indexEl && !showEl) {
6161
+ const testRunnerEl = document.getElementById("profiler-test-runner");
6162
+ if (testRunnerEl) {
6163
+ J(/* @__PURE__ */ u3(TestRunnerPage, {}), testRunnerEl);
6164
+ }
6165
+ if (!indexEl && !showEl && !testRunnerEl) {
5198
6166
  const header = document.querySelector(".header, .profiler-detail");
5199
6167
  if (header) {
5200
6168
  const toggle = createThemeToggle();