pg_reports 0.8.2 → 0.9.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.
@@ -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="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.ide_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>
@@ -191,15 +190,13 @@ pg_stat_statements.track = all</pre>
191
190
  <div id="run-query-content"></div>
192
191
  <% else %>
193
192
  <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>
193
+ <p class="error-title"><%= t("pg_reports.ui.modals.query_execution_disabled_title") %></p>
194
+ <p><%= t("pg_reports.ui.modals.query_execution_disabled_intro") %></p>
195
+ <pre class="code-block">PgReports.configure do |config|
196
+ config.allow_raw_query_execution = true
197
+ end</pre>
198
+ <p><%= t("pg_reports.ui.modals.query_execution_disabled_env_note") %></p>
199
+ <pre class="code-block">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</pre>
203
200
  </div>
204
201
  <% end %>
205
202
  </div>
@@ -217,8 +214,8 @@ end
217
214
  </div>
218
215
  <div class="live-monitoring-controls">
219
216
  <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>
217
+ <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") %>">
218
+ <svg class="icon" id="toggle-icon"><use href="#i-pause"></use></svg>
222
219
  </button>
223
220
  </div>
224
221
  </div>
@@ -227,7 +224,7 @@ end
227
224
  <!-- Connections -->
228
225
  <div class="live-metric-card" data-metric="connections">
229
226
  <div class="metric-header">
230
- <span class="metric-icon" style="background: rgba(107, 159, 232, 0.12); color: var(--accent-blue);">🔗</span>
227
+ <svg class="icon metric-icon"><use href="#i-link"></use></svg>
231
228
  <span class="metric-name"><%= t("pg_reports.ui.metrics.connections_label") %></span>
232
229
  </div>
233
230
  <div class="metric-body">
@@ -248,7 +245,7 @@ end
248
245
  <!-- TPS -->
249
246
  <div class="live-metric-card" data-metric="tps">
250
247
  <div class="metric-header">
251
- <span class="metric-icon" style="background: rgba(95, 184, 154, 0.12); color: var(--accent-green);">⚡</span>
248
+ <svg class="icon metric-icon"><use href="#i-activity"></use></svg>
252
249
  <span class="metric-name"><%= t("pg_reports.ui.metrics.tps_label") %></span>
253
250
  </div>
254
251
  <div class="metric-body">
@@ -269,7 +266,7 @@ end
269
266
  <!-- Cache Hit Ratio -->
270
267
  <div class="live-metric-card" data-metric="cache">
271
268
  <div class="metric-header">
272
- <span class="metric-icon" style="background: rgba(157, 140, 214, 0.12); color: var(--accent-purple);">💾</span>
269
+ <svg class="icon metric-icon"><use href="#i-database"></use></svg>
273
270
  <span class="metric-name"><%= t("pg_reports.ui.metrics.cache_hit_label") %></span>
274
271
  </div>
275
272
  <div class="metric-body">
@@ -288,7 +285,7 @@ end
288
285
  <!-- Long Running Queries -->
289
286
  <div class="live-metric-card" data-metric="longrunning">
290
287
  <div class="metric-header">
291
- <span class="metric-icon" style="background: rgba(212, 160, 86, 0.12); color: var(--accent-amber);">🐢</span>
288
+ <svg class="icon metric-icon"><use href="#i-clock"></use></svg>
292
289
  <span class="metric-name"><%= t("pg_reports.ui.metrics.long_queries_label") %></span>
293
290
  </div>
294
291
  <div class="metric-body">
@@ -307,7 +304,7 @@ end
307
304
  <!-- Blocked Queries -->
308
305
  <div class="live-metric-card" data-metric="blocked">
309
306
  <div class="metric-header">
310
- <span class="metric-icon" style="background: rgba(217, 112, 132, 0.12); color: var(--accent-rose);">🔒</span>
307
+ <svg class="icon metric-icon"><use href="#i-lock"></use></svg>
311
308
  <span class="metric-name"><%= t("pg_reports.ui.metrics.blocked_label") %></span>
312
309
  </div>
313
310
  <div class="metric-body">
