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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +12 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +6 -2
- 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 +80 -9
- 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 +139 -42
- 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,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` |
|
|
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 |
|
|
@@ -47,7 +47,7 @@ module PgReports
|
|
|
47
47
|
timestamp: Time.current.to_i,
|
|
48
48
|
available: true
|
|
49
49
|
}
|
|
50
|
-
rescue PG::InsufficientPrivilege
|
|
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 ||
|
|
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: #
|
|
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
|
}
|