rails-profiler 0.14.0 → 0.15.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: 44b1fd924fe2eb7f0483bc36f0e3956e81535da3ce39e60f6e29c091194f22ce
4
- data.tar.gz: c840abbc0813c4e8c9516c507138e4ead25e9980f5c02a3306461b1a308ff6e3
3
+ metadata.gz: 4f9f680d793888c215c4b0203f3f4ceb538d415de05916400730fd86561e1c84
4
+ data.tar.gz: d0d944230d9e29d4fea24b54c6ea7134d1994e6624c1e7d0994cd7900a716406
5
5
  SHA512:
6
- metadata.gz: 3d5307f599d3822291a99d931cacb1dc70218e32a120e136cebe98790e89f803e0ad84a5520b6961279c88048a7c2262c91600d770f07639cb5e31be32ff3e5a
7
- data.tar.gz: 341cfa488dd91fe1b566dfc7c0a4d28dd6ab42e5fb0eb874250ca4b0482e3bb154d398c1c0919fef09a812a1cae7f897542b5702eb097c83e80e534cbbf777bd
6
+ metadata.gz: b46d2fd2ecc46a051a5616115a0c2ef8b4da155b009ac5734e5e44fe355e14b2b30c4cdeaf52721d403436eebb5076ca6b3c6c3343407d326cdae17394607df7
7
+ data.tar.gz: fc1e2d5d01929200d5fe4f705db509fce7daed7ed13f8a474b31196e397bf797b912a85add2c0f2ae662f3d90b94f65e8dcb383ea142f1706420dfe68b4a961f
@@ -3618,6 +3618,561 @@
3618
3618
  ] });
3619
3619
  }
3620
3620
 