@@ -343,17 +340,17 @@ end
343
340
  </span>
344
341
  </div>
345
342
  <div class="query-monitoring-controls">
346
- <button class="btn btn-small btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
343
+ <button class="btn btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
347
344
  <%= t("pg_reports.ui.actions.start_monitoring") %>
348
345
  </button>
349
- <button class="btn btn-small btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
346
+ <button class="btn btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
350
347
  <%= t("pg_reports.ui.actions.stop_monitoring") %>
351
348
  </button>
352
- <button class="btn btn-small btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn">
349
+ <button class="btn btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn" disabled>
353
350
  <%= t("pg_reports.ui.actions.load_history") %>
354
351
  </button>
355
352
  <div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
356
- <button class="btn btn-small btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
353
+ <button class="btn btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
357
354
  <%= t("pg_reports.ui.actions.download") %>
358
355
  </button>
359
356
  <div class="download-menu" id="monitor-download-menu" style="display: none;">
@@ -376,34 +373,33 @@ end
376
373
  </div>
377
374
  <% end %>
378
375
 
376
+ <%
377
+ # Sprite ids for the category glyphs defined in the layout. The registry's own
378
+ # :icon/:color keys stay untouched — they are part of its public shape.
379
+ category_icons = {
380
+ queries: "queries", indexes: "indexes", tables: "tables",
381
+ connections: "connections", system: "system", schema_analysis: "schema"
382
+ }
383
+ %>
379
384
  <div class="categories-grid">
380
385
  <% @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 %>
386
+ <% disabled_reason = category_disabled_reason(category_key) %>
387
+ <div class="category-card<%= ' disabled' if disabled_reason %>">
388
+ <% if disabled_reason %>
388
389
  <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 %>
390
+ <svg class="icon" aria-hidden="true"><use href="#i-lock"></use></svg>
391
+ <span><%= disabled_reason %></span>
394
392
  </div>
395
393
  <% end %>
396
394
  <div class="category-header">
397
- <div class="category-icon" style="background: <%= category[:color] %>20; color: <%= category[:color] %>;">
398
- <%= category[:icon] %>
399
- </div>
395
+ <svg class="icon category-icon" aria-hidden="true"><use href="#i-<%= category_icons.fetch(category_key, "tables") %>"></use></svg>
400
396
  <span class="category-title"><%= category[:name] %></span>
401
397
  <span class="category-count"><%= category[:reports].size %> <%= t("pg_reports.ui.categories.reports_count_suffix") %></span>
402
398
  </div>
403
399
 
404
400
  <div class="reports-list">
405
401
  <% category[:reports].each do |report_key, report| %>
406
- <% if is_disabled %>
402
+ <% if disabled_reason %>
407
403
  <div class="report-link disabled">
408
404
  <div class="report-link-info">
409
405
  <span class="report-link-name">
@@ -412,7 +408,7 @@ end
412
408
  </span>
413
409
  <span class="report-link-desc"><%= report[:description] %></span>
414
410
  </div>
415
- <span class="lock">🔒</span>
411
+ <svg class="icon lock" aria-hidden="true"><use href="#i-lock"></use></svg>
416
412
  </div>
417
413
  <% else %>
418
414
  <%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
@@ -423,7 +419,7 @@ end
423
419
  </span>
424
420
  <span class="report-link-desc"><%= report[:description] %></span>
425
421
  </div>
426
- <span class="arrow">→</span>
422
+ <svg class="icon arrow" aria-hidden="true"><use href="#i-chevron-right"></use></svg>
427
423
  <% end %>
428
424
  <% end %>
429
425
  <% end %>
@@ -456,7 +452,7 @@ end
456
452
  text-decoration: none;
457
453
  }
458
454
  .dashboard-footer a:hover {
459
- color: var(--accent-purple);
455
+ color: var(--text-primary);
460
456
  text-decoration: underline;
461
457
  }
462
458
  .dashboard-footer .footer-sep {
@@ -469,104 +465,45 @@ end
469
465
  gap: 0.5rem;
470
466
  }
