pgbus 0.16.3 → 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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/app/controllers/pgbus/locks_controller.rb +27 -0
  4. data/app/frontend/pgbus/style.css +1 -1
  5. data/app/models/pgbus/blocked_execution.rb +33 -7
  6. data/app/models/pgbus/semaphore.rb +8 -3
  7. data/app/models/pgbus/uniqueness_key.rb +36 -0
  8. data/app/views/pgbus/dashboard/_stats_cards.html.erb +14 -1
  9. data/app/views/pgbus/locks/_concurrency.html.erb +94 -0
  10. data/app/views/pgbus/locks/_uniqueness.html.erb +80 -0
  11. data/app/views/pgbus/locks/index.html.erb +5 -76
  12. data/config/locales/da.yml +42 -1
  13. data/config/locales/de.yml +42 -1
  14. data/config/locales/en.yml +42 -1
  15. data/config/locales/es.yml +42 -1
  16. data/config/locales/fi.yml +42 -1
  17. data/config/locales/fr.yml +42 -1
  18. data/config/locales/it.yml +42 -1
  19. data/config/locales/ja.yml +42 -1
  20. data/config/locales/nb.yml +42 -1
  21. data/config/locales/nl.yml +42 -1
  22. data/config/locales/pt.yml +42 -1
  23. data/config/locales/sv.yml +42 -1
  24. data/config/routes.rb +5 -0
  25. data/lib/pgbus/active_job/adapter.rb +83 -9
  26. data/lib/pgbus/active_job/executor.rb +55 -15
  27. data/lib/pgbus/concurrency/blocked_execution.rb +59 -18
  28. data/lib/pgbus/concurrency/semaphore.rb +34 -0
  29. data/lib/pgbus/concurrency.rb +36 -2
  30. data/lib/pgbus/instrumentation.rb +3 -0
  31. data/lib/pgbus/integrations/appsignal/dashboard.json +38 -0
  32. data/lib/pgbus/integrations/appsignal/probe.rb +10 -0
  33. data/lib/pgbus/mcp/base_tool.rb +6 -1
  34. data/lib/pgbus/mcp/server.rb +2 -1
  35. data/lib/pgbus/mcp/tools/concurrency_tool.rb +32 -0
  36. data/lib/pgbus/mcp.rb +1 -0
  37. data/lib/pgbus/process/dispatcher.rb +6 -13
  38. data/lib/pgbus/version.rb +1 -1
  39. data/lib/pgbus/visibility_heartbeat.rb +24 -3
  40. data/lib/pgbus/web/data_source.rb +210 -1
  41. data/lib/pgbus/web/metrics_serializer.rb +51 -3
  42. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ccb9204900296bfd53c5845faa89a91eec0eddedd7073818af147ef135daa82
4
- data.tar.gz: db8ee8c7cbfe2ac34700bce9479736f19cf658a0a728a20a4a8a5988978363fd
3
+ metadata.gz: 4382e9b750a7e3538c2ba640aa476bb914cd1c1deba00fda429510ea1dcdc851
4
+ data.tar.gz: bc8bff2b3b08ccc6de7d67c677a8797be4d0a1b9f65019a9728d0531ad5a830a
5
5
  SHA512:
6
- metadata.gz: 305bac06a4cdf80af363d4cdc242165e02319f9c9b060c98c44db31af833f4bb055f625b79cac14853ab8382a6458f6142b3f9543ddef55f329581bed112c0bb
7
- data.tar.gz: '009aea5c6f176d5b19e87dbd0375e5d9624cd82aa48ace058af26107df57903c0896159d4541bd66615e06973abed5cebc1e7f0230ef935a34df25cba2864e2d'
6
+ metadata.gz: 9630731c5e8b4ef6bb9db80e1e950ad6abee7748e49868fb613645b0d16469f477a6b0fe819427135a6bdf85589fb518e6204f80a3a20810939ed1c2038238a7
7
+ data.tar.gz: b4c5b23b4e4513b5447985ab96b1578a3a1931fda6b3b4700338b71d7d822a989cda6498670a31e7ef30848f69f4075b23a2f018764072f51b6e91ae258d8445
data/CHANGELOG.md CHANGED
@@ -2,10 +2,39 @@
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
+
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:
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.
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:`.
16
+ - **A job parked while the holder was finishing was stranded until the semaphore expired.** The enqueue checked the semaphore and inserted the blocked row as two autocommit statements; a holder finishing in between found no parked row, released the slot, and nobody promoted the row it had just missed (solid_queue #456/#712). The adapter now checks and parks in one transaction — the upsert holds the semaphore row lock until commit even when it returns `:blocked` — and the completion signal decrements under that same lock before it looks for parked rows, so it waits for the in-flight park and then sees it.
17
+ - **A duplicate delivery released the slot twice.** A worker whose heartbeat lapsed had its message redelivered; when both copies finished, both signalled, and a `to: 1` key ran 2, then 3 (solid_queue #761). `pgmq.archive` returning `false` — the message was already archived by the other copy — is now the exact-once claim: that execution returns `:duplicate` and skips the concurrency, batch and uniqueness signals.
18
+
19
+ Underneath, the parked payload was **double-encoded**: `BlockedExecution.insert` handed the jsonb column an already-serialized string, so every row was a jsonb *string*, not an object. Promotion worked only because the client passes a String body through untouched; everything that read the column as a document did the wrong thing silently — the job-class lookup (so a promoted job always took the default limit), `scheduled_at` (a parked `perform_later(wait:)` job promoted immediately), `Batch.backfill_execution` (a parked batch job never got its execution row) and the batch sweep's `payload->>'job_id'` (a parked batch job looked orphaned and was un-counted, undoing #423). Rows are now stored as objects; the sweep rewrites rows written before this release in place (`repair_double_encoded!`, idempotent) and `release_next!` reads both shapes. No migration.
20
+
21
+ Six narrower holes in the same guarantee, found in review:
22
+ - **A scheduled job lost its slot while still waiting in the queue.** The slot is taken at enqueue but its lease is only renewed once a worker picks the message up, so a `perform_later(wait: 1.hour)` job under a 15-minute `duration` was presumed dead mid-wait and a second job promoted for its key. A scheduled job's lease now covers its delay. The residual — a queue backed up longer than `duration` before any worker gets to the message — is documented rather than papered over.
23
+ - **A `duration` shorter than the heartbeat interval expired before the first beat.** `duration` is now floored at twice the effective heartbeat interval, so a lease can never lapse before the beat that would have renewed it.
24
+ - **The enqueue sent the message before its slot committed.** PGMQ has its own connection, so the send can never join the AR transaction; a commit failure after a successful send left the message live with the slot rolled back, and the next enqueue ran beside it. The send now happens after the commit, so the only crash window leaves a slot held with no message — under-admitting until the sweep reclaims it, never over-admitting. A send that raises hands its slot back immediately.
25
+ - **A batch backfill failure could undo a promotion.** `Batch.backfill_execution` ran inside the transaction `Semaphore.signal` opens, so a database error there poisoned it: the commit failed, the parked row came back and the slot was released while the promoted message was already live — the job ran twice. The backfill now has its own savepoint.
26
+ - **An ambiguous archive retry was read as a duplicate.** `archive_from` retries once on a connection error; if the first archive had actually committed and only its reply was lost, the retry reported "already archived" and the executor skipped every completion signal, stranding the slot, the batch execution and the uniqueness lock. It now distinguishes `:ambiguous` from `:already_archived` and claims the former. A genuine duplicate also releases its own `:while_executing` lock, conditioned on the message id so it can never drop a successor's.
27
+ - **The sweep's key scan could starve.** It took the thousand oldest parked keys; a thousand keys whose slots were held sat at the head of that window forever, so a key behind them whose holder had died was never serviced. The scan now selects only keys that could take a slot right now.
28
+
29
+ And five more from a second review pass:
30
+ - **An ambiguous send no longer undoes the bookkeeping for a message that may be live.** A connection error can reach the enqueuer after the produce has committed. Releasing the slot then admits a second job beside that message, and rolling the uniqueness lock back leaves it unguarded — so a database-shaped failure raised from inside the produce now keeps the slot, the lock and the batch count. Each is then reclaimed by its own recovery path if the message really was never written: the lease expiry plus the dispatcher's concurrency sweep for the slot, the dispatcher's unbound-lock reaper for the uniqueness row, and `Batch::Sweep`'s orphan pass for the batch count. Anything raised before the produce is called — taking the slot, parking the job — is not ambiguous and still rolls straight back, as is a non-database failure such as a serialization or argument error. The classifier is deliberately an over-approximation in one place: the client's pre-produce queue setup runs inside the same call, so its connection errors are treated as ambiguous too, erring toward state the reapers can reclaim.
31
+ - **`release_if_bound!` is scoped by queue.** PGMQ message ids are per-queue sequences, so matching a lock on `msg_id` alone could delete a successor holding the same key on another queue.
32
+ - **Heartbeat touches are floored too.** The floor was applied when taking a slot but not when renewing one, so a `duration` below the heartbeat interval still let a lease lapse mid-run.
33
+ - **A promoted scheduled job's lease covers its remaining delay**, matching the direct-enqueue path — a parked `wait:` job promoted while its scheduled time was still far out got a lease that expired before the message was ever visible.
34
+ - The concurrency page names the one window a lease cannot cover (enqueue to first heartbeat) and how to size `duration` against it.
35
+
36
+ `Concurrency::BlockedExecution.expire_stale` is gone; `Concurrency::Semaphore.signal(key, client:)` replaces the executor's promote-then-release pair; `Concurrency::BlockedExecution.promote_pending(client:)` is the sweep; `Executor#execute` returns `:duplicate` when another worker archived its message. Docs: the concurrency page now spells out the `:block` guarantees, what `duration` means and the one window it cannot cover.
37
+
9
38
  - **The batch progress bar rendered at zero width — because it rendered with no colour at all.** `app/frontend/pgbus/style.css` is a committed Tailwind build artifact that nothing rebuilds automatically; it was last compiled in #47, before the Batches tab existed. Every Tailwind class added to a view since then resolved to no CSS rule, so the progress fill on `/batches` and `/batches/:id` sized itself correctly from its inline `width:` and then painted nothing — a batch at 483/807 looked stuck at 0% no matter how often you reloaded. 33 classes were missing in total, including the bar's own `bg-green-500` / `bg-amber-500` fill and `bg-gray-200` track, the batches-table bar geometry (`h-2`, `w-24`), and `sm:grid-cols-4`, `hover:underline`, `gap-2`, `z-10`, `break-all`, `cursor-not-allowed` elsewhere on the dashboard. The artifact is rebuilt, `tailwind.css` now names its sources explicitly with `@source` (automatic detection would also walk the nested `docs/` app), `rake frontend:css` makes the build repeatable and version-pinned from `bun.lock`, and a new spec fails the suite whenever a class used in a view is absent from the artifact — the drift can no longer ship silently.
