pgbus 0.8.4 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/app/assets/javascripts/pgbus/stream_source_element.js +150 -5
- data/app/controllers/pgbus/batches_controller.rb +15 -0
- data/app/helpers/pgbus/application_helper.rb +12 -0
- data/app/views/layouts/pgbus/application.html.erb +2 -0
- data/app/views/pgbus/batches/_batches_table.html.erb +54 -0
- data/app/views/pgbus/batches/index.html.erb +8 -0
- data/app/views/pgbus/batches/show.html.erb +90 -0
- data/config/locales/da.yml +34 -0
- data/config/locales/de.yml +34 -0
- data/config/locales/en.yml +34 -0
- data/config/locales/es.yml +34 -0
- data/config/locales/fi.yml +34 -0
- data/config/locales/fr.yml +34 -0
- data/config/locales/it.yml +34 -0
- data/config/locales/ja.yml +34 -0
- data/config/locales/nb.yml +34 -0
- data/config/locales/nl.yml +34 -0
- data/config/locales/pt.yml +34 -0
- data/config/locales/sv.yml +34 -0
- data/config/routes.rb +1 -0
- data/lib/pgbus/client.rb +102 -4
- data/lib/pgbus/configuration.rb +84 -0
- data/lib/pgbus/execution_pools/async_pool.rb +44 -6
- data/lib/pgbus/process/supervisor.rb +2 -1
- data/lib/pgbus/process/worker.rb +38 -1
- data/lib/pgbus/serializer.rb +1 -1
- data/lib/pgbus/streams/coalescer.rb +88 -0
- data/lib/pgbus/streams/envelope.rb +20 -1
- data/lib/pgbus/streams/key.rb +35 -2
- data/lib/pgbus/streams/renderer.rb +67 -0
- data/lib/pgbus/streams.rb +150 -1
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +96 -0
- data/lib/pgbus/web/stream_app.rb +8 -4
- data/lib/pgbus/web/streamer/connection.rb +15 -1
- data/lib/pgbus/web/streamer/falcon_connection.rb +9 -1
- data/lib/pgbus/web/streamer/heartbeat.rb +23 -1
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +129 -14
- data/lib/pgbus.rb +11 -0
- data/lib/tasks/pgbus_queues.rake +54 -0
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5027c420c26dac65291e11e07ca0291da540b7d2aef9d963e9fdebc2217e7943
|
|
4
|
+
data.tar.gz: 9e66b85229c55528b3b5b2093883935c1333733018508bb6d9f7f7836666363d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c476c7de9c0db40e0d2183a11f7d502704a468e68f49b28934619cfab07f1130f3988d6f0fbfa6e77a57d6450aa183746b301367e0d4af7dcd2779319f96ff7
|
|
7
|
+
data.tar.gz: ed62152e6c2c361c5b8f38c61e31ea4e3af5d031cf68f71d0ea95177ab04f538bfbb4fefc84f5d08a2acd8efb23b0024d6433d4a6db2272e7c05521a59e83a3b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Streams: optional publish-side coalescing for high-frequency broadcasts.** A chatty reactive component (live cursor, typing indicator, progress bar) can fan out many small broadcasts per second. Pass `coalesce:` (a window in milliseconds, or `true` for the 50ms default) with `target:` to `broadcast`/`broadcast_render` to batch per `(stream, target)` and publish only the *latest* frame within the window — superseded frames never hit the bus (no PGMQ insert, no NOTIFY, no fan-out). Last-write-wins, so it is opt-in and only safe for idempotent `replace`/`update` of a stable target (exactly the high-frequency case). The new `Pgbus::Streams::Coalescer` is a process-wide, thread-safe, trailing-edge-with-max-wait debounce: the first submit per window schedules the flush (bounding latency to one window) and later submits only overwrite the buffered payload; the flush re-enters the normal broadcast path, so a coalesced frame is just a deferred ordinary broadcast that still composes with `visible_to`/`exclude`/`event`/`durable`. Refs #171.
|
|
6
|
+
|
|
7
|
+
- **Streams: typed SSE event names on broadcasts.** A broadcast can set the SSE `event:` field — `Pgbus.stream(name).broadcast(html, event: "presence")` (and `broadcast_render(..., event:)`) — while keeping the payload a Turbo Stream, so clients route on the typed name instead of sniffing the HTML. The default (`nil` or `"turbo-stream"`) is omitted from the JSONB payload to avoid redundancy, but is still set on the SSE frame's `event:` line (the connection adapter falls back to `turbo-stream`), so default consumers still get the standard `message`/turbo-stream path. The event flows through the JSONB payload → `StreamEnvelope.event` → the SSE frame's `event:` line (both the Puma and Falcon connection adapters). On the client, `<pgbus-stream-source>` dispatches a typed broadcast as a generic `pgbus:event` (`{ event, data, msgId }`) and a named `pgbus:<event>` (`{ data, msgId }`) for `addEventListener` ergonomics; declare typed event names via the element's `listen-events` attribute so they survive the EventSource reconnect path. Refs #170.
|
|
8
|
+
|
|
9
|
+
- **Streams: optional connection-driven presence.** Streams matching `config.streams_presence_patterns` (exact string or Regexp, mirroring `streams_durable_patterns`) now auto-join a member on SSE connect, auto-leave on disconnect, and refresh `last_seen_at` on the keepalive heartbeat — no explicit `join`/`leave`/sweeper wiring. Identity comes from the connection's authorize-hook context: the built-in extractor handles a Hash with `:member_id`/`:id` (plus optional `:metadata`) or any object responding to `#id`, and `config.streams_presence_member` accepts a custom `->(context) { { id:, metadata: } }`. Membership work runs on the dispatcher thread (which already releases AR connections each pass); the heartbeat posts a batched `PresenceTouchMessage` per tick. Presence failures are logged and swallowed so a presence-DB hiccup can't knock a live SSE connection out of the registry. Anonymous connections (no derivable member) are simply skipped. Refs #169.
|
|
10
|
+
|
|
11
|
+
- **Streams: msg_id (revision) exposed to the client for optimistic-UI reconciliation.** Each delivered turbo-stream frame already carries its monotonic `msg_id` as the SSE `id:`; `<pgbus-stream-source>` now surfaces it two ways: the standard `message` MessageEvent sets `lastEventId` to the msg_id (Turbo ignores it; a reactive runtime listening for `message` reads the revision with no pgbus-specific API), and a new `pgbus:message` CustomEvent carries `{ msgId, data }` (msgId is a Number when numeric; a negative value marks an ephemeral frame). Documents the reconciliation pattern: track the highest applied msgId per target and skip the morph if a newer revision was already applied, so a late echo can't clobber a newer optimistic edit. Complements #165 (exclude handles the actor; the revision handles out-of-order delivery for everyone else). Refs #168.
|
|
12
|
+
|
|
13
|
+
- **Streams: `Stream#broadcast_render` — render a renderable and broadcast it in one call.** `Pgbus.stream(name).broadcast_render(renderable:, target:, action: :replace, exclude:, visible_to:, durable:)` renders a Phlex component, a ViewComponent, or a pre-rendered HTML string into a complete `<turbo-stream action target><template>…</template></turbo-stream>` tag and broadcasts it atomically — removing the off-request render + tag-building boilerplate (and the easy-to-get-wrong view context) from every call site. `action` defaults to `:replace`; content-less actions (`remove`) emit no `<template>`. `exclude:`/`visible_to:`/`durable:` forward to `#broadcast`, so actor-echo suppression and audience filtering compose. Self-contained (no turbo-rails/ActionView/Phlex dependency); the renderable is resolved via `String` → `#call` (Phlex) → `#render_in` (ViewComponent) → `#to_s`. Refs #166.
|
|
14
|
+
|
|
15
|
+
- **Streams: actor-echo suppression via `exclude:` on broadcasts.** A broadcast can now name a connection id to skip: `Pgbus.stream(name).broadcast(html, exclude: connection_id)` (and `visible_to:` composes with it). The dispatcher skips delivery to that one SSE connection, so an actor who triggered a change does not receive the echo of its own broadcast — it already applied the change via its action's HTTP response, and re-applying the SSE echo would double-apply (re-run animations, clobber optimistic edits). Every server-minted SSE connection now exposes its id to the page: the server sends a `pgbus:connected` frame right after the open handshake, and `<pgbus-stream-source>` captures it onto the element's `connection-id` attribute and re-dispatches it as a `pgbus:connected` event (also surfaced in `pgbus:open`'s detail). A page reads its connection id and sends it back as the `X-Pgbus-Connection` header on action requests; the broadcaster passes `exclude: request.headers["X-Pgbus-Connection"]`. Reuses the existing per-connection delivery (Filters) path. Refs #165.
|
|
16
|
+
|
|
17
|
+
- **Streams: `Pgbus.stream_key` is idempotent for an already-built key, plus `Pgbus.stream_key!`.** A single `String` argument is now treated as a pre-built pgbus stream key and returned unchanged (after the queue-name budget check) instead of tripping the colon-separator guard. This lets a consumer hold one `stream_key` value and pass it to both `turbo_stream_from` and the broadcaster without `stream_key("chat:lobby")` raising `ArgumentError`. The guard still fires for the genuinely ambiguous multi-fragment join (`stream_key("a:b", :c)`), and `Symbol`/record fragments with colons are still rejected (a colon there never came from `stream_key`). `Pgbus.stream_key!(key)` accepts a pre-built key explicitly (String required, budget enforced). Refs #167.
|
|
18
|
+
|
|
3
19
|
### Breaking Changes
|
|
4
20
|
|
|
5
21
|
- **Queue names must be alphanumeric and underscores only.** Queue names containing dashes (e.g., `my-app-queue`) will now raise `ArgumentError`. Rename to underscored form (e.g., `my_app_queue`) before upgrading. This restriction prevents SQL injection via PGMQ queue identifiers, which are interpolated into table names and cannot be parameterized.
|
|
@@ -17,12 +17,32 @@
|
|
|
17
17
|
// channel — compatibility shim; ignored
|
|
18
18
|
//
|
|
19
19
|
// Events (dispatched on the element):
|
|
20
|
-
//
|
|
20
|
+
// message (MessageEvent) data = turbo-stream HTML;
|
|
21
|
+
// lastEventId = the frame's msg_id (revision)
|
|
22
|
+
// pgbus:message { msgId, data } — same frame, for optimistic-UI
|
|
23
|
+
// reconciliation (skip morph if a newer rev for the
|
|
24
|
+
// target was already applied). See #168.
|
|
25
|
+
// pgbus:event { event, data, msgId } — a typed broadcast (any SSE
|
|
26
|
+
// event name other than turbo-stream). See #170.
|
|
27
|
+
// pgbus:<event> { data, msgId } — the same typed broadcast, named
|
|
28
|
+
// for ergonomic addEventListener("pgbus:presence").
|
|
29
|
+
// pgbus:open { lastEventId, connectionId }
|
|
30
|
+
// pgbus:connected { connectionId }
|
|
21
31
|
// pgbus:replay-start { fromId, toId }
|
|
22
32
|
// pgbus:replay-end {}
|
|
23
33
|
// pgbus:gap-detected { lastSeenId, archiveOldestId }
|
|
24
34
|
// pgbus:close { code, reason }
|
|
25
35
|
//
|
|
36
|
+
// Connection id (issue #165 — actor-echo suppression): the server sends a
|
|
37
|
+
// `pgbus:connected` frame right after the open handshake carrying the
|
|
38
|
+
// server-minted connection id. This element captures it, reflects it onto
|
|
39
|
+
// the `connection-id` attribute, and re-dispatches it as `pgbus:connected`.
|
|
40
|
+
// A page reads it (from the element or a `<meta name="pgbus-connection-id">`
|
|
41
|
+
// the app mirrors it to) and sends it back as the `X-Pgbus-Connection`
|
|
42
|
+
// header on action requests. The broadcaster then passes
|
|
43
|
+
// `exclude: request.headers["X-Pgbus-Connection"]` so the actor does not
|
|
44
|
+
// receive the echo of its own broadcast.
|
|
45
|
+
//
|
|
26
46
|
// The element integrates with Turbo via connectStreamSource /
|
|
27
47
|
// disconnectStreamSource + dispatching MessageEvent("message") so Turbo
|
|
28
48
|
// Stream HTML is automatically consumed by the existing StreamObserver.
|
|
@@ -48,9 +68,17 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
48
68
|
this.abortController = null
|
|
49
69
|
this.eventSource = null
|
|
50
70
|
this.lastEventId = null
|
|
71
|
+
this.connectionId = null
|
|
51
72
|
this.closed = false
|
|
52
73
|
}
|
|
53
74
|
|
|
75
|
+
// The server-minted connection id for this SSE connection, or null
|
|
76
|
+
// until the `pgbus:connected` frame arrives. Public read accessor so
|
|
77
|
+
// a reactive runtime can grab it without poking at attributes.
|
|
78
|
+
get pgbusConnectionId() {
|
|
79
|
+
return this.connectionId
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
connectedCallback() {
|
|
55
83
|
this.closed = false
|
|
56
84
|
connectStreamSource(this)
|
|
@@ -80,6 +108,12 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
80
108
|
// the URL. Parses the SSE event stream by hand because EventSource
|
|
81
109
|
// doesn't expose custom query strings uniformly across browsers.
|
|
82
110
|
async openFetchStream() {
|
|
111
|
+
// Each transport open is a fresh connection: the server mints a new id
|
|
112
|
+
// and sends a new pgbus:connected frame. Clear the previous id so
|
|
113
|
+
// pgbus:open (which fires before that frame arrives) can't surface a
|
|
114
|
+
// stale connection id — a stale id would produce a wrong X-Pgbus-Connection
|
|
115
|
+
// exclude header during reconnect windows.
|
|
116
|
+
this.resetConnectionId()
|
|
83
117
|
const url = this.buildUrl({ includeSince: true })
|
|
84
118
|
this.abortController = new AbortController()
|
|
85
119
|
|
|
@@ -99,7 +133,7 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
99
133
|
|
|
100
134
|
this.setAttribute("connected", "")
|
|
101
135
|
this.dispatchEvent(new CustomEvent("pgbus:open", {
|
|
102
|
-
detail: { lastEventId: this.lastEventId }
|
|
136
|
+
detail: { lastEventId: this.lastEventId, connectionId: this.connectionId }
|
|
103
137
|
}))
|
|
104
138
|
|
|
105
139
|
const reader = response.body.getReader()
|
|
@@ -137,13 +171,17 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
137
171
|
switchToEventSource() {
|
|
138
172
|
if (this.closed) return
|
|
139
173
|
|
|
174
|
+
// Fresh connection on reconnect — drop the previous connection id so
|
|
175
|
+
// pgbus:open can't emit a stale one before the new pgbus:connected
|
|
176
|
+
// frame lands. See openFetchStream.
|
|
177
|
+
this.resetConnectionId()
|
|
140
178
|
const url = this.buildUrl({ includeSince: true })
|
|
141
179
|
this.eventSource = new EventSource(url, { withCredentials: true })
|
|
142
180
|
|
|
143
181
|
this.eventSource.addEventListener("open", () => {
|
|
144
182
|
this.setAttribute("connected", "")
|
|
145
183
|
this.dispatchEvent(new CustomEvent("pgbus:open", {
|
|
146
|
-
detail: { lastEventId: this.lastEventId }
|
|
184
|
+
detail: { lastEventId: this.lastEventId, connectionId: this.connectionId }
|
|
147
185
|
}))
|
|
148
186
|
})
|
|
149
187
|
|
|
@@ -153,7 +191,11 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
153
191
|
|
|
154
192
|
this.eventSource.addEventListener("turbo-stream", (event) => {
|
|
155
193
|
this.lastEventId = event.lastEventId
|
|
156
|
-
this.
|
|
194
|
+
this.emitTurboStream(event.data, event.lastEventId)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
this.eventSource.addEventListener("pgbus:connected", (event) => {
|
|
198
|
+
this.handleConnected(event.data)
|
|
157
199
|
})
|
|
158
200
|
|
|
159
201
|
this.eventSource.addEventListener("pgbus:gap-detected", (event) => {
|
|
@@ -166,6 +208,15 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
166
208
|
detail: { code: "shutdown", reason: "worker restart" }
|
|
167
209
|
}))
|
|
168
210
|
})
|
|
211
|
+
|
|
212
|
+
// Typed broadcasts (issue #170): EventSource only fires listeners
|
|
213
|
+
// registered by name, so we register one per declared typed event.
|
|
214
|
+
for (const name of this.declaredTypedEvents()) {
|
|
215
|
+
this.eventSource.addEventListener(name, (event) => {
|
|
216
|
+
if (event.lastEventId) this.lastEventId = event.lastEventId
|
|
217
|
+
this.emitTypedEvent(name, event.data, event.lastEventId)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
169
220
|
}
|
|
170
221
|
|
|
171
222
|
// Parses a single SSE event block (: comment | id: ... | event: ... | data: ...)
|
|
@@ -185,7 +236,9 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
185
236
|
if (id !== null) this.lastEventId = id
|
|
186
237
|
|
|
187
238
|
if (event === "turbo-stream") {
|
|
188
|
-
this.
|
|
239
|
+
this.emitTurboStream(data, id)
|
|
240
|
+
} else if (event === "pgbus:connected") {
|
|
241
|
+
this.handleConnected(data)
|
|
189
242
|
} else if (event === "pgbus:gap-detected") {
|
|
190
243
|
this.dispatchEvent(new CustomEvent("pgbus:gap-detected", {
|
|
191
244
|
detail: this.safeJsonParse(data)
|
|
@@ -194,9 +247,101 @@ class PgbusStreamSourceElement extends HTMLElement {
|
|
|
194
247
|
this.dispatchEvent(new CustomEvent("pgbus:close", {
|
|
195
248
|
detail: { code: "shutdown", reason: "worker restart" }
|
|
196
249
|
}))
|
|
250
|
+
} else {
|
|
251
|
+
// A typed broadcast (issue #170): event: presence | reactive | ...
|
|
252
|
+
this.emitTypedEvent(event, data, id)
|
|
197
253
|
}
|
|
198
254
|
}
|
|
199
255
|
|
|
256
|
+
// Captures the server-minted connection id from a `pgbus:connected`
|
|
257
|
+
// frame: stores it, reflects it onto the `connection-id` attribute (so
|
|
258
|
+
// it's visible in the DOM / to MutationObservers), and re-dispatches it
|
|
259
|
+
// as a `pgbus:connected` CustomEvent. Idempotent across reconnects — the
|
|
260
|
+
// server mints a fresh id per connection, so a reconnect updates it.
|
|
261
|
+
handleConnected(data) {
|
|
262
|
+
const detail = this.safeJsonParse(data)
|
|
263
|
+
const connectionId = detail && detail.connectionId
|
|
264
|
+
if (!connectionId) return
|
|
265
|
+
|
|
266
|
+
this.connectionId = connectionId
|
|
267
|
+
this.setAttribute("connection-id", connectionId)
|
|
268
|
+
this.dispatchEvent(new CustomEvent("pgbus:connected", {
|
|
269
|
+
detail: { connectionId }
|
|
270
|
+
}))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Clears the cached connection id and its reflected attribute. Called at
|
|
274
|
+
// the start of every transport open so a reconnect doesn't carry the
|
|
275
|
+
// previous connection's id into pgbus:open before the new pgbus:connected
|
|
276
|
+
// frame arrives.
|
|
277
|
+
resetConnectionId() {
|
|
278
|
+
this.connectionId = null
|
|
279
|
+
this.removeAttribute("connection-id")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Delivers a turbo-stream frame to two audiences (issue #168):
|
|
283
|
+
//
|
|
284
|
+
// 1. Turbo's StreamObserver, via a `message` MessageEvent whose `data`
|
|
285
|
+
// is the turbo-stream HTML. The MessageEvent's standard
|
|
286
|
+
// `lastEventId` field carries the frame's msg_id, so a reactive
|
|
287
|
+
// runtime that listens for `message` can read the revision without
|
|
288
|
+
// any pgbus-specific API. (Turbo ignores lastEventId.)
|
|
289
|
+
//
|
|
290
|
+
// 2. A reactive runtime doing optimistic-UI reconciliation, via a
|
|
291
|
+
// `pgbus:message` CustomEvent carrying { msgId, data }. msgId is the
|
|
292
|
+
// monotonic per-stream revision (a negative value marks an ephemeral
|
|
293
|
+
// frame that was never persisted). Pattern: track the highest msgId
|
|
294
|
+
// you have applied per target; when a frame arrives, skip the morph
|
|
295
|
+
// if you have already applied a newer revision for that target —
|
|
296
|
+
// this stops a late echo from clobbering a newer optimistic edit.
|
|
297
|
+
// Complements #165: exclude handles the actor; this handles
|
|
298
|
+
// out-of-order delivery for everyone else.
|
|
299
|
+
//
|
|
300
|
+
// msgId is parsed to a Number when numeric so consumers can compare
|
|
301
|
+
// revisions with `>` directly; left as-is otherwise.
|
|
302
|
+
emitTurboStream(data, id) {
|
|
303
|
+
const msgId = id === null || id === undefined || id === "" ? null
|
|
304
|
+
: (Number.isNaN(Number(id)) ? id : Number(id))
|
|
305
|
+
|
|
306
|
+
const message = new MessageEvent("message", { data, lastEventId: id == null ? "" : String(id) })
|
|
307
|
+
this.dispatchEvent(message)
|
|
308
|
+
|
|
309
|
+
this.dispatchEvent(new CustomEvent("pgbus:message", {
|
|
310
|
+
detail: { msgId, data }
|
|
311
|
+
}))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Delivers a typed broadcast (issue #170) — a frame whose SSE event name
|
|
315
|
+
// is something other than turbo-stream (e.g. "presence", "reactive").
|
|
316
|
+
// The payload is still whatever the broadcaster sent (usually a Turbo
|
|
317
|
+
// Stream); the typed name lets a client route without sniffing the HTML.
|
|
318
|
+
// Dispatched two ways for ergonomics:
|
|
319
|
+
// - a generic `pgbus:event` { event, data, msgId } (one listener for all)
|
|
320
|
+
// - a named `pgbus:<event>` { data, msgId } (addEventListener by name)
|
|
321
|
+
emitTypedEvent(event, data, id) {
|
|
322
|
+
const msgId = id === null || id === undefined || id === "" ? null
|
|
323
|
+
: (Number.isNaN(Number(id)) ? id : Number(id))
|
|
324
|
+
|
|
325
|
+
this.dispatchEvent(new CustomEvent("pgbus:event", {
|
|
326
|
+
detail: { event, data, msgId }
|
|
327
|
+
}))
|
|
328
|
+
this.dispatchEvent(new CustomEvent(`pgbus:${event}`, {
|
|
329
|
+
detail: { data, msgId }
|
|
330
|
+
}))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Typed event names the EventSource (reconnect) path should listen for,
|
|
334
|
+
// declared by the app via the `listen-events` attribute (comma- or
|
|
335
|
+
// space-separated). EventSource only invokes listeners registered by
|
|
336
|
+
// name, so unlike the fetch path it cannot route unknown typed events
|
|
337
|
+
// generically; declaring them here keeps typed delivery working across
|
|
338
|
+
// reconnects. The fetch (initial) path always routes typed events.
|
|
339
|
+
declaredTypedEvents() {
|
|
340
|
+
const raw = this.getAttribute("listen-events")
|
|
341
|
+
if (!raw) return []
|
|
342
|
+
return raw.split(/[\s,]+/).filter((name) => name && name !== "turbo-stream")
|
|
343
|
+
}
|
|
344
|
+
|
|
200
345
|
buildUrl({ includeSince }) {
|
|
201
346
|
const src = this.getAttribute("src")
|
|
202
347
|
if (!includeSince || !this.lastEventId) return src
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class BatchesController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@batches = data_source.batches
|
|
7
|
+
render_frame("pgbus/batches/batches_table") if params[:frame] == "list"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@batch = data_source.batch_detail(params[:id])
|
|
12
|
+
redirect_to batches_path, alert: t("pgbus.batches.show.not_found") unless @batch
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -84,6 +84,18 @@ module Pgbus
|
|
|
84
84
|
class: "inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800")
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
BATCH_BADGE_BASE = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
88
|
+
BATCH_BADGE_CSS = {
|
|
89
|
+
"finished" => "#{BATCH_BADGE_BASE} bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
|
90
|
+
"processing" => "#{BATCH_BADGE_BASE} bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
|
91
|
+
"pending" => "#{BATCH_BADGE_BASE} bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
def pgbus_batch_status_badge(status)
|
|
95
|
+
css = BATCH_BADGE_CSS[status] || BATCH_BADGE_CSS["pending"]
|
|
96
|
+
tag.span(I18n.t("pgbus.helpers.batch_status.#{status}", default: status), class: css)
|
|
97
|
+
end
|
|
98
|
+
|
|
87
99
|
def pgbus_parse_message(message)
|
|
88
100
|
return {} unless message
|
|
89
101
|
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
<%= pgbus_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
|
|
130
130
|
<%= pgbus_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
|
|
131
131
|
<%= pgbus_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
|
|
132
|
+
<%= pgbus_nav_link t("pgbus.layout.nav.batches"), pgbus.batches_path %>
|
|
132
133
|
<%= pgbus_nav_link t("pgbus.layout.nav.dlq"), pgbus.dead_letter_index_path %>
|
|
133
134
|
<%= pgbus_nav_link t("pgbus.layout.nav.outbox"), pgbus.outbox_index_path %>
|
|
134
135
|
<%= pgbus_nav_link t("pgbus.layout.nav.locks"), pgbus.locks_path %>
|
|
@@ -188,6 +189,7 @@
|
|
|
188
189
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
|
|
189
190
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
|
|
190
191
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
|
|
192
|
+
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.batches"), pgbus.batches_path %>
|
|
191
193
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.dlq"), pgbus.dead_letter_index_path %>
|
|
192
194
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.outbox"), pgbus.outbox_index_path %>
|
|
193
195
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.locks"), pgbus.locks_path %>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<turbo-frame id="batches-list">
|
|
2
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
3
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
4
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
5
|
+
<tr>
|
|
6
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.headers.batch_id") %></th>
|
|
7
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.headers.description") %></th>
|
|
8
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.headers.status") %></th>
|
|
9
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.headers.progress") %></th>
|
|
10
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.headers.jobs") %></th>
|
|
11
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.headers.created") %></th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
15
|
+
<% @batches.each do |batch| %>
|
|
16
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
17
|
+
<td data-label="Batch ID" class="px-4 py-3 text-sm">
|
|
18
|
+
<%= link_to batch[:batch_id][0..7], pgbus.batch_path(batch[:batch_id]),
|
|
19
|
+
class: "font-mono text-indigo-600 dark:text-indigo-400 hover:underline",
|
|
20
|
+
data: { turbo_frame: "_top" } %>
|
|
21
|
+
</td>
|
|
22
|
+
<td data-label="Description" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300 max-w-xs truncate">
|
|
23
|
+
<%= batch[:description] || "—" %>
|
|
24
|
+
</td>
|
|
25
|
+
<td data-label="Status" class="px-4 py-3 text-sm">
|
|
26
|
+
<%= pgbus_batch_status_badge(batch[:status]) %>
|
|
27
|
+
</td>
|
|
28
|
+
<td data-label="Progress" class="px-4 py-3 text-sm">
|
|
29
|
+
<div class="flex items-center space-x-2">
|
|
30
|
+
<div class="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
31
|
+
<div class="h-2 rounded-full <%= batch[:discarded_jobs].to_i > 0 ? 'bg-amber-500' : 'bg-green-500' %>"
|
|
32
|
+
style="width: <%= batch[:progress_pct] %>%"></div>
|
|
33
|
+
</div>
|
|
34
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 font-mono"><%= batch[:progress_pct] %>%</span>
|
|
35
|
+
</div>
|
|
36
|
+
</td>
|
|
37
|
+
<td data-label="Jobs" class="px-4 py-3 text-sm text-right font-mono text-gray-500 dark:text-gray-400">
|
|
38
|
+
<%= batch[:completed_jobs] %>/<%= batch[:total_jobs] %>
|
|
39
|
+
<% if batch[:discarded_jobs].to_i > 0 %>
|
|
40
|
+
<span class="text-red-500">(<%= batch[:discarded_jobs] %> dlq)</span>
|
|
41
|
+
<% end %>
|
|
42
|
+
</td>
|
|
43
|
+
<td data-label="Created" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
|
|
44
|
+
<%= pgbus_time_ago(batch[:created_at]) %>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if @batches.empty? %>
|
|
49
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.batches.index.empty") %></td></tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</turbo-frame>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="flex items-center justify-between mb-6">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.batches.index.title") %></h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.index.description") %></p>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%= render "pgbus/batches/batches_table" %>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center space-x-2 mb-2">
|
|
3
|
+
<%= link_to t("pgbus.batches.show.back"), pgbus.batches_path,
|
|
4
|
+
class: "text-sm text-indigo-600 dark:text-indigo-400 hover:underline" %>
|
|
5
|
+
</div>
|
|
6
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
7
|
+
<%= t("pgbus.batches.show.title") %>
|
|
8
|
+
<span class="font-mono text-lg"><%= @batch[:batch_id][0..7] %></span>
|
|
9
|
+
<%= pgbus_batch_status_badge(@batch[:status]) %>
|
|
10
|
+
</h1>
|
|
11
|
+
<% if @batch[:description] %>
|
|
12
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= @batch[:description] %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Progress -->
|
|
17
|
+
<div class="mb-6 overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6">
|
|
18
|
+
<h2 class="text-sm font-semibold text-gray-900 dark:text-white mb-4"><%= t("pgbus.batches.show.progress") %></h2>
|
|
19
|
+
|
|
20
|
+
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4 mb-3">
|
|
21
|
+
<div class="h-4 rounded-full transition-all duration-300 <%= @batch[:discarded_jobs].to_i > 0 ? 'bg-amber-500' : 'bg-green-500' %>"
|
|
22
|
+
style="width: <%= @batch[:progress_pct] %>%"></div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
26
|
+
<div>
|
|
27
|
+
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"><%= t("pgbus.batches.show.total_jobs") %></dt>
|
|
28
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white font-mono"><%= @batch[:total_jobs] %></dd>
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"><%= t("pgbus.batches.show.completed") %></dt>
|
|
32
|
+
<dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400 font-mono"><%= @batch[:completed_jobs] %></dd>
|
|
33
|
+
</div>
|
|
34
|
+
<div>
|
|
35
|
+
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"><%= t("pgbus.batches.show.discarded") %></dt>
|
|
36
|
+
<dd class="mt-1 text-2xl font-semibold <%= @batch[:discarded_jobs].to_i > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white' %> font-mono"><%= @batch[:discarded_jobs] %></dd>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"><%= t("pgbus.batches.show.remaining") %></dt>
|
|
40
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white font-mono"><%= @batch[:total_jobs] - @batch[:completed_jobs] - @batch[:discarded_jobs] %></dd>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Details -->
|
|
46
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
47
|
+
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
48
|
+
<h2 class="text-sm font-semibold text-gray-900 dark:text-white"><%= t("pgbus.batches.show.details") %></h2>
|
|
49
|
+
</div>
|
|
50
|
+
<dl class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
51
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
52
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.batch_id") %></dt>
|
|
53
|
+
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:batch_id] %></dd>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
56
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.created_at") %></dt>
|
|
57
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:created_at] %></dd>
|
|
58
|
+
</div>
|
|
59
|
+
<% if @batch[:finished_at] %>
|
|
60
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
61
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.finished_at") %></dt>
|
|
62
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:finished_at] %></dd>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
65
|
+
<% if @batch[:on_finish_class] %>
|
|
66
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
67
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.on_finish") %></dt>
|
|
68
|
+
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:on_finish_class] %></dd>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% if @batch[:on_success_class] %>
|
|
72
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
73
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.on_success") %></dt>
|
|
74
|
+
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:on_success_class] %></dd>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% if @batch[:on_discard_class] %>
|
|
78
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
79
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.on_discard") %></dt>
|
|
80
|
+
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:on_discard_class] %></dd>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
<% if @batch[:properties].present? %>
|
|
84
|
+
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
85
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.properties") %></dt>
|
|
86
|
+
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white sm:col-span-2 sm:mt-0 break-all"><%= @batch[:properties] %></dd>
|
|
87
|
+
</div>
|
|
88
|
+
<% end %>
|
|
89
|
+
</dl>
|
|
90
|
+
</div>
|
data/config/locales/da.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
da:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Jobbatches med fremdriftssporing og callbacks
|
|
7
|
+
empty: Ingen batches fundet
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: Batch-ID
|
|
10
|
+
created: Oprettet
|
|
11
|
+
description: Beskrivelse
|
|
12
|
+
jobs: Jobs
|
|
13
|
+
progress: Fremgang
|
|
14
|
+
status: Status
|
|
15
|
+
title: Batches
|
|
16
|
+
show:
|
|
17
|
+
back: Tilbage til batches
|
|
18
|
+
batch_id: Batch-ID
|
|
19
|
+
completed: Fuldført
|
|
20
|
+
created_at: Oprettet den
|
|
21
|
+
details: Detaljer
|
|
22
|
+
discarded: Kasseret
|
|
23
|
+
finished_at: Afsluttet den
|
|
24
|
+
not_found: Batch ikke fundet
|
|
25
|
+
on_discard: Ved kassering
|
|
26
|
+
on_finish: Ved afslutning
|
|
27
|
+
on_success: Ved succes
|
|
28
|
+
progress: Fremgang
|
|
29
|
+
properties: Egenskaber
|
|
30
|
+
remaining: Resterende
|
|
31
|
+
title: Batch
|
|
32
|
+
total_jobs: Samlet antal jobs
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: Ingen processer kører
|
|
@@ -189,6 +218,10 @@ da:
|
|
|
189
218
|
not_found: Begivenhed ikke fundet
|
|
190
219
|
title: Begivenhed %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Afsluttet
|
|
223
|
+
pending: Afventer
|
|
224
|
+
processing: Behandler
|
|
192
225
|
bulk_select_all: Vælg alle
|
|
193
226
|
bulk_select_row: Vælg %{id}
|
|
194
227
|
bulk_selected: valgt
|
|
@@ -346,6 +379,7 @@ da:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Batches
|
|
349
383
|
dashboard: Dashboard
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Begivenheder
|
data/config/locales/de.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
de:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Job-Stapel mit Fortschrittsverfolgung und Callbacks
|
|
7
|
+
empty: Keine Stapel gefunden
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: Stapel-ID
|
|
10
|
+
created: Erstellt
|
|
11
|
+
description: Beschreibung
|
|
12
|
+
jobs: Jobs
|
|
13
|
+
progress: Fortschritt
|
|
14
|
+
status: Status
|
|
15
|
+
title: Stapel
|
|
16
|
+
show:
|
|
17
|
+
back: Zurück zu Stapeln
|
|
18
|
+
batch_id: Stapel-ID
|
|
19
|
+
completed: Abgeschlossen
|
|
20
|
+
created_at: Erstellt am
|
|
21
|
+
details: Details
|
|
22
|
+
discarded: Verworfen
|
|
23
|
+
finished_at: Beendet am
|
|
24
|
+
not_found: Stapel nicht gefunden
|
|
25
|
+
on_discard: Bei Verwerfung
|
|
26
|
+
on_finish: Bei Beendigung
|
|
27
|
+
on_success: Bei Erfolg
|
|
28
|
+
progress: Fortschritt
|
|
29
|
+
properties: Eigenschaften
|
|
30
|
+
remaining: Verbleibend
|
|
31
|
+
title: Stapel
|
|
32
|
+
total_jobs: Gesamtanzahl Jobs
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: Keine Prozesse laufen
|
|
@@ -189,6 +218,10 @@ de:
|
|
|
189
218
|
not_found: Ereignis nicht gefunden
|
|
190
219
|
title: Ereignis %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Abgeschlossen
|
|
223
|
+
pending: Ausstehend
|
|
224
|
+
processing: In Bearbeitung
|
|
192
225
|
bulk_select_all: Alle auswählen
|
|
193
226
|
bulk_select_row: "%{id} auswählen"
|
|
194
227
|
bulk_selected: ausgewählt
|
|
@@ -346,6 +379,7 @@ de:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Stapel
|
|
349
383
|
dashboard: Dashboard
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Ereignisse
|
data/config/locales/en.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
en:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Job batches with progress tracking and callbacks
|
|
7
|
+
empty: No batches found
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: Batch ID
|
|
10
|
+
created: Created
|
|
11
|
+
description: Description
|
|
12
|
+
jobs: Jobs
|
|
13
|
+
progress: Progress
|
|
14
|
+
status: Status
|
|
15
|
+
title: Batches
|
|
16
|
+
show:
|
|
17
|
+
back: Back to batches
|
|
18
|
+
batch_id: Batch ID
|
|
19
|
+
completed: Completed
|
|
20
|
+
created_at: Created At
|
|
21
|
+
details: Details
|
|
22
|
+
discarded: Discarded
|
|
23
|
+
finished_at: Finished At
|
|
24
|
+
not_found: Batch not found
|
|
25
|
+
on_discard: On Discard
|
|
26
|
+
on_finish: On Finish
|
|
27
|
+
on_success: On Success
|
|
28
|
+
progress: Progress
|
|
29
|
+
properties: Properties
|
|
30
|
+
remaining: Remaining
|
|
31
|
+
title: Batch
|
|
32
|
+
total_jobs: Total Jobs
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: No processes running
|
|
@@ -189,6 +218,10 @@ en:
|
|
|
189
218
|
not_found: Event not found
|
|
190
219
|
title: Event %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Finished
|
|
223
|
+
pending: Pending
|
|
224
|
+
processing: Processing
|
|
192
225
|
bulk_select_all: Select all
|
|
193
226
|
bulk_select_row: Select %{id}
|
|
194
227
|
bulk_selected: selected
|
|
@@ -346,6 +379,7 @@ en:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Batches
|
|
349
383
|
dashboard: Dashboard
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Events
|