pg_reports 0.8.2 → 0.9.1

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.
@@ -12,26 +12,25 @@
12
12
 
13
13
  <header class="header">
14
14
  <div class="logo">
15
- <div class="logo-icon">🐘</div>
16
- <div class="logo-text">
17
- <h1>
18
- <%= t("pg_reports.ui.branding.title") %>
19
- <span class="logo-version">v<%= PgReports::VERSION %></span>
20
- </h1>
21
- <span class="logo-subtitle"><%= t("pg_reports.ui.branding.subtitle") %></span>
22
- </div>
15
+ <h1>
16
+ <%= t("pg_reports.ui.branding.title") %>
17
+ <span class="logo-version">v<%= PgReports::VERSION %></span>
18
+ </h1>
19
+ <span class="logo-subtitle"><%= t("pg_reports.ui.branding.subtitle") %></span>
23
20
  </div>
24
21
 
25
22
  <div class="header-actions">
26
- <button class="btn btn-small btn-primary" onclick="showRunQueryModal()" id="run-query-open-btn">
23
+ <button class="btn btn-primary" onclick="showRunQueryModal()" id="run-query-open-btn">
27
24
  <%= t("pg_reports.ui.actions.run_query") %>
28
25
  </button>
29
26
  <% if @pg_stat_status[:ready] %>
30
- <button class="btn btn-small btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
27
+ <button class="btn btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
31
28
  <%= t("pg_reports.ui.actions.reset_statistics") %>
32
29
  </button>
33
30
  <% end %>
34
- <button class="btn-info" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">⚙️</button>
31
+ <button type="button" class="btn-icon" onclick="showSettingsModal()" title="<%= t("pg_reports.ui.actions.settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.settings_button_title") %>">
32
+ <svg class="icon icon-lg"><use href="#i-settings"></use></svg>
33
+ </button>
35
34
 
36
35
  <% if !@pg_stat_status[:connected] %>
37
36
  <div class="header-badge" id="pg-stat-badge">
@@ -48,14 +47,14 @@
48
47
  onclick="showPgStatInfo()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
49
48
  <span class="badge-dot warning"></span>
50
49
  <span><%= t("pg_reports.ui.status.not_preloaded") %></span>
51
- <span class="badge-info-icon" aria-hidden="true">ⓘ</span>
50
+ <svg class="icon badge-info-icon" aria-hidden="true"><use href="#i-info"></use></svg>
52
51
  </button>
53
52
  <% else %>
54
53
  <button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
55
54
  onclick="showCreateExtensionModal()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
56
55
  <span class="badge-dot warning"></span>
57
56
  <span><%= t("pg_reports.ui.status.extension_missing") %></span>
58
- <span class="badge-info-icon" aria-hidden="true">ⓘ</span>
57
+ <svg class="icon badge-info-icon" aria-hidden="true"><use href="#i-info"></use></svg>
59
58
  </button>
60
59
  <% end %>
61
60
  </div>
@@ -128,47 +127,7 @@ pg_stat_statements.track = all</pre>
128
127
  </div>
129
128
 
130
129
  <!-- IDE Settings Modal -->
131
- <div id="ide-settings-modal" class="modal" style="display: none;">
132
- <div class="modal-content modal-small">
133
- <div class="modal-header">
134
- <h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
135
- <button class="modal-close" onclick="closeIdeSettingsModal()">&times;</button>
136
- </div>
137
- <div class="modal-body">
138
- <p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
139
- <div class="ide-options">
140
- <label class="ide-option">
141
- <input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
142
- <span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
143
- </label>
144
- <label class="ide-option">
145
- <input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
146
- <span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
147
- </label>
148
- <label class="ide-option">
149
- <input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
150
- <span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
151
- </label>
152
- <label class="ide-option">
153
- <input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
154
- <span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
155
- </label>
156
- <label class="ide-option">
157
- <input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
158
- <span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
159
- </label>
160
- <label class="ide-option">
161
- <input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
162
- <span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
163
- </label>
164
- <label class="ide-option">
165
- <input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
166
- <span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
167
- </label>
168
- </div>
169
- </div>
170
- </div>
171
- </div>
130
+ <%= render "settings_modal" %>
172
131
 
173
132
  <!-- Run Query Modal -->
174
133
  <div id="run-query-modal" class="modal" style="display: none;">
@@ -191,15 +150,13 @@ pg_stat_statements.track = all</pre>
191
150
  <div id="run-query-content"></div>
192
151
  <% else %>
193
152
  <div class="error-message">
194
- <strong><%= t("pg_reports.ui.modals.query_execution_disabled_title") %></strong><br><br>
195
- <%= t("pg_reports.ui.modals.query_execution_disabled_intro") %><br>
196
- <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
197
- PgReports.configure do |config|<br>
198
- &nbsp;&nbsp;config.allow_raw_query_execution = true<br>
199
- end
200
- </code>
201
- <p style="margin-top: 0.75rem;"><%= t("pg_reports.ui.modals.query_execution_disabled_env_note") %></p>
202
- <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</code>
153
+ <p class="error-title"><%= t("pg_reports.ui.modals.query_execution_disabled_title") %></p>
154
+ <p><%= t("pg_reports.ui.modals.query_execution_disabled_intro") %></p>
155
+ <pre class="code-block">PgReports.configure do |config|
156
+ config.allow_raw_query_execution = true
157
+ end</pre>
158
+ <p><%= t("pg_reports.ui.modals.query_execution_disabled_env_note") %></p>
159
+ <pre class="code-block">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</pre>
203
160
  </div>
204
161
  <% end %>
205
162
  </div>
@@ -217,8 +174,8 @@ end
217
174
  </div>
218
175
  <div class="live-monitoring-controls">
219
176
  <span class="live-monitoring-interval"><%= t("pg_reports.ui.monitoring.update_interval") %></span>