10
39
 
11
40
  - **Concurrent first-broadcast notify-insert setup no longer pages on a Postgres deadlock / lock-wait (the residual of #403).** Two processes racing the first durable Turbo/Pgbus stream broadcast can both see "notify trigger not current" and both run `pgmq.enable_notify_insert` → `DROP TRIGGER` under `AccessExclusiveLock`. Postgres deadlocks, or hits `lock_timeout`, or — on pooled connections where `statement_timeout` < `lock_timeout` — `canceling statement due to statement timeout` **while locking**. PGMQ wraps those as `PGMQ::Errors::ConnectionError`. 0.16.1 already skips DROP when the trigger is current and retries stale sockets via `with_stale_connection_retry`, but it did not retry these lock races, so the residual still paged (getzazu/app#3817; AppSignal Zazu ma-prod #546 / za-prod #530). `ensure_stream_queue` now wraps the notify-setup path in a dedicated `with_notify_lock_retry` (up to 3 attempts, short backoff **outside** `@pgmq_mutex`): deadlock, lock-not-available, lock-timeout, and statement-timeout **with** lock-wait context retry; a bare statement timeout and permission errors still fail fast, while a missing-queue `ConnectionError` uses the existing one-time queue-recreation path without lock retries. `with_stale_connection_retry` is unchanged — that helper stays idle-socket / no-SQL-sent only. Consumers that prepended an app-level retry around `ensure_stream_queue` (getzazu/app#3827) can drop that prepend after upgrading.
@@ -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}
@@ -5,19 +5,17 @@ module Pgbus
5
5
  self.table_name = "pgbus_blocked_executions"
6
6
 
7
7
  scope :for_key, ->(key) { where(concurrency_key: key) }
8
- scope :expired, ->(now = Time.current) { where("expires_at < ?", now) }
9
8
 
10
- # Atomic dequeue: DELETE the highest-priority non-expired row with FOR UPDATE SKIP LOCKED.
11
- # Returns { queue_name:, payload: } or nil.
9
+ # Atomic dequeue: DELETE the highest-priority row with FOR UPDATE SKIP LOCKED.
10
+ # A parked job never ages out — `expires_at` orders the sweep, nothing more.
11
+ # Returns { queue_name:, payload:, priority: } or nil.
12
12
  def self.release_next!(concurrency_key)
