upright 0.2.0 → 0.4.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +208 -56
  3. data/Rakefile +22 -0
  4. data/app/assets/images/upright/.keep +0 -0
  5. data/app/assets/stylesheets/upright/_global.css +2 -0
  6. data/app/assets/stylesheets/upright/artifact.css +7 -25
  7. data/app/assets/stylesheets/upright/flash.css +19 -0
  8. data/app/assets/stylesheets/upright/header.css +14 -0
  9. data/app/assets/stylesheets/upright/incidents.css +610 -0
  10. data/app/assets/stylesheets/upright/map.css +6 -0
  11. data/app/assets/stylesheets/upright/public_status.css +534 -0
  12. data/app/assets/stylesheets/upright/uptime-bars.css +10 -1
  13. data/app/controllers/concerns/.keep +0 -0
  14. data/app/controllers/concerns/upright/authentication.rb +1 -0
  15. data/app/controllers/concerns/upright/proxy_authentication.rb +134 -0
  16. data/app/controllers/upright/alertmanager_proxy_controller.rb +12 -4
  17. data/app/controllers/upright/artifacts_controller.rb +1 -1
  18. data/app/controllers/upright/incidents/updates_controller.rb +13 -0
  19. data/app/controllers/upright/incidents_controller.rb +53 -0
  20. data/app/controllers/upright/probe_results_controller.rb +1 -0
  21. data/app/controllers/upright/prometheus_proxy_controller.rb +13 -18
  22. data/app/controllers/upright/public/base_controller.rb +16 -0
  23. data/app/controllers/upright/public/incidents_controller.rb +6 -0
  24. data/app/controllers/upright/public/service_incidents_controller.rb +7 -0
  25. data/app/controllers/upright/public/services_controller.rb +6 -0
  26. data/app/controllers/upright/sessions_controller.rb +37 -1
  27. data/app/controllers/upright/traces_controller.rb +88 -0
  28. data/app/helpers/upright/application_helper.rb +22 -2
  29. data/app/helpers/upright/artifacts_helper.rb +20 -0
  30. data/app/helpers/upright/probe_results_helper.rb +4 -10
  31. data/app/helpers/upright/public/services_helper.rb +70 -0
  32. data/app/javascript/upright/application.js +3 -0
  33. data/app/javascript/upright/controllers/sites_map_controller.js +13 -5
  34. data/app/javascript/upright/controllers/template_controller.js +11 -0
  35. data/app/javascript/upright/public.js +5 -0
  36. data/app/jobs/upright/health_metrics_job.rb +27 -0
  37. data/app/jobs/upright/incident_reporter_job.rb +7 -0
  38. data/app/jobs/upright/maintenance_advance_job.rb +8 -0
  39. data/app/jobs/upright/playwright_video_sweep_job.rb +21 -0
  40. data/app/jobs/upright/rollups/daily_aggregation_job.rb +14 -0
  41. data/app/models/concerns/.keep +0 -0
  42. data/app/models/concerns/upright/incidents/auto_reporting.rb +93 -0
  43. data/app/models/concerns/upright/incidents/lifecycle.rb +47 -0
  44. data/app/models/concerns/upright/playwright/form_authentication.rb +3 -5
  45. data/app/models/concerns/upright/playwright/lifecycle.rb +8 -13
  46. data/app/models/concerns/upright/playwright/trace_recording.rb +53 -0
  47. data/app/models/concerns/upright/playwright/video_recording.rb +55 -14
  48. data/app/models/concerns/upright/probe_result/stale_cleanup.rb +47 -0
  49. data/app/models/concerns/upright/probeable.rb +16 -2
  50. data/app/models/concerns/upright/services/live_status.rb +69 -0
  51. data/app/models/concerns/upright/services/maintenance_status.rb +19 -0
  52. data/app/models/upright/artifact.rb +2 -1
  53. data/app/models/upright/http/request.rb +13 -4
  54. data/app/models/upright/incident/automatic_report.rb +3 -0
  55. data/app/models/upright/incident.rb +87 -0
  56. data/app/models/upright/incident_affected_service.rb +9 -0
  57. data/app/models/upright/incident_update.rb +7 -0
  58. data/app/models/upright/maintenance.rb +36 -0
  59. data/app/models/upright/persistent_record.rb +12 -0
  60. data/app/models/upright/playwright/authenticator/base.rb +50 -70
  61. data/app/models/upright/probe_result.rb +6 -1
  62. data/app/models/upright/probes/http_probe.rb +30 -1
  63. data/app/models/upright/probes/playwright/base.rb +2 -0
  64. data/app/models/upright/probes/status/probe.rb +8 -0
  65. data/app/models/upright/probes/status.rb +5 -13
  66. data/app/models/upright/probes/uptime.rb +5 -12
  67. data/app/models/upright/rollups/daily_uptime.rb +28 -0
  68. data/app/models/upright/rollups/probe_rollup.rb +64 -0
  69. data/app/models/upright/rollups/probe_uptime.rb +9 -0
  70. data/app/models/upright/rollups/site_uptime.rb +75 -0
  71. data/app/models/upright/service/daily_status.rb +39 -0
  72. data/app/models/upright/service/incident_history.rb +24 -0
  73. data/app/models/upright/service/outage.rb +15 -0
  74. data/app/models/upright/service/status_page.rb +35 -0
  75. data/app/models/upright/service.rb +101 -0
  76. data/app/models/upright/status.rb +21 -0
  77. data/app/models/upright/traceroute/ip_metadata_lookup.rb +21 -2
  78. data/app/models/upright/traceroute/result.rb +8 -1
  79. data/app/views/layouts/upright/_header.html.erb +12 -2
  80. data/app/views/layouts/upright/application.html.erb +4 -1
  81. data/app/views/layouts/upright/public.html.erb +18 -0
  82. data/app/views/layouts/upright/public.rss.builder +1 -0
  83. data/app/views/upright/active_storage/attachments/_attachment.html.erb +1 -1
  84. data/app/views/upright/artifacts/show.html.erb +8 -0
  85. data/app/views/upright/dashboards/_uptime_probe_row.html.erb +5 -3
  86. data/app/views/upright/dashboards/probe_statuses/show.html.erb +1 -1
  87. data/app/views/upright/dashboards/uptimes/show.html.erb +1 -1
  88. data/app/views/upright/incidents/_form.html.erb +93 -0
  89. data/app/views/upright/incidents/_row.html.erb +15 -0
  90. data/app/views/upright/incidents/edit.html.erb +64 -0
  91. data/app/views/upright/incidents/index.html.erb +53 -0
  92. data/app/views/upright/incidents/new.html.erb +6 -0
  93. data/app/views/upright/probe_results/index.html.erb +7 -4
  94. data/app/views/upright/public/_branding.html.erb +3 -0
  95. data/app/views/upright/public/_footer.html.erb +3 -0
  96. data/app/views/upright/public/incidents/show.html.erb +29 -0
  97. data/app/views/upright/public/service_incidents/_incident.html.erb +14 -0
  98. data/app/views/upright/public/service_incidents/index.html.erb +50 -0
  99. data/app/views/upright/public/services/_active_events.html.erb +28 -0
  100. data/app/views/upright/public/services/_degraded_list.html.erb +14 -0
  101. data/app/views/upright/public/services/_overall_banner.html.erb +4 -0
  102. data/app/views/upright/public/services/_service.html.erb +34 -0
  103. data/app/views/upright/public/services/_upcoming_maintenance.html.erb +21 -0
  104. data/app/views/upright/public/services/_uptime_bar.html.erb +9 -0
  105. data/app/views/upright/public/services/index.html.erb +13 -0
  106. data/app/views/upright/public/services/index.rss.builder +29 -0
  107. data/app/views/upright/sessions/new.html.erb +14 -1
  108. data/config/ci.rb +2 -0
  109. data/config/importmap.rb +25 -14
  110. data/config/initializers/date_formats.rb +5 -0
  111. data/config/initializers/mime_types.rb +1 -0
  112. data/config/routes.rb +26 -2
  113. data/db/migrate/20260629000001_fix_probe_result_duration_precision.rb +14 -0
  114. data/db/persistent_migrate/20260512000001_create_upright_rollups.rb +16 -0
  115. data/db/persistent_migrate/20260702000001_create_upright_incidents.rb +34 -0
  116. data/db/persistent_migrate/20260707000001_add_probe_identity_to_upright_rollups.rb +11 -0
  117. data/db/persistent_migrate/20260709000001_add_created_by_to_incidents.rb +7 -0
  118. data/db/persistent_migrate/20260901000001_add_auto_reporting_to_upright_incidents.rb +22 -0
  119. data/lib/generators/upright/install/database_config.rb +46 -0
  120. data/lib/generators/upright/install/install_generator.rb +52 -3
  121. data/lib/generators/upright/install/templates/Dockerfile +80 -0
  122. data/lib/generators/upright/install/templates/content_security_policy.rb +42 -0
  123. data/lib/generators/upright/install/templates/deploy.yml +13 -13
  124. data/lib/generators/upright/install/templates/development_prometheus.yml +1 -1
  125. data/lib/generators/upright/install/templates/docker-compose.yml +12 -11
  126. data/lib/generators/upright/install/templates/http_probes.yml +1 -0
  127. data/lib/generators/upright/install/templates/omniauth.rb +7 -3
  128. data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
  129. data/lib/generators/upright/install/templates/recurring.yml +45 -0
  130. data/lib/generators/upright/install/templates/sites.yml +4 -0
  131. data/lib/generators/upright/install/templates/smtp_probes.yml +3 -0
  132. data/lib/generators/upright/install/templates/traceroute_probes.yml +3 -0
  133. data/lib/generators/upright/install/templates/upright.rb +24 -2
  134. data/lib/generators/upright/install/templates/upright.rules.yml +14 -7
  135. data/lib/upright/configuration.rb +127 -6
  136. data/lib/upright/engine.rb +32 -2
  137. data/lib/upright/metrics.rb +27 -2
  138. data/lib/upright/probe_type_registry.rb +33 -0
  139. data/lib/upright/site.rb +31 -1
  140. data/lib/upright/version.rb +3 -1
  141. data/lib/upright.rb +34 -2
  142. metadata +110 -7
  143. data/config/credentials/development.key +0 -1
  144. data/config/credentials/test.key +0 -1
  145. data/lib/tasks/upright_tasks.rake +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d1bb4a1f14f8e4dc16cf5bebcd5868917b2c0703f6dd94592d6f8c818dfa0b9
