ductwork 1.3.0 → 1.5.0

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/audit-common/accepted-tradeoffs.md +0 -5
  3. data/CHANGELOG.md +19 -0
  4. data/app/assets/javascripts/ductwork/application.js +50 -41
  5. data/app/assets/stylesheets/ductwork/application.css +859 -146
  6. data/app/controllers/ductwork/application_controller.rb +39 -8
  7. data/app/controllers/ductwork/dashboards_controller.rb +44 -11
  8. data/app/controllers/ductwork/pipelines_controller.rb +47 -7
  9. data/app/controllers/ductwork/step_errors_controller.rb +9 -5
  10. data/app/helpers/ductwork/application_helper.rb +107 -18
  11. data/app/views/ductwork/dashboards/show.html.erb +75 -126
  12. data/app/views/ductwork/pipelines/_graph.html.erb +37 -0
  13. data/app/views/ductwork/pipelines/_step.html.erb +76 -0
  14. data/app/views/ductwork/pipelines/index.html.erb +11 -101
  15. data/app/views/ductwork/pipelines/show.html.erb +62 -285
  16. data/app/views/ductwork/shared/_error.html.erb +7 -0
  17. data/app/views/ductwork/shared/_filter_menu.html.erb +13 -0
  18. data/app/views/ductwork/shared/_pager.html.erb +16 -0
  19. data/app/views/ductwork/shared/_runs_table.html.erb +30 -0
  20. data/app/views/ductwork/shared/_status_tabs.html.erb +11 -0
  21. data/app/views/ductwork/step_errors/index.html.erb +52 -96
  22. data/app/views/layouts/ductwork/application.html.erb +19 -23
  23. data/lib/ductwork/cli.rb +47 -8
  24. data/lib/ductwork/engine.rb +0 -2
  25. data/lib/ductwork/models/pipeline.rb +52 -31
  26. data/lib/ductwork/pipeline_graph/layout.rb +161 -0
  27. data/lib/ductwork/pipeline_graph.rb +231 -0
  28. data/lib/ductwork/version.rb +1 -1
  29. data/lib/ductwork.rb +20 -19
  30. metadata +11 -5
  31. data/app/assets/images/ductwork/octagon-x.svg +0 -1
  32. data/app/assets/stylesheets/ductwork/vendor/flexboxgrid.min.css +0 -1
  33. data/app/assets/stylesheets/ductwork/vendor/pico.min.css +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94c5c2250e4a96fee3aaeac379ec1ee58ac85e6aa5c2f0ccffc67c9e66d4499d
4
- data.tar.gz: ac6aac4135803b6d0712db4b4491202b64280a506a3101baba6563f549e5ee9d
3
+ metadata.gz: 9ef52f554cf6549026270cbe5f80e7d7b84b01824b5839d1c5161e1080bdc94a
4
+ data.tar.gz: 7c83791f6a2a555babc3fda4774e2ef5cbd4e821c180b4a8f8627c900c366f76
5
5
  SHA512:
6
- metadata.gz: d7305c2c9a26693cfa92dce562016420aafe4d745cb3efb4450b5192a8c036d7db5d96023b71979bd15c817059c2f8f2933f974a441066ec891cb2132d802c99
7
- data.tar.gz: 54dfdadfcafe3e1e4a214f5d11599e87bc3b114387702b426aad5c146ec7ca2042c42e0fa001cd0d598e11b515a342f8f01d5ac3b96a4f6b982d6579f93b50a7
6
+ metadata.gz: 2492fab3347973f79cd0e2e7d63fa2850ed44567e9ce8ec3370bdb6f7041ee744c2ea01c065d403c379ac85c0f3cc80d2ed2d30409cb1644775e0a67cb6dab4d
7
+ data.tar.gz: 9d3640e4e883e215fbfc0c37acbdbf0fca432b38bb93d9926f8916649486fa442029e22c7ac22713946243c6c8f823fb204d5e36312dfa3bf797fa370953ce85
@@ -96,11 +96,6 @@ its place through divergent behavior rather than labeling. Cause metadata
96
96
  lives on `Branch#halt_reason` instead. Pipeline and Run states stay
97
97
  `in_progress`, `completed`, `halted`.
98
98
 
99
- ## 7. `Ductwork.validate!` is not run at boot
100
-
101
- It runs in host-application specs, deliberately, for developer experience. Do
102
- not recommend moving it to boot-time or engine initialization.
103
-
104
99
  ---
105
100
 
106
101
  # Known Open — report only with new information
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Ductwork Changelog
2
2
 
