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
@@ -0,0 +1,53 @@
1
+ <% @page_title = "Incidents and maintenance" %>
2
+
3
+ <div class="incidents-index">
4
+ <header class="incidents-header">
5
+ <h1 class="incidents-header__title">Incidents &amp; maintenance</h1>
6
+ <div class="incidents-actions">
7
+ <%= link_to "New incident", new_incident_path, class: "btn" %>
8
+ <%= link_to "New maintenance", new_incident_path(maintenance: true), class: "btn btn--ghost" %>
9
+ </div>
10
+ </header>
11
+
12
+ <% if @current.none? && @upcoming.none? && @past.none? %>
13
+ <div class="incidents-empty">
14
+ <p class="incidents-empty__headline">All clear</p>
15
+ <p class="incidents-empty__body">Nothing to report. Incidents and maintenance you declare here appear on the public status page.</p>
16
+ </div>
17
+ <% end %>
18
+
19
+ <% if @current.any? %>
20
+ <section class="incident-section">
21
+ <h2 class="incident-section__title">Active</h2>
22
+ <div class="incident-list">
23
+ <% @current.each do |incident| %>
24
+ <%= render "row", incident: incident,
25
+ when_label: incident.maintenance? ? maintenance_window_description(incident) : "Started #{incident.starts_at.to_fs(:month_day_at_zone)}" %>
26
+ <% end %>
27
+ </div>
28
+ </section>
29
+ <% end %>
30
+
31
+ <% if @upcoming.any? %>
32
+ <section class="incident-section">
33
+ <h2 class="incident-section__title">Upcoming maintenance</h2>
34
+ <div class="incident-list">
35
+ <% @upcoming.each do |incident| %>
36
+ <%= render "row", incident: incident, when_label: maintenance_window_description(incident) %>
37
+ <% end %>
38
+ </div>
39
+ </section>
40
+ <% end %>
41
+
42
+ <% if @past.any? %>
43
+ <section class="incident-section">
44
+ <h2 class="incident-section__title">Past</h2>
45
+ <div class="incident-list">
46
+ <% @past.each do |incident| %>
47
+ <%= render "row", incident: incident,
48
+ when_label: "Resolved #{incident.resolved_at&.to_fs(:month_day_at_zone)}" %>
49
+ <% end %>
50
+ </div>
51
+ </section>
52
+ <% end %>
53
+ </div>
@@ -0,0 +1,6 @@
1
+ <% @page_title = "New #{@incident.maintenance? ? "maintenance" : "incident"}" %>
2
+
3
+ <div class="incident-editor">
4
+ <%= link_to "← All incidents", incidents_path, class: "incident-editor__back" %>
5
+ <%= render "form" %>
6
+ </div>
@@ -2,10 +2,9 @@
2
2
  <aside>
3
3
  <ul>
4
4
  <li><%= type_filter_link "All" %></li>
5
- <li><%= type_filter_link "HTTP", "http" %></li>
6
- <li><%= type_filter_link "Playwright", "playwright" %></li>
7
- <li><%= type_filter_link "SMTP", "smtp" %></li>
8
- <li><%= type_filter_link "Traceroute", "traceroute" %></li>
5
+ <% Upright.probe_types.each do |probe_type| %>
6
+ <li><%= type_filter_link probe_type.name, probe_type.type %></li>
7
+ <% end %>
9
8
  </ul>
10
9
  </aside>
11
10
 
@@ -16,6 +15,7 @@
16
15
  <div class="filters">
17
16
  <%= form_with url: site_root_path, method: :get, data: { controller: "form" } do |f| %>
18
17
  <%= hidden_field_tag :probe_type, params[:probe_type] if params[:probe_type].present? %>
18
+ <%= hidden_field_tag :date, params[:date] if params[:date].present? %>
19
19
  <label for="probe_name">Name</label>
20
20
  <%= select_tag :probe_name,
21
21
  options_for_select([["All", ""]] + @probe_names, params[:probe_name]),
@@ -26,6 +26,9 @@
26
26
  options_for_select([["All", ""]] + Upright::ProbeResult.statuses.keys.map { |s| [s.titleize, s] }, params[:status]),
