pgbus 0.19.4 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +97 -7
  4. data/Rakefile +6 -1
  5. data/app/controllers/pgbus/dashboard_controller.rb +2 -0
  6. data/app/controllers/pgbus/workflow_definitions_controller.rb +28 -0
  7. data/app/controllers/pgbus/workflows_controller.rb +197 -0
  8. data/app/frontend/pgbus/modules/charts.js +148 -4
  9. data/app/frontend/pgbus/modules/charts.min.js +1 -1
  10. data/app/frontend/pgbus/modules/charts.min.js.map +1 -1
  11. data/app/frontend/pgbus/style.css +1 -1
  12. data/app/helpers/pgbus/button_helper.rb +4 -2
  13. data/app/helpers/pgbus/workflow_insights_helper.rb +129 -0
  14. data/app/helpers/pgbus/workflows_helper.rb +165 -0
  15. data/app/models/pgbus/blocked_execution.rb +12 -0
  16. data/app/models/pgbus/uniqueness_key.rb +88 -3
  17. data/app/models/pgbus/workflow_entry.rb +172 -0
  18. data/app/models/pgbus/workflow_event.rb +43 -0
  19. data/app/models/pgbus/workflow_signal.rb +77 -0
  20. data/app/models/pgbus/workflow_step.rb +149 -0
  21. data/app/views/layouts/pgbus/application.html.erb +2 -0
  22. data/app/views/pgbus/dashboard/_stats_cards.html.erb +17 -0
  23. data/app/views/pgbus/workflows/_actions.html.erb +59 -0
  24. data/app/views/pgbus/workflows/_events_table.html.erb +24 -0
  25. data/app/views/pgbus/workflows/_filters.html.erb +29 -0
  26. data/app/views/pgbus/workflows/_signal_form.html.erb +31 -0
  27. data/app/views/pgbus/workflows/_signals_table.html.erb +40 -0
  28. data/app/views/pgbus/workflows/_status_filter.html.erb +12 -0
  29. data/app/views/pgbus/workflows/_steps_table.html.erb +79 -0
  30. data/app/views/pgbus/workflows/_timeline.html.erb +49 -0
  31. data/app/views/pgbus/workflows/_workflows_table.html.erb +59 -0
  32. data/app/views/pgbus/workflows/attention.html.erb +88 -0
  33. data/app/views/pgbus/workflows/definitions/_by_revision.html.erb +52 -0
  34. data/app/views/pgbus/workflows/definitions/_duration_table.html.erb +18 -0
  35. data/app/views/pgbus/workflows/definitions/_funnel.html.erb +49 -0
  36. data/app/views/pgbus/workflows/definitions/_pipeline.html.erb +16 -0
  37. data/app/views/pgbus/workflows/definitions/_step_card.html.erb +31 -0
  38. data/app/views/pgbus/workflows/definitions/_throughput_table.html.erb +32 -0
  39. data/app/views/pgbus/workflows/definitions/_window.html.erb +17 -0
  40. data/app/views/pgbus/workflows/definitions/index.html.erb +71 -0
  41. data/app/views/pgbus/workflows/definitions/not_found.html.erb +5 -0
  42. data/app/views/pgbus/workflows/definitions/show.html.erb +80 -0
  43. data/app/views/pgbus/workflows/index.html.erb +15 -0
  44. data/app/views/pgbus/workflows/not_found.html.erb +5 -0
  45. data/app/views/pgbus/workflows/show.html.erb +93 -0
  46. data/config/i18n-tasks.yml +6 -0
  47. data/config/locales/da.yml +445 -0
  48. data/config/locales/de.yml +445 -0
  49. data/config/locales/en.yml +445 -0
  50. data/config/locales/es.yml +445 -0
  51. data/config/locales/fi.yml +445 -0
  52. data/config/locales/fr.yml +445 -0
  53. data/config/locales/it.yml +445 -0
  54. data/config/locales/ja.yml +445 -0
  55. data/config/locales/nb.yml +445 -0
  56. data/config/locales/nl.yml +445 -0
  57. data/config/locales/pt.yml +445 -0
  58. data/config/locales/sv.yml +445 -0
  59. data/config/routes.rb +26 -0
  60. data/lib/generators/pgbus/add_workflow_signals_generator.rb +56 -0
  61. data/lib/generators/pgbus/add_workflows_generator.rb +54 -0
  62. data/lib/generators/pgbus/templates/add_workflow_signals.rb.erb +51 -0
  63. data/lib/generators/pgbus/templates/add_workflows.rb.erb +150 -0
  64. data/lib/generators/pgbus/templates/migration.rb.erb +127 -0
  65. data/lib/pgbus/active_job/adapter.rb +81 -3
  66. data/lib/pgbus/active_job/delivery.rb +63 -0
  67. data/lib/pgbus/active_job/executor.rb +20 -8
  68. data/lib/pgbus/concurrency/blocked_execution.rb +14 -6
  69. data/lib/pgbus/configuration/workflow_settings.rb +99 -0
  70. data/lib/pgbus/configuration.rb +64 -4
  71. data/lib/pgbus/dead_letter_header.rb +9 -1
  72. data/lib/pgbus/generators/migration_detector.rb +40 -0
  73. data/lib/pgbus/instrumentation.rb +33 -0
  74. data/lib/pgbus/integrations/appsignal/dashboard.json +157 -1
  75. data/lib/pgbus/integrations/appsignal/subscriber.rb +16 -1
  76. data/lib/pgbus/mcp/rack_app.rb +7 -2
  77. data/lib/pgbus/mcp/runner.rb +2 -1
  78. data/lib/pgbus/mcp/serialization.rb +25 -0
  79. data/lib/pgbus/mcp/server.rb +53 -10
  80. data/lib/pgbus/mcp/tools/deploy_tasks_tool.rb +1 -10
  81. data/lib/pgbus/mcp/tools/workflow_cancel_tool.rb +23 -0
  82. data/lib/pgbus/mcp/tools/workflow_definitions_tool.rb +36 -0
  83. data/lib/pgbus/mcp/tools/workflow_detail_tool.rb +38 -0
  84. data/lib/pgbus/mcp/tools/workflow_resume_at_tool.rb +32 -0
  85. data/lib/pgbus/mcp/tools/workflow_resume_tool.rb +24 -0
  86. data/lib/pgbus/mcp/tools/workflow_run_now_tool.rb +23 -0
  87. data/lib/pgbus/mcp/tools/workflow_signal_tool.rb +77 -0
  88. data/lib/pgbus/mcp/tools/workflow_skip_step_tool.rb +23 -0
  89. data/lib/pgbus/mcp/tools/workflow_stats_tool.rb +42 -0
  90. data/lib/pgbus/mcp/tools/workflows_attention_tool.rb +42 -0
  91. data/lib/pgbus/mcp/tools/workflows_tool.rb +60 -0
  92. data/lib/pgbus/mcp/workflow_insights_payload.rb +15 -0
  93. data/lib/pgbus/mcp/workflow_payloads.rb +53 -0
  94. data/lib/pgbus/mcp/write_tool.rb +134 -0
  95. data/lib/pgbus/mcp.rb +15 -0
  96. data/lib/pgbus/metrics/backends/statsd.rb +6 -0
  97. data/lib/pgbus/metrics/subscriber.rb +14 -1
  98. data/lib/pgbus/metrics/workflow_metrics.rb +75 -0
  99. data/lib/pgbus/process/consumer.rb +2 -1
  100. data/lib/pgbus/process/dispatcher.rb +39 -3
  101. data/lib/pgbus/process/worker.rb +10 -1
  102. data/lib/pgbus/recurring/schedule.rb +1 -1
  103. data/lib/pgbus/streams/presence.rb +65 -36
  104. data/lib/pgbus/uniqueness.rb +23 -6
  105. data/lib/pgbus/version.rb +1 -1
  106. data/lib/pgbus/web/data_source/workflow_actions.rb +109 -0
  107. data/lib/pgbus/web/data_source/workflow_insights.rb +99 -0
  108. data/lib/pgbus/web/data_source/workflow_list.rb +291 -0
  109. data/lib/pgbus/web/data_source.rb +126 -15
  110. data/lib/pgbus/web/workflow_action_plan.rb +62 -0
  111. data/lib/pgbus/web/workflow_insights_queries.rb +250 -0
  112. data/lib/pgbus/web/workflow_insights_report.rb +171 -0
  113. data/lib/pgbus/web/workflow_payload.rb +36 -0
  114. data/lib/pgbus/web/workflow_query.rb +82 -0
  115. data/lib/pgbus/web/workflow_state.rb +89 -0
  116. data/lib/pgbus/web/workflow_timeline.rb +130 -0
  117. data/lib/pgbus/web/workflow_window.rb +51 -0
  118. data/lib/pgbus/workflow/batch_finished_job.rb +21 -0
  119. data/lib/pgbus/workflow/dsl.rb +105 -0
  120. data/lib/pgbus/workflow/error_capture.rb +63 -0
  121. data/lib/pgbus/workflow/exception_policy.rb +170 -0
  122. data/lib/pgbus/workflow/fan_out.rb +157 -0
  123. data/lib/pgbus/workflow/fan_out_definition.rb +41 -0
  124. data/lib/pgbus/workflow/flow_control.rb +94 -0
  125. data/lib/pgbus/workflow/instance.rb +183 -0
  126. data/lib/pgbus/workflow/outside_verbs.rb +138 -0
  127. data/lib/pgbus/workflow/payload.rb +53 -0
  128. data/lib/pgbus/workflow/registry.rb +53 -0
  129. data/lib/pgbus/workflow/removed_step.rb +14 -0
  130. data/lib/pgbus/workflow/runner.rb +218 -0
  131. data/lib/pgbus/workflow/signal_matcher.rb +53 -0
  132. data/lib/pgbus/workflow/signals.rb +132 -0
  133. data/lib/pgbus/workflow/states.rb +119 -0
  134. data/lib/pgbus/workflow/step_context.rb +58 -0
  135. data/lib/pgbus/workflow/step_definition.rb +133 -0
  136. data/lib/pgbus/workflow/step_execution.rb +242 -0
  137. data/lib/pgbus/workflow/step_job.rb +28 -0
  138. data/lib/pgbus/workflow/step_outcome.rb +151 -0
  139. data/lib/pgbus/workflow/steps.rb +103 -0
  140. data/lib/pgbus/workflow/sweep.rb +156 -0
  141. data/lib/pgbus/workflow/test_helpers.rb +152 -0
  142. data/lib/pgbus/workflow/wait_timeout.rb +94 -0
  143. data/lib/pgbus/workflow/waiting.rb +82 -0
  144. data/lib/pgbus/workflow.rb +183 -0
  145. data/sig/pgbus/configuration.rbs +23 -0
  146. metadata +92 -1
