rails-profiler 0.19.2 → 0.21.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c4382b8c5ad1cd162614ae7eaf1ca940f979c86c8daf511a72a697d9336817e
4
- data.tar.gz: 88e34726b5a3944a12656aab0259f71534b5b085bb6afd7e1dd06857bd8b081b
3
+ metadata.gz: 80a34504e3cb67a077d5f46265cbc265cbf7704f89ad06f90e5ab5df22651a70
4
+ data.tar.gz: d23a6d0d9f107611d66dfe34987447efde5f74f2562efd146d43d910fe431c6a
5
5
  SHA512:
6
- metadata.gz: '0378c6853fd21cbf6ca6c7f999c6cff53b2896e03422f326350fac13b8c83f66e4e72bdbf9602b3c84b50ec93f8cec5b2bcbbe9491257c1d4619095a98a17dbd'
7
- data.tar.gz: a3f70d63f227306d3f01138bbbd3131fc2862f9545b73221f366d580e6c0bd0c809a0258372474fc92dd1b0e2e5e880b1ecd9e765332cff89f09a5aa60a118e8
6
+ metadata.gz: 668d228ede43471fc71a5d65d6498bfd00606865f7cad69dd23e03f79a4e84a2ba5f34dbc2d9e07809df4319946055464b7b8e615ee29e14c8cfafdef10ffc4a
7
+ data.tar.gz: 16926d1a141c64cc72110698cd21e4d5c26d6e9022cd17ad6ba5ba3112aa98ce1917db9d005c112fac62190790433c06b24f1d9a85648edabf679664eb89b30d
@@ -68,8 +68,8 @@
68
68
  }
69
69
  }
70
70
  function P(n2, l3, u4, t3, i3, r3, o3, e3, f4, c3, s3) {
71
- var a3, h3, y3, d3, w3, g2, _2, m3 = t3 && t3.__k || v, b = l3.length;
72
- for (f4 = A(u4, l3, m3, f4, b), a3 = 0; a3 < b; a3++) null != (y3 = u4.__k[a3]) && (h3 = -1 != y3.__i && m3[y3.__i] || p, y3.__i = a3, g2 = z(n2, y3, h3, i3, r3, o3, e3, f4, c3, s3), d3 = y3.__e, y3.ref && h3.ref != y3.ref && (h3.ref && D(h3.ref, null, y3), s3.push(y3.ref, y3.__c || d3, y3)), null == w3 && null != d3 && (w3 = d3), (_2 = !!(4 & y3.__u)) || h3.__k === y3.__k ? f4 = H(y3, f4, n2, _2) : "function" == typeof y3.type && void 0 !== g2 ? f4 = g2 : d3 && (f4 = d3.nextSibling), y3.__u &= -7);
71
+ var a3, h3, y3, d3, w3, g2, _3, m3 = t3 && t3.__k || v, b = l3.length;
72
+ for (f4 = A(u4, l3, m3, f4, b), a3 = 0; a3 < b; a3++) null != (y3 = u4.__k[a3]) && (h3 = -1 != y3.__i && m3[y3.__i] || p, y3.__i = a3, g2 = z(n2, y3, h3, i3, r3, o3, e3, f4, c3, s3), d3 = y3.__e, y3.ref && h3.ref != y3.ref && (h3.ref && D(h3.ref, null, y3), s3.push(y3.ref, y3.__c || d3, y3)), null == w3 && null != d3 && (w3 = d3), (_3 = !!(4 & y3.__u)) || h3.__k === y3.__k ? f4 = H(y3, f4, n2, _3) : "function" == typeof y3.type && void 0 !== g2 ? f4 = g2 : d3 && (f4 = d3.nextSibling), y3.__u &= -7);
73
73
  return u4.__e = w3, f4;
74
74
  }
75
75
  function A(n2, l3, u4, t3, i3) {
@@ -130,11 +130,11 @@
130
130
  };
131
131
  }