471
467
 
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
468
  .badge-dot.error { background: var(--accent-rose); }
537
469
 
538
470
  .category-card.disabled {
539
471
  opacity: 0.7;
540
472
  }
541
473
 
474
+ /* Why a category is unavailable. The icon carries the state; the reason
475
+ itself is body copy, not a wall of amber. */
542
476
  .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;
477
+ display: flex;
478
+ align-items: flex-start;
479
+ gap: 0.45rem;
480
+ padding: 0.5rem 1rem;
481
+ margin: -1rem -1rem 1rem;
482
+ background: var(--bg-tertiary);
483
+ border-bottom: 1px solid var(--border-color);
484
+ border-radius: var(--radius) var(--radius) 0 0;
485
+ color: var(--text-secondary);
486
+ font-size: 0.75rem;
487
+ line-height: 1.5;
488
+ }
489
+
490
+ .category-warning .icon {
491
+ width: 0.8125rem;
492
+ height: 0.8125rem;
493
+ margin-top: 0.25rem;
548
494
  color: var(--accent-amber);
549
- font-size: 0.8rem;
550
- font-weight: 500;
551
- text-align: center;
552
495
  }
553
496
 
554
497
  .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;
498
+ background: transparent;
562
499
  color: var(--text-muted);
563
- font-size: 0.9rem;
564
500
  cursor: not-allowed;
565
501
  }
566
502
 
567
503
  .report-link .lock {
568
- font-size: 0.75rem;
569
- opacity: 0.5;
504
+ width: 0.8125rem;
505
+ height: 0.8125rem;
506
+ opacity: 0.6;
570
507
  }
571
508
 
572
509
  .report-link-info {
@@ -594,66 +531,8 @@ end
594
531
  color: var(--text-secondary);
595
532
  }
596
533
 
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
534
  .modal-body p {
655
535
  color: var(--text-secondary);
656
- margin-bottom: 1rem;
657
536
  }
658
537
 
659
538
  .modal-body ol {
@@ -661,33 +540,14 @@ end
661
540
  color: var(--text-secondary);
662
541
  }
663
542
 
664
- .modal-body li {
665
- margin-bottom: 1.25rem;
666
- }
667
-
668
543
  .modal-body strong {
669
544
  color: var(--text-primary);
670
545
  }
671
546
 
672
- .modal-body pre {
547
+ /* A step's label and its snippet are one unit — tighter than the gap
548
+ between steps. */
549
+ .modal-body li > strong + pre {
673
550
  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
551
  }
692
552
 
693
553
  .modal-header-danger h3 {
@@ -695,58 +555,55 @@ end
695
555
  }
696
556
 
697
557
  .warning-text {
698
- font-size: 1rem;
558
+ font-size: 0.9375rem;
699
559
  font-weight: 500;
700
- color: var(--text-primary) !important;
701
- margin-bottom: 0.5rem !important;
560
+ color: var(--text-primary);
702
561
  }
703
562
 
704
563
  .warning-subtext {
705
- font-size: 0.875rem;
706
- color: var(--text-muted) !important;
707
- margin-bottom: 1.5rem !important;
564
+ font-size: 0.8125rem;
565
+ color: var(--text-muted);
708
566
  }
709
567
 
710
568
  .modal-actions {
711
569
  display: flex;
712
- gap: 0.75rem;
570
+ gap: 0.5rem;
713
571
  justify-content: flex-end;
714
572
  }
715
573
 
716
- .modal-actions .btn {
717
- padding: 0.625rem 1.25rem;
718
- font-size: 0.875rem;
719
- }
720
-
721
574
  /* Live Monitoring Panel */
722
575
  .live-monitoring-panel {
723
- margin-bottom: 2rem;
576
+ margin-bottom: 1.5rem;
724
577
  background: var(--bg-card);
725
578
  border: 1px solid var(--border-color);
726
- border-radius: 6px;
727
- padding: 1.25rem 1.5rem;
579
+ border-radius: var(--radius);
580
+ padding: 0.875rem 1rem;
728
581
  }
729
582
 
730
583
  .live-monitoring-header {
731
584
  display: flex;
732
585
  align-items: center;
733
586
  justify-content: space-between;
734
- margin-bottom: 1.25rem;
735
- padding-bottom: 1rem;
587
+ gap: 1rem;
588
+ flex-wrap: wrap;
589
+ margin-bottom: 0.875rem;
590
+ padding-bottom: 0.75rem;
736
591
  border-bottom: 1px solid var(--border-color);
737
592
  }
738
593
 
739
594
  .live-monitoring-title {
740
595
  display: flex;
741
596
  align-items: center;
742
- gap: 0.75rem;
597
+ gap: 0.625rem;
743
598
  font-weight: 600;
744
- font-size: 1rem;
599
+ font-size: 0.875rem;
600
+ min-width: 0;
745
601
  }
746
602
 
747
603
  .live-indicator {
748
- width: 8px;
749
- height: 8px;
604
+ width: 7px;
605
+ height: 7px;
606
+ flex: none;
750
607
  border-radius: 50%;
751
608
  background: var(--accent-green);
752
609
  animation: pulse 2s infinite;
@@ -768,29 +625,11 @@ end
768
625
  color: var(--text-muted);
769
626
  }
770
627
 
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
628
  /* Metrics Grid */
790
629
  .live-metrics-grid {
791
630
  display: grid;
792
631
  grid-template-columns: repeat(5, 1fr);
793
- gap: 1rem;
632
+ gap: 0.75rem;
794
633
  }
795
634
 
796
635
  @media (max-width: 1200px) {
@@ -811,74 +650,69 @@ end
811
650
  }
812
651
  }
