Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions app/assets/stylesheets/upright/public_status.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
border-radius: 1px;
flex: 1 1 0;
min-width: 2px;
position: relative;
transition: filter 100ms ease-out;
}

Expand All @@ -163,4 +164,73 @@
color: var(--color-ink-dark);
font-weight: 500;
}

.uptime__tooltip {
background: var(--color-canvas);
border: var(--border);
border-radius: 0.5rem;
bottom: calc(100% + 0.5rem);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
font-family: var(--font-sans);
font-size: var(--text-x-small);
gap: 0.2rem;
left: 50%;
line-height: 1.35;
opacity: 0;
padding: 0.45rem 0.6rem;
pointer-events: none;
position: absolute;
transform: translate(-50%, 0.25rem);
transition: opacity 120ms ease-out, transform 120ms ease-out;
visibility: hidden;
white-space: nowrap;
z-index: 10;
}

.uptime__bar:hover .uptime__tooltip {
opacity: 1;
transform: translate(-50%, 0);
visibility: visible;
}

/* Caret, sitting on the tooltip's bottom edge and matching its border. */
.uptime__tooltip::after {
background: var(--color-canvas);
border-bottom: var(--border);
border-right: var(--border);
content: "";
height: 0.5rem;
left: 50%;
position: absolute;
top: calc(100% - 0.25rem);
transform: translateX(-50%) rotate(45deg);
width: 0.5rem;
}

.uptime__tooltip-date {
color: var(--color-ink);
font-weight: 600;
}

.uptime__tooltip-detail {
align-items: center;
color: var(--color-ink-medium);
display: flex;
gap: 0.4rem;
}

.uptime__tooltip-dot {
border-radius: 50%;
flex-shrink: 0;
height: 0.5rem;
width: 0.5rem;
}

.uptime__tooltip-dot--operational { background: var(--status-operational); }
.uptime__tooltip-dot--degraded { background: var(--status-degraded); }
.uptime__tooltip-dot--partial_outage { background: var(--status-partial); }
.uptime__tooltip-dot--major_outage { background: var(--status-major); }
.uptime__tooltip-dot--none { background: var(--status-none); }
}
31 changes: 19 additions & 12 deletions app/models/upright/service/daily_status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,29 @@ def operational?
status == :operational
end

def tooltip
"#{date_label}: #{body}"
def date_label
date == Date.current ? "Today" : date.to_fs(:month_day)
end

private
def date_label
date == Date.current ? "Today" : date.to_fs(:month_day)
def detail
if uptime_fraction
[ "%.2f%% uptime" % (uptime_fraction * 100), downtime ].compact.join(" · ")
elsif status
status.to_s.humanize.downcase
else
"no data"
end
end

def body
if uptime_fraction
"%.2f%% uptime" % (uptime_fraction * 100)
elsif status
status.to_s.humanize.downcase
else
"no data"
def aria_label
"#{date_label}: #{detail}"
end

private
def downtime
if uptime_fraction && uptime_fraction < 1
minutes = ((1 - uptime_fraction) * 24 * 60).round
minutes.zero? ? "<1 min down" : "#{minutes} #{'min'.pluralize(minutes)} down"
end
end
end
4 changes: 2 additions & 2 deletions app/views/upright/public/services/_service.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
</span>
</div>

<% if service.description.present? %>
<p class="service__description"><%= service.description %></p>
<% if description = service.try(:description).presence %>
<p class="service__description"><%= description %></p>
<% end %>

<div class="uptime">
Expand Down
10 changes: 9 additions & 1 deletion app/views/upright/public/services/_uptime_bar.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
<span class="uptime__bar <%= "uptime__bar--#{day.status}" %>" title="<%= day.tooltip %>"></span>
<span class="uptime__bar uptime__bar--<%= day.status %>" aria-label="<%= day.aria_label %>">
<span class="uptime__tooltip" aria-hidden="true">
<span class="uptime__tooltip-date"><%= day.date_label %></span>
<span class="uptime__tooltip-detail">
<span class="uptime__tooltip-dot uptime__tooltip-dot--<%= day.status || "none" %>"></span>
<%= day.detail %>
</span>
</span>
</span>
2 changes: 0 additions & 2 deletions test/dummy/config/services.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
- code: "example_app"
name: "Example App"
description: "Public-facing services for the Example App"
public: true

- code: "internal_tools"
name: "Internal Tools"
description: "Tooling used by staff"
public: false
43 changes: 43 additions & 0 deletions test/models/upright/service/daily_status_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require "test_helper"

class Upright::Service::DailyStatusTest < ActiveSupport::TestCase
test "an operational day states uptime without a downtime clause" do
day = day_with(uptime_fraction: 1.0)
assert_equal "100.00% uptime", day.detail
end

test "a downtime clause reports whole minutes" do
day = day_with(uptime_fraction: 1 - 5.0 / 1440)
assert_equal "99.65% uptime · 5 mins down", day.detail
end

test "one minute is singular" do
day = day_with(uptime_fraction: 1 - 1.0 / 1440)
assert_equal "99.93% uptime · 1 min down", day.detail
end

test "sub-minute downtime reads as less than a minute" do
day = day_with(uptime_fraction: 0.9999)
assert_equal "99.99% uptime · <1 min down", day.detail
end

test "today carries a live status but no fraction" do
day = Upright::Service::DailyStatus.new(date: Date.current, status: :degraded)
assert_equal "Today", day.date_label
assert_equal "degraded", day.detail
end

test "a day with no measurement reads as no data" do
assert_equal "no data", day_with.detail
end

test "aria label combines the date and detail" do
day = day_with(uptime_fraction: 1.0)
assert_equal "#{day.date_label}: 100.00% uptime", day.aria_label
end

private
def day_with(**attributes)
Upright::Service::DailyStatus.new(date: Date.new(2026, 6, 15), **attributes)
end
end