solid_queue_dashboard 0.1.0 → 0.2.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/Procfile.dev +2 -1
  4. data/README.md +13 -9
  5. data/app/assets/javascripts/solid_queue_dashboard/application.js +17 -3
  6. data/app/assets/stylesheets/solid_queue_dashboard/application.css +96 -24
  7. data/app/assets/stylesheets/solid_queue_dashboard/tailwind.css +5 -5
  8. data/app/controllers/solid_queue_dashboard/appearance_controller.rb +1 -1
  9. data/app/controllers/solid_queue_dashboard/dashboard_controller.rb +61 -1
  10. data/app/controllers/solid_queue_dashboard/jobs_controller.rb +1 -1
  11. data/app/controllers/solid_queue_dashboard/stats_controller.rb +8 -0
  12. data/app/helpers/solid_queue_dashboard/icons_helper.rb +23 -0
  13. data/app/helpers/solid_queue_dashboard/jobs_helper.rb +8 -2
  14. data/app/helpers/solid_queue_dashboard/pagination_helper.rb +2 -2
  15. data/app/helpers/solid_queue_dashboard/processes_helper.rb +4 -2
  16. data/app/views/layouts/solid_queue_dashboard/application.html.erb +5 -3
  17. data/app/views/solid_queue_dashboard/application/_navbar.html.erb +28 -5
  18. data/app/views/solid_queue_dashboard/dashboard/index.html.erb +16 -28
  19. data/app/views/solid_queue_dashboard/jobs/_table.html.erb +1 -1
  20. data/app/views/solid_queue_dashboard/jobs/_table_row.html.erb +19 -3
  21. data/app/views/solid_queue_dashboard/jobs/index.html.erb +5 -5
  22. data/app/views/solid_queue_dashboard/jobs/show.html.erb +5 -3
  23. data/app/views/solid_queue_dashboard/processes/_table_row.html.erb +6 -0
  24. data/app/views/solid_queue_dashboard/processes/show.html.erb +14 -1
  25. data/app/views/solid_queue_dashboard/recurring_tasks/show.html.erb +6 -1
  26. data/app/views/solid_queue_dashboard/stats/index.html.erb +25 -0
  27. data/config/routes.rb +2 -0
  28. data/lib/solid_queue_dashboard/decorators/job_decorator.rb +9 -2
  29. data/lib/solid_queue_dashboard/decorators/jobs_decorator.rb +7 -0
  30. data/lib/solid_queue_dashboard/job.rb +5 -3
  31. data/lib/solid_queue_dashboard/process.rb +3 -3
  32. data/lib/solid_queue_dashboard/version.rb +1 -1
  33. data/lib/solid_queue_dashboard.rb +2 -0
  34. metadata +32 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bab2c28edcd8751ad636dcae7b2a464e937c4bf538c77a6f6be7f684bcc21e1d
4
- data.tar.gz: '0701197567cfa3ed1f5a54add9f6b7b59bec01ddaa014b6213885e22ef724774'
3
+ metadata.gz: a6d60665de93e100be378a8e607aaf251e62bf7c991051633dd1aba8d9b2a84f
4
+ data.tar.gz: 47a0d627dbf19a6be53260c846ca3baa35038f64bb7d6e844d77a1de68e62c83
5
5
  SHA512:
6
- metadata.gz: 5f40078988481a4f51e3433de13ce9640d218dda38ece0179e6d0b2526d78014db593b62c7b5ee69a4fa1c17c95a933da107c95690b550927ee4508d633595ed
7
- data.tar.gz: 3a731f4e5c33e77c5989c5f290d7befba922aefd46c0febf4d8de86ad1e6b261825b147d8996394556f43d50f528363e2fa01208c1f48b52e967029f9edacd4c
6
+ metadata.gz: 0dc95cf029ba6e32480b9a10c07d2f50106f0630b66f0385134181d84b1af1bfd4e0dff016f1d140a696aae180eaae83f173c6575e51a04a68bf7574f9177307
7
+ data.tar.gz: 5fdb3e8c5b8f08f3afb4769855595ccf2a8426ac429428f19bd32d82cec932d7882c892b60eb5f2e2ae8ae65822a91fc1ef86cc7701b6fa994fffe31912ddabe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - October 17, 2024
4
+
5
+ - Show running jobs
6
+ - Add charts
7
+ - Add auto-refresh
8
+ - Add number of current processes and recurring tasks to navbar
9
+ - Fix dark mode not switching for the first time
10
+
11
+ ## [0.1.1] - October 7, 2024
12
+
13
+ - Replace OpenStruct with a Hash to avoid including the `ostruct` gem
14
+
3
15
  ## [0.1.0] - October 7, 2024
4
16
 
5
17
  - Initial release
data/Procfile.dev CHANGED
@@ -1,2 +1,3 @@
1
1
  test_app: cd test_app && ./bin/rails server -p 3000
2
- tailwind: bun watch
2
+ jobs: cd test_app && ./bin/jobs
3
+ tailwind: bun watch
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # Solid Queue Dashboard
1
+ # Solid Queue Dashboard <sup>BETA</sup>
2
2
 
3
3
  <p align="center">
4
4
  <a href="https://github.com/akodkod/solid-queue-dashboard#gh-light-mode-only">
5
- <img src="https://github.com/user-attachments/assets/90cb68cf-73d9-41bf-b9d5-93da7c36f204" alt="Solid Queue Dashboard Light Mode">
5
+ <img src="https://github.com/user-attachments/assets/55aa4a3c-da51-471b-8f58-0cf1f9a1f8da" alt="Solid Queue Dashboard Light Mode">
6
6
  </a>
7
7
  <a href="https://github.com/akodkod/solid-queue-dashboard#gh-dark-mode-only">
8
- <img src="https://github.com/user-attachments/assets/ea089277-67fc-4bc9-91d8-2e3e7349b0c9" alt="Solid Queue Dashboard Dark Mode">
8
+ <img src="https://github.com/user-attachments/assets/645558cb-c20f-4d4b-9697-55282710ea6c" alt="Solid Queue Dashboard Dark Mode">
9
9
  </a>