813
652
 
814
- /* Metric Card */
653
+ /* Metric Card — a plain bordered tile. Status is carried by the dot, the
654
+ value colour and the sparkline, never by a coloured edge: "healthy" is
655
+ the common case and should read as quiet, not decorated. */
815
656
  .live-metric-card {
816
657
  background: var(--bg-tertiary);
817
658
  border: 1px solid var(--border-color);
818
- border-radius: 6px;
819
- padding: 1rem;
659
+ border-radius: var(--radius);
660
+ padding: 0.875rem 1rem;
820
661
  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);
662
+ transition: border-color 0.12s ease;
830
663
  }
831
664
 
832
665
  .live-metric-card.status-warning {
833
- border-left: 3px solid var(--accent-amber);
666
+ border-color: var(--amber-line);
834
667
  }
835
668
 
836
669
  .live-metric-card.status-critical {
837
- border-left: 3px solid var(--accent-rose);
670
+ border-color: var(--rose-line);
838
671
  }
839
672
 
840
673
  .metric-header {
841
674
  display: flex;
842
675
  align-items: center;
843
676
  gap: 0.5rem;
844
- margin-bottom: 0.75rem;
677
+ margin-bottom: 0.625rem;
845
678
  }
846
679
 
847
680
  .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;
681
+ width: 0.9375rem;
682
+ height: 0.9375rem;
683
+ color: var(--text-muted);
855
684
  }
856
685
 
857
686
  .metric-name {
858
- font-size: 0.8rem;
687
+ font-size: 0.75rem;
859
688
  font-weight: 500;
689
+ letter-spacing: 0.02em;
860
690
  color: var(--text-secondary);
861
691
  }
862
692
 
863
693
  .metric-body {
864
694
  display: flex;
865
695
  flex-direction: column;
866
- gap: 0.25rem;
696
+ gap: 0.1875rem;
867
697
  }
868
698
 
869
699
  .metric-value {
870
700
  display: flex;
871
701
  align-items: baseline;
872
- gap: 0.25rem;
702
+ gap: 0.3rem;
873
703
  }
874
704
 
875
705
  .metric-value span:first-child {
876
- font-size: 1.5rem;
877
- font-weight: 700;
706
+ font-size: 1.625rem;
707
+ font-weight: 600;
708
+ letter-spacing: -0.02em;
878
709
  color: var(--text-primary);
879
- line-height: 1;
710
+ line-height: 1.1;
880
711
  }
881
712
 