3621
+ // app/assets/typescript/profiler/components/dashboard/tabs/EnvTab.tsx
3622
+ function detectType(value) {
3623
+ if (value === "") return { label: "empty", color: "var(--profiler-text-muted)" };
3624
+ if (/^(true|false|yes|no)$/i.test(value)) return { label: "bool", color: "#a78bfa" };
3625
+ if (/^\d+$/.test(value)) return { label: "int", color: "var(--profiler-success,#22c55e)" };
3626
+ if (/^\d+\.\d+$/.test(value)) return { label: "float", color: "var(--profiler-success,#22c55e)" };
3627
+ return { label: "string", color: "var(--profiler-text-muted)" };
3628
+ }
3629
+ function getPrefix(key) {
3630
+ const idx = key.indexOf("_");
3631
+ return idx > 0 ? key.slice(0, idx) : "OTHER";
3632
+ }
3633
+ function parseEnvFile(content) {
3634
+ const result = {};
3635
+ for (const raw of content.split("\n")) {
3636
+ const line = raw.trim();
3637
+ if (!line || line.startsWith("#")) continue;
3638
+ const eq = line.indexOf("=");
3639
+ if (eq < 1) continue;
3640
+ const key = line.slice(0, eq).trim();
3641
+ let value = line.slice(eq + 1);
3642
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
3643
+ value = value.slice(1, -1);
3644
+ }
3645
+ result[key] = value;
3646
+ }
3647
+ return result;
3648
+ }
3649
+ function loadUnmasked() {
3650
+ try {
3651
+ const raw = localStorage.getItem("profiler_unmasked_env_keys");
3652
+ return raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
3653
+ } catch {
3654
+ return /* @__PURE__ */ new Set();
3655
+ }
3656
+ }
3657
+ function saveUnmasked(keys) {
3658
+ try {
3659
+ localStorage.setItem("profiler_unmasked_env_keys", JSON.stringify([...keys]));
3660
+ } catch {
3661
+ }
3662
+ }
3663
+ function TypeBadgeComp({ value }) {
3664
+ const badge = detectType(value);
3665
+ if (!badge) return null;
3666
+ return /* @__PURE__ */ u3("span", { style: {
3667
+ display: "inline-block",
3668
+ padding: "0 5px",
3669
+ borderRadius: "3px",
3670
+ fontSize: "10px",
3671
+ fontWeight: 600,
3672
+ fontFamily: "monospace",
3673
+ border: `1px solid ${badge.color}`,
3674
+ color: badge.color,
3675
+ marginRight: "6px",
3676
+ verticalAlign: "middle",
3677
+ lineHeight: "16px",
3678
+ flexShrink: 0
3679
+ }, children: badge.label });
3680
+ }
3681
+ function isBool(value) {
3682
+ return /^(true|false|yes|no)$/i.test(value);
3683
+ }
3684
+ function isQuoted(value) {
3685
+ if (value === "") return false;
3686
+ if (isBool(value)) return false;
3687
+ if (/^\d+(\.\d+)?$/.test(value)) return false;
3688
+ return true;
3689
+ }
3690
+ async function patchEnvVar(key, value) {
3691
+ const res = await fetch("/_profiler/api/env_vars", {
3692
+ method: "PATCH",
3693
+ headers: { "Content-Type": "application/json" },
3694
+ body: JSON.stringify({ key, value })
3695
+ });
3696
+ if (!res.ok) {
3697
+ const body = await res.json().catch(() => ({}));
3698
+ throw new Error(body.error ?? `Request failed (${res.status})`);
3699
+ }
3700
+ }
3701
+ function EnvTab({ envData }) {
3702
+ const initial = T2(() => ({ ...envData?.variables ?? {} }), []);
3703
+ const [variables, setVariables] = d2(initial);
3704
+ const [total, setTotal] = d2(envData?.total ?? 0);
3705
+ const [search, setSearch] = d2("");
3706
+ const [collapsedGroups, setCollapsedGroups] = d2(/* @__PURE__ */ new Set());
3707
+ const [editingKey, setEditingKey] = d2(null);
3708
+ const [editValue, setEditValue] = d2("");
3709
+ const [newKey, setNewKey] = d2("");
3710
+ const [newValue, setNewValue] = d2("");
3711
+ const [flash, setFlash] = d2(null);
3712
+ const [saving, setSaving] = d2(false);
3713
+ const [refreshing, setRefreshing] = d2(false);
3714
+ const [unmaskedKeys, setUnmaskedKeys] = d2(loadUnmasked);
3715
+ const [copiedId, setCopiedId] = d2(null);
3716
+ const [readOnly, setReadOnly] = d2(false);
3717
+ const [showImport, setShowImport] = d2(false);
3718
+ const [importContent, setImportContent] = d2("");
3719
+ const editInputRef = A2(null);
3720
+ y2(() => {
3721
+ if (editingKey !== null) editInputRef.current?.focus();
3722
+ }, [editingKey]);
3723
+ y2(() => {
3724
+ refresh();
3725
+ }, []);
3726
+ const showFlash = (type, message) => {
3727
+ setFlash({ type, message });
3728
+ setTimeout(() => setFlash(null), 3e3);
3729
+ };
3730
+ const toggleUnmask = (key) => {
3731
+ setUnmaskedKeys((prev) => {
3732
+ const next = new Set(prev);
3733
+ if (next.has(key)) next.delete(key);
3734
+ else next.add(key);
3735
+ saveUnmasked(next);
3736
+ return next;
3737
+ });
3738
+ };
3739
+ const copy = async (text, id) => {
3740
+ try {
3741
+ await navigator.clipboard.writeText(text);
3742
+ setCopiedId(id);
3743
+ setTimeout(() => setCopiedId(null), 1500);
3744
+ } catch {
3745
+ showFlash("error", "Clipboard access denied");
3746
+ }
3747
+ };
3748
+ const toggleGroup = (prefix) => {
3749
+ setCollapsedGroups((prev) => {
3750
+ const next = new Set(prev);
3751
+ if (next.has(prefix)) next.delete(prefix);
3752
+ else next.add(prefix);
3753
+ return next;
3754
+ });
3755
+ };
3756
+ const refresh = async () => {
3757
+ setRefreshing(true);
3758
+ try {
3759
+ const res = await fetch("/_profiler/api/env_vars");
3760
+ if (!res.ok) throw new Error(`Request failed (${res.status})`);
3761
+ const data = await res.json();
3762
+ setVariables(data.variables);
3763
+ setTotal(data.total);
3764
+ showFlash("success", `Refreshed \u2014 ${data.total} variables`);
3765
+ } catch (e3) {
3766
+ showFlash("error", e3.message ?? "Failed to refresh");
3767
+ } finally {
3768
+ setRefreshing(false);
3769
+ }
3770
+ };
3771
+ const exportEnv = () => {
3772
+ const entries = filteredEntries;
3773
+ const content = entries.map(([k3, v3]) => `${k3}=${v3}`).join("\n") + "\n";
3774
+ const blob = new Blob([content], { type: "text/plain" });
3775
+ const url = URL.createObjectURL(blob);
3776
+ const a3 = document.createElement("a");
3777
+ a3.href = url;
3778
+ a3.download = ".env";
3779
+ a3.click();
3780
+ URL.revokeObjectURL(url);
3781
+ showFlash("success", `Exported ${entries.length} variables`);
3782
+ };
3783
+ const importPreview = T2(() => parseEnvFile(importContent), [importContent]);
3784
+ const importCount = Object.keys(importPreview).length;
3785
+ const importEnv = async () => {
3786
+ const entries = Object.entries(importPreview);
3787
+ if (!entries.length) return;
3788
+ setSaving(true);
3789
+ try {
3790
+ await Promise.all(entries.map(([k3, v3]) => patchEnvVar(k3, v3)));
3791
+ setVariables((prev) => ({ ...prev, ...importPreview }));
3792
+ setImportContent("");
3793
+ setShowImport(false);
3794
+ showFlash("success", `Imported ${entries.length} variables`);
3795
+ } catch (e3) {
3796
+ showFlash("error", e3.message ?? "Import failed");
3797
+ } finally {
3798
+ setSaving(false);
3799
+ }
3800
+ };
3801
+ const startEdit = (key) => {
3802
+ setEditingKey(key);
3803
+ setEditValue(variables[key] ?? "");
3804
+ };
3805
+ const cancelEdit = () => {
3806
+ setEditingKey(null);
3807
+ setEditValue("");
3808
+ };
3809
+ const saveEdit = async () => {
3810
+ if (!editingKey) return;
3811
+ setSaving(true);
3812
+ try {
3813
+ await patchEnvVar(editingKey, editValue);
3814
+ setVariables((prev) => ({ ...prev, [editingKey]: editValue }));
3815
+ showFlash("success", `${editingKey} updated`);
3816
+ setEditingKey(null);
3817
+ } catch (e3) {
3818
+ showFlash("error", e3.message ?? "Failed to update");
3819
+ } finally {
3820
+ setSaving(false);
3821
+ }
3822
+ };
3823
+ const deleteVar = async (key) => {
3824
+ setSaving(true);
3825
+ try {
3826
+ await patchEnvVar(key, null);
3827
+ setVariables((prev) => {
3828
+ const n2 = { ...prev };
3829
+ delete n2[key];
3830
+ return n2;
3831
+ });
3832
+ setUnmaskedKeys((prev) => {
3833
+ const n2 = new Set(prev);
3834
+ n2.delete(key);
3835
+ saveUnmasked(n2);
3836
+ return n2;
3837
+ });
3838
+ showFlash("success", `${key} deleted`);
3839
+ } catch (e3) {
3840
+ showFlash("error", e3.message ?? "Failed to delete");
3841
+ } finally {
3842
+ setSaving(false);
3843
+ }
3844
+ };
3845
+ const toggleBool = async (key, current) => {
3846
+ const next = /^(true|yes)$/i.test(current) ? "false" : "true";
3847
+ setSaving(true);
3848
+ try {
3849
+ await patchEnvVar(key, next);
3850
+ setVariables((prev) => ({ ...prev, [key]: next }));
3851
+ } catch (e3) {
3852
+ showFlash("error", e3.message ?? "Failed to update");
3853
+ } finally {
3854
+ setSaving(false);
3855
+ }
3856
+ };
3857
+ const addVar = async () => {
3858
+ const key = newKey.trim();
3859
+ if (!key) return;
3860
+ setSaving(true);
3861
+ try {
3862
+ await patchEnvVar(key, newValue);
3863
+ setVariables((prev) => ({ ...prev, [key]: newValue }));
3864
+ setNewKey("");
3865
+ setNewValue("");
3866
+ showFlash("success", `${key} added`);
3867
+ } catch (e3) {
3868
+ showFlash("error", e3.message ?? "Failed to add");
3869
+ } finally {
3870
+ setSaving(false);
3871
+ }
3872
+ };
3873
+ const handleEditKeyDown = (e3) => {
3874
+ if (e3.key === "Enter") saveEdit();
3875
+ if (e3.key === "Escape") cancelEdit();
3876
+ };
3877
+ const allEntries = Object.entries(variables);
3878
+ const filteredEntries = T2(() => {
3879
+ const q2 = search.toLowerCase().trim();
3880
+ if (!q2) return allEntries;
3881
+ return allEntries.filter(
3882
+ ([k3, v3]) => k3.toLowerCase().includes(q2) || v3.toLowerCase().includes(q2)
3883
+ );
3884
+ }, [variables, search]);
3885
+ const groups = T2(() => {
3886
+ const map = {};
3887
+ for (const entry of filteredEntries) {
3888
+ const p3 = getPrefix(entry[0]);
3889
+ (map[p3] ?? (map[p3] = [])).push(entry);
3890
+ }
3891
+ return Object.entries(map).sort(
3892
+ ([a3], [b]) => a3 === "OTHER" ? 1 : b === "OTHER" ? -1 : a3.localeCompare(b)
3893
+ );
3894
+ }, [filteredEntries]);
3895
+ const allRevealed = filteredEntries.every(([k3]) => unmaskedKeys.has(k3));
3896
+ const toggleAllMasks = () => {
3897
+ const keys = filteredEntries.map(([k3]) => k3);
3898
+ setUnmaskedKeys((prev) => {
3899
+ const next = new Set(prev);
3900
+ if (allRevealed) {
3901
+ keys.forEach((k3) => next.delete(k3));
3902
+ } else {
3903
+ keys.forEach((k3) => next.add(k3));
3904
+ }
3905
+ saveUnmasked(next);
3906
+ return next;
3907
+ });
3908
+ };
3909
+ const inputStyle = "width:100%;font-family:monospace;font-size:12px;padding:2px 6px;border:1px solid var(--profiler-border);border-radius:4px;background:var(--profiler-bg);color:var(--profiler-text);";
3910
+ const iconBtn = "background:none;border:none;cursor:pointer;font-size:12px;padding:0 3px;color:var(--profiler-text-muted);opacity:0.7;flex-shrink:0;";
3911
+ const headerBtn = "background:none;border:1px solid var(--profiler-border);border-radius:4px;cursor:pointer;color:var(--profiler-text-muted);font-size:11px;padding:3px 8px;";
3912
+ return /* @__PURE__ */ u3(k, { children: [
3913
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-mb-4", style: "align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;", children: [
3914
+ /* @__PURE__ */ u3("h2", { class: "profiler-section__header", style: "margin:0;", children: "Environment Variables" }),
3915
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
3916
+ /* @__PURE__ */ u3("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
3917
+ /* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" }),
3918
+ /* @__PURE__ */ u3("button", { onClick: exportEnv, style: headerBtn, children: "\u2B07 Export" }),
3919
+ /* @__PURE__ */ u3("button", { onClick: toggleAllMasks, style: headerBtn, children: allRevealed ? "\u{1F648} Mask all" : "\u{1F441} Reveal all" }),
3920
+ /* @__PURE__ */ u3(
3921
+ "button",
3922
+ {
3923
+ onClick: () => setReadOnly((v3) => !v3),
3924
+ title: readOnly ? "Unlock editing" : "Lock editing",
3925
+ style: `${headerBtn}${readOnly ? "border-color:var(--profiler-error,#ef4444);color:var(--profiler-error,#ef4444);" : ""}`,
3926
+ children: readOnly ? "\u{1F512} Locked" : "\u{1F513} Edit"
3927
+ }
3928
+ )
3929
+ ] })
3930
+ ] }),
3931
+ /* @__PURE__ */ u3("div", { style: "background:var(--profiler-warning-bg,rgba(245,158,11,0.1));border:1px solid var(--profiler-warning,#f59e0b);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-warning,#f59e0b);", children: "\u26A0 Changes affect the current process only \u2014 for development use." }),
3932
+ flash && /* @__PURE__ */ u3("div", { style: `background:${flash.type === "success" ? "var(--profiler-success-bg,rgba(34,197,94,0.1))" : "var(--profiler-error-bg,rgba(239,68,68,0.08))"};border:1px solid ${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};border-radius:6px;padding:6px 12px;margin-bottom:12px;font-size:12px;color:${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};`, children: [
3933
+ flash.type === "success" ? "\u2713" : "\u2717",
3934
+ " ",
3935
+ flash.message
3936
+ ] }),
3937
+ showImport && /* @__PURE__ */ u3("div", { style: "border:1px solid var(--profiler-border);border-radius:6px;padding:12px;margin-bottom:16px;", children: [
3938
+ /* @__PURE__ */ u3("p", { class: "profiler-text--xs profiler-text--muted", style: "margin:0 0 8px;", children: [
3939
+ "Paste the contents of a ",
3940
+ /* @__PURE__ */ u3("code", { children: ".env" }),
3941
+ " file. Comments and blank lines are ignored."
3942
+ ] }),
3943
+ /* @__PURE__ */ u3(
3944
+ "textarea",
3945
+ {
3946
+ value: importContent,
3947
+ onInput: (e3) => setImportContent(e3.target.value),
3948
+ placeholder: "APP_NAME=MyApp\nFEATURE_BETA=true\n# comment",
3949
+ rows: 6,
3950
+ style: "width:100%;font-family:monospace;font-size:12px;padding:6px 8px;border:1px solid var(--profiler-border);border-radius:4px;background:var(--profiler-bg);color:var(--profiler-text);resize:vertical;box-sizing:border-box;"
3951
+ }
3952
+ ),
3953
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2", style: "align-items:center;", children: [
3954
+ importCount > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
3955
+ importCount,
3956
+ " variable",
3957
+ importCount !== 1 ? "s" : "",
3958
+ " to import"
3959
+ ] }),
3960
+ /* @__PURE__ */ u3(
3961
+ "button",
3962
+ {
3963
+ onClick: importEnv,
3964
+ disabled: saving || importCount === 0,
3965
+ style: "background:var(--profiler-accent);border:none;cursor:pointer;color:#fff;font-size:11px;padding:4px 12px;border-radius:4px;margin-left:auto;",
3966
+ children: "Apply"
3967
+ }
3968
+ )
3969
+ ] })
3970
+ ] }),
3971
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mb-4", children: [
3972
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card", children: [
3973
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card__value", children: total }),
3974
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card__label", children: "Total vars" })
3975
+ ] }),
3976
+ search && /* @__PURE__ */ u3("div", { class: "profiler-stat-card", children: [
3977
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card__value", children: filteredEntries.length }),
3978
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card__label", children: "Matching" })
3979
+ ] })
3980
+ ] }),
3981
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mb-3", children: /* @__PURE__ */ u3(
3982
+ "input",
3983
+ {
3984
+ type: "text",
3985
+ class: "profiler-filter-input",
3986
+ placeholder: "Filter by key or value\u2026",
3987
+ value: search,
3988
+ onInput: (e3) => setSearch(e3.target.value),
3989
+ style: "flex:1;"
3990
+ }
3991
+ ) }),
3992
+ /* @__PURE__ */ u3("table", { class: "profiler-table", children: [
3993
+ /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
3994
+ /* @__PURE__ */ u3("th", { style: "width:32%", children: "Key" }),
3995
+ /* @__PURE__ */ u3("th", { children: "Value" }),
3996
+ !readOnly && /* @__PURE__ */ u3("th", { style: "width:120px;text-align:right;" })
3997
+ ] }) }),
3998
+ /* @__PURE__ */ u3("tbody", { children: [
3999
+ groups.map(([prefix, entries]) => {
4000
+ const collapsed = collapsedGroups.has(prefix);
4001
+ return /* @__PURE__ */ u3(k, { children: [
4002
+ /* @__PURE__ */ u3(
4003
+ "tr",
4004
+ {
4005
+ onClick: () => toggleGroup(prefix),
4006
+ style: "cursor:pointer;background:var(--profiler-bg-subtle,rgba(0,0,0,0.03));",
4007
+ children: /* @__PURE__ */ u3("td", { colspan: readOnly ? 2 : 3, style: "padding:4px 8px;", children: [
4008
+ /* @__PURE__ */ u3("span", { style: "font-size:11px;font-weight:600;color:var(--profiler-text-muted);letter-spacing:0.05em;font-family:monospace;", children: [
4009
+ collapsed ? "\u25B6" : "\u25BC",
4010
+ " ",
4011
+ prefix,
4012
+ "_"
4013
+ ] }),
4014
+ /* @__PURE__ */ u3("span", { style: "font-size:10px;color:var(--profiler-text-muted);margin-left:6px;", children: entries.length })
4015
+ ] })
4016
+ },
4017
+ `group-${prefix}`
4018
+ ),
4019
+ !collapsed && entries.map(([key, value]) => {
4020
+ const unmasked = unmaskedKeys.has(key);
4021
+ const isEditing = editingKey === key;
4022
+ const isCopiedVal = copiedId === key;
4023
+ const isCopiedKey = copiedId === `__key__${key}`;
4024
+ const wasModified = initial[key] !== void 0 && initial[key] !== value;
4025
+ const wasAdded = initial[key] === void 0;
4026
+ const diffStyle = wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
4027
+ return /* @__PURE__ */ u3("tr", { style: diffStyle, children: [
4028
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
4029
+ /* @__PURE__ */ u3("code", { class: "profiler-text--xs profiler-text--mono", children: key }),
4030
+ /* @__PURE__ */ u3(
4031
+ "button",
4032
+ {
4033
+ onClick: () => copy(key, `__key__${key}`),
4034
+ title: "Copy key",
4035
+ style: `${iconBtn}${isCopiedKey ? "color:var(--profiler-success,#22c55e);opacity:1;" : ""}`,
4036
+ children: isCopiedKey ? "\u2713" : "\u2398"
4037
+ }
4038
+ )
4039
+ ] }) }),
4040
+ /* @__PURE__ */ u3("td", { children: isEditing ? /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:2px;", children: [
4041
+ isQuoted(editValue) && /* @__PURE__ */ u3("span", { style: "font-family:monospace;font-size:12px;color:var(--profiler-text-muted);opacity:0.5;flex-shrink:0;", children: '"' }),
4042
+ /* @__PURE__ */ u3(
4043
+ "input",
4044
+ {
4045
+ ref: editInputRef,
4046
+ type: "text",
4047
+ value: editValue,
4048
+ onInput: (e3) => setEditValue(e3.target.value),
4049
+ onKeyDown: handleEditKeyDown,
4050
+ disabled: saving,
4051
+ style: "flex:1;font-family:monospace;font-size:12px;padding:2px 6px;border:1px solid var(--profiler-accent);border-radius:4px;background:var(--profiler-bg);color:var(--profiler-text);"
4052
+ }
4053
+ ),
4054
+ isQuoted(editValue) && /* @__PURE__ */ u3("span", { style: "font-family:monospace;font-size:12px;color:var(--profiler-text-muted);opacity:0.5;flex-shrink:0;", children: '"' })
4055
+ ] }) : isBool(value) ? /* @__PURE__ */ u3(k, { children: [
4056
+ /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:8px;", children: [
4057
+ /* @__PURE__ */ u3(TypeBadgeComp, { value }),
4058
+ /* @__PURE__ */ u3(
4059
+ "button",
4060
+ {
4061
+ onClick: () => !readOnly && !saving && toggleBool(key, value),
4062
+ disabled: saving || readOnly,
4063
+ title: `Click to set ${/^(true|yes)$/i.test(value) ? "false" : "true"}`,
4064
+ style: `position:relative;display:inline-block;width:36px;height:20px;border-radius:10px;border:none;cursor:${readOnly || saving ? "default" : "pointer"};padding:0;transition:background 0.2s;background:${/^(true|yes)$/i.test(value) ? "var(--profiler-accent)" : "var(--profiler-border,#d1d5db)"};flex-shrink:0;`,
4065
+ children: /* @__PURE__ */ u3("span", { style: `position:absolute;top:3px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left 0.2s;left:${/^(true|yes)$/i.test(value) ? "19px" : "3px"};` })
4066
+ }
4067
+ ),
4068
+ /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", style: "color:var(--profiler-text-muted);", children: value }),
4069
+ /* @__PURE__ */ u3(
4070
+ "button",
4071
+ {
4072
+ onClick: () => copy(value, key),
4073
+ title: "Copy value",
4074
+ style: `${iconBtn}${isCopiedVal ? "color:var(--profiler-success,#22c55e);opacity:1;" : ""}`,
4075
+ children: isCopiedVal ? "\u2713" : "\u2398"
4076
+ }
4077
+ )
4078
+ ] }),
4079
+ wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
4080
+ "was: ",
4081
+ initial[key]
4082
+ ] })
4083
+ ] }) : /* @__PURE__ */ u3(k, { children: [
4084
+ /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
4085
+ /* @__PURE__ */ u3(TypeBadgeComp, { value }),
4086
+ /* @__PURE__ */ u3(
4087
+ "span",
4088
+ {
4089
+ class: "profiler-text--xs profiler-text--mono",
4090
+ style: "word-break:break-all;flex:1;",
4091
+ children: unmasked ? isQuoted(value) ? /* @__PURE__ */ u3(k, { children: [
4092
+ /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted);opacity:0.5;", children: '"' }),
4093
+ value,
4094
+ /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted);opacity:0.5;", children: '"' })
4095
+ ] }) : value : "\u2022".repeat(Math.min(value.length, 20))
4096
+ }
4097
+ ),
4098
+ /* @__PURE__ */ u3(
4099
+ "button",
4100
+ {
4101
+ onClick: () => toggleUnmask(key),
4102
+ title: unmasked ? "Hide value" : "Show value",
4103
+ style: `${iconBtn}${unmasked ? "opacity:1;color:var(--profiler-accent);" : ""}`,
4104
+ children: unmasked ? "\u{1F441}" : "\u{1F648}"
4105
+ }
4106
+ ),
4107
+ /* @__PURE__ */ u3(
4108
+ "button",
4109
+ {
4110
+ onClick: () => copy(value, key),
4111
+ title: "Copy value",
4112
+ style: `${iconBtn}${isCopiedVal ? "color:var(--profiler-success,#22c55e);opacity:1;" : ""}`,
4113
+ children: isCopiedVal ? "\u2713" : "\u2398"
4114
+ }
4115
+ )
4116
+ ] }),
4117
+ wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
4118
+ "was: ",
4119
+ unmasked ? isQuoted(initial[key]) ? `"${initial[key]}"` : initial[key] : "\u2022".repeat(Math.min(initial[key].length, 20))
4120
+ ] })
4121
+ ] }) }),
4122
+ !readOnly && /* @__PURE__ */ u3("td", { style: "text-align:right;white-space:nowrap;", children: isEditing ? /* @__PURE__ */ u3(k, { children: [
4123
+ /* @__PURE__ */ u3("button", { onClick: saveEdit, disabled: saving, title: "Save", style: "background:none;border:none;cursor:pointer;color:var(--profiler-success,#22c55e);font-size:14px;padding:0 4px;", children: "\u2713" }),
4124
+ /* @__PURE__ */ u3("button", { onClick: cancelEdit, disabled: saving, title: "Cancel", style: "background:none;border:none;cursor:pointer;color:var(--profiler-text-muted);font-size:14px;padding:0 4px;", children: "\u2717" })
4125
+ ] }) : isBool(value) ? /* @__PURE__ */ u3(k, { children: [
4126
+ /* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:11px;padding:0 4px;", children: "Edit" }),
4127
+ /* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:11px;padding:0 4px;", children: "Delete" })
4128
+ ] }) : /* @__PURE__ */ u3(k, { children: [
4129
+ /* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:11px;padding:0 4px;", children: "Edit" }),
4130
+ /* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:11px;padding:0 4px;", children: "Delete" })
4131
+ ] }) })
4132
+ ] }, key);
4133
+ })
4134
+ ] });
4135
+ }),
4136
+ !readOnly && /* @__PURE__ */ u3("tr", { children: [
4137
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3(
4138
+ "input",
4139
+ {
4140
+ type: "text",
4141
+ value: newKey,
4142
+ onInput: (e3) => setNewKey(e3.target.value),
4143
+ onKeyDown: (e3) => e3.key === "Enter" && addVar(),
4144
+ placeholder: "NEW_KEY",
4145
+ disabled: saving,
4146
+ style: inputStyle
4147
+ }
4148
+ ) }),
4149
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3(
4150
+ "input",
4151
+ {
4152
+ type: "text",
4153
+ value: newValue,
4154
+ onInput: (e3) => setNewValue(e3.target.value),
4155
+ onKeyDown: (e3) => e3.key === "Enter" && addVar(),
4156
+ placeholder: "value",
4157
+ disabled: saving,
4158
+ style: inputStyle
4159
+ }
4160
+ ) }),
4161
+ /* @__PURE__ */ u3("td", { style: "text-align:right;", children: /* @__PURE__ */ u3(
4162
+ "button",
4163
+ {
4164
+ onClick: addVar,
4165
+ disabled: saving || !newKey.trim(),
4166
+ style: "background:var(--profiler-accent);border:none;cursor:pointer;color:#fff;font-size:11px;padding:3px 8px;border-radius:4px;",
4167
+ children: "Add"
4168
+ }
4169
+ ) })
4170
+ ] })
4171
+ ] })
4172
+ ] })
4173
+ ] });
4174
+ }
4175
+
3621
4176
  // app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