132
132
  function z(n2, u4, t3, i3, r3, o3, e3, f4, c3, s3) {
133
- var a3, h3, p3, y3, _2, m3, b, S2, C3, M2, $2, I2, A3, H2, L, T3 = u4.type;
133
+ var a3, h3, p3, y3, _3, m3, b, S2, C3, M2, $2, I2, A3, H2, L, T3 = u4.type;
134
134
  if (void 0 !== u4.constructor) return null;
135
135
  128 & t3.__u && (c3 = !!(32 & t3.__u), o3 = [f4 = u4.__e = t3.__e]), (a3 = l.__b) && a3(u4);
136
136
  n: if ("function" == typeof T3) try {
137
- if (S2 = u4.props, C3 = T3.prototype && T3.prototype.render, M2 = (a3 = T3.contextType) && i3[a3.__c], $2 = a3 ? M2 ? M2.props.value : a3.__ : i3, t3.__c ? b = (h3 = u4.__c = t3.__c).__ = h3.__E : (C3 ? u4.__c = h3 = new T3(S2, $2) : (u4.__c = h3 = new x(S2, $2), h3.constructor = T3, h3.render = G), M2 && M2.sub(h3), h3.state || (h3.state = {}), h3.__n = i3, p3 = h3.__d = true, h3.__h = [], h3._sb = []), C3 && null == h3.__s && (h3.__s = h3.state), C3 && null != T3.getDerivedStateFromProps && (h3.__s == h3.state && (h3.__s = w({}, h3.__s)), w(h3.__s, T3.getDerivedStateFromProps(S2, h3.__s))), y3 = h3.props, _2 = h3.state, h3.__v = u4, p3) C3 && null == T3.getDerivedStateFromProps && null != h3.componentWillMount && h3.componentWillMount(), C3 && null != h3.componentDidMount && h3.__h.push(h3.componentDidMount);
137
+ if (S2 = u4.props, C3 = T3.prototype && T3.prototype.render, M2 = (a3 = T3.contextType) && i3[a3.__c], $2 = a3 ? M2 ? M2.props.value : a3.__ : i3, t3.__c ? b = (h3 = u4.__c = t3.__c).__ = h3.__E : (C3 ? u4.__c = h3 = new T3(S2, $2) : (u4.__c = h3 = new x(S2, $2), h3.constructor = T3, h3.render = G), M2 && M2.sub(h3), h3.state || (h3.state = {}), h3.__n = i3, p3 = h3.__d = true, h3.__h = [], h3._sb = []), C3 && null == h3.__s && (h3.__s = h3.state), C3 && null != T3.getDerivedStateFromProps && (h3.__s == h3.state && (h3.__s = w({}, h3.__s)), w(h3.__s, T3.getDerivedStateFromProps(S2, h3.__s))), y3 = h3.props, _3 = h3.state, h3.__v = u4, p3) C3 && null == T3.getDerivedStateFromProps && null != h3.componentWillMount && h3.componentWillMount(), C3 && null != h3.componentDidMount && h3.__h.push(h3.componentDidMount);
138
138
  else {
139
139
  if (C3 && null == T3.getDerivedStateFromProps && S2 !== y3 && null != h3.componentWillReceiveProps && h3.componentWillReceiveProps(S2, $2), u4.__v == t3.__v || !h3.__e && null != h3.shouldComponentUpdate && false === h3.shouldComponentUpdate(S2, h3.__s, $2)) {
140
140
  u4.__v != t3.__v && (h3.props = S2, h3.state = h3.__s, h3.__d = false), u4.__e = t3.__e, u4.__k = t3.__k, u4.__k.some(function(n3) {
@@ -143,14 +143,14 @@
143
143
  break n;
144
144
  }
145
145
  null != h3.componentWillUpdate && h3.componentWillUpdate(S2, h3.__s, $2), C3 && null != h3.componentDidUpdate && h3.__h.push(function() {
146
- h3.componentDidUpdate(y3, _2, m3);
146
+ h3.componentDidUpdate(y3, _3, m3);
147
147
  });
148
148
  }
149
149
  if (h3.context = $2, h3.props = S2, h3.__P = n2, h3.__e = false, I2 = l.__r, A3 = 0, C3) h3.state = h3.__s, h3.__d = false, I2 && I2(u4), a3 = h3.render(h3.props, h3.state, h3.context), v.push.apply(h3.__h, h3._sb), h3._sb = [];
150
150
  else do {
151
151
  h3.__d = false, I2 && I2(u4), a3 = h3.render(h3.props, h3.state, h3.context), h3.state = h3.__s;
152
152
  } while (h3.__d && ++A3 < 25);
153
- h3.state = h3.__s, null != h3.getChildContext && (i3 = w(w({}, i3), h3.getChildContext())), C3 && !p3 && null != h3.getSnapshotBeforeUpdate && (m3 = h3.getSnapshotBeforeUpdate(y3, _2)), H2 = null != a3 && a3.type === k && null == a3.key ? q(a3.props.children) : a3, f4 = P(n2, d(H2) ? H2 : [H2], u4, t3, i3, r3, o3, e3, f4, c3, s3), h3.base = u4.__e, u4.__u &= -161, h3.__h.length && e3.push(h3), b && (h3.__E = h3.__ = null);
153
+ h3.state = h3.__s, null != h3.getChildContext && (i3 = w(w({}, i3), h3.getChildContext())), C3 && !p3 && null != h3.getSnapshotBeforeUpdate && (m3 = h3.getSnapshotBeforeUpdate(y3, _3)), H2 = null != a3 && a3.type === k && null == a3.key ? q(a3.props.children) : a3, f4 = P(n2, d(H2) ? H2 : [H2], u4, t3, i3, r3, o3, e3, f4, c3, s3), h3.base = u4.__e, u4.__u &= -161, h3.__h.length && e3.push(h3), b && (h3.__E = h3.__ = null);
154
154
  } catch (n3) {
155
155
  if (u4.__v = null, c3 || null != o3) if (n3.then) {
156
156
  for (u4.__u |= c3 ? 160 : 128; f4 && 8 == f4.nodeType && f4.nextSibling; ) f4 = f4.nextSibling;
@@ -184,7 +184,7 @@
184
184
  return "object" != typeof n2 || null == n2 || n2.__b > 0 ? n2 : d(n2) ? n2.map(q) : w({}, n2);
185
185
  }
186
186
  function B(u4, t3, i3, r3, o3, e3, f4, c3, s3) {
187
- var a3, h3, v3, y3, w3, _2, m3, b = i3.props || p, k3 = t3.props, x2 = t3.type;
187
+ var a3, h3, v3, y3, w3, _3, m3, b = i3.props || p, k3 = t3.props, x2 = t3.type;
188
188
  if ("svg" == x2 ? o3 = "http://www.w3.org/2000/svg" : "math" == x2 ? o3 = "http://www.w3.org/1998/Math/MathML" : o3 || (o3 = "http://www.w3.org/1999/xhtml"), null != e3) {
189
189
  for (a3 = 0; a3 < e3.length; a3++) if ((w3 = e3[a3]) && "setAttribute" in w3 == !!x2 && (x2 ? w3.localName == x2 : 3 == w3.nodeType)) {
190
190
  u4 = w3, e3[a3] = null;
@@ -199,10 +199,10 @@
199
199
  else {
200
200
  if (e3 = e3 && n.call(u4.childNodes), !c3 && null != e3) for (b = {}, a3 = 0; a3 < u4.attributes.length; a3++) b[(w3 = u4.attributes[a3]).name] = w3.value;
201
201
  for (a3 in b) w3 = b[a3], "dangerouslySetInnerHTML" == a3 ? v3 = w3 : "children" == a3 || a3 in k3 || "value" == a3 && "defaultValue" in k3 || "checked" == a3 && "defaultChecked" in k3 || F(u4, a3, null, w3, o3);
202
- for (a3 in k3) w3 = k3[a3], "children" == a3 ? y3 = w3 : "dangerouslySetInnerHTML" == a3 ? h3 = w3 : "value" == a3 ? _2 = w3 : "checked" == a3 ? m3 = w3 : c3 && "function" != typeof w3 || b[a3] === w3 || F(u4, a3, w3, b[a3], o3);
202
+ for (a3 in k3) w3 = k3[a3], "children" == a3 ? y3 = w3 : "dangerouslySetInnerHTML" == a3 ? h3 = w3 : "value" == a3 ? _3 = w3 : "checked" == a3 ? m3 = w3 : c3 && "function" != typeof w3 || b[a3] === w3 || F(u4, a3, w3, b[a3], o3);
203
203
  if (h3) c3 || v3 && (h3.__html == v3.__html || h3.__html == u4.innerHTML) || (u4.innerHTML = h3.__html), t3.__k = [];
204
204
  else if (v3 && (u4.innerHTML = ""), P("template" == t3.type ? u4.content : u4, d(y3) ? y3 : [y3], t3, i3, r3, "foreignObject" == x2 ? "http://www.w3.org/1999/xhtml" : o3, e3, f4, e3 ? e3[0] : i3.__k && S(i3, 0), c3, s3), null != e3) for (a3 = e3.length; a3--; ) g(e3[a3]);
205
- c3 || (a3 = "value", "progress" == x2 && null == _2 ? u4.removeAttribute("value") : null != _2 && (_2 !== u4[a3] || "progress" == x2 && !_2 || "option" == x2 && _2 != b[a3]) && F(u4, a3, _2, b[a3], o3), a3 = "checked", null != m3 && m3 != u4[a3] && F(u4, a3, m3, b[a3], o3));
205
+ c3 || (a3 = "value", "progress" == x2 && null == _3 ? u4.removeAttribute("value") : null != _3 && (_3 !== u4[a3] || "progress" == x2 && !_3 || "option" == x2 && _3 != b[a3]) && F(u4, a3, _3, b[a3], o3), a3 = "checked", null != m3 && m3 != u4[a3] && F(u4, a3, m3, b[a3], o3));
206
206
  }
207
207
  return u4;
208
208
  }
@@ -314,6 +314,10 @@
314
314
  var i3 = p2(t2++, 3);
315
315
  !c2.__s && C2(i3.__H, u4) && (i3.__ = n2, i3.u = u4, r2.__H.__h.push(i3));
316
316
  }
317
+ function _2(n2, u4) {
318
+ var i3 = p2(t2++, 4);
319
+ !c2.__s && C2(i3.__H, u4) && (i3.__ = n2, i3.u = u4, r2.__h.push(i3));
320
+ }
317
321
  function A2(n2) {
318
322
  return o2 = 5, T2(function() {
319
323
  return { current: n2 };
@@ -427,7 +431,7 @@
427
431
  }
428
432
  hideTimer.current = setTimeout(() => setVisible(false), 150);
429
433
  };
430
- y2(() => {
434
+ _2(() => {
431
435
  if (visible && panelRef.current) {
432
436
  const el = panelRef.current;
433
437
  el.style.left = "50%";
@@ -453,8 +457,7 @@
453
457
  "div",
454
458
  {
455
459
  ref: panelRef,
456
- class: `profiler-toolbar-panel${panelLarge ? " profiler-toolbar-panel-large" : ""}`,
457
- style: { display: visible ? "block" : "none" },
460
+ class: `profiler-toolbar-panel${panelLarge ? " profiler-toolbar-panel-large" : ""}${visible ? " is-visible" : ""}`,
458
461
  onMouseEnter: show,
459
462
  onMouseLeave: hide,
460
463
  children: panel
@@ -1048,7 +1051,7 @@
1048
1051
  const dbClass = (dbData?.slow_queries ?? 0) > 0 ? "profiler-text--error" : "profiler-text--success";
1049
1052
  const ajaxClass = (ajaxData?.total_requests ?? 0) > 20 ? "profiler-text--error" : "profiler-text--success";
1050
1053
  const cacheClass = (cacheData?.hit_rate ?? 0) > 80 ? "profiler-text--success" : "profiler-text--warning";
1051
- return /* @__PURE__ */ u3("div", { class: "profiler-toolbar-container", children: [
1054
+ return /* @__PURE__ */ u3(k, { children: /* @__PURE__ */ u3("div", { class: "profiler-toolbar-container", children: [
1052
1055
  requestData && /* @__PURE__ */ u3(k, { children: [
1053
1056
  /* @__PURE__ */ u3(
1054
1057
  ToolbarItem,
@@ -1291,67 +1294,84 @@
1291
1294
  ]
1292
1295
  }
1293
1296
  ),
1294
- /* @__PURE__ */ u3("a", { href: "/_profiler", class: "profiler-toolbar-item", children: "\u2B21 Profiler" })
1295
- ] });
1297
+ /* @__PURE__ */ u3("a", { href: "/_profiler", class: "profiler-toolbar-item profiler-toolbar-logo", children: "\u2B21 Profiler" })
1298
+ ] }) });
1296
1299
  }
1297
1300
 
1298
1301
  // app/assets/typescript/profiler/toolbar-bundle.tsx
1299
- function applyTheme(el) {
1302
+ var ANIM_MS = 280;
1303
+ function applyTheme(elements) {
1300
1304
  const stored = localStorage.getItem("profiler-theme");
1301
1305
  const theme = stored === "light" ? "light" : stored === "dark" ? "dark" : window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
1302
- el.setAttribute("data-theme", theme);
1306
+ elements.forEach((el) => el.setAttribute("data-theme", theme));
1307
+ }
1308
+ function ToolbarMount({ token }) {
1309
+ const [profile, setProfile] = d2(null);
1310
+ y2(() => {
1311
+ const load = () => {
1312
+ fetch(`/_profiler/api/toolbar/${token}`).then((res) => res.json()).then((data) => {
1313
+ if (data.profile) setProfile(data.profile);
1314
+ }).catch((err) => console.debug("Profiler toolbar load failed:", err));
1315
+ };
1316
+ window.__PROFILER_REFRESH_TOOLBAR__ = load;
1317
+ load();
1318
+ }, [token]);
1319
+ if (!profile) return null;
1320
+ return /* @__PURE__ */ u3(ToolbarApp, { profile, token });
1303
1321
  }
1304
1322
  function mountToolbar() {
1305
1323
  const el = document.getElementById("profiler-toolbar");
1306
- if (!el) return;
1324
+ const toggleEl = document.getElementById("profiler-toolbar-toggle");
1325
+ if (!el || !toggleEl) return;
1307
1326
  const token = el.dataset.token;
1308
1327
  if (!token) return;
1309
- applyTheme(el);
1328
+ const themeEls = [el, toggleEl];
1329
+ applyTheme(themeEls);
1310
1330
  window.addEventListener("storage", (e3) => {
1311
- if (e3.key === "profiler-theme") applyTheme(el);
1331
+ if (e3.key === "profiler-theme") applyTheme(themeEls);
1312
1332
  });
1313
1333
  window.addEventListener("profiler:theme-change", ((e3) => {
1314
- if (e3.detail?.theme) el.setAttribute("data-theme", e3.detail.theme);
1334
+ if (e3.detail?.theme) themeEls.forEach((elem) => elem.setAttribute("data-theme", e3.detail.theme));
1315
1335
  }));
1316
- const renderToolbar = (profile) => {
1317
- J(/* @__PURE__ */ u3(ToolbarApp, { profile, token }), el);
1318
- applyTheme(el);
1319
- };
1320
- const loadAndRender = () => {
1321
- fetch(`/_profiler/api/toolbar/${token}`).then((res) => res.json()).then((data) => {
1322
- if (data.profile) renderToolbar(data.profile);
1323
- }).catch((err) => console.debug("Profiler toolbar load failed:", err));
1324
- };
1325
- window.__PROFILER_REFRESH_TOOLBAR__ = loadAndRender;
1326
- loadAndRender();
1336
+ let isCollapsed = localStorage.getItem("profiler-toolbar-collapsed") === "true";
1337
+ toggleEl.dataset.collapsed = String(isCollapsed);
1338
+ if (isCollapsed && !el.style.transform) {
1339
+ el.style.animation = "none";
1340
+ el.style.transform = "translateX(calc(100% + 44px))";
1341
+ }
1342
+ if (!isCollapsed) {
1343
+ setTimeout(() => {
1344
+ el.style.animation = "none";
1345
+ }, 400);
1346
+ }
1347
+ function toggleCollapse() {
1348
+ const next = !isCollapsed;
1349
+ isCollapsed = next;
1350
+ localStorage.setItem("profiler-toolbar-collapsed", String(next));
1351
+ toggleEl.dataset.collapsed = String(next);
1352
+ if (next) {
1353
+ el.style.transition = `transform ${ANIM_MS}ms cubic-bezier(0.4,0,0.2,1)`;
1354
+ el.style.transform = "translateX(calc(100% + 44px))";
1355
+ } else {
1356
+ el.style.transition = "none";
1357
+ el.style.transform = "translateX(calc(100% + 44px))";
1358
+ void el.offsetWidth;
1359
+ el.style.transition = `transform ${ANIM_MS}ms cubic-bezier(0.4,0,0.2,1)`;
1360
+ el.style.transform = "translateX(0)";
1361
+ setTimeout(() => {
1362
+ el.style.transform = "";
1363
+ el.style.transition = "";
1364
+ }, ANIM_MS + 50);
1365
+ }
1366
+ }
1367
+ toggleEl.addEventListener("click", toggleCollapse);
1327
1368
  document.addEventListener("keydown", (e3) => {
1328
1369
  if (e3.altKey && e3.key === "p") {
1329
1370
  e3.preventDefault();
1330
- const hidden2 = el.dataset.hidden === "true";
1331
- if (hidden2) {
1332
- el.dataset.hidden = "false";
1333
- el.style.transform = "translateY(0)";
1334
- el.style.opacity = "1";
1335
- localStorage.setItem("profiler-toolbar-hidden", "false");
1336
- } else {
1337
- el.dataset.hidden = "true";
1338
- el.style.transform = "translateY(100%)";
1339
- el.style.opacity = "0";
1340
- localStorage.setItem("profiler-toolbar-hidden", "true");
1341
- }
1342
- }
1343
- if (e3.key === "Escape") {
1344
- el.dataset.hidden = "true";
1345
- el.style.transform = "translateY(100%)";
1346
- el.style.opacity = "0";
1371
+ toggleCollapse();
1347
1372
  }
1348
1373
  });
1349
- const hidden = localStorage.getItem("profiler-toolbar-hidden") === "true";
1350
- if (hidden) {
1351
- el.dataset.hidden = "true";
1352
- el.style.transform = "translateY(100%)";
1353
- el.style.opacity = "0";
1354
- }
1374
+ J(/* @__PURE__ */ u3(ToolbarMount, { token }), el);
1355
1375
  }
1356
1376
  if (document.readyState === "loading") {
1357
1377
  document.addEventListener("DOMContentLoaded", mountToolbar);
@@ -521,6 +521,34 @@ tr:hover .btn-row-delete {
521
521
  border: 1px solid var(--profiler-border);
522
522
  }
523
523
 
524
+ .profiler-version-badge {
525
+ display: inline-flex;
526
+ align-items: center;
527
+ padding: 1px 6px;
528
+ border-radius: var(--profiler-radius-sm);
529
+ font-family: var(--profiler-font-mono);
530
+ font-size: var(--profiler-text-xs);
531
+ color: var(--profiler-text-muted);
532
+ background: var(--profiler-bg-lighter);
533
+ border: 1px solid var(--profiler-border);
534
+ }
535
+
536
+ .profiler-version-warn {
537
+ margin-left: 6px;
538
+ cursor: help;
539
+ font-size: var(--profiler-text-xs);
540
+ }
541
+
542
+ .profiler-version-mismatch {
543
+ margin-top: var(--profiler-space-2);
544
+ padding: var(--profiler-space-2) var(--profiler-space-3);
545
+ border-radius: var(--profiler-radius-sm);
546
+ font-size: var(--profiler-text-sm);
547
+ background: var(--profiler-warning-bg);
548
+ color: var(--profiler-warning);
549
+ border: 1px solid rgba(251, 146, 60, 0.3);
550
+ }
551
+
524
552
  @keyframes fadeInDown {
525
553
  from {
526
554
  opacity: 0;
@@ -1367,6 +1395,26 @@ tr:hover .btn-row-delete {
1367
1395
  color: var(--profiler-text-muted);
1368
1396
  }
1369
1397
 
1398
+ @keyframes pfPop {
1399
+ 0% {
1400
+ transform: scaleX(1);
1401
+ }
1402
+ 15% {
1403
+ transform: scaleX(1.18);
1404
+ }
1405
+ 40% {
1406
+ transform: scaleX(0.87);
1407
+ }
1408
+ 65% {
1409
+ transform: scaleX(1.08);
1410
+ }
1411
+ 85% {
1412
+ transform: scaleX(0.97);
1413
+ }
1414
+ 100% {
1415
+ transform: scaleX(1);
1416
+ }
1417
+ }
1370
1418
  #profiler-toolbar {
1371
1419
  position: fixed;
1372
1420
  bottom: 0;
@@ -1381,6 +1429,7 @@ tr:hover .btn-row-delete {
1381
1429
  font-family: var(--profiler-font-mono);
1382
1430
  font-size: 11px;
1383
1431
  animation: toolbarIn 300ms cubic-bezier(0.4, 0, 0.2, 1) both;
1432
+ transition: left 280ms cubic-bezier(0.4, 0, 0.2, 1);
1384
1433
  }
1385
1434
  #profiler-toolbar::before {
1386
1435
  content: "";
@@ -1436,12 +1485,10 @@ tr:hover .btn-row-delete {
1436
1485
  background: var(--profiler-accent);
1437
1486
  transform: scaleX(0);
1438
1487
  transform-origin: left;
1439
- transition: transform var(--profiler-transition-base);
1488
+ transition: transform 280ms cubic-bezier(0.34, 1.4, 0.64, 1);
1440
1489
  }
1441
1490
  .profiler-toolbar-item:last-child {
1442
1491
  border-right: none;
1443
- margin-left: auto;
1444
- padding-left: 20px;
1445
1492
  }
1446
1493
  .profiler-toolbar-item:hover {
1447
1494
  color: var(--profiler-text);
@@ -1451,12 +1498,30 @@ tr:hover .btn-row-delete {
1451
1498
  transform: scaleX(1);
1452
1499
  }
1453
1500
 
1501
+ .profiler-toolbar-collapse-btn {
1502
+ margin-left: auto;
1503
+ padding: 0 14px;
1504
+ border-left: 1px solid var(--profiler-border);
1505
+ font-size: 10px;
1506
+ color: var(--profiler-text-muted);
1507
+ cursor: pointer;
1508
+ }
1509
+ .profiler-toolbar-collapse-btn:hover {
1510
+ color: var(--profiler-text);
1511
+ background: var(--profiler-accent-bg);
1512
+ animation: pfPop 380ms ease-out both;
1513
+ }
1514
+
1454
1515
  a.profiler-toolbar-item {
1455
1516
  cursor: pointer;
1456
1517
  font-family: var(--profiler-font-mono);
1457
1518
  font-size: 11px;
1458
1519
  }
1459
1520
 
1521
+ .profiler-toolbar-logo {
1522
+ border-right: 1px solid var(--profiler-border) !important;
1523
+ }
1524
+
1460
1525
  .profiler-text--success {
1461
1526
  color: var(--profiler-success) !important;
1462
1527
  }
@@ -1477,19 +1542,22 @@ a.profiler-toolbar-item {
1477
1542
  color: var(--profiler-accent) !important;
1478
1543
  }
1479
1544
 
1480
- a.profiler-toolbar-item.profiler-text--error::after {
1545
+ .profiler-toolbar-item.profiler-text--error::after {
1481
1546
  background: var(--profiler-error);
1482
1547
  }
1483
1548
 
1484
- a.profiler-toolbar-item.profiler-text--warning::after {
1549
+ .profiler-toolbar-item.profiler-text--warning::after {
1485
1550
  background: var(--profiler-warning);
1486
1551
  }
1487
1552
 
1553
+ .profiler-toolbar-item.profiler-text--success::after {
1554
+ background: var(--profiler-success);
1555
+ }
1556
+
1488
1557
  .profiler-toolbar-hoverable {
1489
1558
  position: relative;
1490
1559
  }
1491
1560
  .profiler-toolbar-hoverable .profiler-toolbar-panel {
1492
- display: none;
1493
1561
  position: absolute;
1494
1562
  bottom: calc(100% + 12px);
1495
1563
  left: 50%;
@@ -1502,6 +1570,16 @@ a.profiler-toolbar-item.profiler-text--warning::after {
1502
1570
  box-shadow: var(--profiler-shadow-xl), 0 0 0 1px var(--profiler-border-accent);
1503
1571
  z-index: var(--profiler-z-panel);
1504
1572
  overflow: hidden;
1573
+ opacity: 0;
1574
+ visibility: hidden;
1575
+ pointer-events: none;
1576
+ transition: opacity 160ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 160ms;
1577
+ }
1578
+ .profiler-toolbar-hoverable .profiler-toolbar-panel.is-visible {
1579
+ opacity: 1;
1580
+ visibility: visible;
1581
+ pointer-events: auto;
1582
+ transition: opacity 160ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s;
1505
1583
  }
1506
1584
  .profiler-toolbar-hoverable .profiler-toolbar-panel::before {
1507
1585
  content: "";
@@ -1646,6 +1724,16 @@ a.profiler-toolbar-item.profiler-text--warning::after {
1646
1724
  margin-top: 6px;
1647
1725
  }
1648
1726
 
1727
+ #profiler-toolbar[data-collapsed=true] .profiler-toolbar-item:not(.profiler-toolbar-collapse-btn) {
1728
+ display: none;
1729
+ }
1730
+ #profiler-toolbar[data-collapsed=true] .profiler-toolbar-collapse-btn {
1731
+ margin-left: 0;
1732
+ }
1733
+ #profiler-toolbar[data-collapsed=true]::before {
1734
+ opacity: 0;
1735
+ }
1736
+
1649
1737
  .container > table {
1650
1738
  width: 100%;
1651
1739
  border-collapse: collapse;
@@ -396,6 +396,11 @@
396
396
  return "function" == typeof t3 ? t3(n2) : t3;
397
397
  }
398
398
 
399
+ // app/assets/typescript/profiler/dashboard/utils.ts
400
+ function getGemVersion() {
401
+ return document.querySelector('meta[name="profiler-version"]')?.content ?? "";
402
+ }
403
+
399
404
  // app/assets/typescript/profiler/components/dashboard/tabs/shared/utils.ts
400
405
  function methodBadge(method) {
401
406
  const map = { GET: "info", POST: "success", PUT: "warning", PATCH: "warning", DELETE: "error" };
@@ -1635,6 +1640,7 @@
1635
1640
  { key: "has_exception", label: "Has exception" }
1636
1641
  ];
1637
1642
  function ProfileList() {
1643
+ const currentVersion = getGemVersion();
1638
1644
  const params = new URLSearchParams(window.location.search);
1639
1645
  const initialSection = () => {
1640
1646
  const s3 = params.get("section");
@@ -1982,7 +1988,11 @@
1982
1988
  /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F50D}" }),
1983
1989
  " Rails Profiler"
1984
1990
  ] }),
1985
- /* @__PURE__ */ u3("p", { children: "Recent profiled requests and jobs" })
1991
+ /* @__PURE__ */ u3("p", { children: "Recent profiled requests and jobs" }),
1992
+ currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
1993
+ "v",
1994
+ currentVersion
1995
+ ] })
1986
1996
  ] }),
1987
1997
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
1988
1998
  /* @__PURE__ */ u3("div", { class: "tabs", children: [
@@ -2092,7 +2102,10 @@
2092
2102
  /* @__PURE__ */ u3("tbody", { children: sortedProfiles.map((p3) => /* @__PURE__ */ u3("tr", { children: [
2093
2103
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2094
2104
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: methodClass(p3.method), children: p3.method }) }),
2095
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }) }),
2105
+ /* @__PURE__ */ u3("td", { children: [
2106
+ /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2107
+ 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" })
2108
+ ] }),
2096
2109
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2097
2110
  p3.duration.toFixed(2),
2098
2111
  " ms"
@@ -2169,7 +2182,10 @@
2169
2182
  const isFailed = p3.status === 500;
2170
2183
  return /* @__PURE__ */ u3("tr", { children: [
2171
2184
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2172
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }) }),
2185
+ /* @__PURE__ */ u3("td", { children: [
2186
+ /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2187
+ 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" })
2188
+ ] }),
2173
2189
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
2174
2190
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2175
2191
  p3.duration.toFixed(2),
@@ -4482,7 +4498,17 @@
4482
4498
  " MB"
4483
4499
  ] })
4484
4500
  ] }),
4485
- /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: new Date(profile.started_at).toLocaleString("en", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) })
4501
+ /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: new Date(profile.started_at).toLocaleString("en", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) }),
4502
+ profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
4503
+ "v",
4504
+ profile.gem_version
4505
+ ] })
4506
+ ] }),
4507
+ profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
4508
+ "\u26A0\uFE0F Profil captur\xE9 avec la version ",
4509
+ /* @__PURE__ */ u3("strong", { children: profile.gem_version }),
4510
+ " \u2014 version actuelle : ",
4511
+ /* @__PURE__ */ u3("strong", { children: getGemVersion() })
4486
4512
  ] })