713
+ .status-warning .metric-value span:first-child { color: var(--accent-amber); }
714
+ .status-critical .metric-value span:first-child { color: var(--accent-rose); }
715
+
882
716
  .metric-unit {
883
717
  font-size: 0.75rem;
884
718
  color: var(--text-muted);
@@ -887,71 +721,41 @@ end
887
721
  .metric-detail {
888
722
  font-size: 0.7rem;
889
723
  color: var(--text-muted);
724
+ white-space: nowrap;
725
+ overflow: hidden;
726
+ text-overflow: ellipsis;
890
727
  }
891
728
 
892
729
  /* Sparkline */
893
730
  .metric-sparkline {
894
- height: 30px;
895
- margin-top: 0.5rem;
731
+ height: 28px;
732
+ margin-top: 0.625rem;
733
+ color: var(--text-muted);
896
734
  }
897
735
 
736
+ .status-warning .metric-sparkline { color: var(--accent-amber); }
737
+ .status-critical .metric-sparkline { color: var(--accent-rose); }
738
+
898
739
  .metric-sparkline svg {
899
740
  width: 100%;
900
741
  height: 100%;
742
+ display: block;
901
743
  }
902
744
 
903
745
  /* Status indicator */
904
746
  .metric-status {
905
747
  position: absolute;
906
- top: 0.5rem;
907
- right: 0.5rem;
748
+ top: 0.75rem;
749
+ right: 0.75rem;
908
750
  width: 6px;
909
751
  height: 6px;
910
752
  border-radius: 50%;
753
+ background: var(--border-strong);
911
754
  }
912
755
 
913
- .metric-status.ok { background: var(--accent-green); }
914
756
  .metric-status.warning { background: var(--accent-amber); }
915
757
  .metric-status.critical { background: var(--accent-rose); }
916
758
 
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);
953
- }
954
-
955
759
  /* Run Query Modal */