27
27
  class: "input--select",
28
28
  data: { action: "change->form#submit" } %>
29
+ <% if params[:date].present? %>
30
+ <%= link_to "Clear date filter", site_root_path(probe_type: params[:probe_type], probe_name: params[:probe_name], status: params[:status]), class: "filter-clear" %>
31
+ <% end %>
29
32
  <% end %>
30
33
  </div>
31
34
 
@@ -0,0 +1,3 @@
1
+ <header class="status-header">
2
+ <span class="status-header__label"><%= Upright.configuration.service_name %></span>
3
+ </header>
@@ -0,0 +1,3 @@
1
+ <footer class="status-footer">
2
+ <span class="status-footer__note">Powered by Upright</span>
3
+ </footer>
@@ -0,0 +1,29 @@
1
+ <% @page_title = @incident.title %>
2
+
3
+ <article class="incident-detail incident-detail--<%= @incident.maintenance? ? "maintenance" : @incident.impact %>">
4
+ <%= link_to "← Status", "/", class: "incident-detail__back" %>
5
+
6
+ <header class="incident-detail__head">
7
+ <p class="incident-detail__eyebrow"><%= @incident.maintenance? ? "Scheduled maintenance" : "Incident" %></p>
8
+ <h1 class="incident-detail__title"><%= @incident.title %></h1>
9
+ <div class="incident-detail__meta">
10
+ <span class="incident-detail__status"><%= status_label(@incident.status) %></span>
11
+ <% if @incident.maintenance? %>
12
+ <span><%= local_maintenance_window(@incident) %></span>
13
+ <% end %>
14
+ <% if @incident.public_services.any? %>
15
+ <span><%= @incident.public_services.map(&:name).to_sentence %></span>
16
+ <% end %>
17
+ </div>
18
+ </header>
19
+
20
+ <ol class="incident-detail__timeline">
21
+ <% @incident.updates.each do |update| %>
22
+ <li class="incident-detail__update">
23
+ <span class="incident-detail__update-status"><%= status_label(update.status) %></span>
24
+ <%= local_time update.created_at, format: "%b %-d, %Y · %H:%M %Z", class: "incident-detail__update-time" %>
25
+ <% if update.body.present? %><p class="incident-detail__update-body"><%= update.body %></p><% end %>
26
+ </li>
27
+ <% end %>
28
+ </ol>
29
+ </article>
@@ -0,0 +1,14 @@
1
+ <%= link_to public_incident_path(incident), class: "service-history__incident service-history__incident--#{incident.impact}" do %>
2
+ <div class="service-history__incident-head">
3
+ <h3 class="service-history__incident-title"><%= incident.title %></h3>
4
+ <span class="service-history__incident-impact"><%= incident.impact.humanize %></span>
5
+ </div>
6
+ <p class="service-history__incident-dates">
7
+ <%= local_time incident.starts_at, format: "%b %-d, %Y · %H:%M %Z" %>
8
+
9
+ <%= incident.resolved_at? ? local_time(incident.resolved_at, format: "%b %-d, %Y · %H:%M %Z") : "Ongoing" %>
10
+ </p>
11
+ <% if incident.updates.first&.body.present? %>
12
+ <p class="service-history__incident-update"><%= incident.updates.first.body %></p>
13
+ <% end %>
14
+ <% end %>
@@ -0,0 +1,50 @@
1
+ <% @page_title = @incident_history.name %>
2
+ <% uptime_history = @incident_history.daily_status_history %>
3
+ <% today_status = uptime_history.last.status %>
4
+
5
+ <article class="service-detail">
6
+ <%= link_to "← Status", public_services_root_path, class: "service-detail__back" %>
7
+
8
+ <header class="service-detail__head">
9
+ <div class="service__row">
10
+ <h1 class="service-detail__title"><%= @incident_history.name %></h1>
11
+ <% if @incident_history.maintenance_active? %>
12
+ <span class="service__status service__status--maintenance">Under maintenance</span>
13
+ <% else %>
14
+ <span class="service__status service__status--<%= today_status %>"><%= status_label(today_status) %></span>
15
+ <% end %>
16
+ </div>
17
+
18
+ <div class="uptime">
19
+ <div class="uptime__bars" role="img" aria-label="<%= uptime_history.size %>-day uptime for <%= @incident_history.name %>">
20
+ <%= render partial: "upright/public/services/uptime_bar", collection: uptime_history, as: :day %>
21
+ </div>
22
+ <div class="uptime__meta">
23
+ <span><%= uptime_history.size %> days ago</span>
24
+ <% if uptime = uptime_percentage_label(uptime_history.map(&:uptime_fraction).compact) %>
25
+ <span class="uptime__meta-percent"><%= uptime %> uptime</span>
26
+ <% else %>
27
+ <span class="uptime__meta-percent">No data yet</span>
28
+ <% end %>
29
+ <span>Today</span>
30
+ </div>
31
+ </div>
32
+ </header>
33
+
34
+ <% if @incident_history.active_incidents.any? %>
35
+ <section class="service-history">
36
+ <h2 class="service-history__title">Active incidents</h2>
37
+ <%= render partial: "incident", collection: @incident_history.active_incidents %>
38
+ </section>
39
+ <% end %>
40
+
41
+ <section class="service-history">
42
+ <h2 class="service-history__title">Incident history</h2>
43
+ <% if @incident_history.past_incidents.any? %>
44
+ <%= render partial: "incident", collection: @incident_history.past_incidents %>
45
+ <%= render "upright/probe_results/pagination", page: @incident_history.page %>
46
+ <% else %>
47
+ <p class="service-history__empty">No past incidents.</p>
48
+ <% end %>
49
+ </section>
50
+ </article>
@@ -0,0 +1,28 @@
1
+ <% events = incidents.to_a + maintenances.to_a %>
2
+ <% if events.any? %>
3
+ <section class="events">
4
+ <% events.each do |event| %>
5
+ <%= link_to public_incident_path(event), class: "event event--#{event.maintenance? ? "maintenance" : event.impact}" do %>
6
+ <div class="event__head">
7
+ <span class="event__title"><%= event.title %></span>
8
+ <span class="event__status"><%= status_label(event.status) %></span>
9
+ </div>
10
+
11
+ <% if event.maintenance? %>
12
+ <p class="event__meta"><%= local_maintenance_window(event) %></p>
13
+ <% end %>
14
+
15
+ <% if event.public_services.any? %>
16
+ <p class="event__services"><%= event.public_services.map(&:name).to_sentence %></p>
17
+ <% end %>
18
+
19
+ <% if update = event.updates.first %>
20
+ <p class="event__update">
21
+ <%= update.body %>
22
+ <%= local_time update.created_at, format: :month_day_at_zone, class: "event__time" %>
23
+ </p>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
27
+ </section>
28
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <% if degraded.any? %>
2
+ <ul class="status-degraded">
3
+ <% degraded.each do |outage| %>
4
+ <li class="status-degraded__item">
5
+ <span class="status-degraded__dot status-degraded__dot--<%= outage.status %>" aria-hidden="true"></span>
6
+ <span class="status-degraded__name"><%= outage.service.name %></span>
7
+ <span class="status-degraded__detail">
8
+ <%= status_label(outage.status).downcase %>
9
+ <% if outage.started_at %>· <%= local_time_ago outage.started_at %><% end %>
10
+ </span>
11
+ </li>
12
+ <% end %>
13
+ </ul>
14
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <div class="status-banner status-banner--<%= status %>">
2
+ <span class="status-banner__dot" aria-hidden="true"></span>
3
+ <%= overall_status_label(status) %>
4
+ </div>
@@ -0,0 +1,34 @@
1
+ <% today_status = history.last.status %>
2
+
3
+ <section class="service">
4
+ <div class="service__row">
5
+ <%= link_to service.name, public_service_incidents_path(service.code), class: "service__name" %>
6
+ <% if service.maintenance_active? %>
7
+ <span class="service__status service__status--maintenance">Under maintenance</span>
8
+ <% else %>
9
+ <span class="service__status service__status--<%= today_status %>">
10
+ <%= status_label(today_status) %>
11
+ </span>
12
+ <% end %>
13
+ </div>
14
+
15
+ <% if description = service.try(:description).presence %>
16
+ <p class="service__description"><%= description %></p>
17
+ <% end %>
18
+
19
+ <div class="uptime">
20
+ <div class="uptime__bars" role="img" aria-label="<%= history.size %>-day uptime for <%= service.name %>">
21
+ <%= render partial: "uptime_bar", collection: history, as: :day %>
22
+ </div>
23
+
24
+ <div class="uptime__meta">
25
+ <span><%= history.size %> days ago</span>
26
+ <% if uptime = uptime_percentage_label(history.map(&:uptime_fraction).compact) %>
27
+ <span class="uptime__meta-percent"><%= uptime %> uptime</span>
28
+ <% else %>
29
+ <span class="uptime__meta-percent">No data yet</span>
30
+ <% end %>
31
+ <span>Today</span>
32
+ </div>
33
+ </div>
34
+ </section>
@@ -0,0 +1,21 @@
1
+ <% if maintenances.any? %>
2
+ <section class="upcoming">
3
+ <h2 class="upcoming__title">Scheduled maintenance</h2>
4
+ <% maintenances.each do |maintenance| %>
5
+ <%= link_to public_incident_path(maintenance), class: "upcoming__item" do %>
6
+ <div class="event__head">
7
+ <span class="event__title"><%= maintenance.title %></span>
8
+ <span class="upcoming__when"><%= local_maintenance_window(maintenance) %></span>
9
+ </div>
10
+
11
+ <% if maintenance.public_services.any? %>
12
+ <p class="event__services"><%= maintenance.public_services.map(&:name).to_sentence %></p>
13
+ <% end %>
14
+
15
+ <% if (update = maintenance.updates.first) && update.body.present? %>
16
+ <p class="event__update"><%= update.body %></p>
17
+ <% end %>
18
+ <% end %>
19
+ <% end %>
20
+ </section>
21
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <span class="uptime__bar uptime__bar--<%= day.status %>" aria-label="<%= day.aria_label %>">
2
+ <span class="uptime__tooltip" aria-hidden="true">
3
+ <span class="uptime__tooltip-date"><%= day.date_label %></span>
4
+ <span class="uptime__tooltip-detail">
5
+ <span class="uptime__tooltip-dot uptime__tooltip-dot--<%= day.status || "none" %>"></span>
6
+ <%= day.detail %>
7
+ </span>
8
+ </span>
9
+ </span>
@@ -0,0 +1,13 @@
1
+ <div class="status-page">
2
+ <h1 class="status-page__title">Status</h1>
3
+
4
+ <%= render "overall_banner", status: @status_page.overall_status %>
5
+ <%= render "degraded_list", degraded: @status_page.outages %>
6
+
7
+ <%= render "active_events", incidents: @status_page.active_incidents, maintenances: @status_page.active_maintenances %>
8
+ <%= render "upcoming_maintenance", maintenances: @status_page.upcoming_maintenances %>
9
+
10
+ <% @status_page.service_histories.each do |service, history| %>
11
+ <%= render "service", service: service, history: history %>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1,29 @@
1
+ xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
2
+ xml.rss(version: "2.0") do
3
+ xml.channel do
4
+ xml.title "Upright Status"
5
+ xml.link request.base_url
6
+ xml.description "Currently degraded services"
7
+ xml.lastBuildDate Time.current.rfc822
8
+
9
+ @status_page.outages.each do |outage|
10
+ xml.item do
11
+ xml.title "#{outage.service.name} — #{status_label(outage.status)}"
12
+ xml.description "#{outage.service.name} is currently #{status_label(outage.status).downcase} #{outage_duration_description(started_at: outage.started_at)}."
13
+ xml.pubDate outage.started_at.rfc822 if outage.started_at
14
+ xml.guid feed_item_guid(outage), isPermaLink: "false"
15
+ end
16
+ end
17
+
18
+ @status_page.active_events.each do |event|
19
+ update = event.updates.first
20
+ xml.item do
21
+ xml.title "#{event.title} — #{status_label(event.status)}"
22
+ xml.description update&.body.to_s
23
+ xml.link public_incident_url(event)
24
+ xml.pubDate (update&.created_at || event.starts_at).rfc822
25
+ xml.guid "incident-#{event.id}-#{update&.id}", isPermaLink: "false"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,19 @@
1
1
  <div class="sign-in-container">