4487
4513
  ] }),
4488
4514
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
@@ -4628,8 +4654,18 @@
4628
4654
  (profile.memory / 1024 / 1024).toFixed(2),
4629
4655
  " MB"
4630
4656
  ] })
4657
+ ] }),
4658
+ profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
4659
+ "v",
4660
+ profile.gem_version
4631
4661
  ] })
4632
4662
  ] }),
4663
+ profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
4664
+ "\u26A0\uFE0F Profil captur\xE9 avec la version ",
4665
+ /* @__PURE__ */ u3("strong", { children: profile.gem_version }),
4666
+ " \u2014 version actuelle : ",
4667
+ /* @__PURE__ */ u3("strong", { children: getGemVersion() })
4668
+ ] }),
4633
4669
  parent && /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2 profiler-text--sm", children: [
4634
4670
  /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "Triggered by:" }),
4635
4671
  parent.profile_type === "http" ? /* @__PURE__ */ u3("span", { children: [
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
+ <meta name="profiler-version" content="<%= Profiler::VERSION %>">
8
9
 
9
10
  <link rel="stylesheet" href="/_profiler/assets/profiler.css">
10
11
  <script src="/_profiler/assets/profiler.js" defer></script>
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
+ <meta name="profiler-version" content="<%= Profiler::VERSION %>">
8
9
 
9
10
  <link rel="stylesheet" href="/_profiler/assets/profiler.css">
10
11
  <script src="/_profiler/assets/profiler.js" defer></script>
@@ -50,6 +50,7 @@ module Profiler
50
50
  def run(&block)
51
51
  profile = Models::Profile.new
52
52
  profile.profile_type = "job"
53
+ profile.gem_version = Profiler::VERSION
53
54
  profile.path = @job_class
54
55
  profile.method = "JOB"
55
56
  profile.parent_token = @parent_token if @parent_token
@@ -76,6 +76,13 @@ module Profiler
76
76
  lines << "**Duration:** #{profile.duration.round(2)} ms"
77
77
  lines << "**Memory:** #{(profile.memory / 1024.0 / 1024.0).round(2)} MB" if profile.memory
78
78
  lines << "**Time:** #{profile.started_at}"
79
+ if profile.gem_version
80
+ if profile.gem_version != Profiler::VERSION
81
+ lines << "**Gem Version:** #{profile.gem_version} ⚠️ (current: #{Profiler::VERSION})"
82
+ else
83
+ lines << "**Gem Version:** #{profile.gem_version}"
84
+ end
85
+ end
79
86
  lines << "**Parent Token:** #{profile.parent_token}" if profile.parent_token
80
87
  lines << ""
81
88
  lines
@@ -4,7 +4,7 @@ module Profiler
4
4
  module MCP
5
5
  module Tools
6
6
  class QueryJobs
7
- ALL_FIELDS = %w[time job_class queue status duration token parent_token].freeze
7
+ ALL_FIELDS = %w[time job_class queue status duration gem_version token parent_token].freeze
8
8
 
9
9
  def self.call(params)
10
10
  limit = params["limit"]&.to_i || 20
@@ -64,6 +64,9 @@ module Profiler
64
64
  when "queue" then job_data["queue"] || "-"
65
65
  when "status" then job_data["status"] || "-"
66
66
  when "duration" then "#{profile.duration.round(2)}ms"
67
+ when "gem_version"
68
+ v = profile.gem_version || "-"
69
+ v != Profiler::VERSION ? "#{v} ⚠️" : v
67
70
  when "token" then profile.token.to_s
68
71
  when "parent_token" then profile.parent_token || "-"
69
72
  end
@@ -20,6 +20,7 @@ module Profiler
20
20
  collectors = nil
21
21
 
22
22
  profile = Models::Profile.new(build_request(env))
23
+ profile.gem_version = Profiler::VERSION
23
24
  Profiler::CurrentContext.token = profile.token
24
25
 
25
26
  # Capture request body before app processes it
@@ -62,9 +62,11 @@ module Profiler
62
62
  def toolbar_html
63
63
  <<~HTML
64
64
  #{ajax_interceptor_script}
65
- <div id="profiler-toolbar" data-token="#{@token}"></div>
66
- <script src="/_profiler/assets/profiler-toolbar.js" defer#{nonce_attr}></script>
67
65
  <style>#{toolbar_styles}</style>
66
+ <div id="profiler-toolbar" class="profiler-root" data-token="#{@token}"></div>
67
+ <button id="profiler-toolbar-toggle" class="profiler-root" title="Toggle profiler (Alt+P)"><span class="profiler-toggle-icon">&#9654;</span></button>
68
+ <script#{nonce_attr}>(function(){var c=localStorage.getItem('profiler-toolbar-collapsed')==='true',t=localStorage.getItem('profiler-theme'),theme=t==='light'?'light':t==='dark'?'dark':(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'),el=document.getElementById('profiler-toolbar'),tog=document.getElementById('profiler-toolbar-toggle');if(c){el.style.cssText='animation:none!important;transform:translateX(calc(100% + 44px))';tog.dataset.collapsed='true';}el.setAttribute('data-theme',theme);tog.setAttribute('data-theme',theme);})();</script>
69
+ <script src="/_profiler/assets/profiler-toolbar.js" defer#{nonce_attr}></script>
68
70
  HTML
69
71
  end
70
72
 
@@ -76,39 +78,28 @@ module Profiler
76
78
  # Variables are defined on #profiler-toolbar to avoid polluting the host app.
77
79
  def toolbar_styles
78
80
  <<~'CSS'
79
- #profiler-toolbar {
81
+ .profiler-root {
80
82
  --pf-bg: #080b10;
81
83
  --pf-surface: #0d1117;
82
84
  --pf-raised: #131920;
83
85
  --pf-text: #eef2f7;
84
- --pf-muted: #5e7080;
86
+ --pf-muted: #4d6170;
85
87
  --pf-amber: #f59e0b;
86
88
  --pf-amber-h: #fbbf24;
87
- --pf-amber-bg: rgba(245,158,11,.1);
88
- --pf-amber-glow: rgba(245,158,11,.25);
89
+ --pf-amber-bg: rgba(245,158,11,.08);
90
+ --pf-amber-glow: rgba(245,158,11,.2);
89
91
  --pf-success: #22c55e;
90
92
  --pf-warning: #fb923c;
91
93
  --pf-error: #f87171;
92
94
  --pf-info: #60a5fa;
93
- --pf-border: rgba(255,255,255,.07);
94
- --pf-border-s: rgba(255,255,255,.13);
95
+ --pf-border: rgba(255,255,255,.06);
96
+ --pf-border-s: rgba(255,255,255,.11);
95
97
  --pf-mono: 'JetBrains Mono','SF Mono','Fira Code',monospace;
96
- --pf-tf: 120ms cubic-bezier(.4,0,.2,1);
97
- --pf-tb: 220ms cubic-bezier(.4,0,.2,1);
98
-
99
- position: fixed;
100
- bottom: 0; left: 0; right: 0;
101
- height: 44px;
102
- background: var(--pf-bg);
103
- border-top: 1px solid var(--pf-border-s);
104
- z-index: 999999;
105
- font-family: var(--pf-mono);
106
- font-size: 11px;
107
- color: var(--pf-muted);
108
- animation: pfIn 300ms cubic-bezier(.4,0,.2,1) both;
98
+ --pf-tf: 140ms cubic-bezier(.4,0,.2,1);
99
+ --pf-tb: 240ms cubic-bezier(.4,0,.2,1);
109
100
  }
110
- /* ── Light theme overrides ───────────────────────────────────────── */
111
- #profiler-toolbar[data-theme="light"] {
101
+ /* ── Light theme ─────────────────────────────────────────────────── */
102
+ .profiler-root[data-theme="light"] {
112
103
  --pf-bg: #f5f3ef;
113
104
  --pf-surface: #edeae3;
114
105
  --pf-raised: #e3ded5;
@@ -116,39 +107,63 @@ module Profiler
116
107
  --pf-muted: #8a7a6e;
117
108
  --pf-amber: #b45309;
118
109
  --pf-amber-h: #92400e;
119
- --pf-amber-bg: rgba(180,83,9,.08);
120
- --pf-amber-glow: rgba(180,83,9,.2);
110
+ --pf-amber-bg: rgba(180,83,9,.07);
111
+ --pf-amber-glow: rgba(180,83,9,.18);
121
112
  --pf-success: #15803d;
122
113
  --pf-warning: #c2410c;
123
114
  --pf-error: #dc2626;
124
115
  --pf-info: #1d4ed8;
125
- --pf-border: rgba(28,20,16,.1);
126
- --pf-border-s: rgba(28,20,16,.2);
116
+ --pf-border: rgba(28,20,16,.08);
117
+ --pf-border-s: rgba(28,20,16,.18);
127
118
  }
128
-
119
+ #profiler-toolbar {
120
+ position: fixed;
121
+ bottom: 0; left: 0; right: 44px;
122
+ height: 44px;
123
+ background: linear-gradient(180deg, var(--pf-surface) 0%, var(--pf-bg) 100%);
124
+ border-top: 1px solid var(--pf-border-s);
125
+ z-index: 999999;
126
+ font-family: var(--pf-mono);
127
+ font-size: 11px;
128
+ color: var(--pf-muted);
129
+ animation: pfIn 320ms cubic-bezier(.4,0,.2,1) both;
130
+ }
131
+ /* ── Slide-in animation ──────────────────────────────────────────── */
129
132
  @keyframes pfIn {
130
133
  from { transform: translateY(100%); opacity: 0; }
131
- to { transform: translateY(0); opacity: 1; }
134
+ to { transform: translateY(0); opacity: 1; }
135
+ }
136
+ @keyframes pfPop {
137
+ 0% { transform: scaleX(1); }
138
+ 15% { transform: scaleX(1.18); }
139
+ 40% { transform: scaleX(0.87); }
140
+ 65% { transform: scaleX(1.08); }
141
+ 85% { transform: scaleX(0.97); }
142
+ 100% { transform: scaleX(1); }
132
143
  }
144
+ /* ── Amber accent line ───────────────────────────────────────────── */
133
145
  #profiler-toolbar::before {
134
146
  content: '';
135
147
  position: absolute;
136
148
  top: -1px; left: 0; right: 0;
137
149
  height: 1px;
138
- background: linear-gradient(90deg,transparent 0%,#f59e0b 20%,#fbbf24 50%,#f59e0b 80%,transparent 100%);
139
- opacity: .6;
150
+ background: linear-gradient(90deg, transparent 0%, #f59e0b 25%, #fbbf24 50%, #f59e0b 75%, transparent 100%);
151
+ opacity: .55;
152
+ transition: opacity 280ms;
140
153
  }
154
+ /* ── Container ───────────────────────────────────────────────────── */
141
155
  #profiler-toolbar .profiler-toolbar-container {
142
156
  display: flex;
143
157
  align-items: stretch;
144
158
  height: 100%;
145
159
  }
160
+ /* ── Items ───────────────────────────────────────────────────────── */
146
161
  #profiler-toolbar .profiler-toolbar-item {
147
162
  position: relative;
148
163
  display: inline-flex;
149
164
  align-items: center;
150
165
  gap: 5px;
151
- padding: 0 14px;
166
+ padding: 0 13px;
152
167
  color: var(--pf-muted);
153
168
  text-decoration: none;
154
169
  white-space: nowrap;
@@ -159,7 +174,9 @@ module Profiler
159
174
  font-family: var(--pf-mono);
160
175
  font-size: 11px;
161
176
  transition: color var(--pf-tf), background var(--pf-tf);
177
+ cursor: default;
162
178
  }
179
+ /* Amber bottom indicator — springy easing */
163
180
  #profiler-toolbar .profiler-toolbar-item::after {
164
181
  content: '';
165
182
  position: absolute;
@@ -168,52 +185,114 @@ module Profiler
168
185
  background: var(--pf-amber);
169
186
  transform: scaleX(0);
170
187
  transform-origin: left;
171
- transition: transform var(--pf-tb);
188
+ transition: transform 280ms cubic-bezier(.34,1.4,.64,1);
172
189
  }