956
760
  .modal-run-query {
957
761
  max-width: 1100px;
@@ -960,62 +764,57 @@ end
960
764
 
961
765
  .explain-label {
962
766
  display: block;
963
- font-size: 0.75rem;
767
+ font-size: 0.6875rem;
964
768
  font-weight: 600;
769
+ line-height: 1.4;
770
+ letter-spacing: 0.06em;
965
771
  color: var(--text-muted);
966
772
  text-transform: uppercase;
967
- margin-bottom: 0.5rem;
773
+ margin-bottom: 0.4rem;
968
774
  }
969
775
 
970
776
  .run-query-textarea {
971
777
  width: 100%;
972
778
  min-height: 180px;
973
- padding: 1rem;
779
+ padding: 0.875rem;
974
780
  background: var(--bg-primary);
975
781
  border: 1px solid var(--border-color);
976
- border-radius: 8px;
782
+ border-radius: var(--radius-sm);
977
783
  color: var(--text-primary);
978
- font-family: 'JetBrains Mono', monospace;
979
- font-size: 0.85rem;
980
- line-height: 1.5;
784
+ font-family: var(--font-mono);
785
+ font-size: 0.8125rem;
786
+ line-height: 1.6;
981
787
  resize: vertical;
982
788
  }
983
789
 
984
- .run-query-textarea:focus {
985
- outline: none;
790
+ .run-query-textarea:focus-visible {
791
+ outline: 2px solid var(--accent-blue);
792
+ outline-offset: -1px;
986
793
  border-color: var(--accent-blue);
987
- box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
988
794
  }
989
795
 
990
796
  .run-query-textarea::placeholder {
991
797
  color: var(--text-muted);
992
- font-style: italic;
993
798
  }
994
799
 
995
800
  .run-query-actions {
996
801
  display: flex;
997
- gap: 0.75rem;
998
- margin: 1rem 0;
999
- }
1000
-
1001
- .run-query-actions .btn {
1002
- flex: 1;
802
+ gap: 0.5rem;
1003
803
  }
1004
804
 
1005
805
  /* Query Results Table (mirrors _show_styles.html.erb — index.html.erb has
1006
806
  its own <style> block and isn't loaded through that partial) */
1007
807
  .query-results-wrapper {
1008
- margin-top: 1rem;
1009
808
  max-height: 400px;
1010
809
  overflow: auto;
1011
810
  border: 1px solid var(--border-color);
1012
- border-radius: 8px;
811
+ border-radius: var(--radius);
1013
812
  }
1014
813
 
1015
814
  .query-results-table {
1016
815
  width: 100%;
1017
816
  border-collapse: collapse;
1018
- font-family: 'JetBrains Mono', monospace;
817
+ font-family: var(--font-mono);
1019
818
  font-size: 0.75rem;
1020
819
  }
1021
820
 
@@ -1054,8 +853,7 @@ end
1054
853
  justify-content: space-between;
1055
854
  padding: 0.75rem 1rem;
1056
855
  background: var(--bg-tertiary);
1057
- border-radius: 8px;
1058
- margin-top: 1rem;
856
+ border-radius: var(--radius);
1059
857
  font-size: 0.8rem;
1060
858
  color: var(--text-secondary);
1061
859
  }
@@ -1121,15 +919,7 @@ end
1121
919
 
1122
920
  if (!allowRawQueryExecution) {
1123
921
  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>`;
922
+ content.innerHTML = pgReportsDisabledNotice(PG_REPORTS_I18N.modals.query_execution_disabled_title);
1133
923
  return;
1134
924
  }
1135
925
 
@@ -1425,14 +1215,12 @@ end
1425
1215
  function updateToggleUI() {
1426
1216
  const icon = document.getElementById('toggle-icon');
1427
1217
  const indicator = document.querySelector('.live-indicator');
1218
+ const use = icon && icon.querySelector('use');
1428
1219
 
1429
- if (liveMonitoringEnabled) {
1430
- icon.textContent = '';
1431
- indicator.classList.remove('paused');
1432
- } else {
1433
- icon.textContent = '▶';
1434
- indicator.classList.add('paused');
1220
+ if (use) {
1221
+ use.setAttribute('href', liveMonitoringEnabled ? '#i-pause' : '#i-play');
1435
1222
  }
1223
+ indicator.classList.toggle('paused', !liveMonitoringEnabled);
1436
1224
  }
1437
1225
 
1438
1226
  async function fetchLiveMetrics() {
@@ -1505,18 +1293,18 @@ end
1505
1293
  addToHistory('longrunning', metrics.long_running_count);
1506
1294
  addToHistory('blocked', metrics.blocked_count);
1507
1295
 
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
1296
  updateStatus('connections', metrics.connections.percent, 'higher');
1515
1297
  updateStatus('tps', tps, null);
1516
1298
  updateStatus('cache', metrics.cache_hit_ratio, 'lower');
1517
1299
  updateStatus('longrunning', metrics.long_running_count, 'higher');
1518
1300
  updateStatus('blocked', metrics.blocked_count, 'higher');
1519
1301
 
1302
+ renderSparkline('connections', metricsHistory.connections);
1303
+ renderSparkline('tps', metricsHistory.tps);
1304
+ renderSparkline('cache', metricsHistory.cache);
1305
+ renderSparkline('longrunning', metricsHistory.longrunning);
1306
+ renderSparkline('blocked', metricsHistory.blocked);
1307
+
1520
1308
  previousMetrics = metrics;
1521
1309
  }
1522
1310
 
@@ -1533,7 +1321,7 @@ end
1533
1321
  return num.toString();
1534
1322
  }
1535
1323
 
1536
- function renderSparkline(metric, data, color) {
1324
+ function renderSparkline(metric, data) {
1537
1325
  const svg = document.getElementById(`sparkline-${metric}`);
1538
1326
  if (!svg || data.length < 2) return;
1539
1327
 
@@ -1561,15 +1349,16 @@ end
1561
1349
 
1562
1350
  const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1563
1351
  area.setAttribute('d', areaD);
1564
- area.setAttribute('fill', color);
1565
- area.setAttribute('opacity', '0.15');
1352
+ area.setAttribute('fill', 'currentColor');
1353
+ area.setAttribute('opacity', '0.12');
1566
1354
  svg.appendChild(area);
1567
1355
 
1568
1356
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1569
1357
  path.setAttribute('d', pathD);
1570
1358
  path.setAttribute('fill', 'none');
1571
- path.setAttribute('stroke', color);
1572
- path.setAttribute('stroke-width', '1.5');
1359
+ path.setAttribute('stroke', 'currentColor');
1360
+ path.setAttribute('stroke-width', '1.25');
1361
+ path.setAttribute('vector-effect', 'non-scaling-stroke');
1573
1362
  path.setAttribute('stroke-linecap', 'round');
1574
1363
  path.setAttribute('stroke-linejoin', 'round');
1575
1364
  svg.appendChild(path);
@@ -1581,24 +1370,22 @@ end
1581
1370
  if (!card || !statusDot) return;
1582
1371
 
1583
1372
  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
1373
  let status = 'ok';
1591
1374
 
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';
1375
+ if (thresholds.warning || thresholds.critical) {
1376
+ if (badDirection === 'higher') {
1377
+ if (value >= thresholds.critical) status = 'critical';
1378
+ else if (value >= thresholds.warning) status = 'warning';
1379
+ } else if (badDirection === 'lower') {
1380
+ if (value <= thresholds.critical) status = 'critical';
1381
+ else if (value <= thresholds.warning) status = 'warning';
1382
+ }
1598
1383
  }
1599
1384
 
1600
- card.className = `live-metric-card status-${status}`;
1601
- statusDot.className = `metric-status ${status}`;
1385
+ card.classList.remove('status-ok', 'status-warning', 'status-critical');
1386
+ card.classList.add(`status-${status}`);
1387
+ statusDot.classList.remove('ok', 'warning', 'critical');
1388
+ statusDot.classList.add(status);
1602
1389
  }
1603
1390
 
1604
1391
  document.addEventListener('DOMContentLoaded', initLiveMonitoring);
@@ -1784,6 +1571,8 @@ end
1784
1571
  const response = await fetch(`${pgReportsRoot}/query_monitor/status`);
1785
1572
  const data = await response.json();
1786
1573
 
1574
+ queryHistoryAvailable = data.history_available === true;
1575
+
1787
1576
  if (data.success && data.enabled) {
1788
1577
  queryMonitorEnabled = true;
1789
1578
  currentSessionId = data.session_id;
@@ -1802,6 +1591,17 @@ end
1802
1591
  }
1803
1592
  }
1804
1593
 
1594
+ // Set from /query_monitor/status; null until the first check answers.
1595
+ let queryHistoryAvailable = null;
1596
+
1597
+ function applyHistoryAvailability(button) {
1598
+ const btn = button || document.getElementById('load-history-btn');
1599
+ if (!btn || queryHistoryAvailable === null) return;
1600
+
1601
+ btn.disabled = !queryHistoryAvailable;
1602
+ btn.title = queryHistoryAvailable ? '' : PG_REPORTS_I18N.actions.load_history_unavailable_title;
1603
+ }
1604
+
1805
1605
  async function loadQueryHistory(button) {
1806
1606
  button.disabled = true;
1807
1607
  button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.loading;
@@ -1822,8 +1622,9 @@ end
1822
1622
  console.error('Failed to load query history:', error);
1823
1623
  showToast(PG_REPORTS_I18N.errors.load_history_failed + ' ' + error.message, 'error');
1824
1624
  } finally {
1825
- button.disabled = false;
1826
1625
  button.innerHTML = PG_REPORTS_I18N.actions.load_history;
1626
+ button.disabled = false;
1627
+ applyHistoryAvailability(button);
1827
1628
  }
1828
1629
  }
1829
1630
 
@@ -1924,6 +1725,7 @@ end
1924
1725
  startBtn.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1925
1726
  stopBtn.style.display = 'none';
1926
1727
  loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
1728
+ applyHistoryAvailability(loadHistoryBtn);
1927
1729
  sessionBadge.style.display = 'none';
1928
1730
 
1929
1731
  // Keep results visible after stopping, only hide download if no queries