2
2
  <div class="sign-in-box">
3
3
  <h1>Upright</h1>
4
- <%= button_to "Sign in", "/auth/#{Upright.configuration.auth_provider}", method: :post, data: { turbo: false }, class: "btn" %>
4
+ <% if Upright.configuration.auth_provider.to_sym == :static_credentials %>
5
+ <%# Post credentials straight to the OmniAuth callback, but as a same-origin
6
+ form carrying a Rails authenticity token so a cross-site request can't
7
+ forge a sign-in (CVE-2026-67993). %>
8
+ <%= form_with url: "/auth/static_credentials/callback", method: :post, data: { turbo: false } do |form| %>
9
+ <%= form.label :username, "Username" %>
10
+ <%= form.text_field :username, autocomplete: "username" %>
11
+ <%= form.label :password, "Password" %>
12
+ <%= form.password_field :password, autocomplete: "current-password" %>
13
+ <%= form.submit "Sign in", class: "btn" %>
14
+ <% end %>
15
+ <% else %>
16
+ <%= button_to "Sign in", "/auth/#{Upright.configuration.auth_provider}", method: :post, data: { turbo: false }, class: "btn" %>
17
+ <% end %>
5
18
  </div>
6
19
  </div>
data/config/ci.rb CHANGED
@@ -3,5 +3,7 @@
3
3
  CI.run do