10
10
 
11
- _👋 I'm available for hire → [kodkod.me](https://kodkod.me)_
11
+ _👋 I'm Available for Hire → [kodkod.me](https://kodkod.me)_
12
12
  </p>
13
13
 
14
14
  ## Features
@@ -19,12 +19,12 @@
19
19
  - 📜 View execution history
20
20
  - 🔍 Filter options
21
21
  - 🔄 Retry jobs from the UI
22
- - đŸšĢ No dependencies
22
+ - đŸĨŦ Auto-refresh
23
+ - 📈 Add charts
23
24
  - 🐒 No monkey patching
25
+ - 💈 TailwindCSS
24
26
 
25
27
  ## Roadmap
26
- - đŸŠâ€â™‚ī¸ Auto-pooling
27
- - 📈 Add charts
28
28
  - 🚀 Manually trigger jobs
29
29
  - âšī¸ Cancel long jobs (if possible)
30
30
  - 📊 More statistics and insights
@@ -43,7 +43,7 @@ bundle add solid_queue_dashboard
43
43
  Or add this line to your `Gemfile`:
44
44
 
45
45
  ```bash
46
- gem "solid_queue_dashboard", "~> 0.1.0"
46
+ gem "solid_queue_dashboard", "~> 0.2.0"
47
47
  ```
48
48
 
49
49
  Add this line to `routes.rb`:
@@ -85,10 +85,14 @@ To generate dummy data:
85
85
  ```
86
86
  cd test_app
87
87
  rails jobs:generate_dummy_data
88
- ./bin/jobs
89
88
  ```
90
89
 
91
90
  ## License
92
91
 
93
92
  This gem is open source under the [MIT License](http://opensource.org/licenses/MIT).
94
93
 
94
+ ---
95
+
96
+ _Made with love by Ukrainians 💙💛_
97
+ _[Help Ukraine](https://u24.gov.ua/)_
98
+
@@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', function() {
16
16
  });
17
17
 
18
18
  // Format dates