3622
4177
  function ProfileDashboard({ profile, initialTab, embedded }) {
3623
4178
  const cd = profile.collectors_data || {};
@@ -3688,7 +4243,8 @@
3688
4243
  "Jobs (",
3689
4244
  profile.child_jobs.length,
3690
4245
  ")"
3691
- ] })
4246
+ ] }),
4247
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
3692
4248
  ] }),
3693
4249
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
3694
4250
  activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
@@ -3703,7 +4259,8 @@
3703
4259
  activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
3704
4260
  activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
3705
4261
  activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
3706
- activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
4262
+ activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4263
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
3707
4264
  ] })
3708
4265
  ] }),
3709
4266
  !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ module Api
5
+ class EnvVarsController < ApplicationController
6
+ skip_before_action :verify_authenticity_token
7
+
8
+ def show
9
+ variables = ENV.to_h.sort.to_h
10
+ render json: { variables: variables, total: variables.size }
11
+ end
12
+
13
+ def update
14
+ key = params[:key].to_s.strip
15
+
16
+ if key.blank?
17
+ render json: { error: "Key cannot be blank" }, status: :unprocessable_entity
18
+ return
19
+ end
20
+
21
+ value = params[:value]
22
+
23
+ if value.nil? || value.to_s.empty?
24
+ ENV.delete(key)
25
+ render json: { key: key, value: nil, deleted: true }
26
+ else
27
+ ENV[key] = value.to_s
28
+ render json: { key: key, value: ENV[key] }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/config/routes.rb CHANGED
@@ -34,5 +34,6 @@ Profiler::Engine.routes.draw do
34
34
  post "ajax/link", to: "ajax#link"