173
- #profiler-toolbar a.profiler-toolbar-item { cursor: pointer; }
174
- #profiler-toolbar a.profiler-toolbar-item:hover {
190
+ /* Hover: all interactive items */
191
+ #profiler-toolbar a.profiler-toolbar-item,
192
+ #profiler-toolbar .profiler-toolbar-hoverable { cursor: pointer; }
193
+
194
+ #profiler-toolbar a.profiler-toolbar-item:hover,
195
+ #profiler-toolbar .profiler-toolbar-hoverable:hover {
175
196
  color: var(--pf-text);
176
197
  background: var(--pf-amber-bg);
177
198
  }
178
- #profiler-toolbar a.profiler-toolbar-item:hover::after { transform: scaleX(1); }
199
+ #profiler-toolbar a.profiler-toolbar-item:hover::after,
200
+ #profiler-toolbar .profiler-toolbar-hoverable:hover::after { transform: scaleX(1); }
201
+
202
+ /* Severity underline colors */
203
+ #profiler-toolbar .profiler-text--error::after { background: var(--pf-error); }
204
+ #profiler-toolbar .profiler-text--warning::after { background: var(--pf-warning); }
205
+ #profiler-toolbar .profiler-text--success::after { background: var(--pf-success); }
206
+
207
+ /* Semantic colors */
179
208
  #profiler-toolbar .profiler-text--success { color: var(--pf-success) !important; }