4
- data.tar.gz: 4d3a0593d1f49f256145340f3e64c9ab6f467a759f1c0a60e81e58635528e0d1
3
+ metadata.gz: c91a962bc77007bffeebba4df5da09c213a42c46a416de5fce7d19e9169258f7
4
+ data.tar.gz: 47b63615e16a15aede26f738a3ffc7256fada1aab09b775ff7ddd90282c91ea0
5
5
  SHA512:
6
- metadata.gz: 5d06f946c9874af94e42bca38bb58671e227b3c4f2ac2f9a523d36b3efca04771a3d0a087bb2644ea067e5a8094d35e1370ee85ded8ea140af084afbf4a4e29a
7
- data.tar.gz: c2ac9d3d2822197887e22308dbdce7be1b47692825dcb25cbb660f0de8e24643788b2cd5a63c72c4863041b619a35e3b31b9e241c69861b59ff66c8398fcd9b2
6
+ metadata.gz: 25a77e92224a4869f1da746deba9f5a4440c4a071d2e3b0c8fef363c2abd36e270cc9a1147a11081284a85f0deb8e22cc73a09d8a3dcd183ea29dd7a0419e872
7
+ data.tar.gz: a5e047ccb8feb9e864652496e81cdbb17ea4e0ae0972b8e380df9fa1206b011c23d5fd0b1e8bae2f366313dcb48b7af25d3babeba1f96e1211967bf70e954415
data/README.md CHANGED
@@ -47,7 +47,8 @@ Upright is a self-hosted synthetic monitoring system. It provides a framework fo
47
47
 
