pg_reports 0.5.4 → 0.6.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 +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +12 -4
- data/app/views/layouts/pg_reports/application.html.erb +70 -61
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +53 -1
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +31 -11
- data/app/views/pg_reports/dashboard/index.html.erb +14 -8
- data/app/views/pg_reports/dashboard/show.html.erb +6 -2
- data/config/locales/en.yml +109 -0
- data/config/locales/ru.yml +81 -0
- data/config/locales/uk.yml +126 -0
- data/lib/pg_reports/compatibility.rb +63 -0
- data/lib/pg_reports/configuration.rb +2 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +36 -0
- data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
- data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
- data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
- data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
- data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
- data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
- data/lib/pg_reports/engine.rb +6 -0
- data/lib/pg_reports/modules/indexes.rb +3 -0
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/modules/system.rb +27 -0
- data/lib/pg_reports/modules/tables.rb +1 -0
- data/lib/pg_reports/query_monitor.rb +64 -32
- data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
- data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
- data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
- data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
- data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
- data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
- data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
- data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +5 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e7cfad6574b6b9134d0e1b4bb6ec4ad71b1c8ca9c4fc487acc7839f4ac70168
|
|
4
|
+
data.tar.gz: 918940a09ac75506828c580bc7c64d0b7b8673ae11a9656c425dca69e715d50e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8b00907aaaac1daf07c65ea1b37e9a70b9067f507be0d3f3b8e0863a0848600285dc63cc55a039e86fd5cfcc8af05ce785f58ea40a09677a6832dec0b39de7e
|
|
7
|
+
data.tar.gz: 61d1c769f1e8c9d9eaeaf78f16ca393b782f0ee10ee1cd2d20f468c888d73d79a6c66270219d9938883681ae8a67318655e01ad201bd567b4e3daf056cfa10c3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **7 new reports** covering previously undetected PostgreSQL problems:
|
|
15
|
+
- `inefficient_indexes` — indexes with high read-to-fetch ratio indicating misaligned composite index column order
|
|
16
|
+
- `fk_without_indexes` — foreign keys on child tables missing a supporting index, causing seq scans on parent DELETE/UPDATE
|
|
17
|
+
- `index_correlation` — low physical correlation between index order and row order, causing excessive random I/O on range scans
|
|
18
|
+
- `temp_file_queries` — queries spilling intermediate results to disk due to insufficient `work_mem` (requires pg_stat_statements)
|
|
19
|
+
- `tables_without_pk` — tables missing primary keys, which breaks logical replication and causes ORM issues
|
|
20
|
+
- `wraparound_risk` — transaction ID age proximity to the 2-billion wraparound limit that triggers emergency PostgreSQL shutdown
|
|
21
|
+
- `checkpoint_stats` — checkpoint frequency and background writer metrics with PostgreSQL 17+ support
|
|
22
|
+
- **AI Prompt Export** — "Copy Prompt" button in the Export dropdown generates a ready-to-paste prompt for AI coding assistants (Claude Code, Cursor, Codex) with problem description, fix instructions, and actual report data as examples. Available for 28 actionable reports.
|
|
23
|
+
- **Compatibility warnings** — the gem now warns at boot if Ruby, Rails, or PostgreSQL versions are below minimum supported (Ruby 2.7, Rails 5.0, PostgreSQL 12)
|
|
24
|
+
- **`inefficient_index_threshold_ratio`** configuration option (default: 10) for the inefficient indexes report
|
|
25
|
+
- Full i18n support for all new reports in English, Russian, and Ukrainian
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Critical: infinite recursion in Query Monitor with database-backed cache stores** (SolidCache, ActiveRecord Cache Store) — `handle_sql_event` called `Rails.cache.read()` on every SQL event to check `enabled` state, which with DB-backed caches generated new SQL events, creating an infinite loop. Fixed by storing monitoring state in local instance variables (`@enabled`, `@session_id`) and syncing from cache only at initialization. Added reentrancy guard as additional safety net. (Reported via [PR #7](https://github.com/deadalice/pg_reports/pull/7))
|
|
30
|
+
- **`checkpoint_stats` compatibility with PostgreSQL 17+** — checkpoint columns were moved from `pg_stat_bgwriter` to `pg_stat_checkpointer` with renamed columns. The report now auto-detects the PostgreSQL version and uses the appropriate query.
|
|
31
|
+
- **26 previously failing Query Monitor specs** now pass — tests no longer depend on `Rails.cache` availability
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Dashboard UI redesign** — flattened the visual style to remove typical AI-generated patterns:
|
|
36
|
+
- Removed gradient buttons, gradient text on logo, backdrop blur on modals
|
|
37
|
+
- Removed `translateY` hover animations on cards and buttons
|
|
38
|
+
- Unified `border-radius` to 6px across all components (was 8–16px)
|
|
39
|
+
- Subdued box-shadows (`0 4px 16px` instead of `0 10px 40px`)
|
|
40
|
+
- Muted color palette — same CSS variables, lower saturation
|
|
41
|
+
- Background warmed from `#0f1114` to `#151719`
|
|
42
|
+
- Removed colored icon backgrounds from category cards
|
|
43
|
+
- Report links now transparent by default (tertiary fill on hover only)
|
|
44
|
+
- NEW badge restyled: tinted background instead of solid green
|
|
45
|
+
- Buttons use flat solid color instead of gradients
|
|
46
|
+
- "Download" button renamed to "Export" with AI Prompt option added to the dropdown
|
|
47
|
+
- New reports tagged with `NEW` badge in the dashboard sidebar
|
|
48
|
+
- Query Monitor `enabled` and `session_id` public methods now return local state instead of hitting cache on every call
|
|
49
|
+
|
|
10
50
|
## [0.5.4] - 2026-02-11
|
|
11
51
|
|
|
12
52
|
### Fixed
|
data/README.md
CHANGED
|
@@ -24,6 +24,7 @@ A comprehensive PostgreSQL monitoring and analysis library for Rails application
|
|
|
24
24
|
- 📊 **EXPLAIN ANALYZE** - Advanced query plan analyzer with problem detection and recommendations
|
|
25
25
|
- 🔍 **SQL Query Monitoring** - Real-time monitoring of all executed SQL queries with source location tracking
|
|
26
26
|
- 🔌 **Connection Pool Analytics** - Monitor pool usage, wait times, saturation warnings, and connection churn
|
|
27
|
+
- 🤖 **AI Prompt Export** - Copy a ready-to-paste prompt for Claude Code, Cursor, or Codex with problem context and report data
|
|
27
28
|
- 🗑️ **Migration Generator** - Generate Rails migrations to drop unused indexes
|
|
28
29
|
|
|
29
30
|
## Installation
|
|
@@ -201,6 +202,7 @@ config.active_record.query_log_tags = [:controller, :action]
|
|
|
201
202
|
| `expensive_queries` | Queries consuming most total time |
|
|
202
203
|
| `missing_index_queries` | Queries potentially missing indexes |
|
|
203
204
|
| `low_cache_hit_queries` | Queries with poor cache utilization |
|
|
205
|
+
| `temp_file_queries` | 🆕 Queries spilling to disk via temporary files |
|
|
204
206
|
| `all_queries` | All query statistics |
|
|
205
207
|
| `reset_statistics!` | Reset pg_stat_statements data |
|
|
206
208
|
|
|
@@ -212,6 +214,9 @@ config.active_record.query_log_tags = [:controller, :action]
|
|
|
212
214
|
| `duplicate_indexes` | Redundant indexes |
|
|
213
215
|
| `invalid_indexes` | Indexes that failed to build |
|
|
214
216
|
| `missing_indexes` | Tables potentially missing indexes |
|
|
217
|
+
| `inefficient_indexes` | 🆕 Indexes with high read-to-fetch ratio (misaligned column order) |
|
|
218
|
+
| `fk_without_indexes` | 🆕 Foreign keys missing indexes on child table |
|
|
219
|
+
| `index_correlation` | 🆕 Low physical correlation causing random I/O |
|
|
215
220
|
| `index_usage` | Index scan statistics |
|
|
216
221
|
| `bloated_indexes` | Indexes with high bloat |
|
|
217
222
|
| `index_sizes` | Index disk usage |
|
|
@@ -226,6 +231,7 @@ config.active_record.query_log_tags = [:controller, :action]
|
|
|
226
231
|
| `row_counts` | Table row counts |
|
|
227
232
|
| `cache_hit_ratios` | Table cache statistics |
|
|
228
233
|
| `seq_scans` | Tables with high sequential scans |
|
|
234
|
+
| `tables_without_pk` | 🆕 Tables missing primary keys |
|
|
229
235
|
| `recently_modified` | Tables with recent activity |
|
|
230
236
|
|
|
231
237
|
### Connections
|
|
@@ -238,10 +244,10 @@ config.active_record.query_log_tags = [:controller, :action]
|
|
|
238
244
|
| `blocking_queries` | Queries blocking others |
|
|
239
245
|
| `locks` | Current database locks |
|
|
240
246
|
| `idle_connections` | Idle connections |
|
|
241
|
-
| `pool_usage` |
|
|
242
|
-
| `pool_wait_times` |
|
|
243
|
-
| `pool_saturation` |
|
|
244
|
-
| `connection_churn` |
|
|
247
|
+
| `pool_usage` | Connection pool utilization analysis |
|
|
248
|
+
| `pool_wait_times` | Resource wait time analysis |
|
|
249
|
+
| `pool_saturation` | Pool health warnings with recommendations |
|
|
250
|
+
| `connection_churn` | Connection lifecycle and churn rate analysis |
|
|
245
251
|
| `kill_connection(pid)` | Terminate a backend process |
|
|
246
252
|
| `cancel_query(pid)` | Cancel a running query |
|
|
247
253
|
|
|
@@ -253,6 +259,8 @@ config.active_record.query_log_tags = [:controller, :action]
|
|
|
253
259
|
| `settings` | PostgreSQL configuration |
|
|
254
260
|
| `extensions` | Installed extensions |
|
|
255
261
|
| `activity_overview` | Current activity summary |
|
|
262
|
+
| `wraparound_risk` | 🆕 Transaction ID wraparound proximity |
|
|
263
|
+
| `checkpoint_stats` | 🆕 Checkpoint and bgwriter statistics (PG 12–18+) |
|
|
256
264
|
| `cache_stats` | Database cache statistics |
|
|
257
265
|
| `pg_stat_statements_available?` | Check if extension is ready |
|
|
258
266
|
| `enable_pg_stat_statements!` | Create the extension |
|
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
<% end %>
|
|
14
14
|
<style>
|
|
15
15
|
:root {
|
|
16
|
-
--bg-primary: #
|
|
17
|
-
--bg-secondary: #
|
|
18
|
-
--bg-tertiary: #
|
|
19
|
-
--bg-card: #
|
|
20
|
-
--border-color: #
|
|
21
|
-
--text-primary: #
|
|
22
|
-
--text-secondary: #
|
|
23
|
-
--text-muted: #
|
|
24
|
-
--accent-purple: #
|
|
25
|
-
--accent-blue: #
|
|
26
|
-
--accent-green: #
|
|
27
|
-
--accent-amber: #
|
|
28
|
-
--accent-rose: #
|
|
29
|
-
--accent-indigo: #
|
|
30
|
-
--gradient-start: #
|
|
31
|
-
--gradient-end: #
|
|
16
|
+
--bg-primary: #151719;
|
|
17
|
+
--bg-secondary: #1c1f22;
|
|
18
|
+
--bg-tertiary: #232629;
|
|
19
|
+
--bg-card: #1c1f22;
|
|
20
|
+
--border-color: #2e3235;
|
|
21
|
+
--text-primary: #c9cbcd;
|
|
22
|
+
--text-secondary: #888d93;
|
|
23
|
+
--text-muted: #5e6369;
|
|
24
|
+
--accent-purple: #7c8af6;
|
|
25
|
+
--accent-blue: #5c9eed;
|
|
26
|
+
--accent-green: #45a87a;
|
|
27
|
+
--accent-amber: #c89030;
|
|
28
|
+
--accent-rose: #d45d6e;
|
|
29
|
+
--accent-indigo: #7c8af6;
|
|
30
|
+
--gradient-start: #7c8af6;
|
|
31
|
+
--gradient-end: #7c8af6;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
* {
|
|
@@ -68,26 +68,24 @@
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.logo-icon {
|
|
71
|
-
width:
|
|
72
|
-
height:
|
|
73
|
-
background:
|
|
71
|
+
width: 32px;
|
|
72
|
+
height: 32px;
|
|
73
|
+
background: var(--accent-purple);
|
|
74
74
|
border-radius: 6px;
|
|
75
75
|
display: flex;
|
|
76
76
|
align-items: center;
|
|
77
77
|
justify-content: center;
|
|
78
|
-
font-size:
|
|
78
|
+
font-size: 1rem;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
.logo-text h1 {
|
|
82
|
-
font-size: 1.
|
|
83
|
-
font-weight:
|
|
84
|
-
|
|
85
|
-
-webkit-background-clip: text;
|
|
86
|
-
-webkit-text-fill-color: transparent;
|
|
82
|
+
font-size: 1.1rem;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
color: var(--text-primary);
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
.logo-text span {
|
|
90
|
-
font-size: 0.
|
|
88
|
+
font-size: 0.7rem;
|
|
91
89
|
color: var(--text-muted);
|
|
92
90
|
}
|
|
93
91
|
|
|
@@ -130,14 +128,12 @@
|
|
|
130
128
|
.category-card {
|
|
131
129
|
background: var(--bg-card);
|
|
132
130
|
border: 1px solid var(--border-color);
|
|
133
|
-
border-radius:
|
|
131
|
+
border-radius: 6px;
|
|
134
132
|
padding: 1rem;
|
|
135
|
-
transition: all 0.2s ease;
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
.category-card:hover {
|
|
139
|
-
border-color:
|
|
140
|
-
transform: translateY(-1px);
|
|
136
|
+
border-color: #3e4347;
|
|
141
137
|
}
|
|
142
138
|
|
|
143
139
|
.category-header {
|
|
@@ -148,13 +144,15 @@
|
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
.category-icon {
|
|
151
|
-
width:
|
|
152
|
-
height:
|
|
153
|
-
border-radius:
|
|
147
|
+
width: 28px;
|
|
148
|
+
height: 28px;
|
|
149
|
+
border-radius: 4px;
|
|
154
150
|
display: flex;
|
|
155
151
|
align-items: center;
|
|
156
152
|
justify-content: center;
|
|
157
|
-
font-size:
|
|
153
|
+
font-size: 0.95rem;
|
|
154
|
+
background: transparent !important;
|
|
155
|
+
color: var(--text-secondary) !important;
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
.category-title {
|
|
@@ -181,19 +179,18 @@
|
|
|
181
179
|
display: flex;
|
|
182
180
|
align-items: center;
|
|
183
181
|
justify-content: space-between;
|
|
184
|
-
padding: 0.
|
|
185
|
-
background:
|
|
182
|
+
padding: 0.45rem 0.7rem;
|
|
183
|
+
background: transparent;
|
|
186
184
|
border: 1px solid transparent;
|
|
187
|
-
border-radius:
|
|
185
|
+
border-radius: 4px;
|
|
188
186
|
color: var(--text-secondary);
|
|
189
187
|
text-decoration: none;
|
|
190
|
-
font-size: 0.
|
|
191
|
-
transition:
|
|
188
|
+
font-size: 0.82rem;
|
|
189
|
+
transition: background 0.1s, color 0.1s;
|
|
192
190
|
}
|
|
193
191
|
|
|
194
192
|
.report-link:hover {
|
|
195
|
-
background: var(--bg-
|
|
196
|
-
border-color: var(--border-color);
|
|
193
|
+
background: var(--bg-tertiary);
|
|
197
194
|
color: var(--text-primary);
|
|
198
195
|
}
|
|
199
196
|
|
|
@@ -208,6 +205,20 @@
|
|
|
208
205
|
transform: translateX(0);
|
|
209
206
|
}
|
|
210
207
|
|
|
208
|
+
.report-badge-new {
|
|
209
|
+
display: inline-block;
|
|
210
|
+
margin-left: 0.4rem;
|
|
211
|
+
padding: 0.1rem 0.35rem;
|
|
212
|
+
font-size: 0.55rem;
|
|
213
|
+
font-weight: 600;
|
|
214
|
+
letter-spacing: 0.04em;
|
|
215
|
+
line-height: 1.2;
|
|
216
|
+
color: var(--accent-green);
|
|
217
|
+
background: rgba(69, 168, 122, 0.12);
|
|
218
|
+
border-radius: 3px;
|
|
219
|
+
vertical-align: middle;
|
|
220
|
+
}
|
|
221
|
+
|
|
211
222
|
/* Report Detail Page */
|
|
212
223
|
.report-page {
|
|
213
224
|
display: flex;
|
|
@@ -230,7 +241,7 @@
|
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
.breadcrumb a:hover {
|
|
233
|
-
color: var(--
|
|
244
|
+
color: var(--text-primary);
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
.report-header {
|
|
@@ -264,25 +275,24 @@
|
|
|
264
275
|
height: 36px;
|
|
265
276
|
padding: 0 1rem;
|
|
266
277
|
border: 1px solid transparent;
|
|
267
|
-
border-radius:
|
|
278
|
+
border-radius: 4px;
|
|
268
279
|
font-family: inherit;
|
|
269
280
|
font-size: 0.8rem;
|
|
270
281
|
font-weight: 500;
|
|
271
282
|
cursor: pointer;
|
|
272
|
-
transition:
|
|
283
|
+
transition: background 0.1s;
|
|
273
284
|
white-space: nowrap;
|
|
274
285
|
text-decoration: none;
|
|
275
286
|
}
|
|
276
287
|
|
|
277
288
|
.btn-primary {
|
|
278
|
-
background:
|
|
279
|
-
color:
|
|
280
|
-
border-color:
|
|
289
|
+
background: var(--accent-purple);
|
|
290
|
+
color: #fff;
|
|
291
|
+
border-color: var(--accent-purple);
|
|
281
292
|
}
|
|
282
293
|
|
|
283
294
|
.btn-primary:hover {
|
|
284
|
-
|
|
285
|
-
transform: translateY(-1px);
|
|
295
|
+
background: #6b79e4;
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
.btn-secondary {
|
|
@@ -292,19 +302,19 @@
|
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
.btn-secondary:hover {
|
|
295
|
-
background:
|
|
305
|
+
background: #2a2d31;
|
|
296
306
|
color: var(--text-primary);
|
|
297
307
|
}
|
|
298
308
|
|
|
299
309
|
.btn-telegram {
|
|
300
|
-
background:
|
|
301
|
-
color:
|
|
302
|
-
border-color
|
|
310
|
+
background: var(--bg-tertiary);
|
|
311
|
+
color: var(--text-secondary);
|
|
312
|
+
border: 1px solid var(--border-color);
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
.btn-telegram:hover {
|
|
306
|
-
background: #
|
|
307
|
-
|
|
316
|
+
background: #2a2d31;
|
|
317
|
+
color: var(--text-primary);
|
|
308
318
|
}
|
|
309
319
|
|
|
310
320
|
.btn:disabled {
|
|
@@ -316,7 +326,7 @@
|
|
|
316
326
|
.results-container {
|
|
317
327
|
background: var(--bg-card);
|
|
318
328
|
border: 1px solid var(--border-color);
|
|
319
|
-
border-radius:
|
|
329
|
+
border-radius: 6px;
|
|
320
330
|
overflow: hidden;
|
|
321
331
|
}
|
|
322
332
|
|
|
@@ -462,18 +472,17 @@
|
|
|
462
472
|
.modal {
|
|
463
473
|
position: fixed;
|
|
464
474
|
inset: 0;
|
|
465
|
-
background: rgba(0, 0, 0, 0.
|
|
475
|
+
background: rgba(0, 0, 0, 0.6);
|
|
466
476
|
display: flex;
|
|
467
477
|
align-items: center;
|
|
468
478
|
justify-content: center;
|
|
469
479
|
z-index: 1000;
|
|
470
|
-
backdrop-filter: blur(4px);
|
|
471
480
|
}
|
|
472
481
|
|
|
473
482
|
.modal-content {
|
|
474
483
|
background: var(--bg-card);
|
|
475
484
|
border: 1px solid var(--border-color);
|
|
476
|
-
border-radius:
|
|
485
|
+
border-radius: 6px;
|
|
477
486
|
max-width: 900px;
|
|
478
487
|
width: 90%;
|
|
479
488
|
max-height: 90vh;
|
|
@@ -553,7 +562,7 @@
|
|
|
553
562
|
margin-bottom: 1.5rem;
|
|
554
563
|
background: var(--bg-card);
|
|
555
564
|
border: 1px solid var(--border-color);
|
|
556
|
-
border-radius:
|
|
565
|
+
border-radius: 6px;
|
|
557
566
|
padding: 0.875rem 1rem;
|
|
558
567
|
}
|
|
559
568
|
|
|
@@ -899,7 +908,7 @@
|
|
|
899
908
|
padding: 0.75rem 1rem;
|
|
900
909
|
background: var(--bg-tertiary);
|
|
901
910
|
border: 1px solid var(--border-color);
|
|
902
|
-
border-radius:
|
|
911
|
+
border-radius: 6px;
|
|
903
912
|
cursor: pointer;
|
|
904
913
|
transition: all 0.15s;
|
|
905
914
|
}
|
|
@@ -248,6 +248,58 @@
|
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// AI Prompt builder — assembles a prompt from documentation + report data
|
|
252
|
+
const aiPromptInstruction = <%== @documentation[:ai_prompt].to_json %>;
|
|
253
|
+
const aiPromptWhat = <%== @documentation[:what].to_json %>;
|
|
254
|
+
const aiPromptHow = <%== @documentation[:how].to_json %>;
|
|
255
|
+
const aiPromptNuances = <%== (@documentation[:nuances] || []).to_json %>;
|
|
256
|
+
|
|
257
|
+
function buildAiPrompt() {
|
|
258
|
+
if (!currentReportData || !currentReportData.data) return null;
|
|
259
|
+
|
|
260
|
+
const rows = currentReportData.data.slice(0, 15);
|
|
261
|
+
const cols = currentReportData.columns || [];
|
|
262
|
+
|
|
263
|
+
// Build markdown table from report data
|
|
264
|
+
let table = '| ' + cols.join(' | ') + ' |\n';
|
|
265
|
+
table += '| ' + cols.map(() => '---').join(' | ') + ' |\n';
|
|
266
|
+
rows.forEach(row => {
|
|
267
|
+
const values = cols.map(c => {
|
|
268
|
+
const v = row[c];
|
|
269
|
+
return v == null ? '' : String(v).replace(/\|/g, '\\|').substring(0, 120);
|
|
270
|
+
});
|
|
271
|
+
table += '| ' + values.join(' | ') + ' |\n';
|
|
272
|
+
});
|
|
273
|
+
if (currentReportData.data.length > 15) {
|
|
274
|
+
table += `\n(${currentReportData.data.length - 15} more rows omitted)\n`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Assemble the prompt
|
|
278
|
+
let prompt = `## Problem\n\n${aiPromptWhat}\n\n${aiPromptHow}\n`;
|
|
279
|
+
prompt += `\n## What needs to be done\n\n${aiPromptInstruction}\n`;
|
|
280
|
+
prompt += `\n## Detected issues\n\n${table}`;
|
|
281
|
+
if (aiPromptNuances.length > 0) {
|
|
282
|
+
prompt += `\n## Important context\n\n`;
|
|
283
|
+
aiPromptNuances.forEach(n => { prompt += `- ${n}\n`; });
|
|
284
|
+
}
|
|
285
|
+
return prompt;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function copyAiPrompt(el) {
|
|
289
|
+
const prompt = buildAiPrompt();
|
|
290
|
+
if (!prompt) {
|
|
291
|
+
showToast('Run the report first', 'error');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
document.getElementById('dropdown-menu').classList.remove('show');
|
|
296
|
+
navigator.clipboard.writeText(prompt).then(() => {
|
|
297
|
+
showToast('AI prompt copied to clipboard');
|
|
298
|
+
}).catch(() => {
|
|
299
|
+
showToast('Failed to copy', 'error');
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
251
303
|
// Check if a value exceeds threshold
|
|
252
304
|
function checkThreshold(value, threshold, inverted = false) {
|
|
253
305
|
if (!threshold || value === null || value === undefined) return null;
|
|
@@ -688,7 +740,7 @@
|
|
|
688
740
|
`;
|
|
689
741
|
}
|
|
690
742
|
|
|
691
|
-
// Show
|
|
743
|
+
// Show export and telegram buttons
|
|
692
744
|
if (downloadDropdown) downloadDropdown.style.display = 'inline-block';
|
|
693
745
|
if (telegramBtn) telegramBtn.style.display = 'inline-flex';
|
|
694
746
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
.documentation-section {
|
|
4
4
|
background: var(--bg-card);
|
|
5
5
|
border: 1px solid var(--border-color);
|
|
6
|
-
border-radius:
|
|
6
|
+
border-radius: 6px;
|
|
7
7
|
overflow: hidden;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -138,9 +138,9 @@
|
|
|
138
138
|
margin-top: 4px;
|
|
139
139
|
background: var(--bg-card);
|
|
140
140
|
border: 1px solid var(--border-color);
|
|
141
|
-
border-radius:
|
|
141
|
+
border-radius: 6px;
|
|
142
142
|
min-width: 160px;
|
|
143
|
-
box-shadow: 0
|
|
143
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
144
144
|
z-index: 100;
|
|
145
145
|
overflow: hidden;
|
|
146
146
|
}
|
|
@@ -167,6 +167,26 @@
|
|
|
167
167
|
border-bottom: 1px solid var(--border-color);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
.dropdown-divider {
|
|
171
|
+
height: 0;
|
|
172
|
+
margin: 0;
|
|
173
|
+
border-top: 1px solid var(--border-color);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.dropdown-menu .ai-icon {
|
|
177
|
+
display: inline-block;
|
|
178
|
+
font-size: 0.6rem;
|
|
179
|
+
font-weight: 800;
|
|
180
|
+
background: #8b5cf6;
|
|
181
|
+
color: #fff;
|
|
182
|
+
border-radius: 3px;
|
|
183
|
+
padding: 0.1rem 0.3rem;
|
|
184
|
+
letter-spacing: 0.03em;
|
|
185
|
+
line-height: 1.2;
|
|
186
|
+
vertical-align: middle;
|
|
187
|
+
margin-right: 0.2rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
170
190
|
/* Clickable rows */
|
|
171
191
|
.results-table tbody tr.data-row {
|
|
172
192
|
cursor: pointer;
|
|
@@ -260,7 +280,7 @@
|
|
|
260
280
|
.problem-modal-content {
|
|
261
281
|
background: var(--bg-card);
|
|
262
282
|
border: 1px solid var(--border-color);
|
|
263
|
-
border-radius:
|
|
283
|
+
border-radius: 6px;
|
|
264
284
|
max-width: 600px;
|
|
265
285
|
width: 90%;
|
|
266
286
|
max-height: 80vh;
|
|
@@ -649,7 +669,7 @@
|
|
|
649
669
|
.saved-records-section {
|
|
650
670
|
background: var(--bg-card);
|
|
651
671
|
border: 1px solid var(--accent-purple);
|
|
652
|
-
border-radius:
|
|
672
|
+
border-radius: 6px;
|
|
653
673
|
margin-bottom: 1rem;
|
|
654
674
|
overflow: hidden;
|
|
655
675
|
}
|
|
@@ -1161,7 +1181,7 @@
|
|
|
1161
1181
|
.filter-section {
|
|
1162
1182
|
background: var(--bg-card);
|
|
1163
1183
|
border: 1px solid var(--border-color);
|
|
1164
|
-
border-radius:
|
|
1184
|
+
border-radius: 6px;
|
|
1165
1185
|
overflow: hidden;
|
|
1166
1186
|
}
|
|
1167
1187
|
|
|
@@ -1272,7 +1292,7 @@
|
|
|
1272
1292
|
/* Summary card at top */
|
|
1273
1293
|
.explain-summary {
|
|
1274
1294
|
background: var(--bg-card);
|
|
1275
|
-
border-radius:
|
|
1295
|
+
border-radius: 6px;
|
|
1276
1296
|
padding: 1.25rem;
|
|
1277
1297
|
margin-bottom: 1.5rem;
|
|
1278
1298
|
border-left: 4px solid var(--accent-blue);
|
|
@@ -1344,7 +1364,7 @@
|
|
|
1344
1364
|
.explain-stat {
|
|
1345
1365
|
background: var(--bg-card);
|
|
1346
1366
|
border: 1px solid var(--border-color);
|
|
1347
|
-
border-radius:
|
|
1367
|
+
border-radius: 6px;
|
|
1348
1368
|
padding: 1rem;
|
|
1349
1369
|
display: flex;
|
|
1350
1370
|
flex-direction: column;
|
|
@@ -1387,7 +1407,7 @@
|
|
|
1387
1407
|
.explain-problems {
|
|
1388
1408
|
background: var(--bg-card);
|
|
1389
1409
|
border: 1px solid var(--border-color);
|
|
1390
|
-
border-radius:
|
|
1410
|
+
border-radius: 6px;
|
|
1391
1411
|
padding: 1.25rem;
|
|
1392
1412
|
margin-bottom: 1.5rem;
|
|
1393
1413
|
}
|
|
@@ -1482,7 +1502,7 @@
|
|
|
1482
1502
|
.explain-output {
|
|
1483
1503
|
background: var(--bg-card);
|
|
1484
1504
|
border: 1px solid var(--border-color);
|
|
1485
|
-
border-radius:
|
|
1505
|
+
border-radius: 6px;
|
|
1486
1506
|
overflow: hidden;
|
|
1487
1507
|
}
|
|
1488
1508
|
|
|
@@ -1633,7 +1653,7 @@
|
|
|
1633
1653
|
overflow-x: auto;
|
|
1634
1654
|
background: var(--bg-card);
|
|
1635
1655
|
border: 1px solid var(--border-color);
|
|
1636
|
-
border-radius:
|
|
1656
|
+
border-radius: 6px;
|
|
1637
1657
|
padding: 1.25rem;
|
|
1638
1658
|
color: var(--text-secondary);
|
|
1639
1659
|
}
|
|
@@ -313,7 +313,10 @@ pg_stat_statements.track = all</pre>
|
|
|
313
313
|
<% if category_key == :queries && !@pg_stat_status[:ready] %>
|
|
314
314
|
<div class="report-link disabled">
|
|
315
315
|
<div class="report-link-info">
|
|
316
|
-
<span class="report-link-name"
|
|
316
|
+
<span class="report-link-name">
|
|
317
|
+
<%= report[:name] %>
|
|
318
|
+
<% if report[:new] %><span class="report-badge-new">NEW</span><% end %>
|
|
319
|
+
</span>
|
|
317
320
|
<span class="report-link-desc"><%= report[:description] %></span>
|
|
318
321
|
</div>
|
|
319
322
|
<span class="lock">🔒</span>
|
|
@@ -321,7 +324,10 @@ pg_stat_statements.track = all</pre>
|
|
|
321
324
|
<% else %>
|
|
322
325
|
<%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
|
|
323
326
|
<div class="report-link-info">
|
|
324
|
-
<span class="report-link-name"
|
|
327
|
+
<span class="report-link-name">
|
|
328
|
+
<%= report[:name] %>
|
|
329
|
+
<% if report[:new] %><span class="report-badge-new">NEW</span><% end %>
|
|
330
|
+
</span>
|
|
325
331
|
<span class="report-link-desc"><%= report[:description] %></span>
|
|
326
332
|
</div>
|
|
327
333
|
<span class="arrow">→</span>
|
|
@@ -404,7 +410,7 @@ pg_stat_statements.track = all</pre>
|
|
|
404
410
|
margin: -1.5rem -1.5rem 1rem -1.5rem;
|
|
405
411
|
background: rgba(245, 158, 11, 0.1);
|
|
406
412
|
border-bottom: 1px solid rgba(245, 158, 11, 0.2);
|
|
407
|
-
border-radius:
|
|
413
|
+
border-radius: 6px 6px 0 0;
|
|
408
414
|
color: var(--accent-amber);
|
|
409
415
|
font-size: 0.8rem;
|
|
410
416
|
font-weight: 500;
|
|
@@ -418,7 +424,7 @@ pg_stat_statements.track = all</pre>
|
|
|
418
424
|
padding: 0.75rem 1rem;
|
|
419
425
|
background: var(--bg-tertiary);
|
|
420
426
|
border: 1px solid transparent;
|
|
421
|
-
border-radius:
|
|
427
|
+
border-radius: 6px;
|
|
422
428
|
color: var(--text-muted);
|
|
423
429
|
font-size: 0.9rem;
|
|
424
430
|
cursor: not-allowed;
|
|
@@ -469,7 +475,7 @@ pg_stat_statements.track = all</pre>
|
|
|
469
475
|
.modal-content {
|
|
470
476
|
background: var(--bg-card);
|
|
471
477
|
border: 1px solid var(--border-color);
|
|
472
|
-
border-radius:
|
|
478
|
+
border-radius: 6px;
|
|
473
479
|
max-width: 600px;
|
|
474
480
|
width: 90%;
|
|
475
481
|
max-height: 80vh;
|
|
@@ -583,7 +589,7 @@ pg_stat_statements.track = all</pre>
|
|
|
583
589
|
margin-bottom: 2rem;
|
|
584
590
|
background: var(--bg-card);
|
|
585
591
|
border: 1px solid var(--border-color);
|
|
586
|
-
border-radius:
|
|
592
|
+
border-radius: 6px;
|
|
587
593
|
padding: 1.25rem 1.5rem;
|
|
588
594
|
}
|
|
589
595
|
|
|
@@ -691,7 +697,7 @@ pg_stat_statements.track = all</pre>
|
|
|
691
697
|
.live-metric-card {
|
|
692
698
|
background: var(--bg-tertiary);
|
|
693
699
|
border: 1px solid var(--border-color);
|
|
694
|
-
border-radius:
|
|
700
|
+
border-radius: 6px;
|
|
695
701
|
padding: 1rem;
|
|
696
702
|
position: relative;
|
|
697
703
|
transition: all 0.2s;
|
|
@@ -798,7 +804,7 @@ pg_stat_statements.track = all</pre>
|
|
|
798
804
|
padding: 1rem 1.5rem;
|
|
799
805
|
background: var(--bg-card);
|
|
800
806
|
border: 1px solid var(--border-color);
|
|
801
|
-
border-radius:
|
|
807
|
+
border-radius: 6px;
|
|
802
808
|
color: var(--text-primary);
|
|
803
809
|
font-size: 0.875rem;
|
|
804
810
|
font-weight: 500;
|
|
@@ -20,15 +20,19 @@
|
|
|
20
20
|
▶ Run Report
|
|
21
21
|
</button>
|
|
22
22
|
|
|
23
|
-
<!--
|
|
23
|
+
<!-- Export dropdown -->
|
|
24
24
|
<div class="dropdown" id="download-dropdown" style="display: none;">
|
|
25
25
|
<button class="btn btn-secondary" onclick="toggleDropdown()">
|
|
26
|
-
⬇
|
|
26
|
+
⬇ Export
|
|
27
27
|
</button>
|
|
28
28
|
<div class="dropdown-menu" id="dropdown-menu">
|
|
29
29
|
<a href="#" onclick="downloadReport('txt'); return false;">📄 Text (.txt)</a>
|
|
30
30
|
<a href="#" onclick="downloadReport('csv'); return false;">📊 CSV (.csv)</a>
|
|
31
31
|
<a href="#" onclick="downloadReport('json'); return false;">📋 JSON (.json)</a>
|
|
32
|
+
<% if @documentation && @documentation[:ai_prompt].present? %>
|
|
33
|
+
<div class="dropdown-divider"></div>
|
|
34
|
+
<a href="#" id="ai-prompt-btn" onclick="copyAiPrompt(this); return false;"><span class="ai-icon">AI</span> Copy Prompt</a>
|
|
35
|
+
<% end %>
|
|
32
36
|
</div>
|
|
33
37
|
</div>
|
|
34
38
|
|