pg_reports 0.8.1 → 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.
@@ -5,30 +5,69 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
7
7
  <title><%= t("pg_reports.ui.branding.page_title") %></title>
8
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
8
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%235561d4'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
9
9
  <% if PgReports.config.load_external_fonts %>
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
13
  <% end %>
14
14
  <style>
15
15
  :root {
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;
16
+ --bg-primary: #141618;
17
+ --bg-secondary: #1a1d20;
18
+ --bg-tertiary: #212427;
19
+ --bg-card: #1a1d20;
20
+ --bg-hover: #272b2f;
21
+ --border-color: #2c3034;
22
+ --border-strong: #3b4045;
23
+
24
+ --text-primary: #d3d6d9;
25
+ --text-secondary: #8d9299;
26
+ --text-muted: #666b71;
27
+ --text-tertiary: #666b71;
28
+
24
29
  --accent-purple: #7c8af6;
25
30
  --accent-blue: #5c9eed;
26
31
  --accent-green: #45a87a;
27
32
  --accent-amber: #c89030;
28
33
  --accent-rose: #d45d6e;
29
- --accent-indigo: #7c8af6;
30
- --gradient-start: #7c8af6;
31
- --gradient-end: #7c8af6;
34
+ --accent-red: #d45d6e;
35
+
36
+ /* Tints derived from the accents above. Every status surface uses these
37
+ two levels — fill and hairline — so tinted areas stay in one family. */
38
+ --purple-soft: rgba(124, 138, 246, 0.10);
39
+ --purple-line: rgba(124, 138, 246, 0.32);
40
+ --blue-soft: rgba(92, 158, 237, 0.10);
41
+ --blue-line: rgba(92, 158, 237, 0.32);
42
+ --green-soft: rgba(69, 168, 122, 0.10);
43
+ --green-line: rgba(69, 168, 122, 0.32);
44
+ --amber-soft: rgba(200, 144, 48, 0.10);
45
+ --amber-line: rgba(200, 144, 48, 0.34);
46
+ --rose-soft: rgba(212, 93, 110, 0.10);
47
+ --rose-line: rgba(212, 93, 110, 0.34);
48
+
49
+ --btn-primary-bg: #5561d4;
50
+ --btn-primary-bg-hover: #616de0;
51
+ --btn-primary-bg-active: #4a55c2;
52
+ --btn-danger-bg: #b64d5c;
53
+ --btn-danger-bg-hover: #c45a6a;
54
+ --btn-danger-bg-active: #a34351;
55
+
56
+ --flow-gap: 0.875rem;
57
+ --flow-gap-lg: 1.25rem;
58
+
59
+ --radius-sm: 4px;
60
+ --radius: 6px;
61
+ --radius-lg: 8px;
62
+ --control-h: 2.125rem;
63
+ --control-h-sm: 1.75rem;
64
+
65
+ --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3);
66
+ --shadow-2: 0 6px 20px rgba(0, 0, 0, 0.38);
67
+ --shadow-3: 0 12px 40px rgba(0, 0, 0, 0.45);
68
+
69
+ --font-sans: <%= PgReports.config.load_external_fonts ? "'Inter', " : "" %>-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
70
+ --font-mono: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
32
71
  }
33
72
 
34
73
  * {
@@ -38,11 +77,28 @@
38
77
  }
39
78
 
40
79
  body {
41
- font-family: <%= PgReports.config.load_external_fonts ? "'Plus Jakarta Sans', " : "" %>-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
80
+ font-family: var(--font-sans);
42
81
  background: var(--bg-primary);
43
82
  color: var(--text-primary);
44
83
  min-height: 100vh;
45
- line-height: 1.6;
84
+ line-height: 1.5;
85
+ font-size: 0.875rem;
86
+ -webkit-font-smoothing: antialiased;
87
+ -moz-osx-font-smoothing: grayscale;
88
+ }
89
+
90
+ /* Numbers that refresh in place must not reflow as digits change. */
91
+ .results-table,
92
+ .query-results-table,
93
+ .metric-value,
94
+ .explain-stat-value {
95
+ font-variant-numeric: tabular-nums;
96
+ }
97
+
98
+ :focus-visible {
99
+ outline: 2px solid var(--accent-blue);
100
+ outline-offset: 2px;
101
+ border-radius: var(--radius-sm);
46
102
  }
47
103
 
48
104
  .container {
@@ -62,89 +118,72 @@
62
118
  }
63
119
 
64
120
  .logo {
65
- display: flex;
66
- align-items: center;
67
- gap: 0.75rem;
68
- }
69
-
70
- .logo-icon {
71
- width: 32px;
72
- height: 32px;
73
- background: var(--accent-purple);
74
- border-radius: 6px;
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- font-size: 1rem;
79
- }
80
-
81
- .logo-text {
82
121
  display: flex;
83
122
  flex-direction: column;
84
- gap: 5px;
85
- line-height: 1.15;
123
+ gap: 0.125rem;
124
+ min-width: 0;
86
125
  }
87
126
 