180
209
  #profiler-toolbar .profiler-text--warning { color: var(--pf-warning) !important; }
181
210
  #profiler-toolbar .profiler-text--error { color: var(--pf-error) !important; }
182
211
  #profiler-toolbar .profiler-text--accent { color: var(--pf-amber) !important; }
183
212
  #profiler-toolbar .profiler-text--muted { color: var(--pf-muted) !important; }
184
- #profiler-toolbar a.profiler-toolbar-item.profiler-text--error::after { background: var(--pf-error); }
185
- #profiler-toolbar a.profiler-toolbar-item.profiler-text--warning::after { background: var(--pf-warning); }
186
- #profiler-toolbar .profiler-toolbar-item:last-child {
187
- border-right: none;
188
- margin-left: auto;
213
+
214
+ /* Logo link */
215
+ #profiler-toolbar .profiler-toolbar-logo {
216
+ border-right: 1px solid var(--pf-border) !important;
189
217
  color: var(--pf-amber);
190
- padding-left: 20px;
191
218
  font-weight: 600;
219
+ letter-spacing: .02em;
220
+ }
221
+ #profiler-toolbar .profiler-toolbar-logo:hover { color: var(--pf-amber-h); }
222
+
223
+ /* ── Toggle button (fixed, always visible) ───────────────────────── */
224
+ #profiler-toolbar-toggle {
225
+ position: fixed;
226
+ bottom: 0; right: 0;
227
+ width: 44px; height: 44px;
228
+ display: flex; align-items: center; justify-content: center;
229
+ cursor: pointer;
230
+ background: linear-gradient(180deg, var(--pf-surface) 0%, var(--pf-bg) 100%);
231
+ border: none;
232
+ border-top: 1px solid var(--pf-border-s);
233
+ border-left: 1px solid var(--pf-border);
234
+ color: var(--pf-muted);
235
+ font-family: var(--pf-mono);
236
+ font-size: 13px;
237
+ z-index: 1000000;
238
+ border-radius: 0;
239
+ transition: color var(--pf-tf), background var(--pf-tf);
240
+ }
241
+ #profiler-toolbar-toggle:hover {
242
+ color: var(--pf-amber);
243
+ animation: pfPop 380ms ease-out both;
244
+ transform: none;
192
245
  }