48
48
  ## Installation
49
49
 
50
- 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.
51
52
 
52
53
  ### Quick Start (New Project)
53
54
 
@@ -58,7 +59,7 @@ rails new my-upright --database=sqlite3 --skip-test
58
59
  cd my-upright
59
60
  bundle add upright
60
61
  bin/rails generate upright:install
61
- bin/rails db:migrate
62
+ bin/rails db:prepare
62
63
  ```
63
64
 
64
65
  Start the server:
@@ -75,7 +76,10 @@ Visit http://app.my-upright.localhost:3000 to see your Upright instance.
75
76
 
76
77
  The `upright:install` generator creates:
77
78
 
79
+ - `config/database.yml` - Development and test split into `primary` (probe results), `persistent` (rollups and incidents, migrated from the gem) and `queue` (Solid Queue) databases
80
+ - `config/recurring.yml` - Probe schedules and the engine's housekeeping, health, rollup, incident and maintenance jobs
78
81
  - `config/initializers/upright.rb` - Engine configuration
82
+ - `config/initializers/content_security_policy.rb` - Ready-to-enable CSP covering the engine's CDN dependencies
79
83
  - `config/sites.yml` - Site definitions for each VPS you host Upright on
80
84
  - `config/prometheus/prometheus.yml` - Prometheus configuration
81
85
  - `config/alertmanager/alertmanager.yml` - AlertManager configuration
@@ -121,6 +125,8 @@ shared:
121
125
  country: NL
122
126
  geohash: u17982
123
127
  provider: digitalocean
128
+ stores_metrics: true
129
+ primary: true
124
130
 
125
131
  - code: sfo
126
132
  city: San Francisco
@@ -131,14 +137,16 @@ shared:
131
137
 
132
138
  Each site node identifies itself via the `SITE_SUBDOMAIN` environment variable, configured in your Kamal deploy.yml.
133
139
 
140
+ Two optional flags give a site a role beyond running probes: `primary` serves the app and status hostnames and runs the jobs writing the shared database, and `stores_metrics` runs a local Prometheus and Alertmanager. See [Sites and their roles](https://github.com/basecamp/upright/blob/main/docs/sites.md) for what each one changes, the health metrics every site exports, and how daily rollups read across them.
141
+
134
142
  ### Authentication
135
143
 
136
144
  #### Static Credentials
137
145
 
138
- Upright uses static credentials by default with username `admin` and password `upright`.
146
+ Upright uses static credentials by default with username `admin` and the password taken from the `ADMIN_PASSWORD` environment variable.
139
147
 
140
- > [!WARNING]
141
- > Change the default password before deploying to production by setting the `ADMIN_PASSWORD` environment variable.
148
+ > [!IMPORTANT]
149
+ > There is no default password: `ADMIN_PASSWORD` must be set, and sign-in fails closed without it. Set it as a Kamal secret in production (the generated `config/deploy.yml` already lists it) and export it locally for development.
142
150
 
143
151
 
144
152
  #### OpenID Connect
@@ -157,6 +165,97 @@ Upright.configure do |config|
157
165
  end
158
166
  ```