35
35
  post "explain", to: "explain#create"
36
36
  resource :function_profiling, only: [:show, :update], controller: "function_profiling"
37
+ resource :env_vars, only: [:show, :update], controller: "env_vars"
37
38
  end
38
39
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_collector"
4
+
5
+ module Profiler
6
+ module Collectors
7
+ class EnvCollector < BaseCollector
8
+ def icon
9
+ "⚙️"
10
+ end
11
+
12
+ def priority
13
+ 90
14
+ end
15
+
16
+ def tab_config
17
+ {
18
+ key: "env",
19
+ label: "Env",
20
+ icon: icon,
21
+ priority: priority,
22
+ enabled: true,
23
+ default_active: false
24
+ }
25
+ end
26
+
27
+ def collect
28
+ variables = ENV.to_h.sort.to_h
29
+ store_data({ variables: variables, total: variables.size })
30
+ end
31
+
32
+ def toolbar_summary
33
+ data = panel_content
34
+ { text: "#{data[:total] || 0} vars", color: "gray" }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.0"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -94,6 +94,7 @@ require_relative "profiler/collectors/log_collector"
94
94
  require_relative "profiler/collectors/exception_collector"
95
95
  require_relative "profiler/collectors/routes_collector"
96
96
  require_relative "profiler/collectors/i18n_collector"