4
4
  step "Style: Ruby", "bin/rubocop"
5
5
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
6
+ step "Lint: GitHub Actions (actionlint)", "actionlint"
7
+ step "Lint: GitHub Actions (zizmor)", "zizmor ."
6
8
  step "Tests: Rails", "bin/rails test"
7
9
  end
data/config/importmap.rb CHANGED
@@ -1,18 +1,29 @@
1
- # Pin engine JavaScript
2
- pin "application", to: "upright/application.js"
3
- pin "upright/application", to: "upright/application.js"
4
- pin "upright/controllers/application", to: "upright/controllers/application.js"
5
- pin "upright/controllers", to: "upright/controllers/index.js"
1
+ # Two entry points share this map. The admin app loads "application"; the public
2
+ # status page loads "upright/public". Pins used only by the admin app are
3
+ # preloaded for the "application" entry point alone, so the public page does not
4
+ # fetch Turbo, Stimulus, Leaflet or the charts library.
5
+
6
+ # Admin app
7
+ pin "application", to: "upright/application.js", preload: "application"
8
+ pin "upright/application", to: "upright/application.js", preload: "application"
9
+ pin "upright/controllers/application", to: "upright/controllers/application.js", preload: "application"
10
+ pin "upright/controllers", to: "upright/controllers/index.js", preload: "application"
6
11
 
