upright 0.3.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.
- checksums.yaml +4 -4
- data/README.md +66 -7
- data/Rakefile +22 -0
- data/app/assets/images/upright/.keep +0 -0
- data/app/assets/stylesheets/upright/_global.css +2 -0
- data/app/assets/stylesheets/upright/artifact.css +7 -25
- data/app/assets/stylesheets/upright/flash.css +19 -0
- data/app/assets/stylesheets/upright/header.css +14 -0
- data/app/assets/stylesheets/upright/incidents.css +610 -0
- data/app/assets/stylesheets/upright/map.css +6 -0
- data/app/assets/stylesheets/upright/public_status.css +534 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/upright/authentication.rb +1 -0
- data/app/controllers/concerns/upright/proxy_authentication.rb +134 -0
- data/app/controllers/upright/alertmanager_proxy_controller.rb +12 -4
- data/app/controllers/upright/artifacts_controller.rb +1 -1
- data/app/controllers/upright/incidents/updates_controller.rb +13 -0
- data/app/controllers/upright/incidents_controller.rb +53 -0
- data/app/controllers/upright/prometheus_proxy_controller.rb +13 -18
- data/app/controllers/upright/public/base_controller.rb +16 -0
- data/app/controllers/upright/public/incidents_controller.rb +6 -0
- data/app/controllers/upright/public/service_incidents_controller.rb +7 -0
- data/app/controllers/upright/public/services_controller.rb +6 -0
- data/app/controllers/upright/sessions_controller.rb +36 -1
- data/app/controllers/upright/traces_controller.rb +88 -0
- data/app/helpers/upright/application_helper.rb +22 -2
- data/app/helpers/upright/artifacts_helper.rb +20 -0
- data/app/helpers/upright/probe_results_helper.rb +1 -0
- data/app/helpers/upright/public/services_helper.rb +70 -0
- data/app/javascript/upright/application.js +3 -0
- data/app/javascript/upright/controllers/sites_map_controller.js +13 -5
- data/app/javascript/upright/controllers/template_controller.js +11 -0
- data/app/javascript/upright/public.js +5 -0
- data/app/jobs/upright/health_metrics_job.rb +27 -0
- data/app/jobs/upright/incident_reporter_job.rb +7 -0
- data/app/jobs/upright/maintenance_advance_job.rb +8 -0
- data/app/jobs/upright/playwright_video_sweep_job.rb +21 -0
- data/app/jobs/upright/rollups/daily_aggregation_job.rb +14 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/concerns/upright/incidents/auto_reporting.rb +93 -0
- data/app/models/concerns/upright/incidents/lifecycle.rb +47 -0
- data/app/models/concerns/upright/playwright/form_authentication.rb +3 -2
- data/app/models/concerns/upright/playwright/lifecycle.rb +8 -13
- data/app/models/concerns/upright/playwright/trace_recording.rb +53 -0
- data/app/models/concerns/upright/playwright/video_recording.rb +55 -14
- data/app/models/concerns/upright/probe_result/stale_cleanup.rb +26 -2
- data/app/models/concerns/upright/probeable.rb +9 -1
- data/app/models/concerns/upright/services/live_status.rb +69 -0
- data/app/models/concerns/upright/services/maintenance_status.rb +19 -0
- data/app/models/upright/artifact.rb +2 -1
- data/app/models/upright/http/request.rb +12 -3
- data/app/models/upright/incident/automatic_report.rb +3 -0
- data/app/models/upright/incident.rb +87 -0
- data/app/models/upright/incident_affected_service.rb +9 -0
- data/app/models/upright/incident_update.rb +7 -0
- data/app/models/upright/maintenance.rb +36 -0
- data/app/models/upright/persistent_record.rb +12 -0
- data/app/models/upright/playwright/authenticator/base.rb +50 -70
- data/app/models/upright/probes/http_probe.rb +30 -1
- data/app/models/upright/probes/playwright/base.rb +2 -0
- data/app/models/upright/probes/status/probe.rb +8 -0
- data/app/models/upright/probes/status.rb +3 -11
- data/app/models/upright/probes/uptime.rb +2 -9
- data/app/models/upright/rollups/daily_uptime.rb +28 -0
- data/app/models/upright/rollups/probe_rollup.rb +64 -0
- data/app/models/upright/rollups/probe_uptime.rb +9 -0
- data/app/models/upright/rollups/site_uptime.rb +75 -0
- data/app/models/upright/service/daily_status.rb +39 -0
- data/app/models/upright/service/incident_history.rb +24 -0
- data/app/models/upright/service/outage.rb +15 -0
- data/app/models/upright/service/status_page.rb +35 -0
- data/app/models/upright/service.rb +101 -0
- data/app/models/upright/status.rb +21 -0
- data/app/models/upright/traceroute/ip_metadata_lookup.rb +21 -2
- data/app/models/upright/traceroute/result.rb +8 -1
- data/app/views/layouts/upright/_header.html.erb +12 -2
- data/app/views/layouts/upright/application.html.erb +4 -1
- data/app/views/layouts/upright/public.html.erb +18 -0
- data/app/views/layouts/upright/public.rss.builder +1 -0
- data/app/views/upright/active_storage/attachments/_attachment.html.erb +1 -1
- data/app/views/upright/artifacts/show.html.erb +8 -0
- data/app/views/upright/incidents/_form.html.erb +93 -0
- data/app/views/upright/incidents/_row.html.erb +15 -0
- data/app/views/upright/incidents/edit.html.erb +64 -0
- data/app/views/upright/incidents/index.html.erb +53 -0
- data/app/views/upright/incidents/new.html.erb +6 -0
- data/app/views/upright/public/_branding.html.erb +3 -0
- data/app/views/upright/public/_footer.html.erb +3 -0
- data/app/views/upright/public/incidents/show.html.erb +29 -0
- data/app/views/upright/public/service_incidents/_incident.html.erb +14 -0
- data/app/views/upright/public/service_incidents/index.html.erb +50 -0
- data/app/views/upright/public/services/_active_events.html.erb +28 -0
- data/app/views/upright/public/services/_degraded_list.html.erb +14 -0
- data/app/views/upright/public/services/_overall_banner.html.erb +4 -0
- data/app/views/upright/public/services/_service.html.erb +34 -0
- data/app/views/upright/public/services/_upcoming_maintenance.html.erb +21 -0
- data/app/views/upright/public/services/_uptime_bar.html.erb +9 -0
- data/app/views/upright/public/services/index.html.erb +13 -0
- data/app/views/upright/public/services/index.rss.builder +29 -0
- data/app/views/upright/sessions/new.html.erb +14 -1
- data/config/importmap.rb +25 -14
- data/config/initializers/date_formats.rb +5 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/routes.rb +26 -2
- data/db/migrate/20260629000001_fix_probe_result_duration_precision.rb +14 -0
- data/db/persistent_migrate/20260512000001_create_upright_rollups.rb +16 -0
- data/db/persistent_migrate/20260702000001_create_upright_incidents.rb +34 -0
- data/db/persistent_migrate/20260707000001_add_probe_identity_to_upright_rollups.rb +11 -0
- data/db/persistent_migrate/20260709000001_add_created_by_to_incidents.rb +7 -0
- data/db/persistent_migrate/20260901000001_add_auto_reporting_to_upright_incidents.rb +22 -0
- data/lib/generators/upright/install/database_config.rb +46 -0
- data/lib/generators/upright/install/install_generator.rb +18 -10
- data/lib/generators/upright/install/templates/Dockerfile +80 -0
- data/lib/generators/upright/install/templates/content_security_policy.rb +42 -0
- data/lib/generators/upright/install/templates/deploy.yml +13 -13
- data/lib/generators/upright/install/templates/development_prometheus.yml +1 -1
- data/lib/generators/upright/install/templates/docker-compose.yml +12 -11
- data/lib/generators/upright/install/templates/omniauth.rb +7 -3
- data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
- data/lib/generators/upright/install/templates/recurring.yml +20 -0
- data/lib/generators/upright/install/templates/sites.yml +4 -0
- data/lib/generators/upright/install/templates/upright.rb +21 -2
- data/lib/generators/upright/install/templates/upright.rules.yml +11 -4
- data/lib/upright/configuration.rb +113 -6
- data/lib/upright/engine.rb +25 -2
- data/lib/upright/metrics.rb +25 -0
- data/lib/upright/site.rb +31 -1
- data/lib/upright/version.rb +3 -1
- data/lib/upright.rb +29 -2
- metadata +107 -7
- data/config/credentials/development.key +0 -1
- data/config/credentials/test.key +0 -1
- data/lib/tasks/upright_tasks.rake +0 -4
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
:root {
|
|
3
|
+
--status-operational: oklch(70% 0.18 145);
|
|
4
|
+
--status-degraded: oklch(78% 0.18 95);
|
|
5
|
+
--status-partial: oklch(70% 0.18 50);
|
|
6
|
+
--status-major: oklch(60% 0.21 28);
|
|
7
|
+
--status-maintenance: oklch(64% 0.14 250);
|
|
8
|
+
--status-none: oklch(var(--lch-ink-lightest));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.public {
|
|
12
|
+
font-family: var(--font-sans);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.status-page {
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
max-width: 56rem;
|
|
18
|
+
padding: calc(var(--block-space) * 3) var(--main-padding);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.status-header,
|
|
22
|
+
.status-footer {
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
max-width: 56rem;
|
|
25
|
+
padding: 0 var(--main-padding);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.status-header {
|
|
29
|
+
align-items: center;
|
|
30
|
+
display: flex;
|
|
31
|
+
gap: 0.75rem;
|
|
32
|
+
padding-top: calc(var(--block-space) * 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.status-header__label {
|
|
36
|
+
font-size: var(--text-large);
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
letter-spacing: -0.01em;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.status-footer {
|
|
42
|
+
border-top: var(--border);
|
|
43
|
+
color: var(--color-ink-medium);
|
|
44
|
+
font-size: var(--text-small);
|
|
45
|
+
margin-top: calc(var(--block-space) * 2);
|
|
46
|
+
padding-bottom: calc(var(--block-space) * 3);
|
|
47
|
+
padding-top: calc(var(--block-space) * 1.5);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.status-page__title {
|
|
51
|
+
font-size: var(--text-x-large);
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
letter-spacing: -0.02em;
|
|
54
|
+
margin: 0 0 calc(var(--block-space) * 1.5);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.status-banner {
|
|
58
|
+
align-items: center;
|
|
59
|
+
border-radius: 0.5rem;
|
|
60
|
+
color: var(--color-white);
|
|
61
|
+
display: flex;
|
|
62
|
+
font-size: var(--text-large);
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
gap: 0.75rem;
|
|
65
|
+
padding: 1rem 1.25rem;
|
|
66
|
+
margin-bottom: calc(var(--block-space) * 3);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.status-banner--operational { background: var(--status-operational); }
|
|
70
|
+
.status-banner--degraded { background: var(--status-degraded); color: oklch(20% 0 0); }
|
|
71
|
+
.status-banner--partial_outage { background: var(--status-partial); }
|
|
72
|
+
.status-banner--major_outage { background: var(--status-major); }
|
|
73
|
+
.status-banner--maintenance { background: var(--status-maintenance); }
|
|
74
|
+
|
|
75
|
+
.status-banner__dot {
|
|
76
|
+
background: currentColor;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
height: 0.6rem;
|
|
79
|
+
opacity: 0.8;
|
|
80
|
+
width: 0.6rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.status-degraded {
|
|
84
|
+
border: var(--border);
|
|
85
|
+
border-radius: 0.5rem;
|
|
86
|
+
list-style: none;
|
|
87
|
+
margin: calc(var(--block-space) * -2) 0 calc(var(--block-space) * 3);
|
|
88
|
+
padding: 0.25rem 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.status-degraded__item {
|
|
92
|
+
align-items: center;
|
|
93
|
+
display: flex;
|
|
94
|
+
gap: 0.75rem;
|
|
95
|
+
padding: 0.5rem 1rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.status-degraded__item + .status-degraded__item {
|
|
99
|
+
border-top: var(--border);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.status-degraded__dot {
|
|
103
|
+
border-radius: 50%;
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
height: 0.6rem;
|
|
106
|
+
width: 0.6rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.status-degraded__dot--degraded { background: var(--status-degraded); }
|
|
110
|
+
.status-degraded__dot--partial_outage { background: var(--status-partial); }
|
|
111
|
+
.status-degraded__dot--major_outage { background: var(--status-major); }
|
|
112
|
+
|
|
113
|
+
.status-degraded__name { font-weight: 600; }
|
|
114
|
+
|
|
115
|
+
.status-degraded__detail {
|
|
116
|
+
color: var(--color-ink-medium);
|
|
117
|
+
margin-left: auto;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.service {
|
|
121
|
+
border-top: var(--border);
|
|
122
|
+
padding: calc(var(--block-space) * 1.25) 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.service__row {
|
|
126
|
+
align-items: center;
|
|
127
|
+
display: flex;
|
|
128
|
+
gap: 1rem;
|
|
129
|
+
justify-content: space-between;
|
|
130
|
+
margin-bottom: 0.75rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.service__name {
|
|
134
|
+
color: inherit;
|
|
135
|
+
font-size: var(--text-large);
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.service-detail {
|
|
140
|
+
margin: 0 auto;
|
|
141
|
+
max-width: 56rem;
|
|
142
|
+
padding: calc(var(--block-space) * 3) var(--main-padding);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.service-detail__back {
|
|
146
|
+
color: var(--color-ink-medium);
|
|
147
|
+
font-size: var(--text-small);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.service-detail__head {
|
|
151
|
+
border-bottom: var(--border);
|
|
152
|
+
margin: var(--block-space) 0 calc(var(--block-space) * 2);
|
|
153
|
+
padding-bottom: calc(var(--block-space) * 2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.service-detail__title {
|
|
157
|
+
font-size: var(--text-x-large);
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
letter-spacing: -0.02em;
|
|
160
|
+
margin: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.service-history + .service-history {
|
|
164
|
+
margin-top: calc(var(--block-space) * 2);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.service-history__title {
|
|
168
|
+
font-size: var(--text-large);
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
margin: 0 0 var(--block-space);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.service-history__incident {
|
|
174
|
+
border: var(--border);
|
|
175
|
+
border-left: 3px solid var(--status-none);
|
|
176
|
+
border-radius: 0.5rem;
|
|
177
|
+
color: inherit;
|
|
178
|
+
display: block;
|
|
179
|
+
padding: 0.75rem 1rem;
|
|
180
|
+
text-decoration: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.service-history__incident + .service-history__incident { margin-top: 0.5rem; }
|
|
184
|
+
.service-history__incident--minor { border-left-color: var(--status-degraded); }
|
|
185
|
+
.service-history__incident--major { border-left-color: var(--status-partial); }
|
|
186
|
+
.service-history__incident--critical { border-left-color: var(--status-major); }
|
|
187
|
+
.service-history__incident--maintenance { border-left-color: var(--status-maintenance); }
|
|
188
|
+
|
|
189
|
+
.service-history__incident:hover { border-color: var(--color-ink-medium); }
|
|
190
|
+
|
|
191
|
+
.service-history__incident-head {
|
|
192
|
+
align-items: baseline;
|
|
193
|
+
display: flex;
|
|
194
|
+
gap: 1rem;
|
|
195
|
+
justify-content: space-between;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.service-history__incident-title {
|
|
199
|
+
font-size: inherit;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
margin: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.service-history__incident-impact,
|
|
205
|
+
.service-history__incident-dates,
|
|
206
|
+
.service-history__empty {
|
|
207
|
+
color: var(--color-ink-medium);
|
|
208
|
+
font-size: var(--text-small);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.service-history__incident-impact {
|
|
212
|
+
letter-spacing: 0.05em;
|
|
213
|
+
text-transform: uppercase;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.service-history__incident-dates { margin: 0.35rem 0 0; }
|
|
217
|
+
.service-history__incident-update { margin: 0.5rem 0 0; }
|
|
218
|
+
.service-history__empty { margin: 0; }
|
|
219
|
+
|
|
220
|
+
.service__status {
|
|
221
|
+
color: var(--color-ink-medium);
|
|
222
|
+
font-size: var(--text-small);
|
|
223
|
+
font-weight: 500;
|
|
224
|
+
text-transform: uppercase;
|
|
225
|
+
letter-spacing: 0.05em;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.service__status--operational { color: var(--status-operational); }
|
|
229
|
+
.service__status--degraded { color: var(--status-degraded); }
|
|
230
|
+
.service__status--partial_outage { color: var(--status-partial); }
|
|
231
|
+
.service__status--major_outage { color: var(--status-major); }
|
|
232
|
+
.service__status--maintenance { color: var(--status-maintenance); }
|
|
233
|
+
|
|
234
|
+
.service__description {
|
|
235
|
+
color: var(--color-ink-medium);
|
|
236
|
+
font-size: var(--text-small);
|
|
237
|
+
margin: 0 0 0.75rem;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.uptime {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
gap: 0.4rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.uptime__bars {
|
|
247
|
+
display: flex;
|
|
248
|
+
gap: 2px;
|
|
249
|
+
height: 2rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.uptime__bar {
|
|
253
|
+
background: var(--status-none);
|
|
254
|
+
border-radius: 1px;
|
|
255
|
+
flex: 1 1 0;
|
|
256
|
+
min-width: 2px;
|
|
257
|
+
position: relative;
|
|
258
|
+
transition: filter 100ms ease-out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.uptime__bar:hover {
|
|
262
|
+
filter: brightness(0.9);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.uptime__bar--operational { background: var(--status-operational); }
|
|
266
|
+
.uptime__bar--degraded { background: var(--status-degraded); }
|
|
267
|
+
.uptime__bar--partial_outage { background: var(--status-partial); }
|
|
268
|
+
.uptime__bar--major_outage { background: var(--status-major); }
|
|
269
|
+
|
|
270
|
+
.uptime__meta {
|
|
271
|
+
color: var(--color-ink-medium);
|
|
272
|
+
display: flex;
|
|
273
|
+
font-size: var(--text-x-small);
|
|
274
|
+
justify-content: space-between;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.uptime__meta-percent {
|
|
278
|
+
color: var(--color-ink-dark);
|
|
279
|
+
font-weight: 500;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.uptime__tooltip {
|
|
283
|
+
background: var(--color-canvas);
|
|
284
|
+
border: var(--border);
|
|
285
|
+
border-radius: 0.5rem;
|
|
286
|
+
bottom: calc(100% + 0.5rem);
|
|
287
|
+
box-shadow: var(--shadow);
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
font-family: var(--font-sans);
|
|
291
|
+
font-size: var(--text-x-small);
|
|
292
|
+
gap: 0.2rem;
|
|
293
|
+
left: 50%;
|
|
294
|
+
line-height: 1.35;
|
|
295
|
+
opacity: 0;
|
|
296
|
+
padding: 0.45rem 0.6rem;
|
|
297
|
+
pointer-events: none;
|
|
298
|
+
position: absolute;
|
|
299
|
+
transform: translate(-50%, 0.25rem);
|
|
300
|
+
transition: opacity 120ms ease-out, transform 120ms ease-out;
|
|
301
|
+
visibility: hidden;
|
|
302
|
+
white-space: nowrap;
|
|
303
|
+
z-index: 10;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.uptime__bar:hover .uptime__tooltip {
|
|
307
|
+
opacity: 1;
|
|
308
|
+
transform: translate(-50%, 0);
|
|
309
|
+
visibility: visible;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Caret, sitting on the tooltip's bottom edge and matching its border. */
|
|
313
|
+
.uptime__tooltip::after {
|
|
314
|
+
background: var(--color-canvas);
|
|
315
|
+
border-bottom: var(--border);
|
|
316
|
+
border-right: var(--border);
|
|
317
|
+
content: "";
|
|
318
|
+
height: 0.5rem;
|
|
319
|
+
left: 50%;
|
|
320
|
+
position: absolute;
|
|
321
|
+
top: calc(100% - 0.25rem);
|
|
322
|
+
transform: translateX(-50%) rotate(45deg);
|
|
323
|
+
width: 0.5rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.uptime__tooltip-date {
|
|
327
|
+
color: var(--color-ink);
|
|
328
|
+
font-weight: 600;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.uptime__tooltip-detail {
|
|
332
|
+
align-items: center;
|
|
333
|
+
color: var(--color-ink-medium);
|
|
334
|
+
display: flex;
|
|
335
|
+
gap: 0.4rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.uptime__tooltip-dot {
|
|
339
|
+
border-radius: 50%;
|
|
340
|
+
flex-shrink: 0;
|
|
341
|
+
height: 0.5rem;
|
|
342
|
+
width: 0.5rem;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.uptime__tooltip-dot--operational { background: var(--status-operational); }
|
|
346
|
+
.uptime__tooltip-dot--degraded { background: var(--status-degraded); }
|
|
347
|
+
.uptime__tooltip-dot--partial_outage { background: var(--status-partial); }
|
|
348
|
+
.uptime__tooltip-dot--major_outage { background: var(--status-major); }
|
|
349
|
+
.uptime__tooltip-dot--none { background: var(--status-none); }
|
|
350
|
+
|
|
351
|
+
.events,
|
|
352
|
+
.upcoming {
|
|
353
|
+
margin: calc(var(--block-space) * -2) 0 calc(var(--block-space) * 3);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.event,
|
|
357
|
+
.upcoming__item {
|
|
358
|
+
border: var(--border);
|
|
359
|
+
border-left: 3px solid var(--status-none);
|
|
360
|
+
border-radius: 0.5rem;
|
|
361
|
+
padding: 0.75rem 1rem;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.event + .event,
|
|
365
|
+
.upcoming__item + .upcoming__item {
|
|
366
|
+
margin-top: 0.5rem;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.event--minor { border-left-color: var(--status-degraded); }
|
|
370
|
+
.event--major { border-left-color: var(--status-partial); }
|
|
371
|
+
.event--critical { border-left-color: var(--status-major); }
|
|
372
|
+
.event--maintenance { border-left-color: var(--status-maintenance); }
|
|
373
|
+
|
|
374
|
+
.upcoming__title {
|
|
375
|
+
font-size: var(--text-large);
|
|
376
|
+
font-weight: 600;
|
|
377
|
+
margin: 0 0 0.75rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.event__head {
|
|
381
|
+
align-items: baseline;
|
|
382
|
+
display: flex;
|
|
383
|
+
gap: 1rem;
|
|
384
|
+
justify-content: space-between;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.event__title { font-weight: 600; }
|
|
388
|
+
|
|
389
|
+
.event__status,
|
|
390
|
+
.upcoming__when {
|
|
391
|
+
color: var(--color-ink-medium);
|
|
392
|
+
flex-shrink: 0;
|
|
393
|
+
font-size: var(--text-small);
|
|
394
|
+
text-transform: uppercase;
|
|
395
|
+
letter-spacing: 0.05em;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.event__meta,
|
|
399
|
+
.event__services {
|
|
400
|
+
color: var(--color-ink-medium);
|
|
401
|
+
font-size: var(--text-small);
|
|
402
|
+
margin: 0.35rem 0 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.event__update {
|
|
406
|
+
margin: 0.5rem 0 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.event__time {
|
|
410
|
+
color: var(--color-ink-medium);
|
|
411
|
+
display: block;
|
|
412
|
+
font-size: var(--text-x-small);
|
|
413
|
+
margin-top: 0.15rem;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.event,
|
|
417
|
+
.upcoming__item {
|
|
418
|
+
color: inherit;
|
|
419
|
+
display: block;
|
|
420
|
+
text-decoration: none;
|
|
421
|
+
transition: border-color 120ms ease-out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.event:hover,
|
|
425
|
+
.upcoming__item:hover {
|
|
426
|
+
border-color: var(--color-ink-medium);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.event:focus-visible,
|
|
430
|
+
.upcoming__item:focus-visible {
|
|
431
|
+
outline: 2px solid var(--color-ink);
|
|
432
|
+
outline-offset: 2px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.incident-detail {
|
|
436
|
+
--accent: var(--status-none);
|
|
437
|
+
margin: 0 auto;
|
|
438
|
+
max-width: 44rem;
|
|
439
|
+
padding: calc(var(--block-space) * 3) var(--main-padding);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.incident-detail--minor { --accent: var(--status-degraded); }
|
|
443
|
+
.incident-detail--major { --accent: var(--status-partial); }
|
|
444
|
+
.incident-detail--critical { --accent: var(--status-major); }
|
|
445
|
+
.incident-detail--maintenance { --accent: var(--status-maintenance); }
|
|
446
|
+
|
|
447
|
+
.incident-detail__back {
|
|
448
|
+
color: var(--color-ink-medium);
|
|
449
|
+
font-size: var(--text-small);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.incident-detail__head {
|
|
453
|
+
border-bottom: var(--border);
|
|
454
|
+
margin: var(--block-space) 0 calc(var(--block-space) * 2);
|
|
455
|
+
padding-bottom: calc(var(--block-space) * 1.5);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.incident-detail__eyebrow {
|
|
459
|
+
color: var(--accent);
|
|
460
|
+
font-size: var(--text-small);
|
|
461
|
+
font-weight: 600;
|
|
462
|
+
letter-spacing: 0.04em;
|
|
463
|
+
margin: 0 0 0.4rem;
|
|
464
|
+
text-transform: uppercase;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.incident-detail__title {
|
|
468
|
+
font-size: var(--text-x-large);
|
|
469
|
+
font-weight: 600;
|
|
470
|
+
letter-spacing: -0.02em;
|
|
471
|
+
margin: 0 0 0.75rem;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.incident-detail__meta {
|
|
475
|
+
align-items: center;
|
|
476
|
+
color: var(--color-ink-medium);
|
|
477
|
+
display: flex;
|
|
478
|
+
flex-wrap: wrap;
|
|
479
|
+
font-size: var(--text-small);
|
|
480
|
+
gap: 0.4rem 0.9rem;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.incident-detail__status {
|
|
484
|
+
background: color-mix(in oklab, var(--accent) 16%, var(--color-canvas));
|
|
485
|
+
border-radius: 2rem;
|
|
486
|
+
color: var(--color-ink);
|
|
487
|
+
font-weight: 500;
|
|
488
|
+
padding: 0.15rem 0.7rem;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.incident-detail__timeline {
|
|
492
|
+
list-style: none;
|
|
493
|
+
margin: 0;
|
|
494
|
+
padding: 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.incident-detail__update {
|
|
498
|
+
border-left: 2px solid var(--color-ink-lighter);
|
|
499
|
+
padding: 0 0 calc(var(--block-space) * 1.75) calc(var(--block-space) * 1.5);
|
|
500
|
+
position: relative;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.incident-detail__update:last-child {
|
|
504
|
+
border-left-color: transparent;
|
|
505
|
+
padding-bottom: 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.incident-detail__update::before {
|
|
509
|
+
background: var(--color-canvas);
|
|
510
|
+
border: 2px solid var(--accent);
|
|
511
|
+
border-radius: 50%;
|
|
512
|
+
content: "";
|
|
513
|
+
height: 0.75rem;
|
|
514
|
+
left: calc(-0.375rem - 1px);
|
|
515
|
+
position: absolute;
|
|
516
|
+
top: 0.2rem;
|
|
517
|
+
width: 0.75rem;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.incident-detail__update-status {
|
|
521
|
+
font-weight: 600;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.incident-detail__update-time {
|
|
525
|
+
color: var(--color-ink-medium);
|
|
526
|
+
font-size: var(--text-x-small);
|
|
527
|
+
margin-left: 0.5rem;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.incident-detail__update-body {
|
|
531
|
+
color: var(--color-ink-dark);
|
|
532
|
+
margin: 0.4rem 0 0;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
File without changes
|
|
@@ -11,6 +11,7 @@ module Upright::Authentication
|
|
|
11
11
|
if session[:user_info].present?
|
|
12
12
|
Upright::Current.user = Upright::User.new(session[:user_info])
|
|
13
13
|
else
|
|
14
|
+
session[:return_to] = request.url if request.get?
|
|
14
15
|
redirect_to new_admin_session_url(default_url_options.merge(subdomain: Upright.configuration.global_subdomain)), allow_other_host: true
|
|
15
16
|
end
|
|
16
17
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Upright::ProxyAuthentication
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
# A forwarded path may only be a path: no authority (`//host`), no traversal,
|
|
5
|
+
# nothing outside the RFC 3986 path character set. The upstream host and port
|
|
6
|
+
# never come from the request (see #upstream_url), so this isn't the SSRF
|
|
7
|
+
# control — it keeps a normalizing hop from rewriting the path into a denied
|
|
8
|
+
# one, and keeps junk out of the upstream request.
|
|
9
|
+
SAFE_UPSTREAM_PATH = %r{\A/[A-Za-z0-9\-._~!$&'()*+,;=:@/]*\z}
|
|
10
|
+
|
|
11
|
+
# Cap the query we relay upstream. Rack's query parser already protects *us*
|
|
12
|
+
# from a nested-params bomb (it enforces param count, depth and bytesize limits
|
|
13
|
+
# before the action runs); this is about not handing an unbounded query to
|
|
14
|
+
# Prometheus or Alertmanager.
|
|
15
|
+
MAX_UPSTREAM_QUERY = 4096
|
|
16
|
+
|
|
17
|
+
# Destructive/admin upstream endpoints automation must never reach with a token
|
|
18
|
+
# (Prometheus `/-/reload`, `/-/quit`, Alertmanager admin under `/-/`). The
|
|
19
|
+
# embedded same-origin UI can still use them on the session path.
|
|
20
|
+
TOKEN_DENIED_PREFIXES = %w[ /-/ ]
|
|
21
|
+
|
|
22
|
+
included do
|
|
23
|
+
prepend_before_action :block_cross_site_session_requests
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
def authenticate_user
|
|
28
|
+
if proxy_token_provided?
|
|
29
|
+
authenticate_proxy_token
|
|
30
|
+
else
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Specifically "carries a bearer token", not "carries any Authorization
|
|
36
|
+
# header": a `Basic` header belongs on the session path, and shouldn't buy an
|
|
37
|
+
# exemption from the cross-site gate on its way to a 401.
|
|
38
|
+
def proxy_token_provided?
|
|
39
|
+
ActionController::HttpAuthentication::Token.token_and_options(request).present?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def authenticate_proxy_token
|
|
43
|
+
authenticate_or_request_with_http_token do |token|
|
|
44
|
+
ActiveSupport::SecurityUtils.secure_compare(token, Upright.configuration.proxy_token.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The proxies can't demand an authenticity token: the embedded Prometheus and
|
|
49
|
+
# Alertmanager UIs post from JS we don't render, so there's no token to embed.
|
|
50
|
+
# Rather than skipping Rails' verification, widen what counts as verified — a
|
|
51
|
+
# valid token, a bearer token (not an ambient credential, so a cross-site page
|
|
52
|
+
# can't attach one), or same-origin provenance. Cross-site requests are
|
|
53
|
+
# already refused by #block_cross_site_session_requests; leaving verification
|
|
54
|
+
# in the chain means removing that gate can't quietly leave the proxies with
|
|
55
|
+
# no CSRF defence at all.
|
|
56
|
+
def verified_request?
|
|
57
|
+
super || proxy_token_provided? || !cross_site_request?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# CSRF gate for the browser (session-cookie) path: nothing in Action Pack
|
|
61
|
+
# reads Fetch-Metadata, so we gate on it here (CVE-2026-67990). Unlike Rails'
|
|
62
|
+
# token verification this covers GET too, because a proxied GET can reach a
|
|
63
|
+
# state-changing upstream endpoint. Automation authenticates with a bearer
|
|
64
|
+
# token and is out of scope; a missing/invalid token is not exempt — it simply
|
|
65
|
+
# falls through to a 401 at authentication.
|
|
66
|
+
def block_cross_site_session_requests
|
|
67
|
+
head :forbidden if !proxy_token_provided? && cross_site_request?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def cross_site_request?
|
|
71
|
+
site = request.headers["Sec-Fetch-Site"]
|
|
72
|
+
|
|
73
|
+
if site.present?
|
|
74
|
+
# Only same-origin (the embedded UI) and user-initiated `none` (a typed
|
|
75
|
+
# URL or bookmark) are allowed. same-site is refused too: a sibling on the
|
|
76
|
+
# same registrable domain (evil.example.com vs ams.upright.example.com) is
|
|
77
|
+
# same-site, and a top-level navigation from it would otherwise carry the
|
|
78
|
+
# Lax cookie into a forged proxy request.
|
|
79
|
+
!site.in?(%w[ same-origin none ])
|
|
80
|
+
elsif request.origin.present?
|
|
81
|
+
# The same comparison Rails makes in #valid_request_origin?. A sandboxed
|
|
82
|
+
# iframe or a cross-origin redirect sends `Origin: null`, which never
|
|
83
|
+
# equals base_url and so lands here as cross-site.
|
|
84
|
+
request.origin != request.base_url
|
|
85
|
+
else
|
|
86
|
+
# No Fetch-Metadata and no Origin: treat as cross-site and refuse. Every
|
|
87
|
+
# current browser sends Sec-Fetch-Site (and the embedded same-origin UI
|
|
88
|
+
# does too), so only a pre-2023 client omits it; failing closed keeps a
|
|
89
|
+
# header-less cross-site GET from riding the Lax session cookie.
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Build the upstream URL for a forwarded request. Scheme, host and port always
|
|
95
|
+
# come from the configured upstream, so no forwarded path can retarget Faraday
|
|
96
|
+
# at another host — an authority override like
|
|
97
|
+
# `/prometheus//169.254.169.254/latest/meta-data` (SSRF, CVE-2026-25765) can't
|
|
98
|
+
# survive being assigned as a path component. Returns the URI to forward, or
|
|
99
|
+
# nil to reject.
|
|
100
|
+
def upstream_url(base_url, raw)
|
|
101
|
+
path, _separator, query = raw.to_s.partition("?")
|
|
102
|
+
path = "/" if path.empty?
|
|
103
|
+
|
|
104
|
+
if safe_upstream_path?(path) && query.length <= MAX_UPSTREAM_QUERY
|
|
105
|
+
URI.parse(base_url).tap do |url|
|
|
106
|
+
url.path = path
|
|
107
|
+
url.query = query.presence
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
rescue URI::Error
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Faraday and the upstream both normalize and percent-decode the path, so
|
|
115
|
+
# `/./-/reload` or a double-encoded `/%252d%252freload` would arrive at a
|
|
116
|
+
# denied `/-/` endpoint if we only checked the literal prefix. Reject any
|
|
117
|
+
# dot-segment, any percent-encoding, and anything outside the path set.
|
|
118
|
+
# Rack::Utils.valid_path? runs first: it screens null bytes and broken
|
|
119
|
+
# encoding, which would otherwise raise out of the regexp match.
|
|
120
|
+
def safe_upstream_path?(path)
|
|
121
|
+
Rack::Utils.valid_path?(path) &&
|
|
122
|
+
!path.start_with?("//") &&
|
|
123
|
+
!path.match?(%r{(?:\A|/)\.\.?(?:/|\z)}) &&
|
|
124
|
+
!path.include?("%") &&
|
|
125
|
+
path.match?(SAFE_UPSTREAM_PATH)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A token may read through the proxy but not drive destructive or
|
|
129
|
+
# state-changing upstream endpoints; those stay on the same-origin session UI.
|
|
130
|
+
def token_forbidden_path?(path)
|
|
131
|
+
proxy_token_provided? &&
|
|
132
|
+
((!request.get? && !request.head?) || path.start_with?(*TOKEN_DENIED_PREFIXES))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
class Upright::AlertmanagerProxyController < Upright::ApplicationController
|
|
2
|
-
|
|
2
|
+
include Upright::ProxyAuthentication
|
|
3
3
|
|
|
4
4
|
def show
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def proxy
|
|
8
|
-
|
|
8
|
+
url = upstream_url(alertmanager_url, request.fullpath.delete_prefix("/alertmanager"))
|
|
9
|
+
|
|
10
|
+
if url.nil?
|
|
11
|
+
head :bad_request
|
|
12
|
+
elsif token_forbidden_path?(url.path)
|
|
13
|
+
head :forbidden
|
|
14
|
+
else
|
|
15
|
+
proxy_to_alertmanager url, body: request.body&.read
|
|
16
|
+
end
|
|
9
17
|
end
|
|
10
18
|
|
|
11
19
|
private
|
|
12
|
-
def proxy_to_alertmanager(
|
|
13
|
-
response = Faraday.new(url: alertmanager_url).run_request(method.downcase.to_sym,
|
|
20
|
+
def proxy_to_alertmanager(url, method: request.method, body: nil)
|
|
21
|
+
response = Faraday.new(url: alertmanager_url).run_request(method.downcase.to_sym, url.to_s, body, { "Content-Type" => request.content_type })
|
|
14
22
|
|
|
15
23
|
render body: response.body, status: response.status, content_type: response.headers["content-type"]
|
|
16
24
|
end
|