railswatch 1.0.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 (138) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +485 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/config/railswatch_manifest.js +0 -0
  6. data/app/assets/images/activity.svg +13 -0
  7. data/app/assets/images/bot.svg +1 -0
  8. data/app/assets/images/close.svg +13 -0
  9. data/app/assets/images/details.svg +3 -0
  10. data/app/assets/images/download.svg +3 -0
  11. data/app/assets/images/export.svg +13 -0
  12. data/app/assets/images/external.svg +1 -0
  13. data/app/assets/images/git.svg +1 -0
  14. data/app/assets/images/github.svg +1 -0
  15. data/app/assets/images/home.svg +16 -0
  16. data/app/assets/images/import.svg +13 -0
  17. data/app/assets/images/menu.svg +16 -0
  18. data/app/assets/images/moon.svg +3 -0
  19. data/app/assets/images/stat.svg +1 -0
  20. data/app/assets/images/sun.svg +4 -0
  21. data/app/assets/images/user.svg +1 -0
  22. data/app/controllers/railswatch/base_controller.rb +35 -0
  23. data/app/controllers/railswatch/concerns/csv_exportable.rb +31 -0
  24. data/app/controllers/railswatch/railswatch_controller.rb +183 -0
  25. data/app/engine_assets/javascripts/apex_ext.js +30 -0
  26. data/app/engine_assets/javascripts/application.js +9 -0
  27. data/app/engine_assets/javascripts/autoupdate.js +79 -0
  28. data/app/engine_assets/javascripts/charts.js +279 -0
  29. data/app/engine_assets/javascripts/navbar.js +11 -0
  30. data/app/engine_assets/javascripts/panel.js +43 -0
  31. data/app/engine_assets/javascripts/table.js +12 -0
  32. data/app/engine_assets/javascripts/theme.js +43 -0
  33. data/app/engine_assets/stylesheets/panel.css +111 -0
  34. data/app/engine_assets/stylesheets/responsive.css +102 -0
  35. data/app/engine_assets/stylesheets/style.css +960 -0
  36. data/app/helpers/railswatch/railswatch_helper.rb +338 -0
  37. data/app/views/railswatch/_panel.html.erb +15 -0
  38. data/app/views/railswatch/layouts/railswatch.html.erb +81 -0
  39. data/app/views/railswatch/railswatch/_card.html.erb +7 -0
  40. data/app/views/railswatch/railswatch/_chart.html.erb +13 -0
  41. data/app/views/railswatch/railswatch/_crashes_table_content.html.erb +62 -0
  42. data/app/views/railswatch/railswatch/_custom_events_table_content.html.erb +27 -0
  43. data/app/views/railswatch/railswatch/_delayed_job_table_content.html.erb +52 -0
  44. data/app/views/railswatch/railswatch/_export.html.erb +4 -0
  45. data/app/views/railswatch/railswatch/_grape_requests_table_content.html.erb +31 -0
  46. data/app/views/railswatch/railswatch/_overview.html.erb +124 -0
  47. data/app/views/railswatch/railswatch/_rake_tasks_table_content.html.erb +25 -0
  48. data/app/views/railswatch/railswatch/_recent_requests_table_content.html.erb +28 -0
  49. data/app/views/railswatch/railswatch/_recent_row.html.erb +41 -0
  50. data/app/views/railswatch/railswatch/_requests_table_content.html.erb +51 -0
  51. data/app/views/railswatch/railswatch/_sidekiq_jobs_table_content.html.erb +50 -0
  52. data/app/views/railswatch/railswatch/_summary.html.erb +50 -0
  53. data/app/views/railswatch/railswatch/_table.html.erb +30 -0
  54. data/app/views/railswatch/railswatch/_trace.html.erb +78 -0
  55. data/app/views/railswatch/railswatch/crashes.html.erb +2 -0
  56. data/app/views/railswatch/railswatch/custom.html.erb +6 -0
  57. data/app/views/railswatch/railswatch/delayed_job.html.erb +6 -0
  58. data/app/views/railswatch/railswatch/grape.html.erb +6 -0
  59. data/app/views/railswatch/railswatch/index.html.erb +9 -0
  60. data/app/views/railswatch/railswatch/rake.html.erb +6 -0
  61. data/app/views/railswatch/railswatch/recent.html.erb +2 -0
  62. data/app/views/railswatch/railswatch/requests.html.erb +2 -0
  63. data/app/views/railswatch/railswatch/resources.html.erb +28 -0
  64. data/app/views/railswatch/railswatch/sidekiq.html.erb +6 -0
  65. data/app/views/railswatch/railswatch/slow.html.erb +2 -0
  66. data/app/views/railswatch/railswatch/summary.js.erb +3 -0
  67. data/app/views/railswatch/railswatch/trace.js.erb +9 -0
  68. data/app/views/railswatch/shared/_header.html.erb +39 -0
  69. data/app/views/railswatch/shared/_page_header.html.erb +23 -0
  70. data/config/routes.rb +27 -0
  71. data/lib/generators/railswatch/install/USAGE +19 -0
  72. data/lib/generators/railswatch/install/install_generator.rb +46 -0
  73. data/lib/generators/railswatch/install/templates/create_railswatch_tables.rb +140 -0
  74. data/lib/generators/railswatch/install/templates/initializer.rb +87 -0
  75. data/lib/railswatch/data_source.rb +106 -0
  76. data/lib/railswatch/engine.rb +103 -0
  77. data/lib/railswatch/events/record.rb +63 -0
  78. data/lib/railswatch/extensions/trace.rb +14 -0
  79. data/lib/railswatch/extensions/trace_db.rb +21 -0
  80. data/lib/railswatch/gems/custom_ext.rb +38 -0
  81. data/lib/railswatch/gems/delayed_job_ext.rb +70 -0
  82. data/lib/railswatch/gems/grape_ext.rb +64 -0
  83. data/lib/railswatch/gems/rake_ext.rb +69 -0
  84. data/lib/railswatch/gems/sidekiq_ext.rb +55 -0
  85. data/lib/railswatch/instrument/metrics_collector.rb +70 -0
  86. data/lib/railswatch/interface.rb +9 -0
  87. data/lib/railswatch/models/application_record.rb +31 -0
  88. data/lib/railswatch/models/base_record.rb +59 -0
  89. data/lib/railswatch/models/collection.rb +37 -0
  90. data/lib/railswatch/models/custom_record.rb +32 -0
  91. data/lib/railswatch/models/delayed_job_record.rb +39 -0
  92. data/lib/railswatch/models/event_record.rb +11 -0
  93. data/lib/railswatch/models/grape_record.rb +61 -0
  94. data/lib/railswatch/models/rake_record.rb +33 -0
  95. data/lib/railswatch/models/request_record.rb +105 -0
  96. data/lib/railswatch/models/resource_record.rb +33 -0
  97. data/lib/railswatch/models/sidekiq_record.rb +41 -0
  98. data/lib/railswatch/models/trace_record.rb +21 -0
  99. data/lib/railswatch/pruner.rb +47 -0
  100. data/lib/railswatch/rails/middleware.rb +117 -0
  101. data/lib/railswatch/rails/query_builder.rb +20 -0
  102. data/lib/railswatch/reports/annotations_report.rb +13 -0
  103. data/lib/railswatch/reports/base_report.rb +48 -0
  104. data/lib/railswatch/reports/breakdown_report.rb +11 -0
  105. data/lib/railswatch/reports/crash_report.rb +11 -0
  106. data/lib/railswatch/reports/overview_report.rb +88 -0
  107. data/lib/railswatch/reports/percentile_report.rb +16 -0
  108. data/lib/railswatch/reports/recent_requests_report.rb +21 -0
  109. data/lib/railswatch/reports/requests_report.rb +75 -0
  110. data/lib/railswatch/reports/resources_report.rb +42 -0
  111. data/lib/railswatch/reports/response_time_report.rb +27 -0
  112. data/lib/railswatch/reports/slow_requests_report.rb +21 -0
  113. data/lib/railswatch/reports/throughput_report.rb +16 -0
  114. data/lib/railswatch/reports/trace_report.rb +17 -0
  115. data/lib/railswatch/system_monitor/resources_monitor.rb +88 -0
  116. data/lib/railswatch/thread/current_request.rb +37 -0
  117. data/lib/railswatch/utils.rb +58 -0
  118. data/lib/railswatch/version.rb +7 -0
  119. data/lib/railswatch/widgets/base.rb +17 -0
  120. data/lib/railswatch/widgets/card.rb +19 -0
  121. data/lib/railswatch/widgets/chart.rb +33 -0
  122. data/lib/railswatch/widgets/crashes_table.rb +27 -0
  123. data/lib/railswatch/widgets/custom_events_table.rb +48 -0
  124. data/lib/railswatch/widgets/delayed_job_table.rb +31 -0
  125. data/lib/railswatch/widgets/grape_requests_table.rb +31 -0
  126. data/lib/railswatch/widgets/percentile_card.rb +23 -0
  127. data/lib/railswatch/widgets/rake_tasks_table.rb +31 -0
  128. data/lib/railswatch/widgets/recent_requests_table.rb +35 -0
  129. data/lib/railswatch/widgets/requests_table.rb +27 -0
  130. data/lib/railswatch/widgets/resource_chart.rb +116 -0
  131. data/lib/railswatch/widgets/response_time_chart.rb +29 -0
  132. data/lib/railswatch/widgets/sidekiq_jobs_table.rb +31 -0
  133. data/lib/railswatch/widgets/slow_requests_table.rb +33 -0
  134. data/lib/railswatch/widgets/table.rb +43 -0
  135. data/lib/railswatch/widgets/throughput_chart.rb +29 -0
  136. data/lib/railswatch.rb +184 -0
  137. data/lib/tasks/railswatch.rake +9 -0
  138. metadata +445 -0