@@ -0,0 +1,88 @@
1
+ <%# The needs-attention view (issue #542): every ongoing run with a stuck
2
+ reason, grouped by (class, step, reason, error class). Each group has bulk
3
+ Resume and Cancel (as its runs' status allows) with an optional reason,
4
+ and a link to the filtered list. Not auto-refreshed: a typed reason stays. %>
5
+ <div class="flex flex-wrap items-start justify-between gap-4 mb-6">
6
+ <div>
7
+ <div class="mb-2"><%= pgbus_link_to t("pgbus.workflows.attention.back"), pgbus.workflows_path %></div>
8
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflows.attention.title") %></h1>
9
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.description") %></p>
10
+ <% if @workflow_class %>
11
+ <p class="mt-2 flex flex-wrap items-center gap-2 text-sm text-gray-700 dark:text-gray-300" data-testid="attention-class-filter">
12
+ <span><%= t("pgbus.workflows.attention.only_class_html", workflow: tag.code(@workflow_class, class: "font-mono")) %></span>
13
+ <%= pgbus_link_to t("pgbus.workflows.attention.show_all"), pgbus.attention_workflows_path %>
14
+ </p>
15
+ <% end %>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
20
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
21
+ <thead class="bg-gray-50 dark:bg-gray-900">
22
+ <tr>
23
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.workflow") %></th>
24
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.reason") %></th>
25
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.runs") %></th>
26
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.seen") %></th>
27
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.actions") %></th>
28
+ </tr>
29
+ </thead>
30
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
31
+ <% @groups.each_with_index do |group, index| %>
32
+ <% actions = group[:available_actions] & %i[resume cancel] %>
33
+ <% list_query = { status: group[:status], workflow_class: group[:workflow_class], step: group[:step_name], error_class: group[:error_class] }.compact %>
34
+ <tr data-reason="<%= group[:reason] %>" class="align-top hover:bg-gray-50 dark:hover:bg-gray-700/50">
35
+ <td data-label="<%= t("pgbus.workflows.attention.headers.workflow") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white">
36
+ <span class="block font-medium"><%= group[:workflow_class] %></span>
37
+ <span class="block text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.attention.at_step_html", step: tag.code(group[:step_name], class: "font-mono")) %></span>
38
+ </td>
39
+ <td data-label="<%= t("pgbus.workflows.attention.headers.reason") %>" class="px-4 py-3 text-sm max-w-md">
40
+ <span class="block text-gray-900 dark:text-white"><%= t("pgbus.workflows.attention.reasons.#{group[:reason]}") %></span>
41
+ <% if group[:error_class] %>
42
+ <span class="mt-1 block font-mono text-xs text-red-700 dark:text-red-300"><%= group[:error_class] %></span>
43
+ <% end %>
44
+ <% if group[:error_message].present? %>
45
+ <span class="block text-xs text-gray-700 dark:text-gray-300 break-words"><%= group[:error_message] %></span>
46
+ <% end %>
47
+ </td>
48
+ <td data-label="<%= t("pgbus.workflows.attention.headers.runs") %>" class="px-4 py-3 text-right font-mono text-sm text-gray-900 dark:text-white"><%= number_with_delimiter(group[:count]) %></td>
49
+ <td data-label="<%= t("pgbus.workflows.attention.headers.seen") %>" class="px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
50
+ <span class="block"><%= t("pgbus.workflows.attention.first_seen_html", time: pgbus_time(group[:first_seen_at])) %></span>
51
+ <span class="block"><%= t("pgbus.workflows.attention.last_seen_html", time: pgbus_time(group[:last_seen_at])) %></span>
52
+ </td>
53
+ <td data-label="<%= t("pgbus.workflows.attention.headers.actions") %>" class="px-4 py-3 text-sm">
54
+ <% if actions.any? %>
55
+ <form action="<%= pgbus.bulk_cancel_workflows_path %>" method="post" data-turbo-frame="_top" class="space-y-2">
56
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token, id: nil %>
57
+ <% if @workflow_class %>
58
+ <input type="hidden" name="workflow_class" value="<%= @workflow_class %>">
59
+ <% end %>
60
+ <% Pgbus::WorkflowsController::GROUP_FIELDS.each do |field| %>
61
+ <input type="hidden" name="group[<%= field %>]" value="<%= group[field] %>">
62
+ <% end %>
63
+ <label for="attention-reason-<%= index %>" class="block text-xs font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.show.reason_label") %></label>
64
+ <input type="text" autocomplete="off" id="attention-reason-<%= index %>" name="reason" maxlength="1000" class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %>">
65
+ <div class="flex flex-wrap gap-2">
66
+ <% if actions.include?(:resume) %>
67
+ <%= pgbus_button_tag t("pgbus.workflows.attention.resume_all", count: group[:count]), variant: :success, size: :sm,
68
+ formaction: pgbus.bulk_resume_workflows_path, confirm: pgbus_workflow_bulk_confirm(:resume, group) %>
69
+ <% end %>
70
+ <% if actions.include?(:cancel) %>
71
+ <%= pgbus_button_tag t("pgbus.workflows.attention.cancel_all", count: group[:count]), variant: :danger, size: :sm,
72
+ formaction: pgbus.bulk_cancel_workflows_path, confirm: pgbus_workflow_bulk_confirm(:cancel, group) %>
73
+ <% end %>
74
+ </div>
75
+ </form>
76
+ <% end %>
77
+ <div class="mt-2"><%= pgbus_link_to t("pgbus.workflows.attention.show_runs"), pgbus.workflows_path(list_query), size: :sm %></div>
78
+ </td>
79
+ </tr>
80
+ <% end %>
81
+ <% if @groups.empty? %>
82
+ <%= render "pgbus/shared/empty_row", colspan: 5,
83
+ message: safe_join([content_tag(:p, t("pgbus.workflows.attention.empty_title"), class: "text-lg font-medium"),
84
+ content_tag(:p, t("pgbus.workflows.attention.empty_hint"), class: "mt-1")]) %>
85
+ <% end %>
86
+ </tbody>
87
+ </table>
88
+ </div>
@@ -0,0 +1,52 @@
1
+ <%# The numbers per app revision (issue #549): the last five revisions that
2
+ finished an attempt in the window, newest first, each with its totals and
3
+ its steps. A failure rate more than five points above the same row on the
4
+ revision before says so. Locals: definition. %>
5
+ <% revisions = definition[:by_revision] %>
6
+ <section class="mb-8" aria-labelledby="workflow-by-revision-title">
7
+ <h2 id="workflow-by-revision-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.show.by_revision.title") %></h2>
8
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.by_revision.caption") %></p>
9
+ <div class="mt-3 rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-testid="workflow-by-revision">
10
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
11
+ <thead class="bg-gray-50 dark:bg-gray-900">
12
+ <tr>
13
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.revision") %></th>
14
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.step") %></th>
15
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.attempts") %></th>
16
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.failed") %></th>
17
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.reattempted") %></th>
18
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.skipped") %></th>
19
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.by_revision.headers.run_time") %></th>
20
+ </tr>
21
+ </thead>
22
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
23
+ <% revisions.each_with_index do |revision, index| %>
24
+ <% [nil, *revision[:steps]].each do |step| %>
25
+ <% row = step || revision %>
26
+ <% worse = pgbus_insight_worse(revisions, index, step) %>
27
+ <tr data-revision="<%= revision[:revision] %>" data-step="<%= step ? step[:name] : "" %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
28
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.revision") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
29
+ <code class="font-mono text-gray-900 dark:text-white"><%= revision[:revision] %></code>
30
+ <% unless step %>
31
+ <span class="block text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.by_revision.seen_html", from: pgbus_time(revision[:first_seen_at]), to: pgbus_time(revision[:last_seen_at])) %></span>
32
+ <% end %>
33
+ </td>
34
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.step") %>" class="px-4 py-3 text-sm <%= step ? 'font-mono text-gray-700 dark:text-gray-300' : 'font-medium text-gray-900 dark:text-white' %>"><%= step ? step[:name] : t("pgbus.workflow_insights.show.by_revision.all_steps") %></td>
35
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.attempts") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:attempts]) %></td>
36
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.failed") %>" class="px-4 py-3 text-sm text-right font-mono <%= worse ? 'text-red-700 dark:text-red-300' : 'text-gray-700 dark:text-gray-300' %>">
37
+ <%= pgbus_insight_rate(row[:rates][:failed]) %>
38
+ <% if worse %><span class="block text-xs"><%= worse %></span><% end %>
39
+ </td>
40
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.reattempted") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_insight_rate(row[:rates][:reattempted]) %></td>
41
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.skipped") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_insight_rate(row[:rates][:skipped]) %></td>
42
+ <td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.run_time") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_insight_percentiles(row[:run_ms], %i[p50 p95]) %></td>
43
+ </tr>
44
+ <% end %>
45
+ <% end %>
46
+ <% if revisions.empty? %>
47
+ <%= render "pgbus/shared/empty_row", colspan: 7, message: t("pgbus.workflow_insights.show.by_revision.empty") %>
48
+ <% end %>
49
+ </tbody>
50
+ </table>
51
+ </div>
52
+ </section>
@@ -0,0 +1,18 @@
1
+ <%# The duration chart's data table (issue #549): the finished runs per
2
+ end-to-end duration range. Locals: definition. %>
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.workflow_insights.show.charts.durations.headers.range") %></th>
7
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.durations.headers.runs") %></th>
8
+ </tr>
9
+ </thead>
10
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
11
+ <% definition[:duration_histogram].each do |bin| %>
12
+ <tr>
13
+ <td data-label="<%= t("pgbus.workflow_insights.show.charts.durations.headers.range") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_insight_bin_label(bin) %></td>
14
+ <td data-label="<%= t("pgbus.workflow_insights.show.charts.durations.headers.runs") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(bin[:count]) %></td>
15
+ </tr>
16
+ <% end %>
17
+ </tbody>
18
+ </table>
@@ -0,0 +1,49 @@
1
+ <%# The funnel (issue #549): of the runs started in the window, how many
2
+ reached each step (have a row for it), and where each run is now, by
3
+ status, each count linked to those runs. Locals: definition. %>
4
+ <% name = definition[:workflow_class] %>
5
+ <% since = definition[:window][:since] %>
6
+ <% started = definition[:started] %>
7
+ <section class="mb-8" aria-labelledby="workflow-funnel-title">
8
+ <h2 id="workflow-funnel-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.show.funnel.title") %></h2>
9
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.funnel.caption", count: started) %></p>
10
+ <div class="mt-3 rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-testid="workflow-funnel">
11
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
12
+ <thead class="bg-gray-50 dark:bg-gray-900">
13
+ <tr>
14
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.funnel.headers.step") %></th>
15
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.funnel.headers.reached") %></th>
16
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.funnel.headers.share") %></th>
17
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.funnel.headers.now_here") %></th>
18
+ </tr>
19
+ </thead>
20
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
21
+ <% if started.positive? %>
22
+ <% definition[:funnel].each do |step| %>
23
+ <% here = Pgbus::WorkflowInsightsHelper::STOPPED_STATUSES.select { |status| step[:stopped][status.to_sym].positive? } %>
24
+ <tr data-step="<%= step[:name] %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
25
+ <td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.step") %>" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= step[:name] %></td>
26
+ <td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.reached") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(step[:reached]) %></td>
27
+ <td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.share") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_percent(step[:reached].to_f / started) %></td>
28
+ <td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.now_here") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
29
+ <% if here.empty? %>
30
+ <%= Pgbus::Web::TimeFormat::NONE %>
31
+ <% else %>
32
+ <ul class="flex flex-wrap gap-x-3">
33
+ <% here.each do |status| %>
34
+ <li><%= pgbus_link_to t("pgbus.workflow_insights.show.funnel.here.#{status}", count: step[:stopped][status.to_sym]),
35
+ pgbus_insight_runs_path(status: status, workflow_class: name, step: step[:name], created_from: since, created_to: @window.now),
36
+ data: { turbo_frame: "_top" } %></li>
37
+ <% end %>
38
+ </ul>
39
+ <% end %>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ <% else %>
44
+ <%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.workflow_insights.show.funnel.empty") %>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+ </div>
49
+ </section>
@@ -0,0 +1,16 @@
1
+ <%# The pipeline (issue #549): the class's steps in order, one card each,
2
+ side by side and scrolled sideways when they do not fit. Locals:
3
+ definition. %>
4
+ <section class="mb-8" aria-labelledby="workflow-pipeline-title">
5
+ <h2 id="workflow-pipeline-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.show.pipeline.title") %></h2>
6
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.caption") %></p>
7
+ <% if definition[:steps].empty? %>
8
+ <p class="mt-3 rounded-lg bg-white dark:bg-gray-800 p-6 text-center text-sm text-gray-500 dark:text-gray-400 shadow ring-1 ring-gray-200 dark:ring-gray-700"><%= t("pgbus.workflow_insights.show.pipeline.empty") %></p>
9
+ <% else %>
10
+ <ol class="mt-3 flex gap-4 overflow-x-auto pb-2" data-testid="workflow-pipeline" tabindex="0" aria-labelledby="workflow-pipeline-title">
11
+ <% definition[:steps].each_with_index do |step, index| %>
12
+ <%= render "pgbus/workflows/definitions/step_card", step: step, position: index + 1, name: definition[:workflow_class] %>
13
+ <% end %>
14
+ </ol>
15
+ <% end %>
16
+ </section>
@@ -0,0 +1,31 @@
1
+ <%# One step of the pipeline (issue #549): the runs sitting at it now, each
2
+ count linked to those runs, then how its attempts that finished in the
3
+ window did. Locals: step, position, name (the workflow class). %>
4
+ <li data-step="<%= step[:name] %>" class="w-72 shrink-0 rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
5
+ <div class="flex items-start justify-between gap-2">
6
+ <h3 class="text-sm font-medium text-gray-900 dark:text-white">
7
+ <span class="text-gray-600 dark:text-gray-300"><%= position %>.</span>
8
+ <%= pgbus_link_to step[:name], pgbus_insight_runs_path(workflow_class: name, step: step[:name]), class: "font-mono",
9
+ aria: { label: t("pgbus.workflow_insights.show.pipeline.step_link", step: step[:name]) } %>
10
+ </h3>
11
+ <%= pgbus_insight_kind(step) %>
12
+ </div>
13
+ <ul class="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-sm" aria-label="<%= t("pgbus.workflow_insights.show.pipeline.now_label") %>">
14
+ <% Pgbus::WorkflowInsightsHelper::LIVE_STATUSES.each do |status| %>
15
+ <% count = step[:counts][status.to_sym] %>
16
+ <li class="flex items-center justify-between gap-2">
17
+ <span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.live.#{status}") %></span>
18
+ <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_count(count, pgbus_insight_runs_path(status: status, workflow_class: name, step: step[:name]),
19
+ t("pgbus.workflow_insights.links.at_step", status: t("pgbus.workflow_insights.show.pipeline.live.#{status}").downcase, step: step[:name])) %></span>
20
+ </li>
21
+ <% end %>
22
+ </ul>
23
+ <ul class="mt-3 space-y-1 border-t border-gray-200 pt-3 text-sm dark:border-gray-700" aria-label="<%= t("pgbus.workflow_insights.show.pipeline.window_label") %>">
24
+ <li><span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.run_time") %></span> <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_percentiles(step[:run_ms]) %></span></li>
25
+ <li><span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.pickup") %></span> <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_percentiles(step[:pickup_ms], %i[p50 p95]) %></span></li>
26
+ <% %i[failed reattempted skipped].each do |rate| %>
27
+ <li><span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.#{rate}") %></span> <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_rate(step[:rates][rate]) %></span></li>
28
+ <% end %>
29
+ <li class="text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.attempts", count: step[:sample_size]) %></li>
30
+ </ul>
31
+ </li>
@@ -0,0 +1,32 @@
1
+ <%# The throughput chart's data table (issue #549): one row per bucket, each
2
+ count linked to those runs (from the bucket's first moment to just before
3
+ the next one's, so a 23- or 25-hour DST day links exactly). Locals: definition. %>
4
+ <% name = definition[:workflow_class] %>
5
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
6
+ <thead class="bg-gray-50 dark:bg-gray-900">
7
+ <tr>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.from") %></th>
9
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.started") %></th>
10
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.finished") %></th>
11
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.cancelled") %></th>
12
+ </tr>
13
+ </thead>
14
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
15
+ <% definition[:throughput].each do |bucket| %>
16
+ <% from = bucket[:from] %>
17
+ <% to = bucket[:to] - Rational(1, 1_000_000) %>
18
+ <tr>
19
+ <td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.from") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_absolute_time(from) %></td>
20
+ <td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.started") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
21
+ <%= pgbus_insight_count(bucket[:started], pgbus_insight_runs_path(status: "all", workflow_class: name, created_from: from, created_to: to), t("pgbus.workflow_insights.links.started_bucket")) %>
22
+ </td>
23
+ <td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.finished") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
24
+ <%= pgbus_insight_count(bucket[:finished], pgbus_insight_runs_path(status: "finished", workflow_class: name, ended_from: from, ended_to: to), t("pgbus.workflow_insights.links.finished_bucket")) %>
25
+ </td>
26
+ <td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.cancelled") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
27
+ <%= pgbus_insight_count(bucket[:cancelled], pgbus_insight_runs_path(status: "cancelled", workflow_class: name, ended_from: from, ended_to: to), t("pgbus.workflow_insights.links.cancelled_bucket")) %>
28
+ </td>
29
+ </tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
@@ -0,0 +1,17 @@
1
+ <%# The window selector (issue #549): 1h, 24h, 7d, 30d. Locals: path, a
2
+ lambda from a window name to the same page over that window. A window
3
+ longer than workflow_retention is capped, and the page says so. %>
4
+ <div>
5
+ <nav class="flex flex-wrap gap-2" aria-label="<%= t("pgbus.workflow_insights.window_label") %>" data-testid="workflow-insights-window">
6
+ <% Pgbus::Web::WorkflowWindow::CHOICES.each_key do |name| %>
7
+ <% active = name == @window.name %>
8
+ <a href="<%= path.call(name) %>" aria-current="<%= active ? "page" : "false" %>"
9
+ class="inline-flex items-center rounded-md px-3 py-2 text-sm font-medium <%= active ? 'bg-gray-800 text-white dark:bg-gray-700' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white' %>">
10
+ <%= name %>
11
+ </a>
12
+ <% end %>
13
+ </nav>
14
+ <% if @window.capped? %>
15
+ <p class="mt-2 text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.capped", retention: pgbus_duration(@window.seconds)) %></p>
16
+ <% end %>
17
+ </div>
@@ -0,0 +1,71 @@
1
+ <%# The workflow insights' class list (issue #549): every registered
2
+ workflow class and every class still in the rows, with its runs going on
3
+ now, the runs that ended in the window and how long a run takes end to
4
+ end. Each count links to the Workflows list filtered to those runs. %>
5
+ <div class="flex flex-wrap items-start justify-between gap-4 mb-6">
6
+ <div>
7
+ <div class="mb-2"><%= pgbus_link_to t("pgbus.workflow_insights.back_to_runs"), pgbus.workflows_path %></div>
8
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.index.title") %></h1>
9
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.index.description", window: pgbus_insight_window_name(@window)) %></p>
10
+ </div>
11
+ <%= render "pgbus/workflows/definitions/window", path: ->(name) { pgbus.workflow_definitions_path(window: name) } %>
12
+ </div>
13
+
14
+ <%# The window's own lower bound: the one the counts were read with. %>
15
+ <% since = @window.since %>
16
+ <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
17
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
18
+ <thead class="bg-gray-50 dark:bg-gray-900">
19
+ <tr>
20
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.workflow_class") %></th>
21
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.ongoing") %></th>
22
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.paused") %></th>
23
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.attention") %></th>
24
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.finished") %></th>
25
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.cancelled") %></th>
26
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.duration_p50") %></th>
27
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.duration_p95") %></th>
28
+ </tr>
29
+ </thead>
30
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
31
+ <% @definitions.each do |row| %>
32
+ <% name = row[:workflow_class] %>
33
+ <tr data-workflow-class="<%= name %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
34
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.workflow_class") %>" class="px-4 py-3 text-sm">
35
+ <div><%= pgbus_link_to name, pgbus_insight_definition_path(name, @window), class: "font-mono" %></div>
36
+ <div class="mt-1 flex flex-wrap gap-1">
37
+ <%= pgbus_insight_not_loaded_badge unless row[:loaded] %>
38
+ <%= pgbus_insight_removed_links(name, row[:removed_in_flight]) %>
39
+ </div>
40
+ </td>
41
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.ongoing") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
42
+ <%= pgbus_insight_count(row[:ongoing], pgbus_insight_runs_path(workflow_class: name), t("pgbus.workflow_insights.links.ongoing", workflow: name)) %>
43
+ </td>
44
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.paused") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
45
+ <%= pgbus_insight_count(row[:paused], pgbus_insight_runs_path(status: "paused", workflow_class: name), t("pgbus.workflow_insights.links.paused", workflow: name)) %>
46
+ </td>
47
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.attention") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
48
+ <%= pgbus_insight_count(row[:attention], pgbus.attention_workflows_path(workflow_class: name), t("pgbus.workflow_insights.links.attention", workflow: name)) %>
49
+ </td>
50
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.finished") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
51
+ <%= pgbus_insight_count(row[:finished], pgbus_insight_runs_path(status: "finished", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.finished", workflow: name)) %>
52
+ </td>
53
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.cancelled") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
54
+ <%= pgbus_insight_count(row[:cancelled], pgbus_insight_runs_path(status: "cancelled", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.cancelled", workflow: name)) %>
55
+ </td>
56
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.duration_p50") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
57
+ <%= pgbus_insight_ms(row[:e2e_ms][:p50]) %>
58
+ </td>
59
+ <td data-label="<%= t("pgbus.workflow_insights.index.headers.duration_p95") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
60
+ <%= pgbus_insight_ms(row[:e2e_ms][:p95]) %>
61
+ </td>
62
+ </tr>
63
+ <% end %>
64
+ <% if @definitions.empty? %>
65
+ <%= render "pgbus/shared/empty_row", colspan: 8,
66
+ message: safe_join([content_tag(:p, t("pgbus.workflow_insights.index.empty_title"), class: "text-lg font-medium"),
67
+ content_tag(:p, t("pgbus.workflow_insights.index.empty_hint"), class: "mt-1")]) %>
68
+ <% end %>
69
+ </tbody>
70
+ </table>
71
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="mb-6">
2
+ <div class="mb-2"><%= pgbus_link_to t("pgbus.workflow_insights.back_to_classes"), pgbus.workflow_definitions_path(window: @window.name) %></div>
3
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.not_found.title") %></h1>
4
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.not_found.hint", workflow: params[:workflow_class]) %></p>
5
+ </div>
@@ -0,0 +1,80 @@
1
+ <%# One workflow class's insights (issue #549) over a window: the runs it
2
+ started and ended, its pipeline (each step with the runs at it now and
3
+ how its attempts did), the throughput and duration charts, the funnel of
4
+ the runs started in the window and the same numbers per app revision. %>
5
+ <% definition = @definition %>
6
+ <% name = definition[:workflow_class] %>
7
+ <% since = definition[:window][:since] %>
8
+ <div class="flex flex-wrap items-start justify-between gap-4 mb-6">
9
+ <div>
10
+ <div class="mb-2"><%= pgbus_link_to t("pgbus.workflow_insights.back_to_classes"), pgbus.workflow_definitions_path(window: @window.name) %></div>
11
+ <h1 class="text-2xl font-bold font-mono text-gray-900 dark:text-white"><%= name %></h1>
12
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.description", window: pgbus_insight_window_name(@window)) %></p>
13
+ <div class="mt-2 flex flex-wrap gap-1">
14
+ <%= pgbus_insight_not_loaded_badge unless definition[:loaded] %>
15
+ <%= pgbus_insight_removed_links(name, definition[:removed_in_flight]) %>
16
+ </div>
17
+ <% unless definition[:loaded] %>
18
+ <p class="mt-2 max-w-2xl text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.not_loaded_hint") %></p>
19
+ <% end %>
20
+ </div>
21
+ <%= render "pgbus/workflows/definitions/window", path: ->(window) { pgbus.workflow_definition_path(name, window: window) } %>
22
+ </div>
23
+
24
+ <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6 mb-8" data-testid="workflow-insights-summary">
25
+ <% summary = {
26
+ started: pgbus_insight_count(definition[:started], pgbus_insight_runs_path(status: "all", workflow_class: name, created_from: since, created_to: @window.now), t("pgbus.workflow_insights.links.started", workflow: name), size: :lg),
27
+ finished: pgbus_insight_count(definition[:finished], pgbus_insight_runs_path(status: "finished", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.finished", workflow: name), size: :lg),
28
+ cancelled: pgbus_insight_count(definition[:cancelled], pgbus_insight_runs_path(status: "cancelled", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.cancelled", workflow: name), size: :lg),
29
+ ongoing: pgbus_insight_count(definition[:ongoing], pgbus_insight_runs_path(workflow_class: name), t("pgbus.workflow_insights.links.ongoing", workflow: name), size: :lg),
30
+ attention: pgbus_insight_count(definition[:attention], pgbus.attention_workflows_path(workflow_class: name), t("pgbus.workflow_insights.links.attention", workflow: name), size: :lg)
31
+ } %>
32
+ <% summary.each do |key, value| %>
33
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
34
+ <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.summary.#{key}") %></dt>
35
+ <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= value %></dd>
36
+ </dl>
37
+ <% end %>
38
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
39
+ <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.summary.duration") %></dt>
40
+ <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_insight_ms(definition[:e2e_ms][:p50]) %></dd>
41
+ <dd class="mt-1 text-xs text-gray-600 dark:text-gray-300"><%= pgbus_insight_percentiles(definition[:e2e_ms], %i[p95 p99]) %></dd>
42
+ </dl>
43
+ </div>
44
+
45
+ <%= render "pgbus/workflows/definitions/pipeline", definition: definition %>
46
+
47
+ <% payload = pgbus_insight_chart_payload(definition) %>
48
+ <% totals = definition[:throughput].each_with_object(Hash.new(0)) { |bucket, sums| %i[started finished cancelled].each { |key| sums[key] += bucket[key] } } %>
49
+ <% bucket = pgbus_insight_span(definition[:bucket_seconds]) %>
50
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-2 mb-8">
51
+ <%= render layout: "pgbus/insights/chart_card",
52
+ locals: { id: "workflow-throughput-chart", title: t("pgbus.workflow_insights.show.charts.throughput.title", bucket: bucket),
53
+ caption: t("pgbus.workflow_insights.show.charts.throughput.caption", bucket: bucket, window: pgbus_insight_window_name(@window),
54
+ started: totals[:started], finished: totals[:finished], cancelled: totals[:cancelled]),
55
+ empty: totals.values.sum.zero? } do %>
56
+ <%= render "pgbus/workflows/definitions/throughput_table", definition: definition %>
57
+ <% end %>
58
+ <% finished = definition[:duration_histogram].sum { |bin| bin[:count] } %>
59
+ <%= render layout: "pgbus/insights/chart_card",
60
+ locals: { id: "workflow-duration-chart", title: t("pgbus.workflow_insights.show.charts.durations.title"),
61
+ caption: t("pgbus.workflow_insights.show.charts.durations.caption", count: finished),
62
+ empty: finished.zero? } do %>
63
+ <%= render "pgbus/workflows/definitions/duration_table", definition: definition %>
64
+ <% end %>
65
+ </div>
66
+
67
+ <%= render "pgbus/workflows/definitions/funnel", definition: definition %>
68
+ <%= render "pgbus/workflows/definitions/by_revision", definition: definition %>
69
+
70
+ <%# The chart series, embedded once; json_escape keeps a value from closing the tag. %>
71
+ <%= tag.script(json_escape(payload.to_json).html_safe, type: "application/json", id: "pgbus-workflow-insights-data") %> <%# herb:disable erb-no-unsafe-raw %>
72
+ <script type="module" nonce="<%= content_security_policy_nonce %>">
73
+ import { renderWorkflowInsights, observeThemeChanges } from "charts";
74
+
75
+ const i18n = <%= json_escape(pgbus_insight_chart_i18n.to_json).html_safe %>;
76
+ const data = JSON.parse(document.getElementById("pgbus-workflow-insights-data").textContent);
77
+
78
+ renderWorkflowInsights(data, i18n);
79
+ observeThemeChanges(() => data, i18n, renderWorkflowInsights);
80
+ </script>
@@ -0,0 +1,15 @@
1
+ <div class="flex flex-wrap items-start justify-between gap-4 mb-6">
2
+ <div>
3
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflows.index.title") %></h1>
4
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.index.description") %></p>
5
+ </div>
6
+ <div class="flex flex-wrap items-center gap-2">
7
+ <%= pgbus_link_to t("pgbus.workflows.index.insights_link"), pgbus.workflow_definitions_path, variant: :secondary %>
8
+ <%= pgbus_link_to t("pgbus.workflows.index.attention_link", count: @attention_count), pgbus.attention_workflows_path,
9
+ variant: @attention_count.positive? ? :warning : :secondary %>
10
+ </div>
11
+ </div>
12
+
13
+ <%= render "pgbus/workflows/status_filter" %>
14
+ <%= render "pgbus/workflows/filters" %>
15
+ <%= render "pgbus/workflows/workflows_table" %>
@@ -0,0 +1,5 @@
1
+ <div class="mb-6">
2
+ <div class="mb-2"><%= pgbus_link_to t("pgbus.workflows.show.back"), pgbus.workflows_path %></div>
3
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflows.not_found.title") %></h1>
4
+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.not_found.hint") %></p>
5
+ </div>
@@ -0,0 +1,93 @@
1
+ <% result = pgbus_workflow_state(@workflow) %>
2
+ <% card = "rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" %>
3
+ <div class="mb-6">
4
+ <div class="mb-2"><%= pgbus_link_to t("pgbus.workflows.show.back"), pgbus.workflows_path %></div>
5
+ <div class="flex flex-wrap items-center gap-3">
6
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= @workflow[:workflow_class] %></h1>
7
+ <%= pgbus_workflow_badge(result) %>
8
+ </div>
9
+ <p class="mt-1 font-mono text-xs text-gray-500 dark:text-gray-400"><%= @workflow[:workflow_id] %></p>
10
+ <p class="mt-2 text-sm text-gray-700 dark:text-gray-300" data-testid="workflow-next"><%= pgbus_workflow_next(@workflow, result) %></p>
11
+ <% if @workflow[:definition_outdated] %>
12
+ <p class="mt-3 rounded-md bg-yellow-50 px-3 py-2 text-sm text-yellow-800 ring-1 ring-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-200 dark:ring-yellow-800">
13
+ <%= t("pgbus.workflows.show.definition_outdated", workflow_class: @workflow[:workflow_class]) %>
14
+ </p>
15
+ <% end %>
16
+ <% unless @workflow[:class_loaded] %>
17
+ <p class="mt-3 rounded-md bg-gray-50 px-3 py-2 text-sm text-gray-700 ring-1 ring-gray-200 dark:bg-gray-900/40 dark:text-gray-200 dark:ring-gray-700">
18
+ <%= t("pgbus.workflows.show.class_missing", workflow_class: @workflow[:workflow_class]) %>
19
+ </p>
20
+ <% end %>
21
+ </div>
22
+
23
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-8">
24
+ <section class="<%= card %> lg:col-span-2" aria-labelledby="workflow-details-title">
25
+ <h2 id="workflow-details-title" class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.show.details") %></h2>
26
+ <dl class="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2">
27
+ <div>
28
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.subject") %></dt>
29
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_workflow_subject(@workflow) %></dd>
30
+ </div>
31
+ <div>
32
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.current_step") %></dt>
33
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white"><code class="font-mono"><%= @workflow[:current_step] || Pgbus::Web::TimeFormat::NONE %></code></dd>
34
+ </div>
35
+ <div>
36
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.started") %></dt>
37
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@workflow[:created_at]) %></dd>
38
+ </div>
39
+ <div>
40
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.last_activity") %></dt>
41
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@workflow[:updated_at]) %></dd>
42
+ </div>
43
+ <% { paused_at: :paused_at, finished_at: :finished_at, cancelled_at: :cancelled_at }.each do |field, label| %>
44
+ <% next unless @workflow[field] %>
45
+ <div>
46
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.#{label}") %></dt>
47
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@workflow[field]) %></dd>
48
+ </div>
49
+ <% end %>
50
+ <% if @workflow[:rerun_of_workflow_id] %>
51
+ <div>
52
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.rerun_of") %></dt>
53
+ <dd class="mt-1 text-sm font-mono"><%= pgbus_link_to @workflow[:rerun_of_workflow_id][0, 8], pgbus.workflow_path(@workflow[:rerun_of_workflow_id]) %></dd>
54
+ </div>
55
+ <% end %>
56
+ <% if @workflow[:reruns].any? %>
57
+ <div>
58
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.reruns") %></dt>
59
+ <dd class="mt-1 space-y-1 text-sm text-gray-900 dark:text-white">
60
+ <% @workflow[:reruns].each do |rerun| %>
61
+ <span class="block"><%= pgbus_link_to rerun[:workflow_id][0, 8], pgbus.workflow_path(rerun[:workflow_id]), class: "font-mono" %>
62
+ · <%= t("pgbus.workflows.statuses.#{rerun[:status]}") %> · <%= pgbus_time(rerun[:created_at]) %></span>
63
+ <% end %>
64
+ </dd>
65
+ </div>
66
+ <% end %>
67
+ </dl>
68
+ </section>
69
+
70
+ <section class="<%= card %>" aria-labelledby="workflow-arguments-title" data-testid="workflow-arguments">
71
+ <h2 id="workflow-arguments-title" class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.show.arguments") %></h2>
72
+ <% arguments = Pgbus::Web::PayloadFilter.filter(@workflow[:arguments] || {}) %>
73
+ <% if arguments.empty? %>
74
+ <p class="p-4 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.no_arguments") %></p>
75
+ <% else %>
76
+ <dl class="space-y-3 p-4">
77
+ <% arguments.each do |key, value| %>
78
+ <div>
79
+ <dt class="font-mono text-xs font-medium text-gray-500 dark:text-gray-400"><%= key %></dt>
80
+ <dd class="mt-0.5 break-all font-mono text-sm text-gray-900 dark:text-white"><%= pgbus_workflow_value(value) %></dd>
81
+ </div>
82
+ <% end %>
83
+ </dl>
84
+ <% end %>
85
+ </section>
86
+ </div>
87
+
88
+ <%= render "pgbus/workflows/actions" %>
89
+ <%= render "pgbus/workflows/signal_form" if @workflow[:available_actions].include?(:signal) %>
90
+ <%= render "pgbus/workflows/timeline" %>
91
+ <%= render "pgbus/workflows/steps_table" %>
92
+ <%= render "pgbus/workflows/signals_table" %>
93
+ <%= render "pgbus/workflows/events_table" %>