88
- .logo-text h1 {
89
- font-size: 1.1rem;
90
- font-weight: 600;
91
- color: var(--text-primary);
92
- line-height: 1.15;
93
- margin-top: -3px;
127
+ .logo h1 {
94
128
  display: flex;
95
129
  align-items: baseline;
96
130
  gap: 0.5rem;
131
+ font-size: 1.0625rem;
132
+ font-weight: 600;
133
+ letter-spacing: -0.01em;
134
+ color: var(--text-primary);
135
+ line-height: 1.2;
97
136
  }
98
137
 
99
138
  .logo-version {
100
- font-size: 0.7rem;
101
- font-weight: 500;
139
+ font-family: var(--font-mono);
140
+ font-size: 0.6875rem;
141
+ font-weight: 400;
102
142
  color: var(--text-muted);
103
- letter-spacing: 0.02em;
104
143
  }
105
144
 
106
145
  .logo-subtitle {
107
- font-size: 0.7rem;
146
+ font-size: 0.75rem;
108
147
  color: var(--text-muted);
109
- line-height: 1.15;
148
+ line-height: 1.2;
110
149
  }
111
150
 
112
151
  .header-badge {
113
- display: flex;
152
+ display: inline-flex;
114
153
  align-items: center;
115
154
  gap: 0.4rem;
116
- height: 2.125rem;
155
+ height: var(--control-h);
117
156
  padding: 0 0.75rem;
118
157
  background: var(--bg-tertiary);
119
158
  border: 1px solid var(--border-color);
120
- border-radius: 4px;
159
+ border-radius: var(--radius-sm);
121
160
  font-size: 0.75rem;
161
+ color: var(--text-secondary);
122
162
  }
123
163
 
124
164
  .header-badge-clickable {
125
165
  cursor: pointer;
126
166
  font-family: inherit;
127
- color: inherit;
128
- transition: border-color 0.15s ease, background 0.15s ease;
167
+ transition: border-color 0.12s ease, background-color 0.12s ease;
129
168
  }
130
169
 
131
170
  .header-badge-clickable:hover {
132
- background: var(--bg-card);
133
- border-color: var(--text-muted);
171
+ background: var(--bg-hover);
172
+ border-color: var(--border-strong);
173
+ color: var(--text-primary);
134
174
  }
135
175
 
136
176
  .badge-info-icon {
137
177
  color: var(--text-muted);
138
- font-size: 0.85rem;
139
- line-height: 1;
178
+ line-height: 0;
140
179
  }
141
180
 
142
181
  .badge-dot {
143
- width: 8px;
144
- height: 8px;
182
+ width: 7px;
183
+ height: 7px;
184
+ flex: none;
145
185
  border-radius: 50%;
146
186
  background: var(--accent-green);
147
- animation: pulse 2s infinite;
148
187
  }
149
188
 
150
189
  .badge-dot.warning {
@@ -153,13 +192,14 @@
153
192
 
154
193
  @keyframes pulse {
155
194
  0%, 100% { opacity: 1; }
156
- 50% { opacity: 0.5; }
195
+ 50% { opacity: 0.45; }
157
196
  }
158
197
 
159
198
  /* Categories Grid */
160
199
  .categories-grid {
161
200
  display: grid;
162
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
201
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
202
+ align-items: start;
163
203
  gap: 1rem;
164
204
  margin-bottom: 1.5rem;
165
205
  }
@@ -167,12 +207,13 @@
167
207
  .category-card {
168
208
  background: var(--bg-card);
169
209
  border: 1px solid var(--border-color);
170
- border-radius: 6px;
210
+ border-radius: var(--radius);
171
211
  padding: 1rem;
212
+ transition: border-color 0.12s ease;
172
213
  }
173
214
 
174
215
  .category-card:hover {
175
- border-color: #3e4347;
216
+ border-color: var(--border-strong);
176
217
  }
177
218
 
178
219
  .category-header {
@@ -183,28 +224,25 @@
183
224
  }
184
225
 
185
226
  .category-icon {
186
- width: 28px;
187
- height: 28px;
188
- border-radius: 4px;
189
- display: flex;
190
- align-items: center;
191
- justify-content: center;
192
- font-size: 0.95rem;
193
- background: transparent !important;
194
- color: var(--text-secondary) !important;
227
+ width: 1rem;
228
+ height: 1rem;
229
+ color: var(--text-secondary);
195
230
  }
196
231
 
197
232
  .category-title {
198
- font-size: 1rem;
233
+ font-size: 0.9375rem;
199
234
  font-weight: 600;
235
+ letter-spacing: -0.01em;
200
236
  }
201
237
 
202
238
  .category-count {
203
239
  margin-left: auto;
204
- padding: 0.2rem 0.6rem;
240
+ padding: 0.15rem 0.45rem;
205
241
  background: var(--bg-tertiary);
206
- border-radius: 4px;
207
- font-size: 0.7rem;
242
+ border: 1px solid var(--border-color);
243
+ border-radius: var(--radius-sm);
244
+ font-size: 0.6875rem;
245
+ font-variant-numeric: tabular-nums;
208
246
  color: var(--text-muted);
209
247
  }
210
248
 
@@ -217,15 +255,16 @@
217
255
  .report-link {
218
256
  display: flex;
219
257
  align-items: center;
258
+ gap: 0.75rem;
220
259
  justify-content: space-between;
221
- padding: 0.45rem 0.7rem;
260
+ padding: 0.4rem 0.6rem;
222
261
  background: transparent;
223
262
  border: 1px solid transparent;
224
- border-radius: 4px;
263
+ border-radius: var(--radius-sm);
225
264
  color: var(--text-secondary);
226
265
  text-decoration: none;
227
- font-size: 0.82rem;
228
- transition: background 0.1s, color 0.1s;
266
+ font-size: 0.8125rem;
267
+ transition: background-color 0.1s ease, color 0.1s ease;
229
268
  }
230
269
 
231
270
  .report-link:hover {
@@ -234,26 +273,29 @@
234
273
  }
235
274
 
236
275
  .report-link .arrow {
276
+ width: 0.8125rem;
277
+ height: 0.8125rem;
278
+ color: var(--text-muted);
237
279
  opacity: 0;
238
- transform: translateX(-4px);
239
- transition: all 0.15s ease;
280
+ transition: opacity 0.12s ease;
240
281
  }
241
282
 
242
- .report-link:hover .arrow {
283
+ .report-link:hover .arrow,
284
+ .report-link:focus-visible .arrow {
243
285
  opacity: 1;
244
- transform: translateX(0);
245
286
  }
246
287
 
247
288
  .report-badge-new {
248
289
  display: inline-block;
249
290
  margin-left: 0.4rem;
250
- padding: 0.1rem 0.35rem;
251
- font-size: 0.55rem;
291
+ padding: 0.05rem 0.3rem;
292
+ font-size: 0.5625rem;
252
293
  font-weight: 600;
253
- letter-spacing: 0.04em;
254
- line-height: 1.2;
294
+ letter-spacing: 0.06em;
295
+ line-height: 1.4;
255
296
  color: var(--accent-green);
256
- background: rgba(69, 168, 122, 0.12);
297
+ background: var(--green-soft);
298
+ border: 1px solid var(--green-line);
257
299
  border-radius: 3px;
258
300
  vertical-align: middle;
259
301
  }
@@ -269,6 +311,7 @@
269
311
  display: flex;
270
312
  align-items: center;
271
313
  gap: 0.4rem;
314
+ min-height: var(--control-h);
272
315
  font-size: 0.75rem;
273
316
  color: var(--text-muted);
274
317
  }
@@ -276,13 +319,41 @@
276
319
  .breadcrumb a {
277
320
  color: var(--text-secondary);
278
321
  text-decoration: none;
279
- transition: color 0.15s;
322
+ transition: color 0.12s ease, background-color 0.12s ease;
280
323
  }
281
324
 
282
325
  .breadcrumb a:hover {
283
326
  color: var(--text-primary);
284
327
  }
285
328
 
329
+ /* First crumb doubles as the back action — the arrow makes the affordance
330
+ explicit without a second control duplicating it in the header. */
331
+ .breadcrumb-back {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 0.35rem;
335
+ margin-left: -0.4rem;
336
+ padding: 0.2rem 0.45rem;
337
+ border-radius: var(--radius-sm);
338
+ }
339
+
340
+ .breadcrumb-back:hover {
341
+ background: var(--bg-tertiary);
342
+ }
343
+
344
+ .breadcrumb-back .icon {
345
+ width: 0.8125rem;
346
+ height: 0.8125rem;
347
+ }
348
+
349
+ .breadcrumb-sep {
350
+ color: var(--border-strong);
351
+ }
352
+
353
+ .breadcrumb-current {
354
+ color: var(--text-secondary);
355
+ }
356
+
286
357
  .report-header {
287
358
  display: flex;
288
359
  align-items: flex-start;
@@ -306,62 +377,163 @@
306
377
  gap: 0.5rem;
307
378
  }
308
379
 
380
+ /* Buttons — one solid, classic control. Height, radius and weight come from
381
+ the tokens so every variant and every page lines up on the same grid. */
309
382
  .btn {
310
383
  display: inline-flex;
311
384
  align-items: center;
312
385
  justify-content: center;
313
386
  gap: 0.4rem;
314
- height: 36px;
315
- padding: 0 1rem;
387
+ height: var(--control-h);
388
+ padding: 0 0.875rem;
316
389
  border: 1px solid transparent;
317
- border-radius: 4px;
390
+ border-radius: var(--radius-sm);
318
391
  font-family: inherit;
319
- font-size: 0.8rem;
392
+ font-size: 0.8125rem;
320
393
  font-weight: 500;
394
+ line-height: 1;
321
395
  cursor: pointer;
322
- transition: background 0.1s;
396
+ user-select: none;
323
397
  white-space: nowrap;
324
398
  text-decoration: none;
399
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
325
400
  }
326
401
 
327
402
  .btn-primary {
328
- background: rgba(124, 138, 246, 0.14);
329
- color: var(--accent-purple);
330
- border-color: rgba(124, 138, 246, 0.35);
403
+ background: var(--btn-primary-bg);
404
+ border-color: var(--btn-primary-bg);
405
+ color: #fff;
331
406
  }
332
407
 
333
408
  .btn-primary:hover {
334
- background: rgba(124, 138, 246, 0.24);
335
- border-color: rgba(124, 138, 246, 0.55);
409
+ background: var(--btn-primary-bg-hover);
410
+ border-color: var(--btn-primary-bg-hover);
411
+ }
412
+
413
+ .btn-primary:active {
414
+ background: var(--btn-primary-bg-active);
415
+ border-color: var(--btn-primary-bg-active);
416
+ }
417
+
418
+ .btn-secondary,
419
+ .btn-telegram {
420
+ background: var(--bg-tertiary);
421
+ color: var(--text-primary);
422
+ border-color: var(--border-color);
423
+ }
424
+
425
+ .btn-secondary:hover,
426
+ .btn-telegram:hover {
427
+ background: var(--bg-hover);
428
+ border-color: var(--border-strong);
336
429
  }
337
430
 
338
- .btn-secondary {
431
+ .btn-secondary:active,
432
+ .btn-telegram:active {
339
433
  background: var(--bg-tertiary);
434
+ }
435
+
436
+ .btn-ghost {
437
+ background: transparent;
340
438
  color: var(--text-secondary);
341
- border: 1px solid var(--border-color);
439
+ border-color: transparent;
342
440
  }
343
441
 
344
- .btn-secondary:hover {
345
- background: #2a2d31;
442
+ .btn-ghost:hover {
443
+ background: var(--bg-tertiary);
346
444
  color: var(--text-primary);
347
445
  }
348
446
 
349
- .btn-telegram {
447
+ .btn-danger {
448
+ background: var(--btn-danger-bg);
449
+ border-color: var(--btn-danger-bg);
450
+ color: #fff;
451
+ }
452
+
453
+ .btn-danger:hover {
454
+ background: var(--btn-danger-bg-hover);
455
+ border-color: var(--btn-danger-bg-hover);
456
+ }
457
+
458
+ .btn-danger:active {
459
+ background: var(--btn-danger-bg-active);
460
+ border-color: var(--btn-danger-bg-active);
461
+ }
462
+
463
+ /* Destructive, but not the page's main action: neutral until hovered. */
464
+ .btn-muted {
350
465
  background: var(--bg-tertiary);
466
+ border-color: var(--border-color);
351
467
  color: var(--text-secondary);
468
+ }
469
+
470
+ .btn-muted:hover {
471
+ background: var(--rose-soft);
472
+ border-color: var(--rose-line);
473
+ color: var(--accent-rose);
474
+ }
475
+
476
+ .btn-small {
477
+ height: var(--control-h-sm);
478
+ padding: 0 0.625rem;
479
+ font-size: 0.75rem;
480
+ }
481
+
482
+ /* Square control sized to match .btn — for a lone icon with no label. */
483
+ .btn-icon {
484
+ width: var(--control-h);
485
+ height: var(--control-h);
486
+ padding: 0;
487
+ flex: none;
488
+ display: inline-flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ background: var(--bg-tertiary);
352
492
  border: 1px solid var(--border-color);
493
+ border-radius: var(--radius-sm);
494
+ color: var(--text-secondary);
495
+ cursor: pointer;
496
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
353
497
  }
354
498
 
355
- .btn-telegram:hover {
356
- background: #2a2d31;
499
+ .btn-icon:hover {
500
+ background: var(--bg-hover);
501
+ border-color: var(--border-strong);
357
502
  color: var(--text-primary);
358
503
  }
359
504
 
360
- .btn:disabled {
361
- opacity: 0.5;
505
+ .btn-small.btn-icon {
506
+ width: var(--control-h-sm);
507
+ height: var(--control-h-sm);
508
+ }
509
+
510
+ .btn:disabled,
511
+ .btn-icon:disabled {
512
+ opacity: 0.45;
362
513
  cursor: not-allowed;
363
514
  }
364
515
 
516
+ .btn:disabled:hover,
517
+ .btn-icon:disabled:hover {
518
+ background: inherit;
519
+ border-color: inherit;
520
+ }
521
+
522
+ /* Icons — stroke-drawn, inherit the parent's colour and optical size. */
523
+ .icon {
524
+ width: 1em;
525
+ height: 1em;
526
+ flex: none;
527
+ fill: none;
528
+ stroke: currentColor;
529
+ stroke-width: 1.75;
530
+ stroke-linecap: round;
531
+ stroke-linejoin: round;
532
+ }
533
+
534
+ .icon-lg { width: 1.125rem; height: 1.125rem; }
535
+ .icon-sm { width: 0.875rem; height: 0.875rem; }
536
+
365
537
  /* Results Table */
366
538
  .results-container {
367
539
  background: var(--bg-card);
@@ -468,63 +640,177 @@
468
640
  }
469
641
 
470
642
  /* Error state */
643
+ .error-title {
644
+ font-weight: 600;
645
+ color: var(--accent-rose);
646
+ }
647
+
471
648
  .error-message {
472
- padding: 0.875rem;
473
- background: rgba(244, 63, 94, 0.1);
474
- border: 1px solid rgba(244, 63, 94, 0.3);
475
- border-radius: 6px;
649
+ padding: 0.875rem 1rem;
650
+ background: var(--rose-soft);
651
+ border: 1px solid var(--rose-line);
652
+ border-radius: var(--radius);
476
653
  color: var(--accent-rose);
477
- font-size: 0.85rem;
654
+ font-size: 0.8125rem;
655
+ line-height: 1.6;
656
+ }
657
+
658
+ .error-message .code-block {
659
+ color: var(--text-primary);
660
+ }
661
+
662
+ /* Vertical rhythm. The global `* { margin: 0 }` reset leaves prose
663
+ containers with no spacing of their own, and per-element margins drifted
664
+ into asymmetric gaps (1rem above a code block, 0 below). One rule per
665
+ container spaces any two siblings identically, whatever their tags. */
666
+ .modal-body > * + *,
667
+ .modal-body li > * + *,
668
+ .error-message > * + *,
669
+ .problem-modal-body > * + *,
670
+ .doc-block > * + *,
671
+ .explain-query-section > * + *,
672
+ .explain-params-section > * + *,
673
+ #explain-content > * + *,
674
+ #run-query-content > * + *,
675
+ .callout-body > * + * {
676
+ margin-top: var(--flow-gap);
677
+ }
678
+
679
+ .modal-body > ol > li + li {
680
+ margin-top: var(--flow-gap-lg);
478
681
  }
479
682
 
480
- /* Toast notification */
683
+ .modal-body > .modal-actions,
684
+ .modal-body > .migration-actions {
685
+ margin-top: var(--flow-gap-lg);
686
+ }
687
+
688
+ /* Code block — one definition for every fenced snippet in the UI. */
689
+ .code-block,
690
+ .modal-body pre {
691
+ display: block;
692
+ padding: 0.6875rem 0.875rem;
693
+ background: var(--bg-primary);
694
+ border: 1px solid var(--border-color);
695
+ border-radius: var(--radius-sm);
696
+ font-family: var(--font-mono);
697
+ font-size: 0.78rem;
698
+ line-height: 1.6;
699
+ color: var(--text-primary);
700
+ white-space: pre;
701
+ overflow-x: auto;
702
+ tab-size: 2;
703
+ }
704
+
705
+ .code-block-wrap {
706
+ white-space: pre-wrap;
707
+ word-break: break-word;
708
+ }
709
+
710
+ /* Callout — an inline notice inside a panel or modal. */
711
+ .callout {
712
+ display: flex;
713
+ align-items: flex-start;
714
+ gap: 0.5rem;
715
+ padding: 0.75rem 0.875rem;
716
+ border: 1px solid var(--border-color);
717
+ border-radius: var(--radius-sm);
718
+ margin-bottom: 1rem;
719
+ font-size: 0.8125rem;
720
+ line-height: 1.6;
721
+ }
722
+
723
+ .callout .icon {
724
+ margin-top: 0.22rem;
725
+ width: 0.875rem;
726
+ height: 0.875rem;
727
+ }
728
+
729
+ .callout p {
730
+ margin: 0.25rem 0 0;
731
+ color: var(--text-secondary);
732
+ }
733
+
734
+ .callout-warning {
735
+ background: var(--amber-soft);
736
+ border-color: var(--amber-line);
737
+ color: var(--accent-amber);
738
+ }
739
+
740
+ .callout-danger {
741
+ background: var(--rose-soft);
742
+ border-color: var(--rose-line);
743
+ color: var(--accent-rose);
744
+ }
745
+
746
+ /* Page-level flash sits above the header, like .db-error-banner. */
747
+ .page-notice {
748
+ margin-bottom: 1.25rem;
749
+ }
750
+
751
+ /* Toast notification — one definition for every page. */
481
752
  .toast {
482
753
  position: fixed;
483
754
  bottom: 1.5rem;
484
755
  right: 1.5rem;
485
- padding: 0.75rem 1rem;
486
- background: var(--bg-card);
487
- border: 1px solid var(--border-color);
488
- border-radius: 6px;
756
+ max-width: 26rem;
757
+ padding: 0.625rem 0.875rem;
758
+ background: var(--bg-tertiary);
759
+ border: 1px solid var(--border-strong);
760
+ border-radius: var(--radius);
489
761
  color: var(--text-primary);
490
- font-size: 0.85rem;
491
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
492
- transform: translateY(100px);
762
+ font-size: 0.8125rem;
763
+ box-shadow: var(--shadow-2);
764
+ transform: translateY(0.5rem);
493
765
  opacity: 0;
494
- transition: all 0.3s ease;
495
- z-index: 1000;
766
+ visibility: hidden;
767
+ transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
768
+ z-index: 2000;
496
769
  }
497
770
 
498
771
  .toast.show {
499
772
  transform: translateY(0);
500
773
  opacity: 1;
774
+ visibility: visible;
775
+ }
776
+
777
+ .toast.success,
778
+ .toast.toast-success {
779
+ border-color: var(--green-line);
780
+ background: var(--green-soft);
501
781
  }
502
782
 
503
- .toast.success {
504
- border-color: var(--accent-green);
783
+ .toast.error,
784
+ .toast.toast-error {
785
+ border-color: var(--rose-line);
786
+ background: var(--rose-soft);
505
787
  }
506
788
 
507
- .toast.error {
508
- border-color: var(--accent-rose);
789
+ .toast.warning,
790
+ .toast.toast-warning {
791
+ border-color: var(--amber-line);
792
+ background: var(--amber-soft);
509
793
  }
510
794
 
511
795
  /* Modal */
512
796
  .modal {
513
797
  position: fixed;
514
798
  inset: 0;
515
- background: rgba(0, 0, 0, 0.6);
799
+ background: rgba(10, 11, 12, 0.72);
516
800
  display: flex;
517
801
  align-items: center;
518
802
  justify-content: center;
803
+ padding: 1.5rem;
519
804
  z-index: 1000;
520
805
  }
521
806
 
522
807
  .modal-content {
523
808
  background: var(--bg-card);
524
- border: 1px solid var(--border-color);
525
- border-radius: 6px;
809
+ border: 1px solid var(--border-strong);
810
+ border-radius: var(--radius);
811
+ box-shadow: var(--shadow-3);
526
812
  max-width: 900px;
527
- width: 90%;
813
+ width: 100%;
528
814
  max-height: 90vh;
529
815
  overflow: auto;
530
816
  scrollbar-width: thin;
@@ -578,21 +864,25 @@
578
864
  }
579
865
 
580
866
  .modal-close {
581
- width: 28px;
582
- height: 28px;
583
- background: var(--bg-tertiary);
584
- border: 1px solid var(--border-color);
585
- border-radius: 4px;
867
+ width: 1.75rem;
868
+ height: 1.75rem;
869
+ display: inline-flex;
870
+ align-items: center;
871
+ justify-content: center;
872
+ background: transparent;
873
+ border: 1px solid transparent;
874
+ border-radius: var(--radius-sm);
586
875
  color: var(--text-muted);
587
876
  font-size: 1.125rem;
877
+ line-height: 1;
588
878
  cursor: pointer;
589
- transition: all 0.15s;
879
+ transition: background-color 0.12s ease, color 0.12s ease;
590
880
  }
591
881
 
592
882
  .modal-close:hover {
593
- background: var(--accent-rose);
594
- border-color: var(--accent-rose);
595
- color: white;
883
+ background: var(--bg-tertiary);
884
+ border-color: var(--border-color);
885
+ color: var(--text-primary);
596
886
  }
597
887
 
598
888
  .modal-body {
@@ -611,18 +901,66 @@
611
901
  align-items: flex-start;
612
902
  }
613
903
 
904
+ .header-actions {
905
+ width: 100%;
906
+ flex-wrap: wrap;
907
+ }
908
+
614
909
  .report-header {
615
910
  flex-direction: column;
911
+ gap: 0.75rem;
616
912
  }
617
913
 
618
914
  .report-actions {
619
915
  width: 100%;
916
+ flex-wrap: wrap;
620
917
  }
621
918
 
622
- .btn {
919
+ /* Only action bars stretch their buttons — elsewhere a stretched button
920
+ overruns the label sitting next to it. */
921
+ .header-actions .btn,
922
+ .report-actions .btn,
923
+ .modal-actions .btn {
623
924
  flex: 1;
624
925
  justify-content: center;
625
926
  }
927
+
928
+ /* The selectors take a line of their own instead of squeezing the trail. */
929
+ .breadcrumb {
930
+ flex-wrap: wrap;
931
+ row-gap: 0.5rem;
932
+ }
933
+
934
+ .breadcrumb-spacer {
935
+ flex-basis: 100%;
936
+ }
937
+
938
+ .results-header {
939
+ flex-direction: column;
940
+ align-items: flex-start;
941
+ gap: 0.5rem;
942
+ }
943
+
944
+ .results-meta {
945
+ flex-wrap: wrap;
946
+ gap: 0.25rem 0.75rem;
947
+ }
948
+
949
+ .modal {
950
+ padding: 0.75rem;
951
+ }
952
+
953
+ .modal-large {
954
+ width: 100%;
955
+ }
956
+
957
+ /* Placed by `right`, a toast wider than the screen hangs off the left. */
958
+ .toast {
959
+ left: 0.875rem;
960
+ right: 0.875rem;
961
+ bottom: 0.875rem;
962
+ max-width: none;
963
+ }
626
964
  }
627
965
 
628
966
  /* ============================================
@@ -677,7 +1015,7 @@
677
1015
  color: var(--text-secondary);
678
1016
  border: 1px solid var(--border-color);
679
1017
  border-radius: 6px;
680
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
1018
+ box-shadow: var(--shadow-2);
681
1019
  font-size: 0.75rem;
682
1020
  font-weight: 400;
683
1021
  line-height: 1.5;
@@ -797,7 +1135,7 @@
797
1135
  }
798
1136
 
799
1137
  .query-item:hover {
800
- border-color: var(--accent-purple);
1138
+ border-color: var(--border-strong);
801
1139
  }
802
1140
 
803
1141
  .query-item:last-child {
@@ -917,7 +1255,7 @@
917
1255
  background: var(--bg-card);
918
1256
  border: 1px solid var(--border-color);
919
1257
  border-radius: 6px;
920
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1258
+ box-shadow: var(--shadow-2);
921
1259
  z-index: 1000;
922
1260
  min-width: 160px;
923
1261
  overflow: hidden;
@@ -961,7 +1299,7 @@
961
1299
  background: var(--bg-card);
962
1300
  border: 1px solid var(--border-color);
963
1301
  border-radius: 6px;
964
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1302
+ box-shadow: var(--shadow-2);
965
1303
  z-index: 100;
966
1304
  overflow: hidden;
967
1305
  }
@@ -985,24 +1323,6 @@
985
1323
  color: var(--text-primary);
986
1324
  }
987
1325
 
988
- .btn-small {
989
- height: 30px;
990
- padding: 0 0.875rem;
991
- font-size: 0.75rem;
992
- line-height: 1;
993
- }
994
-
995
- .btn-danger {
996
- background: var(--accent-rose);
997
- color: white;
998
- border-color: var(--accent-rose);
999
- }
1000
-
1001
- .btn-danger:hover {
1002
- opacity: 0.9;
1003
- transform: translateY(-1px);
1004
- }
1005
-
1006
1326
  /* IDE Settings Modal */
1007
1327
  .settings-label {
1008
1328
  color: var(--text-secondary);
@@ -1029,13 +1349,13 @@
1029
1349
  }
1030
1350
 
1031
1351
  .ide-option:hover {
1032
- background: var(--bg-secondary);
1033
- border-color: var(--accent-purple);
1352
+ background: var(--bg-hover);
1353
+ border-color: var(--border-strong);
1034
1354
  }
1035
1355
 
1036
1356
  .ide-option:has(input:checked) {
1037
- background: rgba(157, 140, 214, 0.15);
1038
- border-color: var(--accent-purple);
1357
+ background: var(--purple-soft);
1358
+ border-color: var(--purple-line);
1039
1359
  }
1040
1360
 
1041
1361
  .ide-option input[type="radio"] {
@@ -1081,12 +1401,14 @@
1081
1401
  }
1082
1402
 
1083
1403
  .db-selector {
1084
- background: var(--bg-card, rgba(255, 255, 255, 0.04));
1404
+ background: var(--bg-tertiary);
1085
1405
  color: var(--text-primary);
1086
- border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
1087
- border-radius: 6px;
1088
- padding: 0.375rem 0.625rem;
1089
- font-size: 0.875rem;
1406
+ border: 1px solid var(--border-color);
1407
+ border-radius: var(--radius-sm);
1408
+ height: var(--control-h-sm);
1409
+ padding: 0 0.5rem;
1410
+ font-family: inherit;
1411
+ font-size: 0.8125rem;
1090
1412
  cursor: pointer;
1091
1413
  width: auto;
1092
1414
  max-width: 100%;
@@ -1098,13 +1420,12 @@
1098
1420
  }
1099
1421
 
1100
1422
  .db-selector:hover {
1101
- border-color: var(--accent-blue, #6b9fe8);
1423
+ border-color: var(--border-strong);
1102
1424
  }
1103
1425
 
1104
- .db-selector:focus {
1105
- outline: none;
1106
- border-color: var(--accent-blue, #6b9fe8);
1107
- box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
1426
+ .db-selector:focus-visible {
1427
+ outline: 2px solid var(--accent-blue);
1428
+ outline-offset: 1px;
1108
1429
  }
1109
1430
 
1110
1431
  .db-selector-static {
@@ -1124,11 +1445,11 @@
1124
1445
  align-items: center;
1125
1446
  gap: 0.375rem;
1126
1447
  padding: 0.375rem 0.625rem;
1127
- background: rgba(232, 107, 107, 0.1);
1128
- border: 1px solid rgba(232, 107, 107, 0.3);
1129
- border-radius: 6px;
1448
+ background: var(--rose-soft);
1449
+ border: 1px solid var(--rose-line);
1450
+ border-radius: var(--radius-sm);
1130
1451
  font-size: 0.8125rem;
1131
- color: var(--accent-red, #e86b6b);
1452
+ color: var(--accent-red);
1132
1453
  cursor: help;
1133
1454
  }
1134
1455
 
@@ -1136,47 +1457,123 @@
1136
1457
  flex: 1;
1137
1458
  }
1138
1459
 
1139
- .breadcrumb {
1140
- display: flex;
1141
- align-items: center;
1142
- gap: 0.5rem;
1143
- }
1144
-
1145
1460
  .db-error-banner {
1146
- background: rgba(232, 107, 107, 0.08);
1147
- border: 1px solid rgba(232, 107, 107, 0.3);
1148
- border-radius: 8px;
1149
- padding: 0.875rem 1.125rem;
1461
+ background: var(--rose-soft);
1462
+ border: 1px solid var(--rose-line);
1463
+ border-radius: var(--radius);
1464
+ padding: 0.875rem 1rem;
1150
1465
  margin-bottom: 1rem;
1151
1466
  display: flex;
1152
1467
  flex-direction: column;
1153
- gap: 0.375rem;
1468
+ align-items: flex-start;
1469
+ gap: var(--flow-gap);
1154
1470
  }
1155
1471
 
1156
1472
  .db-error-banner strong {
1157
- color: var(--accent-red, #e86b6b);
1158
- font-size: 0.9375rem;
1473
+ color: var(--accent-red);
1474
+ font-weight: 600;
1159
1475
  }
1160
1476
 
1161
1477
  .db-error-banner span {
1162
- font-size: 0.875rem;
1163
1478
  color: var(--text-secondary);
1164
1479
  }
1165
1480
 
1166
1481
  .db-error-banner code {
1167
- font-family: ui-monospace, SFMono-Regular, monospace;
1168
- font-size: 0.8125rem;
1169
- background: rgba(0, 0, 0, 0.25);
1170
- padding: 0.375rem 0.625rem;
1171
- border-radius: 4px;
1482
+ font-family: var(--font-mono);
1483
+ font-size: 0.78rem;
1484
+ line-height: 1.6;
1485
+ background: var(--bg-primary);
1486
+ border: 1px solid var(--border-color);
1487
+ padding: 0.5rem 0.75rem;
1488
+ border-radius: var(--radius-sm);
1172
1489
  color: var(--text-primary);
1173
- display: inline-block;
1174
- width: fit-content;
1490
+ max-width: 100%;
1491
+ overflow-x: auto;
1492
+ }
1493
+
1494
+ /* Responsive, for the panels defined above this point. */
1495
+ @media (max-width: 768px) {
1496
+ .query-monitoring-header {
1497
+ flex-direction: column;
1498
+ align-items: flex-start;
1499
+ gap: 0.75rem;
1500
+ }
1501
+
1502
+ .query-monitoring-title {
1503
+ flex-wrap: wrap;
1504
+ }
1505
+
1506
+ .query-monitoring-controls {
1507
+ width: 100%;
1508
+ flex-wrap: wrap;
1509
+ }
1510
+
1511
+ /* Anchored to its chip, this tooltip runs off the screen and stretches
1512
+ the page with it; on a phone it reads as a bottom sheet instead. */
1513
+ .query-monitoring-scope::after {
1514
+ position: fixed;
1515
+ inset: auto 0.875rem 0.875rem;
1516
+ width: auto;
1517
+ max-width: none;
1518
+ }
1175
1519
  }
1176
1520
  </style>
1177
1521
  </head>
1178
1522
  <body>
1523
+ <%# Icon sprite. One definition per glyph, referenced with
1524
+ <svg class="icon"><use href="#i-name"></use></svg>. Stroke-drawn and
1525
+ colourless so every icon inherits its context's colour and size. %>
1526
+ <svg width="0" height="0" aria-hidden="true" focusable="false" style="position:absolute">
1527
+ <defs>
1528
+ <symbol id="i-queries" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z"/></symbol>
1529
+ <symbol id="i-indexes" viewBox="0 0 24 24"><path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/></symbol>
1530
+ <symbol id="i-tables" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9.5h18M9 9.5V20"/></symbol>
1531
+ <symbol id="i-connections" viewBox="0 0 24 24"><path d="M9 2v6M15 2v6M6 8h12v4a6 6 0 0 1-12 0V8ZM12 18v4"/></symbol>
1532
+ <symbol id="i-system" viewBox="0 0 24 24"><rect x="2.5" y="4" width="19" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></symbol>
1533
+ <symbol id="i-schema" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-4.2-4.2"/></symbol>
1534
+ <symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7L11.8 5.2"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></symbol>
1535
+ <symbol id="i-activity" viewBox="0 0 24 24"><path d="M3 12h4l3 8 4-16 3 8h4"/></symbol>
1536
+ <symbol id="i-database" viewBox="0 0 24 24"><ellipse cx="12" cy="5.5" rx="8" ry="3.5"/><path d="M4 5.5v13c0 1.9 3.6 3.5 8 3.5s8-1.6 8-3.5v-13M4 12c0 1.9 3.6 3.5 8 3.5s8-1.6 8-3.5"/></symbol>
1537
+ <symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5.2l3.2 2"/></symbol>
1538
+ <symbol id="i-lock" viewBox="0 0 24 24"><rect x="4" y="10.5" width="16" height="10.5" rx="2"/><path d="M8 10.5V7a4 4 0 0 1 8 0v3.5"/></symbol>
1539
+ <symbol id="i-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 14.6a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.1A1.7 1.7 0 0 0 8.9 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.7 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34H9.1A1.7 1.7 0 0 0 10.13 3V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v.08A1.7 1.7 0 0 0 21 10.13H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1.03Z"/></symbol>
1540
+ <symbol id="i-check" viewBox="0 0 24 24"><path d="m4.5 12.5 5 5 10-11"/></symbol>
1541
+ <symbol id="i-alert" viewBox="0 0 24 24"><path d="M12 3.5 2.8 19.5h18.4L12 3.5Z"/><path d="M12 9.5v4.2M12 17h.01"/></symbol>
1542
+ <symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.8h.01"/></symbol>
1543
+ <symbol id="i-play" viewBox="0 0 24 24"><path d="M7 4.5v15l13-7.5-13-7.5Z"/></symbol>
1544
+ <symbol id="i-pause" viewBox="0 0 24 24"><path d="M9 4.5v15M15 4.5v15"/></symbol>
1545
+ <symbol id="i-chevron-right" viewBox="0 0 24 24"><path d="m9.5 5.5 6.5 6.5-6.5 6.5"/></symbol>
1546
+ <symbol id="i-arrow-left" viewBox="0 0 24 24"><path d="M20 12H4.5M10.5 5.5 4 12l6.5 6.5"/></symbol>
1547
+ <symbol id="i-download" viewBox="0 0 24 24"><path d="M12 3.5v12M7 11l5 5 5-5M4 20h16"/></symbol>
1548
+ <symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M15 6V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1"/></symbol>
1549
+ <symbol id="i-send" viewBox="0 0 24 24"><path d="M21 3 3 10.5l7 3 3 7L21 3Z"/></symbol>
1550
+ <symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 6.5h16M9.5 6.5V4h5v2.5M6.5 6.5 7.4 20a1 1 0 0 0 1 1h7.2a1 1 0 0 0 1-1l.9-13.5"/></symbol>
1551
+ <symbol id="i-pin" viewBox="0 0 24 24"><path d="M9 3.5h6l-.8 5.2 3.3 3.3H6.5l3.3-3.3L9 3.5ZM12 12v8.5"/></symbol>
1552
+ <symbol id="i-file" viewBox="0 0 24 24"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M14 3v5h5"/></symbol>
1553
+ <symbol id="i-book" viewBox="0 0 24 24"><path d="M4 4.5A1.5 1.5 0 0 1 5.5 3H19v15H5.5A1.5 1.5 0 0 0 4 19.5v-15ZM4 19.5A1.5 1.5 0 0 0 5.5 21H19v-3"/></symbol>
1554
+ <symbol id="i-external" viewBox="0 0 24 24"><path d="M14 4h6v6M20 4l-8.5 8.5"/><path d="M18 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5"/></symbol>
1555
+ <symbol id="i-bulb" viewBox="0 0 24 24"><path d="M9.5 18h5M10 21h4"/><path d="M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.3 1 2.1h5c0-.8.4-1.6 1-2.1A6 6 0 0 0 12 3Z"/></symbol>
1556
+ <symbol id="i-question" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.4 2.3c-.6.2-.9.8-.9 1.4v.6M12 16.8h.01"/></symbol>
1557
+ <symbol id="i-sliders" viewBox="0 0 24 24"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></symbol>
1558
+ </defs>
1559
+ </svg>
1560
+
1179
1561
  <div class="container">
1562
+ <%# Redirects out of an unavailable report set flash[:alert]; without this
1563
+ the user would just land back on the dashboard with no explanation. %>
1564
+ <% if flash[:alert].present? %>
1565
+ <div class="callout callout-warning page-notice" role="alert">
1566
+ <svg class="icon" aria-hidden="true"><use href="#i-alert"></use></svg>
1567
+ <span><%= flash[:alert] %></span>
1568
+ </div>
1569
+ <% end %>
1570
+ <% if flash[:notice].present? %>
1571
+ <div class="callout page-notice" role="status">
1572
+ <svg class="icon" aria-hidden="true"><use href="#i-info"></use></svg>
1573
+ <span><%= flash[:notice] %></span>
1574
+ </div>
1575
+ <% end %>
1576
+
1180
1577
  <%= yield %>
1181
1578
  </div>
1182
1579
 
@@ -1219,6 +1616,24 @@
1219
1616
  }
1220
1617
  });
1221
1618
 
1619
+ // The "feature is disabled, here is how to enable it" panel. Four callers
1620
+ // had drifted into hand-spaced copies of this markup (<br><br>, &nbsp;
1621
+ // indentation, inline styles); they all render through here now.
1622
+ window.pgReportsDisabledNotice = function(title) {
1623
+ const i18n = PG_REPORTS_I18N.modals;
1624
+ const esc = (t) => String(t == null ? '' : t)
1625
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1626
+ return [
1627
+ '<div class="error-message">',
1628
+ '<p class="error-title">' + esc(title) + '</p>',
1629
+ '<p>' + esc(i18n.query_execution_disabled_intro) + '</p>',
1630
+ '<pre class="code-block">PgReports.configure do |config|\n config.allow_raw_query_execution = true\nend</pre>',
1631
+ '<p>' + esc(i18n.query_execution_disabled_env_note) + '</p>',
1632
+ '<pre class="code-block">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</pre>',
1633
+ '</div>'
1634
+ ].join('');
1635
+ };
1636
+
1222
1637
  function showToast(message, type = 'success') {
1223
1638
  const toast = document.getElementById('toast');
1224
1639
  toast.textContent = message;