220
- <button class="btn-toggle-live" onclick="toggleLiveMonitoring()" title="<%= t("pg_reports.ui.monitoring.toggle_title") %>">
221
- <span id="toggle-icon">⏸</span>
177
+ <button type="button" class="btn-icon btn-small" onclick="toggleLiveMonitoring()" title="<%= t("pg_reports.ui.monitoring.toggle_title") %>" aria-label="<%= t("pg_reports.ui.monitoring.toggle_title") %>">
178
+ <svg class="icon" id="toggle-icon"><use href="#i-pause"></use></svg>
222
179
  </button>
223
180
  </div>
224
181
  </div>
@@ -227,7 +184,7 @@ end
227
184
  <!-- Connections -->
228
185
  <div class="live-metric-card" data-metric="connections">
229
186
  <div class="metric-header">
230
- <span class="metric-icon" style="background: rgba(107, 159, 232, 0.12); color: var(--accent-blue);">🔗</span>
187
+ <svg class="icon metric-icon"><use href="#i-link"></use></svg>
231
188
  <span class="metric-name"><%= t("pg_reports.ui.metrics.connections_label") %></span>
232
189
  </div>
233
190
  <div class="metric-body">
@@ -242,13 +199,12 @@ end
242
199
  <svg id="sparkline-connections" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
243
200
  </div>
244
201
  </div>
245
- <div class="metric-status" id="status-connections"></div>
246
202
  </div>
247
203
 
248
204
  <!-- TPS -->
249
205
  <div class="live-metric-card" data-metric="tps">
250
206
  <div class="metric-header">
251
- <span class="metric-icon" style="background: rgba(95, 184, 154, 0.12); color: var(--accent-green);">⚡</span>
207
+ <svg class="icon metric-icon"><use href="#i-activity"></use></svg>
252
208
  <span class="metric-name"><%= t("pg_reports.ui.metrics.tps_label") %></span>
253
209
  </div>
254
210
  <div class="metric-body">
@@ -263,13 +219,12 @@ end
263
219
  <svg id="sparkline-tps" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
264
220
  </div>
265
221
  </div>
266
- <div class="metric-status" id="status-tps"></div>
267
222
  </div>
268
223
 
269
224
  <!-- Cache Hit Ratio -->
270
225
  <div class="live-metric-card" data-metric="cache">
271
226
  <div class="metric-header">
272
- <span class="metric-icon" style="background: rgba(157, 140, 214, 0.12); color: var(--accent-purple);">💾</span>
227
+ <svg class="icon metric-icon"><use href="#i-database"></use></svg>
273
228
  <span class="metric-name"><%= t("pg_reports.ui.metrics.cache_hit_label") %></span>
274
229
  </div>
275
230
  <div class="metric-body">
@@ -282,13 +237,12 @@ end
282
237
  <svg id="sparkline-cache" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
283
238
  </div>
284
239
  </div>
285
- <div class="metric-status" id="status-cache"></div>
286
240
  </div>
287
241
 
288
242
  <!-- Long Running Queries -->
289
243
  <div class="live-metric-card" data-metric="longrunning">
290
244
  <div class="metric-header">
291
- <span class="metric-icon" style="background: rgba(212, 160, 86, 0.12); color: var(--accent-amber);">🐢</span>
245
+ <svg class="icon metric-icon"><use href="#i-clock"></use></svg>
292
246
  <span class="metric-name"><%= t("pg_reports.ui.metrics.long_queries_label") %></span>
293
247
  </div>
294
248
  <div class="metric-body">
@@ -301,13 +255,12 @@ end
301
255
  <svg id="sparkline-longrunning" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
302
256
  </div>
303
257
  </div>
304
- <div class="metric-status" id="status-longrunning"></div>
305
258
  </div>
306
259
 
307
260
  <!-- Blocked Queries -->
308
261
  <div class="live-metric-card" data-metric="blocked">
309
262
  <div class="metric-header">
310
- <span class="metric-icon" style="background: rgba(217, 112, 132, 0.12); color: var(--accent-rose);">🔒</span>
263
+ <svg class="icon metric-icon"><use href="#i-lock"></use></svg>
311
264
  <span class="metric-name"><%= t("pg_reports.ui.metrics.blocked_label") %></span>
312
265
  </div>
313
266
  <div class="metric-body">
@@ -320,7 +273,6 @@ end
320
273
  <svg id="sparkline-blocked" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
321
274
  </div>
322
275
  </div>
323
- <div class="metric-status" id="status-blocked"></div>
324
276
  </div>
325
277
  </div>
326
278
  </div>
@@ -343,17 +295,17 @@ end
343
295
  </span>
344
296
  </div>
345
297
  <div class="query-monitoring-controls">
346
- <button class="btn btn-small btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
298
+ <button class="btn btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
347
299
  <%= t("pg_reports.ui.actions.start_monitoring") %>
348
300
  </button>
349
- <button class="btn btn-small btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
301
+ <button class="btn btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
350
302
  <%= t("pg_reports.ui.actions.stop_monitoring") %>
351
303
  </button>
352
- <button class="btn btn-small btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn">
304
+ <button class="btn btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn" disabled>
353
305
  <%= t("pg_reports.ui.actions.load_history") %>
354
306
  </button>
355
307
  <div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
356
- <button class="btn btn-small btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
308
+ <button class="btn btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
357
309
  <%= t("pg_reports.ui.actions.download") %>
358
310
  </button>
359
311
  <div class="download-menu" id="monitor-download-menu" style="display: none;">
@@ -376,34 +328,33 @@ end
376
328
  </div>
377
329
  <% end %>
378
330
 
331
+ <%
332
+ # Sprite ids for the category glyphs defined in the layout. The registry's own
333
+ # :icon/:color keys stay untouched — they are part of its public shape.
334
+ category_icons = {
335
+ queries: "queries", indexes: "indexes", tables: "tables",
336
+ connections: "connections", system: "system", schema_analysis: "schema"
337
+ }
338
+ %>
379
339
  <div class="categories-grid">
380
340
  <% @categories.each do |category_key, category| %>