159
167
 
168
+ ### Probe Result Cleanup
169
+
170
+ Upright automatically cleans up old probe results on a recurring schedule. You can configure the retention thresholds:
171
+
172
+ ```ruby
173
+ Upright.configure do |config|
174
+ config.stale_success_threshold = 24.hours # Delete successful results older than this (default: 24 hours)
175
+ config.stale_failure_threshold = 30.days # Delete failed results older than this (default: 30 days)
176
+ config.failure_retention_limit = 20_000 # Keep at most this many failed results (default: 20,000)
177
+ end
178
+ ```
179
+
180
+ ## Custom Probe Types
181
+
182
+ Upright ships with four built-in probe types: HTTP, Playwright, SMTP, and Traceroute. You can register your own to extend the system.
183
+
184
+ ### 1. Register the type
185
+
186
+ Add it to your initializer so Upright knows about its name and icon:
187
+
188
+ ```ruby
189
+ # config/initializers/upright.rb
190
+ Upright.configure do |config|
191
+ config.probe_types.register :ping, name: "Ping", icon: "📶"
192
+ end
193
+ ```
194
+
195
+ ### 2. Create the probe class
196
+
197
+ 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`:
198
+
199
+ ```ruby
200
+ # probes/ping_probe.rb
201
+ class PingProbe < FrozenRecord::Base
202
+ include Upright::Probeable
203
+ include Upright::ProbeYamlSource
204
+
205
+ stagger_by_site 3.seconds
206
+
207
+ # Hostnames, IPv4, and IPv6 addresses only. The first character may not be
208
+ # a dash, so a configured host can never be mistaken for a ping flag.
209
+ HOST_PATTERN = /\A[a-z0-9:][a-z0-9:._-]*\z/i
210
+
211
+ def check
212
+ raise ArgumentError, "invalid ping host: #{host.inspect}" unless HOST_PATTERN.match?(host.to_s)
213
+
214
+ @ping_output, status = Open3.capture2e("ping", "-c", "1", "-W", "5", "--", host)
215
+ status.success?
216
+ end
217
+
218
+ def on_check_recorded(probe_result)
219
+ if @ping_output.present?
220
+ Upright::Artifact.new(name: "ping.log", content: @ping_output).attach_to(probe_result)
221
+ end
222
+ end
223
+
224
+ def probe_type = "ping"
225
+ def probe_target = host
226
+ end
227
+ ```
228
+
229
+ The `check` method should return a truthy value on success and a falsy value on failure.
230
+
231
+ ### 3. Define probes in YAML
232
+
233
+ Create a YAML file matching the class name (e.g., `PingProbe` → `probes/ping_probes.yml`):
234
+
235
+ ```yaml
236
+ # probes/ping_probes.yml
237
+ - name: "Cloudflare DNS"
238
+ host: "1.1.1.1"
239
+
240
+ - name: "Google DNS"
241
+ host: "8.8.8.8"
242
+ ```
243
+
244
+ Fields defined in the YAML are available as methods on the probe instance (e.g., `host`).
245
+
246
+ ### 4. Schedule it
247
+
248
+ Add a recurring job in `config/recurring.yml`:
249
+
250
+ ```yaml
251
+ production:
252
+ ping_probes:
253
+ command: "PingProbe.check_and_record_all_later"
254
+ schedule: every 30 seconds
255
+ ```
256
+
257
+ The custom type will automatically appear in all dashboard dropdowns and filter links.
258
+
160
259
  ## Defining Probes
161
260
 
162
261
  ### HTTP Probes
@@ -171,12 +270,15 @@ Add probes to `probes/http_probes.yml`:
171
270
  - name: API Health
172
271
  url: https://api.example.com/health
173
272
  expected_status: 200
273
+ alert_severity: critical
174
274
 
175
275
  - name: Admin Panel
176
276
  url: https://admin.example.com
177
277
  basic_auth_credentials: admin_auth # Key in Rails credentials
178
278
  ```