7
12
  pin_all_from Upright::Engine.root.join("app/javascript/upright/controllers"),
8
13
  under: "upright/controllers",
9
- to: "upright/controllers"
14
+ to: "upright/controllers",
15
+ preload: "application"
16
+
17
+ pin "@hotwired/turbo-rails", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo-rails@8.0.20/+esm", preload: "application"
18
+ pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.20/+esm", preload: "application"
19
+ pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js", preload: "application"
20
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: "application"
21
+ pin "@rails/actioncable/src", to: "https://cdn.jsdelivr.net/npm/@rails/actioncable@8.1.100/src/index.js", preload: "application"
22
+ pin "leaflet", to: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js", preload: "application"
23
+ pin "frappe-charts", to: "https://cdn.jsdelivr.net/npm/frappe-charts@1.6.2/dist/frappe-charts.min.esm.js", preload: "application"
24
+
25
+ # Public status page
26
+ pin "upright/public", to: "upright/public.js", preload: "upright/public"
10
27
 
11
- # Dependencies
12
- pin "@hotwired/turbo-rails", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo-rails@8.0.20/+esm"
13
- pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.20/+esm"
14
- pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
15
- pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
16
- pin "@rails/actioncable/src", to: "https://cdn.jsdelivr.net/npm/@rails/actioncable@8.1.100/src/index.js"
17
- pin "leaflet", to: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js"
18
- pin "frappe-charts", to: "https://cdn.jsdelivr.net/npm/frappe-charts@1.6.2/dist/frappe-charts.min.esm.js"
28
+ # Shared
29
+ pin "local-time", to: "local-time.es2017-esm.js", preload: true
@@ -0,0 +1,5 @@
1
+ Date::DATE_FORMATS[:month_day] ||= "%b %-d"
2
+
3
+ Time::DATE_FORMATS[:month_day_at] ||= "%b %-d, %H:%M"
4
+ Time::DATE_FORMATS[:month_day_at_zone] ||= "%b %-d, %H:%M %Z"
5
+ Time::DATE_FORMATS[:clock_zone] ||= "%H:%M %Z"
@@ -0,0 +1 @@
1
+ Mime::Type.register "application/rss+xml", :rss unless Mime::Type.lookup_by_extension(:rss)
data/config/routes.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  Upright::Engine.routes.draw do
2
- global_subdomain = ->(req) { Upright.configuration.global_subdomain == req.subdomain }
3
- site_subdomain = ->(req) { Upright.configuration.site_subdomains.include?(req.subdomain) }
2
+ global_subdomain = ->(req) { Upright.configuration.global_subdomain == req.subdomain }
3
+ site_subdomain = ->(req) { Upright.configuration.site_subdomains.include?(req.subdomain) }
4
+ metrics_subdomain = ->(req) { global_subdomain.call(req) || Upright.find_site(req.subdomain)&.stores_metrics? }
5
+ public_status = ->(req) {
6
+ Upright.configuration.public_status_enabled &&
7
+ req.subdomain == Upright.configuration.public_status_subdomain
8
+ }
4
9
 