381
- <%
382
- requires_pg_stat = (category_key == :queries && !@pg_stat_status[:ready])
383
- target_disabled_reason = category_disabled_reason(category_key)
384
- is_disabled = requires_pg_stat || target_disabled_reason.present?
385
- %>
386
- <div class="category-card<%= ' disabled' if is_disabled %>">
387
- <% if requires_pg_stat %>
341
+ <% disabled_reason = category_disabled_reason(category_key) %>
342
+ <div class="category-card<%= ' disabled' if disabled_reason %>">
343
+ <% if disabled_reason %>
388
344
  <div class="category-warning">
389
- <%= t("pg_reports.ui.categories.requires_pg_stat") %>
390
- </div>
391
- <% elsif target_disabled_reason %>
392
- <div class="category-warning">
393
- <%= target_disabled_reason %>
345
+ <svg class="icon" aria-hidden="true"><use href="#i-lock"></use></svg>
346
+ <span><%= disabled_reason %></span>
394
347
  </div>
395
348
  <% end %>
396
349
  <div class="category-header">
397
- <div class="category-icon" style="background: <%= category[:color] %>20; color: <%= category[:color] %>;">
398
- <%= category[:icon] %>
399
- </div>
350
+ <svg class="icon category-icon" aria-hidden="true"><use href="#i-<%= category_icons.fetch(category_key, "tables") %>"></use></svg>
400
351
  <span class="category-title"><%= category[:name] %></span>
401
352
  <span class="category-count"><%= category[:reports].size %> <%= t("pg_reports.ui.categories.reports_count_suffix") %></span>
402
353
  </div>
403
354
 
404
355
  <div class="reports-list">
405
356
  <% category[:reports].each do |report_key, report| %>
406
- <% if is_disabled %>
357
+ <% if disabled_reason %>
407
358
  <div class="report-link disabled">
408
359
  <div class="report-link-info">
409
360
  <span class="report-link-name">
@@ -412,7 +363,7 @@ end
412
363
  </span>
413
364
  <span class="report-link-desc"><%= report[:description] %></span>
414
365
  </div>
415
- <span class="lock">🔒</span>
366
+ <svg class="icon lock" aria-hidden="true"><use href="#i-lock"></use></svg>
416
367
  </div>
417
368
  <% else %>
418
369
  <%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
@@ -423,7 +374,7 @@ end
423
374
  </span>
424
375
  <span class="report-link-desc"><%= report[:description] %></span>
425
376
  </div>
426
- <span class="arrow">→</span>
377
+ <svg class="icon arrow" aria-hidden="true"><use href="#i-chevron-right"></use></svg>
427
378
  <% end %>
428
379
  <% end %>
429
380
  <% end %>
@@ -456,7 +407,7 @@ end
456
407
  text-decoration: none;
457
408
  }
458
409
  .dashboard-footer a:hover {
459
- color: var(--accent-purple);
410
+ color: var(--text-primary);
460
411
  text-decoration: underline;
461
412
  }
462
413
  .dashboard-footer .footer-sep {
@@ -469,104 +420,45 @@ end
469
420
  gap: 0.5rem;
470
421
  }
471
422
 
472
- .btn.btn-small {
473
- padding: 0.5rem 1rem;
474
- font-size: 0.8rem;
475
- }
476
-
477
- /* Keep the header action button the same height as the status badge and
478
- the settings button (all 2.125rem); inline-flex centers the label so no
479
- vertical padding is needed. */
480
- .header-actions .btn {
481
- height: 2.125rem;
482
- padding: 0 1rem;
483
- }
484
-
485
- .btn-info {
486
- width: 2.125rem;
487
- height: 2.125rem;
488
- padding: 0;
489
- display: inline-flex;
490
- align-items: center;
491
- justify-content: center;
492
- background: var(--bg-tertiary);
493
- color: var(--text-muted);
494
- border: 1px solid var(--border-color);
495
- border-radius: 4px;
496
- font-size: 0.9rem;
497
- font-weight: 600;
498
- cursor: pointer;
499
- transition: all 0.15s;
500
- }
501
- .btn-info:hover {
502
- background: var(--accent-purple);
503
- color: white;
504
- border-color: var(--accent-purple);
505
- }
506
-
507
- .btn.btn-danger {
508
- background: var(--accent-rose);
509
- border-color: var(--accent-rose);
510
- }
511
- .btn.btn-danger:hover {
512
- background: #c45a6e;
513
- border-color: #c45a6e;
514
- }
515
-
516
- .btn.btn-secondary {
517
- background: var(--bg-tertiary);
518
- border-color: var(--border-color);
519
- color: var(--text-secondary);
520
- }
521
- .btn.btn-secondary:hover {
522
- background: var(--bg-card);
523
- color: var(--text-primary);
524
- }
525
-
526
- .btn.btn-muted {
527
- background: var(--bg-tertiary);
528
- border-color: var(--accent-rose);
529
- color: var(--text-secondary);
530
- }
531
- .btn.btn-muted:hover {
532
- background: var(--bg-card);
533
- color: var(--text-primary);
534
- }
535
-
536
423
  .badge-dot.error { background: var(--accent-rose); }
537
424
 
538
425
  .category-card.disabled {
539
426
  opacity: 0.7;
540
427
  }
541
428
 
429
+ /* Why a category is unavailable. The icon carries the state; the reason
430
+ itself is body copy, not a wall of amber. */
542
431
  .category-warning {
543
- padding: 0.625rem 1rem;
544
- margin: -1rem -1rem 1rem -1rem;
545
- background: rgba(245, 158, 11, 0.1);
546
- border-bottom: 1px solid rgba(245, 158, 11, 0.2);
547
- border-radius: 6px 6px 0 0;
432
+ display: flex;
433
+ align-items: flex-start;
434
+ gap: 0.45rem;
435
+ padding: 0.5rem 1rem;
436
+ margin: -1rem -1rem 1rem;
437
+ background: var(--bg-tertiary);
438
+ border-bottom: 1px solid var(--border-color);
439
+ border-radius: var(--radius) var(--radius) 0 0;
440
+ color: var(--text-secondary);
441
+ font-size: 0.75rem;
442
+ line-height: 1.5;
443
+ }
444
+
445
+ .category-warning .icon {
446
+ width: 0.8125rem;
447
+ height: 0.8125rem;
448
+ margin-top: 0.25rem;
548
449
  color: var(--accent-amber);
549
- font-size: 0.8rem;
550
- font-weight: 500;
551
- text-align: center;
552
450
  }