179
279
 
280
+ The optional `alert_severity` field controls the Prometheus alert severity when a probe fails. Values: `medium`, `high` (default), `critical`.
281
+
180
282
  ### SMTP Probes
181
283
 
182
284
  Add probes to `probes/smtp_probes.yml`:
@@ -317,70 +419,83 @@ image: your-org/upright
317
419
  servers:
318
420
  web:
319
421
  hosts:
320
- - nyc.upright.example.com
321
- env:
322
- tags:
323
- SITE_SUBDOMAIN: nyc
324
-
422
+ - ams.upright.example.com: [amsterdam]
423
+ - nyc.upright.example.com: [new_york]
424
+ - sfo.upright.example.com: [san_francisco]
425
+ jobs:
325
426
  hosts:
326
- - ams.upright.example.com
327
- env:
328
- tags:
329
- SITE_SUBDOMAIN: ams
427
+ - ams.upright.example.com: [amsterdam]
428
+ - nyc.upright.example.com: [new_york]
429
+ - sfo.upright.example.com: [san_francisco]
430
+ cmd: bin/jobs
330
431
 
331
- hosts:
332
- - sfo.upright.example.com
333
- env:
334
- tags:
335
- SITE_SUBDOMAIN: sfo
336
-
337
- registry:
338
- server: ghcr.io
339
- username: your-org
340
- password:
341
- - KAMAL_REGISTRY_PASSWORD
432
+ proxy:
433
+ app_port: 3000
434
+ ssl: true
435
+ hosts:
436
+ - "*.upright.example.com"
342
437
 
343
438
  env:
344
- clear:
345
- RAILS_ENV: production
346
- RAILS_LOG_TO_STDOUT: true
347
439
  secret:
348
440
  - RAILS_MASTER_KEY