193
- #profiler-toolbar a.profiler-toolbar-item:last-child:hover { color: var(--pf-amber-h); }
246
+ .profiler-toggle-icon {
247
+ display: inline-block;
248
+ transition: transform 280ms cubic-bezier(.4,0,.2,1);
249
+ }
250
+ #profiler-toolbar-toggle[data-collapsed="true"] .profiler-toggle-icon {
251
+ transform: scaleX(-1);
252
+ }
253
+
254
+ /* ── Hoverable wrapper ───────────────────────────────────────────── */
194
255
  #profiler-toolbar .profiler-toolbar-hoverable { position: relative; }
256
+
257
+ /* ── Panel (CSS-driven visibility) ───────────────────────────────── */
195
258
  #profiler-toolbar .profiler-toolbar-panel {
196
- display: none;
197
259
  position: absolute;
198
- bottom: calc(100% + 10px);
260
+ bottom: calc(100% + 12px);
199
261
  left: 50%;
200
262
  transform: translateX(-50%);
201
263
  min-width: 300px;
202
264
  max-width: 420px;
203
265
  background: var(--pf-surface);
204
266
  border: 1px solid var(--pf-border-s);
205
- border-radius: 8px;
206
- box-shadow: 0 8px 32px rgba(0,0,0,.65), 0 0 0 1px var(--pf-amber-glow);
267
+ border-radius: 10px;
268
+ box-shadow: 0 12px 40px rgba(0,0,0,.7), 0 0 0 1px var(--pf-amber-glow);
207
269
  z-index: 1000000;
