upright 0.1.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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +10 -0
  3. data/README.md +455 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/upright/_global.css +104 -0
  6. data/app/assets/stylesheets/upright/artifact.css +148 -0
  7. data/app/assets/stylesheets/upright/base.css +68 -0
  8. data/app/assets/stylesheets/upright/buttons.css +21 -0
  9. data/app/assets/stylesheets/upright/dashboard.css +287 -0
  10. data/app/assets/stylesheets/upright/forms.css +104 -0
  11. data/app/assets/stylesheets/upright/header.css +124 -0
  12. data/app/assets/stylesheets/upright/layout.css +100 -0
  13. data/app/assets/stylesheets/upright/map.css +25 -0
  14. data/app/assets/stylesheets/upright/pagination.css +45 -0
  15. data/app/assets/stylesheets/upright/probes.css +72 -0
  16. data/app/assets/stylesheets/upright/reset.css +26 -0
  17. data/app/assets/stylesheets/upright/tables.css +63 -0
  18. data/app/assets/stylesheets/upright/typography.css +27 -0
  19. data/app/assets/stylesheets/upright/uptime-bars.css +154 -0
  20. data/app/controllers/concerns/upright/authentication.rb +21 -0
  21. data/app/controllers/concerns/upright/subdomain_scoping.rb +18 -0
  22. data/app/controllers/upright/alertmanager_proxy_controller.rb +21 -0
  23. data/app/controllers/upright/application_controller.rb +12 -0
  24. data/app/controllers/upright/artifacts_controller.rb +5 -0
  25. data/app/controllers/upright/dashboards/uptimes_controller.rb +6 -0
  26. data/app/controllers/upright/jobs_controller.rb +4 -0
  27. data/app/controllers/upright/probe_results_controller.rb +17 -0
  28. data/app/controllers/upright/prometheus_proxy_controller.rb +62 -0
  29. data/app/controllers/upright/sessions_controller.rb +29 -0
  30. data/app/controllers/upright/sites_controller.rb +5 -0
  31. data/app/helpers/upright/application_helper.rb +11 -0
  32. data/app/helpers/upright/dashboards_helper.rb +31 -0
  33. data/app/helpers/upright/probe_results_helper.rb +49 -0
  34. data/app/javascript/upright/application.js +2 -0
  35. data/app/javascript/upright/controllers/application.js +5 -0
  36. data/app/javascript/upright/controllers/form_controller.js +7 -0
  37. data/app/javascript/upright/controllers/index.js +4 -0
  38. data/app/javascript/upright/controllers/popover_controller.js +15 -0
  39. data/app/javascript/upright/controllers/probe_results_chart_controller.js +79 -0
  40. data/app/javascript/upright/controllers/results_table_controller.js +16 -0
  41. data/app/javascript/upright/controllers/sites_map_controller.js +33 -0
  42. data/app/jobs/upright/application_job.rb +2 -0
  43. data/app/jobs/upright/probe_check_job.rb +42 -0
  44. data/app/models/concerns/upright/exception_recording.rb +38 -0
  45. data/app/models/concerns/upright/playwright/form_authentication.rb +27 -0
  46. data/app/models/concerns/upright/playwright/helpers.rb +7 -0
  47. data/app/models/concerns/upright/playwright/lifecycle.rb +44 -0
  48. data/app/models/concerns/upright/playwright/logging.rb +87 -0
  49. data/app/models/concerns/upright/playwright/otel_tracing.rb +137 -0
  50. data/app/models/concerns/upright/playwright/video_recording.rb +60 -0
  51. data/app/models/concerns/upright/probe_yaml_source.rb +10 -0
  52. data/app/models/concerns/upright/probeable.rb +125 -0
  53. data/app/models/concerns/upright/staggerable.rb +22 -0
  54. data/app/models/concerns/upright/traceroute/otel_tracing.rb +108 -0
  55. data/app/models/upright/application_record.rb +3 -0
  56. data/app/models/upright/artifact.rb +61 -0
  57. data/app/models/upright/current.rb +9 -0
  58. data/app/models/upright/http/request.rb +59 -0
  59. data/app/models/upright/http/response.rb +55 -0
  60. data/app/models/upright/playwright/authenticator/base.rb +128 -0
  61. data/app/models/upright/playwright/storage_state.rb +31 -0
  62. data/app/models/upright/probe_result.rb +31 -0
  63. data/app/models/upright/probes/http_probe.rb +102 -0
  64. data/app/models/upright/probes/playwright/base.rb +48 -0
  65. data/app/models/upright/probes/smtp_probe.rb +48 -0
  66. data/app/models/upright/probes/traceroute_probe.rb +32 -0
  67. data/app/models/upright/probes/uptime/summary.rb +36 -0
  68. data/app/models/upright/probes/uptime.rb +36 -0
  69. data/app/models/upright/traceroute/hop.rb +49 -0
  70. data/app/models/upright/traceroute/ip_metadata_lookup.rb +107 -0
  71. data/app/models/upright/traceroute/mtr_parser.rb +47 -0
  72. data/app/models/upright/traceroute/result.rb +57 -0
  73. data/app/models/upright/user.rb +14 -0
  74. data/app/views/layouts/upright/_header.html.erb +23 -0
  75. data/app/views/layouts/upright/application.html.erb +25 -0
  76. data/app/views/upright/active_storage/attachments/_attachment.html.erb +21 -0
  77. data/app/views/upright/alertmanager_proxy/show.html.erb +1 -0
  78. data/app/views/upright/artifacts/show.html.erb +9 -0
  79. data/app/views/upright/dashboards/_uptime_bars.html.erb +17 -0
  80. data/app/views/upright/dashboards/_uptime_probe_row.html.erb +22 -0
  81. data/app/views/upright/dashboards/uptimes/show.html.erb +17 -0
  82. data/app/views/upright/jobs/show.html.erb +1 -0
  83. data/app/views/upright/probe_results/_pagination.html.erb +19 -0
  84. data/app/views/upright/probe_results/index.html.erb +72 -0
  85. data/app/views/upright/prometheus_proxy/show.html.erb +1 -0
  86. data/app/views/upright/sessions/new.html.erb +6 -0
  87. data/app/views/upright/sites/index.html.erb +22 -0
  88. data/config/brakeman.ignore +39 -0
  89. data/config/ci.rb +7 -0
  90. data/config/importmap.rb +18 -0
  91. data/config/routes.rb +41 -0
  92. data/db/migrate/20250114000001_create_upright_probe_results.rb +19 -0
  93. data/lib/generators/upright/install/install_generator.rb +83 -0
  94. data/lib/generators/upright/install/templates/alertmanager.yml +14 -0
  95. data/lib/generators/upright/install/templates/deploy.yml +118 -0
  96. data/lib/generators/upright/install/templates/development_alertmanager.yml +11 -0
  97. data/lib/generators/upright/install/templates/development_prometheus.yml +12 -0
  98. data/lib/generators/upright/install/templates/docker-compose.yml +38 -0
  99. data/lib/generators/upright/install/templates/http_probes.yml +14 -0
  100. data/lib/generators/upright/install/templates/omniauth.rb +8 -0
  101. data/lib/generators/upright/install/templates/otel_collector.yml +24 -0
  102. data/lib/generators/upright/install/templates/prometheus.yml +10 -0
  103. data/lib/generators/upright/install/templates/puma.rb +40 -0
  104. data/lib/generators/upright/install/templates/sites.yml +26 -0
  105. data/lib/generators/upright/install/templates/smtp_probes.yml +9 -0
  106. data/lib/generators/upright/install/templates/upright.rb +21 -0
  107. data/lib/generators/upright/install/templates/upright.rules.yml +256 -0
  108. data/lib/generators/upright/playwright_probe/playwright_probe_generator.rb +30 -0
  109. data/lib/generators/upright/playwright_probe/templates/authenticator.rb.tt +14 -0
  110. data/lib/generators/upright/playwright_probe/templates/probe.rb.tt +14 -0
  111. data/lib/omniauth/strategies/static_credentials.rb +57 -0
  112. data/lib/tasks/upright_tasks.rake +4 -0
  113. data/lib/upright/configuration.rb +106 -0
  114. data/lib/upright/engine.rb +157 -0
  115. data/lib/upright/metrics.rb +62 -0
  116. data/lib/upright/playwright/collect_performance_metrics.js +36 -0
  117. data/lib/upright/site.rb +49 -0
  118. data/lib/upright/tracing.rb +49 -0
  119. data/lib/upright/version.rb +3 -0
  120. data/lib/upright.rb +68 -0
  121. metadata +513 -0