553
451
 
554
452
  .report-link.disabled {
555
- display: flex;
556
- align-items: center;
557
- justify-content: space-between;
558
- padding: 0.75rem 1rem;
559
- background: var(--bg-tertiary);
560
- border: 1px solid transparent;
561
- border-radius: 6px;
453
+ background: transparent;
562
454
  color: var(--text-muted);
563
- font-size: 0.9rem;
564
455
  cursor: not-allowed;
565
456
  }
566
457
 
567
458
  .report-link .lock {
568
- font-size: 0.75rem;
569
- opacity: 0.5;
459
+ width: 0.8125rem;
460
+ height: 0.8125rem;
461
+ opacity: 0.6;
570
462
  }
571
463
 
572
464
  .report-link-info {
@@ -594,66 +486,8 @@ end
594
486
  color: var(--text-secondary);
595
487
  }
596
488
 
597
- /* Modal */
598
- .modal {
599
- position: fixed;
600
- inset: 0;
601
- background: rgba(0, 0, 0, 0.7);
602
- display: flex;
603
- align-items: center;
604
- justify-content: center;
605
- z-index: 1000;
606
- backdrop-filter: blur(4px);
607
- }
608
-
609
- .modal-content {
610
- background: var(--bg-card);
611
- border: 1px solid var(--border-color);
612
- border-radius: 6px;
613
- max-width: 600px;
614
- width: 90%;
615
- max-height: 80vh;
616
- overflow: auto;
617
- }
618
-
619
- .modal-header {
620
- display: flex;
621
- align-items: center;
622
- justify-content: space-between;
623
- padding: 1.25rem 1.5rem;
624
- border-bottom: 1px solid var(--border-color);
625
- }
626
-
627
- .modal-header h3 {
628
- font-size: 1.125rem;
629
- font-weight: 600;
630
- }
631
-
632
- .modal-close {
633
- width: 32px;
634
- height: 32px;
635
- background: var(--bg-tertiary);
636
- border: 1px solid var(--border-color);
637
- border-radius: 8px;
638
- color: var(--text-muted);
639
- font-size: 1.25rem;
640
- cursor: pointer;
641
- transition: all 0.15s;
642
- }
643
-
644
- .modal-close:hover {
645
- background: var(--accent-rose);
646
- border-color: var(--accent-rose);
647
- color: white;
648
- }
649
-
650
- .modal-body {
651
- padding: 1.5rem;
652
- }
653
-
654
489
  .modal-body p {
655
490
  color: var(--text-secondary);
656
- margin-bottom: 1rem;
657
491
  }
658
492
 
659
493
  .modal-body ol {
@@ -661,33 +495,14 @@ end
661
495
  color: var(--text-secondary);
662
496
  }
663
497
 
664
- .modal-body li {
665
- margin-bottom: 1.25rem;
666
- }
667
-
668
498
  .modal-body strong {
669
499
  color: var(--text-primary);
670
500
  }
671
501
 
672
- .modal-body pre {
502
+ /* A step's label and its snippet are one unit — tighter than the gap
503
+ between steps. */
504
+ .modal-body li > strong + pre {
673
505
  margin-top: 0.5rem;
674
- padding: 1rem;
675
- background: var(--bg-primary);
676
- border: 1px solid var(--border-color);
677
- border-radius: 8px;
678
- font-family: 'JetBrains Mono', monospace;
679
- font-size: 0.8rem;
680
- color: var(--accent-green);
681
- overflow-x: auto;
682
- }
683
-
684
- .modal-small {
685
- max-width: 420px;
686
- }
687
-
688
- .modal-header-danger {
689
- background: rgba(244, 63, 94, 0.1);
690
- border-bottom-color: rgba(244, 63, 94, 0.2);
691
506
  }
692
507
 
693
508
  .modal-header-danger h3 {
@@ -695,58 +510,55 @@ end
695
510
  }
696
511
 
697
512
  .warning-text {
698
- font-size: 1rem;
513
+ font-size: 0.9375rem;
699
514
  font-weight: 500;
700
- color: var(--text-primary) !important;
701
- margin-bottom: 0.5rem !important;
515
+ color: var(--text-primary);
702
516
  }
703
517
 
704
518
  .warning-subtext {
705
- font-size: 0.875rem;
706
- color: var(--text-muted) !important;
707
- margin-bottom: 1.5rem !important;
519
+ font-size: 0.8125rem;
520
+ color: var(--text-muted);
708
521
  }
709
522
 
710
523
  .modal-actions {
711
524
  display: flex;
712
- gap: 0.75rem;
525
+ gap: 0.5rem;
713
526
  justify-content: flex-end;
714
527
  }
715
528
 
716
- .modal-actions .btn {
717
- padding: 0.625rem 1.25rem;
718
- font-size: 0.875rem;
719
- }
720
-
721
529
  /* Live Monitoring Panel */
722
530
  .live-monitoring-panel {
723
- margin-bottom: 2rem;
531
+ margin-bottom: 1.5rem;
724
532
  background: var(--bg-card);
725
533
  border: 1px solid var(--border-color);
726
- border-radius: 6px;
727
- padding: 1.25rem 1.5rem;
534
+ border-radius: var(--radius);
535
+ padding: 0.875rem 1rem;
728
536
  }
729
537
 
730
538
  .live-monitoring-header {
731
539
  display: flex;
732
540
  align-items: center;
733
541
  justify-content: space-between;
734
- margin-bottom: 1.25rem;
735
- padding-bottom: 1rem;
542
+ gap: 1rem;
543
+ flex-wrap: wrap;
544
+ margin-bottom: 0.875rem;
545
+ padding-bottom: 0.75rem;
736
546
  border-bottom: 1px solid var(--border-color);
737
547
  }
738
548
 
739
549
  .live-monitoring-title {
740
550
  display: flex;
741
551
  align-items: center;
742
- gap: 0.75rem;
552
+ gap: 0.625rem;
743
553
  font-weight: 600;
744
- font-size: 1rem;
554
+ font-size: 0.875rem;
555
+ min-width: 0;
745
556
  }
746
557
 
747
558
  .live-indicator {
748
- width: 8px;
749
- height: 8px;
559
+ width: 7px;
560
+ height: 7px;
561
+ flex: none;
750
562
  border-radius: 50%;
751
563
  background: var(--accent-green);
752
564
  animation: pulse 2s infinite;
@@ -768,29 +580,11 @@ end
768
580
  color: var(--text-muted);
769
581
  }
770
582
 
771
- .btn-toggle-live {
772
- width: 32px;
773
- height: 32px;
774
- padding: 0;
775
- background: var(--bg-tertiary);
776
- border: 1px solid var(--border-color);
777
- border-radius: 8px;
778
- color: var(--text-muted);
779
- font-size: 0.875rem;
780
- cursor: pointer;
781
- transition: all 0.15s;
782
- }
783
-
784
- .btn-toggle-live:hover {
785
- background: var(--bg-secondary);
786
- color: var(--text-primary);
787
- }
788
-
789
583
  /* Metrics Grid */
790
584
  .live-metrics-grid {
791
585
  display: grid;
792
586
  grid-template-columns: repeat(5, 1fr);
793
- gap: 1rem;
587
+ gap: 0.75rem;
794
588
  }
795
589
 
796
590
  @media (max-width: 1200px) {
@@ -811,145 +605,112 @@ end
811
605
  }
812
606
  }
