switch error handling to sentry

This commit is contained in:
nora 2025-12-29 16:19:10 -05:00 committed by GitHub
parent 69c2507793
commit 3c70ca8c28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 532 additions and 416 deletions

View file

@ -47,7 +47,8 @@ gem "vite_rails"
gem "pundit", "~> 2.5"
gem "honeybadger", "~> 5.28"
gem "sentry-ruby", "~> 5.22"
gem "sentry-rails", "~> 5.22"
gem "http", "~> 5.2"
@ -152,4 +153,3 @@ end
gem "premailer-rails", "~> 1.12"
gem "openssl", "~> 3.3"

View file

@ -254,9 +254,6 @@ GEM
hashids (~> 1.0)
hashids (1.0.6)
hashie (5.0.0)
honeybadger (5.28.0)
logger
ostruct
htmlentities (4.4.2)
http (5.2.0)
addressable (~> 2.8)
@ -537,6 +534,12 @@ GEM
securerandom (0.4.1)
semantic_logger (4.16.1)
concurrent-ruby (~> 1.0)
sentry-rails (5.28.1)
railties (>= 5.0)
sentry-ruby (~> 5.28.1)
sentry-ruby (5.28.1)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
serve_byte_range (1.0.0)
rack (>= 1.0)
slack-ruby-client (2.6.0)
@ -636,7 +639,6 @@ DEPENDENCIES
geocoder (~> 1.8)
good_job (~> 4.10)
hashid-rails (~> 1.4)
honeybadger (~> 5.28)
http (~> 5.2)
image_processing (~> 1.2)
jb (~> 0.8.2)
@ -667,6 +669,8 @@ DEPENDENCIES
rubocop-rails-omakase
ruby-vips (~> 2.2)
saml2 (~> 3.2)
sentry-rails (~> 5.22)
sentry-ruby (~> 5.22)
slack-ruby-client (~> 2.6)
slocks (~> 0.1.0)
superform (~> 0.5.1)

View file

@ -1,19 +1,38 @@
# frozen_string_literal: true
class Components::Banner < Components::Base
def initialize(kind:)
def initialize(kind:, event_id: nil)
@kind = kind
@event_id = event_id
end
def view_template(&block)
div(class: "banner flex #{banner_class}") do
render_icon
yield
div(class: "flex-1") do
yield
render_event_id if @event_id.present?
end
end
end
private
def render_event_id
div(class: "error-id-container mt-1") do
small(class: "error-id-text text-sm opacity-75") do
plain "Error ID: "
code(
class: "error-id-code cursor-pointer hover:opacity-80",
data_error_id: @event_id,
onclick: "copyErrorId(this)",
title: "Click to copy"
) { @event_id }
span(class: "copy-feedback ml-1 hidden") { " ✓ Copied!" }
end
end
end
def banner_class
case @kind.to_sym
when :success then "success"

View file

@ -47,9 +47,18 @@ class ApplicationController < ActionController::Base
end
def set_honeybadger_context
Honeybadger.context({
identity_id: current_identity&.id
})
return unless current_identity
Sentry.set_user(
id: current_identity.public_id, # Use public_id (ident!xyz) not database ID
email: current_identity.primary_email
)
Sentry.set_context(:identity, {
identity_public_id: current_identity.public_id,
identity_email: current_identity.primary_email,
slack_id: current_identity.slack_id
}.compact)
end
# Best-effort country detection from request IP; returns ISO3166 alpha-2.
@ -86,8 +95,19 @@ class ApplicationController < ActionController::Base
end
rescue_from ActiveRecord::RecordNotFound do |e|
event_id = Sentry.capture_exception(e)
flash[:error] = "sorry, couldn't find that object... (404)"
redirect_to root_path
flash[:sentry_event_id] = event_id if event_id
redirect_to root_path unless request.path == "/"
end
rescue_from StandardError do |e|
event_id = Sentry.capture_exception(e)
flash[:error] = "Something went wrong. Please try again."
flash[:sentry_event_id] = event_id if event_id
raise e if Rails.env.development?
redirect_to root_path unless request.path == "/"
end
private

View file