208
270
  overflow: hidden;
271
+ /* Hidden state */
272
+ opacity: 0;
273
+ visibility: hidden;
274
+ pointer-events: none;
275
+ transition:
276
+ opacity 160ms cubic-bezier(.4,0,.2,1),
277
+ visibility 0s linear 160ms;
209
278
  }
279
+ #profiler-toolbar .profiler-toolbar-panel.is-visible {
280
+ opacity: 1;
281
+ visibility: visible;
282
+ pointer-events: auto;
283
+ transition:
284
+ opacity 160ms cubic-bezier(.4,0,.2,1),
285
+ visibility 0s;
286
+ }
287
+ /* Amber top bar on panel */
210
288
  #profiler-toolbar .profiler-toolbar-panel::before {
211
289
  content: '';
212
290
  position: absolute;
213
291
  top: 0; left: 0; right: 0;
214
292
  height: 2px;
215
- background: linear-gradient(90deg,#f59e0b,#fbbf24);
293
+ background: linear-gradient(90deg, #f59e0b, #fbbf24);
216
294
  }
295
+ /* Arrow */
217
296
  #profiler-toolbar .profiler-toolbar-panel::after {
218
297
  content: '';
219
298
  position: absolute;
@@ -223,11 +302,13 @@ module Profiler
223
302
  border-top-color: var(--pf-border-s);
224
303
  }
225
304
  #profiler-toolbar .profiler-toolbar-panel-large { min-width: 420px; max-width: 560px; }
305
+
306
+ /* ── Panel header ────────────────────────────────────────────────── */
226
307
  #profiler-toolbar .profiler-toolbar-panel-header {
227
308
  display: flex;
228
309
  align-items: center;
229
310
  justify-content: space-between;
230
- padding: 10px 14px 8px;
311
+ padding: 10px 14px 9px;
231
312
  font-size: 10px;
232
313
  font-weight: 700;
233
314
  letter-spacing: .12em;
@@ -244,6 +325,8 @@ module Profiler
244
325
  letter-spacing: 0;
245
326
  font-size: 10px;
246
327
  }
328
+
329
+ /* ── Panel content ───────────────────────────────────────────────── */
247
330
  #profiler-toolbar .profiler-toolbar-panel-content {
248
331
  padding: 8px 14px 12px;
249
332
  max-height: 380px;
@@ -258,16 +341,22 @@ module Profiler
258
341
  background: var(--pf-border-s);
259
342
  border-radius: 99px;
260
343
  }
344
+
345
+ /* ── Panel rows ──────────────────────────────────────────────────── */
261
346
  #profiler-toolbar .profiler-toolbar-panel-row {
262
347
  display: flex;
263
348
  justify-content: space-between;
264
349
  align-items: baseline;
265
- padding: 5px 0;
350
+ padding: 6px 0;
266
351
  border-bottom: 1px solid var(--pf-border);
267
352
  gap: 12px;
268
353
  }
269
354
  #profiler-toolbar .profiler-toolbar-panel-row:last-child { border-bottom: none; }
270
- #profiler-toolbar .profiler-toolbar-panel-row span { color: var(--pf-muted); font-size: 10px; flex-shrink: 0; }
355
+ #profiler-toolbar .profiler-toolbar-panel-row span {
356
+ color: var(--pf-muted);
357
+ font-size: 10px;
358
+ flex-shrink: 0;
359
+ }
271
360
  #profiler-toolbar .profiler-toolbar-panel-row strong {
272
361
  color: var(--pf-text);
273
362
  font-weight: 600;
@@ -275,6 +364,8 @@ module Profiler
275
364
  font-variant-numeric: tabular-nums;
276
365
  word-break: break-all;
277
366
  }
367
+
368
+ /* ── Section header ──────────────────────────────────────────────── */
278
369
  #profiler-toolbar .profiler-section__header {
279
370
  font-size: 9px;
280
371
  font-weight: 700;
@@ -282,22 +373,26 @@ module Profiler
282
373
  text-transform: uppercase;
283
374
  color: var(--pf-amber);
284
375
  padding: 10px 0 4px;
285
- border-bottom: 1px solid rgba(245,158,11,.2);
376
+ border-bottom: 1px solid rgba(245,158,11,.18);
286
377
  margin-bottom: 6px;
287
378
  font-family: var(--pf-mono);
288
379
  }
289
380
  #profiler-toolbar .profiler-section__header:first-child { padding-top: 4px; }
381
+
382
+ /* ── Query cards ─────────────────────────────────────────────────── */
290
383
  #profiler-toolbar .profiler-toolbar-panel-query {
291
384
  padding: 7px 10px;
292
385
  background: var(--pf-raised);
293
386
  border: 1px solid var(--pf-border);
294
- border-radius: 4px;
387
+ border-radius: 5px;
295
388
  margin-bottom: 5px;
389
+ transition: border-color var(--pf-tf);
296
390
  }
297
391
  #profiler-toolbar .profiler-toolbar-panel-query:last-child { margin-bottom: 0; }