349
- - OIDC_CLIENT_ID
350
- - OIDC_CLIENT_SECRET
441
+ - ADMIN_PASSWORD
442
+ tags:
443
+ amsterdam:
444
+ SITE_SUBDOMAIN: ams
445
+ new_york:
446
+ SITE_SUBDOMAIN: nyc
447
+ san_francisco:
448
+ SITE_SUBDOMAIN: sfo
351
449
 
352
450
  accessories:
353
451
  playwright:
354
- image: mcr.microsoft.com/playwright:v1.55.0-noble
355
- cmd: npx -y playwright run-server --port 53333
356
- host: nyc.upright.example.com
357
- port: 53333
358
- env:
359
- clear:
360
- DEBUG: pw:api
452
+ image: jacoblincool/playwright:chromium-server-1.55.0
453
+ port: "127.0.0.1:53333:53333"
454
+ roles:
455
+ - jobs
456
+
457
+ prometheus:
458
+ image: prom/prometheus:v3.2.1
459
+ hosts:
460
+ - ams.upright.example.com
461
+ cmd: >-
462
+ --config.file=/etc/prometheus/prometheus.yml
463
+ --storage.tsdb.path=/prometheus
464
+ --storage.tsdb.retention.time=30d
465
+ --web.enable-otlp-receiver
466
+ files:
467
+ - config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
468
+ - config/prometheus/rules/upright.rules.yml:/etc/prometheus/rules/upright.rules.yml
469
+
470
+ alertmanager:
471
+ image: prom/alertmanager:v0.28.1
472
+ hosts:
473
+ - ams.upright.example.com
474
+ cmd: --config.file=/etc/alertmanager/alertmanager.yml
475
+ files:
476
+ - config/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
361
477
  ```
362
478
 
363
479
  ## Observability
364
480
 
365
481
  ### Prometheus
366
482
 
367
- The engine exposes metrics at `/metrics`. Configure Prometheus to scrape:
483
+ Metrics are exposed via a Puma plugin at `http://0.0.0.0:9394/metrics`. Configure Prometheus to scrape:
368
484
 
369
485
  ```yaml
370
486
  scrape_configs:
371
487
  - job_name: upright
372
488
  static_configs:
373
- - targets: ['localhost:3000']
374
- metrics_path: /metrics
489
+ - targets: ['localhost:9394']
375
490
  ```
376
491
 
377
492
  ### Metrics Exposed
378
493
 
379
494
  - `upright_probe_duration_seconds` - Probe execution duration
380
- - `upright_probe_success` - Probe success/failure (1/0)
381
- - `upright_probe_status_code` - HTTP status code for HTTP probes
495
+ - `upright_probe_up` - Probe status (1 = up, 0 = down)
496
+ - `upright_http_response_status` - HTTP response status code
382
497
 
383
- Labels include: `probe_name`, `probe_type`, `site_code`, `site_city`, `site_country`
498
+ Labels include: `type`, `name`, `site_code`, `site_city`, `site_country`
384
499
 
385
500
  ### AlertManager
386
501
 
@@ -391,20 +506,12 @@ groups:
391
506
  - name: upright
392
507
  rules:
393
508
  - alert: ProbeDown
394
- expr: upright_probe_success == 0
395
- for: 5m
396
- labels:
397
- severity: critical
398
- annotations:
399
- summary: "Probe {{ $labels.probe_name }} is down"
400
-
401
- - alert: ProbeSlow
402
- expr: upright_probe_duration_seconds > 10
509
+ expr: upright_probe_up == 0
403
510
  for: 5m
404
511
  labels:
405
- severity: warning
512
+ severity: "{{ $labels.alert_severity }}"
406
513
  annotations:
407
- summary: "Probe {{ $labels.probe_name }} is slow"
514
+ summary: "Probe {{ $labels.name }} is down"
408
515
  ```
409
516
 
410
517
  ### OpenTelemetry
@@ -443,20 +550,65 @@ bin/dev
443
550
 
444
551
  Visit http://app.upright.localhost:3000 and sign in with:
445
552
  - **Username**: `admin`
446
- - **Password**: `upright` (or value of `ADMIN_PASSWORD` env var)
553
+ - **Password**: the value of the `ADMIN_PASSWORD` env var, e.g. `ADMIN_PASSWORD=upright bin/dev` (a throwaway value like this is fine for local testing only — never for a deployed site)
447
554
 
448
555
  ### Testing Playwright Probes
449
556
 
450
557
  Run probes with a visible browser window:
451
558
 
452
559
  ```bash