5
10
  constraints global_subdomain do
6
11
  root "sites#index", as: :admin_root
@@ -13,6 +18,12 @@ Upright::Engine.routes.draw do
13
18
  resource :probe_status, only: :show
14
19
  end
15
20
 
21
+ resources :incidents do
22
+ resources :updates, only: :create, controller: "incidents/updates"
23
+ end
24
+ end
25
+
26
+ constraints metrics_subdomain do
16
27
  scope :framed do
17
28
  resource :prometheus, only: :show, controller: :prometheus_proxy
18
29
  resource :alertmanager, only: :show, controller: :alertmanager_proxy
@@ -29,6 +40,8 @@ Upright::Engine.routes.draw do
29
40
 
30
41
  resources :artifacts, only: :show, as: :site_artifacts
31
42
 
43
+ match "traces/:signed_id", to: "traces#show", as: :site_trace, via: [ :get, :options ]
44
+
32
45
  scope :framed do
33
46
  resource :jobs, only: :show
34
47
  end
@@ -36,6 +49,17 @@ Upright::Engine.routes.draw do
36
49
  mount MissionControl::Jobs::Engine, at: "/jobs"
37
50
  end
38
51
 
52
+ constraints public_status do
53
+ scope module: :public, as: :public do
54
+ root "services#index", as: :services_root
55
+ get "feed", to: "services#index", as: :services_feed, defaults: { format: :rss }
56
+ resources :services, only: [], param: :code do
57
+ resources :incidents, only: :index, controller: :service_incidents
58
+ end
59
+ resources :incidents, only: :show
60
+ end
61
+ end
62
+
39
63
  # Base routes (no subdomain constraint)
40
64
  resource :session, only: :destroy
41
65
  root "sites#index"