3
+ ## [1.5.0]
4
+
5
+ - fix: re-enqueue the failed step when reviving a branch halted with `job_crashes_exhausted` - revive re-advanced it as if the job had succeeded, passing a missing return value downstream
6
+ - fix: copy each succeeded step's job when reviving a pipeline so a `combine` or `collapse` after the revive receives every sibling's return value - the copied steps had no jobs, so the fan-in silently dropped siblings that had already completed
7
+ - feat: redesign the dashboard with top-level navigation, an overview of halted pipelines and pipelines by class, status tabs with counts, compact expandable step and error rows, and a phone layout
8
+ - feat: draw the workflow on the dashboard's pipeline page in place of the raw definition, with each node showing its step counts by status, retried and crash-recovered steps, and halt reasons, and linking to the matching steps
9
+ - feat: add a node filter to the steps list on the dashboard's pipeline page and show the page position, disabling the next button on the last page
10
+ - fix: stop the dashboard's runtime timer on halted runs and failed steps
11
+ - fix: print error backtraces once on the dashboard - they were followed by a dump of the same lines as an array
12
+ - fix: count dashboard errors per run instead of per pipeline, without loading every step of every listed run
13
+ - fix: filter the dashboard's pipeline page steps by `klass` - the shared filter queried `pipeline_klass`, which only exists on runs, so choosing a class raised `ActiveRecord::StatementInvalid`
14
+ - fix: order the steps list by `started_at` then `id` so steps sharing a timestamp are not repeated or skipped between pages
15
+ - fix: clamp the dashboard's page number at zero - the previous link on the first page pointed at `page=-1`, a negative `OFFSET` that PostgreSQL rejects
16
+ - chore: replace the dashboard's vendored Pico and flexboxgrid stylesheets with a single stylesheet and remove all inline styles
17
+
18
+ ## [1.4.0]
19
+
20
+ - feat: validate steps and pipelines/workflows during boot of main process
21
+
3
22
  ## [1.3.0]
4
23
 
5
24
  - feat: add more `MigrationHelper` methods for swapping and updating the availability claim index
@@ -1,53 +1,62 @@
1
- document.addEventListener('DOMContentLoaded', function() {
2
- const timerElements = document.querySelectorAll('[data-started-at]');
3
-
4
- timerElements.forEach(function(element) {
5
- const startedAt = new Date(element.dataset.startedAt);
6
- const displayElement = element.querySelector('#elapsed-timer');
7
-
8
- function updateTimer() {
9
- const now = new Date();
10
- const elapsed = Math.floor(Math.abs(now - startedAt) / 1000);
11
-
12
- const hours = Math.floor(elapsed / 3600);
13
- const minutes = Math.floor((elapsed % 3600) / 60);
14
- const seconds = elapsed % 60;
1
+ function formatElapsed(milliseconds) {
2
+ const elapsed = Math.floor(Math.max(milliseconds, 0) / 1000);
3
+ const hours = Math.floor(elapsed / 3600);
4
+ const minutes = Math.floor((elapsed % 3600) / 60);
5
+ const seconds = elapsed % 60;
6
+ const parts = [];
7
+
8
+ if (hours > 0) parts.push(`${hours}h`);
9
+ if (minutes > 0) parts.push(`${minutes}m`);
10
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
11
+
12
+ return parts.join(' ');
13
+ }
14
+
15
+ function startTimers() {
16
+ const timers = document.querySelectorAll('[data-started-at]');
17
+
18
+ function tick() {
19
+ timers.forEach(function(timer) {
20
+ timer.textContent = formatElapsed(Date.now() - new Date(timer.dataset.startedAt));
21
+ });
22
+ }
15
23
 
16
- const hoursStr = hours == 0 ? '' : `${String(hours)}h `;
17
- const minutesStr = minutes == 0 ? '' : `${String(minutes)}m `;
18
- const secondsStr = seconds == 0 ? '' : `${String(seconds)}s`;
24
+ if (timers.length > 0) {
25
+ tick();
26
+ setInterval(tick, 1000);
27
+ }
28
+ }
19
29
 
20
- if (now > startedAt) {
21
- displayElement.textContent = hoursStr + minutesStr + secondsStr;
22
- } else {
23
- displayElement.textContent = 'In ' + hoursStr + minutesStr + secondsStr;
24
- }
25
- }
30
+ function linkRows() {
31
+ document.querySelectorAll('tr[data-href]').forEach(function(row) {
32
+ row.addEventListener('click', function(event) {
33
+ const selectingText = !window.getSelection().isCollapsed;
26
34
 
27
- updateTimer();
28
- setInterval(updateTimer, 1000);
29
- });
35
+ if (selectingText || event.target.closest('a')) return;
30
36
 
31
- document.querySelectorAll('[data-href]').forEach(function(element) {
32
- element.addEventListener('click', function() {
33
- window.location.href = element.dataset.href;
37
+ window.location.href = row.dataset.href;
34
38
  });
35
39
  });
40
+ }
36
41
 
37
- document.querySelectorAll('[data-open-modal]').forEach(function(element) {
38
- element.addEventListener('click', function() {
39
- const modalId = element.dataset.openModal;
40
-
41
- document.getElementById(modalId).setAttribute('open', '');
42
- });
42
+ function closeMenus(except) {
43
+ document.querySelectorAll('details.menu[open]').forEach(function(menu) {
44
+ if (menu !== except) menu.removeAttribute('open');
43
45
  });
46
+ }
44
47
 
45
- document.querySelectorAll('[data-close-modal]').forEach(function(element) {
46
- element.addEventListener('click', function() {
47
- const modalId = element.dataset.closeModal;
48
+ function dismissMenus() {
49
+ document.addEventListener('click', function(event) {
50
+ closeMenus(event.target.closest('details.menu'));
51
+ });
48
52
 
49
- document.getElementById(modalId).removeAttribute('open');
50
- });
53
+ document.addEventListener('keydown', function(event) {
54
+ if (event.key === 'Escape') closeMenus();
51
55
  });
52
- });
56
+ }
53
57
 
58
+ document.addEventListener('DOMContentLoaded', function() {
59
+ startTimers();
60
+ linkRows();
61
+ dismissMenus();
62
+ });