453
- LOCAL_PLAYWRIGHT=1 bin/rails console
560
+ HEADLESS=false bin/rails console
454
561
  ```
455
562
 
456
563
  ```ruby
457
564
  Probes::Playwright::MyServiceAuthProbe.check
458
565
  ```
459
566
 
567
+ ### Upgrading Playwright
568
+
569
+ Playwright versions are pinned via `Upright::PLAYWRIGHT_VERSION` in `lib/upright/version.rb`. This drives the Ruby gem and npm package versions. To upgrade:
570
+
571
+ 1. Update `PLAYWRIGHT_VERSION` in `lib/upright/version.rb`
572
+ 2. Update the version in `package.json`
573
+ 3. Run `bin/setup` (or manually: `npm install && npx playwright install chromium`)
574
+ 4. Run `bin/rails test` to verify compatibility
575
+ 5. Commit the updated `package.json` and `package-lock.json`
576
+
577
+ ### Viewing Playwright Traces
578
+
579
+ Upright stores a Playwright trace as a probe result artifact and does not serve
580
+ the Playwright Trace Viewer. The viewer renders a trace's tags and attributes as
581
+ HTML in its own origin, so serving it from Upright's hostname would let a trace
582
+ run script against an admin session.
583
+
584
+ A trace artifact links to `config.trace_viewer_url`, which defaults to
585
+ upstream's hosted viewer at `https://trace.playwright.dev`. That viewer runs
586
+ entirely in the browser, and being on its own registrable domain is what keeps a
587
+ trace's contents off Upright's origin.
588
+
589
+ Following the link hands that origin a URL it can read for 24 hours. Set
590
+ `config.trace_viewer_url` to a viewer you host to keep traces to yourself, or
591
+ assign `nil` to keep them download-only:
592
+
593
+ ```bash
594
+ npx playwright show-trace trace.zip
595
+ ```
596
+
597
+ A URL under `config.hostname` raises `Upright::ConfigurationError`, since that
598
+ would put the trace back on the admin origin.
599
+
600
+ That trace URL is served by `Upright::TracesController`, not Active Storage. The
601
+ viewer fetches it from its own origin, so the request arrives without the admin
602
+ session and needs `Access-Control-Allow-Origin` to be readable at all. The
603
+ controller sends that header for the configured viewer's origin and nothing
604
+ else, and takes a signed id in place of the session: scoped to a purpose,
605
+ expiring after 24 hours, and resolvable only to a blob attached to an
606
+ `Upright::ProbeResult` under a `.zip` filename. With no viewer configured the
607
+ route 404s, so it exists only where it's used.
608
+
609
+ Range requests are answered, which is how the viewer reads a ZIP's central
610
+ directory without downloading the whole archive.
611
+
460
612
  ### Running Tests
461
613
 
462
614
  ```bash
data/Rakefile CHANGED
@@ -4,3 +4,25 @@ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  require "bundler/gem_tasks"
7
+
8
+ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
9
+
10
+ desc "Tag v<current version> and push it, which starts the Release workflow"
11
+ task :tag do
12
+ require_relative "lib/upright/version"
13
+ tag = "v#{Upright::VERSION}"
14
+
15
+ abort "tag: the working tree has uncommitted changes" unless `git status --porcelain`.empty?
16
+
17
+ branch = `git rev-parse --abbrev-ref HEAD`.strip
18
+ abort "tag: run this on main (currently on #{branch})" unless branch == "main"
19
+
20
+ sh "git fetch origin main --quiet"
21
+ abort "tag: HEAD differs from origin/main; pull or push first" unless `git rev-parse HEAD`.strip == `git rev-parse origin/main`.strip
22
+ abort "tag: #{tag} already exists locally" unless `git tag --list #{tag}`.strip.empty?
23
+ abort "tag: #{tag} already exists on origin" unless `git ls-remote --tags origin refs/tags/#{tag}`.strip.empty?
24
+
25
+ sh "git tag --annotate #{tag} --message 'upright #{tag}'"
26
+ sh "git push origin #{tag}"
27
+ puts "Pushed #{tag}. Approve the release-rubygems environment when the Release workflow pauses."
28
+ end
File without changes
@@ -35,6 +35,7 @@
35
35
 