813
607
 
814
- /* Metric Card */
608
+ /* Metric Card — a plain bordered tile. Status is carried by the dot, the
609
+ value colour and the sparkline, never by a coloured edge: "healthy" is
610
+ the common case and should read as quiet, not decorated. */
815
611
  .live-metric-card {
612
+ display: flex;
613
+ flex-direction: column;
816
614
  background: var(--bg-tertiary);
817
615
  border: 1px solid var(--border-color);
818
- border-radius: 6px;
819
- padding: 1rem;
820
- position: relative;
821
- transition: all 0.2s;
822
- }
823
-
824
- .live-metric-card:hover {
825
- border-color: var(--accent-purple);
826
- }
827
-
828
- .live-metric-card.status-ok {
829
- border-left: 3px solid var(--accent-green);
616
+ border-radius: var(--radius);
617
+ padding: 0.875rem 1rem;
618
+ /* The trend band below bleeds to the card's edges. */
619
+ overflow: hidden;
620
+ transition: border-color 0.12s ease;
830
621
  }
831
622
 
832
623
  .live-metric-card.status-warning {
833
- border-left: 3px solid var(--accent-amber);
624
+ border-color: var(--amber-line);
834
625
  }
835
626
 
836
627
  .live-metric-card.status-critical {
837
- border-left: 3px solid var(--accent-rose);
628
+ border-color: var(--rose-line);
838
629
  }
839
630
 
840
631
  .metric-header {
841
632
  display: flex;
842
633
  align-items: center;
843
634
  gap: 0.5rem;
844
- margin-bottom: 0.75rem;
635
+ margin-bottom: 0.25rem;
845
636
  }
846
637
 
847
638
  .metric-icon {
848
- width: 28px;
849
- height: 28px;
850
- border-radius: 6px;
851
- display: flex;
852
- align-items: center;
853
- justify-content: center;
854
- font-size: 0.875rem;
639
+ width: 0.9375rem;
640
+ height: 0.9375rem;
641
+ color: var(--text-muted);
855
642
  }
856
643
 
857
644
  .metric-name {
858
- font-size: 0.8rem;
645
+ font-size: 0.75rem;
859
646
  font-weight: 500;
647
+ letter-spacing: 0.02em;
860
648
  color: var(--text-secondary);
861
649
  }
862
650
 
863
651
  .metric-body {
864
652
  display: flex;
865
653
  flex-direction: column;
866
- gap: 0.25rem;
654
+ /* Figures hug the right edge; the trend band below opts back out. */
655
+ align-items: flex-end;
656
+ flex: 1;
657
+ gap: 0.1875rem;
867
658
  }
868
659
 
869
660
  .metric-value {
870
661
  display: flex;
871
662
  align-items: baseline;
872
- gap: 0.25rem;
663
+ gap: 0.3rem;
873
664
  }
874
665
 
875
666
  .metric-value span:first-child {
876
- font-size: 1.5rem;
877
- font-weight: 700;
667
+ font-size: 1.625rem;
668
+ font-weight: 600;
669
+ letter-spacing: -0.02em;
878
670
  color: var(--text-primary);
879
- line-height: 1;
671
+ line-height: 1.1;
880
672
  }
881
673
 
674
+ .status-warning .metric-value span:first-child { color: var(--accent-amber); }
675
+ .status-critical .metric-value span:first-child { color: var(--accent-rose); }
676
+
882
677
  .metric-unit {
883
678
  font-size: 0.75rem;
884
679
  color: var(--text-muted);
885
680
  }
886
681
 
887
682
  .metric-detail {
683
+ max-width: 100%;
888
684
  font-size: 0.7rem;
889
685
  color: var(--text-muted);
686
+ text-align: right;
687
+ white-space: nowrap;
688
+ overflow: hidden;
689
+ text-overflow: ellipsis;
890
690
  }
891
691
 