97
+ require_relative "profiler/collectors/env_collector"
97
98
 
98
99
  require_relative "profiler/railtie" if defined?(Rails::Railtie)
99
100
  require_relative "profiler/engine" if defined?(Rails::Engine)
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.14.0
4
+ version: 0.15.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-09 00:00:00.000000000 Z
11
+ date: 2026-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -107,6 +107,7 @@ files:
107
107
  - app/assets/builds/profiler.css
108
108
  - app/assets/builds/profiler.js
109
109
  - app/controllers/profiler/api/ajax_controller.rb
110
+ - app/controllers/profiler/api/env_vars_controller.rb
110
111
  - app/controllers/profiler/api/explain_controller.rb
111
112
  - app/controllers/profiler/api/function_profiling_controller.rb
112
113
  - app/controllers/profiler/api/jobs_controller.rb
@@ -128,6 +129,7 @@ files:
128
129
  - lib/profiler/collectors/cache_collector.rb
129
130
  - lib/profiler/collectors/database_collector.rb
130
131
  - lib/profiler/collectors/dump_collector.rb
132
+ - lib/profiler/collectors/env_collector.rb
131
133
  - lib/profiler/collectors/exception_collector.rb
132
134
  - lib/profiler/collectors/flamegraph_collector.rb
133
135
  - lib/profiler/collectors/function_profiler_collector.rb