19
- document.querySelectorAll('[data-date]').forEach(function(element) {
19
+ document.querySelectorAll('[data-date]').forEach(function (element) {
20
20
  const dateString = element.textContent.trim();
21
21
  const date = new Date(dateString);
22
22
 
@@ -37,10 +37,24 @@ document.addEventListener('DOMContentLoaded', function() {
37
37
  });
38
38
 
39
39
  // Handle auto-submit elements
40
- document.querySelectorAll('[data-auto-submit]').forEach(function(element) {
41
- element.addEventListener('change', function() {
40
+ document.querySelectorAll('[data-auto-submit]').forEach(function (element) {
41
+ element.addEventListener('change', function () {
42
42
  const form = element.closest('form');
43
43
  if (form) form.submit();
44
44
  });
45
45
  });
46
+
47
+
48
+ // Auto-refresh functionality for dashboard page
49
+ const searchParams = new URLSearchParams(window.location.search);
50
+ const refreshInterval = parseInt(searchParams.get('auto_refresh_period'));
51
+
52
+ if (refreshInterval) {
53
+ setInterval(refreshHomePage, refreshInterval * 1000);
54
+ }
55
+
56
+ function refreshHomePage() {
57
+ // TODO: Implement a smart refresh strategy using Fetch or Turbo
58
+ window.location.reload();
59
+ }
46
60
  });
@@ -584,12 +584,12 @@ video {
584
584
 
585
585
  .dark {
586
586
  --background: 240 10% 5.4%;
587
- --foreground: 0 0% 98%;
587
+ --foreground: 0 0% 92%;
588
588
  --card: 240 10% 3.9%;
589
- --card-foreground: 0 0% 98%;
589
+ --card-foreground: 0 0% 92%;
590
590
  --popover: 240 10% 3.9%;
591
591
  --popover-foreground: 0 0% 98%;
592
- --primary: 0 0% 98%;
592
+ --primary: 0 0% 95%;
593
593
  --primary-foreground: 240 5.9% 10%;
594
594
  --secondary: 240 3.7% 15.9%;
595
595
  --secondary-foreground: 0 0% 98%;
@@ -796,6 +796,16 @@ body {
796
796
  background-color: hsl(var(--primary) / 0.8);
797
797
  }
798
798
 
799
+ .badge-secondary {
800
+ border-color: transparent;
801
+ background-color: hsl(var(--secondary));
802
+ color: hsl(var(--secondary-foreground));
803
+ }
804
+
805
+ .badge-secondary:hover {
806
+ background-color: hsl(var(--secondary) / 0.8);
807
+ }
808
+
799
809
  .badge-destructive {
800
810
  border-color: transparent;
801
811
  background-color: hsl(var(--destructive) / 0.15);
@@ -901,6 +911,18 @@ body {
901
911
  color: rgb(96 165 250 / var(--tw-text-opacity));
902
912
  }
903
913
 
914
+ .badge-indigo {
915
+ border-color: transparent;
916
+ background-color: rgb(99 102 241 / 0.15);
917
+ --tw-text-opacity: 1;
918
+ color: rgb(67 56 202 / var(--tw-text-opacity));
919
+ }
920
+
921
+ .badge-indigo:is(.dark *) {
922
+ --tw-text-opacity: 1;
923
+ color: rgb(129 140 248 / var(--tw-text-opacity));
924
+ }
925
+
904
926
  .badge-purple {
905
927
  border-color: transparent;
906
928
  background-color: rgb(168 85 247 / 0.15);
@@ -915,7 +937,7 @@ body {
915
937
 
916
938
  .badge-zinc {
917
939
  border-color: transparent;
918
- background-color: rgb(82 82 91 / 0.1);
940
+ background-color: rgb(82 82 91 / 0.075);
919
941
  --tw-text-opacity: 1;
920
942
  color: rgb(63 63 70 / var(--tw-text-opacity));
921
943
  }
@@ -969,6 +991,11 @@ body {
969
991
  background-color: rgb(59 130 246 / var(--tw-bg-opacity));
970
992
  }
971
993
 
994
+ .circle-indigo {
995
+ --tw-bg-opacity: 1;
996
+ background-color: rgb(99 102 241 / var(--tw-bg-opacity));
997
+ }
998
+
972
999
  .circle-purple {
973
1000
  --tw-bg-opacity: 1;
974
1001
  background-color: rgb(168 85 247 / var(--tw-bg-opacity));
@@ -976,7 +1003,7 @@ body {
976
1003
 
977
1004
  .circle-zinc {
978
1005
  --tw-bg-opacity: 1;
979
- background-color: rgb(82 82 91 / var(--tw-bg-opacity));
1006
+ background-color: rgb(161 161 170 / var(--tw-bg-opacity));
980
1007
  }
981
1008
 
982
1009
  /*
@@ -1504,6 +1531,22 @@ body {
1504
1531
  position: static;
1505
1532
  }
1506
1533
 
1534
+ .absolute {
1535
+ position: absolute;
1536
+ }
1537
+
1538
+ .relative {
1539
+ position: relative;
1540
+ }
1541
+
1542
+ .right-6 {
1543
+ right: 1.5rem;
1544
+ }
1545
+
1546
+ .top-6 {
1547
+ top: 1.5rem;
1548
+ }
1549
+
1507
1550
  .mx-auto {
1508
1551
  margin-left: auto;
1509
1552
  margin-right: auto;
@@ -1529,6 +1572,10 @@ body {
1529
1572
  margin-left: 0.125rem;
1530
1573
  }
1531
1574
 
1575
+ .ml-1 {
1576
+ margin-left: 0.25rem;
1577
+ }
1578
+
1532
1579
  .ml-1\.5 {
1533
1580
  margin-left: 0.375rem;
1534
1581
  }
@@ -1561,6 +1608,10 @@ body {
1561
1608
  margin-right: 0.375rem;
1562
1609
  }
1563
1610
 
1611
+ .mt-0\.5 {
1612
+ margin-top: 0.125rem;
1613
+ }
1614
+
1564
1615
  .mt-1 {
1565
1616
  margin-top: 0.25rem;
1566
1617
  }
@@ -1581,10 +1632,6 @@ body {
1581
1632
  margin-top: 2rem;
1582
1633
  }
1583
1634
 
1584
- .mt-0\.5 {
1585
- margin-top: 0.125rem;
1586
- }
1587
-
1588
1635
  .inline-block {
1589
1636
  display: inline-block;
1590
1637
  }
@@ -1641,6 +1688,10 @@ body {
1641
1688
  width: 6rem;
1642
1689
  }
1643
1690
 
1691
+ .w-40 {
1692
+ width: 10rem;
1693
+ }
1694
+
1644
1695
  .w-5 {
1645
1696
  width: 1.25rem;
1646
1697
  }
@@ -1667,8 +1718,8 @@ body {
1667
1718
  grid-template-columns: repeat(1, minmax(0, 1fr));
1668
1719
  }
1669
1720
 
1670
- .grid-cols-5 {
1671
- grid-template-columns: repeat(5, minmax(0, 1fr));
1721
+ .grid-cols-6 {
1722
+ grid-template-columns: repeat(6, minmax(0, 1fr));
1672
1723
  }
1673
1724
 
1674
1725
  .\!flex-row {
@@ -1683,6 +1734,10 @@ body {
1683
1734
  flex-wrap: wrap;
1684
1735
  }
1685
1736
 
1737
+ .items-start {
1738
+ align-items: flex-start;
1739
+ }
1740
+
1686
1741
  .items-center {
1687
1742
  align-items: center;
1688
1743
  }
@@ -1703,6 +1758,10 @@ body {
1703
1758
  gap: 0.25rem;
1704
1759
  }
1705
1760
 
1761
+ .gap-1\.5 {
1762
+ gap: 0.375rem;
1763
+ }
1764
+
1706
1765
  .gap-2 {
1707
1766
  gap: 0.5rem;
1708
1767
  }
@@ -1785,6 +1844,11 @@ body {
1785
1844
  border-top-color: rgb(239 68 68 / var(--tw-border-opacity));
1786
1845
  }
1787
1846
 
1847
+ .border-t-sky-500 {
1848
+ --tw-border-opacity: 1;
1849
+ border-top-color: rgb(14 165 233 / var(--tw-border-opacity));
1850
+ }
1851
+
1788
1852
  .border-t-zinc-500 {
1789
1853
  --tw-border-opacity: 1;
1790
1854
  border-top-color: rgb(113 113 122 / var(--tw-border-opacity));
@@ -1798,6 +1862,14 @@ body {
1798
1862
  padding: 0px !important;
1799
1863
  }
1800
1864
 
1865
+ .p-0 {
1866
+ padding: 0px;
1867
+ }
1868
+
1869
+ .p-6 {
1870
+ padding: 1.5rem;
1871
+ }
1872
+
1801
1873
  .px-2 {
1802
1874
  padding-left: 0.5rem;
1803
1875
  padding-right: 0.5rem;
@@ -1822,6 +1894,10 @@ body {
1822
1894
  padding-bottom: 15vh;
1823
1895
  }
1824
1896
 
1897
+ .pl-1 {
1898
+ padding-left: 0.25rem;
1899
+ }
1900
+
1825
1901
  .pl-6 {
1826
1902
  padding-left: 1.5rem;
1827
1903
  }
@@ -1867,19 +1943,6 @@ body {
1867
1943
  line-height: 1rem;
1868
1944
  }
1869
1945
 
1870
- .text-base {
1871
- font-size: 1rem;
1872
- line-height: 1.5rem;
1873
- }
1874
-
1875
- .text-\[15px\] {
1876
- font-size: 15px;
1877
- }
1878
-
1879
- .text-\[14px\] {
1880
- font-size: 14px;
1881
- }
1882
-
1883
1946
  .font-bold {
1884
1947
  font-weight: 700;
1885
1948
  }
@@ -1964,6 +2027,10 @@ body {
1964
2027
  }
1965
2028
  }
1966
2029
 
2030
+ .running {
2031
+ animation-play-state: running;
2032
+ }
2033
+
1967
2034
  .marker\:text-sm *::marker {
1968
2035
  font-size: 0.875rem;
1969
2036
  line-height: 1.25rem;
@@ -2009,6 +2076,11 @@ body {
2009
2076
  border-top-color: rgb(147 51 234 / var(--tw-border-opacity));
2010
2077
  }
2011
2078
 
2079
+ .dark\:border-t-sky-600:is(.dark *) {
2080
+ --tw-border-opacity: 1;
2081
+ border-top-color: rgb(2 132 199 / var(--tw-border-opacity));
2082
+ }
2083
+
2012
2084
  .dark\:border-t-zinc-600:is(.dark *) {
2013
2085
  --tw-border-opacity: 1;
2014
2086
  border-top-color: rgb(82 82 91 / var(--tw-border-opacity));
@@ -33,12 +33,12 @@
33
33
 
34
34
  .dark {
35
35
  --background: 240 10% 5.4%;
36
- --foreground: 0 0% 98%;
36
+ --foreground: 0 0% 92%;
37
37
  --card: 240 10% 3.9%;
38
- --card-foreground: 0 0% 98%;
38
+ --card-foreground: 0 0% 92%;
39
39
  --popover: 240 10% 3.9%;
40
40
  --popover-foreground: 0 0% 98%;
41
- --primary: 0 0% 98%;
41
+ --primary: 0 0% 95%;
42
42
  --primary-foreground: 240 5.9% 10%;
43
43
  --secondary: 240 3.7% 15.9%;
44
44
  --secondary-foreground: 0 0% 98%;
@@ -188,7 +188,7 @@
188
188
  }
189
189
 
190
190
  .badge-zinc {
191
- @apply border-transparent bg-zinc-600/10 text-zinc-700 dark:bg-white/5 dark:text-zinc-400;
191
+ @apply border-transparent bg-zinc-600/7.5 text-zinc-700 dark:bg-white/5 dark:text-zinc-400;
192
192
  }
193
193
 
194
194
  /*
@@ -284,7 +284,7 @@
284
284
  }
285
285
 
286
286
  .circle-zinc {
287
- @apply bg-zinc-600;
287
+ @apply bg-zinc-400;
288
288
  }
289
289
 
290
290
  /*
@@ -1,7 +1,7 @@
1
1
  module SolidQueueDashboard
2
2
  class AppearanceController < ApplicationController
3
3
  def toggle
4
- cookies[:dark_mode] = cookies[:dark_mode] == "false" ? "true" : "false"
4
+ cookies[:dark_mode] = cookies[:dark_mode] == "true" ? "false" : "true"
5
5
  redirect_to request.referer
6
6
  end
7
7
  end
@@ -2,7 +2,67 @@ module SolidQueueDashboard
2
2
  class DashboardController < ApplicationController
3
3
  def index
4
4
  @jobs = SolidQueueDashboard.decorate(SolidQueue::Job.all)
5
- @job_class_names = SolidQueueDashboard.job_class_names
5
+ load_charts
6
+ end
7
+
8
+ private
9
+
10
+ def load_charts
11
+ case params[:chart_period] || "30m"
12
+ when "15m"
13
+ n = 1
14
+ last = 15
15
+ when "30m"
16
+ n = 1
17
+ last = 30
18
+ when "1h"
19
+ n = 2
20
+ last = 30
21
+ when "3h"
22
+ n = 6
23
+ last = 30
24
+ when "6h"
25
+ n = 12
26
+ last = 30
27
+ when "12h"
28
+ n = 20
29
+ last = 36
30
+ when "1d"
31
+ n = 30
32
+ last = 48
33
+ when "3d"
34
+ n = 90 # 1.5 hours
35
+ last = 48
36
+ when "1w"
37
+ n = 180 # 3 hours
38
+ last = 56
39
+ else
40
+ n = 1
41
+ last = 30
42
+ end
43
+
44
+ @charts = [
45
+ {
46
+ name: "Enqueued",
47
+ data: SolidQueue::Job.group_by_minute(:created_at, last: last, n: n).count,
48
+ color: "#A1A1AB"
49
+ },
50
+ {
51
+ name: "Finished",
52
+ data: @jobs.finished.group_by_minute(:finished_at, last: last, n: n).count,
53
+ color: "#23C55E"
54
+ },
55
+ {
56
+ name: "Retried",
57
+ data: @jobs.retried.group_by_minute(:finished_at, last: last, n: n).count,
58
+ color: "#FBBF26"
59
+ },
60
+ {
61
+ name: "Failed",
62
+ data: SolidQueue::FailedExecution.group_by_minute(:created_at, last: last, n: n).count,
63
+ color: "#F04444"
64
+ }
65
+ ]
6
66
  end
7
67
  end
8
68
  end
@@ -32,7 +32,7 @@ module SolidQueueDashboard
32
32
  jobs = jobs.where(queue_name: params[:queue_name]) if params[:queue_name].present?
33
33
 
34
34
  @pagination = paginate(jobs, page: params[:page].to_i, per_page: params[:per_page].to_i)
35
- @jobs = SolidQueueDashboard.decorate(@pagination.records)
35
+ @jobs = SolidQueueDashboard.decorate(@pagination[:records])
36
36
  end
37
37
 
38
38
  def set_job
@@ -0,0 +1,8 @@
1
+ module SolidQueueDashboard
2
+ class StatsController < ApplicationController
3
+ def index
4
+ @jobs = SolidQueueDashboard.decorate(SolidQueue::Job.all)
5
+ @job_class_names = SolidQueueDashboard.job_class_names
6
+ end
7
+ end
8
+ end
@@ -227,5 +227,28 @@ module SolidQueueDashboard
227
227
  concat tag.polygon(points: "6 3 20 12 6 21 6 3")
228
228
  end
229
229
  end
230
+
231
+ def icon_chart_scatter(options = {})
232
+ svg_options = {
233
+ xmlns: "http://www.w3.org/2000/svg",
234
+ width: "24",
235
+ height: "24",
236
+ viewBox: "0 0 24 24",
237
+ fill: "none",
238
+ stroke: "currentColor",
239
+ "stroke-width": "2",
240
+ "stroke-linecap": "round",
241
+ "stroke-linejoin": "round"
242
+ }.merge(options)
243
+
244
+ tag.svg(**svg_options) do
245
+ concat tag.circle(cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor")
246
+ concat tag.circle(cx: "18.5", cy: "5.5", r: ".5", fill: "currentColor")
247
+ concat tag.circle(cx: "11.5", cy: "11.5", r: ".5", fill: "currentColor")
248
+ concat tag.circle(cx: "7.5", cy: "16.5", r: ".5", fill: "currentColor")
249
+ concat tag.circle(cx: "17.5", cy: "14.5", r: ".5", fill: "currentColor")
250
+ concat tag.path(d: "M3 3v16a2 2 0 0 0 2 2h16")
251
+ end
252
+ end
230
253
  end
231
254
  end
@@ -11,7 +11,10 @@ module SolidQueueDashboard
11
11
  "amber": "circle-amber",
12
12
  "red": "circle-red",
13
13
  "blue": "circle-blue",
14
- "zinc": "circle-zinc"
14
+ "sky": "circle-sky",
15
+ "zinc": "circle-zinc",
16
+ "indigo": "circle-indigo",
17
+ "purple": "circle-purple"
15
18
  }[Job::STATUS_COLORS[status]&.to_sym || :zinc]
16
19
  end
17
20
 
@@ -26,7 +29,10 @@ module SolidQueueDashboard
26
29
  "amber": "badge-amber",
27
30
  "red": "badge-red",
28
31
  "blue": "badge-blue",
29
- "zinc": "badge-zinc"
32
+ "sky": "badge-sky",
33
+ "zinc": "badge-zinc",
34
+ "indigo": "badge-indigo",
35
+ "purple": "badge-purple"
30
36
  }[Job::STATUS_COLORS[status]&.to_sym || :zinc]
31
37
  end
32
38
 
@@ -10,13 +10,13 @@ module SolidQueueDashboard
10
10
  total_count = scope.count
11
11
  total_pages = (total_count.to_f / per_page).ceil
12
12
 
13
- OpenStruct.new(
13
+ {
14
14
  records: records,
15
15
  current_page: page,
16
16
  per_page: per_page,
17
17
  total_pages: total_pages,
18
18
  total_count: total_count
19
- )
19
+ }
20
20
  end
21
21
 
22
22
  def page_range(current_page, total_pages, window: 2)
@@ -10,7 +10,8 @@ module SolidQueueDashboard
10
10
  "blue": "circle-blue",
11
11
  "green": "circle-green",
12
12
  "yellow": "circle-yellow",
13
- "purple": "circle-purple"
13
+ "purple": "circle-purple",
14
+ "sky": "circle-sky",
14
15
  }[Process::KIND_COLORS[kind]&.to_sym || :zinc]
15
16
  end
16
17
 
@@ -24,7 +25,8 @@ module SolidQueueDashboard
24
25
  "blue": "badge-blue",
25
26
  "green": "badge-green",
26
27
  "yellow": "badge-yellow",
27
- "purple": "badge-purple"
28
+ "purple": "badge-purple",
29
+ "sky": "badge-sky",
28
30
  }[Process::KIND_COLORS[kind]&.to_sym || :zinc]
29
31
  end
30
32
 
@@ -8,14 +8,16 @@
8
8
 
9
9
  <%= stylesheet_link_tag "solid_queue_dashboard/application", media: "all" %>
10
10
  <%= javascript_include_tag "solid_queue_dashboard/application" %>
11
+ <%= javascript_include_tag "chartkick" %>
12
+ <%= javascript_include_tag "Chart.bundle" %>
11
13
  <%= javascript_include_tag "solid_queue_dashboard/alpine", defer: true %>
12
14
  </head>
13
15
  <body class="pb-[15vh] sm:pb-[25vh]">
14
16
  <div class="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8">
15
- <%= render 'navbar' %>
16
- <%= render 'flash_messages' %>
17
+ <%= render "navbar" %>
18
+ <%= render "flash_messages" %>
17
19
  <%= yield %>
18
- <%= render 'footer' %>
20
+ <%= render "footer" %>
19
21
  </div>
20
22
  </body>
21
23
  </html>
@@ -1,5 +1,5 @@
1
1
  <nav class="navbar mb-6">
2
- <%= link_to root_path, class: "inline-flex items-center gap-0.5 text-xl font-bold tracking-tight translate-y-px" do %>
2
+ <%= link_to root_path, class: "inline-flex items-center gap-0.5 text-xl font-bold tracking-tight translate-y-px pl-1" do %>
3
3
  <span class="circle circle-blue"></span>
4
4
  <span class="circle circle-green"></span>
5
5
  <span class="circle circle-yellow"></span>
@@ -21,18 +21,41 @@
21
21
  <%= link_to processes_path, class: "navbar-item #{current_page?(processes_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
22
22
  <%= icon_server class: "size-4 mr-1.5" %>
23
23
  Processes
24
+
25
+ <span class="badge <%= current_page?(processes_path) ? 'badge-secondary' : 'badge-primary' %> ml-1 text-xs">
26
+ <%= SolidQueue::Process.count %>
27
+ </span>
24
28
  <% end %>
25
29
 
26
30
  <%= link_to recurring_tasks_path, class: "navbar-item #{current_page?(recurring_tasks_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
27
31
  <%= icon_clock class: "size-4 mr-1.5" %>
28
32
  Recurring Tasks
33
+
34
+ <span class="badge <%= current_page?(recurring_tasks_path) ? 'badge-secondary' : 'badge-primary' %> ml-1 text-xs">
35
+ <%= SolidQueue::RecurringTask.count %>
36
+ </span>
37
+ <% end %>
38
+
39
+ <%= link_to stats_path, class: "navbar-item #{current_page?(stats_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
40
+ <%= icon_chart_scatter class: "size-4 mr-1.5" %>
41
+ Stats
29
42
  <% end %>
30
43
  </div>
31
44
 
32
- <div class="ml-auto">
33
- <div>
34
-
35
- </div>
45
+ <div class="ml-auto flex gap-4">
46
+ <% if current_page?(controller: 'dashboard', action: 'index') %>
47
+ <%= form_with url: root_path, method: :get, class: "flex gap-1.5 items-center" do |form| %>
48
+ <%= form.hidden_field :chart_period %>
49
+
50
+ <%= icon_refresh_cw class: "size-4 text-muted-foreground" %>
51
+ <%= form.select :auto_refresh_period,
52
+ [["Auto-Refresh Off", "off"], ["15 seconds", "15"], ["30 seconds", "30"], ["1 minute", "60"], ["3 minutes", "180"], ["5 minutes", "300"], ["10 minutes", "600"], ["15 minutes", "900"], ["30 minutes", "1800"], ["1 hour", "3600"]],
53
+ { selected: params[:auto_refresh_period].presence || "off" },
54
+ class: "select w-40",
55
+ data: { auto_submit: true }
56
+ %>
57
+ <% end %>
58
+ <% end %>
36
59
 
37
60
  <%= form_with url: toggle_appearance_path, method: :post do |f| %>
38
61
  <button
@@ -1,4 +1,18 @@
1
- <div class="grid grid-cols-5 gap-4">
1
+ <div class="card card-content p-6 relative">
2
+ <%= form_with url: root_path, method: :get, class: "absolute top-6 right-6" do |form| %>
3
+ <%= form.hidden_field :auto_refresh_period %>
4
+ <%= form.select :chart_period,
5
+ [["15 minutes", "15m"], ["30 minutes", "30m"], ["1 hour", "1h"], ["3 hours", "3h"], ["6 hours", "6h"], ["12 hours", "12h"], ["1 day", "1d"], ["3 days", "3d"], ["1 week", "1w"]],
6
+ { selected: params[:chart_period].presence || "30m" },
7
+ class: "select",
8
+ data: { auto_submit: true }
9
+ %>
10
+ <% end %>
11
+
12
+ <%= line_chart @charts, points: false, thousands: "," %>
13
+ </div>
14
+
15
+ <div class="grid grid-cols-6 gap-4 mt-4">
2
16
  <% SolidQueueDashboard::Job::STATUSES.each do |status| %>
3
17
  <div class="card" data-href="<%= jobs_path(status:) %>">
4
18
  <div class="card-content pt-5">
@@ -7,35 +21,9 @@
7
21
  <span class="ml-0.5 -translate-y-px circle <%= job_status_circle_class(status) %>"></span>
8
22
  </h4>
9
23
  <p class="text-4xl font-bold mt-1 text-black dark:text-white">
10
- <%= @jobs.with_status(status).count %>
24
+ <%= number_with_delimiter(@jobs.with_status(status).count) %>
11
25
  </p>
12
26
  </div>
13
27
  </div>
14
28
  <% end %>
15
29
  </div>
16
-
17
- <div class="card mt-4">
18
- <div class="card-header border-b">
19
- <h3 class="card-title">Failure Rate</h3>
20
- </div>
21
- <div class="card-content !p-0">
22
- <div class="table-wrapper">
23
- <table class="table">
24
- <thead class="table-header">
25
- <tr class="table-row">
26
- <th class="table-head">Job</th>
27
- <th class="table-head">Failure Rate</th>
28
- </tr>
29
- </thead>
30
- <tbody class="table-body">
31
- <% @job_class_names.each do |class_name| %>
32
- <tr class="table-row" data-href="<%= jobs_path(class_name:, status: :failed) %>">
33
- <td class="table-cell font-medium"><%= class_name.titleize %></td>
34
- <td class="table-cell font-medium"><%= format_failure_rate(SolidQueueDashboard.decorate(SolidQueue::Job.where(class_name:)).failure_rate) %></td>
35
- </tr>
36
- <% end %>
37
- </tbody>
38
- </table>
39
- </div>
40
- </div>
41
- </div>
@@ -13,7 +13,7 @@
13
13
  </tr>
14
14
  </thead>
15
15
  <tbody class="table-body">
16
- <%= render partial: 'solid_queue_dashboard/jobs/table_row', collection: jobs, as: :job, locals: { highlight_ids: highlight_ids } %>
16
+ <%= render partial: "solid_queue_dashboard/jobs/table_row", collection: jobs, as: :job, locals: { highlight_ids: highlight_ids } %>
17
17
  </tbody>
18
18
  </table>
19
19
  </div>
@@ -14,6 +14,12 @@
14
14
 
15
15
  <td class="table-cell">
16
16
  <%= job_status_badge(job.status) %>
17
+ <% if job.running? %>
18
+ <br />
19
+ <span class="text-xs text-muted-foreground">
20
+ <%= time_ago_in_words(job.claimed_execution.created_at, include_seconds: true) %>
21
+ </span>
22
+ <% end %>
17
23
  </td>
18
24
 
19
25
  <td class="table-cell">
@@ -29,9 +35,9 @@
29
35
  <p class="font-medium"><%= job.class_name %></p>
30
36
 
31
37
  <% if job.arguments["arguments"].present? %>
32
- <p class="mt-0.5">
38
+ <p class="inline-flex flex-wrap gap-0.5 items-start mt-0.5">
33
39
  <% job.arguments["arguments"].each do |argument| %>
34
- <span class="badge badge-zinc"><%= truncate(JSON.pretty_generate(argument), length: 80) %></span>
40
+ <span class="badge badge-zinc text-xs"><%= truncate(JSON.pretty_generate(argument), length: 60) %></span>
35
41
  <% end %>
36
42
  </p>
37
43
  <% end %>
@@ -51,7 +57,17 @@
51
57
  </td>
52
58
 
53
59
  <td class="table-cell">
54
- <% if job.success? || job.retried? %>
60
+ <% if job.running? %>
61
+ <p class="text-sm text-muted-foreground">
62
+ Running for <strong class="font-medium text-foreground"><%= time_ago_in_words(job.claimed_execution.created_at, include_seconds: true) %></strong>
63
+ <% if job.claimed_execution.process %>
64
+ by <strong class="font-medium text-foreground">
65
+ <%= process_kind_badge(job.claimed_execution.process.kind) %>
66
+ <%= link_to "##{job.claimed_execution.process.id}", process_path(job.claimed_execution.process), class: "link" %>
67
+ </strong>
68
+ <% end %>
69
+ </p>
70
+ <% elsif job.success? || job.retried? %>
55
71
  <p class="text-sm text-muted-foreground">
56
72
  <%= job.retried? ? "Failed" : "Finished" %> at <strong class="font-medium text-foreground" data-date="<%= job.finished_at.to_fs(:database) %>"><%= job.finished_at %></strong><br />
57
73
  <span class="text-xs"><%= time_ago_in_words(job.scheduled_at, include_seconds: true) %> ago</span>
@@ -25,12 +25,12 @@
25
25
  <% end %>
26
26
  </div>
27
27
 
28
- <% if @pagination.total_pages > 1 %>
28
+ <% if @pagination[:total_pages] > 1 %>
29
29
  <div class="card-footer pt-6 border-t flex justify-between">
30
30
  <%= render partial: 'solid_queue_dashboard/application/pagination', locals: {
31
- current_page: @pagination.current_page,
32
- total_pages: @pagination.total_pages,
33
- per_page: @pagination.per_page
31
+ current_page: @pagination[:current_page],
32
+ total_pages: @pagination[:total_pages],
33
+ per_page: @pagination[:per_page]
34
34
  } %>
35
35
 
36
36
  <div class="flex items-center gap-2">
@@ -42,7 +42,7 @@
42
42
  <%= f.hidden_field :page, value: 1 %>
43
43
  <%= f.select :per_page,
44
44
  [10, 25, 50, 100],
45
- { selected: @pagination.per_page },
45
+ { selected: @pagination[:per_page] },
46
46
  class: "select w-24",
47
47
  data: { auto_submit: true }
48
48
  %>
@@ -126,9 +126,11 @@
126
126
  <% if @job.class_name === SolidQueueDashboard::Job::COMMAND_CLASS_NAME %>
127
127
  <pre x-show="!showAll"><%= @job.arguments["arguments"][0] %></pre>
128
128
  <% else %>
129
- <% @job.arguments["arguments"].each do |argument| %>
130
- <span class="badge badge-zinc text-sm"><%= JSON.pretty_generate(argument) %></span>
131
- <% end %>
129
+ <div x-show="!showAll">
130
+ <% @job.arguments["arguments"].each do |argument| %>
131
+ <span class="badge badge-zinc text-sm"><%= JSON.pretty_generate(argument) %></span>
132
+ <% end %>
133
+ </div>
132
134
  <% end %>
133
135
 
134
136
  <pre x-show="showAll"><%= JSON.pretty_generate(@job.arguments) %></pre>
@@ -5,6 +5,12 @@
5
5
 
6
6
  <td class="table-cell">
7
7
  <%= process_kind_badge(process.kind) %>
8
+
9
+ <% if process.claimed_executions.any? %>
10
+ <span class="ml-0.5 text-xs text-muted-foreground">
11
+ Running <%= pluralize(process.claimed_executions.count, "job") %>
12
+ </span>
13
+ <% end %>
8
14
  </td>
9
15
 
10
16
  <td class="table-cell"><%= process.hostname %></td>
@@ -66,9 +66,22 @@
66
66
  </div>
67
67
  </div>
68
68
 
69
+ <% if @process.claimed_executions.any? %>
70
+ <div class="card mt-8 overflow-hidden">
71
+ <div class="card-header border-b border-t-4 border-t-sky-500 dark:border-t-sky-600">
72
+ <h2 class="card-title">Running Jobs</h2>
73
+ <p class="card-description">Jobs that this process is currently running</p>
74
+ </div>
75
+
76
+ <div class="card-content p-0">
77
+ <%= render partial: 'solid_queue_dashboard/jobs/table', locals: { jobs: @process.claimed_executions.map { |execution| SolidQueueDashboard::Decorators::JobDecorator.new(execution.job) } } %>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
69
82
  <% if @process.metadata.present? %>
70
83
  <div class="card mt-8 overflow-hidden">
71
- <div class="card-header border-b border-t-4 border-t-blue-500 dark:border-t-blue-600">
84
+ <div class="card-header border-b border-t-4 border-t-amber-500 dark:border-t-amber-600">
72
85
  <h2 class="card-title">Metadata</h2>
73
86
  <p class="card-description">Additional information about this process</p>
74
87
  </div>
@@ -122,7 +122,12 @@
122
122
  <div class="card-content pt-5">
123
123
  <ul class="list-decimal marker:text-sm marker:text-muted-foreground ml-4 space-y-1">
124
124
  <% @recurring_task.next_runs.each do |run_time| %>
125
- <li data-date><%= run_time.strftime("%Y-%m-%d %H:%M:%S %Z") %></li>
125
+ <li>
126
+ <span data-date><%= run_time.strftime("%Y-%m-%d %H:%M:%S %Z") %></span>
127
+ <span class="text-muted-foreground">
128
+ in <%= distance_of_time_in_words(Time.current, run_time) %>
129
+ </span>
130
+ </li>
126
131
  <% end %>
127
132
  </ul>
128
133
  </div>
@@ -0,0 +1,25 @@
1
+ <div class="card mt-4">
2
+ <div class="card-header border-b">
3
+ <h3 class="card-title">Failure Rate</h3>
4
+ </div>
5
+ <div class="card-content !p-0">
6
+ <div class="table-wrapper">
7
+ <table class="table">
8
+ <thead class="table-header">
9
+ <tr class="table-row">
10
+ <th class="table-head">Job</th>
11
+ <th class="table-head">Failure Rate</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody class="table-body">
15
+ <% @job_class_names.each do |class_name| %>
16
+ <tr class="table-row" data-href="<%= jobs_path(class_name:, status: :failed) %>">
17
+ <td class="table-cell font-medium"><%= class_name.titleize %></td>
18
+ <td class="table-cell font-medium"><%= format_failure_rate(SolidQueueDashboard.decorate(SolidQueue::Job.where(class_name:)).failure_rate) %></td>
19
+ </tr>
20
+ <% end %>
21
+ </tbody>
22
+ </table>
23
+ </div>
24
+ </div>
25
+ </div>
data/config/routes.rb CHANGED
@@ -13,6 +13,8 @@ SolidQueueDashboard::Engine.routes.draw do
13
13
  end
14
14
  end
15
15
 
16
+ get "stats", to: "stats#index", as: :stats
16
17
  post "appearance/toggle", to: "appearance#toggle", as: :toggle_appearance
18
+
17
19
  root "dashboard#index"
18
20
  end
@@ -8,7 +8,9 @@ module SolidQueueDashboard
8
8
  def status
9
9
  return @status if defined?(@status)
10
10
 
11
- @status = if retried?
11
+ @status = if running?
12
+ Job::RUNNING
13
+ elsif retried?
12
14
  Job::RETRIED
13
15
  elsif failed?
14
16
  Job::FAILED
@@ -21,6 +23,11 @@ module SolidQueueDashboard
21
23
  end
22
24
  end
23
25
 
26
+ def running?
27
+ return @running if defined?(@running)
28
+ @running = claimed_execution.present?
29
+ end
30
+
24
31
  def success?
25
32
  return @success if defined?(@success)
26
33
  @success = finished_at.present? && !failed? && !retried?
@@ -44,7 +51,7 @@ module SolidQueueDashboard
44
51
 
45
52
  def pending?
46
53
  return @pending if defined?(@pending)
47
- @pending = !finished_at.present?
54
+ @pending = !finished_at.present? && !running?
48
55
  end
49
56
 
50
57
  def execution_history
@@ -3,6 +3,8 @@ module SolidQueueDashboard
3
3
  class JobsDecorator < SimpleDelegator
4
4
  def with_status(status)
5
5
  case status.to_sym
6
+ when Job::RUNNING
7
+ running
6
8
  when Job::SUCCESS
7
9
  success
8
10
  when Job::FAILED
@@ -18,6 +20,10 @@ module SolidQueueDashboard
18
20
  end
19
21
  end
20
22
 
23
+ def running
24
+ where.associated(:claimed_execution)
25
+ end
26
+
21
27
  def success
22
28
  where.not(finished_at: nil)
23
29
  .where.not(id: failed)
@@ -31,6 +37,7 @@ module SolidQueueDashboard
31
37
  def pending
32
38
  where(finished_at: nil, scheduled_at: ..Time.current)
33
39
  .where.not(id: failed)
40
+ .where.not(id: running)
34
41
  end
35
42
 
36
43
  def retried
@@ -1,20 +1,22 @@
1
1
  module SolidQueueDashboard
2
2
  module Job
3
3
  # Constants
4
+ RUNNING = :running
4
5
  SUCCESS = :success
5
6
  RETRIED = :retried
6
7
  FAILED = :failed
7
8
  PENDING = :pending
8
9
  SCHEDULED = :scheduled
9
10
 
10
- STATUSES = [ SUCCESS, RETRIED, FAILED, SCHEDULED, PENDING ]
11
+ STATUSES = [ RUNNING, SUCCESS, RETRIED, FAILED, SCHEDULED, PENDING ]
11
12
 
12
13
  STATUS_COLORS = {
13
14
  SUCCESS => "green",
14
15
  RETRIED => "amber",
15
16
  FAILED => "red",
16
- SCHEDULED => "blue",
17
- PENDING => "zinc"
17
+ SCHEDULED => "purple",
18
+ PENDING => "zinc",
19
+ RUNNING => "sky"
18
20
  }
19
21
 
20
22
  COMMAND_CLASS_NAME = "SolidQueue::RecurringJob"
@@ -9,13 +9,13 @@ module SolidQueueDashboard
9
9
  KINDS = [ SUPERVISOR, DISPATCHER, WORKER, SCHEDULER ]
10
10
 
11
11
  KIND_COLORS = {
12
- SUPERVISOR => "blue",
12
+ SUPERVISOR => "yellow",
13
13
  DISPATCHER => "green",
14
- WORKER => "yellow",
14
+ WORKER => "sky",
15
15
  SCHEDULER => "purple"
16
16
  }
17
17
 
18
- HEARTBEAT_DEAD_THRESHOLD = 1.minute
18
+ HEARTBEAT_DEAD_THRESHOLD = 3.minutes
19
19
 
20
20
  def self.kind_color(kind)
21
21
  KIND_COLORS[kind] || "zinc"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueDashboard
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "groupdate"
5
+ require "chartkick"
4
6
  require_relative "solid_queue_dashboard/version"
5
7
  require_relative "solid_queue_dashboard/configuration"
6
8
  require_relative "solid_queue_dashboard/engine"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kodkod
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-07 00:00:00.000000000 Z
11
+ date: 2024-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: solid_queue
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: groupdate
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: chartkick
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
27
55
  description: Dashboard for Solid Queue
28
56
  email:
29
57
  - andrew@kodkod.me
@@ -48,6 +76,7 @@ files:
48
76
  - app/controllers/solid_queue_dashboard/jobs_controller.rb
49
77
  - app/controllers/solid_queue_dashboard/processes_controller.rb
50
78
  - app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb
79
+ - app/controllers/solid_queue_dashboard/stats_controller.rb
51
80
  - app/helpers/solid_queue_dashboard/appearance_helper.rb
52
81
  - app/helpers/solid_queue_dashboard/application_helper.rb
53
82
  - app/helpers/solid_queue_dashboard/icons_helper.rb
@@ -76,6 +105,7 @@ files:
76
105
  - app/views/solid_queue_dashboard/recurring_tasks/_table_row.html.erb
77
106
  - app/views/solid_queue_dashboard/recurring_tasks/index.html.erb
78
107
  - app/views/solid_queue_dashboard/recurring_tasks/show.html.erb
108
+ - app/views/solid_queue_dashboard/stats/index.html.erb
79
109
  - bun.lockb
80
110
  - config/routes.rb
81
111
  - lib/solid_queue_dashboard.rb