892
- /* Sparkline */
692
+ /* Trend band. Negative margins cancel the card's padding so it spans the full
693
+ width and sits flush with the bottom edge — as a floating inset line it left
694
+ the lower half of the card reading as dead space. `margin-top: auto` keeps
695
+ it pinned down when cards in a row differ in height. */
893
696
  .metric-sparkline {
894
- height: 30px;
895
- margin-top: 0.5rem;
697
+ /* Full width, regardless of the right-alignment its siblings use. */
698
+ align-self: stretch;
699
+ height: 2.25rem;
700
+ /* auto keeps the band on the bottom edge when cards in a row differ in
701
+ height; the negative side/bottom margins cancel the card's padding. */
702
+ margin: auto -1rem -0.875rem;
703
+ padding-top: 0.5rem;
704
+ color: var(--text-muted);
896
705
  }
897
706
 
707
+ .status-warning .metric-sparkline { color: var(--accent-amber); }
708
+ .status-critical .metric-sparkline { color: var(--accent-rose); }
709
+
898
710
  .metric-sparkline svg {
899
711
  width: 100%;
900
712
  height: 100%;
901
- }
902
-
903
- /* Status indicator */
904
- .metric-status {
905
- position: absolute;
906
- top: 0.5rem;
907
- right: 0.5rem;
908
- width: 6px;
909
- height: 6px;
910
- border-radius: 50%;
911
- }
912
-
913
- .metric-status.ok { background: var(--accent-green); }
914
- .metric-status.warning { background: var(--accent-amber); }
915
- .metric-status.critical { background: var(--accent-rose); }
916
-
917
- /* Toast Notifications */
918
- .toast {
919
- position: fixed;
920
- bottom: 2rem;
921
- right: 2rem;
922
- padding: 1rem 1.5rem;
923
- background: var(--bg-card);
924
- border: 1px solid var(--border-color);
925
- border-radius: 6px;
926
- color: var(--text-primary);
927
- font-size: 0.875rem;
928
- font-weight: 500;
929
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
930
- z-index: 9999;
931
- opacity: 0;
932
- transform: translateY(20px);
933
- transition: all 0.3s ease;
934
- }
935
-
936
- .toast.show {
937
- opacity: 1;
938
- transform: translateY(0);
939
- }
940
-
941
- .toast.toast-success {
942
- border-left: 3px solid var(--accent-green);
943
- }
944
-
945
- .toast.toast-error {
946
- border-left: 3px solid var(--accent-rose);
947
- background: rgba(244, 63, 94, 0.1);
948
- border-color: var(--accent-rose);
949
- }
950
-
951
- .toast.toast-warning {
952
- border-left: 3px solid var(--accent-amber);
713
+ display: block;
953
714
  }
954
715
 
955
716
  /* Run Query Modal */
@@ -960,62 +721,57 @@ end
960
721
 
961
722
  .explain-label {
962
723
  display: block;
963
- font-size: 0.75rem;
724
+ font-size: 0.6875rem;
964
725
  font-weight: 600;
726
+ line-height: 1.4;
727
+ letter-spacing: 0.06em;
965
728
  color: var(--text-muted);
966
729
  text-transform: uppercase;
967
- margin-bottom: 0.5rem;
730
+ margin-bottom: 0.4rem;
968
731
  }
969
732
 
970
733
  .run-query-textarea {
971
734
  width: 100%;
972
735
  min-height: 180px;
973
- padding: 1rem;
736
+ padding: 0.875rem;
974
737
  background: var(--bg-primary);
975
738
  border: 1px solid var(--border-color);
976
- border-radius: 8px;
739
+ border-radius: var(--radius-sm);
977
740
  color: var(--text-primary);
978
- font-family: 'JetBrains Mono', monospace;
979
- font-size: 0.85rem;
980
- line-height: 1.5;
741
+ font-family: var(--font-mono);
742
+ font-size: 0.8125rem;
743
+ line-height: 1.6;
981
744
  resize: vertical;
982
745
  }
983
746
 
984
- .run-query-textarea:focus {
985
- outline: none;
747
+ .run-query-textarea:focus-visible {
748
+ outline: 2px solid var(--accent-blue);
749
+ outline-offset: -1px;
986
750
  border-color: var(--accent-blue);
987
- box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
988
751
  }
989
752
 
990
753
  .run-query-textarea::placeholder {
991
754
  color: var(--text-muted);
992
- font-style: italic;
993
755
  }
994
756
 
995
757
  .run-query-actions {
996
758
  display: flex;
997
- gap: 0.75rem;
998
- margin: 1rem 0;
999
- }
1000
-
1001
- .run-query-actions .btn {
1002
- flex: 1;
759
+ gap: 0.5rem;
1003
760
  }
1004
761
 
1005
762
  /* Query Results Table (mirrors _show_styles.html.erb — index.html.erb has
1006
763
  its own <style> block and isn't loaded through that partial) */
1007
764
  .query-results-wrapper {
1008
- margin-top: 1rem;
1009
765
  max-height: 400px;
1010
766
  overflow: auto;
1011
767
  border: 1px solid var(--border-color);
1012
- border-radius: 8px;
768
+ border-radius: var(--radius);
1013
769
  }
1014
770
 
1015
771
  .query-results-table {
1016
772
  width: 100%;
1017
773
  border-collapse: collapse;
1018
- font-family: 'JetBrains Mono', monospace;
774
+ font-family: var(--font-mono);
1019
775
  font-size: 0.75rem;
1020
776
  }
1021
777
 
@@ -1054,8 +810,7 @@ end
1054
810
  justify-content: space-between;
1055
811
  padding: 0.75rem 1rem;
1056
812
  background: var(--bg-tertiary);
1057
- border-radius: 8px;
1058
- margin-top: 1rem;
813
+ border-radius: var(--radius);
1059
814
  font-size: 0.8rem;
1060
815
  color: var(--text-secondary);
1061
816
  }
@@ -1121,15 +876,7 @@ end
1121
876
 
