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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +12 -4
  4. data/app/views/layouts/pg_reports/application.html.erb +70 -61
  5. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +53 -1
  6. data/app/views/pg_reports/dashboard/_show_styles.html.erb +31 -11
  7. data/app/views/pg_reports/dashboard/index.html.erb +14 -8
  8. data/app/views/pg_reports/dashboard/show.html.erb +6 -2
  9. data/config/locales/en.yml +109 -0
  10. data/config/locales/ru.yml +81 -0
  11. data/config/locales/uk.yml +126 -0
  12. data/lib/pg_reports/compatibility.rb +63 -0
  13. data/lib/pg_reports/configuration.rb +2 -0
  14. data/lib/pg_reports/dashboard/reports_registry.rb +36 -0
  15. data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
  16. data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
  17. data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
  18. data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
  19. data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
  20. data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
  21. data/lib/pg_reports/engine.rb +6 -0
  22. data/lib/pg_reports/modules/indexes.rb +3 -0
  23. data/lib/pg_reports/modules/queries.rb +1 -0
  24. data/lib/pg_reports/modules/system.rb +27 -0
  25. data/lib/pg_reports/modules/tables.rb +1 -0
  26. data/lib/pg_reports/query_monitor.rb +64 -32
  27. data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
  28. data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
  29. data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
  30. data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
  31. data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
  32. data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
  33. data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
  34. data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
  35. data/lib/pg_reports/version.rb +1 -1
  36. data/lib/pg_reports.rb +5 -0
  37. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e98f6b7de8825ba0c851061a0d5e6f341986b9f710d594a9079a0a220c86f043
4
- data.tar.gz: 713c08469b6c9d7aaea22c133e838cc73ac294acc64bba8fb21fa094c3680745
3
+ metadata.gz: 4e7cfad6574b6b9134d0e1b4bb6ec4ad71b1c8ca9c4fc487acc7839f4ac70168
4
+ data.tar.gz: 918940a09ac75506828c580bc7c64d0b7b8673ae11a9656c425dca69e715d50e
5
5
  SHA512:
6
- metadata.gz: 8e0380c79d58828741eb17529e23272bb3e6fcafcfa7193e2a378e436a55b72b9e5be73e06ec3709836620e50ae704f7213cda83146fec489919695f364d7375
7
- data.tar.gz: 7c5fdd5d289836e04a7b907d88e191e7a270d7bfffbc056230cc1469c4a83ff434e5fbf55dc0c49b6f84bdc2fc949e7b6858627e2e9387f0d9d7c22db423e77e
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` | 🆕 Connection pool utilization analysis |
242
- | `pool_wait_times` | 🆕 Resource wait time analysis |
243
- | `pool_saturation` | 🆕 Pool health warnings with recommendations |
244
- | `connection_churn` | 🆕 Connection lifecycle and churn rate analysis |
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: #0f1114;
17
- --bg-secondary: #161a1e;
18
- --bg-tertiary: #1e2328;
19
- --bg-card: #1a1e23;
20
- --border-color: #2d3339;
21
- --text-primary: #d4d7dc;
22
- --text-secondary: #9ca3ab;
23
- --text-muted: #6b7280;
24
- --accent-purple: #9d8cd6;
25
- --accent-blue: #6b9fe8;
26
- --accent-green: #5fb89a;
27
- --accent-amber: #d4a056;
28
- --accent-rose: #d97084;
29
- --accent-indigo: #8b8fd9;
30
- --gradient-start: #6b9fe8;
31
- --gradient-end: #8b8fd9;
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: 44px;
72
- height: 44px;
73
- background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
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: 1.4rem;
78
+ font-size: 1rem;
79
79
  }
80
80
 
81
81
  .logo-text h1 {
82
- font-size: 1.25rem;
83
- font-weight: 700;
84
- background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
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.75rem;
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: 8px;
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: var(--accent-purple);
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: 36px;
152
- height: 36px;
153
- border-radius: 6px;
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: 1.125rem;
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.6rem 0.875rem;
185
- background: var(--bg-tertiary);
182
+ padding: 0.45rem 0.7rem;
183
+ background: transparent;
186
184
  border: 1px solid transparent;
187
- border-radius: 6px;
185
+ border-radius: 4px;
188
186
  color: var(--text-secondary);
189
187
  text-decoration: none;
190
- font-size: 0.85rem;
191
- transition: all 0.15s ease;
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-secondary);
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(--accent-purple);
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: 6px;
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: all 0.15s ease;
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: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
279
- color: white;
280
- border-color: transparent;
289
+ background: var(--accent-purple);
290
+ color: #fff;
291
+ border-color: var(--accent-purple);
281
292
  }
282
293
 
283
294
  .btn-primary:hover {
284
- opacity: 0.9;
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: var(--bg-secondary);
305
+ background: #2a2d31;
296
306
  color: var(--text-primary);
297
307
  }
298
308
 
299
309
  .btn-telegram {
300
- background: #4a9ebe;
301
- color: white;
302
- border-color: #4a9ebe;
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: #5aaccc;
307
- border-color: #5aaccc;
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: 8px;
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.7);
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: 8px;
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: 8px;
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: 8px;
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 download and telegram buttons
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: 12px;
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: 10px;
141
+ border-radius: 6px;
142
142
  min-width: 160px;
143
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
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: 16px;
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: 12px;
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: 12px;
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: 12px;
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: 10px;
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: 12px;
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: 12px;
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: 12px;
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"><%= report[:name] %></span>
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"><%= report[:name] %></span>
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: 16px 16px 0 0;
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: 10px;
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: 16px;
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: 16px;
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: 12px;
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: 12px;
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
- <!-- Download dropdown -->
23
+ <!-- Export dropdown -->
24
24
  <div class="dropdown" id="download-dropdown" style="display: none;">
25
25
  <button class="btn btn-secondary" onclick="toggleDropdown()">
26
- Download
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