@@ -0,0 +1,279 @@
1
+ import ApexCharts from "railswatch/apex_ext";
2
+ import ms from "ms";
3
+
4
+ class RailswatchChart extends HTMLElement {
5
+ connectedCallback() {
6
+ this.legend = this.getAttribute('legend');
7
+ this.units = this.getAttribute('units');
8
+ this.type = this.getAttribute('type');
9
+ this.dataText = this.textContent.trim();
10
+ this.handleThemeChange = () => this.rebuildChart();
11
+ this.chart = this.buildChart();
12
+ this.observeDataChanges();
13
+ window.addEventListener('railswatch:theme-change', this.handleThemeChange);
14
+ }
15
+
16
+ disconnectedCallback() {
17
+ this.mutationObserver?.disconnect();
18
+ window.removeEventListener('railswatch:theme-change', this.handleThemeChange);
19
+ this.chart?.destroy();
20
+ }
21
+
22
+ buildChart() {
23
+ this.chart?.destroy();
24
+
25
+ if (!this.shadowRoot) {
26
+ this.attachShadow({ mode: 'open' });
27
+ }
28
+
29
+ const chartDiv = document.createElement('div');
30
+ this.shadowRoot.innerHTML = '';
31
+ this.shadowRoot.appendChild(chartDiv);
32
+
33
+ const data = this.safeParseJson(this.dataText);
34
+ const renderMethod = this[`render${this.type}Chart`];
35
+ return renderMethod.call(this, chartDiv, data);
36
+ }
37
+
38
+ rebuildChart() {
39
+ this.chart = this.buildChart();
40
+ }
41
+
42
+ safeParseJson(text, defaultValue = []) {
43
+ try {
44
+ return JSON.parse(text);
45
+ } catch (_error) {
46
+ return defaultValue;
47
+ }
48
+ }
49
+
50
+ observeDataChanges() {
51
+ this.mutationObserver = new MutationObserver(() => {
52
+ const currentText = this.textContent.trim();
53
+ if (currentText === this.dataText) return;
54
+ this.dataText = currentText;
55
+ const data = this.safeParseJson(currentText);
56
+ this.updateChart(data);
57
+ });
58
+ this.mutationObserver.observe(this, { childList: true, characterData: true, subtree: true });
59
+ }
60
+
61
+ updateChart(newData) {
62
+ let incoming = newData;
63
+ if (this.type === 'Usage') {
64
+ const { bytes } = calculateByteUnit(newData);
65
+ incoming = newData.map(([timestamp, value]) => [timestamp, typeof value === 'number' ? (value / bytes).toFixed(2) : null]);
66
+ }
67
+ this.chart.updateRollingWindow(incoming, { windowSizeMs: this.windowSizeMs });
68
+ }
69
+
70
+ get windowSizeMs() {
71
+ if (this.type === 'TIR' || this.type === 'RT') {
72
+ return window.railswatchDuration || ms("4h");
73
+ }
74
+
75
+ return ms("24h");
76
+ }
77
+
78
+ renderTIRChart(element, data) {
79
+ return renderChart(element, data, {
80
+ chartType: 'area',
81
+ yAxisTitle: 'RPM',
82
+ seriesName: this.legend,
83
+ units: this.units,
84
+ colorKey: 'accent2'
85
+ });
86
+ }
87
+
88
+ renderRTChart(element, data) {
89
+ return renderChart(element, data, {
90
+ chartType: 'area',
91
+ yAxisTitle: 'Time',
92
+ seriesName: 'Response Time',
93
+ units: 'ms',
94
+ colorKey: 'accent'
95
+ });
96
+ }
97
+
98
+ renderPercentageChart(element, data) {
99
+ return renderChart(element, data, {
100
+ chartType: 'line',
101
+ yAxisTitle: '%',
102
+ seriesName: this.legend,
103
+ units: '%',
104
+ colorKey: 'warning'
105
+ });
106
+ }
107
+
108
+ renderUsageChart(element, data) {
109
+ const { units, bytes } = calculateByteUnit(data);
110
+ return renderChart(element, data, {
111
+ chartType: 'line',
112
+ yAxisTitle: this.legend,
113
+ seriesName: this.legend,
114
+ units: units,
115
+ colorKey: 'accent',
116
+ dataTransform: (points) => {
117
+ return points.map(([timestamp, value]) => [timestamp, typeof value === 'number' ? (value / bytes).toFixed(2) : null]);
118
+ }
119
+ });
120
+ }
121
+ }
122
+
123
+ customElements.define('railswatch-chart', RailswatchChart);
124
+
125
+ function calculateByteUnit(data) {
126
+ let max = data.reduce((accumulator, [_timestamp, value]) => (value > accumulator ? value : accumulator), -Infinity);
127
+ let power = 0;
128
+ while (max >= 1024 && power < 5) {
129
+ max /= 1024;
130
+ power += 1;
131
+ }
132
+
133
+ const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'][power];
134
+ const bytes = Math.pow(1024, power);
135
+
136
+ return { units, bytes };
137
+ }
138
+
139
+ function themePalette() {
140
+ const styles = getComputedStyle(document.documentElement);
141
+ const theme = document.documentElement.dataset.theme || 'light';
142
+
143
+ return {
144
+ theme,
145
+ fontFamily: styles.getPropertyValue('--rm-font-body').trim() || 'sans-serif',
146
+ text: styles.getPropertyValue('--rm-chart-text').trim() || '#64748b',
147
+ grid: styles.getPropertyValue('--rm-chart-grid').trim() || 'rgba(148, 163, 184, 0.16)',
148
+ surface: styles.getPropertyValue('--rm-chart-surface').trim() || '#ffffff',
149
+ accent: styles.getPropertyValue('--rm-accent').trim() || '#0f766e',
150
+ accent2: styles.getPropertyValue('--rm-accent-2').trim() || '#2563eb',
151
+ warning: styles.getPropertyValue('--rm-warning').trim() || '#d97706'
152
+ };
153
+ }
154
+
155
+ function renderChart(element, data, { chartType = 'area', yAxisTitle, seriesName, units, colorKey = 'accent', dataTransform = (points) => points } = {}) {
156
+ const palette = themePalette();
157
+ const color = palette[colorKey] || palette.accent;
158
+
159
+ const chart = new ApexCharts(element, {
160
+ chart: {
161
+ type: chartType,
162
+ height: 320,
163
+ width: '100%',
164
+ id: `chart-${Math.random().toFixed(10)}`,
165
+ group: 'chart',
166
+ fontFamily: palette.fontFamily,
167
+ toolbar: {
168
+ show: false
169
+ },
170
+ zoom: {
171
+ type: 'x'
172
+ },
173
+ animations: {
174
+ easing: 'easeinout',
175
+ speed: 450
176
+ },
177
+ background: 'transparent'
178
+ },
179
+ colors: [color],
180
+ stroke: {
181
+ width: chartType === 'area' ? 2.8 : 2.4,
182
+ curve: 'smooth'
183
+ },
184
+ fill: chartType === 'area' ? {
185
+ type: 'gradient',
186
+ gradient: {
187
+ opacityFrom: 0.28,
188
+ opacityTo: 0.02,
189
+ shadeIntensity: 0.8
190
+ }
191
+ } : {
192
+ opacity: 1
193
+ },
194
+ grid: {
195
+ borderColor: palette.grid,
196
+ strokeDashArray: 4,
197
+ padding: {
198
+ left: 4,
199
+ right: 8
200
+ }
201
+ },
202
+ markers: {
203
+ size: 0,
204
+ hover: {
205
+ size: 5
206
+ }
207
+ },
208
+ dataLabels: {
209
+ enabled: false
210
+ },
211
+ legend: {
212
+ show: false
213
+ },
214
+ xaxis: {
215
+ crosshairs: {
216
+ show: true,
217
+ stroke: {
218
+ color: palette.grid
219
+ }
220
+ },
221
+ type: 'datetime',
222
+ labels: {
223
+ datetimeUTC: false,
224
+ style: {
225
+ colors: [palette.text]
226
+ }
227
+ },
228
+ axisBorder: {
229
+ color: palette.grid
230
+ },
231
+ axisTicks: {
232
+ color: palette.grid
233
+ }
234
+ },
235
+ yaxis: {
236
+ min: 0,
237
+ title: {
238
+ text: yAxisTitle,
239
+ style: {
240
+ color: palette.text,
241
+ fontWeight: 600
242
+ }
243
+ },
244
+ labels: {
245
+ style: {
246
+ colors: [palette.text]
247
+ }
248
+ }
249
+ },
250
+ tooltip: {
251
+ theme: palette.theme,
252
+ style: {
253
+ fontSize: '13px',
254
+ fontFamily: palette.fontFamily
255
+ },
256
+ marker: {
257
+ show: false
258
+ },
259
+ x: {
260
+ show: true,
261
+ format: 'dd MMM yyyy HH:mm'
262
+ },
263
+ y: {
264
+ formatter: (value) => value ? `${value} ${units}`.trim() : undefined,
265
+ title: {
266
+ formatter: () => ''
267
+ }
268
+ }
269
+ },
270
+ series: [{
271
+ name: seriesName,
272
+ data: dataTransform(data)
273
+ }],
274
+ annotations: window?.annotationsData || {}
275
+ });
276
+
277
+ chart.render();
278
+ return chart;
279
+ }
@@ -0,0 +1,11 @@
1
+ document.addEventListener('click', (event) => {
2
+ const toggle = event.target.closest('[data-nav-toggle]');
3
+ if (!toggle) return;
4
+
5
+ const topbar = toggle.closest('.rm-topbar');
6
+ const nav = topbar?.querySelector('#railswatch-nav');
7
+ if (!topbar || !nav) return;
8
+
9
+ const isOpen = topbar.classList.toggle('is-nav-open');
10
+ toggle.setAttribute('aria-expanded', String(isOpen));
11
+ });
@@ -0,0 +1,43 @@
1
+ import $ from 'jquery';
2
+
3
+ function hidePanel() {
4
+ $('.cd-panel').removeClass('cd-panel--is-visible');
5
+ $('body').removeClass('panel-visible');
6
+ $(".panel-overlay").hide();
7
+ }
8
+
9
+ function showPanel() {
10
+ $(".cd-panel__container").scrollTop(0);
11
+ $(".panel-overlay").show();
12
+ $('.cd-panel').addClass('cd-panel--is-visible');
13
+ $('body').addClass('panel-visible');
14
+ $('.cd-panel__content table').stupidtable();
15
+ }
16
+
17
+ window.hidePanel = hidePanel;
18
+ window.showPanel = showPanel;
19
+
20
+ $(function() {
21
+ var panel = {};
22
+ window.panel = panel;
23
+ window.panel.header = $(".panel-heading span");
24
+ window.panel.content = $(".cd-panel__content");
25
+ window.panel.close = '<a class="panel-close" href="#" onclick="javascript: hidePanel(); return false;">&times;</a>'
26
+
27
+ $(document).ajaxStart(function(e) {
28
+ $('.loader-wrapper').addClass('is-active');
29
+ });
30
+
31
+ $(document).ajaxComplete(function(e) {
32
+ $('.loader-wrapper').removeClass('is-active');
33
+ });
34
+
35
+ $(".toggle-panel").on('click', function(e) {
36
+ showPanel();
37
+ });
38
+
39
+ $(document).on("click", ".panel-overlay", function() {
40
+ hidePanel();
41
+ });
42
+
43
+ });
@@ -0,0 +1,12 @@
1
+ import $ from 'jquery';
2
+ window.jQuery = $; // shim for stupid-table-plugin
3
+ await import('stupid-table-plugin');
4
+
5
+ function initializeTables() {
6
+ $('table').stupidtable();
7
+ }
8
+
9
+ window.initializeRailswatchTables = initializeTables;
10
+
11
+ $(initializeTables);
12
+ document.addEventListener('railswatch:content-updated', initializeTables);
@@ -0,0 +1,43 @@
1
+ const STORAGE_KEY = 'railswatch:theme';
2
+
3
+ function preferredTheme() {
4
+ try {
5
+ const savedTheme = localStorage.getItem(STORAGE_KEY);
6
+ if (savedTheme === 'light' || savedTheme === 'dark') return savedTheme;
7
+ } catch (_error) {
8
+ return 'light';
9
+ }
10
+
11
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
12
+ }
13
+
14
+ function syncThemeButtons(theme) {
15
+ document.querySelectorAll('[data-theme-label]').forEach((label) => {
16
+ label.textContent = theme === 'dark' ? 'Dark' : 'Light';
17
+ });
18
+ }
19
+
20
+ function applyTheme(theme) {
21
+ document.documentElement.dataset.theme = theme;
22
+ try {
23
+ localStorage.setItem(STORAGE_KEY, theme);
24
+ } catch (_error) {
25
+ // no-op
26
+ }
27
+ syncThemeButtons(theme);
28
+ window.dispatchEvent(new CustomEvent('railswatch:theme-change', { detail: { theme } }));
29
+ }
30
+
31
+ function initializeThemeToggle() {
32
+ syncThemeButtons(preferredTheme());
33
+
34
+ document.addEventListener('click', (event) => {
35
+ const toggle = event.target.closest('[data-theme-toggle]');
36
+ if (!toggle) return;
37
+
38
+ const currentTheme = document.documentElement.dataset.theme || preferredTheme();
39
+ applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
40
+ });
41
+ }
42
+
43
+ document.addEventListener('DOMContentLoaded', initializeThemeToggle);
@@ -0,0 +1,111 @@
1
+ .panel-overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: 40;
5
+ display: none;
6
+ background: rgba(2, 6, 23, 0.42);
7
+ backdrop-filter: blur(8px);
8
+ }
9
+
10
+ .panel-close {
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ width: 2rem;
15
+ height: 2rem;
16
+ margin-left: auto;
17
+ border-radius: 999px;
18
+ background: rgba(220, 38, 38, 0.1);
19
+ color: var(--rm-danger);
20
+ font-size: 1.35rem;
21
+ line-height: 1;
22
+ text-decoration: none;
23
+ }
24
+
25
+ .cd-panel {
26
+ position: relative;
27
+ visibility: hidden;
28
+ transition: visibility 0s 0.35s;
29
+ }
30
+
31
+ .cd-panel.cd-panel--is-visible {
32
+ visibility: visible;
33
+ transition-delay: 0s;
34
+ }
35
+
36
+ .cd-panel__container {
37
+ position: fixed;
38
+ top: 0;
39
+ right: 0;
40
+ z-index: 50;
41
+ width: min(1120px, 92vw);
42
+ height: 100%;
43
+ padding: 1rem;
44
+ transform: translate3d(100%, 0, 0);
45
+ transition: transform 0.35s ease;
46
+ }
47
+
48
+ .cd-panel--is-visible .cd-panel__container {
49
+ transform: translate3d(0, 0, 0);
50
+ }
51
+
52
+ .cd-panel__container .panel {
53
+ min-height: 100%;
54
+ border: 1px solid var(--rm-border);
55
+ border-radius: 1.6rem;
56
+ background: var(--rm-bg-elevated);
57
+ box-shadow: var(--rm-shadow);
58
+ overflow: hidden;
59
+ backdrop-filter: blur(18px);
60
+ }
61
+
62
+ html[data-theme="dark"] .cd-panel__container .panel {
63
+ background: var(--rm-surface-strong);
64
+ backdrop-filter: none;
65
+ }
66
+
67
+ .panel-heading {
68
+ display: flex;
69
+ align-items: flex-start;
70
+ gap: 0.75rem;
71
+ padding: 1rem 1.1rem !important;
72
+ background: transparent !important;
73
+ border-bottom: 1px solid var(--rm-border) !important;
74
+ }
75
+
76
+ .panel-heading .panel-close {
77
+ margin-left: auto;
78
+ flex-shrink: 0;
79
+ align-self: center;
80
+ }
81
+
82
+ .panel-heading .field {
83
+ width: 100%;
84
+ }
85
+
86
+ .panel-block {
87
+ display: block !important;
88
+ padding: 0 !important;
89
+ border: 0 !important;
90
+ }
91
+
92
+ .cd-panel__content {
93
+ padding: 1rem;
94
+ max-height: calc(100vh - 7rem);
95
+ overflow: auto;
96
+ }
97
+
98
+ .cd-panel__content .rm-table-shell {
99
+ margin-top: 1rem;
100
+ }
101
+
102
+ @media screen and (max-width: 768px) {
103
+ .cd-panel__container {
104
+ width: 100vw;
105
+ padding: 0.5rem;
106
+ }
107
+
108
+ .cd-panel__content {
109
+ padding: 0.75rem;
110
+ }
111
+ }
@@ -0,0 +1,102 @@
1
+ @media screen and (max-width: 1200px) {
2
+ .rm-overview-grid,
3
+ .rm-kpi-grid {
4
+ grid-template-columns: repeat(2, minmax(0, 1fr));
5
+ }
6
+
7
+ .row > .widget:nth-child(1):nth-last-child(3),
8
+ .row > .widget:nth-child(2):nth-last-child(2),
9
+ .row > .widget:nth-child(3):nth-last-child(1) {
10
+ grid-column: span 6;
11
+ }
12
+ }
13
+
14
+ @media screen and (max-width: 960px) {
15
+ .rm-shell {
16
+ padding: 1rem;
17
+ }
18
+
19
+ .rm-topbar {
20
+ grid-template-columns: 1fr;
21
+ }
22
+
23
+ .rm-topbar__controls {
24
+ justify-content: space-between;
25
+ }
26
+
27
+ .rm-mobile-nav-toggle {
28
+ display: inline-flex;
29
+ }
30
+
31
+ .rm-nav {
32
+ display: none;
33
+ padding-top: 0.25rem;
34
+ overflow-x: auto;
35
+ flex-wrap: nowrap;
36
+ }
37
+
38
+ .rm-topbar.is-nav-open .rm-nav {
39
+ display: flex;
40
+ }
41
+
42
+ .rm-page-header,
43
+ .rm-section-heading {
44
+ flex-direction: column;
45
+ }
46
+
47
+ .rm-resource-grid > * {
48
+ grid-column: span 12;
49
+ }
50
+ }
51
+
52
+ @media screen and (max-width: 768px) {
53
+ .rm-container {
54
+ padding-left: 0;
55
+ padding-right: 0;
56
+ }
57
+
58
+ .rm-overview-grid,
59
+ .rm-kpi-grid,
60
+ .row,
61
+ .rm-resource-grid {
62
+ grid-template-columns: minmax(0, 1fr);
63
+ }
64
+
65
+ .row > .widget,
66
+ .row > .widget:nth-child(1):nth-last-child(2),
67
+ .row > .widget:nth-child(2):nth-last-child(1),
68
+ .row > .widget:nth-child(1):nth-last-child(3),
69
+ .row > .widget:nth-child(2):nth-last-child(2),
70
+ .row > .widget:nth-child(3):nth-last-child(1),
71
+ .rm-resource-grid > * {
72
+ grid-column: span 1;
73
+ }
74
+
75
+ .rm-brand__copy small,
76
+ .rm-theme-toggle__label {
77
+ display: none;
78
+ }
79
+
80
+ .rm-widget-card,
81
+ .rm-overview-panel,
82
+ .rm-page-header {
83
+ padding: 1rem;
84
+ }
85
+
86
+ .rm-ranking-item {
87
+ grid-template-columns: auto 1fr;
88
+ }
89
+
90
+ .rm-ranking-item__value {
91
+ grid-column: 2;
92
+ }
93
+
94
+ .rm-footer {
95
+ padding-inline: 1rem;
96
+ }
97
+
98
+ .rm-footer__inner {
99
+ flex-direction: column;
100
+ align-items: flex-start;
101
+ }
102
+ }