36
36
  --lch-green-dark: 55% 0.162 147;
37
37
  --lch-red-dark: 59% 0.19 38;
38
+ --lch-orange-dark: 70% 0.17 65;
38
39
 
39
40
  /* Colors: Semantic */
40
41
  --color-black: oklch(var(--lch-black));
@@ -52,6 +53,7 @@
52
53
  --color-link: oklch(var(--lch-blue-dark));
53
54
  --color-positive: oklch(var(--lch-green-dark));
54
55
  --color-negative: oklch(var(--lch-red-dark));
56
+ --color-warning: oklch(var(--lch-orange-dark));
55
57
  --color-selected: oklch(var(--lch-blue-lighter));
56
58
 
57
59
  /* Borders & Shadows */
@@ -22,17 +22,11 @@
22
22
 
23
23
  .artifact__content {
24
24
  background: var(--color-ink-lightest);
25
- border-radius: 0.25rem;
26
- box-shadow: var(--shadow);
27
25
  display: flex;
28
26
  flex-direction: column;
29
- height: 80vh;
30
- left: 50%;
27
+ inset: 0;
31
28
  overflow: hidden;
32
29
  position: fixed;
33
- top: 50%;
34
- transform: translate(-50%, -50%);
35
- width: 80vw;
36
30
  z-index: 1000;
37
31
  }
38
32
 
@@ -123,26 +117,14 @@
123
117
  }
124
118
  }
125
119
 
126
- td .artifact + .artifact {
127
- margin-left: calc(var(--inline-space) / 2);
120
+ .artifact__trace {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 0.5em;
128
124
  }
129
125
 
130
- @media (max-width: 140ch) {
131
- .artifact__content {
132
- width: calc(100vw - var(--block-space) * 2);
133
- height: calc(100vh - var(--block-space) * 2);
134
- left: 50%;
135
- top: 50%;
136
- }
126
+ td .artifact + .artifact {
127
+ margin-left: calc(var(--inline-space) / 2);
137
128
  }
138
129
 
139
- @media (max-width: 55ch) {
140
- .artifact__content {
141
- inset: 0;
142
- width: 100%;
143
- height: 100%;
144
- transform: none;
145
- border-radius: 0;
146
- }
147
- }
148
130
  }
@@ -0,0 +1,19 @@
1
+ @layer components {
2
+ .flash {
3
+ border-radius: 0.5rem;
4
+ color: var(--color-ink);
5
+ font-size: var(--text-small);
6
+ margin-block-end: var(--block-space);
7
+ padding: 0.75rem 1rem;
8
+ }
9
+
10
+ .flash--notice {
11
+ background: color-mix(in oklab, var(--color-positive) 12%, var(--color-canvas));
12
+ border: 1px solid color-mix(in oklab, var(--color-positive) 40%, var(--color-canvas));
13
+ }
14
+
15
+ .flash--alert {
16
+ background: color-mix(in oklab, var(--color-negative) 12%, var(--color-canvas));
17
+ border: 1px solid color-mix(in oklab, var(--color-negative) 40%, var(--color-canvas));
18
+ }
19
+ }
@@ -38,6 +38,15 @@
38
38
  text-transform: uppercase;
39
39
  }
40
40
 
41
+ .header__env {
42
+ color: var(--color-warning);
43
+ font-size: var(--text-x-small);
44
+ font-weight: 500;
45
+ letter-spacing: 0.06em;
46
+ margin-right: calc(var(--inline-space) * 2);
47
+ text-transform: uppercase;
48
+ }
49
+
41
50
  .header__nav {
42
51
  align-items: center;
43
52
  display: flex;
@@ -62,6 +71,11 @@
62
71
  &.active {
63
72
  color: var(--color-positive);
64
73
  }
74
+
75
+ &.unavailable {
76
+ color: var(--color-ink-light);
77
+ cursor: help;
78
+ }
65
79
  }
66
80
 
67
81
  .header__divider {