@@ -0,0 +1,57 @@
1
+ require "open3"
2
+
3
+ class Upright::Traceroute::Result
4
+ PROBE_COUNT = 10
5
+ MAX_HOPS = 30
6
+
7
+ attr_reader :host, :hops, :raw_json
8
+
9
+ def self.for(host)
10
+ new(host).tap(&:run)
11
+ end
12
+
13
+ def initialize(host)
14
+ @host = host
15
+ @hops = []
16
+ end
17
+
18
+ def run
19
+ @raw_json = run_mtr
20
+ @hops = build_hops(Upright::Traceroute::MtrParser.new(@raw_json).parse)
21
+ end
22
+
23
+ def reached_destination?
24
+ hops.any? && hops.last.responded?
25
+ end
26
+
27
+ private
28
+ def run_mtr
29
+ stdout, _status = Open3.capture2(
30
+ "mtr",
31
+ "--json",
32
+ "--aslookup",
33
+ "--report-cycles", PROBE_COUNT.to_s,
34
+ "--max-ttl", MAX_HOPS.to_s,
35
+ host.to_s
36
+ )
37
+
38
+ Rails.logger.info stdout
39
+
40
+ stdout
41
+ end
42
+
43
+ def build_hops(parsed_hops)
44
+ ips = parsed_hops.map { |h| h[:ip] }.compact
45
+ metadata_by_ip = Upright::Traceroute::IpMetadataLookup.for_many(ips)
46
+
47
+ parsed_hops.map.with_index do |hop_data, index|
48
+ metadata = metadata_by_ip[hop_data[:ip]] || {}
49
+
50
+ Upright::Traceroute::Hop.new(
51
+ **hop_data,
52
+ **metadata,
53
+ geohash: index == 0 ? Upright.current_site.geohash : metadata[:geohash]
54
+ )
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,14 @@
1
+ class Upright::User
2
+ include ActiveModel::Model
3
+ include ActiveModel::Attributes
4
+
5
+ attribute :email, :string
6
+ attribute :name, :string
7
+
8
+ def self.from_omniauth(auth)
9
+ new(
10
+ email: auth.info.email,
11
+ name: auth.info.name
12
+ )
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ <header class="header">
2
+ <div class="header__left">
3
+ <%= link_to "Upright", root_url(subdomain: Upright.configuration.global_subdomain), class: "header__logo" %>
4
+ <% if Upright::Current.site.present? %>
5
+ <span class="header__site"><%= Upright::Current.site.city %></span>
6
+ <% end %>
7
+ </div>
8
+
9
+ <nav class="header__nav">
10
+ <%= link_to "Probes", upright.site_root_url(subdomain: current_or_default_site.code), class: "header__link" %>
11
+ <%= link_to "Uptime", upright.dashboards_uptime_url(subdomain: Upright.configuration.global_subdomain), class: "header__link" %>
12
+ <span class="header__divider"></span>
13
+ <%= link_to "Jobs", upright.jobs_url(subdomain: current_or_default_site.code), class: "header__link" %>
14
+ <span class="header__divider"></span>
15
+ <%= link_to "Prometheus", upright.prometheus_url(subdomain: Upright.configuration.global_subdomain), class: "header__link" %>
16
+ <%= link_to "Alertmanager", upright.alertmanager_url(subdomain: Upright.configuration.global_subdomain), class: "header__link" %>
17
+ </nav>
18
+
19
+ <div class="header__user">
20
+ <span><%= Upright::Current.user.name %></span>
21
+ <%= button_to "Sign out", upright.session_path, method: :delete, data: { turbo: false }, class: "btn" %>
22
+ </div>
23
+ </header>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Upright</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
7
+ <meta name="theme-color" content="#1f2937" media="(prefers-color-scheme: dark)">
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
11
+ <%= upright_stylesheet_link_tag "data-turbo-track": "reload" %>
12
+ <%= javascript_importmap_tags %>
13
+ </head>
14
+
15
+ <body>
16
+ <% if signed_in? %>
17
+ <%= render "layouts/upright/header" %>
18
+ <main class="main">
19
+ <%= yield %>
20
+ </main>
21
+ <% else %>
22
+ <%= yield %>
23
+ <% end %>
24
+ </body>
25
+ </html>
@@ -0,0 +1,21 @@
1
+ <details class="artifact" data-controller="popover" data-action="popover#loadFrame" data-results-table-target="popover">
2
+ <summary title="<%= attachment.filename %>"><%= artifact_icon(attachment) %></summary>
3
+ <div class="artifact__content">
4
+ <header class="artifact__header">
5
+ <div class="artifact__header-text">
6
+ <% if local_assigns[:probe_result] %>
7
+ <div class="artifact__probe"><%= probe_type_icon(probe_result.probe_type) %> <%= probe_result.probe_name %></div>
8
+ <% if probe_result.probe_target.present? %>
9
+ <div class="artifact__probe-target"><%= probe_result.probe_target %></div>
10
+ <% end %>
11
+ <div class="artifact__probe-time" title="<%= probe_result.created_at.to_fs(:db) %>"><%= time_ago_in_words(probe_result.created_at) %> ago</div>
12
+ <% end %>
13
+ <h3 class="artifact__title"><%= artifact_icon(attachment) %> <%= attachment.filename %></h3>
14
+ </div>
15
+ <button type="button" class="artifact__close" data-action="popover#close" aria-label="Close">×</button>
16
+ </header>
17
+ <div class="artifact__body">
18
+ <%= turbo_frame_tag dom_id(attachment), src: upright.site_artifact_path(attachment), loading: :lazy, data: { popover_target: "frame" } %>
19
+ </div>
20
+ </div>
21
+ </details>
@@ -0,0 +1 @@
1
+ <iframe src="<%= alertmanager_proxy_path %>" class="service-frame"></iframe>
@@ -0,0 +1,9 @@
1
+ <%= turbo_frame_tag dom_id(@artifact) do %>
2
+ <% if @artifact.video? %>
3
+ <video controls width="960" preload="none" autoplay muted>
4
+ <source src="<%= main_app.rails_blob_path(@artifact, disposition: :inline) %>" type="video/webm">
5
+ </video>
6
+ <% else %>
7
+ <pre><%= @artifact.download %></pre>
8
+ <% end %>
9
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <% if probes.empty? %>
2
+ <div class="no-data-message">
3
+ No probe data available for the selected filters.
4
+ </div>
5
+ <% else %>
6
+ <div class="uptime-bars">
7
+ <div class="uptime-bars__header">
8
+ <div>Probe</div>
9
+ <div>Past 30 days</div>
10
+ <div>Uptime</div>
11
+ </div>
12
+
13
+ <% probes.each do |probe| %>
14
+ <%= render "upright/dashboards/uptime_probe_row", probe: probe, dates: date_range %>
15
+ <% end %>
16
+ </div>
17
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <div class="uptime-bars__row">
2
+ <%= link_to upright.site_root_url(subdomain: Upright.sites.first.code, probe_type: probe.type, probe_name: probe.name), class: "uptime-bars__probe" do %>
3
+ <%= probe_type_icon(probe.type) %>
4
+ <span class="probe__name"><%= probe.name %></span>
5
+ <% end %>
6
+ <div class="uptime-bars__days">
7
+ <% dates.each do |date| %>
8
+ <% uptime = probe.uptime_for_date(date) %>
9
+ <% if uptime.nil? %>
10
+ <div class="uptime-bar uptime-bar--no-data" title="<%= date.to_fs(:short) %>: No data"></div>
11
+ <% else %>
12
+ <% uptime_percent = uptime * 100 %>
13
+ <% downtime_minutes = ((1 - uptime) * 24 * 60).round %>
14
+ <div class="uptime-bar uptime-bar--<%= uptime_label(uptime_percent) %>"
15
+ title="<%= uptime_bar_tooltip(date, uptime_percent, downtime_minutes) %>"></div>
16
+ <% end %>
17
+ <% end %>
18
+ </div>
19
+ <div class="uptime-bars__uptime uptime-bars__uptime--<%= uptime_label(probe.overall_uptime) %>">
20
+ <%= number_with_precision(probe.overall_uptime, precision: 2) %>%
21
+ </div>
22
+ </div>
@@ -0,0 +1,17 @@
1
+ <div class="dashboard uptime-dashboard">
2
+ <header class="dashboard-header">
3
+ <h1>Uptime Dashboard</h1>
4
+
5
+ <div class="dashboard-filters">
6
+ <%= form_with url: dashboards_uptime_path, method: :get, data: { controller: "form", turbo_frame: :uptime_bars } do %>
7
+ <%= collection_select nil, :probe_type, Upright::Probeable::TYPES, :itself, :titleize,
8
+ { selected: @probe_type },
9
+ { class: "input--select", data: { action: "change->form#submit" } } %>
10
+ <% end %>
11
+ </div>
12
+ </header>
13
+
14
+ <%= turbo_frame_tag :uptime_bars do %>
15
+ <%= render "upright/dashboards/uptime_bars", probes: @probes %>
16
+ <% end %>
17
+ </div>
@@ -0,0 +1 @@
1
+ <iframe src="<%= mission_control_jobs.root_path %>" class="service-frame"></iframe>
@@ -0,0 +1,19 @@
1
+ <% if page.recordset.page_count > 1 %>
2
+ <nav class="pagination">
3
+ <% if page.first? %>
4
+ <span class="pagination__prev pagination__prev--disabled">Previous</span>
5
+ <% else %>
6
+ <%= link_to "Previous", url_for(request.query_parameters.merge(page: page.number - 1)), class: "pagination__prev" %>
7
+ <% end %>
8
+
9
+ <span class="pagination__info">
10
+ Page <%= page.number %> of <%= page.recordset.page_count %>
11
+ </span>
12
+
13
+ <% if page.last? %>
14
+ <span class="pagination__next pagination__next--disabled">Next</span>
15
+ <% else %>
16
+ <%= link_to "Next", url_for(request.query_parameters.merge(page: page.next_param)), class: "pagination__next" %>
17
+ <% end %>
18
+ </nav>
19
+ <% end %>
@@ -0,0 +1,72 @@
1
+ <div class="container">
2
+ <aside>
3
+ <ul>
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>
9
+ </ul>
10
+ </aside>
11
+
12
+ <main>
13
+ <h1>Probe Results</h1>
14
+ <p><%= results_summary(@page) %></p>
15
+
16
+ <div class="filters">
17
+ <%= form_with url: site_root_path, method: :get, data: { controller: "form" } do |f| %>
18
+ <%= hidden_field_tag :probe_type, params[:probe_type] if params[:probe_type].present? %>
19
+ <label for="probe_name">Name</label>
20
+ <%= select_tag :probe_name,
21
+ options_for_select([["All", ""]] + @probe_names, params[:probe_name]),
22
+ class: "input--select",
23
+ data: { action: "change->form#submit" } %>
24
+ <label for="status">Status</label>
25
+ <%= select_tag :status,
26
+ options_for_select([["All", ""]] + Upright::ProbeResult.statuses.keys.map { |s| [s.titleize, s] }, params[:status]),
27
+ class: "input--select",
28
+ data: { action: "change->form#submit" } %>
29
+ <% end %>
30
+ </div>
31
+
32
+ <div class="probe-results__chart"
33
+ data-controller="probe-results-chart"
34
+ data-probe-results-chart-results-value="<%= @chart_data.to_json %>">
35
+ </div>
36
+
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ <th>Probe Name</th>
41
+ <th>Status</th>
42
+ <th>Duration</th>
43
+ <th>Time</th>
44
+ <th>Artifacts</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody data-controller="results-table" data-action="click->results-table#openArtifact">
48
+ <% @page.records.each do |result| %>
49
+ <tr>
50
+ <td class="probe__cell">
51
+ <span class="probe__icon"><%= probe_type_icon(result.probe_type) %></span>
52
+ <div class="probe__details">
53
+ <div class="probe__name"><%= result.probe_name %></div>
54
+ <% if result.probe_target.present? %>
55
+ <div class="probe__target"><%= result.probe_target %></div>
56
+ <% end %>
57
+ </div>
58
+ </td>
59
+ <td class="status--<%= result.status %>"><%= result.status.titleize %></td>
60
+ <td><%= result.duration ? "#{sprintf('%.3f', result.duration)}s" : "N/A" %></td>
61
+ <td title="<%= result.created_at.to_fs(:db) %>"><%= time_ago_in_words(result.created_at) %> ago</td>
62
+ <td>
63
+ <%= render partial: "upright/active_storage/attachments/attachment", collection: result.artifacts, as: :attachment, locals: { probe_result: result } %>
64
+ </td>
65
+ </tr>
66
+ <% end %>
67
+ </tbody>
68
+ </table>
69
+
70
+ <%= render "pagination", page: @page %>
71
+ </main>
72
+ </div>
@@ -0,0 +1 @@
1
+ <iframe src="<%= prometheus_proxy_path %>" class="service-frame"></iframe>
@@ -0,0 +1,6 @@
1
+ <div class="sign-in-container">
2
+ <div class="sign-in-box">
3
+ <h1>Upright</h1>
4
+ <%= button_to "Sign in", "/auth/#{Upright.configuration.auth_provider}", method: :post, data: { turbo: false }, class: "btn" %>
5
+ </div>
6
+ </div>
@@ -0,0 +1,22 @@
1
+ <main>
2
+ <h1>Sites</h1>
3
+
4
+ <div id="sites-map" data-controller="sites-map" data-sites-map-sites-value="<%= @sites.map(&:to_leaflet).to_json %>"></div>
5
+
6
+ <table>
7
+ <thead>
8
+ <tr>
9
+ <th>Host</th>
10
+ <th>City</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% @sites.each do |site| %>
15
+ <tr>
16
+ <td><%= link_to site.host, site.url %></td>
17
+ <td><%= site.city %></td>
18
+ </tr>
19
+ <% end %>
20
+ </tbody>
21
+ </table>
22
+ </main>
@@ -0,0 +1,39 @@
1
+ {
2
+ "ignored_warnings": [
3
+ {
4
+ "warning_type": "Cross-Site Scripting",
5
+ "warning_code": 4,
6
+ "fingerprint": "842fc181b52f46f9a26f463b023f2567ff74c2b5b2b79c6004bab29f2e62203c",
7
+ "check_name": "LinkToHref",
8
+ "message": "Potentially unsafe model attribute in `link_to` href",
9
+ "file": "app/views/upright/sites/index.html.erb",
10
+ "line": 16,
11
+ "link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
12
+ "code": "link_to(Upright::Site.new.host, Upright::Site.new.url)",
13
+ "render_path": [
14
+ {
15
+ "type": "controller",
16
+ "class": "Upright::SitesController",
17
+ "method": "index",
18
+ "line": 5,
19
+ "file": "app/controllers/upright/sites_controller.rb",
20
+ "rendered": {
21
+ "name": "upright/sites/index",
22
+ "file": "app/views/upright/sites/index.html.erb"
23
+ }
24
+ }
25
+ ],
26
+ "location": {
27
+ "type": "template",
28
+ "template": "upright/sites/index"
29
+ },
30
+ "user_input": "Upright::Site.new.url",
31
+ "confidence": "Weak",
32
+ "cwe_id": [
33
+ 79
34
+ ],
35
+ "note": "URL is generated from Rails route helpers, not user input"
36
+ }
37
+ ],
38
+ "brakeman_version": "7.1.2"
39
+ }
data/config/ci.rb ADDED
@@ -0,0 +1,7 @@
1
+ # Run using bin/ci
2
+
3
+ CI.run do
4
+ step "Style: Ruby", "bin/rubocop"
5
+ step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
6
+ step "Tests: Rails", "bin/rails test"
7
+ end
@@ -0,0 +1,18 @@
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"
6
+
7
+ pin_all_from Upright::Engine.root.join("app/javascript/upright/controllers"),
8
+ under: "upright/controllers",
9
+ to: "upright/controllers"
10
+
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"
data/config/routes.rb ADDED
@@ -0,0 +1,41 @@
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) }
4
+
5
+ constraints global_subdomain do
6
+ root "sites#index", as: :admin_root
7
+
8
+ resource :session, only: [ :new, :create ], as: :admin_session
9
+ match "auth/:provider/callback", to: "sessions#create", as: :auth_callback, via: [ :get, :post ]
10
+
11
+ namespace :dashboards do
12
+ resource :uptime, only: :show
13
+ end
14
+
15
+ scope :framed do
16
+ resource :prometheus, only: :show, controller: :prometheus_proxy
17
+ resource :alertmanager, only: :show, controller: :alertmanager_proxy
18
+ end
19
+
20
+ post "prometheus/api/v1/otlp/v1/metrics", to: "prometheus_proxy#otlp"
21
+
22
+ match "prometheus(/*path)", to: "prometheus_proxy#proxy", via: :all, as: :prometheus_proxy
23
+ match "alertmanager(/*path)", to: "alertmanager_proxy#proxy", via: :all, as: :alertmanager_proxy
24
+ end
25
+
26
+ constraints site_subdomain do
27
+ root "probe_results#index", as: :site_root
28
+
29
+ resources :artifacts, only: :show, as: :site_artifacts
30
+
31
+ scope :framed do
32
+ resource :jobs, only: :show
33
+ end
34
+
35
+ mount MissionControl::Jobs::Engine, at: "/jobs"
36
+ end
37
+
38
+ # Base routes (no subdomain constraint)
39
+ resource :session, only: :destroy
40
+ root "sites#index"
41
+ end
@@ -0,0 +1,19 @@
1
+ class CreateUprightProbeResults < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :upright_probe_results do |t|
4
+ t.string :probe_name
5
+ t.string :probe_type
6
+ t.string :probe_target
7
+ t.string :probe_service
8
+ t.decimal :duration
9
+ t.integer :status
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :upright_probe_results, :probe_type
15
+ add_index :upright_probe_results, :probe_name
16
+ add_index :upright_probe_results, :status
17
+ add_index :upright_probe_results, :created_at
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ module Upright
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ desc "Install Upright engine into your application"
7
+
8
+ def copy_initializers
9
+ template "upright.rb", "config/initializers/upright.rb"
10
+ template "omniauth.rb", "config/initializers/omniauth.rb"
11
+ end
12
+
13
+ def copy_sites_config
14
+ template "sites.yml", "config/sites.yml"
15
+ end
16
+
17
+ def create_probe_directories
18
+ empty_directory "probes"
19
+ empty_directory "probes/authenticators"
20
+ template "http_probes.yml", "probes/http_probes.yml"
21
+ template "smtp_probes.yml", "probes/smtp_probes.yml"
22
+ end
23
+
24
+ def copy_observability_configs
25
+ template "prometheus.yml", "config/prometheus/prometheus.yml"
26
+ template "upright.rules.yml", "config/prometheus/rules/upright.rules.yml"
27
+ template "alertmanager.yml", "config/alertmanager/alertmanager.yml"
28
+ template "otel_collector.yml", "config/otel_collector.yml"
29
+ template "development_prometheus.yml", "config/prometheus/development/prometheus.yml"
30
+ template "development_alertmanager.yml", "config/alertmanager/development/alertmanager.yml"
31
+ end
32
+
33
+ def copy_dev_services
34
+ template "docker-compose.yml", "docker-compose.yml"
35
+ end
36
+
37
+ def copy_deploy_config
38
+ template "deploy.yml", "config/deploy.yml"
39
+ end
40
+
41
+ def copy_puma_config
42
+ template "puma.rb", "config/puma.rb"
43
+ end
44
+
45
+ def add_routes
46
+ route 'mount Upright::Engine => "/", as: :upright'
47
+ end
48
+
49
+ def show_post_install_message
50
+ say ""
51
+ say "Upright has been installed!", :green
52
+ say ""
53
+ say "Next steps:"
54
+ say " 1. Run migrations: bin/rails db:migrate"
55
+ say " 2. Configure your servers in config/deploy.yml"
56
+ say " 3. Configure sites in config/sites.yml"
57
+ say " 4. Add probes in probes/*.yml"
58
+ say " 5. Set ADMIN_PASSWORD env var (default: upright)"
59
+ say ""
60
+ say "For production, review config/initializers/upright.rb and update:"
61
+ say " config.hostname = \"honcho-upright.com\""
62
+ say ""
63
+ say "Start dev services (Prometheus, Alertmanager, Playwright):"
64
+ say " docker compose up -d"
65
+ say ""
66
+ say "Start the development server with: bin/dev"
67
+ say ""
68
+ say "Then access your app at:"
69
+ say " http://app.#{app_name}.localhost:3000"
70
+ say ""
71
+ end
72
+
73
+ private
74
+ def app_name
75
+ Rails.application.class.module_parent_name.underscore.dasherize
76
+ end
77
+
78
+ def app_domain
79
+ "#{app_name}.example.com"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,14 @@
1
+ global:
2
+ resolve_timeout: 5m
3
+
4
+ route:
5
+ receiver: default
6
+ group_by: ['alertname', 'probe_name']
7
+ group_wait: 30s
8
+ group_interval: 5m
9
+ repeat_interval: 4h
10
+
11
+ receivers:
12
+ - name: default
13
+ # webhook_configs:
14
+ # - url: 'http://your-webhook-url'