392
+ #profiler-toolbar .profiler-toolbar-panel-query:hover { border-color: var(--pf-border-s); }
298
393
  #profiler-toolbar .profiler-toolbar-panel-query-slow {
299
394
  border-left: 2px solid var(--pf-error);
300
- background: rgba(248,113,113,.06);
395
+ background: rgba(248,113,113,.05);
301
396
  }
302
397
  #profiler-toolbar .profiler-toolbar-panel-query code {
303
398
  display: block;
@@ -313,6 +408,8 @@ module Profiler
313
408
  padding: 0;
314
409
  border: none;
315
410
  }
411
+
412
+ /* ── More hint ───────────────────────────────────────────────────── */
316
413
  #profiler-toolbar .profiler-more {
317
414
  text-align: center;
318
415
  padding: 7px;
@@ -322,13 +419,17 @@ module Profiler
322
419
  border-top: 1px dashed var(--pf-border);
323
420
  margin-top: 6px;
324
421
  }
422
+
423
+ /* ── Ajax cards ──────────────────────────────────────────────────── */
325
424
  #profiler-toolbar .profiler-ajax-card {
326
425
  background: var(--pf-raised);
327
426
  border: 1px solid var(--pf-border);
328
- border-radius: 4px;
427
+ border-radius: 5px;
329
428
  padding: 6px 8px;
330
429
  margin-bottom: 4px;
430
+ transition: border-color var(--pf-tf);
331
431
  }
432
+ #profiler-toolbar .profiler-ajax-card:hover { border-color: var(--pf-border-s); }
332
433
  #profiler-toolbar .profiler-ajax-card--success { border-left: 2px solid var(--pf-success); }
333
434
  #profiler-toolbar .profiler-ajax-card--error { border-left: 2px solid var(--pf-error); }
334
435
  #profiler-toolbar .profiler-ajax-card__row {
@@ -337,10 +438,12 @@ module Profiler
337
438
  align-items: center;
338
439
  gap: 8px;
339
440
  }
441
+
442
+ /* ── Dump cards ──────────────────────────────────────────────────── */
340
443
  #profiler-toolbar .profiler-dump-card {
341
444
  background: var(--pf-raised);
342
445
  border: 1px solid var(--pf-border);
343
- border-radius: 4px;
446
+ border-radius: 5px;
344
447
  padding: 6px 8px;
345
448
  margin-bottom: 4px;
346
449
  }
@@ -349,11 +452,13 @@ module Profiler
349
452
  justify-content: space-between;
350
453
  margin-bottom: 4px;
351
454
  }
455
+
456
+ /* ── Badges ──────────────────────────────────────────────────────── */
352
457
  #profiler-toolbar .badge {
353
458
  display: inline-flex;
354
459
  align-items: center;
355
- padding: 1px 5px;
356
- border-radius: 3px;
460
+ padding: 1px 6px;
461
+ border-radius: 4px;
357
462
  font-size: 9px;
358
463
  font-weight: 700;
359
464
  letter-spacing: .05em;
@@ -361,10 +466,12 @@ module Profiler
361
466
  background: var(--pf-raised);
362
467
  color: var(--pf-muted);
363
468
  }
364
- #profiler-toolbar .badge-info { background: rgba(96,165,250,.12); color: var(--pf-info); }
365
- #profiler-toolbar .badge-success { background: rgba(34,197,94,.12); color: var(--pf-success); }
366
- #profiler-toolbar .badge-warning { background: rgba(251,146,60,.12); color: var(--pf-warning); }
367
- #profiler-toolbar .badge-error { background: rgba(248,113,113,.12); color: var(--pf-error); }
469
+ #profiler-toolbar .badge-info { background: rgba(96,165,250,.1); color: var(--pf-info); }
470
+ #profiler-toolbar .badge-success { background: rgba(34,197,94,.1); color: var(--pf-success); }
471
+ #profiler-toolbar .badge-warning { background: rgba(251,146,60,.1); color: var(--pf-warning); }
472
+ #profiler-toolbar .badge-error { background: rgba(248,113,113,.1); color: var(--pf-error); }
473
+
474
+ /* ── Utilities ───────────────────────────────────────────────────── */
368
475
  #profiler-toolbar .profiler-flex { display: flex; }
369
476
  #profiler-toolbar .profiler-flex--between { justify-content: space-between; }
370
477
  #profiler-toolbar .profiler-flex--gap-2 { gap: 8px; }
@@ -13,7 +13,8 @@ module Profiler
13
13
  :response_headers, :collectors_data, :collectors_metadata,
14
14
  :parent_token, :is_ajax, :profile_type,
15
15
  :request_body, :request_body_encoding,
16
- :response_body, :response_body_encoding
16
+ :response_body, :response_body_encoding,
17
+ :gem_version
17
18
 
18
19
  def initialize(request = nil)
19
20
  @token = SecureRandom.hex(16)
@@ -76,6 +77,7 @@ module Profiler
76
77
 
77
78
  {
78
79
  profile_type: @profile_type,
80
+ gem_version: @gem_version,
79
81
  token: @token,
80
82
  path: @path,
81
83
  method: @method,
@@ -127,6 +129,7 @@ module Profiler
127
129
  profile.parent_token = data[:parent_token]
128
130
  profile.is_ajax = data[:is_ajax] || false
129
131
  profile.profile_type = data[:profile_type] || "http"
132
+ profile.gem_version = data[:gem_version]
130
133
 
131
134
  # Convert collectors_data keys to strings recursively for consistency
132
135
  profile.collectors_data = (data[:collectors_data] || {}).transform_keys(&:to_s).transform_values do |value|
@@ -51,17 +51,18 @@ module Profiler
51
51
  @db.execute(
52
52
  <<~SQL,
53
53
  INSERT OR REPLACE INTO profiler_profiles (
54
- token, profile_type, path, method, status, duration, memory,
54
+ token, profile_type, gem_version, path, method, status, duration, memory,
55
55
  started_at, finished_at, parent_token, is_ajax,
56
56
  tabs, params, headers, response_headers, collectors_meta
57
57
  ) VALUES (
58
- :token, :profile_type, :path, :method, :status, :duration, :memory,
58
+ :token, :profile_type, :gem_version, :path, :method, :status, :duration, :memory,
59
59
  :started_at, :finished_at, :parent_token, :is_ajax,
60
60
  :tabs, :params, :headers, :response_headers, :collectors_meta
61
61
  )
62
62
  SQL
63
63
  token: token,
64
64
  profile_type: data[:profile_type] || "http",
65
+ gem_version: data[:gem_version],
65
66
  path: data[:path],
66
67
  method: data[:method],
67
68
  status: data[:status],
@@ -176,6 +177,7 @@ module Profiler
176
177
  CREATE TABLE IF NOT EXISTS profiler_profiles (
177
178
  token TEXT PRIMARY KEY,
178
179
  profile_type TEXT NOT NULL DEFAULT 'http',
180
+ gem_version TEXT,
179
181
  path TEXT,
180
182
  method TEXT,
181
183
  status INTEGER,
@@ -202,6 +204,12 @@ module Profiler
202
204
  CREATE INDEX IF NOT EXISTS idx_profiler_profile_type
203
205
  ON profiler_profiles(profile_type);
204
206
  SQL
207
+
208
+ begin
209
+ @db.execute("ALTER TABLE profiler_profiles ADD COLUMN gem_version TEXT")
210
+ rescue SQLite3::Exception
211
+ # column already exists
212
+ end
205
213
  end
206
214
 
207
215
  def row_to_profile(row, load_blobs: true)
@@ -223,6 +231,7 @@ module Profiler
223
231
  Models::Profile.from_hash(
224
232
  token: row["token"],
225
233
  profile_type: row["profile_type"],
234
+ gem_version: row["gem_version"],
226
235
  path: row["path"],
227
236
  method: row["method"],
228
237
  status: row["status"],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.19.2"
4
+ VERSION = "0.21.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.2
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-24 00:00:00.000000000 Z
11
+ date: 2026-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails