hanikamu-rate-limit 0.3.1 → 0.3.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/lib/hanikamu/rate_limit/metrics.rb +30 -16
- data/lib/hanikamu/rate_limit/ui/app/views/hanikamu/rate_limit/ui/dashboard/index.html.erb +81 -5
- data/lib/hanikamu/rate_limit/ui/presenters/limit_presenter.rb +4 -18
- data/lib/hanikamu/rate_limit/ui/presenters/override_helpers.rb +57 -0
- data/lib/hanikamu/rate_limit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9febd701c4dea4feea383f710cda6de80d26385f5e85b20971a2c5ed6825f5b9
|
|
4
|
+
data.tar.gz: 0d29be734df15391fd08a02eca7deddd46c754af3c4129683013ca9eed28c8dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 249ac5c8f437ea501a643e5a87c8d3263d80e3917ebada5140893b12ffb71804d78f44d8fcd4ee733fe04080b37834a0662b28a07b22e0c7ddd0bf81ff5321ce
|
|
7
|
+
data.tar.gz: eaa4bdc88f9704766dd1d5332c39d3450d654b440f53cb5316ec6686ed2e851f5e15047eddbfab5436b4ac74d13267dd508e0ba5e9c9f4cc4227528261d95031
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 - 2026-02-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Short-reset overrides now visible in the dashboard.** Overrides with a very short reset (e.g. 2 seconds) previously expired before the SSE interval could push them, so the pill always showed "None". Expired overrides are now returned with `active: false` and displayed as a dimmed pill with a relative timestamp ("Registered 10s ago").
|
|
8
|
+
- **Override pill no longer flickers** on SSE updates. The update path now patches the existing DOM element in-place instead of replacing `innerHTML`, and the "Registered" prefix slides in/out with a CSS transition.
|
|
9
|
+
- Expired override pill uses neutral grey styling instead of the red accent colour.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Extracted `OverrideHelpers` module from `LimitPresenter` to keep class size under RuboCop limits.
|
|
14
|
+
- Refactored `resolve_override` into `live_override_from` and `stored_override_from` for clarity.
|
|
15
|
+
- Override snapshot hashes now always include an `"active"` boolean key.
|
|
16
|
+
- Added `override_active?`, `override_updated_at`, and `override_age_label` to `LimitPresenter`.
|
|
17
|
+
- Added footer with gem version and GitHub link to the dashboard.
|
|
18
|
+
|
|
3
19
|
## 0.3.1 - 2026-02-17
|
|
4
20
|
|
|
5
21
|
### Fixed
|
data/README.md
CHANGED
|
@@ -333,22 +333,30 @@ module Hanikamu
|
|
|
333
333
|
end
|
|
334
334
|
|
|
335
335
|
def resolve_override(live_remaining, live_ttl, stored_meta)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
336
|
+
live_override_from(live_remaining, live_ttl) ||
|
|
337
|
+
stored_override_from(stored_meta)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def live_override_from(remaining_raw, ttl)
|
|
341
|
+
return nil unless remaining_raw && ttl&.positive?
|
|
342
|
+
|
|
343
|
+
remaining = Integer(remaining_raw, exception: false)
|
|
344
|
+
return nil unless remaining
|
|
345
|
+
|
|
346
|
+
{ "remaining" => remaining, "reset" => ttl,
|
|
347
|
+
"updated_at" => Time.now.to_i, "active" => true }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def stored_override_from(stored_meta)
|
|
345
351
|
return nil if stored_meta.nil? || stored_meta.empty?
|
|
346
352
|
|
|
347
353
|
remaining_reset = compute_remaining_reset(stored_meta)
|
|
348
|
-
|
|
354
|
+
updated_at = stored_meta["updated_at"].to_i
|
|
355
|
+
active = !remaining_reset.nil?
|
|
349
356
|
|
|
350
|
-
{ "remaining" => stored_meta["remaining"].to_i,
|
|
351
|
-
"
|
|
357
|
+
{ "remaining" => stored_meta["remaining"].to_i,
|
|
358
|
+
"reset" => remaining_reset || stored_meta["reset"].to_i,
|
|
359
|
+
"updated_at" => updated_at, "active" => active }
|
|
352
360
|
end
|
|
353
361
|
|
|
354
362
|
def parse_override_history(raw, window, bucket_seconds)
|
|
@@ -407,7 +415,7 @@ module Hanikamu
|
|
|
407
415
|
remaining = Integer(remaining_value, exception: false)
|
|
408
416
|
return nil if remaining.nil?
|
|
409
417
|
|
|
410
|
-
{ "remaining" => remaining, "reset" => ttl, "updated_at" => Time.now.to_i }
|
|
418
|
+
{ "remaining" => remaining, "reset" => ttl, "updated_at" => Time.now.to_i, "active" => true }
|
|
411
419
|
end
|
|
412
420
|
|
|
413
421
|
def stored_override(registry)
|
|
@@ -415,10 +423,16 @@ module Hanikamu
|
|
|
415
423
|
return nil if meta.empty?
|
|
416
424
|
|
|
417
425
|
remaining_reset = compute_remaining_reset(meta)
|
|
418
|
-
|
|
426
|
+
updated_at = meta["updated_at"].to_i
|
|
419
427
|
|
|
420
|
-
|
|
421
|
-
"
|
|
428
|
+
if remaining_reset
|
|
429
|
+
{ "remaining" => meta["remaining"].to_i, "reset" => remaining_reset,
|
|
430
|
+
"updated_at" => updated_at, "active" => true }
|
|
431
|
+
else
|
|
432
|
+
{ "remaining" => meta["remaining"].to_i,
|
|
433
|
+
"reset" => meta["reset"].to_i,
|
|
434
|
+
"updated_at" => updated_at, "active" => false }
|
|
435
|
+
end
|
|
422
436
|
end
|
|
423
437
|
|
|
424
438
|
def compute_remaining_reset(meta)
|
|
@@ -156,6 +156,21 @@
|
|
|
156
156
|
color: var(--accent);
|
|
157
157
|
font-size: 12px;
|
|
158
158
|
font-weight: 600;
|
|
159
|
+
transition: opacity 0.3s ease, border-color 0.3s ease;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.pill-age {
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
max-width: 0;
|
|
166
|
+
opacity: 0;
|
|
167
|
+
transition: max-width 0.4s ease, opacity 0.3s ease;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.pill--expired .pill-age {
|
|
172
|
+
max-width: 250px;
|
|
173
|
+
opacity: 1;
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
.stats {
|
|
@@ -278,6 +293,17 @@
|
|
|
278
293
|
font-weight: 600;
|
|
279
294
|
}
|
|
280
295
|
|
|
296
|
+
.pill--expired {
|
|
297
|
+
opacity: 0.55;
|
|
298
|
+
border: 1px dashed var(--line);
|
|
299
|
+
background: rgba(161, 161, 170, 0.10);
|
|
300
|
+
color: var(--muted);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.pill--expired strong {
|
|
304
|
+
color: var(--muted);
|
|
305
|
+
}
|
|
306
|
+
|
|
281
307
|
.empty {
|
|
282
308
|
padding: 40px 20px;
|
|
283
309
|
background: var(--card);
|
|
@@ -287,6 +313,23 @@
|
|
|
287
313
|
color: var(--muted);
|
|
288
314
|
}
|
|
289
315
|
|
|
316
|
+
.page-footer {
|
|
317
|
+
text-align: center;
|
|
318
|
+
padding: 32px 0 16px;
|
|
319
|
+
color: #6b7280;
|
|
320
|
+
font-size: 0.8rem;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.page-footer a {
|
|
324
|
+
color: #6b7280;
|
|
325
|
+
text-decoration: none;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.page-footer a:hover {
|
|
329
|
+
color: #374151;
|
|
330
|
+
text-decoration: underline;
|
|
331
|
+
}
|
|
332
|
+
|
|
290
333
|
@media (max-width: 720px) {
|
|
291
334
|
header {
|
|
292
335
|
padding-top: 24px;
|
|
@@ -415,7 +458,7 @@
|
|
|
415
458
|
<div class="override">
|
|
416
459
|
<span>Override</span>
|
|
417
460
|
<% if limit.override? %>
|
|
418
|
-
<span class="pill">Remaining <strong><%= limit.override_remaining %></strong> | Reset <strong><%= limit.override_reset %>s</strong></span>
|
|
461
|
+
<span class="pill<%= ' pill--expired' unless limit.override_active? %>"><span class="pill-age">Registered <strong data-override-age><%= limit.override_age_label unless limit.override_active? %></strong> — </span>Remaining <strong data-override-remaining><%= limit.override_remaining %></strong> | Reset <strong data-override-reset><%= limit.override_reset %>s</strong></span>
|
|
419
462
|
<% else %>
|
|
420
463
|
<strong>None</strong>
|
|
421
464
|
<% end %>
|
|
@@ -432,6 +475,14 @@
|
|
|
432
475
|
const grid = document.getElementById("limits-grid");
|
|
433
476
|
const emptyState = document.getElementById("empty-state");
|
|
434
477
|
|
|
478
|
+
const timeAgo = (unixTs) => {
|
|
479
|
+
const seconds = Math.max(0, Math.floor(Date.now() / 1000) - Number(unixTs || 0));
|
|
480
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
481
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
482
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
483
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
484
|
+
};
|
|
485
|
+
|
|
435
486
|
const escapeHtml = (value) => {
|
|
436
487
|
const str = String(value ?? "");
|
|
437
488
|
return str
|
|
@@ -742,7 +793,9 @@
|
|
|
742
793
|
</div>
|
|
743
794
|
<div class="override">
|
|
744
795
|
<span>Override</span>
|
|
745
|
-
${override
|
|
796
|
+
${override
|
|
797
|
+
? `<span class="pill${override.active ? '' : ' pill--expired'}"><span class="pill-age">Registered <strong data-override-age>${override.active ? '' : timeAgo(override.updated_at)}</strong> — </span>Remaining <strong data-override-remaining>${escapeHtml(override.remaining)}</strong> | Reset <strong data-override-reset>${escapeHtml(override.reset)}s</strong></span>`
|
|
798
|
+
: "<strong>None</strong>"}
|
|
746
799
|
</div>
|
|
747
800
|
</article>
|
|
748
801
|
`;
|
|
@@ -800,9 +853,28 @@
|
|
|
800
853
|
|
|
801
854
|
const overrideEl = card.querySelector('.override');
|
|
802
855
|
if (overrideEl) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
856
|
+
if (override) {
|
|
857
|
+
let pillEl = overrideEl.querySelector('.pill');
|
|
858
|
+
if (!pillEl) {
|
|
859
|
+
overrideEl.innerHTML = `<span>Override</span> <span class="pill${override.active ? '' : ' pill--expired'}"><span class="pill-age">Registered <strong data-override-age>${override.active ? '' : timeAgo(override.updated_at)}</strong> — </span>Remaining <strong data-override-remaining>${escapeHtml(override.remaining)}</strong> | Reset <strong data-override-reset>${escapeHtml(override.reset)}s</strong></span>`;
|
|
860
|
+
} else {
|
|
861
|
+
pillEl.classList.toggle('pill--expired', !override.active);
|
|
862
|
+
const ageEl = pillEl.querySelector('[data-override-age]');
|
|
863
|
+
if (!override.active) {
|
|
864
|
+
if (ageEl) ageEl.textContent = timeAgo(override.updated_at);
|
|
865
|
+
} else {
|
|
866
|
+
if (ageEl) ageEl.textContent = '';
|
|
867
|
+
}
|
|
868
|
+
const remEl = pillEl.querySelector('[data-override-remaining]');
|
|
869
|
+
if (remEl) remEl.textContent = override.remaining;
|
|
870
|
+
const resetEl = pillEl.querySelector('[data-override-reset]');
|
|
871
|
+
if (resetEl) resetEl.textContent = `${override.reset}s`;
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
if (overrideEl.querySelector('.pill')) {
|
|
875
|
+
overrideEl.innerHTML = '<span>Override</span> <strong>None</strong>';
|
|
876
|
+
}
|
|
877
|
+
}
|
|
806
878
|
}
|
|
807
879
|
});
|
|
808
880
|
}
|
|
@@ -827,5 +899,9 @@
|
|
|
827
899
|
// This prevents thread starvation on servers with few threads.
|
|
828
900
|
window.addEventListener("beforeunload", () => source.close());
|
|
829
901
|
</script>
|
|
902
|
+
|
|
903
|
+
<footer class="page-footer">
|
|
904
|
+
<a href="https://github.com/Hanikamu/hanikamu-rate-limit" target="_blank" rel="noopener noreferrer">hanikamu-rate-limit</a> v<%= Hanikamu::RateLimit::VERSION %>
|
|
905
|
+
</footer>
|
|
830
906
|
</body>
|
|
831
907
|
</html>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "override_helpers"
|
|
4
|
+
|
|
3
5
|
module Hanikamu
|
|
4
6
|
module RateLimit
|
|
5
7
|
module UI
|
|
6
8
|
class LimitPresenter
|
|
9
|
+
include OverrideHelpers
|
|
10
|
+
|
|
7
11
|
EMPTY_HISTORY = { "allowed" => [], "blocked" => [], "buckets" => [] }.freeze
|
|
8
12
|
EMPTY_LIFETIME = { "allowed" => 0, "blocked" => 0 }.freeze
|
|
9
13
|
|
|
@@ -110,24 +114,6 @@ module Hanikamu
|
|
|
110
114
|
lifetime.fetch("blocked", 0)
|
|
111
115
|
end
|
|
112
116
|
|
|
113
|
-
# ── Override ─────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
def override
|
|
116
|
-
data["override"]
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def override?
|
|
120
|
-
!override.nil?
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def override_remaining
|
|
124
|
-
override&.fetch("remaining", nil)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def override_reset
|
|
128
|
-
override&.fetch("reset", nil)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
117
|
# ── Metrics ──────────────────────────────────────────────
|
|
132
118
|
|
|
133
119
|
def metrics_enabled?
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanikamu
|
|
4
|
+
module RateLimit
|
|
5
|
+
module UI
|
|
6
|
+
# Shared helpers for override presentation logic.
|
|
7
|
+
module OverrideHelpers
|
|
8
|
+
TIME_THRESHOLDS = [
|
|
9
|
+
[60, ->(s) { "#{s}s ago" }],
|
|
10
|
+
[3600, ->(s) { "#{s / 60}m ago" }],
|
|
11
|
+
[86_400, ->(s) { "#{s / 3600}h ago" }]
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def override
|
|
15
|
+
data["override"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def override?
|
|
19
|
+
!override.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def override_active?
|
|
23
|
+
override&.fetch("active", false) == true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def override_remaining
|
|
27
|
+
override&.fetch("remaining", nil)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def override_reset
|
|
31
|
+
override&.fetch("reset", nil)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def override_updated_at
|
|
35
|
+
override&.fetch("updated_at", nil)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def override_age_label
|
|
39
|
+
ts = override_updated_at
|
|
40
|
+
return nil unless ts
|
|
41
|
+
|
|
42
|
+
seconds = [Time.now.to_i - ts, 0].max
|
|
43
|
+
time_ago_in_words(seconds)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def time_ago_in_words(seconds)
|
|
49
|
+
TIME_THRESHOLDS.each do |threshold, formatter|
|
|
50
|
+
return formatter.call(seconds) if seconds < threshold
|
|
51
|
+
end
|
|
52
|
+
"#{seconds / 86_400}d ago"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hanikamu-rate-limit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolai Seerup
|
|
@@ -95,6 +95,7 @@ files:
|
|
|
95
95
|
- lib/hanikamu/rate_limit/ui/engine.rb
|
|
96
96
|
- lib/hanikamu/rate_limit/ui/presenters/dashboard_presenter.rb
|
|
97
97
|
- lib/hanikamu/rate_limit/ui/presenters/limit_presenter.rb
|
|
98
|
+
- lib/hanikamu/rate_limit/ui/presenters/override_helpers.rb
|
|
98
99
|
- lib/hanikamu/rate_limit/version.rb
|
|
99
100
|
homepage: https://github.com/Hanikamu/hanikamu-rate-limit
|
|
100
101
|
licenses:
|