diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-01 02:39:59 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-01 02:39:59 +0200 |
| commit | 6df48c1413a14516e7ee8919f33fbc13f0141966 (patch) | |
| tree | d81df761eb6607a6f4508241c043375e9860679d /app/views/layouts | |
| parent | f6c1f0f08f031778a491465d35ac694bfcdc12b0 (diff) | |
Show and extend the elevation window
A banner appears while elevated, counting down in minutes and carrying
extend and drop controls. Three extensions of 30 minutes are allowed, so
the window is at most two hours without a fresh code; a code resets the
budget. The countdown is advisory, the server-side check authoritative, so
it says "expired" rather than vanishing.
The post-login flash tells an admin the timer has started.
Diffstat (limited to 'app/views/layouts')
| -rw-r--r-- | app/views/layouts/_elevation_banner.html.erb | 47 | ||||
| -rw-r--r-- | app/views/layouts/admin.html.erb | 1 |
2 files changed, 48 insertions, 0 deletions
diff --git a/app/views/layouts/_elevation_banner.html.erb b/app/views/layouts/_elevation_banner.html.erb new file mode 100644 index 00000000..a7d15a2e --- /dev/null +++ b/app/views/layouts/_elevation_banner.html.erb | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | <% if elevated? %> | ||
| 2 | <div id="elevation_banner" | ||
| 3 | data-expires-at="<%= elevation_expires_at.to_i %>" | ||
| 4 | data-label-minutes="<%= t(".minutes_short") %>" | ||
| 5 | data-label-less-than-a-minute="<%= t(".less_than_a_minute") %>" | ||
| 6 | data-label-expired="<%= t(".expired") %>"> | ||
| 7 | <span class="elevation_state"> | ||
| 8 | <%= icon("shield-check", library: "tabler", "aria-hidden": true) %> | ||
| 9 | <%= t(".active") %> | ||
| 10 | <span id="elevation_remaining"></span> | ||
| 11 | </span> | ||
| 12 | <span class="elevation_actions"> | ||
| 13 | <% if elevation_extensions_left > 0 %> | ||
| 14 | <%= button_to t(".renew"), renew_elevation_path, method: :post, | ||
| 15 | form: { class: "button_to state_changing" } %> | ||
| 16 | <% end %> | ||
| 17 | <%= button_to t(".drop"), elevation_path, method: :delete, | ||
| 18 | form: { class: "button_to destructive" } %> | ||
| 19 | </span> | ||
| 20 | </div> | ||
| 21 | |||
| 22 | <%= javascript_tag nonce: true do %> | ||
| 23 | (function () { | ||
| 24 | var banner = document.getElementById("elevation_banner"); | ||
| 25 | if (!banner) return; | ||
| 26 | var out = document.getElementById("elevation_remaining"); | ||
| 27 | var expires = parseInt(banner.dataset.expiresAt, 10) * 1000; | ||
| 28 | |||
| 29 | function tick() { | ||
| 30 | var ms = expires - Date.now(); | ||
| 31 | if (ms <= 0) { | ||
| 32 | out.textContent = banner.dataset.labelExpired; | ||
| 33 | banner.classList.remove("elevation_soon"); | ||
| 34 | banner.classList.add("elevation_expired"); | ||
| 35 | return; | ||
| 36 | } | ||
| 37 | var mins = Math.floor(ms / 60000); | ||
| 38 | out.textContent = mins < 1 | ||
| 39 | ? banner.dataset.labelLessThanAMinute | ||
| 40 | : banner.dataset.labelMinutes.replace("%{count}", mins); | ||
| 41 | if (mins < 5) banner.classList.add("elevation_soon"); | ||
| 42 | setTimeout(tick, 15000); | ||
| 43 | } | ||
| 44 | tick(); | ||
| 45 | })(); | ||
| 46 | <% end %> | ||
| 47 | <% end %> | ||
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 43f09b96..a84c8927 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb | |||
| @@ -43,6 +43,7 @@ | |||
| 43 | </div> | 43 | </div> |
| 44 | </div> | 44 | </div> |
| 45 | <div class="admin_content_spacer"></div> | 45 | <div class="admin_content_spacer"></div> |
| 46 | <%= render "layouts/elevation_banner" %> | ||
| 46 | <%= render "layouts/flash" %> | 47 | <%= render "layouts/flash" %> |
| 47 | <div id="content"> | 48 | <div id="content"> |
| 48 | <%= yield %> | 49 | <%= yield %> |