@ -51,22 +51,54 @@ module Backend
end
def set_honeybadger_context
Honeybadger.context({
user_id: current_user&.id,
return unless current_identity
# Set user context with public_id
Sentry.set_user(
id: current_identity.public_id, # Use identity public_id (ident!xyz)
username: current_user&.username,
email: current_identity.primary_email
)
# Set backend user context
Sentry.set_context(:user, {
user_username: current_user&.username,
identity_id: current_identity&.id,
identity_email: current_identity&.primary_email
})
user_identity_public_id: current_user&.identity&.public_id,
user_slack_id: current_user&.slack_id,
is_super_admin: current_user&.super_admin?,
is_program_manager: current_user&.program_manager?,
can_break_glass: current_user&.can_break_glass?
}.compact)
# Set identity context (the identity being acted upon)
Sentry.set_context(:identity, {
identity_public_id: current_identity.public_id,
identity_email: current_identity.primary_email,
slack_id: current_identity.slack_id
}.compact)
# Set impersonation context if applicable
if current_impersonator
Sentry.set_context(:impersonation, {
impersonator_username: current_impersonator.username,
impersonator_identity_public_id: current_impersonator.identity&.public_id,
is_impersonating: true
}.compact)
end
end
rescue_from Pundit::NotAuthorizedError do |e|
event_id = Sentry.capture_exception(e)
flash[:error] = "you don't seem to be authorized to do that?"
redirect_to backend_root_path
flash[:sentry_event_id] = event_id if event_id
redirect_to backend_root_path unless request.path == "/backend" || request.path == "/backend/"
end
rescue_from ActiveRecord::RecordNotFound do |e|
event_id = Sentry.capture_exception(e)
flash[:error] = "sorry, couldn't find that object... (404)"
redirect_to backend_root_path
flash[:sentry_event_id] = event_id if event_id
redirect_to backend_root_path unless request.path == "/backend" || request.path == "/backend/"
end
end
end

View file

@ -0,0 +1,25 @@
class ErrorsController < ActionController::Base
layout "errors"
def not_found
@event_id = request.env["sentry.error_event_id"]
render status: :not_found
rescue => e
# Last resort: render plain text to avoid infinite loops
render plain: "404 - Page Not Found", status: :not_found
end
def unprocessable_entity
@event_id = request.env["sentry.error_event_id"]
render status: :unprocessable_entity
rescue => e
render plain: "422 - Unprocessable Entity", status: :unprocessable_entity
end
def internal_server_error
@event_id = request.env["sentry.error_event_id"]
render status: :internal_server_error
rescue => e
render plain: "500 - Internal Server Error", status: :internal_server_error
end
end

View file

