upright 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +2 -3
  3. data/README.md +166 -60
  4. data/app/assets/stylesheets/upright/dashboard.css +65 -193
  5. data/app/assets/stylesheets/upright/tables.css +60 -0
  6. data/app/assets/stylesheets/upright/uptime-bars.css +13 -50
  7. data/app/controllers/upright/alertmanager_proxy_controller.rb +1 -1
  8. data/app/controllers/upright/dashboards/probe_statuses_controller.rb +7 -0
  9. data/app/controllers/upright/probe_results_controller.rb +1 -0
  10. data/app/controllers/upright/sessions_controller.rb +1 -0
  11. data/app/helpers/upright/application_helper.rb +10 -0
  12. data/app/helpers/upright/dashboards_helper.rb +12 -0
  13. data/app/helpers/upright/probe_results_helper.rb +3 -10
  14. data/app/javascript/upright/controllers/auto_refresh_controller.js +16 -0
  15. data/app/models/concerns/upright/playwright/form_authentication.rb +0 -3
  16. data/app/models/concerns/upright/playwright/lifecycle.rb +3 -2
  17. data/app/models/concerns/upright/probe_result/stale_cleanup.rb +23 -0
  18. data/app/models/concerns/upright/probeable.rb +7 -1
  19. data/app/models/upright/http/request.rb +1 -1
  20. data/app/models/upright/playwright/storage_state.rb +12 -3
  21. data/app/models/upright/probe_result.rb +6 -1
  22. data/app/models/upright/probes/http_probe.rb +6 -2
  23. data/app/models/upright/probes/status/probe.rb +24 -0
  24. data/app/models/upright/probes/status/site_status.rb +71 -0
  25. data/app/models/upright/probes/status.rb +54 -0
  26. data/app/models/upright/probes/uptime.rb +4 -4
  27. data/app/models/upright/traceroute/ip_metadata_lookup.rb +1 -2
  28. data/app/views/layouts/upright/_header.html.erb +2 -1
  29. data/app/views/upright/dashboards/_uptime_bars.html.erb +2 -2
  30. data/app/views/upright/dashboards/_uptime_probe_row.html.erb +7 -5
  31. data/app/views/upright/dashboards/probe_statuses/_matrix.html.erb +48 -0
  32. data/app/views/upright/dashboards/probe_statuses/show.html.erb +17 -0
  33. data/app/views/upright/dashboards/uptimes/show.html.erb +1 -1
  34. data/app/views/upright/probe_results/index.html.erb +7 -4
  35. data/app/views/upright/sites/index.html.erb +1 -1
  36. data/config/ci.rb +2 -0
  37. data/config/credentials/development.key +1 -0
  38. data/config/credentials/test.key +1 -0
  39. data/config/routes.rb +1 -0
  40. data/lib/generators/upright/install/install_generator.rb +52 -2
  41. data/lib/generators/upright/install/templates/http_probes.yml +1 -0
  42. data/lib/generators/upright/install/templates/recurring.yml +25 -0
  43. data/lib/generators/upright/install/templates/smtp_probes.yml +3 -0
  44. data/lib/generators/upright/install/templates/traceroute_probes.yml +12 -0
  45. data/lib/generators/upright/install/templates/upright.rb +4 -1
  46. data/lib/generators/upright/install/templates/upright.rules.yml +5 -5
  47. data/lib/upright/configuration.rb +14 -0
  48. data/lib/upright/engine.rb +7 -0
  49. data/lib/upright/geohash.rb +46 -0
  50. data/lib/upright/metrics.rb +2 -2
  51. data/lib/upright/probe_type_registry.rb +33 -0
  52. data/lib/upright/site.rb +1 -3
  53. data/lib/upright/version.rb +1 -1
  54. data/lib/upright.rb +6 -1
  55. metadata +17 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ec7e02ebd4e740ce3d54364ad34d3d0e65df4ad2f6801ffb2a8ebbbf9af12cf
4
- data.tar.gz: 898bccbce30edeb9ab0f38e92e49b894ea197aace8e824bb3f0164e6c050e91b
3
+ metadata.gz: ccb561e794bdafa5e31caed2347f7d371d02e5230ca875c5f507d1d0620d7363
4
+ data.tar.gz: 3bc20100e850cdd93a81e58b7e9b6f3ef9377c44b17bbd7891dbe697699b23a6
5
5
  SHA512:
6
- metadata.gz: ed90551122e4fba67b74cae406d70ab58b4e419cffab41df84e816fbe4b53aeba8a1805a0c6f7949fd5fbc232589ca812b458056cd24098f38884d0d50acf364
7
- data.tar.gz: 68df1174db51f31b0a7e4d466fc9d451ca3f81151fe1c4048e1d5ea0cb2859f1689e875edaa02a7e03b010902efbc9e895b37ac8a3c78d7cb0bdfcd649257346
6
+ metadata.gz: 474970b3ccbce05a396ceb2a3575a58272b602d679cf70d55d10538cc546aa31d6142e1de401ee5098c6c3a2733a624cba54e34b79a0dcd9c6af7ad7e8124f0d
7
+ data.tar.gz: 35ed0971379b1c50dd9583f95ed27aa2586f53002e8acd81c05851797d6f2d1c59ced8d6a08817613ecb89ab376d453a9697cf241b82be1409c11f1caa87c6d3
data/LICENSE.md CHANGED
@@ -1,10 +1,9 @@
1
- # O'Saasy License Agreement
1
+ # MIT License
2
2
 
3
3
  Copyright © 2026, 37signals LLC.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
7
- 1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
- 2. No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9
8
 
10
9
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  Upright is a self-hosted synthetic monitoring system. It provides a framework for running health check probes from multiple geographic sites and reporting metrics via Prometheus. Alerts can then be configured with AlertManager.
4
4
 
5
+ <table>
6
+ <tr>
7
+ <td><img src="docs/screenshots/dashboard.png" alt="Dashboard" width="100%"></td>
8
+ <td><img src="docs/screenshots/uptime.png" alt="Uptime" width="100%"></td>
9
+ </tr>
10
+ <tr>
11
+ <td><em>Site overview with world map</em></td>
12
+ <td><em>30-day uptime history</em></td>
13
+ </tr>
14
+ <tr>
15
+ <td colspan="2"><img src="docs/screenshots/probe-status.png" alt="Probe status" width="100%"></td>
16
+ </tr>
17
+ <tr>
18
+ <td colspan="2"><em>Probe status across all sites</em></td>
19
+ </tr>
20
+ </table>
21
+
5
22
  ## Features
6
23
 
7
24
  - **Playwright Probes** - Browser-based probes for user flows with video recording and logs
@@ -14,7 +31,6 @@ Upright is a self-hosted synthetic monitoring system. It provides a framework fo
14
31
 
15
32
  ### Not Included
16
33
 
17
- - **Dashboards** - Instead, Grafana is suggested for monitoring the Prometheus metrics generated here
18
34
  - **Notifications** - Instead, Alertmanager is included for alerting and notifications
19
35
  - **Hosting** - Instead, you can use a VPS from DigitalOcean, Hetzner, etc.
20
36
 
@@ -31,7 +47,8 @@ Upright is a self-hosted synthetic monitoring system. It provides a framework fo
31
47
 
32
48
  ## Installation
33
49
 
34
- Upright is designed to be run in it's own Rails app and deployed with Kamal.
50
+ > [!NOTE]
51
+ > Upright is designed to be run in its own Rails app and deployed with Kamal.
35
52
 
36
53
  ### Quick Start (New Project)
37
54
 
@@ -40,9 +57,9 @@ Create a new Rails application and install Upright:
40
57
  ```bash
41
58
  rails new my-upright --database=sqlite3 --skip-test
42
59
  cd my-upright
43
- bundle add upright --github=basecamp/upright
60
+ bundle add upright
44
61
  bin/rails generate upright:install
45
- bin/rails db:setup
62
+ bin/rails db:migrate
46
63
  ```
47
64
 
48
65
  Start the server:
@@ -141,6 +158,91 @@ Upright.configure do |config|
141
158
  end
142
159
  ```
143
160
 
161
+ ### Probe Result Cleanup
162
+
163
+ Upright automatically cleans up old probe results on a recurring schedule. You can configure the retention thresholds:
164
+
165
+ ```ruby
166
+ Upright.configure do |config|
167
+ config.stale_success_threshold = 24.hours # Delete successful results older than this (default: 24 hours)
168
+ config.stale_failure_threshold = 30.days # Delete failed results older than this (default: 30 days)
169
+ config.failure_retention_limit = 20_000 # Keep at most this many failed results (default: 20,000)
170
+ end
171
+ ```
172
+
173
+ ## Custom Probe Types
174
+
175
+ Upright ships with four built-in probe types: HTTP, Playwright, SMTP, and Traceroute. You can register your own to extend the system.
176
+
177
+ ### 1. Register the type
178
+
179
+ Add it to your initializer so Upright knows about its name and icon:
180
+
181
+ ```ruby
182
+ # config/initializers/upright.rb
183
+ Upright.configure do |config|
184
+ config.probe_types.register :ping, name: "Ping", icon: "📶"
185
+ end
186
+ ```
187
+
188
+ ### 2. Create the probe class
189
+
190
+ Add a Ruby class in your `probes/` directory that extends `FrozenRecord::Base` and includes `Upright::Probeable` and `Upright::ProbeYamlSource`. Implement `probe_type`, `probe_target`, `check`, and `on_check_recorded`:
191
+
192
+ ```ruby
193
+ # probes/ping_probe.rb
194
+ class PingProbe < FrozenRecord::Base
195
+ include Upright::Probeable
196
+ include Upright::ProbeYamlSource
197
+
198
+ stagger_by_site 3.seconds
199
+
200
+ def check
201
+ @ping_output, status = Open3.capture2e("ping", "-c", "1", "-W", "5", host)
202
+ status.success?
203
+ end
204
+
205
+ def on_check_recorded(probe_result)
206
+ if @ping_output.present?
207
+ Upright::Artifact.new(name: "ping.log", content: @ping_output).attach_to(probe_result)
208
+ end
209
+ end
210
+
211
+ def probe_type = "ping"
212
+ def probe_target = host
213
+ end
214
+ ```
215
+
216
+ The `check` method should return a truthy value on success and a falsy value on failure.
217
+
218
+ ### 3. Define probes in YAML
219
+
220
+ Create a YAML file matching the class name (e.g., `PingProbe` → `probes/ping_probes.yml`):
221
+
222
+ ```yaml
223
+ # probes/ping_probes.yml
224
+ - name: "Cloudflare DNS"
225
+ host: "1.1.1.1"
226
+
227
+ - name: "Google DNS"
228
+ host: "8.8.8.8"
229
+ ```
230
+
231
+ Fields defined in the YAML are available as methods on the probe instance (e.g., `host`).
232
+
233
+ ### 4. Schedule it
234
+
235
+ Add a recurring job in `config/recurring.yml`:
236
+
237
+ ```yaml
238
+ production:
239
+ ping_probes:
240
+ command: "PingProbe.check_and_record_all_later"
241
+ schedule: every 30 seconds
242
+ ```
243
+
244
+ The custom type will automatically appear in all dashboard dropdowns and filter links.
245
+
144
246
  ## Defining Probes
145
247
 
146
248
  ### HTTP Probes
@@ -155,12 +257,15 @@ Add probes to `probes/http_probes.yml`:
155
257
  - name: API Health
156
258
  url: https://api.example.com/health
157
259
  expected_status: 200
260
+ alert_severity: critical
158
261
 
159
262
  - name: Admin Panel
160
263
  url: https://admin.example.com
161
264
  basic_auth_credentials: admin_auth # Key in Rails credentials
162
265
  ```
163
266
 
267
+ The optional `alert_severity` field controls the Prometheus alert severity when a probe fails. Values: `medium`, `high` (default), `critical`.
268
+
164
269
  ### SMTP Probes
165
270
 
166
271
  Add probes to `probes/smtp_probes.yml`:
@@ -227,18 +332,15 @@ Configure probe scheduling with Solid Queue in `config/recurring.yml`:
227
332
  ```yaml
228
333
  production:
229
334
  http_probes:
230
- class: Upright::ProbeCheckJob
231
- args: [http]
335
+ command: "Upright::Probes::HTTPProbe.check_and_record_all_later"
232
336
  schedule: every 30 seconds
233
337
 
234
338
  smtp_probes:
235
- class: Upright::ProbeCheckJob
236
- args: [smtp]
339
+ command: "Upright::Probes::SMTPProbe.check_and_record_all_later"
237
340
  schedule: every 30 seconds
238
341
 
239
342
  my_service_auth:
240
- class: Upright::ProbeCheckJob
241
- args: [playwright, MyServiceAuth]
343
+ command: "Probes::Playwright::MyServiceAuthProbe.check_and_record_later"
242
344
  schedule: every 15 minutes
243
345
  ```
244
346
 
@@ -304,70 +406,82 @@ image: your-org/upright
304
406
  servers:
305
407
  web:
306
408
  hosts:
307
- - nyc.upright.example.com
308
- env:
309
- tags:
310
- SITE_SUBDOMAIN: nyc
311
-
312
- hosts:
313
- - ams.upright.example.com
314
- env:
315
- tags:
316
- SITE_SUBDOMAIN: ams
317
-
409
+ - ams.upright.example.com: [amsterdam]
410
+ - nyc.upright.example.com: [new_york]
411
+ - sfo.upright.example.com: [san_francisco]
412
+ jobs:
318
413
  hosts:
319
- - sfo.upright.example.com
320
- env:
321
- tags:
322
- SITE_SUBDOMAIN: sfo
414
+ - ams.upright.example.com: [amsterdam]
415
+ - nyc.upright.example.com: [new_york]
416
+ - sfo.upright.example.com: [san_francisco]
417
+ cmd: bin/jobs
323
418
 
324
- registry:
325
- server: ghcr.io
326
- username: your-org
327
- password:
328
- - KAMAL_REGISTRY_PASSWORD
419
+ proxy:
420
+ app_port: 3000
421
+ ssl: true
422
+ hosts:
423
+ - "*.upright.example.com"
329
424
 
330
425
  env:
331
- clear:
332
- RAILS_ENV: production
333
- RAILS_LOG_TO_STDOUT: true
334
426
  secret:
335
427
  - RAILS_MASTER_KEY
336
- - OIDC_CLIENT_ID
337
- - OIDC_CLIENT_SECRET
428
+ tags:
429
+ amsterdam:
430
+ SITE_SUBDOMAIN: ams
431
+ new_york:
432
+ SITE_SUBDOMAIN: nyc
433
+ san_francisco:
434
+ SITE_SUBDOMAIN: sfo
338
435
 
339
436
  accessories:
340
437
  playwright:
341
- image: mcr.microsoft.com/playwright:v1.55.0-noble
342
- cmd: npx -y playwright run-server --port 53333
343
- host: nyc.upright.example.com
344
- port: 53333
345
- env:
346
- clear:
347
- DEBUG: pw:api
438
+ image: jacoblincool/playwright:chromium-server-1.55.0
439
+ port: "127.0.0.1:53333:53333"
440
+ roles:
441
+ - jobs
442
+
443
+ prometheus:
444
+ image: prom/prometheus:v3.2.1
445
+ hosts:
446
+ - ams.upright.example.com
447
+ cmd: >-
448
+ --config.file=/etc/prometheus/prometheus.yml
449
+ --storage.tsdb.path=/prometheus
450
+ --storage.tsdb.retention.time=30d
451
+ --web.enable-otlp-receiver
452
+ files:
453
+ - config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
454
+ - config/prometheus/rules/upright.rules.yml:/etc/prometheus/rules/upright.rules.yml
455
+
456
+ alertmanager:
457
+ image: prom/alertmanager:v0.28.1
458
+ hosts:
459
+ - ams.upright.example.com
460
+ cmd: --config.file=/etc/alertmanager/alertmanager.yml
461
+ files:
462
+ - config/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
348
463
  ```
349
464
 
350
465
  ## Observability
351
466
 
352
467
  ### Prometheus
353
468
 
354
- The engine exposes metrics at `/metrics`. Configure Prometheus to scrape:
469
+ Metrics are exposed via a Puma plugin at `http://0.0.0.0:9394/metrics`. Configure Prometheus to scrape:
355
470
 
356
471
  ```yaml
357
472
  scrape_configs:
358
473
  - job_name: upright
359
474
  static_configs:
360
- - targets: ['localhost:3000']
361
- metrics_path: /metrics
475
+ - targets: ['localhost:9394']
362
476
  ```
363
477
 
364
478
  ### Metrics Exposed
365
479
 
366
480
  - `upright_probe_duration_seconds` - Probe execution duration
367
- - `upright_probe_success` - Probe success/failure (1/0)
368
- - `upright_probe_status_code` - HTTP status code for HTTP probes
481
+ - `upright_probe_up` - Probe status (1 = up, 0 = down)
482
+ - `upright_http_response_status` - HTTP response status code
369
483
 
370
- Labels include: `probe_name`, `probe_type`, `site_code`, `site_city`, `site_country`
484
+ Labels include: `type`, `name`, `site_code`, `site_city`, `site_country`
371
485
 
372
486
  ### AlertManager
373
487
 
@@ -378,20 +492,12 @@ groups:
378
492
  - name: upright
379
493
  rules:
380
494
  - alert: ProbeDown
381
- expr: upright_probe_success == 0
382
- for: 5m
383
- labels:
384
- severity: critical
385
- annotations:
386
- summary: "Probe {{ $labels.probe_name }} is down"
387
-
388
- - alert: ProbeSlow
389
- expr: upright_probe_duration_seconds > 10
495
+ expr: upright_probe_up == 0
390
496
  for: 5m
391
497
  labels:
392
- severity: warning
498
+ severity: "{{ $labels.alert_severity }}"
393
499
  annotations:
394
- summary: "Probe {{ $labels.probe_name }} is slow"
500
+ summary: "Probe {{ $labels.name }} is down"
395
501
  ```
396
502
 
397
503
  ### OpenTelemetry
@@ -452,4 +558,4 @@ bin/rails test
452
558
 
453
559
  ## License
454
560
 
455
- The gem is available under the terms of the [O'Saasy License](LICENSE.md).
561
+ The gem is available under the terms of the [MIT License](LICENSE.md).
@@ -34,227 +34,83 @@
34
34
  width: auto;
35
35
  }
36
36
 
37
- .dashboard-section {
38
- margin-bottom: calc(var(--block-space) * 2);
39
- }
40
-
41
- .dashboard-section h2 {
42
- font-size: var(--text-large);
43
- margin-bottom: var(--block-space);
44
- }
45
-
46
- .dashboard-section h3 {
47
- font-size: var(--text-normal);
48
- font-weight: 500;
49
- margin-bottom: var(--block-space);
50
- }
51
-
52
- .dashboard-loading {
53
- align-items: center;
54
- background: oklch(var(--lch-ink-lightest) / 85%);
55
- border-radius: 0.25rem;
56
- display: flex;
57
- gap: var(--inline-space);
58
- justify-content: center;
59
- margin-bottom: var(--block-space);
60
- padding: var(--block-space);
61
- }
62
-
63
- .dashboard-loading.hidden {
64
- display: none;
65
- }
66
-
67
- .loading-spinner {
68
- animation: spin 1s linear infinite;
69
- border: 2px solid var(--color-ink-light);
70
- border-radius: 50%;
71
- border-top-color: var(--color-link);
72
- height: 1rem;
73
- width: 1rem;
74
- }
75
-
76
- @keyframes spin {
77
- to { transform: rotate(360deg); }
78
- }
79
-
80
- .uptime-card {
37
+ .no-data-message {
81
38
  background: oklch(var(--lch-ink-lightest) / 85%);
82
39
  border-radius: 0.25rem;
83
40
  box-shadow: var(--shadow);
84
- display: inline-block;
85
- padding: calc(var(--block-space) * 1.5) calc(var(--block-space) * 2);
86
- text-align: center;
87
- }
88
-
89
- .uptime-value {
90
- font-size: 3rem;
91
- font-weight: 600;
92
- letter-spacing: -0.02em;
93
- line-height: 1;
94
- margin-bottom: 0.25rem;
95
- }
96
-
97
- .uptime-label {
98
41
  color: var(--color-ink-dark);
99
- font-size: var(--text-small);
100
- font-weight: 500;
101
- text-transform: uppercase;
102
- }
103
-
104
- .uptime-period {
105
- color: var(--color-ink-medium);
106
- font-size: var(--text-x-small);
107
- margin-top: 0.25rem;
42
+ padding: calc(var(--block-space) * 2);
43
+ text-align: center;
108
44
  }
109
45
 
110
- .dashboard-chart {
111
- background: oklch(var(--lch-ink-lightest) / 85%);
46
+ .error-message {
47
+ background: oklch(var(--lch-red-dark) / 15%);
112
48
  border-radius: 0.25rem;
113
- box-shadow: var(--shadow);
114
- min-height: 250px;
115
- padding: var(--block-space);
116
- }
117
-
118
- .dashboard-chart circle {
119
- stroke: none;
120
- }
121
-
122
- .dashboard-chart text {
123
- font-family: var(--font-mono);
124
- font-size: var(--text-x-small);
125
- }
126
-
127
- .uptime-table {
128
- width: 100%;
129
- }
130
-
131
- .uptime-table tbody tr {
132
- cursor: default;
133
- }
134
-
135
- .status-2xx {
136
- color: var(--color-positive);
137
- font-weight: 500;
138
- }
139
-
140
- .status-3xx {
141
- color: oklch(70% 0.12 250);
142
- font-weight: 500;
143
- }
144
-
145
- .status-4xx {
146
- color: oklch(75% 0.15 85);
147
- font-weight: 500;
148
- }
149
-
150
- .status-5xx {
151
49
  color: var(--color-negative);
152
- font-weight: 600;
153
- }
154
-
155
- .status-unknown {
156
- color: var(--color-ink-medium);
157
- }
158
-
159
- .heatmap-wrapper {
160
- background: oklch(var(--lch-ink-lightest) / 85%);
161
- border-radius: 0.25rem;
162
- box-shadow: var(--shadow);
163
- overflow-x: auto;
50
+ padding: var(--block-space);
51
+ text-align: center;
164
52
  }
165
53
 
166
- .heatmap-table {
167
- border-collapse: collapse;
168
- box-shadow: none;
169
- min-width: 100%;
54
+ /* Probe Status Matrix */
55
+ .probe-status__header,
56
+ .probe-status__row {
57
+ display: grid;
58
+ gap: 0;
59
+ grid-template-columns: minmax(200px, 1fr) repeat(var(--cols), 1fr);
170
60
  }
171
61
 
172
- .heatmap-table th,
173
- .heatmap-table td {
174
- border: 1px solid var(--color-ink-lighter);
175
- font-size: var(--text-x-small);
176
- padding: 0.5em 0.75em;
177
- text-align: center;
62
+ .probe-status__header > *,
63
+ .probe-status__row > * {
64
+ min-width: 8em;
65
+ padding: calc(var(--block-space) * 0.75) var(--block-space);
178
66
  white-space: nowrap;
179
67
  }
180
68
 
181
- .heatmap-probe-header,
182
- .heatmap-probe {
183
- background: var(--color-ink-lighter);
184
- font-weight: 500;
185
- left: 0;
186
- position: sticky;
187
- text-align: left;
69
+ .probe-status-cell-link {
70
+ color: inherit;
71
+ display: block;
72
+ text-decoration: none;
188
73
  }
189
74
 
190
- .heatmap-time {
191
- font-size: calc(var(--text-x-small) * 0.9);
192
- font-weight: 400;
193
- }
194
-
195
- .heatmap-cell {
196
- font-weight: 500;
197
- min-width: 3em;
75
+ .probe-status-cell-link:hover {
76
+ opacity: 0.8;
198
77
  }
199
78
 
200
- .heatmap-cell.status-2xx {
201
- background: oklch(var(--lch-green-dark) / 15%);
79
+ .probe-status-row--down {
80
+ background: oklch(var(--lch-red-dark) / 8%);
202
81
  }
203
82
 
204
- .heatmap-cell.status-3xx {
205
- background: oklch(70% 0.08 250 / 15%);
83
+ .probe-status-cell--up {
84
+ background: oklch(var(--lch-green-dark) / 10%);
206
85
  }
207
86
 
208
- .heatmap-cell.status-4xx {
209
- background: oklch(75% 0.1 85 / 20%);
87
+ .probe-status-cell--down {
88
+ background: oklch(var(--lch-red-dark) / 15%);
210
89
  }
211
90
 
212
- .heatmap-cell.status-5xx {
213
- background: oklch(var(--lch-red-dark) / 20%);
91
+ .probe-status-cell--stale {
92
+ background: oklch(75% 0.1 85 / 15%);
214
93
  }
215
94
 
216
- .error-table {
217
- width: 100%;
95
+ .probe-status-indicator {
96
+ font-weight: 600;
97
+ display: block;
218
98
  }
219
99
 
220
- .error-table tbody tr {
221
- cursor: default;
100
+ .probe-status-indicator--up {
101
+ color: var(--color-positive);
222
102
  }
223
103
 
224
- .error-time {
225
- color: var(--color-ink-dark);
226
- white-space: nowrap;
104
+ .probe-status-indicator--down {
105
+ color: var(--color-negative);
227
106
  }
228
107
 
229
- .error-probe {
230
- font-weight: 500;
108
+ .probe-status-indicator--stale {
109
+ color: oklch(75% 0.15 85);
231
110
  }
232
111
 
233
- .error-target {
112
+ .probe-status-indicator--unknown {
234
113
  color: var(--color-ink-medium);
235
- font-size: var(--text-x-small);
236
- max-width: 300px;
237
- overflow: hidden;
238
- text-overflow: ellipsis;
239
- white-space: nowrap;
240
- }
241
-
242
- .no-data-message,
243
- .no-errors-message {
244
- background: oklch(var(--lch-ink-lightest) / 85%);
245
- border-radius: 0.25rem;
246
- box-shadow: var(--shadow);
247
- color: var(--color-ink-dark);
248
- padding: calc(var(--block-space) * 2);
249
- text-align: center;
250
- }
251
-
252
- .error-message {
253
- background: oklch(var(--lch-red-dark) / 15%);
254
- border-radius: 0.25rem;
255
- color: var(--color-negative);
256
- padding: var(--block-space);
257
- text-align: center;
258
114
  }
259
115
 
260
116
  @media (max-width: 85ch) {
@@ -271,17 +127,33 @@
271
127
  .dashboard-filters {
272
128
  flex-wrap: wrap;
273
129
  }
274
-
275
- .uptime-value {
276
- font-size: 2rem;
277
- }
278
130
  }
279
131
 
280
132
  @media (max-width: 55ch) {
281
- .heatmap-table th,
282
- .heatmap-table td {
283
- padding: 0.25em 0.5em;
284
- font-size: calc(var(--text-x-small) * 0.9);
133
+ .probe-status__header {
134
+ display: none;
135
+ }
136
+
137
+ .probe-status__row {
138
+ grid-template-columns: 1fr;
139
+ }
140
+
141
+ .probe-status__row > .sticky-left {
142
+ position: static;
143
+ }
144
+
145
+ .probe-status__row > [data-label] {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: var(--inline-space);
149
+ text-align: left;
150
+
151
+ &::before {
152
+ content: attr(data-label);
153
+ font-size: var(--text-x-small);
154
+ font-weight: 500;
155
+ min-width: 10em;
156
+ }
285
157
  }
286
158
  }
287
159
  }