pg_reports 0.5.3 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +12 -4
  4. data/app/controllers/pg_reports/dashboard_controller.rb +6 -2
  5. data/app/views/layouts/pg_reports/application.html.erb +70 -61
  6. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +53 -1
  7. data/app/views/pg_reports/dashboard/_show_styles.html.erb +31 -11
  8. data/app/views/pg_reports/dashboard/index.html.erb +80 -9
  9. data/app/views/pg_reports/dashboard/show.html.erb +6 -2
  10. data/config/locales/en.yml +109 -0
  11. data/config/locales/ru.yml +81 -0
  12. data/config/locales/uk.yml +126 -0
  13. data/lib/pg_reports/compatibility.rb +63 -0
  14. data/lib/pg_reports/configuration.rb +2 -0
  15. data/lib/pg_reports/dashboard/reports_registry.rb +36 -0
  16. data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
  17. data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
  18. data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
  19. data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
  20. data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
  21. data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
  22. data/lib/pg_reports/engine.rb +6 -0
  23. data/lib/pg_reports/modules/indexes.rb +3 -0
  24. data/lib/pg_reports/modules/queries.rb +1 -0
  25. data/lib/pg_reports/modules/system.rb +27 -0
  26. data/lib/pg_reports/modules/tables.rb +1 -0
  27. data/lib/pg_reports/query_monitor.rb +139 -42
  28. data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
  29. data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
  30. data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
  31. data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
  32. data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
  33. data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
  34. data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
  35. data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
  36. data/lib/pg_reports/version.rb +1 -1
  37. data/lib/pg_reports.rb +5 -0
  38. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e9a2129fba68b259fc72598215a7cc02ff8785c93a786603505d9b9987d5df1
4
- data.tar.gz: 7910113c5cc18115f59463067ab201f1942ec8b968570557af8aefda645098c7
3
+ metadata.gz: 4e7cfad6574b6b9134d0e1b4bb6ec4ad71b1c8ca9c4fc487acc7839f4ac70168
4
+ data.tar.gz: 918940a09ac75506828c580bc7c64d0b7b8673ae11a9656c425dca69e715d50e
5
5
  SHA512:
6
- metadata.gz: 7e5573dd2cca17cc74cdfe104e6eb09249069d59d50b033effd15e9c46bcb63040da50d998d91c8a2c7e31b8dc4fd3f86c2692f5b60d83050db766942d9a5d46
7
- data.tar.gz: 025a5a37c86817e22265aff1227e332a1f5dd09cf0954a48321fe1ca5dc8c8f402cd13671c4b7b4c1b69e7ec739ef7f8ca47280b11894718d1bcaa946d7983b7
6
+ metadata.gz: e8b00907aaaac1daf07c65ea1b37e9a70b9067f507be0d3f3b8e0863a0848600285dc63cc55a039e86fd5cfcc8af05ce785f58ea40a09677a6832dec0b39de7e
7
+ data.tar.gz: 61d1c769f1e8c9d9eaeaf78f16ca393b782f0ee10ee1cd2d20f468c888d73d79a6c66270219d9938883681ae8a67318655e01ad201bd567b4e3daf056cfa10c3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,67 @@ 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
+
50
+ ## [0.5.4] - 2026-02-11
51
+
52
+ ### Fixed
53
+
54
+ - **Live Query Monitor critical fix for multi-process servers** - monitoring now works correctly with Puma, Unicorn, and other multi-process web servers:
55
+ - Migrated from Singleton instance variables to Rails.cache for cross-process state sharing
56
+ - Fixed "Monitoring not active" errors when requests hit different worker processes
57
+ - Each process now subscribes to SQL notifications when monitoring is enabled
58
+ - State (enabled/session_id) stored in Rails.cache with 24-hour TTL
59
+ - Added cache helper methods with graceful error handling
60
+ - Monitoring state now persists across all processes in multi-worker environments
61
+ - Exclude `query_monitor.rb` itself from `query_from_pg_reports?` check to prevent false positives
62
+
63
+ ### Added
64
+
65
+ - **Enhanced error handling for Query Monitor**:
66
+ - Toast notification system with visual feedback (success/error/warning types)
67
+ - Server errors now displayed to users with clear messages
68
+ - Automatic monitoring stop and UI reset when errors occur
69
+ - Smooth animations with auto-dismiss after 4 seconds
70
+
10
71
  ## [0.5.3] - 2026-02-11
11
72
 
12
73
  ### 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 |
@@ -47,7 +47,7 @@ module PgReports
47
47
  timestamp: Time.current.to_i,
48
48
  available: true
49
49
  }
50
- rescue PG::InsufficientPrivilege => e
50
+ rescue PG::InsufficientPrivilege
51
51
  render json: {
52
52
  success: false,
53
53
  error: "Insufficient database permissions to access statistics views",
@@ -363,8 +363,10 @@ module PgReports
363
363
 
364
364
  def start_query_monitoring
365
365
  monitor = PgReports::QueryMonitor.instance
366
+ Rails.logger.info("PgReports: start_query_monitoring called. Instance: #{monitor.object_id}")
366
367
 
367
368
  result = monitor.start
369
+ Rails.logger.info("PgReports: start result: #{result.inspect}")
368
370
 
369
371
  if result[:success]
370
372
  render json: result
@@ -372,6 +374,7 @@ module PgReports
372
374
  render json: result, status: :unprocessable_entity
373
375
  end
374
376
  rescue => e
377
+ Rails.logger.error("PgReports: start_query_monitoring error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
375
378
  render json: {success: false, error: e.message}, status: :unprocessable_entity
376
379
  end
377
380
 
@@ -407,6 +410,7 @@ module PgReports
407
410
  monitor = PgReports::QueryMonitor.instance
408
411
 
409
412
  unless monitor.enabled
413
+ Rails.logger.warn("PgReports: query_monitor_feed called but monitoring not active. Instance: #{monitor.object_id}, enabled: #{monitor.enabled}, session_id: #{monitor.session_id}")
410
414
  render json: {success: false, message: "Monitoring not active"}
411
415
  return
412
416
  end
@@ -634,7 +638,7 @@ module PgReports
634
638
 
635
639
  # Must start with SELECT (case insensitive)
636
640
  unless normalized.start_with?("select")
637
- raise SecurityError, "Only SELECT queries are allowed. Found: #{normalized.split.first&.upcase || 'unknown'}"
641
+ raise SecurityError, "Only SELECT queries are allowed. Found: #{normalized.split.first&.upcase || "unknown"}"
638
642
  end
639
643
 
640
644
  # Check for dangerous keywords that might be in subqueries or CTEs
@@ -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
  }