@ -294,12 +294,13 @@ class LoginsController < ApplicationController
slack_result
else
Rails.logger.error "Slack provisioning failed for #{@identity.id}: #{slack_result[:error]}"
Honeybadger.notify(
Sentry.capture_message(
"Slack provisioning failed on first login",
context: {
identity_id: @identity.id,
email: @identity.primary_email,
error: slack_result[:error]
level: :error,
extra: {
identity_public_id: @identity.public_id,
identity_email: @identity.primary_email,
slack_error: slack_result[:error]
}
)
if scenario.should_create_slack? || @attempt.next_action == "slack"

View file

@ -113,12 +113,13 @@ class SAMLController < ApplicationController
Rails.logger.info "Slack provisioning successful via SCIM for #{current_identity.id}: #{slack_result[:message]}"
else
Rails.logger.error "Slack provisioning failed via SCIM for #{current_identity.id}: #{slack_result[:error]}"
Honeybadger.notify(
Sentry.capture_message(
"Slack provisioning failed via SCIM",
context: {
identity_id: current_identity.id,
email: current_identity.primary_email,
error: slack_result[:error]
level: :error,
extra: {
identity_public_id: current_identity.public_id,
identity_email: current_identity.primary_email,
slack_error: slack_result[:error]
}
)
flash[:error] = "We couldn't create your Slack account. Please contact support."

View file

@ -12,4 +12,26 @@ document.addEventListener('htmx:configRequest', (event) => {
if (csrfToken) {
event.detail.headers['X-CSRF-Token'] = csrfToken;
}
});
});
// Copy error ID to clipboard
window.copyErrorId = function(element) {
const errorId = element.dataset.errorId;
const feedback = element.nextElementSibling || element.parentElement.querySelector('.copy-feedback');
navigator.clipboard.writeText(errorId).then(() => {
// Show feedback
if (feedback) {
feedback.classList.add('show');
feedback.classList.remove('hidden');
// Hide after 2 seconds
setTimeout(() => {
feedback.classList.remove('show');
feedback.classList.add('hidden');
}, 2000);
}
}).catch(err => {
console.error('Failed to copy:', err);
});
};

View file

@ -101,7 +101,10 @@ module RalseiEngine
Rails.logger.info "RalseiEngine sent message to #{identity.slack_id} via #{channel_id} (template: #{template_name})"
rescue => e
Rails.logger.error "RalseiEngine failed to send message: #{e.message}"
Honeybadger.notify(e, context: { identity_id: identity.id, template: template_name })
Sentry.capture_exception(e, extra: {
identity_public_id: identity.public_id,
ralsei_template: template_name
})
raise
end
@ -128,7 +131,9 @@ module RalseiEngine
dm_channel_id
rescue => e
Rails.logger.error "RalseiEngine failed to open DM channel: #{e.message}"
Honeybadger.notify(e, context: { identity_id: identity.id })
Sentry.capture_exception(e, extra: {
identity_public_id: identity.public_id
})
nil
end

View file

@ -154,7 +154,10 @@ module SCIMService
}
rescue => e
Rails.logger.error "Error creating Slack user: #{e.message}"
Honeybadger.notify(e, context: { identity_id: identity.id, email: identity.primary_email })
Sentry.capture_exception(e, extra: {
identity_public_id: identity.public_id,
identity_email: identity.primary_email
})
{
success: false,

View file

@ -0,0 +1,102 @@
<style>
.error-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
text-align: center;
}
.error-page-content {
max-width: 600px;
}
.error-icon {
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
color: var(--pico-primary);
}
.error-code {
font-size: 4rem;
font-weight: 700;
color: var(--pico-primary);
margin-bottom: 0.5rem;
line-height: 1;
}
.error-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.error-description {
margin-bottom: 2rem;
opacity: 0.8;
}
.error-id-box {
background: var(--pico-card-background-color);
border: 2px solid var(--pico-primary);
border-radius: var(--pico-border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
}
.error-id-label {
font-size: 0.875rem;
margin-bottom: 1rem;
opacity: 0.9;
}
.error-id-code {
display: block;
font-family: monospace;
font-size: 0.9rem;
padding: 0.75rem 1rem;
background: var(--pico-background-color);
border: 1px solid var(--pico-muted-border-color);
border-radius: var(--pico-border-radius);
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
}
.error-id-code:hover {
border-color: var(--pico-primary);
background: var(--pico-primary-focus);
}
.copy-feedback {
display: none;
margin-top: 0.5rem;
color: #2ecc71;
font-size: 0.875rem;
font-weight: 600;
}
.copy-feedback.show {
display: block;
}
</style>
<div class="error-page">
<div class="error-page-content">
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<div class="error-code">500</div>
<h1 class="error-title">Internal Server Error</h1>
<p class="error-description">We're sorry, but something went wrong on our end.</p>
<% if @event_id.present? %>
<div class="error-id-box">
<p class="error-id-label">
If you need help, please provide this error ID:
</p>
<code
class="error-id-code"
data-error-id="<%= @event_id %>"
onclick="copyErrorId(this)"
title="Click to copy">
<%= @event_id %>
</code>
<div class="copy-feedback">✓ Copied to clipboard!</div>
</div>
<% end %>
<a href="/" role="button">Go back home</a>
</div>
</div>

View file

@ -0,0 +1,100 @@
<style>
.error-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
text-align: center;
}
.error-page-content {
max-width: 600px;
}
.error-icon {
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
color: var(--pico-primary);
}
.error-code {
font-size: 4rem;
font-weight: 700;
color: var(--pico-primary);
margin-bottom: 0.5rem;
line-height: 1;
}
.error-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.error-description {
margin-bottom: 2rem;
opacity: 0.8;
}
.error-id-box {
background: var(--pico-card-background-color);
border: 2px solid var(--pico-primary);
border-radius: var(--pico-border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
}
.error-id-label {
font-size: 0.875rem;
margin-bottom: 1rem;
opacity: 0.9;
}
.error-id-code {
display: block;
font-family: monospace;
font-size: 0.9rem;
padding: 0.75rem 1rem;
background: var(--pico-background-color);
border: 1px solid var(--pico-muted-border-color);
border-radius: var(--pico-border-radius);
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
}
.error-id-code:hover {
border-color: var(--pico-primary);
background: var(--pico-primary-focus);
}
.copy-feedback {
display: none;
margin-top: 0.5rem;
color: #2ecc71;
font-size: 0.875rem;
font-weight: 600;
}
.copy-feedback.show {
display: block;
}
</style>
<div class="error-page">
<div class="error-page-content">
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div class="error-code">404</div>
<h1 class="error-title">Page Not Found</h1>
<p class="error-description">The page you were looking for doesn't exist.</p>
<% if @event_id.present? %>
<div class="error-id-box">
<p class="error-id-label">Error ID:</p>
<code
class="error-id-code"
data-error-id="<%= @event_id %>"
onclick="copyErrorId(this)"
title="Click to copy">
<%= @event_id %>
</code>
<div class="copy-feedback">✓ Copied to clipboard!</div>
</div>
<% end %>
<a href="/" role="button">Go back home</a>
</div>
</div>

View file

@ -0,0 +1,100 @@
<style>
.error-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
text-align: center;
}
.error-page-content {
max-width: 600px;
}
.error-icon {
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
color: var(--pico-primary);
}
.error-code {
font-size: 4rem;
font-weight: 700;
color: var(--pico-primary);
margin-bottom: 0.5rem;
line-height: 1;
}
.error-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.error-description {
margin-bottom: 2rem;
opacity: 0.8;
}
.error-id-box {
background: var(--pico-card-background-color);
border: 2px solid var(--pico-primary);
border-radius: var(--pico-border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
}
.error-id-label {
font-size: 0.875rem;
margin-bottom: 1rem;
opacity: 0.9;
}
.error-id-code {
display: block;
font-family: monospace;
font-size: 0.9rem;
padding: 0.75rem 1rem;
background: var(--pico-background-color);
border: 1px solid var(--pico-muted-border-color);
border-radius: var(--pico-border-radius);
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
}
.error-id-code:hover {
border-color: var(--pico-primary);
background: var(--pico-primary-focus);
}
.copy-feedback {
display: none;
margin-top: 0.5rem;
color: #2ecc71;
font-size: 0.875rem;
font-weight: 600;
}
.copy-feedback.show {
display: block;
}
</style>
<div class="error-page">
<div class="error-page-content">
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
</svg>
<div class="error-code">422</div>
<h1 class="error-title">Unprocessable Entity</h1>
<p class="error-description">The change you wanted was rejected.</p>
<% if @event_id.present? %>
<div class="error-id-box">
<p class="error-id-label">Error ID:</p>
<code
class="error-id-code"
data-error-id="<%= @event_id %>"
onclick="copyErrorId(this)"
title="Click to copy">
<%= @event_id %>
</code>
<div class="copy-feedback">✓ Copied to clipboard!</div>
</div>
<% end %>
<a href="/" role="button">Go back home</a>
</div>
</div>

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html data-theme="light">
<head>
<title><%= content_for(:title) || "Error - Hack Club Auth" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<script>
(function() {
try {
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const defaultTheme = systemPrefersDark ? "dark" : "light";
const savedTheme = localStorage.getItem("theme") || defaultTheme;
document.documentElement.dataset.theme = savedTheme;
} catch(e) {
// Silently fail if localStorage is unavailable
document.documentElement.dataset.theme = "light";
}
})();
</script>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%= vite_client_tag %>
<%= vite_stylesheet_tag "application.css" %>
<%= vite_javascript_tag 'application' %>
</head>
<body>
<%= yield %>
<% unless Rails.env.production? %>
<div class="environment <%= Rails.env %>" />
<% end %>
</body>
</html>

View file

@ -1,6 +1,7 @@
<% flash.each do |type, message| %>
<% next if type == "sentry_event_id" %> <!-- Skip event_id from being rendered as message -->
<% if message.present? %>
<%= render Components::Banner.new(kind: type) do %>
<%= render Components::Banner.new(kind: type, event_id: %w[error alert danger].include?(type) ? flash[:sentry_event_id] : nil) do %>
<%= message %>
<% end %>
<% end %>

View file

@ -49,9 +49,12 @@ module IdentityVault
unless Rails.env.development?
config.rails_semantic_logger.add_file_appender = false
config.semantic_logger.add_appender(io: $stdout, formatter: :json)
config.semantic_logger.add_appender(appender: :honeybadger_insights)
config.semantic_logger.add_appender(appender: :sentry_ruby)
end
# Use dynamic error pages to display Sentry event IDs
config.exceptions_app = self.routes
config.to_prepare do
Doorkeeper::ApplicationController.layout "logged_out"
Doorkeeper::ApplicationController.skip_before_action :authenticate_identity!

View file

@ -1,39 +0,0 @@
---
# For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration
api_key: '<%= ENV["HONEYBADGER_API_KEY"] %>'
# The environment your app is running in.
env: "<%= Rails.env %>"
# The absolute path to your project folder.
root: "<%= Rails.root.to_s %>"
# Honeybadger won't report errors in these environments.
development_environments:
- test
- development
- cucumber
# By default, Honeybadger won't report errors in the development_environments.
# You can override this by explicitly setting report_data to true or false.
# report_data: true
# The current Git revision of your project. Defaults to the last commit hash.
# revision: null
# Enable verbose debug logging (useful for troubleshooting).
debug: false
# Enable Honeybadger Insights
insights:
enabled: true
rails:
insights:
metrics: true
net_http:
insights:
metrics: true
puma:
insights:
metrics: true

View file

@ -0,0 +1,18 @@
git_hash = ENV["SOURCE_COMMIT"] || `git rev-parse HEAD` rescue "unknown"
short_hash = git_hash[0..7]
is_dirty = `git status --porcelain`.strip.length > 0 rescue false
git_version = is_dirty ? "#{short_hash}-dirty" : short_hash
Sentry.init do |config|
config.dsn = ENV["SENTRY_DSN"]
config.environment = Rails.env
config.release = git_version
config.enabled_environments = %w[production staging uat]
config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
config.send_default_pii = true
config.traces_sample_rate = Rails.env.production? ? 0.1 : 1.0
config.enabled_patches = [ :http, :redis, :puma ]
# Capture error event IDs in Rack env for error pages
config.rails.report_rescued_exceptions = true
end

View file

@ -367,6 +367,11 @@ Rails.application.routes.draw do
get "/auth", to: "saml#sp_initiated_get"
end
# Error pages
match "/404", to: "errors#not_found", via: :all
match "/422", to: "errors#unprocessable_entity", via: :all
match "/500", to: "errors#internal_server_error", via: :all
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check

View file

@ -1,114 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>The page you were looking for doesnt exist (404 Not found)</title>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, width=device-width">
<meta name="robots" content="noindex, nofollow">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html {
font-size: 16px;
}
body {
background: #FFF;
color: #261B23;
display: grid;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: clamp(1rem, 2.5vw, 2rem);
-webkit-font-smoothing: antialiased;
font-style: normal;
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
min-height: 100vh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
a {
color: inherit;
font-weight: 700;
text-decoration: underline;
text-underline-offset: 0.0925em;
}
b, strong {
font-weight: 700;
}
i, em {
font-style: italic;
}
main {
display: grid;
gap: 1em;
padding: 2em;
place-items: center;
text-align: center;
}
main header {
width: min(100%, 12em);
}
main header svg {
height: auto;
max-width: 100%;
width: 100%;
}
main article {
width: min(100%, 30em);
}
main article p {
font-size: 75%;
}
main article br {
display: none;
@media(min-width: 48em) {
display: inline;
}
}
</style>
</head>
<body>
<!-- This file lives in public/404.html -->
<main>
<header>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" fill="#f0eff0"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" fill="#d30001"/></svg>
</header>
<article>
<p><strong>The page you were looking for doesnt exist.</strong> You may have mistyped the address or the page may have moved. If youre the application owner check the logs for more information.</p>
</article>
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long