1122
877
  if (!allowRawQueryExecution) {
1123
878
  showToast(PG_REPORTS_I18N.errors.execute_disabled_toast, 'error');
1124
- content.innerHTML = `<div class="error-message">
1125
- <strong>${PG_REPORTS_I18N.modals.query_execution_disabled_title}</strong><br><br>
1126
- ${PG_REPORTS_I18N.modals.query_execution_disabled_intro}<br>
1127
- <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
1128
- PgReports.configure do |config|<br>
1129
- &nbsp;&nbsp;config.allow_raw_query_execution = true<br>
1130
- end
1131
- </code>
1132
- </div>`;
879
+ content.innerHTML = pgReportsDisabledNotice(PG_REPORTS_I18N.modals.query_execution_disabled_title);
1133
880
  return;
1134
881
  }
1135
882
 
@@ -1425,14 +1172,12 @@ end
1425
1172
  function updateToggleUI() {
1426
1173
  const icon = document.getElementById('toggle-icon');
1427
1174
  const indicator = document.querySelector('.live-indicator');
1175
+ const use = icon && icon.querySelector('use');
1428
1176
 
1429
- if (liveMonitoringEnabled) {
1430
- icon.textContent = '';
1431
- indicator.classList.remove('paused');
1432
- } else {
1433
- icon.textContent = '▶';
1434
- indicator.classList.add('paused');
1177
+ if (use) {
1178
+ use.setAttribute('href', liveMonitoringEnabled ? '#i-pause' : '#i-play');
1435
1179
  }
1180
+ indicator.classList.toggle('paused', !liveMonitoringEnabled);
1436
1181
  }
1437
1182
 
1438
1183
  async function fetchLiveMetrics() {
@@ -1505,18 +1250,18 @@ end
1505
1250
  addToHistory('longrunning', metrics.long_running_count);
1506
1251
  addToHistory('blocked', metrics.blocked_count);
1507
1252
 
1508
- renderSparkline('connections', metricsHistory.connections, 'var(--accent-blue)');
1509
- renderSparkline('tps', metricsHistory.tps, 'var(--accent-green)');
1510
- renderSparkline('cache', metricsHistory.cache, 'var(--accent-purple)');
1511
- renderSparkline('longrunning', metricsHistory.longrunning, 'var(--accent-amber)');
1512
- renderSparkline('blocked', metricsHistory.blocked, 'var(--accent-rose)');
1513
-
1514
1253
  updateStatus('connections', metrics.connections.percent, 'higher');
1515
1254
  updateStatus('tps', tps, null);
1516
1255
  updateStatus('cache', metrics.cache_hit_ratio, 'lower');
1517
1256
  updateStatus('longrunning', metrics.long_running_count, 'higher');
1518
1257
  updateStatus('blocked', metrics.blocked_count, 'higher');
1519
1258
 
1259
+ renderSparkline('connections', metricsHistory.connections);
1260
+ renderSparkline('tps', metricsHistory.tps);
1261
+ renderSparkline('cache', metricsHistory.cache);
1262
+ renderSparkline('longrunning', metricsHistory.longrunning);
1263
+ renderSparkline('blocked', metricsHistory.blocked);
1264
+
1520
1265
  previousMetrics = metrics;
1521
1266
  }
1522
1267
 
@@ -1533,7 +1278,7 @@ end
1533
1278
  return num.toString();
1534
1279
  }
1535
1280
 