13
- now = Time.current
14
13
  result = connection.exec_query(
15
14
  <<~SQL,
16
15
  DELETE FROM pgbus_blocked_executions
17
16
  WHERE id = (
18
17
  SELECT id FROM pgbus_blocked_executions
19
18
  WHERE concurrency_key = $1
20
- AND expires_at >= $2
21
19
  ORDER BY priority ASC, created_at ASC
22
20
  LIMIT 1
23
21
  FOR UPDATE SKIP LOCKED
@@ -25,16 +23,44 @@ module Pgbus
25
23
  RETURNING queue_name, payload, priority
26
24
  SQL
27
25
  "Pgbus Blocked Release",
28
- [concurrency_key, now]
26
+ [concurrency_key]
29
27
  )
30
28
 
31
29
  row = result.first
32
30
  return nil unless row
33
31
 
34
32
  payload = row["payload"]
35
- payload = JSON.parse(payload) if payload.is_a?(String)
33
+ # exec_query returns jsonb as text; a row written before the
34
+ # double-encoding fix parses to a String holding the real document.
35
+ 2.times { payload = JSON.parse(payload) if payload.is_a?(String) }
36
36
 
37
37
  { queue_name: row["queue_name"], payload: payload, priority: row["priority"] }
38
38
  end
39
+
40
+ # Rewrite rows parked before the double-encoding fix (a jsonb string
41
+ # holding the document) as the document itself, so SQL readers such as
42
+ # the batch sweep's `payload->>'job_id'` see them. Idempotent; the sweep
43
+ # runs it before every promotion pass.
44
+ def self.repair_double_encoded!
45
+ where("jsonb_typeof(payload) = 'string'").update_all("payload = (payload #>> '{}')::jsonb")
46
+ end
47
+
48
+ # Keys with parked jobs that could take a slot right now — no semaphore
49
+ # row, or one with room — longest-waiting first.
50
+ #
51
+ # Filtering in SQL rather than skipping in Ruby is what keeps the scan's
52
+ # cap honest: a plain oldest-first list would fill with keys whose slots
53
+ # are held (their holders promote for themselves on completion anyway)
54
+ # and starve a key behind them whose holder died.
55
+ def self.promotable_keys(limit: 1000)
56
+ joins(<<~SQL)
57
+ LEFT JOIN pgbus_semaphores ON pgbus_semaphores.key = pgbus_blocked_executions.concurrency_key
58
+ SQL
59
+ .where("pgbus_semaphores.key IS NULL OR pgbus_semaphores.value < pgbus_semaphores.max_value")
60
+ .group(:concurrency_key)
61
+ .order(Arel.sql("MIN(pgbus_blocked_executions.created_at)"))
62
+ .limit(limit)
63
+ .pluck(:concurrency_key)
64
+ end
39
65
  end
40
66
  end
@@ -7,16 +7,21 @@ module Pgbus
7
7
  scope :expired, ->(now = Time.current) { where("expires_at < ? OR value <= 0", now) }
8
8
 
9
9
  # Atomic conditional UPSERT. Returns :acquired or :blocked.
10
+ #
11
+ # A nil max_value means "keep the limit this row already records" — used
12
+ # when promoting a parked job whose class no longer resolves, so the
13
+ # promotion is judged against the real limit rather than a guessed 1.
14
+ # On a fresh row there is no recorded limit, so nil falls back to 1.
10
15
  def self.acquire!(key, max_value, expires_at)
11
16
  result = connection.exec_query(
12
17
  <<~SQL,
13
18
  INSERT INTO pgbus_semaphores (key, value, max_value, expires_at)
14
- VALUES ($1, 1, $2, $3)
19
+ VALUES ($1, 1, COALESCE($2, 1), $3)
15
20
  ON CONFLICT (key) DO UPDATE
16
21
  SET value = pgbus_semaphores.value + 1,
17
- max_value = EXCLUDED.max_value,
22
+ max_value = COALESCE($2, pgbus_semaphores.max_value),
18
23
  expires_at = GREATEST(pgbus_semaphores.expires_at, EXCLUDED.expires_at)
19
- WHERE pgbus_semaphores.value < EXCLUDED.max_value
24
+ WHERE pgbus_semaphores.value < COALESCE($2, pgbus_semaphores.max_value)
20
25
  RETURNING value
21
26
  SQL
22
27
  "Pgbus Semaphore Acquire",
@@ -70,6 +70,42 @@ module Pgbus
70
70
  )
71
71
  end
72
72
 
73
+ # Release a lock only while it still points at this message. Used by the
74
+ # executor when it finds its message already archived by another worker:
75
+ # the :while_executing lock it took belongs to this attempt and has to go
76
+ # back, but an unconditional key-only DELETE could drop a successor that
77
+ # has since acquired the same key.
78
+ # PGMQ message ids are per-queue sequences, so a msg_id alone is not an
79
+ # identity: the same number addresses a different message on every other
80
+ # queue. The queue is part of the match.
81
+ def self.release_if_bound!(lock_key, queue_name:, msg_id:)
82
+ Thread.current[:pgbus_uniqueness_created_at]&.delete(lock_key)
83
+ connection.exec_delete(
84
+ "DELETE FROM #{table_name} WHERE lock_key = $1 AND queue_name = $2 AND msg_id = $3",
85
+ "UniquenessKey Release If Bound", [lock_key, queue_name.to_s, msg_id.to_i]
86
+ )
87
+ end
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
+
73
109
  # Check if a key is currently locked.
74
110
  def self.locked?(lock_key)
75
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-6 mb-8">
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>