@@ -0,0 +1,14 @@
1
+ class FixProbeResultDurationPrecision < ActiveRecord::Migration[8.0]
2
+ # `t.decimal :duration` with no precision becomes DECIMAL(10,0) on MySQL,
3
+ # which rounds every duration to a whole second on write. Probe durations
4
+ # are sub-second floats from a monotonic clock, so they were all stored as
5
+ # 0. SQLite preserved the fractional value, which is why this only surfaced
6
+ # after the MySQL migration. Give the column microsecond precision.
7
+ def up
8
+ change_column :upright_probe_results, :duration, :decimal, precision: 10, scale: 6
9
+ end
10
+
11
+ def down
12
+ change_column :upright_probe_results, :duration, :decimal
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ class CreateUprightRollups < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :upright_rollups_probe_rollups do |t|
4
+ t.string :probe_name, null: false
5
+ t.string :probe_service
6
+ t.datetime :period_start, null: false
7
+ t.float :uptime_fraction, null: false
8
+ t.integer :status, default: 0, null: false
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :upright_rollups_probe_rollups, [ :probe_name, :period_start ], unique: true
14
+ add_index :upright_rollups_probe_rollups, [ :probe_service, :period_start ]
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ class CreateUprightIncidents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :upright_incidents do |t|
4
+ t.string :type
5
+ t.string :title, null: false
6
+ t.string :status, null: false
7
+ t.string :impact, null: false
8
+ t.datetime :starts_at, null: false
9
+ t.datetime :ends_at
10
+ t.datetime :resolved_at
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :upright_incidents, [ :resolved_at, :starts_at ]
16
+ add_index :upright_incidents, [ :type, :starts_at ]
17
+
18
+ create_table :upright_incident_updates do |t|
19
+ t.references :incident, null: false, foreign_key: { to_table: :upright_incidents }
20
+ t.string :status, null: false
21
+ t.text :body
22
+
23
+ t.datetime :created_at, null: false
24
+ end
25
+
26
+ create_table :upright_incident_affected_services do |t|
27
+ t.references :incident, null: false, foreign_key: { to_table: :upright_incidents }
28
+ t.string :service_code, null: false
29
+ end
30
+
31
+ add_index :upright_incident_affected_services, [ :incident_id, :service_code ], unique: true
32
+ add_index :upright_incident_affected_services, :service_code
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ class AddProbeIdentityToUprightRollups < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :upright_rollups_probe_rollups, :probe_type, :string
4
+ add_column :upright_rollups_probe_rollups, :probe_target, :string
5
+
6
+ remove_index :upright_rollups_probe_rollups, column: [ :probe_name, :period_start ], unique: true
7
+ add_index :upright_rollups_probe_rollups,
8
+ [ :probe_name, :probe_type, :probe_target, :period_start ],
9
+ unique: true, name: "idx_probe_rollups_identity_period"
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ class AddCreatedByToIncidents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :upright_incidents, :created_by, :string
4
+ add_column :upright_incidents, :updated_by, :string
5
+ add_column :upright_incident_updates, :created_by, :string
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ class AddAutoReportingToUprightIncidents < ActiveRecord::Migration[8.0]
2
+ def up
3
+ add_column :upright_incidents, :auto_created, :boolean, default: false, null: false
4
+ add_column :upright_incidents, :last_seen_down_at, :datetime
5
+ add_column :upright_incidents, :recovery_started_at, :datetime
6
+
7
+ create_table :upright_incident_automatic_reports do |t|
8
+ t.references :incident, null: false, foreign_key: { to_table: :upright_incidents }, index: { unique: true }
9
+ t.string :service_code, null: false, index: { unique: true }
10
+ t.timestamps
11
+ end
12
+ end
13
+
14
+ def down
15
+ drop_table :upright_incident_automatic_reports, if_exists: true
16
+ remove_index :upright_incidents, :auto_service_code, if_exists: true
17
+ remove_column :upright_incidents, :auto_service_code, if_exists: true
18
+ remove_column :upright_incidents, :recovery_started_at, if_exists: true
19
+ remove_column :upright_incidents, :last_seen_down_at
20
+ remove_column :upright_incidents, :auto_created
21
+ end
22
+ end