1536
- function renderSparkline(metric, data, color) {
1281
+ function renderSparkline(metric, data) {
1537
1282
  const svg = document.getElementById(`sparkline-${metric}`);
1538
1283
  if (!svg || data.length < 2) return;
1539
1284
 
@@ -1541,35 +1286,43 @@ end
1541
1286
 
1542
1287
  const width = 100;
1543
1288
  const height = 30;
1289
+ // No horizontal inset: the band spans the card's full width, so the trace
1290
+ // should reach both edges. Vertical inset only, to keep the stroke from
1291
+ // being clipped at the extremes.
1544
1292
  const padding = 2;
1545
1293
 
1546
1294
  const min = Math.min(...data);
1547
1295
  const max = Math.max(...data);
1548
- const range = max - min || 1;
1296
+ const range = max - min;
1549
1297
 
1550
1298
  const points = data.map((value, index) => {
1551
- const x = padding + (index / (data.length - 1)) * (width - 2 * padding);
1552
- const y = height - padding - ((value - min) / range) * (height - 2 * padding);
1299
+ const x = (index / (data.length - 1)) * width;
1300
+ // A series that never moves is drawn mid-band: pinned to the floor it
1301
+ // reads as "zero" (or as a stray underline) rather than "steady".
1302
+ const y = range === 0
1303
+ ? height / 2
1304
+ : height - padding - ((value - min) / range) * (height - 2 * padding);
1553
1305
  return { x, y };
1554
1306
  });
1555
1307
 
1556
1308
  const pathD = points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ');
1557
1309
 
1558
1310
  const areaD = pathD +
1559
- ` L ${points[points.length - 1].x} ${height - padding}` +
1560
- ` L ${points[0].x} ${height - padding} Z`;
1311
+ ` L ${points[points.length - 1].x} ${height}` +
1312
+ ` L ${points[0].x} ${height} Z`;
1561
1313
 
1562
1314
  const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1563
1315
  area.setAttribute('d', areaD);
1564
- area.setAttribute('fill', color);
1565
- area.setAttribute('opacity', '0.15');
1316
+ area.setAttribute('fill', 'currentColor');
1317
+ area.setAttribute('opacity', '0.12');
1566
1318
  svg.appendChild(area);
1567
1319
 
1568
1320
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1569
1321
  path.setAttribute('d', pathD);
1570
1322
  path.setAttribute('fill', 'none');
1571
- path.setAttribute('stroke', color);
1572
- path.setAttribute('stroke-width', '1.5');
1323
+ path.setAttribute('stroke', 'currentColor');
1324
+ path.setAttribute('stroke-width', '1.25');
1325
+ path.setAttribute('vector-effect', 'non-scaling-stroke');
1573
1326
  path.setAttribute('stroke-linecap', 'round');
1574
1327
  path.setAttribute('stroke-linejoin', 'round');
1575
1328
  svg.appendChild(path);
@@ -1577,28 +1330,23 @@ end
1577
1330
 
1578
1331
  function updateStatus(metric, value, badDirection) {
1579
1332
  const card = document.querySelector(`[data-metric="${metric}"]`);
1580
- const statusDot = document.getElementById(`status-${metric}`);
1581
- if (!card || !statusDot) return;
1333
+ if (!card) return;
1582
1334
 
1583
1335
  const thresholds = LIVE_CONFIG.thresholds[metric];
1584
- if (!thresholds.warning && !thresholds.critical) {
1585
- card.className = 'live-metric-card status-ok';
1586
- statusDot.className = 'metric-status ok';
1587
- return;
1588
- }
1589
-
1590
1336
  let status = 'ok';
1591
1337
 
1592
- if (badDirection === 'higher') {
1593
- if (value >= thresholds.critical) status = 'critical';
1594
- else if (value >= thresholds.warning) status = 'warning';
1595
- } else if (badDirection === 'lower') {
1596
- if (value <= thresholds.critical) status = 'critical';
1597
- else if (value <= thresholds.warning) status = 'warning';
1338
+ if (thresholds.warning || thresholds.critical) {
1339
+ if (badDirection === 'higher') {
1340
+ if (value >= thresholds.critical) status = 'critical';
1341
+ else if (value >= thresholds.warning) status = 'warning';
1342
+ } else if (badDirection === 'lower') {
1343
+ if (value <= thresholds.critical) status = 'critical';
1344
+ else if (value <= thresholds.warning) status = 'warning';
1345
+ }
1598
1346
  }
1599
1347
 
1600
- card.className = `live-metric-card status-${status}`;
1601
- statusDot.className = `metric-status ${status}`;
1348
+ card.classList.remove('status-ok', 'status-warning', 'status-critical');
1349
+ card.classList.add(`status-${status}`);
1602
1350
  }
1603
1351
 
1604
1352
  document.addEventListener('DOMContentLoaded', initLiveMonitoring);
@@ -1620,10 +1368,6 @@ end
1620
1368
  'cursor': 5
1621
1369
  };
1622
1370
 
1623
- function getDefaultIde() {
1624
- return localStorage.getItem('pgReportsDefaultIde') || '';
1625
- }
1626
-
1627
1371
  function generateIdeUrls(filePath, lineNumber) {
1628
1372
  if (!filePath) return [];
1629
1373
 
@@ -1722,41 +1466,6 @@ end
1722
1466
  document.getElementById('ide-dropdown-overlay')?.classList.remove('show');
1723
1467
  }
1724
1468
 
1725
- function showIdeSettingsModal() {
1726
- const modal = document.getElementById('ide-settings-modal');
1727
- modal.style.display = 'flex';
1728
- loadIdeSettingsState();
1729
- }
1730
-
1731
- function closeIdeSettingsModal() {
1732
- document.getElementById('ide-settings-modal').style.display = 'none';
1733
- }
1734
-
1735
- function setDefaultIde(ideKey) {
1736
- if (ideKey) {
1737
- localStorage.setItem('pgReportsDefaultIde', ideKey);
1738
- } else {
1739
- localStorage.removeItem('pgReportsDefaultIde');
1740
- }
1741
- }
1742
-
1743
- function loadIdeSettingsState() {
1744
- const currentIde = getDefaultIde();
1745
- const radios = document.querySelectorAll('input[name="default-ide"]');
1746
- radios.forEach(radio => {
1747
- radio.checked = (radio.value === currentIde);
1748
- });
1749
- }
1750
-
1751
- // Close IDE settings modal on backdrop click
1752
- document.addEventListener('DOMContentLoaded', function() {
1753
- document.getElementById('ide-settings-modal')?.addEventListener('click', function(e) {
1754
- if (e.target === this) {
1755
- closeIdeSettingsModal();
1756
- }
1757
- });
1758
- });
1759
-
1760
1469
  // ============================================
1761
1470
  // Query Monitoring
1762
1471
  // ============================================
@@ -1784,6 +1493,8 @@ end
1784
1493
  const response = await fetch(`${pgReportsRoot}/query_monitor/status`);
1785
1494
  const data = await response.json();
1786
1495
 
1496
+ queryHistoryAvailable = data.history_available === true;
1497
+
1787
1498
  if (data.success && data.enabled) {
1788
1499
  queryMonitorEnabled = true;
1789
1500
  currentSessionId = data.session_id;
@@ -1802,6 +1513,17 @@ end
1802
1513
  }
1803
1514
  }
1804
1515
 
1516
+ // Set from /query_monitor/status; null until the first check answers.
1517
+ let queryHistoryAvailable = null;
1518
+
1519
+ function applyHistoryAvailability(button) {
1520
+ const btn = button || document.getElementById('load-history-btn');
1521
+ if (!btn || queryHistoryAvailable === null) return;
1522
+
1523
+ btn.disabled = !queryHistoryAvailable;
1524
+ btn.title = queryHistoryAvailable ? '' : PG_REPORTS_I18N.actions.load_history_unavailable_title;
1525
+ }
1526
+
1805
1527
  async function loadQueryHistory(button) {
1806
1528
  button.disabled = true;
1807
1529
  button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.loading;
@@ -1822,8 +1544,9 @@ end
1822
1544
  console.error('Failed to load query history:', error);
1823
1545
  showToast(PG_REPORTS_I18N.errors.load_history_failed + ' ' + error.message, 'error');
1824
1546
  } finally {
1825
- button.disabled = false;
1826
1547
  button.innerHTML = PG_REPORTS_I18N.actions.load_history;
1548
+ button.disabled = false;
1549
+ applyHistoryAvailability(button);
1827
1550
  }
1828
1551
  }
1829
1552
 
@@ -1924,6 +1647,7 @@ end
1924
1647
  startBtn.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1925
1648
  stopBtn.style.display = 'none';
1926
1649
  loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
1650
+ applyHistoryAvailability(loadHistoryBtn);
1927
1651
  sessionBadge.style.display = 'none';
1928
1652
 
1929
1653
  // Keep results visible after stopping, only hide download if no queries