pgbus 0.16.4 → 0.16.5
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 +4 -0
- data/app/controllers/pgbus/locks_controller.rb +27 -0
- data/app/frontend/pgbus/style.css +1 -1
- data/app/models/pgbus/uniqueness_key.rb +20 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +14 -1
- data/app/views/pgbus/locks/_concurrency.html.erb +94 -0
- data/app/views/pgbus/locks/_uniqueness.html.erb +80 -0
- data/app/views/pgbus/locks/index.html.erb +5 -76
- data/config/locales/da.yml +42 -1
- data/config/locales/de.yml +42 -1
- data/config/locales/en.yml +42 -1
- data/config/locales/es.yml +42 -1
- data/config/locales/fi.yml +42 -1
- data/config/locales/fr.yml +42 -1
- data/config/locales/it.yml +42 -1
- data/config/locales/ja.yml +42 -1
- data/config/locales/nb.yml +42 -1
- data/config/locales/nl.yml +42 -1
- data/config/locales/pt.yml +42 -1
- data/config/locales/sv.yml +42 -1
- data/config/routes.rb +5 -0
- data/lib/pgbus/instrumentation.rb +3 -0
- data/lib/pgbus/integrations/appsignal/dashboard.json +38 -0
- data/lib/pgbus/integrations/appsignal/probe.rb +10 -0
- data/lib/pgbus/mcp/base_tool.rb +6 -1
- data/lib/pgbus/mcp/server.rb +2 -1
- data/lib/pgbus/mcp/tools/concurrency_tool.rb +32 -0
- data/lib/pgbus/mcp.rb +1 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +210 -1
- data/lib/pgbus/web/metrics_serializer.rb +51 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4382e9b750a7e3538c2ba640aa476bb914cd1c1deba00fda429510ea1dcdc851
|
|
4
|
+
data.tar.gz: bc8bff2b3b08ccc6de7d67c677a8797be4d0a1b9f65019a9728d0531ad5a830a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9630731c5e8b4ef6bb9db80e1e950ad6abee7748e49868fb613645b0d16469f477a6b0fe819427135a6bdf85589fb518e6204f80a3a20810939ed1c2038238a7
|
|
7
|
+
data.tar.gz: b4c5b23b4e4513b5447985ab96b1578a3a1931fda6b3b4700338b71d7d822a989cda6498670a31e7ef30848f69f4075b23a2f018764072f51b6e91ae258d8445
|
data/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
### Added
|
|
4
4
|
|
|
5
|
+
- **Concurrency is now visible: a section on the Locks page, three gauges, a home stat card and an MCP tool.** `limits_concurrency … on_conflict: :block` became durable in #460 — a parked job is never dropped and a key never runs past `to:` — but nothing showed it, so an operator whose per-record pipeline looked "stuck" had no way to see that the key's slot was held, how many jobs were parked behind it, or how long the oldest had waited, short of SQL. `pgbus_semaphores` and `pgbus_blocked_executions` appeared on no page, in no gauge, in no tool. One new read, `Web::DataSource#concurrency_stats`, now feeds four surfaces: the Locks page's **Concurrency** section (parked jobs, oldest wait, slots held, keys at limit, then a row per key with value / limit, lease state, parked count and oldest wait, auto-refreshing like the other dashboard panels); a **Parked jobs** card on the dashboard home; three unlabelled gauges on both `/pgbus/api/metrics` and the AppSignal probe (`pgbus_concurrency_blocked_executions`, `pgbus_concurrency_blocked_oldest_age_seconds`, `pgbus_concurrency_slots_held` — keys are per-record, so a per-key label would be an unbounded series); and a read-only `pgbus_concurrency` MCP tool returning the same data. The key rows come from a FULL OUTER JOIN, not a LEFT one: a key can have parked jobs and no semaphore (its holder died and the sweep removed the row) or a semaphore and nothing parked, and both shapes matter to an operator. Each row carries two guarded escape hatches. **Release** drops the semaphore and promotes through `Concurrency::BlockedExecution.promote_next`, so it takes each slot through the same guarded upsert an enqueue uses and can never reintroduce the over-admission #460 closed; when the lease is still fresh the confirm says so, because a job is then probably still running and releasing lets another start beside it. **Discard parked** drops a single key's parked jobs, naming the exact count in the confirm, and resolves the bookkeeping those jobs will never resolve themselves — a parked batch child is marked failed so its batch stops waiting and `on_failure` fires (#413), and an `:until_executed` uniqueness lock is released rather than orphaned (#423); each discard logs a warning naming the job and emits `pgbus.blocked_execution_discarded`. There is deliberately no cross-key "discard all parked": not losing work is the whole point of `:block`. Nothing is added to the enqueue or execute path. Refs #461.
|
|
6
|
+
|
|
5
7
|
- **The batches list and a batch's progress panel now refresh themselves.** Batches was the only dashboard list whose `turbo-frame` was missing `data-auto-refresh`, and the batch detail page had no frame at all, so watching a batch drain meant reloading by hand. Both now poll on the existing `web_refresh_interval` (5s, pauses while you interact) like every other list — the batch progress panel is served by `GET /batches/:id?frame=progress`. This reuses the dashboard's turbo-frame polling rather than the SSE stack in `lib/pgbus/web/stream_app.rb`: that is the host-app-facing stream feature, and pointing it at the dashboard would open a PG `LISTEN` connection per viewer to move four integers.
|
|
6
8
|
|
|
7
9
|
### Fixed
|
|
8
10
|
|
|
11
|
+
- **The AppSignal probe's gauges and every MCP tool response were frozen at their first read.** `Web::DataSource` memoizes `queues_with_metrics` for the life of the instance — correct for the dashboard, which builds one instance per request, but the AppSignal `Probe::Runner` holds a single DataSource for the life of the process and `MCP::Server.build` injects one into the server context, so both replayed the first read forever: queue depth in AppSignal never moved after the first minute, and `pgbus_queues` returned the same snapshot for every call in a session. `DataSource#reset_cache!` drops the memos, and both long-lived consumers call it — the probe once per iteration, `MCP::BaseTool.data_source_from` once per tool call. Found while reviewing the concurrency aggregates added in the same release, which memoize the same way.
|
|
12
|
+
|
|
9
13
|
- **`limits_concurrency … on_conflict: :block` now constrains a job without ever losing it, and never runs more than `to:` at once.** The same bug class solid_queue closed in rails/solid_queue#712 and #761, checked against pgbus's implementation. Four defects, each with a regression spec:
|
|
10
14
|
- **A parked job was silently deleted once it had waited longer than `duration`.** The dispatcher's sweep `delete_all`-ed blocked rows past their `expires_at` (a debug log, nothing else), and `release_next!` refused to promote one — so with `to: 1` and a slow holder, the N-th parked enqueue was simply gone. Blocked rows no longer expire; the only way out of `pgbus_blocked_executions` is promotion. `expires_at` now orders the sweep, nothing more.
|
|
11
15
|
- **A semaphore expired under a job that was still running, and two runs of one key overlapped.** `duration` capped run time: once it passed, the sweep deleted the semaphore and promoted the next parked job beside the still-running holder — and that promotion took no slot at all, so a third enqueue then created a fresh semaphore and ran too. The visibility heartbeat now re-arms the semaphore alongside the message (`Semaphore.touch`), so `duration` bounds heartbeat silence, not run time; and every promotion — completion-time or sweep — takes its slot through the same `value < max_value` upsert an enqueue uses, so it can never push a key past `to:`.
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class LocksController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
+
@concurrency = data_source.concurrency_stats
|
|
7
|
+
return render_frame("pgbus/locks/concurrency") if params[:frame] == "concurrency"
|
|
8
|
+
|
|
6
9
|
@locks = data_source.job_locks
|
|
7
10
|
end
|
|
8
11
|
|
|
@@ -30,5 +33,29 @@ module Pgbus
|
|
|
30
33
|
count = data_source.discard_all_locks
|
|
31
34
|
redirect_to locks_path, notice: t("pgbus.locks.index.all_locks_discarded", count: count)
|
|
32
35
|
end
|
|
36
|
+
|
|
37
|
+
# Drop a concurrency key's semaphore and promote whatever can now run.
|
|
38
|
+
# The key is passed through verbatim — a `key:` proc may produce a string
|
|
39
|
+
# with surrounding whitespace, and stripping it here would address a
|
|
40
|
+
# different key (or none).
|
|
41
|
+
# The promotion goes through the guarded upsert inside the data source, so
|
|
42
|
+
# this is an escape hatch, never a way past the limit.
|
|
43
|
+
def release_key
|
|
44
|
+
key = params[:key].to_s
|
|
45
|
+
return redirect_to(locks_path, alert: t("pgbus.locks.concurrency.no_key")) if key.strip.empty?
|
|
46
|
+
|
|
47
|
+
count = data_source.release_concurrency_key(key)
|
|
48
|
+
redirect_to locks_path, notice: t("pgbus.locks.concurrency.key_released", key: key, count: count)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Drop every job parked behind a concurrency key. They never run, so the
|
|
52
|
+
# data source resolves their batch and uniqueness bookkeeping.
|
|
53
|
+
def discard_parked
|
|
54
|
+
key = params[:key].to_s
|
|
55
|
+
return redirect_to(locks_path, alert: t("pgbus.locks.concurrency.no_key")) if key.strip.empty?
|
|
56
|
+
|
|
57
|
+
count = data_source.discard_parked_jobs(key)
|
|
58
|
+
redirect_to locks_path, notice: t("pgbus.locks.concurrency.parked_discarded", count: count)
|
|
59
|
+
end
|
|
33
60
|
end
|
|
34
61
|
end
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-orange-400:oklch(75% .183 55.934);--color-orange-600:oklch(64.6% .222 41.116);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.end{inset-inline-end:var(--spacing)}.top-4{top:calc(var(--spacing) * 4)}.right-0{right:calc(var(--spacing) * 0)}.right-4{right:calc(var(--spacing) * 4)}.z-10{z-index:10}.z-50{z-index:50}.z-\[100\]{z-index:100}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:calc(var(--spacing) * 1)}@media (max-width:1023px){.pgbus-table{box-sizing:border-box;width:100%;max-width:100%;display:block}.pgbus-table thead{display:none}.pgbus-table tbody{width:100%;display:block}.pgbus-table tbody tr{box-sizing:border-box;overflow-wrap:anywhere;border:1px solid #e5e7eb;border-radius:.5rem;width:100%;max-width:100%;margin-bottom:.75rem;padding:.75rem;display:block}.pgbus-table:where(.dark,.dark *) tbody tr{border-color:#374151}.pgbus-table tbody td{box-sizing:border-box;text-align:right;border:none;justify-content:space-between;align-items:baseline;gap:1rem;width:100%;min-width:0;max-width:100%;padding:.25rem 0;display:flex}.pgbus-table tbody td>*{overflow-wrap:anywhere;min-width:0;max-width:100%}.pgbus-table tbody td:before{content:attr(data-label);text-transform:uppercase;color:#6b7280;text-align:left;flex-shrink:0;font-size:.75rem;font-weight:600}.pgbus-table:where(.dark,.dark *) tbody td:before{color:#9ca3af}.pgbus-table tbody td[colspan]{text-align:center;display:block}.pgbus-table tbody td[colspan]:before{display:none}}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.-ml-px{margin-left:-1px}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-96{max-height:calc(var(--spacing) * 96)}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-40{width:calc(var(--spacing) * 40)}.w-44{width:calc(var(--spacing) * 44)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.origin-top-right{transform-origin:100% 0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-200{border-color:var(--color-blue-200)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-800{border-color:var(--color-gray-800)}.border-green-200{border-color:var(--color-green-200)}.border-indigo-600{border-color:var(--color-indigo-600)}.border-red-200{border-color:var(--color-red-200)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-red-800{background-color:var(--color-red-800)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-600{background-color:var(--color-yellow-600)}.p-0{padding:calc(var(--spacing) * 0)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-all{word-break:break-all}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-orange-600{color:var(--color-orange-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.ring-gray-200{--tw-ring-color:var(--color-gray-200)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.backdrop\:bg-gray-900\/50::backdrop{background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-gray-900\/50::backdrop{background-color:color-mix(in oklab, var(--color-gray-900) 50%, transparent)}}@media (hover:hover){.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-yellow-200:hover{background-color:var(--color-yellow-200)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.hover\:text-blue-700:hover{color:var(--color-blue-700)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-green-800:hover{color:var(--color-green-800)}.hover\:text-indigo-500:hover{color:var(--color-indigo-500)}.hover\:text-indigo-800:hover{color:var(--color-indigo-800)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-white:hover{color:var(--color-white)}.hover\:text-yellow-800:hover{color:var(--color-yellow-800)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:z-10:focus{z-index:10}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}.focus\:ring-indigo-600:focus{--tw-ring-color:var(--color-indigo-600)}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-yellow-500:focus{--tw-ring-color:var(--color-yellow-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:mt-0{margin-top:calc(var(--spacing) * 0)}.sm\:grid{display:grid}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:64rem){.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}.dark\:block:where(.dark,.dark *){display:block}.dark\:hidden:where(.dark,.dark *){display:none}:where(.dark\:divide-gray-700:where(.dark,.dark *)>:not(:last-child)){border-color:var(--color-gray-700)}.dark\:border-blue-800:where(.dark,.dark *){border-color:var(--color-blue-800)}.dark\:border-gray-600:where(.dark,.dark *){border-color:var(--color-gray-600)}.dark\:border-gray-700:where(.dark,.dark *){border-color:var(--color-gray-700)}.dark\:border-green-800:where(.dark,.dark *){border-color:var(--color-green-800)}.dark\:border-indigo-500:where(.dark,.dark *){border-color:var(--color-indigo-500)}.dark\:border-red-800:where(.dark,.dark *){border-color:var(--color-red-800)}.dark\:bg-blue-900\/30:where(.dark,.dark *){background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-blue-900) 30%, transparent)}}.dark\:bg-gray-700:where(.dark,.dark *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:where(.dark,.dark *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:where(.dark,.dark *){background-color:var(--color-gray-900)}.dark\:bg-gray-900\/50:where(.dark,.dark *){background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.dark\:bg-gray-900\/50:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-gray-900) 50%, transparent)}}.dark\:bg-gray-950:where(.dark,.dark *){background-color:var(--color-gray-950)}.dark\:bg-green-900\/30:where(.dark,.dark *){background-color:#0d542b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-green-900) 30%, transparent)}}.dark\:bg-indigo-900\/30:where(.dark,.dark *){background-color:#312c854d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-indigo-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-indigo-900) 30%, transparent)}}.dark\:bg-red-900:where(.dark,.dark *){background-color:var(--color-red-900)}.dark\:bg-red-900\/30:where(.dark,.dark *){background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-red-900) 30%, transparent)}}.dark\:bg-yellow-900\/30:where(.dark,.dark *){background-color:#733e0a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-yellow-900) 30%, transparent)}}.dark\:text-amber-400:where(.dark,.dark *){color:var(--color-amber-400)}.dark\:text-blue-300:where(.dark,.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:where(.dark,.dark *){color:var(--color-blue-400)}.dark\:text-gray-300:where(.dark,.dark *){color:var(--color-gray-300)}.dark\:text-gray-400:where(.dark,.dark *){color:var(--color-gray-400)}.dark\:text-gray-500:where(.dark,.dark *){color:var(--color-gray-500)}.dark\:text-gray-600:where(.dark,.dark *){color:var(--color-gray-600)}.dark\:text-green-300:where(.dark,.dark *){color:var(--color-green-300)}.dark\:text-green-400:where(.dark,.dark *){color:var(--color-green-400)}.dark\:text-indigo-300:where(.dark,.dark *){color:var(--color-indigo-300)}.dark\:text-indigo-400:where(.dark,.dark *){color:var(--color-indigo-400)}.dark\:text-orange-400:where(.dark,.dark *){color:var(--color-orange-400)}.dark\:text-red-200:where(.dark,.dark *){color:var(--color-red-200)}.dark\:text-red-300:where(.dark,.dark *){color:var(--color-red-300)}.dark\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.dark\:text-white:where(.dark,.dark *){color:var(--color-white)}.dark\:text-yellow-400:where(.dark,.dark *){color:var(--color-yellow-400)}.dark\:ring-gray-700:where(.dark,.dark *){--tw-ring-color:var(--color-gray-700)}.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media (hover:hover){.dark\:hover\:bg-gray-600:where(.dark,.dark *):hover{background-color:var(--color-gray-600)}.dark\:hover\:bg-gray-700:where(.dark,.dark *):hover{background-color:var(--color-gray-700)}.dark\:hover\:bg-gray-700\/50:where(.dark,.dark *):hover{background-color:#36415380}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-gray-700\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab, var(--color-gray-700) 50%, transparent)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-orange-400:oklch(75% .183 55.934);--color-orange-600:oklch(64.6% .222 41.116);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.end{inset-inline-end:var(--spacing)}.top-4{top:calc(var(--spacing) * 4)}.right-0{right:calc(var(--spacing) * 0)}.right-4{right:calc(var(--spacing) * 4)}.z-10{z-index:10}.z-50{z-index:50}.z-\[100\]{z-index:100}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:calc(var(--spacing) * 1)}@media (max-width:1023px){.pgbus-table{box-sizing:border-box;width:100%;max-width:100%;display:block}.pgbus-table thead{display:none}.pgbus-table tbody{width:100%;display:block}.pgbus-table tbody tr{box-sizing:border-box;overflow-wrap:anywhere;border:1px solid #e5e7eb;border-radius:.5rem;width:100%;max-width:100%;margin-bottom:.75rem;padding:.75rem;display:block}.pgbus-table:where(.dark,.dark *) tbody tr{border-color:#374151}.pgbus-table tbody td{box-sizing:border-box;text-align:right;border:none;justify-content:space-between;align-items:baseline;gap:1rem;width:100%;min-width:0;max-width:100%;padding:.25rem 0;display:flex}.pgbus-table tbody td>*{overflow-wrap:anywhere;min-width:0;max-width:100%}.pgbus-table tbody td:before{content:attr(data-label);text-transform:uppercase;color:#6b7280;text-align:left;flex-shrink:0;font-size:.75rem;font-weight:600}.pgbus-table:where(.dark,.dark *) tbody td:before{color:#9ca3af}.pgbus-table tbody td[colspan]{text-align:center;display:block}.pgbus-table tbody td[colspan]:before{display:none}}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.-ml-px{margin-left:-1px}.ml-1{margin-left:calc(var(--spacing) * 1)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-96{max-height:calc(var(--spacing) * 96)}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-40{width:calc(var(--spacing) * 40)}.w-44{width:calc(var(--spacing) * 44)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.origin-top-right{transform-origin:100% 0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-200{border-color:var(--color-blue-200)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-800{border-color:var(--color-gray-800)}.border-green-200{border-color:var(--color-green-200)}.border-indigo-600{border-color:var(--color-indigo-600)}.border-red-200{border-color:var(--color-red-200)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-red-800{background-color:var(--color-red-800)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-600{background-color:var(--color-yellow-600)}.p-0{padding:calc(var(--spacing) * 0)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-all{word-break:break-all}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-orange-600{color:var(--color-orange-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.ring-gray-200{--tw-ring-color:var(--color-gray-200)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.backdrop\:bg-gray-900\/50::backdrop{background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-gray-900\/50::backdrop{background-color:color-mix(in oklab, var(--color-gray-900) 50%, transparent)}}@media (hover:hover){.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-yellow-200:hover{background-color:var(--color-yellow-200)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.hover\:text-blue-700:hover{color:var(--color-blue-700)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-green-800:hover{color:var(--color-green-800)}.hover\:text-indigo-500:hover{color:var(--color-indigo-500)}.hover\:text-indigo-800:hover{color:var(--color-indigo-800)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-white:hover{color:var(--color-white)}.hover\:text-yellow-800:hover{color:var(--color-yellow-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:ring-gray-300:hover{--tw-ring-color:var(--color-gray-300)}}.focus\:z-10:focus{z-index:10}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}.focus\:ring-indigo-600:focus{--tw-ring-color:var(--color-indigo-600)}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-yellow-500:focus{--tw-ring-color:var(--color-yellow-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:mt-0{margin-top:calc(var(--spacing) * 0)}.sm\:grid{display:grid}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:64rem){.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}.dark\:block:where(.dark,.dark *){display:block}.dark\:hidden:where(.dark,.dark *){display:none}:where(.dark\:divide-gray-700:where(.dark,.dark *)>:not(:last-child)){border-color:var(--color-gray-700)}.dark\:border-blue-800:where(.dark,.dark *){border-color:var(--color-blue-800)}.dark\:border-gray-600:where(.dark,.dark *){border-color:var(--color-gray-600)}.dark\:border-gray-700:where(.dark,.dark *){border-color:var(--color-gray-700)}.dark\:border-green-800:where(.dark,.dark *){border-color:var(--color-green-800)}.dark\:border-indigo-500:where(.dark,.dark *){border-color:var(--color-indigo-500)}.dark\:border-red-800:where(.dark,.dark *){border-color:var(--color-red-800)}.dark\:bg-blue-900\/30:where(.dark,.dark *){background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-blue-900) 30%, transparent)}}.dark\:bg-gray-700:where(.dark,.dark *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:where(.dark,.dark *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:where(.dark,.dark *){background-color:var(--color-gray-900)}.dark\:bg-gray-900\/50:where(.dark,.dark *){background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.dark\:bg-gray-900\/50:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-gray-900) 50%, transparent)}}.dark\:bg-gray-950:where(.dark,.dark *){background-color:var(--color-gray-950)}.dark\:bg-green-900\/30:where(.dark,.dark *){background-color:#0d542b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-green-900) 30%, transparent)}}.dark\:bg-indigo-900\/30:where(.dark,.dark *){background-color:#312c854d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-indigo-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-indigo-900) 30%, transparent)}}.dark\:bg-red-900:where(.dark,.dark *){background-color:var(--color-red-900)}.dark\:bg-red-900\/30:where(.dark,.dark *){background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-red-900) 30%, transparent)}}.dark\:bg-yellow-900\/30:where(.dark,.dark *){background-color:#733e0a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-yellow-900) 30%, transparent)}}.dark\:text-amber-400:where(.dark,.dark *){color:var(--color-amber-400)}.dark\:text-blue-300:where(.dark,.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:where(.dark,.dark *){color:var(--color-blue-400)}.dark\:text-gray-300:where(.dark,.dark *){color:var(--color-gray-300)}.dark\:text-gray-400:where(.dark,.dark *){color:var(--color-gray-400)}.dark\:text-gray-500:where(.dark,.dark *){color:var(--color-gray-500)}.dark\:text-gray-600:where(.dark,.dark *){color:var(--color-gray-600)}.dark\:text-green-300:where(.dark,.dark *){color:var(--color-green-300)}.dark\:text-green-400:where(.dark,.dark *){color:var(--color-green-400)}.dark\:text-indigo-300:where(.dark,.dark *){color:var(--color-indigo-300)}.dark\:text-indigo-400:where(.dark,.dark *){color:var(--color-indigo-400)}.dark\:text-orange-400:where(.dark,.dark *){color:var(--color-orange-400)}.dark\:text-red-200:where(.dark,.dark *){color:var(--color-red-200)}.dark\:text-red-300:where(.dark,.dark *){color:var(--color-red-300)}.dark\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.dark\:text-white:where(.dark,.dark *){color:var(--color-white)}.dark\:text-yellow-400:where(.dark,.dark *){color:var(--color-yellow-400)}.dark\:ring-gray-700:where(.dark,.dark *){--tw-ring-color:var(--color-gray-700)}.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media (hover:hover){.dark\:hover\:bg-gray-600:where(.dark,.dark *):hover{background-color:var(--color-gray-600)}.dark\:hover\:bg-gray-700:where(.dark,.dark *):hover{background-color:var(--color-gray-700)}.dark\:hover\:bg-gray-700\/50:where(.dark,.dark *):hover{background-color:#36415380}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-gray-700\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab, var(--color-gray-700) 50%, transparent)}}.dark\:hover\:ring-gray-600:where(.dark,.dark *):hover{--tw-ring-color:var(--color-gray-600)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}
|
|
@@ -86,6 +86,26 @@ module Pgbus
|
|
|
86
86
|
)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Release a lock only while it is still UNBOUND (msg_id = 0) AND was
|
|
90
|
+
# acquired no later than +acquired_before+. Used when discarding a parked
|
|
91
|
+
# job: a parked job never got a msg_id, so its lock was never bound, and
|
|
92
|
+
# there is no message to identify it by the way release_if_bound! does.
|
|
93
|
+
#
|
|
94
|
+
# Two things could otherwise be dropped by mistake once the unbound-lock
|
|
95
|
+
# reaper has removed the parked job's own row and a successor has taken the
|
|
96
|
+
# same key: a successor that was actually sent (its row is bound, excluded
|
|
97
|
+
# by msg_id = 0), and a successor that is itself parked (also unbound, so
|
|
98
|
+
# only the timestamp separates them). The lock is acquired immediately
|
|
99
|
+
# before the row is parked, so a lock created AFTER the parked row cannot
|
|
100
|
+
# belong to it — pass the parked row's created_at as the ceiling.
|
|
101
|
+
def self.release_if_unbound!(lock_key, acquired_before:)
|
|
102
|
+
Thread.current[:pgbus_uniqueness_created_at]&.delete(lock_key)
|
|
103
|
+
connection.exec_delete(
|
|
104
|
+
"DELETE FROM #{table_name} WHERE lock_key = $1 AND msg_id = 0 AND created_at <= $2",
|
|
105
|
+
"UniquenessKey Release If Unbound", [lock_key, acquired_before]
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
89
109
|
# Check if a key is currently locked.
|
|
90
110
|
def self.locked?(lock_key)
|
|
91
111
|
result = connection.select_value(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<turbo-frame id="dashboard-stats" data-auto-refresh data-src="<%= pgbus.root_path(frame: 'stats') %>">
|
|
2
|
-
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-
|
|
2
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-7 mb-8">
|
|
3
3
|
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
4
4
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.queues") %></p>
|
|
5
5
|
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:total_queues] %></p>
|
|
@@ -39,5 +39,18 @@
|
|
|
39
39
|
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:throughput_rate] %></p>
|
|
40
40
|
<p class="text-xs text-gray-400"><%= t("pgbus.dashboard.stats_cards.throughput_unit") %></p>
|
|
41
41
|
</div>
|
|
42
|
+
|
|
43
|
+
<%# The whole card links to the Locks page: parked jobs are only actionable
|
|
44
|
+
there. turbo_frame: "_top" because this card sits inside the
|
|
45
|
+
dashboard-stats frame, and /locks has no frame of that id to render into. %>
|
|
46
|
+
<%= link_to pgbus.locks_path, data: { turbo_frame: "_top" }, class: "rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700 block hover:ring-gray-300 dark:hover:ring-gray-600" do %>
|
|
47
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.parked_jobs") %></p>
|
|
48
|
+
<p class="mt-1 text-3xl font-semibold <%= @stats[:parked_total].to_i > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-900 dark:text-white' %>">
|
|
49
|
+
<%= pgbus_number(@stats[:parked_total]) %>
|
|
50
|
+
</p>
|
|
51
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
52
|
+
<%= t("pgbus.dashboard.stats_cards.parked_jobs_oldest", age: pgbus_duration(@stats[:oldest_parked_age_sec])) %>
|
|
53
|
+
</p>
|
|
54
|
+
<% end %>
|
|
42
55
|
</div>
|
|
43
56
|
</turbo-frame>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<turbo-frame id="locks-concurrency" data-auto-refresh data-src="<%= pgbus.locks_path(frame: 'concurrency') %>">
|
|
2
|
+
<div class="mb-8">
|
|
3
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.locks.concurrency.title") %></h2>
|
|
4
|
+
<p class="mt-1 mb-3 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.description") %></p>
|
|
5
|
+
|
|
6
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-4">
|
|
7
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
8
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.cards.parked_jobs") %></p>
|
|
9
|
+
<p class="mt-1 text-3xl font-semibold <%= @concurrency[:parked_total] > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-900 dark:text-white' %>">
|
|
10
|
+
<%= pgbus_number(@concurrency[:parked_total]) %>
|
|
11
|
+
</p>
|
|
12
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.concurrency.cards.parked_jobs_hint") %></p>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
16
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.cards.oldest_wait") %></p>
|
|
17
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_duration(@concurrency[:oldest_parked_age_sec]) %></p>
|
|
18
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.concurrency.cards.oldest_wait_hint") %></p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
22
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.cards.slots_held") %></p>
|
|
23
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@concurrency[:slots_held]) %></p>
|
|
24
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.concurrency.cards.slots_held_hint") %></p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
28
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.cards.keys_at_limit") %></p>
|
|
29
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@concurrency[:keys_at_limit]) %></p>
|
|
30
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.concurrency.cards.keys_at_limit_hint") %></p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
35
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
36
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
37
|
+
<tr>
|
|
38
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.headers.key") %></th>
|
|
39
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.headers.in_use") %></th>
|
|
40
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.headers.lease") %></th>
|
|
41
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.headers.parked") %></th>
|
|
42
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.concurrency.headers.oldest_wait") %></th>
|
|
43
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"></th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
47
|
+
<% @concurrency[:keys].each do |row| %>
|
|
48
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
49
|
+
<td data-label="Key" class="px-4 py-3 text-sm font-mono text-gray-700 dark:text-gray-300 max-w-xs truncate"><%= row[:key] %></td>
|
|
50
|
+
<td data-label="In use" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
51
|
+
<% if row[:value] %>
|
|
52
|
+
<%= row[:value] %> / <%= row[:max_value] %>
|
|
53
|
+
<% else %>
|
|
54
|
+
—
|
|
55
|
+
<% end %>
|
|
56
|
+
</td>
|
|
57
|
+
<td data-label="Lease" class="px-4 py-3 text-sm">
|
|
58
|
+
<% if row[:value].nil? %>
|
|
59
|
+
<span class="text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.concurrency.lease.none") %></span>
|
|
60
|
+
<% elsif row[:lease_fresh] %>
|
|
61
|
+
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800"><%= t("pgbus.locks.concurrency.lease.live") %></span>
|
|
62
|
+
<span class="ml-1 text-xs text-gray-400 dark:text-gray-500"><%= pgbus_time_ago_future(row[:expires_at]) %></span>
|
|
63
|
+
<% else %>
|
|
64
|
+
<span class="inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"><%= t("pgbus.locks.concurrency.lease.expired") %></span>
|
|
65
|
+
<% end %>
|
|
66
|
+
</td>
|
|
67
|
+
<td data-label="Parked" class="px-4 py-3 text-sm text-right <%= row[:parked_count] > 0 ? 'text-amber-600 dark:text-amber-400 font-semibold' : 'text-gray-500 dark:text-gray-400' %>"><%= pgbus_number(row[:parked_count]) %></td>
|
|
68
|
+
<td data-label="Oldest wait" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_duration(row[:oldest_parked_age_sec]) %></td>
|
|
69
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
70
|
+
<div class="flex items-center justify-end space-x-3">
|
|
71
|
+
<%= button_to t("pgbus.locks.concurrency.release"), pgbus.release_key_locks_path,
|
|
72
|
+
method: :post, params: { key: row[:key] },
|
|
73
|
+
class: "text-xs text-indigo-600 hover:text-indigo-800 font-medium",
|
|
74
|
+
data: { turbo_frame: "_top",
|
|
75
|
+
turbo_confirm: row[:lease_fresh] ? t("pgbus.locks.concurrency.release_confirm_live", key: row[:key]) : t("pgbus.locks.concurrency.release_confirm", key: row[:key]) } %>
|
|
76
|
+
<% if row[:parked_count] > 0 %>
|
|
77
|
+
<%= button_to t("pgbus.locks.concurrency.discard_parked"), pgbus.discard_parked_locks_path,
|
|
78
|
+
method: :post, params: { key: row[:key] },
|
|
79
|
+
class: "text-xs text-red-600 hover:text-red-800 font-medium",
|
|
80
|
+
data: { turbo_frame: "_top",
|
|
81
|
+
turbo_confirm: t("pgbus.locks.concurrency.discard_parked_confirm", count: row[:parked_count]) } %>
|
|
82
|
+
<% end %>
|
|
83
|
+
</div>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
<% end %>
|
|
87
|
+
<% if @concurrency[:keys].empty? %>
|
|
88
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.concurrency.empty") %></td></tr>
|
|
89
|
+
<% end %>
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</turbo-frame>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<div class="mb-8">
|
|
2
|
+
<div class="flex items-center justify-between mb-3">
|
|
3
|
+
<div>
|
|
4
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.locks.index.uniqueness_title") %></h2>
|
|
5
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.description") %></p>
|
|
6
|
+
</div>
|
|
7
|
+
<% if @locks.any? %>
|
|
8
|
+
<div class="flex items-center space-x-2">
|
|
9
|
+
<div data-bulk-actions class="hidden flex items-center space-x-2">
|
|
10
|
+
<span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
|
|
11
|
+
<button type="submit" form="bulk-discard-locks-form"
|
|
12
|
+
class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
13
|
+
data-turbo-confirm="<%= t("pgbus.locks.index.discard_selected_confirm") %>">
|
|
14
|
+
<%= t("pgbus.locks.index.discard_selected") %>
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
<%= button_to t("pgbus.locks.index.discard_all"), pgbus.discard_all_locks_path, method: :post,
|
|
18
|
+
class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
|
|
19
|
+
data: { turbo_confirm: t("pgbus.locks.index.discard_all_confirm") } %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<%# Bulk form lives outside the table to avoid nested form issues with button_to %>
|
|
25
|
+
<form id="bulk-discard-locks-form" action="<%= pgbus.discard_selected_locks_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
26
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
27
|
+
</form>
|
|
28
|
+
|
|
29
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="locks">
|
|
30
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
31
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
32
|
+
<tr>
|
|
33
|
+
<% if @locks.any? %>
|
|
34
|
+
<th class="w-10 px-4 py-3">
|
|
35
|
+
<input type="checkbox" data-bulk-select-all
|
|
36
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
37
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
38
|
+
</th>
|
|
39
|
+
<% end %>
|
|
40
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.lock_key") %></th>
|
|
41
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.queue_name", default: "Queue") %></th>
|
|
42
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.msg_id", default: "Message ID") %></th>
|
|
43
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.age") %></th>
|
|
44
|
+
<% if @locks.any? %>
|
|
45
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"></th>
|
|
46
|
+
<% end %>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
50
|
+
<% @locks.each do |lock| %>
|
|
51
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
52
|
+
<td class="w-10 px-4 py-3">
|
|
53
|
+
<input type="checkbox" name="lock_keys[]" value="<%= lock[:lock_key] %>"
|
|
54
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: lock[:lock_key]) %>"
|
|
55
|
+
form="bulk-discard-locks-form" data-bulk-item
|
|
56
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
57
|
+
</td>
|
|
58
|
+
<td data-label="Lock Key" class="px-4 py-3 text-sm font-mono text-gray-700 dark:text-gray-300 max-w-xs truncate"><%= lock[:lock_key] %></td>
|
|
59
|
+
<td data-label="Queue" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300"><%= lock[:queue_name] %></td>
|
|
60
|
+
<td data-label="Message ID" class="px-4 py-3 text-sm text-right font-mono text-gray-500 dark:text-gray-400"><%= lock[:msg_id] %></td>
|
|
61
|
+
<td data-label="Age" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
|
|
62
|
+
<% if lock[:age_seconds] %>
|
|
63
|
+
<%= pgbus_duration(lock[:age_seconds]) %>
|
|
64
|
+
<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
67
|
+
<%= button_to t("pgbus.locks.index.discard"), pgbus.discard_lock_path(lock[:lock_key]),
|
|
68
|
+
method: :post,
|
|
69
|
+
class: "text-xs text-red-600 hover:text-red-800 font-medium",
|
|
70
|
+
data: { turbo_confirm: t("pgbus.locks.index.discard_confirm"), turbo_frame: "_top" } %>
|
|
71
|
+
</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% if @locks.empty? %>
|
|
75
|
+
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.index.empty") %></td></tr>
|
|
76
|
+
<% end %>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
@@ -1,78 +1,7 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.description") %></p>
|
|
5
|
-
</div>
|
|
6
|
-
<% if @locks.any? %>
|
|
7
|
-
<div class="flex items-center space-x-2">
|
|
8
|
-
<div data-bulk-actions class="hidden flex items-center space-x-2">
|
|
9
|
-
<span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
|
|
10
|
-
<button type="submit" form="bulk-discard-locks-form"
|
|
11
|
-
class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
12
|
-
data-turbo-confirm="<%= t("pgbus.locks.index.discard_selected_confirm") %>">
|
|
13
|
-
<%= t("pgbus.locks.index.discard_selected") %>
|
|
14
|
-
</button>
|
|
15
|
-
</div>
|
|
16
|
-
<%= button_to t("pgbus.locks.index.discard_all"), pgbus.discard_all_locks_path, method: :post,
|
|
17
|
-
class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
|
|
18
|
-
data: { turbo_confirm: t("pgbus.locks.index.discard_all_confirm") } %>
|
|
19
|
-
</div>
|
|
20
|
-
<% end %>
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.locks.index.title") %></h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.page_description") %></p>
|
|
21
4
|
</div>
|
|
22
5
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
26
|
-
</form>
|
|
27
|
-
|
|
28
|
-
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="locks">
|
|
29
|
-
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
30
|
-
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
31
|
-
<tr>
|
|
32
|
-
<% if @locks.any? %>
|
|
33
|
-
<th class="w-10 px-4 py-3">
|
|
34
|
-
<input type="checkbox" data-bulk-select-all
|
|
35
|
-
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
36
|
-
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
37
|
-
</th>
|
|
38
|
-
<% end %>
|
|
39
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.lock_key") %></th>
|
|
40
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.queue_name", default: "Queue") %></th>
|
|
41
|
-
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.msg_id", default: "Message ID") %></th>
|
|
42
|
-
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.age") %></th>
|
|
43
|
-
<% if @locks.any? %>
|
|
44
|
-
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"></th>
|
|
45
|
-
<% end %>
|
|
46
|
-
</tr>
|
|
47
|
-
</thead>
|
|
48
|
-
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
49
|
-
<% @locks.each do |lock| %>
|
|
50
|
-
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
51
|
-
<td class="w-10 px-4 py-3">
|
|
52
|
-
<input type="checkbox" name="lock_keys[]" value="<%= lock[:lock_key] %>"
|
|
53
|
-
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: lock[:lock_key]) %>"
|
|
54
|
-
form="bulk-discard-locks-form" data-bulk-item
|
|
55
|
-
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
56
|
-
</td>
|
|
57
|
-
<td data-label="Lock Key" class="px-4 py-3 text-sm font-mono text-gray-700 dark:text-gray-300 max-w-xs truncate"><%= lock[:lock_key] %></td>
|
|
58
|
-
<td data-label="Queue" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300"><%= lock[:queue_name] %></td>
|
|
59
|
-
<td data-label="Message ID" class="px-4 py-3 text-sm text-right font-mono text-gray-500 dark:text-gray-400"><%= lock[:msg_id] %></td>
|
|
60
|
-
<td data-label="Age" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
|
|
61
|
-
<% if lock[:age_seconds] %>
|
|
62
|
-
<%= pgbus_duration(lock[:age_seconds]) %>
|
|
63
|
-
<% end %>
|
|
64
|
-
</td>
|
|
65
|
-
<td class="px-4 py-3 text-sm text-right">
|
|
66
|
-
<%= button_to t("pgbus.locks.index.discard"), pgbus.discard_lock_path(lock[:lock_key]),
|
|
67
|
-
method: :post,
|
|
68
|
-
class: "text-xs text-red-600 hover:text-red-800 font-medium",
|
|
69
|
-
data: { turbo_confirm: t("pgbus.locks.index.discard_confirm"), turbo_frame: "_top" } %>
|
|
70
|
-
</td>
|
|
71
|
-
</tr>
|
|
72
|
-
<% end %>
|
|
73
|
-
<% if @locks.empty? %>
|
|
74
|
-
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.index.empty") %></td></tr>
|
|
75
|
-
<% end %>
|
|
76
|
-
</tbody>
|
|
77
|
-
</table>
|
|
78
|
-
</div>
|
|
6
|
+
<%= render "pgbus/locks/uniqueness" %>
|
|
7
|
+
<%= render "pgbus/locks/concurrency" %>
|
data/config/locales/da.yml
CHANGED
|
@@ -86,6 +86,8 @@ da:
|
|
|
86
86
|
enqueued: Sat i kø
|
|
87
87
|
failed_dlq: Mislykkedes / DLQ
|
|
88
88
|
failed_dlq_labels: Fejlet / Dead Lettered
|
|
89
|
+
parked_jobs: Parkerede job
|
|
90
|
+
parked_jobs_oldest: ældst %{age}
|
|
89
91
|
processes: Processer
|
|
90
92
|
queues: Køer
|
|
91
93
|
recurring: Gentagende
|
|
@@ -404,6 +406,43 @@ da:
|
|
|
404
406
|
toggle_dark_mode: Skift til mørk tilstand
|
|
405
407
|
toggle_menu: Skift menu
|
|
406
408
|
locks:
|
|
409
|
+
concurrency:
|
|
410
|
+
cards:
|
|
411
|
+
keys_at_limit: Nøgler ved grænsen
|
|
412
|
+
keys_at_limit_hint: ingen ledig plads
|
|
413
|
+
oldest_wait: Længste ventetid
|
|
414
|
+
oldest_wait_hint: længst et job har ventet
|
|
415
|
+
parked_jobs: Parkerede job
|
|
416
|
+
parked_jobs_hint: venter på en plads
|
|
417
|
+
slots_held: Holdte pladser
|
|
418
|
+
slots_held_hint: på tværs af alle nøgler
|
|
419
|
+
description: Nøgler erklæret med limits_concurrency, pladserne de holder, og de job der er parkeret bag dem
|
|
420
|
+
discard_parked: Kassér parkerede
|
|
421
|
+
discard_parked_confirm:
|
|
422
|
+
one: Kassér det 1 job der er parkeret bag denne nøgle? Det vil aldrig køre.
|
|
423
|
+
other: Kassér de %{count} job der er parkeret bag denne nøgle? De vil aldrig køre.
|
|
424
|
+
empty: Ingen samtidighedsnøgler i brug
|
|
425
|
+
headers:
|
|
426
|
+
in_use: I brug
|
|
427
|
+
key: Nøgle
|
|
428
|
+
lease: Lease
|
|
429
|
+
oldest_wait: Længste ventetid
|
|
430
|
+
parked: Parkerede
|
|
431
|
+
key_released:
|
|
432
|
+
one: Frigav %{key} og fremrykkede 1 job.
|
|
433
|
+
other: Frigav %{key} og fremrykkede %{count} job.
|
|
434
|
+
lease:
|
|
435
|
+
expired: Udløbet
|
|
436
|
+
live: Aktiv
|
|
437
|
+
none: Ingen indehaver
|
|
438
|
+
no_key: Ingen samtidighedsnøgle angivet.
|
|
439
|
+
parked_discarded:
|
|
440
|
+
one: Kasserede 1 parkeret job.
|
|
441
|
+
other: Kasserede %{count} parkerede job.
|
|
442
|
+
release: Frigiv
|
|
443
|
+
release_confirm: Frigiv %{key} og fremryk dens parkerede job?
|
|
444
|
+
release_confirm_live: Leasen på %{key} er stadig frisk, så et job kører sandsynligvis stadig. Frigivelse lader et andet starte ved siden af. Fortsæt?
|
|
445
|
+
title: Samtidighed
|
|
407
446
|
index:
|
|
408
447
|
all_locks_discarded:
|
|
409
448
|
one: Kasserede 1 lås.
|
|
@@ -427,7 +466,9 @@ da:
|
|
|
427
466
|
one: Kasserede 1 lås.
|
|
428
467
|
other: Kasserede %{count} låse.
|
|
429
468
|
none_selected: Ingen låse valgt.
|
|
430
|
-
|
|
469
|
+
page_description: Unikhedsnøgler og samtidighedspladser der holder job tilbage
|
|
470
|
+
title: Låse
|
|
471
|
+
uniqueness_title: Unikhedsnøgler
|
|
431
472
|
outbox:
|
|
432
473
|
index:
|
|
433
474
|
description: Transaktionelle udbakke-poster, der venter på publicering til PGMQ
|