pgbus 0.19.1 → 0.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +22 -28
- data/app/frontend/pgbus/application.js +4 -3
- data/app/frontend/pgbus/application.min.js +1 -1
- data/app/frontend/pgbus/application.min.js.map +1 -1
- data/app/frontend/pgbus/style.css +1 -1
- data/app/views/layouts/pgbus/application.html.erb +2 -2
- data/config/locales/en.yml +1 -1
- data/lib/generators/pgbus/add_batch_callback_jobs_generator.rb +4 -1
- data/lib/pgbus/active_job/batch_id.rb +10 -4
- data/lib/pgbus/batch/builder.rb +92 -0
- data/lib/pgbus/batch.rb +97 -22
- data/lib/pgbus/configuration/snapshot.rb +27 -0
- data/lib/pgbus/generators/migration_detector.rb +1 -1
- data/lib/pgbus/instrumentation.rb +11 -0
- data/lib/pgbus/integrations/appsignal/subscriber.rb +15 -0
- data/lib/pgbus/metrics/subscriber.rb +12 -0
- data/lib/pgbus/process/consumer.rb +56 -3
- data/lib/pgbus/shareable.rb +11 -1
- data/lib/pgbus/streams/key.rb +12 -3
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus.rb +42 -6
- data/sig/pgbus/configuration/snapshot.rbs +12 -0
- data/sig/pgbus.rbs +10 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd3bef8697d00eb6c47ce8c5d96c0c7780f8905015db0f3ffdfd963c23685c05
|
|
4
|
+
data.tar.gz: 62484196117af7568316b85c3e90cda2c36a1ecb0d65de651b586593abb27fa8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4319f21f8a8bf47371677468b81c4c99d44c0e00f59f62014d910a1912208cbd58d0410f6177d6d393f6ec0336a455e36351e4cd8ebb31d1dce242afca155b6a
|
|
7
|
+
data.tar.gz: b0eb5972c1a2a7d0a2e6a8606b8e58911df19aeb759b817d1b96beb576d5785dd0851ebc9457a60546d7cf65ee76a58aac13f342ef49b6d744361bc16d15dfdd
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
### Added
|
|
4
4
|
|
|
5
|
+
- **`Pgbus.stream_key` works inside a non-main Ractor.** `Pgbus.stream_key`, `Pgbus.stream_key!` and `Pgbus.stream_name_budget` (and `Streams::Key`) check the queue-name budget against the `queue_prefix`, which they read from `Pgbus.configuration`; a non-main Ractor cannot read that (it holds the logger and the app's callables), so they raised `Ractor::IsolationError` there. Each `Pgbus.finalize!` and finalizing `Pgbus.configure` (so also the engine's `after_initialize` and `pgbus start`) now publishes `Pgbus.configuration_snapshot`, a Ractor-shareable `Pgbus::Configuration::Snapshot` of the settings a non-main Ractor reads (the `queue_prefix` for now), and the stream keys read it there. On the main Ractor nothing changes: they read the live configuration, so a `queue_prefix` set through a setter still applies at once; a non-main Ractor sees it from the next finalize. Refs #527.
|
|
5
6
|
- **Deploy tasks: one-time work owed by a release, run once the fleet has rolled over.** Declare a backfill, data fix or cache warm in `config/deploy_tasks.yml` (the shape of `recurring.yml` minus `schedule:`, plus `version:` and `every_deploy:`) or `config.deploy_tasks`; the scheduler enqueues it once per `(key, version)` (or once per revision for an `every_deploy:` task) after every live pgbus process reports the new app revision and a worker on it is alive, so it never runs on old code during the overlap window and two schedulers never both fire it. It is not exactly-once: a failure between the enqueue and recording the run can repeat a task after a scheduler restart, and a scheduler killed between claiming and enqueueing loses that arm, so deploy tasks must be idempotent. Every pgbus process stamps the revision (`config.app_revision`, `PGBUS_REVISION`, `DASH_VERSION`, `KAMAL_VERSION` or the `REVISION` file) into its heartbeat. The dashboard gets a "Deploy tasks" page with the rollover status, Run now and Enable/Disable; run history lives in `pgbus_deploy_task_runs` (`deploy_task_run_retention`, default 90 days); `pgbus.deploy_task.enqueue` is instrumented and the MCP server gains `pgbus_deploy_tasks`. Set up with `rails generate pgbus:add_deploy_tasks` (`pgbus:update` detects the missing tables). Refs #536.
|
|
7
|
+
- **`pgbus.event_dead_lettered`: the event consumer says when it dead-letters an event.** A job that exhausted `max_retries` emitted `pgbus.job_dead_lettered`; an event moved to `<queue>_dlq` the same way emitted nothing, so a pager, an error tracker or a metrics counter had no push signal for dead events. `Process::Consumer` now emits `pgbus.event_dead_lettered` once per dead-lettered event, after `move_to_dead_letter` returns and the failed-event row is cleared, with `queue_name` (logical), `routing_key`, `handler` (the class that owns the queue in this process, or nil), `event_id`, `read_ct`, `msg_id` and `error` (the class of the last recorded failure, or nil). `handler` is nil unless exactly one subscriber owns the queue. A body that is not a JSON object (unparseable text, or a jsonb scalar or array written straight to the queue) is still dead-lettered, with `routing_key`, `handler` and `event_id` nil, and its `error` is the consumer's own failure to read it (`JSON::ParserError`, `NoMethodError` or `TypeError`): no handler ran. A move that raises emits nothing. A subscriber that raises is logged at debug level and does not change the outcome: the message stays dead-lettered, with one `dead_lettered` stat, no `failed` stat and no circuit-breaker failure. The AppSignal and metrics subscribers count it as `pgbus_event_count` with `status: "dead_lettered"`, tagged `handler` and `routing_key`. `pgbus.job_dead_lettered` and the consumer's success path are unchanged: the envelope is parsed a second time only on the dead-letter branch. Refs #543.
|
|
8
|
+
- **feat(batches): `Pgbus.batch do |b| … end`, a builder for starting a batch.** `b.on_complete`, `b.on_success` and `b.on_failure` take an ActiveJob class or a configured instance; `b.add(*jobs)` takes job instances (each keeps its own `.set(wait:, queue:, priority:)`) and sends them in one bulk enqueue. The batch row exists, and a `uniqueness_key` is held, before the block runs, and the batch stays pending until the block returns, so the calls can come in any order and a job that finishes while the block is still adding cannot finish the batch early. Every `perform_later` inside the block joins the batch, as inside `Batch#enqueue`. With `on_conflict: :discard` or `:log` and the key held, the block does not run. Inside a job, `batch(description: …) { |b| … }` opens a new, independent batch; `batch` without a block still reads the job's own batch. A member job adds siblings with `batch.add(NextJob.new(id))`, the open-batch form of `batch.enqueue { … }`. `Pgbus::Batch.start` is the same as `Pgbus.batch`, and `Pgbus::Batch.new` + `#enqueue` stay as the lower-level API. Refs #538.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **The batch completion callback is `on_complete`.** `Pgbus::Batch.new(on_complete:)`, the builder's `b.on_complete` and the `#on_complete` reader replace `on_finish`, which keeps working as a deprecated alias until 1.0 and logs one warning per process. The database columns stay `on_finish_class` / `on_finish_job` until the 1.0 schema cleanup, so batches created before upgrading still fire, and the `pgbus.batch_finished` instrumentation event keeps its name. The dashboard's batch page labels the callback "On Complete". Refs #538.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The dashboard's confirm and alert dialogs open centred again (issue #529).** The engine's stylesheet is built with Tailwind 4, whose preflight sets `margin: 0` on every element. A modal `<dialog>` keeps the browser's `inset: 0` but lost the `margin: auto` that centres it inside that inset, so every Pause, Purge and Delete confirmation opened in the top-left corner. Both dialogs now carry `m-auto`. The confirm dialog's Confirm button was white on yellow (1.91:1). It now uses the dark-on-yellow warning colours (`bg-yellow-400 text-yellow-950`), and the axe gate audits an open confirm dialog in light and dark mode. Refs #529.
|
|
6
17
|
|
|
7
18
|
## [0.19.0] - 2026-10-16
|
|
8
19
|
|
data/README.md
CHANGED
|
@@ -139,7 +139,7 @@ The capsule string DSL is the shortest form for the common case. Use `c.capsule`
|
|
|
139
139
|
|
|
140
140
|
Configuration is validated eagerly: `Pgbus.configure` runs `Configuration#validate!` right after your block yields, so an invalid value (`visibility_timeout = 0`, for example) raises `Pgbus::ConfigurationError` at boot instead of failing later inside a worker. Set `c.eager_validation = false` for the rare setup that intentionally holds a transiently-invalid config across sequential `configure` calls.
|
|
141
141
|
|
|
142
|
-
Configuration is finalized at boot: once the app's initializers have run, the engine calls `Pgbus.finalize!`, which freezes every plain-data setting (strings, symbols, numbers, regexps, and arrays and hashes of those) into a Ractor-shareable value. Callables and the logger are left as they are. Setters keep working, and a later `Pgbus.configure` finalizes again, but changing a collection in place after boot (`Pgbus.configuration.streams_durable_patterns << "x"`) raises `FrozenError`: assign a new value instead. `c.error_reporters << reporter` keeps working. Without Rails, each `Pgbus.configure` finalizes at its end; `pgbus start` finalizes again once its flags are applied. Call `Pgbus.finalize!` yourself only when you configure pgbus through setters alone. A value that cannot be made shareable (a lambda that closes over app state, a logger, a filter list holding a proc) never makes finalizing raise: it is kept as it is, for the main Ractor. More in [docs/concurrency.md](docs/concurrency.md).
|
|
142
|
+
Configuration is finalized at boot: once the app's initializers have run, the engine calls `Pgbus.finalize!`, which freezes every plain-data setting (strings, symbols, numbers, regexps, and arrays and hashes of those) into a Ractor-shareable value. Callables and the logger are left as they are. Setters keep working, and a later `Pgbus.configure` finalizes again, but changing a collection in place after boot (`Pgbus.configuration.streams_durable_patterns << "x"`) raises `FrozenError`: assign a new value instead. `c.error_reporters << reporter` keeps working. Without Rails, each `Pgbus.configure` finalizes at its end; `pgbus start` finalizes again once its flags are applied. Call `Pgbus.finalize!` yourself only when you configure pgbus through setters alone. A value that cannot be made shareable (a lambda that closes over app state, a logger, a filter list holding a proc) never makes finalizing raise: it is kept as it is, for the main Ractor. Each finalize also publishes `Pgbus.configuration_snapshot`, the shareable settings a non-main Ractor reads (so `Pgbus.stream_key` works there). More in [docs/concurrency.md](docs/concurrency.md).
|
|
143
143
|
|
|
144
144
|
> **Upgrading from an older pgbus?** Run `rails generate pgbus:update`. It inspects your live database and adds any missing pgbus migrations to `db/migrate` (or `db/pgbus_migrate` if you use `connects_to`). The generator detects your separate-database config automatically from `Pgbus.configuration.connects_to` or by scanning the initializer / `config/application.rb`, so you don't have to re-specify `--database=pgbus` every time.
|
|
145
145
|
>
|
|
@@ -801,28 +801,27 @@ How Pgbus integrates with your application's transactions and tracks groups of r
|
|
|
801
801
|
Coordinate groups of jobs with callbacks when all complete:
|
|
802
802
|
|
|
803
803
|
```ruby
|
|
804
|
-
batch = Pgbus
|
|
805
|
-
|
|
806
|
-
on_success
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
properties: { initiated_by: current_user.id }
|
|
810
|
-
)
|
|
811
|
-
|
|
812
|
-
batch.enqueue do
|
|
813
|
-
users.each { |user| ImportUserJob.perform_later(user.id) }
|
|
804
|
+
batch = Pgbus.batch(description: "Import users", properties: { initiated_by: current_user.id }) do |b|
|
|
805
|
+
b.on_complete BatchFinishedJob
|
|
806
|
+
b.on_success BatchSucceededJob
|
|
807
|
+
b.on_failure BatchFailedJob
|
|
808
|
+
b.add(*users.map { |user| ImportUserJob.new(user.id) })
|
|
814
809
|
end
|
|
815
810
|
```
|
|
816
811
|
|
|
812
|
+
`b.add` takes job instances (each keeps its own `.set(wait:, queue:)`) and sends them in one bulk enqueue. The batch row exists before the block runs and stays pending until it returns, so a job that finishes while the block is still adding cannot finish the batch early. Set callbacks first: if the block raises after an `add`, the added jobs still run and the batch finishes later with only the callbacks set by then. A block that goes `config.batch_stall_threshold` (default 5 minutes) without adding a job is started by the stalled-batch sweep, so keep slow work outside it. Callbacks are job classes or configured instances, never blocks: they are stored on the batch row.
|
|
813
|
+
|
|
814
|
+
Inside a job, `batch { |b| ... }` opens a new batch the same way, and a job that is already a member adds siblings with `batch.add(NextJob.new(id))`. `Pgbus::Batch.new(on_complete: ...)` plus `batch.enqueue { ... }` remains the lower-level form.
|
|
815
|
+
|
|
817
816
|
#### Callbacks
|
|
818
817
|
|
|
819
818
|
| Callback | Fired when |
|
|
820
819
|
|----------|------------|
|
|
821
|
-
| `
|
|
822
|
-
| `on_success` | All jobs completed successfully (zero
|
|
823
|
-
| `
|
|
820
|
+
| `on_complete` | All jobs completed (success or failure). `on_finish` is a deprecated alias until 1.0 |
|
|
821
|
+
| `on_success` | All jobs completed successfully (zero failed) |
|
|
822
|
+
| `on_failure` | At least one job was dead-lettered. `on_discard` is a deprecated alias until 1.0 |
|
|
824
823
|
|
|
825
|
-
Callback jobs receive the batch `properties` hash as their argument:
|
|
824
|
+
Callback jobs given as a bare class receive the batch `properties` hash as their argument:
|
|
826
825
|
|
|
827
826
|
```ruby
|
|
828
827
|
class BatchFinishedJob < ApplicationJob
|
|
@@ -838,23 +837,18 @@ end
|
|
|
838
837
|
`ensures_uniqueness` protects one job. A multi-job run — a nightly import that fans out into hundreds of chunk jobs, a pipeline whose jobs enqueue the next stage — has no single job that lives for the whole run, so a lock on the entry job releases the moment that job finishes and the next scheduler tick starts a second run on top of the first. Give the batch the lock instead:
|
|
839
838
|
|
|
840
839
|
```ruby
|
|
841
|
-
batch
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
on_success: ExportPricesJob
|
|
845
|
-
)
|
|
846
|
-
|
|
847
|
-
batch.enqueue do
|
|
848
|
-
ImportChunkJob.perform_later("products", 0)
|
|
840
|
+
Pgbus.batch(uniqueness_key: "perfecta-daily", on_conflict: :discard) do |b| # :reject (raise AlreadyRunning), :discard, :log
|
|
841
|
+
b.on_success ExportPricesJob
|
|
842
|
+
b.add(ImportChunkJob.new("products", 0))
|
|
849
843
|
end
|
|
850
844
|
```
|
|
851
845
|
|
|
852
|
-
The lock is taken at `enqueue` and held until the batch finishes — through every open-batch `batch.
|
|
846
|
+
The lock is taken at the top of the block (or at `enqueue`) and held until the batch finishes — through every open-batch `batch.add` from inside a running job — then released in the same single-winner finish that fires the callbacks, so completion, the stalled-batch sweep and cleanup all release it. While it is held, another `Pgbus.batch(uniqueness_key: "perfecta-daily") { ... }` raises `Pgbus::Batch::AlreadyRunning` (`:reject`), or skips its block and returns a batch that answers `discarded?` (`:discard`, `:log`). The row lives in `pgbus_uniqueness_keys` as `batch:<key>`, so it never collides with a job's key and shows up in the dashboard like any other lock; the reaper judges it by the batch's status, never by queue contents.
|
|
853
847
|
|
|
854
848
|
#### How batches work
|
|
855
849
|
|
|
856
|
-
1. `
|
|
857
|
-
2.
|
|
850
|
+
1. The top of the `Pgbus.batch` block (or the first `batch.enqueue`) creates a tracking row in `pgbus_batches` with `status: "pending"`
|
|
851
|
+
2. Each job enqueued in the block is tagged with the `pgbus_batch_id` in its payload
|
|
858
852
|
3. After each job completes or is dead-lettered, the executor atomically updates the batch counters
|
|
859
853
|
4. When `completed_jobs + discarded_jobs == total_jobs`, the batch status flips to `"finished"` and callback jobs are enqueued
|
|
860
854
|
5. The dispatcher cleans up finished batches older than 7 days
|
|
@@ -1017,7 +1011,7 @@ c.metrics_backend = MyOpenTelemetryBackend.new
|
|
|
1017
1011
|
| `pgbus_queue_job_count` | counter | `queue`, `job_class`, `status` (`processed`/`failed`/`dead_lettered`) |
|
|
1018
1012
|
| `pgbus_visibility_extended` | counter | `queue`, `job_class` — one per re-armed visibility timeout; a class listed here outlives `visibility_timeout` |
|
|
1019
1013
|
| `pgbus_job_duration_ms` | histogram | `queue`, `job_class` |
|
|
1020
|
-
| `pgbus_event_count` | counter | `handler`, `routing_key`, `status` |
|
|
1014
|
+
| `pgbus_event_count` | counter | `handler`, `routing_key`, `status` (`processed`/`failed`/`skipped`/`dead_lettered`), plus `reason` on `skipped` only (`completed`/`cached`/`owned`) |
|
|
1021
1015
|
| `pgbus_event_duration_ms` | histogram | `handler`, `routing_key` |
|
|
1022
1016
|
| `pgbus_messages_sent` / `pgbus_messages_read` | counter | `queue` |
|
|
1023
1017
|
| `pgbus_stream_broadcast_count` | counter | `stream`, `deferred` |
|
|
@@ -1052,7 +1046,7 @@ ActiveSupport::Notifications.subscribe(/^pgbus\./) do |name, start, finish, _id,
|
|
|
1052
1046
|
end
|
|
1053
1047
|
```
|
|
1054
1048
|
|
|
1055
|
-
Metrics-relevant events emitted (the subset the metrics subscriber maps; see `lib/pgbus/instrumentation.rb` for the full `pgbus.*` catalog): `pgbus.executor.execute`, `pgbus.job_completed`, `pgbus.job_failed`, `pgbus.job_dead_lettered`, `pgbus.event_processed`, `pgbus.event_failed`, `pgbus.client.send_message`, `pgbus.client.send_batch`, `pgbus.client.read_batch`, `pgbus.client.pool`, `pgbus.stream.broadcast`, `pgbus.outbox.publish`, `pgbus.recurring.enqueue`, `pgbus.worker.recycle`, `pgbus.consumer.recycle`. Payload keys are documented in `lib/pgbus/instrumentation.rb`.
|
|
1049
|
+
Metrics-relevant events emitted (the subset the metrics subscriber maps; see `lib/pgbus/instrumentation.rb` for the full `pgbus.*` catalog): `pgbus.executor.execute`, `pgbus.job_completed`, `pgbus.job_failed`, `pgbus.job_dead_lettered`, `pgbus.event_processed`, `pgbus.event_failed`, `pgbus.event_dead_lettered`, `pgbus.event_skipped`, `pgbus.job_visibility_extended`, `pgbus.client.send_message`, `pgbus.client.send_batch`, `pgbus.client.read_batch`, `pgbus.client.pool`, `pgbus.stream.broadcast`, `pgbus.outbox.publish`, `pgbus.recurring.enqueue`, `pgbus.worker.recycle`, `pgbus.consumer.recycle`. Payload keys are documented in `lib/pgbus/instrumentation.rb`.
|
|
1056
1050
|
|
|
1057
1051
|
### Connection pool metrics
|
|
1058
1052
|
|
|
@@ -17,13 +17,14 @@ Turbo.config.forms.confirm = (message, element) => {
|
|
|
17
17
|
: (i18n?.dataset.confirmTitle || "Are you sure?");
|
|
18
18
|
messageEl.textContent = message;
|
|
19
19
|
|
|
20
|
-
confirmBtn.className = "rounded-md px-4 py-2 text-sm font-medium
|
|
20
|
+
confirmBtn.className = "rounded-md px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2";
|
|
21
21
|
if (isDelete) {
|
|
22
|
-
confirmBtn.classList.add("bg-red-600", "hover:bg-red-500", "focus:ring-red-500");
|
|
22
|
+
confirmBtn.classList.add("bg-red-600", "text-white", "hover:bg-red-500", "focus:ring-red-500");
|
|
23
23
|
confirmBtn.textContent = i18n?.dataset.deleteLabel || "Delete";
|
|
24
24
|
iconEl.className = "flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30";
|
|
25
25
|
} else {
|
|
26
|
-
|
|
26
|
+
// White on yellow fails contrast; yellow buttons carry dark text (ButtonHelper's warning variant).
|
|
27
|
+
confirmBtn.classList.add("bg-yellow-400", "text-yellow-950", "hover:bg-yellow-300", "focus:ring-yellow-500");
|
|
27
28
|
confirmBtn.textContent = i18n?.dataset.confirmLabel || "Confirm";
|
|
28
29
|
iconEl.className = "flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30";
|
|
29
30
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as m from"turbo";m.config.forms.confirm=(e,
|
|
1
|
+
import*as m from"turbo";m.config.forms.confirm=(e,r)=>{let a=document.getElementById("pgbus-confirm-dialog"),n=document.getElementById("pgbus-confirm-message"),d=document.getElementById("pgbus-confirm-title"),t=document.getElementById("pgbus-confirm-btn"),c=document.getElementById("pgbus-confirm-icon"),o=r.getAttribute("data-turbo-method")==="delete",s=document.getElementById("pgbus-i18n");if(d.textContent=o?s?.dataset.deleteTitle||"Delete":s?.dataset.confirmTitle||"Are you sure?",n.textContent=e,t.className="rounded-md px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2",o)t.classList.add("bg-red-600","text-white","hover:bg-red-500","focus:ring-red-500"),t.textContent=s?.dataset.deleteLabel||"Delete",c.className="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30";else t.classList.add("bg-yellow-400","text-yellow-950","hover:bg-yellow-300","focus:ring-yellow-500"),t.textContent=s?.dataset.confirmLabel||"Confirm",c.className="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30";return a.showModal(),new Promise((g)=>{a.addEventListener("close",()=>{g(a.returnValue==="confirm")},{once:!0})})};function f(e,r="success"){let a=document.getElementById("pgbus-toast-container");if(!a)return;let n=document.createElement("div"),d={success:"bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800",error:"bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800"};n.className=`rounded-md border p-3 text-sm shadow-lg transition-all duration-300 ${d[r]||d.info}`,n.textContent=e,a.appendChild(n),setTimeout(()=>{n.style.opacity="0",n.style.transform="translateX(100%)",setTimeout(()=>n.remove(),300)},5000)}function b(){document.querySelectorAll("template[data-pgbus-toast]").forEach((e)=>{f(e.content.textContent.trim(),e.dataset.pgbusToast),e.remove()})}b();document.addEventListener("turbo:load",b);function u(){document.querySelectorAll("[data-bulk-select-all]").forEach((e)=>{if(e.dataset.bulkInitialized==="true")return;e.dataset.bulkInitialized="true";let r=e.closest("[data-bulk-scope]")||document,a=()=>r.querySelectorAll("input[data-bulk-item]"),n=r.closest("[data-bulk-page]")||r.closest("turbo-frame")||r.parentElement?.closest("div")||document,d=n.querySelector("[data-bulk-count]"),t=n.querySelector("[data-bulk-actions]");function c(){let o=r.querySelectorAll("input[data-bulk-item]:checked");if(d)d.textContent=o.length;if(t)t.classList.toggle("hidden",o.length===0);let s=a();e.checked=s.length>0&&o.length===s.length,e.indeterminate=o.length>0&&o.length<s.length}let l=!1;e.addEventListener("change",()=>{l=!0,a().forEach((o)=>{o.checked=e.checked,o.dispatchEvent(new Event("change",{bubbles:!1}))}),l=!1,c()}),r.addEventListener("change",(o)=>{if(o.target.matches("input[data-bulk-item]"))c()})})}u();document.addEventListener("turbo:load",u);document.addEventListener("turbo:frame-load",u);var i=parseInt(document.body?.dataset.pgbusRefreshInterval||"0",10);if(i>0){let r=function(){if(document.querySelector("input[data-bulk-item]:checked, input[data-bulk-select-all]:checked"))return!0;if(document.querySelector("turbo-frame[data-auto-refresh] details[open]"))return!0;return!1},a=function(){if(document.hidden)return;if(r())return;document.querySelectorAll("turbo-frame[data-auto-refresh]").forEach((t)=>{try{if(!t.src&&t.dataset.src)t.src=t.dataset.src;if(t.src)t.reload()}catch(c){}})},n=function(){e=setInterval(a,i)},d=function(){clearInterval(e)},e;document.addEventListener("visibilitychange",()=>document.hidden?d():n()),n()}
|
|
2
2
|
//# sourceMappingURL=application.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["src/application.js"],"sourcesContent":["import * as Turbo from \"turbo\";\n\n// -- Custom confirm dialog (replaces browser confirm) --\nTurbo.config.forms.confirm = (message, element) => {\n const dialog = document.getElementById(\"pgbus-confirm-dialog\");\n const messageEl = document.getElementById(\"pgbus-confirm-message\");\n const titleEl = document.getElementById(\"pgbus-confirm-title\");\n const confirmBtn = document.getElementById(\"pgbus-confirm-btn\");\n const iconEl = document.getElementById(\"pgbus-confirm-icon\");\n\n const turboMethod = element.getAttribute(\"data-turbo-method\");\n const isDelete = turboMethod === \"delete\";\n\n const i18n = document.getElementById(\"pgbus-i18n\");\n titleEl.textContent = isDelete\n ? (i18n?.dataset.deleteTitle || \"Delete\")\n : (i18n?.dataset.confirmTitle || \"Are you sure?\");\n messageEl.textContent = message;\n\n confirmBtn.className = \"rounded-md px-4 py-2 text-sm font-medium
|
|
1
|
+
{"version":3,"sources":["src/application.js"],"sourcesContent":["import * as Turbo from \"turbo\";\n\n// -- Custom confirm dialog (replaces browser confirm) --\nTurbo.config.forms.confirm = (message, element) => {\n const dialog = document.getElementById(\"pgbus-confirm-dialog\");\n const messageEl = document.getElementById(\"pgbus-confirm-message\");\n const titleEl = document.getElementById(\"pgbus-confirm-title\");\n const confirmBtn = document.getElementById(\"pgbus-confirm-btn\");\n const iconEl = document.getElementById(\"pgbus-confirm-icon\");\n\n const turboMethod = element.getAttribute(\"data-turbo-method\");\n const isDelete = turboMethod === \"delete\";\n\n const i18n = document.getElementById(\"pgbus-i18n\");\n titleEl.textContent = isDelete\n ? (i18n?.dataset.deleteTitle || \"Delete\")\n : (i18n?.dataset.confirmTitle || \"Are you sure?\");\n messageEl.textContent = message;\n\n confirmBtn.className = \"rounded-md px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2\";\n if (isDelete) {\n confirmBtn.classList.add(\"bg-red-600\", \"text-white\", \"hover:bg-red-500\", \"focus:ring-red-500\");\n confirmBtn.textContent = i18n?.dataset.deleteLabel || \"Delete\";\n iconEl.className = \"flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30\";\n } else {\n // White on yellow fails contrast; yellow buttons carry dark text (ButtonHelper's warning variant).\n confirmBtn.classList.add(\"bg-yellow-400\", \"text-yellow-950\", \"hover:bg-yellow-300\", \"focus:ring-yellow-500\");\n confirmBtn.textContent = i18n?.dataset.confirmLabel || \"Confirm\";\n iconEl.className = \"flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30\";\n }\n\n dialog.showModal();\n\n return new Promise((resolve) => {\n dialog.addEventListener(\"close\", () => {\n resolve(dialog.returnValue === \"confirm\");\n }, { once: true });\n });\n};\n\n// -- Toast notifications --\nfunction showToast(message, type = \"success\") {\n const container = document.getElementById(\"pgbus-toast-container\");\n if (!container) return;\n const toast = document.createElement(\"div\");\n\n const colors = {\n success: \"bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800\",\n error: \"bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-800\",\n info: \"bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800\",\n };\n\n toast.className = `rounded-md border p-3 text-sm shadow-lg transition-all duration-300 ${colors[type] || colors.info}`;\n toast.textContent = message;\n container.appendChild(toast);\n\n setTimeout(() => {\n toast.style.opacity = \"0\";\n toast.style.transform = \"translateX(100%)\";\n setTimeout(() => toast.remove(), 300);\n }, 5000);\n}\n\nfunction renderFlashToasts() {\n document.querySelectorAll(\"template[data-pgbus-toast]\").forEach(tpl => {\n showToast(tpl.content.textContent.trim(), tpl.dataset.pgbusToast);\n tpl.remove();\n });\n}\nrenderFlashToasts();\ndocument.addEventListener(\"turbo:load\", renderFlashToasts);\n\n// -- Bulk checkbox selection --\nfunction initBulkSelect() {\n document.querySelectorAll(\"[data-bulk-select-all]\").forEach(selectAll => {\n if (selectAll.dataset.bulkInitialized === \"true\") return;\n selectAll.dataset.bulkInitialized = \"true\";\n\n const scope = selectAll.closest(\"[data-bulk-scope]\") || document;\n const checkboxes = () => scope.querySelectorAll(\"input[data-bulk-item]\");\n // Look for bulk-actions in the parent page container (outside the table scope)\n const pageScope = scope.closest(\"[data-bulk-page]\") || scope.closest(\"turbo-frame\") || scope.parentElement?.closest(\"div\") || document;\n const countEl = pageScope.querySelector(\"[data-bulk-count]\");\n const actions = pageScope.querySelector(\"[data-bulk-actions]\");\n\n function updateUI() {\n const checked = scope.querySelectorAll(\"input[data-bulk-item]:checked\");\n if (countEl) countEl.textContent = checked.length;\n if (actions) actions.classList.toggle(\"hidden\", checked.length === 0);\n\n const all = checkboxes();\n selectAll.checked = all.length > 0 && checked.length === all.length;\n selectAll.indeterminate = checked.length > 0 && checked.length < all.length;\n }\n\n let inSelectAll = false;\n selectAll.addEventListener(\"change\", () => {\n inSelectAll = true;\n checkboxes().forEach(cb => {\n cb.checked = selectAll.checked;\n // Dispatch change event so inline onchange handlers fire (e.g., hidden input sync)\n cb.dispatchEvent(new Event(\"change\", { bubbles: false }));\n });\n inSelectAll = false;\n updateUI();\n });\n\n scope.addEventListener(\"change\", (e) => {\n if (e.target.matches(\"input[data-bulk-item]\")) updateUI();\n });\n });\n\n}\ninitBulkSelect();\ndocument.addEventListener(\"turbo:load\", initBulkSelect);\ndocument.addEventListener(\"turbo:frame-load\", initBulkSelect);\n\n// -- Auto-refresh (pauses when user is interacting) --\nconst refreshInterval = parseInt(document.body?.dataset.pgbusRefreshInterval || \"0\", 10);\nif (refreshInterval > 0) {\n let timer;\n\n function hasUserInteraction() {\n // Pause when any checkbox is checked\n const checked = document.querySelector(\"input[data-bulk-item]:checked, input[data-bulk-select-all]:checked\");\n if (checked) return true;\n // Pause when any job detail row is expanded\n const openDetails = document.querySelector(\"turbo-frame[data-auto-refresh] details[open]\");\n if (openDetails) return true;\n return false;\n }\n\n function refreshFrames() {\n if (document.hidden) return;\n if (hasUserInteraction()) return;\n document.querySelectorAll(\"turbo-frame[data-auto-refresh]\")\n .forEach(frame => {\n try {\n if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;\n if (frame.src) frame.reload();\n } catch (_) { /* Turbo may abort in-flight fetches during navigation */ }\n });\n }\n function start() { timer = setInterval(refreshFrames, refreshInterval); }\n function stop() { clearInterval(timer); }\n document.addEventListener(\"visibilitychange\", () => document.hidden ? stop() : start());\n start();\n}\n"],"mappings":"AAAA,wBAGM,SAAO,MAAM,QAAU,CAAC,EAAS,IAAY,CACjD,IAAM,EAAS,SAAS,eAAe,sBAAsB,EACvD,EAAY,SAAS,eAAe,uBAAuB,EAC3D,EAAU,SAAS,eAAe,qBAAqB,EACvD,EAAa,SAAS,eAAe,mBAAmB,EACxD,EAAS,SAAS,eAAe,oBAAoB,EAGrD,EADc,EAAQ,aAAa,mBAAmB,IAC3B,SAE3B,EAAO,SAAS,eAAe,YAAY,EAOjD,GANA,EAAQ,YAAc,EACjB,GAAM,QAAQ,aAAe,SAC7B,GAAM,QAAQ,cAAgB,gBACnC,EAAU,YAAc,EAExB,EAAW,UAAY,2EACnB,EACF,EAAW,UAAU,IAAI,aAAc,aAAc,mBAAoB,oBAAoB,EAC7F,EAAW,YAAc,GAAM,QAAQ,aAAe,SACtD,EAAO,UAAY,sGAGnB,OAAW,UAAU,IAAI,gBAAiB,kBAAmB,sBAAuB,uBAAuB,EAC3G,EAAW,YAAc,GAAM,QAAQ,cAAgB,UACvD,EAAO,UAAY,4GAKrB,OAFA,EAAO,UAAU,EAEV,IAAI,QAAQ,CAAC,IAAY,CAC9B,EAAO,iBAAiB,QAAS,IAAM,CACrC,EAAQ,EAAO,cAAgB,SAAS,GACvC,CAAE,KAAM,EAAK,CAAC,EAClB,GAIH,SAAS,CAAS,CAAC,EAAS,EAAO,UAAW,CAC5C,IAAM,EAAY,SAAS,eAAe,uBAAuB,EACjE,GAAI,CAAC,EAAW,OAChB,IAAM,EAAQ,SAAS,cAAc,KAAK,EAEpC,EAAS,CACb,QAAS,6GACT,MAAO,iGACP,KAAM,sGACR,EAEA,EAAM,UAAY,uEAAuE,EAAO,IAAS,EAAO,OAChH,EAAM,YAAc,EACpB,EAAU,YAAY,CAAK,EAE3B,WAAW,IAAM,CACf,EAAM,MAAM,QAAU,IACtB,EAAM,MAAM,UAAY,mBACxB,WAAW,IAAM,EAAM,OAAO,EAAG,GAAG,GACnC,IAAI,EAGT,SAAS,CAAiB,EAAG,CAC3B,SAAS,iBAAiB,4BAA4B,EAAE,QAAQ,KAAO,CACrE,EAAU,EAAI,QAAQ,YAAY,KAAK,EAAG,EAAI,QAAQ,UAAU,EAChE,EAAI,OAAO,EACZ,EAEH,EAAkB,EAClB,SAAS,iBAAiB,aAAc,CAAiB,EAGzD,SAAS,CAAc,EAAG,CACxB,SAAS,iBAAiB,wBAAwB,EAAE,QAAQ,KAAa,CACvE,GAAI,EAAU,QAAQ,kBAAoB,OAAQ,OAClD,EAAU,QAAQ,gBAAkB,OAEpC,IAAM,EAAQ,EAAU,QAAQ,mBAAmB,GAAK,SAClD,EAAa,IAAM,EAAM,iBAAiB,uBAAuB,EAEjE,EAAY,EAAM,QAAQ,kBAAkB,GAAK,EAAM,QAAQ,aAAa,GAAK,EAAM,eAAe,QAAQ,KAAK,GAAK,SACxH,EAAU,EAAU,cAAc,mBAAmB,EACrD,EAAU,EAAU,cAAc,qBAAqB,EAE7D,SAAS,CAAQ,EAAG,CAClB,IAAM,EAAU,EAAM,iBAAiB,+BAA+B,EACtE,GAAI,EAAS,EAAQ,YAAc,EAAQ,OAC3C,GAAI,EAAS,EAAQ,UAAU,OAAO,SAAU,EAAQ,SAAW,CAAC,EAEpE,IAAM,EAAM,EAAW,EACvB,EAAU,QAAU,EAAI,OAAS,GAAK,EAAQ,SAAW,EAAI,OAC7D,EAAU,cAAgB,EAAQ,OAAS,GAAK,EAAQ,OAAS,EAAI,OAGvE,IAAI,EAAc,GAClB,EAAU,iBAAiB,SAAU,IAAM,CACzC,EAAc,GACd,EAAW,EAAE,QAAQ,KAAM,CACzB,EAAG,QAAU,EAAU,QAEvB,EAAG,cAAc,IAAI,MAAM,SAAU,CAAE,QAAS,EAAM,CAAC,CAAC,EACzD,EACD,EAAc,GACd,EAAS,EACV,EAED,EAAM,iBAAiB,SAAU,CAAC,IAAM,CACtC,GAAI,EAAE,OAAO,QAAQ,uBAAuB,EAAG,EAAS,EACzD,EACF,EAGH,EAAe,EACf,SAAS,iBAAiB,aAAc,CAAc,EACtD,SAAS,iBAAiB,mBAAoB,CAAc,EAG5D,IAAM,EAAkB,SAAS,SAAS,MAAM,QAAQ,sBAAwB,IAAK,EAAE,EACvF,GAAI,EAAkB,EAAG,CAGvB,IAAS,EAAT,QAA2B,EAAG,CAG5B,GADgB,SAAS,cAAc,oEAAoE,EAC9F,MAAO,GAGpB,GADoB,SAAS,cAAc,8CAA8C,EACxE,MAAO,GACxB,MAAO,IAGA,EAAT,QAAsB,EAAG,CACvB,GAAI,SAAS,OAAQ,OACrB,GAAI,EAAmB,EAAG,OAC1B,SAAS,iBAAiB,gCAAgC,EACvD,QAAQ,KAAS,CAChB,GAAI,CACF,GAAI,CAAC,EAAM,KAAO,EAAM,QAAQ,IAAK,EAAM,IAAM,EAAM,QAAQ,IAC/D,GAAI,EAAM,IAAK,EAAM,OAAO,EAC5B,MAAO,EAAG,GACb,GAEI,EAAT,QAAc,EAAG,CAAE,EAAQ,YAAY,EAAe,CAAe,GAC5D,EAAT,QAAa,EAAG,CAAE,cAAc,CAAK,GAxBjC,EAyBJ,SAAS,iBAAiB,mBAAoB,IAAM,SAAS,OAAS,EAAK,EAAI,EAAM,CAAC,EACtF,EAAM","names":[]}
|
|
@@ -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-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--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-300:oklch(87.9% .169 91.605);--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-amber-700:oklch(55.5% .163 48.998);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--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-yellow-950:oklch(28.6% .066 53.813);--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-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-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--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-purple-100:oklch(94.6% .033 307.174);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--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}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.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)}.ml-2{margin-left:calc(var(--spacing) * 2)}.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-6{min-height:calc(var(--spacing) * 6)}.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-24{width:calc(var(--spacing) * 24)}.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{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-disc{list-style-type:disc}.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}.flex-wrap{flex-wrap:wrap}.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}.justify-start{justify-content:flex-start}.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-auto{overflow:auto}.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-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.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-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-600{background-color:var(--color-gray-600)}.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-700{background-color:var(--color-green-700)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-purple-100{background-color:var(--color-purple-100)}.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-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.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)}.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)}.py-24{padding-block:calc(var(--spacing) * 24)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.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-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.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-800{color:var(--color-gray-800)}.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-indigo-800{color:var(--color-indigo-800)}.text-orange-600{color:var(--color-orange-600)}.text-purple-800{color:var(--color-purple-800)}.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-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.text-yellow-950{color:var(--color-yellow-950)}.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)}.ring-green-200{--tw-ring-color:var(--color-green-200)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.ring-yellow-200{--tw-ring-color:var(--color-yellow-200)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.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-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-800:hover{background-color:var(--color-green-800)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-red-900:hover{background-color:var(--color-red-900)}.hover\:bg-yellow-300:hover{background-color:var(--color-yellow-300)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-900:hover{color:var(--color-green-900)}.hover\:text-indigo-800:hover{color:var(--color-indigo-800)}.hover\:text-indigo-900:hover{color:var(--color-indigo-900)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-white:hover{color:var(--color-white)}.hover\:text-yellow-900:hover{color:var(--color-yellow-900)}.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}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-indigo-600:focus-visible{outline-color:var(--color-indigo-600)}@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\:grid-cols-\[auto_1fr\]{grid-template-columns:auto 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\:gap-x-6{column-gap:calc(var(--spacing) * 6)}.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)}.lg\:pl-10{padding-left:calc(var(--spacing) * 10)}.lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:96rem){.\32 xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}.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\/40:where(.dark,.dark *){background-color:#10182866}@supports (color:color-mix(in lab, red, red)){.dark\:bg-gray-900\/40:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-gray-900) 40%, transparent)}}.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-purple-900\/30:where(.dark,.dark *){background-color:#59168b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-purple-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-300:where(.dark,.dark *){color:var(--color-amber-300)}.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-200:where(.dark,.dark *){color:var(--color-gray-200)}.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-green-200:where(.dark,.dark *){color:var(--color-green-200)}.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-purple-400:where(.dark,.dark *){color:var(--color-purple-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-200:where(.dark,.dark *){color:var(--color-yellow-200)}.dark\:text-yellow-300:where(.dark,.dark *){color:var(--color-yellow-300)}.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-green-800:where(.dark,.dark *){--tw-ring-color:var(--color-green-800)}.dark\:ring-red-800:where(.dark,.dark *){--tw-ring-color:var(--color-red-800)}.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)}}.dark\:ring-yellow-800:where(.dark,.dark *){--tw-ring-color:var(--color-yellow-800)}@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\:bg-gray-800:where(.dark,.dark *):hover{background-color:var(--color-gray-800)}.dark\:hover\:text-gray-200:where(.dark,.dark *):hover{color:var(--color-gray-200)}.dark\:hover\:text-green-200:where(.dark,.dark *):hover{color:var(--color-green-200)}.dark\:hover\:text-indigo-200:where(.dark,.dark *):hover{color:var(--color-indigo-200)}.dark\:hover\:text-indigo-300:where(.dark,.dark *):hover{color:var(--color-indigo-300)}.dark\:hover\:text-red-200:where(.dark,.dark *):hover{color:var(--color-red-200)}.dark\:hover\:text-white:where(.dark,.dark *):hover{color:var(--color-white)}.dark\:hover\:text-yellow-200:where(.dark,.dark *):hover{color:var(--color-yellow-200)}.dark\:hover\:ring-gray-600:where(.dark,.dark *):hover{--tw-ring-color:var(--color-gray-600)}}.dark\:focus-visible\:outline-indigo-400:where(.dark,.dark *):focus-visible{outline-color:var(--color-indigo-400)}}.pgbus-table tbody tr.pgbus-job-detail{display:none}.pgbus-table tbody tr:has(details[data-job-toggle][open])+tr.pgbus-job-detail{display:table-row}@media (max-width:1023px){.pgbus-table tbody tr:has(details[data-job-toggle][open])+tr.pgbus-job-detail{display:block}.pgbus-table tbody tr.pgbus-job-detail>td{text-align:left}}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@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-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--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-300:oklch(87.9% .169 91.605);--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-amber-700:oklch(55.5% .163 48.998);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--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-yellow-950:oklch(28.6% .066 53.813);--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-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-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--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-purple-100:oklch(94.6% .033 307.174);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--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}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.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}}.m-auto{margin:auto}.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)}.ml-2{margin-left:calc(var(--spacing) * 2)}.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-6{min-height:calc(var(--spacing) * 6)}.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-24{width:calc(var(--spacing) * 24)}.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{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-disc{list-style-type:disc}.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}.flex-wrap{flex-wrap:wrap}.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}.justify-start{justify-content:flex-start}.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-auto{overflow:auto}.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-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.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-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-600{background-color:var(--color-gray-600)}.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-700{background-color:var(--color-green-700)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-purple-100{background-color:var(--color-purple-100)}.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-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-400{background-color:var(--color-yellow-400)}.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)}.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)}.py-24{padding-block:calc(var(--spacing) * 24)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.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-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.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-800{color:var(--color-gray-800)}.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-indigo-800{color:var(--color-indigo-800)}.text-orange-600{color:var(--color-orange-600)}.text-purple-800{color:var(--color-purple-800)}.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-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.text-yellow-950{color:var(--color-yellow-950)}.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)}.ring-green-200{--tw-ring-color:var(--color-green-200)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.ring-yellow-200{--tw-ring-color:var(--color-yellow-200)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.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-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-800:hover{background-color:var(--color-green-800)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-red-900:hover{background-color:var(--color-red-900)}.hover\:bg-yellow-300:hover{background-color:var(--color-yellow-300)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-900:hover{color:var(--color-green-900)}.hover\:text-indigo-800:hover{color:var(--color-indigo-800)}.hover\:text-indigo-900:hover{color:var(--color-indigo-900)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-white:hover{color:var(--color-white)}.hover\:text-yellow-900:hover{color:var(--color-yellow-900)}.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}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-indigo-600:focus-visible{outline-color:var(--color-indigo-600)}@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\:grid-cols-\[auto_1fr\]{grid-template-columns:auto 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\:gap-x-6{column-gap:calc(var(--spacing) * 6)}.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)}.lg\:pl-10{padding-left:calc(var(--spacing) * 10)}.lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:96rem){.\32 xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}.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\/40:where(.dark,.dark *){background-color:#10182866}@supports (color:color-mix(in lab, red, red)){.dark\:bg-gray-900\/40:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-gray-900) 40%, transparent)}}.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-purple-900\/30:where(.dark,.dark *){background-color:#59168b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/30:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-purple-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-300:where(.dark,.dark *){color:var(--color-amber-300)}.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-200:where(.dark,.dark *){color:var(--color-gray-200)}.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-green-200:where(.dark,.dark *){color:var(--color-green-200)}.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-purple-400:where(.dark,.dark *){color:var(--color-purple-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-200:where(.dark,.dark *){color:var(--color-yellow-200)}.dark\:text-yellow-300:where(.dark,.dark *){color:var(--color-yellow-300)}.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-green-800:where(.dark,.dark *){--tw-ring-color:var(--color-green-800)}.dark\:ring-red-800:where(.dark,.dark *){--tw-ring-color:var(--color-red-800)}.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)}}.dark\:ring-yellow-800:where(.dark,.dark *){--tw-ring-color:var(--color-yellow-800)}@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\:bg-gray-800:where(.dark,.dark *):hover{background-color:var(--color-gray-800)}.dark\:hover\:text-gray-200:where(.dark,.dark *):hover{color:var(--color-gray-200)}.dark\:hover\:text-green-200:where(.dark,.dark *):hover{color:var(--color-green-200)}.dark\:hover\:text-indigo-200:where(.dark,.dark *):hover{color:var(--color-indigo-200)}.dark\:hover\:text-indigo-300:where(.dark,.dark *):hover{color:var(--color-indigo-300)}.dark\:hover\:text-red-200:where(.dark,.dark *):hover{color:var(--color-red-200)}.dark\:hover\:text-white:where(.dark,.dark *):hover{color:var(--color-white)}.dark\:hover\:text-yellow-200:where(.dark,.dark *):hover{color:var(--color-yellow-200)}.dark\:hover\:ring-gray-600:where(.dark,.dark *):hover{--tw-ring-color:var(--color-gray-600)}}.dark\:focus-visible\:outline-indigo-400:where(.dark,.dark *):focus-visible{outline-color:var(--color-indigo-400)}}.pgbus-table tbody tr.pgbus-job-detail{display:none}.pgbus-table tbody tr:has(details[data-job-toggle][open])+tr.pgbus-job-detail{display:table-row}@media (max-width:1023px){.pgbus-table tbody tr:has(details[data-job-toggle][open])+tr.pgbus-job-detail{display:block}.pgbus-table tbody tr.pgbus-job-detail>td{text-align:left}}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
<% end %>
|
|
212
212
|
|
|
213
213
|
<!-- Confirm dialog -->
|
|
214
|
-
<dialog id="pgbus-confirm-dialog" class="rounded-lg shadow-xl bg-white dark:bg-gray-800 p-0 backdrop:bg-gray-900/50 max-w-md w-full">
|
|
214
|
+
<dialog id="pgbus-confirm-dialog" class="m-auto rounded-lg shadow-xl bg-white dark:bg-gray-800 p-0 backdrop:bg-gray-900/50 max-w-md w-full">
|
|
215
215
|
<div class="p-6">
|
|
216
216
|
<div class="flex items-start space-x-4">
|
|
217
217
|
<div id="pgbus-confirm-icon" class="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30">
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
</dialog>
|
|
233
233
|
|
|
234
234
|
<!-- Alert dialog -->
|
|
235
|
-
<dialog id="pgbus-alert-dialog" class="rounded-lg shadow-xl bg-white dark:bg-gray-800 p-0 backdrop:bg-gray-900/50 max-w-md w-full">
|
|
235
|
+
<dialog id="pgbus-alert-dialog" class="m-auto rounded-lg shadow-xl bg-white dark:bg-gray-800 p-0 backdrop:bg-gray-900/50 max-w-md w-full">
|
|
236
236
|
<div class="p-6">
|
|
237
237
|
<div class="flex items-start space-x-4">
|
|
238
238
|
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-blue-100 dark:bg-blue-900/30">
|
data/config/locales/en.yml
CHANGED
|
@@ -33,7 +33,10 @@ module Pgbus
|
|
|
33
33
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
34
34
|
say ""
|
|
35
35
|
say "You can now pass a configured ActiveJob instance to a batch:"
|
|
36
|
-
say " Pgbus
|
|
36
|
+
say " Pgbus.batch do |b|"
|
|
37
|
+
say " b.on_complete ReportJob.new.set(queue: :critical, wait: 5.minutes)"
|
|
38
|
+
say " b.add(*rows.map { |row| ImportRowJob.new(row.id) })"
|
|
39
|
+
say " end"
|
|
37
40
|
say ""
|
|
38
41
|
end
|
|
39
42
|
|
|
@@ -8,9 +8,9 @@ module Pgbus
|
|
|
8
8
|
#
|
|
9
9
|
# * +batch_id+ — the batch this job is a *member* of. Assigned by the
|
|
10
10
|
# executor from the payload's +pgbus_batch_id+ before +perform+, so a
|
|
11
|
-
# running job can call +batch.
|
|
11
|
+
# running job can call +batch.add+ to add siblings.
|
|
12
12
|
# * +callback_batch_id+ — the batch this job *reports on*. Set on
|
|
13
|
-
# +
|
|
13
|
+
# +on_complete+/+on_success+/+on_failure+ jobs at fire time. A callback is
|
|
14
14
|
# never a member of the batch it reports on, so its +batch_id+ is nil.
|
|
15
15
|
#
|
|
16
16
|
# Both round-trip through +serialize+/+deserialize+, and both are omitted
|
|
@@ -36,8 +36,14 @@ module Pgbus
|
|
|
36
36
|
self.callback_batch_id = job_data["callback_batch_id"]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
|
|
39
|
+
# Without a block: the batch this job reports on, or is a member of; nil
|
|
40
|
+
# when neither. With a block: open a new, independent batch through
|
|
41
|
+
# Pgbus.batch (this job's own membership is untouched).
|
|
42
|
+
#
|
|
43
|
+
# batch(description: "Import") { |b| b.add(*rows.map { |r| RowJob.new(r.id) }) }
|
|
44
|
+
def batch(**options, &block)
|
|
45
|
+
return Pgbus.batch(**options, &block) if block
|
|
46
|
+
raise ArgumentError, "batch(...) with options needs a block: batch(description: ...) { |b| ... }" if options.any?
|
|
41
47
|
return @batch if defined?(@batch)
|
|
42
48
|
|
|
43
49
|
id = callback_batch_id || batch_id
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class Batch
|
|
5
|
+
# What Pgbus.batch (and +batch { |b| ... }+ inside a job) yields: callback
|
|
6
|
+
# setters and +add+ on a batch that is open for the length of the block.
|
|
7
|
+
#
|
|
8
|
+
# Pgbus.batch(description: "Import #{source.id}") do |b|
|
|
9
|
+
# b.on_complete ImportDoneJob.new.set(queue: :critical)
|
|
10
|
+
# b.on_failure ImportFailedJob
|
|
11
|
+
# b.add(*source.rows.map { |row| ImportRowJob.new(row.id) })
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# The batch row exists (and a uniqueness key is held) before the block
|
|
15
|
+
# runs, and it stays pending until the block returns, so the order of the
|
|
16
|
+
# calls does not matter and a job that completes during the block cannot
|
|
17
|
+
# finish the batch early. Once the block has returned the builder is
|
|
18
|
+
# closed: add more jobs through +batch.add+ on the returned batch.
|
|
19
|
+
class Builder
|
|
20
|
+
CLOSED_MESSAGE = "the batch builder only works inside its Pgbus.batch block; " \
|
|
21
|
+
"add more jobs later with batch.add on the batch Pgbus.batch returned"
|
|
22
|
+
UNTAGGED_MESSAGE = "b.add ran on a thread or fiber outside its Pgbus.batch block (or inside another " \
|
|
23
|
+
"batch's block), where its jobs would not join the batch; call b.add from the block itself"
|
|
24
|
+
|
|
25
|
+
# Flatten +jobs+ and check that each one is an ActiveJob instance.
|
|
26
|
+
def self.job_instances(jobs)
|
|
27
|
+
jobs = jobs.flatten
|
|
28
|
+
invalid = jobs.grep_v(::ActiveJob::Base)
|
|
29
|
+
return jobs if invalid.empty?
|
|
30
|
+
|
|
31
|
+
raise ArgumentError,
|
|
32
|
+
"a batch takes job instances (MyJob.new(args), optionally .set(wait:, queue:)), got #{invalid.first.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(batch)
|
|
36
|
+
@batch = batch
|
|
37
|
+
@closed = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_complete(job = nil, &) = set_callback(:on_complete, job, &)
|
|
41
|
+
|
|
42
|
+
def on_success(job = nil, &) = set_callback(:on_success, job, &)
|
|
43
|
+
|
|
44
|
+
def on_failure(job = nil, &) = set_callback(:on_failure, job, &)
|
|
45
|
+
|
|
46
|
+
# Deprecated alias of #on_complete until 1.0.
|
|
47
|
+
def on_finish(job = nil, &)
|
|
48
|
+
Batch.warn_on_finish_deprecated
|
|
49
|
+
set_callback(:on_complete, job, &)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Enqueue job instances into the batch with one bulk send (and anything
|
|
53
|
+
# the block enqueues). Returns the jobs. Membership is the batch tag the
|
|
54
|
+
# block holds (Thread.current, so fiber-local), so an add from another
|
|
55
|
+
# thread or fiber, or from inside a nested batch's block, raises rather
|
|
56
|
+
# than send jobs that would not be counted.
|
|
57
|
+
def add(*jobs)
|
|
58
|
+
raise ArgumentError, CLOSED_MESSAGE if @closed
|
|
59
|
+
raise ArgumentError, UNTAGGED_MESSAGE unless Thread.current[:pgbus_batch_id] == @batch.batch_id
|
|
60
|
+
|
|
61
|
+
jobs = self.class.job_instances(jobs)
|
|
62
|
+
::ActiveJob.perform_all_later(jobs) if jobs.any?
|
|
63
|
+
yield if block_given?
|
|
64
|
+
jobs
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Called by Batch.start when the block returns or raises.
|
|
68
|
+
def close
|
|
69
|
+
@closed = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def set_callback(slot, job, &block)
|
|
75
|
+
raise ArgumentError, CLOSED_MESSAGE if @closed
|
|
76
|
+
|
|
77
|
+
unless block.nil? && callback_job?(job)
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"#{slot} takes an ActiveJob class or a configured instance (MyJob.new.set(queue: :critical)), " \
|
|
80
|
+
"not #{block ? "a block" : job.inspect}: callbacks are stored on the batch row"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@batch.send(:write_callback, slot, job)
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def callback_job?(job)
|
|
88
|
+
(job.is_a?(Class) && job < ::ActiveJob::Base) || job.is_a?(::ActiveJob::Base)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/pgbus/batch.rb
CHANGED
|
@@ -21,21 +21,57 @@ module Pgbus
|
|
|
21
21
|
LOCK_QUEUE_PREFIX = "batch:"
|
|
22
22
|
VALID_CONFLICTS = %i[reject discard log].freeze
|
|
23
23
|
|
|
24
|
+
# The callback slots, each mapped to the prefix of its pgbus_batches
|
|
25
|
+
# columns. on_complete keeps the on_finish_* columns until the 1.0 schema
|
|
26
|
+
# cleanup, so rows written before the rename still fire.
|
|
27
|
+
CALLBACK_COLUMNS = { on_complete: "on_finish", on_success: "on_success", on_failure: "on_failure" }.freeze
|
|
28
|
+
|
|
24
29
|
attr_reader :batch_id, :properties, :description,
|
|
25
|
-
:
|
|
30
|
+
:on_complete, :on_success, :on_failure,
|
|
26
31
|
:uniqueness_key, :on_conflict
|
|
27
32
|
|
|
33
|
+
# Deprecated alias of #on_complete until 1.0.
|
|
34
|
+
def on_finish = on_complete
|
|
35
|
+
|
|
28
36
|
def on_discard
|
|
29
37
|
on_failure
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# Open a batch, yield a Pgbus::Batch::Builder, and start the batch when
|
|
41
|
+
# the block returns. Every job the block enqueues (through +b.add+ or a
|
|
42
|
+
# plain +perform_later+) joins the batch, which stays pending, and so
|
|
43
|
+
# cannot finish, until the block is done. Keywords go to Batch.new.
|
|
44
|
+
# Membership is the block's own thread or fiber (the tag lives in
|
|
45
|
+
# Thread.current): b.add raises anywhere else, and a plain perform_later
|
|
46
|
+
# from a thread or fiber the block spawns does not join the batch.
|
|
47
|
+
#
|
|
48
|
+
# Pgbus::Batch.start(description: "Import") do |b|
|
|
49
|
+
# b.on_complete ImportDoneJob
|
|
50
|
+
# b.add(*rows.map { |row| ImportRowJob.new(row.id) })
|
|
51
|
+
# end
|
|
52
|
+
def self.start(**, &block)
|
|
53
|
+
raise ArgumentError, "Pgbus.batch needs a block: Pgbus.batch { |b| b.add(...) }" unless block
|
|
54
|
+
|
|
55
|
+
batch = new(**)
|
|
56
|
+
builder = Builder.new(batch)
|
|
57
|
+
begin
|
|
58
|
+
batch.enqueue { yield builder }
|
|
59
|
+
ensure
|
|
60
|
+
builder.close
|
|
61
|
+
end
|
|
62
|
+
batch
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param on_complete [Class, ActiveJob::Base, nil] fired when every job
|
|
66
|
+
# reached a terminal state (+on_finish:+ is a deprecated alias)
|
|
32
67
|
# @param uniqueness_key [String, nil] at most one unfinished batch with
|
|
33
68
|
# this key may exist; see #enqueue for what happens to the next one
|
|
34
69
|
# @param on_conflict [Symbol] :reject (raise AlreadyRunning), :discard
|
|
35
70
|
# (skip the block silently) or :log (skip the block, warn)
|
|
36
|
-
def initialize(
|
|
37
|
-
uniqueness_key: nil, on_conflict: :reject)
|
|
71
|
+
def initialize(on_complete: nil, on_finish: nil, on_success: nil, on_discard: nil, on_failure: nil,
|
|
72
|
+
description: nil, properties: {}, uniqueness_key: nil, on_conflict: :reject)
|
|
38
73
|
raise ArgumentError, "pass on_failure: only — on_discard: is a deprecated alias" if on_discard && on_failure
|
|
74
|
+
raise ArgumentError, "pass on_complete: only — on_finish: is a deprecated alias" if on_finish && on_complete
|
|
39
75
|
unless VALID_CONFLICTS.include?(on_conflict)
|
|
40
76
|
raise ArgumentError, "on_conflict must be one of #{VALID_CONFLICTS.join(", ")}, got #{on_conflict.inspect}"
|
|
41
77
|
end
|
|
@@ -45,9 +81,10 @@ module Pgbus
|
|
|
45
81
|
"[Pgbus] Batch on_discard: is deprecated and will be removed in 1.0 — use on_failure: instead"
|
|
46
82
|
end
|
|
47
83
|
end
|
|
84
|
+
self.class.warn_on_finish_deprecated if on_finish
|
|
48
85
|
|
|
49
86
|
@batch_id = SecureRandom.uuid
|
|
50
|
-
@
|
|
87
|
+
@on_complete = on_complete || on_finish
|
|
51
88
|
@on_success = on_success
|
|
52
89
|
@on_failure = on_failure || on_discard
|
|
53
90
|
@description = description
|
|
@@ -91,6 +128,23 @@ module Pgbus
|
|
|
91
128
|
self
|
|
92
129
|
end
|
|
93
130
|
|
|
131
|
+
# Enqueue ActiveJob instances (and anything the block enqueues) into this
|
|
132
|
+
# batch: +enqueue { ActiveJob.perform_all_later(jobs) }+. Instances keep
|
|
133
|
+
# their own +.set(queue:, wait:, priority:)+ options. On a started batch
|
|
134
|
+
# this is an open-batch add, so a job running inside the batch can call
|
|
135
|
+
# +batch.add(NextJob.new(id))+. Returns the jobs; on a unique batch that
|
|
136
|
+
# another run discarded (#discarded?) nothing was sent.
|
|
137
|
+
def add(*jobs, &block)
|
|
138
|
+
jobs = Builder.job_instances(jobs)
|
|
139
|
+
return jobs if jobs.empty? && block.nil?
|
|
140
|
+
|
|
141
|
+
enqueue do
|
|
142
|
+
::ActiveJob.perform_all_later(jobs) if jobs.any?
|
|
143
|
+
yield if block
|
|
144
|
+
end
|
|
145
|
+
jobs
|
|
146
|
+
end
|
|
147
|
+
|
|
94
148
|
# --- readers on a live batch ---------------------------------------
|
|
95
149
|
|
|
96
150
|
def status = record&.status
|
|
@@ -210,11 +264,22 @@ module Pgbus
|
|
|
210
264
|
end
|
|
211
265
|
|
|
212
266
|
# Schema-probe memos (a true answer is kept for the life of the process)
|
|
213
|
-
# and the warn-once
|
|
267
|
+
# and the warn-once flags, read and written under @schema_lock (issue
|
|
214
268
|
# #527). The probes run outside it: a duplicate probe on a race is
|
|
215
269
|
# harmless, a lock held across a query is not.
|
|
216
270
|
@schema_lock = Mutex.new
|
|
217
271
|
|
|
272
|
+
# on_finish: is in most batch calls written before the rename (issue
|
|
273
|
+
# #538), so it warns once per process rather than on every batch.
|
|
274
|
+
def self.warn_on_finish_deprecated
|
|
275
|
+
first = @schema_lock.synchronize { @warned_on_finish ? false : (@warned_on_finish = true) }
|
|
276
|
+
return unless first
|
|
277
|
+
|
|
278
|
+
Pgbus.logger.warn do
|
|
279
|
+
"[Pgbus] Batch on_finish is deprecated and will be removed in 1.0 — use on_complete instead"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
218
283
|
def self.executions_migrated?
|
|
219
284
|
return true if @schema_lock.synchronize { @executions_migrated }
|
|
220
285
|
|
|
@@ -243,6 +308,7 @@ module Pgbus
|
|
|
243
308
|
@executions_migrated = nil
|
|
244
309
|
@callback_jobs_migrated = nil
|
|
245
310
|
@warned_callback_jobs_unmigrated = nil
|
|
311
|
+
@warned_on_finish = nil
|
|
246
312
|
end
|
|
247
313
|
end
|
|
248
314
|
|
|
@@ -521,7 +587,7 @@ module Pgbus
|
|
|
521
587
|
@batch_id = record.batch_id
|
|
522
588
|
@description = record.description
|
|
523
589
|
@properties = parse_properties(record.properties)
|
|
524
|
-
@
|
|
590
|
+
@on_complete = nil
|
|
525
591
|
@on_success = nil
|
|
526
592
|
@on_failure = nil
|
|
527
593
|
@uniqueness_key = nil
|
|
@@ -572,20 +638,37 @@ module Pgbus
|
|
|
572
638
|
attrs = {
|
|
573
639
|
batch_id: batch_id,
|
|
574
640
|
description: description,
|
|
575
|
-
on_finish_class: callback_class_name(on_finish),
|
|
576
|
-
on_success_class: callback_class_name(on_success),
|
|
577
641
|
properties: JSON.generate(properties),
|
|
578
642
|
status: "pending"
|
|
579
643
|
}
|
|
580
|
-
|
|
581
|
-
attrs[:on_failure_class] = callback_class_name(on_failure)
|
|
582
|
-
else
|
|
583
|
-
attrs[:on_discard_class] = callback_class_name(on_failure)
|
|
584
|
-
end
|
|
585
|
-
attrs.merge!(callback_job_attributes) if self.class.callback_jobs_migrated?
|
|
644
|
+
CALLBACK_COLUMNS.each_key { |slot| attrs.merge!(callback_attributes(slot, public_send(slot))) }
|
|
586
645
|
@record = BatchEntry.create!(attrs)
|
|
587
646
|
end
|
|
588
647
|
|
|
648
|
+
# Replace one callback on a batch that has not finished yet (the
|
|
649
|
+
# builder's setters, inside the Pgbus.batch block, while the row is
|
|
650
|
+
# still pending and so cannot fire its callbacks).
|
|
651
|
+
def write_callback(slot, callback)
|
|
652
|
+
updated = BatchEntry.where(batch_id: batch_id, status: %w[pending processing])
|
|
653
|
+
.update_all(callback_attributes(slot, callback))
|
|
654
|
+
raise AlreadyFinished, "Can't change the callbacks of an already finished batch" if updated.zero?
|
|
655
|
+
|
|
656
|
+
instance_variable_set(:"@#{slot}", callback)
|
|
657
|
+
reload
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# The pgbus_batches columns for one callback slot. Before the
|
|
661
|
+
# add_batch_executions migration the failure class lives in
|
|
662
|
+
# on_discard_class; before add_batch_callback_jobs there is no *_job
|
|
663
|
+
# column, and a configured instance degrades to its class.
|
|
664
|
+
def callback_attributes(slot, callback)
|
|
665
|
+
prefix = CALLBACK_COLUMNS.fetch(slot)
|
|
666
|
+
class_column = slot == :on_failure && !self.class.executions_migrated? ? "on_discard" : prefix
|
|
667
|
+
attrs = { "#{class_column}_class": callback_class_name(callback) }
|
|
668
|
+
attrs[:"#{prefix}_job"] = serialize_callback(callback) if self.class.callback_jobs_migrated?
|
|
669
|
+
attrs
|
|
670
|
+
end
|
|
671
|
+
|
|
589
672
|
# A callback given as a bare class keeps the legacy *_class column; a
|
|
590
673
|
# configured ActiveJob instance is serialized now (so .set options resolve
|
|
591
674
|
# at creation, matching solid_queue) into the *_job jsonb column. Before
|
|
@@ -600,14 +683,6 @@ module Pgbus
|
|
|
600
683
|
callback.class.name
|
|
601
684
|
end
|
|
602
685
|
|
|
603
|
-
def callback_job_attributes
|
|
604
|
-
{
|
|
605
|
-
on_finish_job: serialize_callback(on_finish),
|
|
606
|
-
on_success_job: serialize_callback(on_success),
|
|
607
|
-
on_failure_job: serialize_callback(on_failure)
|
|
608
|
-
}
|
|
609
|
-
end
|
|
610
|
-
|
|
611
686
|
def serialize_callback(callback)
|
|
612
687
|
return nil if callback.nil? || callback.is_a?(Class)
|
|
613
688
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class Configuration
|
|
5
|
+
# The settings a non-main Ractor reads, as one Ractor-shareable value
|
|
6
|
+
# (issue #527). The configuration itself stays on the main Ractor: it holds
|
|
7
|
+
# the logger and the app's callables. Pgbus.finalize! and Pgbus.configure
|
|
8
|
+
# publish a snapshot of it as Pgbus.configuration_snapshot, which any
|
|
9
|
+
# Ractor can read.
|
|
10
|
+
#
|
|
11
|
+
# Only what pgbus's Ractor-safe code reads is here: the queue_prefix, for
|
|
12
|
+
# the stream-key budget (Streams::Key). A setting joins when such code
|
|
13
|
+
# needs it.
|
|
14
|
+
Snapshot = Data.define(:queue_prefix) do
|
|
15
|
+
# A shareable snapshot of +configuration+. The configuration's own
|
|
16
|
+
# values are copied, never frozen in place. Nil when a setting is not
|
|
17
|
+
# plain data (Shareable.data?), which only a configuration that skipped
|
|
18
|
+
# validation can hold: the caller keeps the snapshot it has.
|
|
19
|
+
def self.of(configuration)
|
|
20
|
+
prefix = configuration.queue_prefix
|
|
21
|
+
return unless Shareable.data?(prefix)
|
|
22
|
+
|
|
23
|
+
new(queue_prefix: Shareable.freeze_copy(prefix))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -263,7 +263,7 @@ module Pgbus
|
|
|
263
263
|
[:add_batch_executions]
|
|
264
264
|
end
|
|
265
265
|
|
|
266
|
-
# Configured callback instances (
|
|
266
|
+
# Configured callback instances (on_complete: MyJob.new.set(...)) need
|
|
267
267
|
# somewhere to store the serialized job — issue #415.
|
|
268
268
|
def batch_callback_jobs_migrations
|
|
269
269
|
return [] unless table_exists?("pgbus_batches")
|
|
@@ -21,6 +21,17 @@ module Pgbus
|
|
|
21
21
|
# payload: queue, job_class, msg_id, vt, extensions
|
|
22
22
|
# pgbus.event_processed — event handler succeeded
|
|
23
23
|
# pgbus.event_failed — event handler raised; carries :exception_object
|
|
24
|
+
# pgbus.event_dead_lettered — a consumer moved an event that exceeded max_retries
|
|
25
|
+
# to its DLQ; fires once, after the move
|
|
26
|
+
# payload: queue_name, routing_key, handler, event_id,
|
|
27
|
+
# read_ct, msg_id, error (the class of the last
|
|
28
|
+
# recorded failure: a handler's error, or the
|
|
29
|
+
# consumer's own parse error for a body it could
|
|
30
|
+
# not route; nil when none was recorded). Any of
|
|
31
|
+
# routing_key/handler/event_id can be nil: all
|
|
32
|
+
# three when the body is not a JSON object, a
|
|
33
|
+
# field the envelope lacks, and handler when no
|
|
34
|
+
# single subscriber in this process owns the queue
|
|
24
35
|
# pgbus.event_unrouted — a consumer read an event from a queue no
|
|
25
36
|
# subscriber in this process owns (stale topic
|
|
26
37
|
# binding); the message is archived
|
|
@@ -60,6 +60,7 @@ module Pgbus
|
|
|
60
60
|
subscribe("pgbus.job_dead_lettered") { |event| on_job_dead_lettered(event) },
|
|
61
61
|
subscribe("pgbus.event_processed") { |event| on_event_processed(event) },
|
|
62
62
|
subscribe("pgbus.event_failed") { |event| on_event_failed(event) },
|
|
63
|
+
subscribe("pgbus.event_dead_lettered") { |event| on_event_dead_lettered(event) },
|
|
63
64
|
subscribe("pgbus.client.send_message") { |event| on_send_message(event) },
|
|
64
65
|
subscribe("pgbus.client.send_batch") { |event| on_send_batch(event) },
|
|
65
66
|
subscribe("pgbus.client.read_batch") { |event| on_read_batch(event) },
|
|
@@ -175,6 +176,20 @@ module Pgbus
|
|
|
175
176
|
::Appsignal.set_error(err) if err && ::Appsignal.respond_to?(:set_error)
|
|
176
177
|
end
|
|
177
178
|
|
|
179
|
+
# A counter only, like on_job_dead_lettered: nothing is raised at
|
|
180
|
+
# move time, and the handler's error already went out through
|
|
181
|
+
# pgbus.event_failed on the attempt that raised (issue #543). A body
|
|
182
|
+
# that is not JSON leaves handler and routing_key nil; compact drops
|
|
183
|
+
# those tags, as Metrics::Subscriber does.
|
|
184
|
+
def on_event_dead_lettered(event)
|
|
185
|
+
payload = event.payload
|
|
186
|
+
::Appsignal.increment_counter(
|
|
187
|
+
"#{METRIC_PREFIX}event_count",
|
|
188
|
+
1,
|
|
189
|
+
{ handler: payload[:handler], routing_key: payload[:routing_key], status: "dead_lettered" }.compact
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
178
193
|
# ── Client (PGMQ wrapper) ───────────────────────────────────────
|
|
179
194
|
|
|
180
195
|
def on_send_message(event)
|
|
@@ -63,6 +63,7 @@ module Pgbus
|
|
|
63
63
|
subscribe("pgbus.job_visibility_extended") { |event| on_job_visibility_extended(event) },
|
|
64
64
|
subscribe("pgbus.event_processed") { |event| on_event_processed(event) },
|
|
65
65
|
subscribe("pgbus.event_failed") { |event| on_event_failed(event) },
|
|
66
|
+
subscribe("pgbus.event_dead_lettered") { |event| on_event_dead_lettered(event) },
|
|
66
67
|
subscribe("pgbus.event_skipped") { |event| on_event_skipped(event) },
|
|
67
68
|
subscribe("pgbus.client.send_message") { |event| on_send_message(event) },
|
|
68
69
|
subscribe("pgbus.client.send_batch") { |event| on_send_batch(event) },
|
|
@@ -154,6 +155,17 @@ module Pgbus
|
|
|
154
155
|
)
|
|
155
156
|
end
|
|
156
157
|
|
|
158
|
+
# The consumer moved an event that exhausted max_retries to its DLQ
|
|
159
|
+
# (issue #543). A body that is not JSON leaves handler and routing_key
|
|
160
|
+
# nil; compact drops those tags.
|
|
161
|
+
def on_event_dead_lettered(event)
|
|
162
|
+
payload = event.payload
|
|
163
|
+
backend.increment(
|
|
164
|
+
"#{METRIC_PREFIX}event_count", 1,
|
|
165
|
+
compact(handler: payload[:handler], routing_key: payload[:routing_key], status: "dead_lettered")
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
157
169
|
# An idempotent handler that did not run this delivery. `reason` is what
|
|
158
170
|
# makes the skip readable: :completed/:cached is deduplication working,
|
|
159
171
|
# :owned means a second delivery arrived while the holder was still
|
|
@@ -289,9 +289,9 @@ module Pgbus
|
|
|
289
289
|
@jobs_processed.increment
|
|
290
290
|
end
|
|
291
291
|
|
|
292
|
-
# Order: read the last error, move with the dead-letter block,
|
|
293
|
-
#
|
|
294
|
-
# next attempt still carries the reason.
|
|
292
|
+
# Order: read the last error, move with the dead-letter block, clear the
|
|
293
|
+
# failed-event row, then announce it. A move that raises keeps the row,
|
|
294
|
+
# so the next attempt still carries the reason, and announces nothing.
|
|
295
295
|
def dead_letter(message, queue_name)
|
|
296
296
|
last_error = FailedEventRecorder.last_error(queue_name: queue_name, msg_id: message.msg_id.to_i)
|
|
297
297
|
headers = DeadLetterHeader.build(
|
|
@@ -301,6 +301,59 @@ module Pgbus
|
|
|
301
301
|
)
|
|
302
302
|
Pgbus.client.move_to_dead_letter(queue_name, message, headers: headers)
|
|
303
303
|
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: message.msg_id.to_i)
|
|
304
|
+
instrument_dead_lettered(message, queue_name, last_error)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# pgbus.event_dead_lettered, the event twin of the executor's
|
|
308
|
+
# pgbus.job_dead_lettered (issue #543). The dead-letter branch runs
|
|
309
|
+
# before handle_message parses the envelope, so it is parsed here: once
|
|
310
|
+
# per dead-lettered message, never on the success path. The move has
|
|
311
|
+
# already happened, so nothing here may turn the dead-letter into a
|
|
312
|
+
# failure (a failed stat, a circuit-breaker trip): the guard is the
|
|
313
|
+
# executor's instrument rescue, widened on purpose to cover building the
|
|
314
|
+
# payload as well as the subscribers, and logs at the same debug level.
|
|
315
|
+
def instrument_dead_lettered(message, queue_name, last_error)
|
|
316
|
+
envelope = dead_letter_envelope(message)
|
|
317
|
+
routing_key = envelope_routing_key(envelope)
|
|
318
|
+
Pgbus::Instrumentation.instrument(
|
|
319
|
+
"pgbus.event_dead_lettered",
|
|
320
|
+
queue_name: queue_name, routing_key: routing_key,
|
|
321
|
+
handler: owning_handler_name(routing_key, queue_name), event_id: envelope["event_id"],
|
|
322
|
+
read_ct: message.read_ct.to_i, msg_id: message.msg_id.to_i, error: last_error&.dig(:error_class)
|
|
323
|
+
)
|
|
324
|
+
rescue StandardError => e
|
|
325
|
+
Pgbus.logger.debug { "[Pgbus] Notification failure pgbus.event_dead_lettered: #{e.class}: #{e.message}" }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# The body that killed a message may not be a JSON object at all: PGMQ
|
|
329
|
+
# stores jsonb, so a producer writing straight to the queue can leave a
|
|
330
|
+
# scalar or an array, and the success path's dig raises on those every
|
|
331
|
+
# read. Any of them reads as an empty envelope, so the notification
|
|
332
|
+
# still fires.
|
|
333
|
+
def dead_letter_envelope(message)
|
|
334
|
+
raw = JSON.parse(message.message)
|
|
335
|
+
raw.is_a?(Hash) ? raw : {}
|
|
336
|
+
rescue JSON::ParserError, TypeError
|
|
337
|
+
{}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The success path's lookup (headers first, then the top level),
|
|
341
|
+
# tolerating a headers value that is not an object.
|
|
342
|
+
def envelope_routing_key(envelope)
|
|
343
|
+
headers = envelope["headers"]
|
|
344
|
+
(headers["routing_key"] if headers.is_a?(Hash)) || envelope["routing_key"]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The handler of the one subscriber that owns +queue_name+ in this
|
|
348
|
+
# process, or nil when there is none, no usable routing key, or more than
|
|
349
|
+
# one: #469 makes one owner per queue the convention, but Registry does
|
|
350
|
+
# not enforce it, and dispatch runs every owner, so naming one of several
|
|
351
|
+
# would misattribute the dead letter.
|
|
352
|
+
def owning_handler_name(routing_key, queue_name)
|
|
353
|
+
return unless routing_key.is_a?(String)
|
|
354
|
+
|
|
355
|
+
owners = @registry.handlers_for(routing_key, queue_name: queue_name)
|
|
356
|
+
owners.first.handler_class.name if owners.one?
|
|
304
357
|
end
|
|
305
358
|
|
|
306
359
|
# The executor's handle_failure, for events (issue #494): one upsert per
|
data/lib/pgbus/shareable.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
# Deep-frozen, Ractor-shareable copies of plain data, for the settings
|
|
5
|
-
# Configuration#finalize! freezes at boot
|
|
5
|
+
# Configuration#finalize! freezes at boot and the snapshot non-main Ractors
|
|
6
|
+
# read (issue #527).
|
|
6
7
|
module Shareable
|
|
7
8
|
# Leaves that are plain data. Modules and classes count too: they are
|
|
8
9
|
# shareable by nature.
|
|
@@ -39,6 +40,15 @@ module Pgbus
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# True on the main Ractor, the only one that can read Pgbus.configuration,
|
|
44
|
+
# and wherever the engine has no Ractors. Ractor.main? is Ruby 3.4+.
|
|
45
|
+
def main_ractor?
|
|
46
|
+
return true unless defined?(::Ractor)
|
|
47
|
+
return ::Ractor.main? if ::Ractor.respond_to?(:main?)
|
|
48
|
+
|
|
49
|
+
::Ractor.current.equal?(::Ractor.main)
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
# A deep-frozen copy of plain data (see data?), Ractor-shareable where the
|
|
43
53
|
# engine has Ractors. The caller's own objects are copied, never frozen in
|
|
44
54
|
# place.
|
data/lib/pgbus/streams/key.rb
CHANGED
|
@@ -105,7 +105,7 @@ module Pgbus
|
|
|
105
105
|
raise ArgumentError,
|
|
106
106
|
"stream_key #{key.inspect} is #{key.length} chars, " \
|
|
107
107
|
"exceeds pgbus budget of #{budget} " \
|
|
108
|
-
"(queue_prefix=#{
|
|
108
|
+
"(queue_prefix=#{queue_prefix.inspect}, " \
|
|
109
109
|
"pgbus_max_queue_name_length=#{QueueNameValidator::MAX_QUEUE_NAME_LENGTH}). " \
|
|
110
110
|
"Shorten the streamables or use Pgbus::Streams::Streamable on the model."
|
|
111
111
|
end
|
|
@@ -182,10 +182,19 @@ module Pgbus
|
|
|
182
182
|
# override `config.queue_prefix` get the correct budget
|
|
183
183
|
# automatically.
|
|
184
184
|
def queue_name_budget
|
|
185
|
-
QueueNameValidator::MAX_QUEUE_NAME_LENGTH -
|
|
186
|
-
Pgbus.configuration.queue_prefix.length - 1
|
|
185
|
+
QueueNameValidator::MAX_QUEUE_NAME_LENGTH - queue_prefix.length - 1
|
|
187
186
|
end
|
|
188
187
|
|
|
188
|
+
# The live configuration's queue_prefix on the main Ractor. A non-main
|
|
189
|
+
# Ractor cannot read Pgbus.configuration (it holds the logger and the
|
|
190
|
+
# app's callables), so there it is the published snapshot's: the
|
|
191
|
+
# prefix as the last Pgbus.finalize! or Pgbus.configure left it.
|
|
192
|
+
def queue_prefix
|
|
193
|
+
config = Shareable.main_ractor? ? Pgbus.configuration : Pgbus.configuration_snapshot
|
|
194
|
+
config.queue_prefix
|
|
195
|
+
end
|
|
196
|
+
private_class_method :queue_prefix
|
|
197
|
+
|
|
189
198
|
# Raises if a normalized fragment contains the `:` separator.
|
|
190
199
|
# Kept private-ish via module_function so the guard is shared
|
|
191
200
|
# between stream_key and any future composer without becoming
|
data/lib/pgbus/version.rb
CHANGED
data/lib/pgbus.rb
CHANGED
|
@@ -90,6 +90,13 @@ module Pgbus
|
|
|
90
90
|
# Readers need no lock: a reference is published whole.
|
|
91
91
|
attr_reader :loader, :models_loader, :configuration
|
|
92
92
|
|
|
93
|
+
# The settings a non-main Ractor reads (Configuration::Snapshot): a
|
|
94
|
+
# Ractor-shareable copy of the configuration as the last Pgbus.finalize!
|
|
95
|
+
# or Pgbus.configure left it, readable from any Ractor. The defaults until
|
|
96
|
+
# the first finalize, and again after reset!. A setter alone does not
|
|
97
|
+
# publish: the main Ractor reads the configuration itself.
|
|
98
|
+
attr_reader :configuration_snapshot
|
|
99
|
+
|
|
93
100
|
# Process-global flag set by Worker#graceful_shutdown so the adapter
|
|
94
101
|
# can report stopping? to ActiveJob::Continuation (Rails 8.1+).
|
|
95
102
|
def stopping
|
|
@@ -210,20 +217,21 @@ module Pgbus
|
|
|
210
217
|
# configure once a Rails app has booted. While a Rails app boots, the
|
|
211
218
|
# engine's after_initialize finalizes instead, after every initializer
|
|
212
219
|
# and after_initialize block has had its say.
|
|
213
|
-
|
|
220
|
+
finalize_configuration! if configuration.finalized? || !rails_booting?
|
|
214
221
|
configuration
|
|
215
222
|
end
|
|
216
223
|
|
|
217
224
|
# The end of boot: validates the configuration (unless eager_validation
|
|
218
|
-
# is off)
|
|
219
|
-
# (Configuration#finalize!)
|
|
220
|
-
#
|
|
225
|
+
# is off), freezes its plain-data settings into Ractor-shareable values
|
|
226
|
+
# (Configuration#finalize!) and publishes configuration_snapshot for
|
|
227
|
+
# non-main Ractors. Callables and the logger stay as they are, on the main
|
|
228
|
+
# Ractor. Runs on its own: from the engine's after_initialize in a
|
|
221
229
|
# Rails app, at the end of each Pgbus.configure without Rails, and in
|
|
222
230
|
# `pgbus start` after its flags. Call it yourself only when you configure
|
|
223
231
|
# pgbus through setters alone, without Rails.
|
|
224
232
|
def finalize!
|
|
225
233
|
configuration.validate! if configuration.eager_validation
|
|
226
|
-
|
|
234
|
+
finalize_configuration!
|
|
227
235
|
end
|
|
228
236
|
|
|
229
237
|
# Built on first use, under @state_lock: two threads making the first
|
|
@@ -298,6 +306,17 @@ module Pgbus
|
|
|
298
306
|
Streams::Key.queue_name_budget
|
|
299
307
|
end
|
|
300
308
|
|
|
309
|
+
# Open a batch, yield a Pgbus::Batch::Builder, and start the batch when
|
|
310
|
+
# the block returns — the top-level shortcut for Pgbus::Batch.start.
|
|
311
|
+
#
|
|
312
|
+
# Pgbus.batch(description: "Import") do |b|
|
|
313
|
+
# b.on_complete ImportDoneJob
|
|
314
|
+
# b.add(*rows.map { |row| ImportRowJob.new(row.id) })
|
|
315
|
+
# end
|
|
316
|
+
def batch(**, &)
|
|
317
|
+
Batch.start(**, &)
|
|
318
|
+
end
|
|
319
|
+
|
|
301
320
|
# Publish an event to the bus — the top-level shortcut for
|
|
302
321
|
# `Pgbus::EventBus::Publisher.publish`, symmetric with `Pgbus.stream`.
|
|
303
322
|
#
|
|
@@ -322,6 +341,7 @@ module Pgbus
|
|
|
322
341
|
@client&.close
|
|
323
342
|
@client = nil
|
|
324
343
|
@configuration = Configuration.new
|
|
344
|
+
@configuration_snapshot = Configuration::Snapshot.of(@configuration)
|
|
325
345
|
@stream_cache = Concurrent::Map.new
|
|
326
346
|
end
|
|
327
347
|
end
|
|
@@ -353,11 +373,26 @@ module Pgbus
|
|
|
353
373
|
# Process-local, like the client it holds; replaced (never mutated in
|
|
354
374
|
# place) by reset!/reset_client!.
|
|
355
375
|
attr_reader :stream_cache
|
|
376
|
+
|
|
377
|
+
# Freezes the configuration's plain-data settings and publishes the
|
|
378
|
+
# snapshot non-main Ractors read, unless a reset! replaced the
|
|
379
|
+
# configuration meanwhile (its own snapshot stands) or the snapshot could
|
|
380
|
+
# not be taken (the last one stands).
|
|
381
|
+
def finalize_configuration!
|
|
382
|
+
config = configuration
|
|
383
|
+
config.finalize!
|
|
384
|
+
snapshot = Configuration::Snapshot.of(config)
|
|
385
|
+
@state_lock.synchronize do
|
|
386
|
+
@configuration_snapshot = snapshot if snapshot && @configuration.equal?(config)
|
|
387
|
+
end
|
|
388
|
+
config
|
|
389
|
+
end
|
|
356
390
|
end
|
|
357
391
|
|
|
358
392
|
# Module state, built here at load rather than by `||=` on first use, so no
|
|
359
393
|
# two threads can each build their own (issue #527). @state_lock guards every
|
|
360
|
-
# later write: the client memo, reset!/reset_client!, the stopping flag
|
|
394
|
+
# later write: the client memo, reset!/reset_client!, the stopping flag, the
|
|
395
|
+
# configuration snapshot.
|
|
361
396
|
@state_lock = Mutex.new
|
|
362
397
|
@stopping = false
|
|
363
398
|
@client = nil
|
|
@@ -367,6 +402,7 @@ module Pgbus
|
|
|
367
402
|
|
|
368
403
|
# After loader.setup: Configuration is Zeitwerk-managed.
|
|
369
404
|
@configuration = Configuration.new
|
|
405
|
+
@configuration_snapshot = Configuration::Snapshot.of(@configuration)
|
|
370
406
|
|
|
371
407
|
# In non-Rails contexts, set up model autoloading via a separate loader.
|
|
372
408
|
# This is torn down by the Engine initializer when Rails boots.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Pgbus
|
|
2
|
+
class Configuration
|
|
3
|
+
# The settings a non-main Ractor reads, as one shareable value
|
|
4
|
+
# (Pgbus.configuration_snapshot).
|
|
5
|
+
class Snapshot < Data
|
|
6
|
+
attr_reader queue_prefix: String
|
|
7
|
+
def self.new: (queue_prefix: String) -> instance
|
|
8
|
+
| (String queue_prefix) -> instance
|
|
9
|
+
def self.of: (Configuration configuration) -> Snapshot?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/sig/pgbus.rbs
CHANGED
|
@@ -49,7 +49,15 @@ module Pgbus
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Partial: only the types Pgbus.batch names. Batch's own methods are not
|
|
53
|
+
# covered by spec/pgbus/rbs_spec.rb yet.
|
|
54
|
+
class Batch
|
|
55
|
+
class Builder
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def self.configuration: () -> Configuration
|
|
60
|
+
def self.configuration_snapshot: () -> Configuration::Snapshot
|
|
53
61
|
def self.configure: () { (Configuration) -> void } -> Configuration
|
|
54
62
|
def self.finalize!: () -> Configuration
|
|
55
63
|
def self.client: () -> Client
|
|
@@ -57,6 +65,8 @@ module Pgbus
|
|
|
57
65
|
def self.stream_key: (*untyped parts, ?digest_bits: Integer) -> String
|
|
58
66
|
def self.stream_key!: (String key) -> String
|
|
59
67
|
def self.stream_name_budget: () -> Integer
|
|
68
|
+
# Keywords go to Pgbus::Batch.new (description:, properties:, on_complete:, uniqueness_key:, …).
|
|
69
|
+
def self.batch: (**untyped options) { (Batch::Builder) -> void } -> Batch
|
|
60
70
|
# delay: seconds, as a Numeric or an ActiveSupport::Duration (1.hour).
|
|
61
71
|
def self.publish: (String routing_key, untyped payload, ?headers: Hash[String, untyped]?, ?delay: Numeric | _ToI) -> untyped
|
|
62
72
|
def self.publish_later: (String routing_key, untyped payload, delay: Numeric | _ToI, ?headers: Hash[String, untyped]?) -> untyped
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.19.
|
|
4
|
+
version: 0.19.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -307,6 +307,7 @@ files:
|
|
|
307
307
|
- lib/pgbus/active_job/executor.rb
|
|
308
308
|
- lib/pgbus/autovacuum_tuning.rb
|
|
309
309
|
- lib/pgbus/batch.rb
|
|
310
|
+
- lib/pgbus/batch/builder.rb
|
|
310
311
|
- lib/pgbus/batch/sweep.rb
|
|
311
312
|
- lib/pgbus/bus_record.rb
|
|
312
313
|
- lib/pgbus/circuit_breaker.rb
|
|
@@ -328,6 +329,7 @@ files:
|
|
|
328
329
|
- lib/pgbus/configuration/capsule_dsl.rb
|
|
329
330
|
- lib/pgbus/configuration/deploy_settings.rb
|
|
330
331
|
- lib/pgbus/configuration/presence_settings.rb
|
|
332
|
+
- lib/pgbus/configuration/snapshot.rb
|
|
331
333
|
- lib/pgbus/configuration/stream_authorization.rb
|
|
332
334
|
- lib/pgbus/current_attributes.rb
|
|
333
335
|
- lib/pgbus/database_tasks_guard.rb
|
|
@@ -514,6 +516,7 @@ files:
|
|
|
514
516
|
- sig/pgbus/active_job/adapter.rbs
|
|
515
517
|
- sig/pgbus/client.rbs
|
|
516
518
|
- sig/pgbus/configuration.rbs
|
|
519
|
+
- sig/pgbus/configuration/snapshot.rbs
|
|
517
520
|
- sig/pgbus/streams.rbs
|
|
518
521
|
- sig/pgbus/streams/stream.rbs
|
|
519
522
|
homepage: https://github.com/zoolutions/pgbus
|