que-view 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a3458af823c9bb66e334138e63455e4c0dc94392d6f09b959276733bcb30ef3
4
- data.tar.gz: c4b4be700ff4456f882488ccdd0adf34af14fa98bd9ab94380cc21a835ac1d49
3
+ metadata.gz: 38f9f8ac4b062389e01cec98fee2320a8c8e59f29d8edd86aa2be02cec38e1af
4
+ data.tar.gz: 1768c7efe2e99b73707f1fa869ea30e36b988423193dad3ddd92754cf5831099
5
5
  SHA512:
6
- metadata.gz: dd7d0ce017b6c4e2becf99b429dd0450aee7b250761ee3d9772d1dd5dc1e9f3bbfaf55508ab0e120ec3a30513e62a0857b5b3c375d2d44835c75f34b16b0c1aa
7
- data.tar.gz: f3aeb0ced2fc4b59bf5e783073505a51f507ea5114a79df6034c9764a8e12637a18f912581399492fef18fb33973c53144493a25dffa86c57a1aaf6a6c2c64d4
6
+ metadata.gz: d3e47f14f62dfad432f2a172199c32635f1095770bcf81184845848f3464e2e414b5640db774cd35ba800dbe873ecdddb7530e56128a8697b32c7b63c15a9681
7
+ data.tar.gz: 15f29072e2fc6d31b8b675f52c1210821645f3180e1190b0d48b918a949f6a2dae61f1102afe59c84f800e792f06dc194b196c0c5a7e571ed684e968919e1e4f
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Que::View
2
- Short description and motivation.
3
-
4
- ## Usage
5
- How to use my plugin.
2
+ Rails engine inspired by [Que::Web](https://github.com/statianzo/que-web) for [Que](https://github.com/que-rb/que) job queue.
3
+ SQL queries came from Que::Web, some styling from there too.
4
+ Benefits for using this one: independent from Sinatra (que-web based on Sinatra)
6
5
 
7
6
  ## Installation
7
+
8
8
  Add this line to your application's Gemfile:
9
9
 
10
10
  ```ruby
@@ -17,13 +17,40 @@ And then execute:
17
17
  $ bundle install
18
18
  ```
19
19
 
20
- Or install it yourself as:
21
- ```bash
22
- $ gem install que-view
20
+ ## Configuration
21
+
22
+ You can configure username/password for production web view.
23
+ Add this lines to config/initializers/que_view.rb
24
+
25
+ ```ruby
26
+ Que::View.configure do |config|
27
+ config.ui_username = 'username'
28
+ config.ui_password = 'password'
29
+ config.ui_secured_environments = ['production']
30
+ end
23
31
  ```
24
32
 
25
- ## Contributing
26
- Contribution directions go here.
33
+ ## Usage
34
+
35
+ Add this line to config/routes.rb
36
+
37
+ ```ruby
38
+ mount Que::Web::Engine => '/que_web'
39
+ ```
40
+
41
+ Add this line to assets/config/manifest.js
42
+
43
+ ```js
44
+ //= link que/view/application.css
45
+ ```
46
+
47
+ ## TODO
48
+
49
+ - [X] rescheduling jobs
50
+ - [X] deleting jobs
51
+ - [ ] better styles for UI
52
+ - [ ] rendering running jobs
53
+ - [ ] tests
27
54
 
28
55
  ## License
29
56
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -24,24 +24,45 @@
24
24
  display: flex;
25
25
  flex-direction: row;
26
26
  margin: 0 auto;
27
- max-width: 62.5rem;
27
+ max-width: 75rem;
28
28
  width: 100%;
29
29
  }
30
30
 
31
- .row .column, .row table {
31
+ .row .column {
32
32
  flex: 1;
33
33
  padding: 1rem;
34
34
  }
35
35
 
36
+ .row table {
37
+ flex: 1;
38
+ margin-bottom: 1rem;
39
+ }
40
+
36
41
  table th, table td {
37
42
  padding: .25rem .75rem;
38
- border-bottom: 1px solid #999;
43
+ border-bottom: 1px solid #BBB;
39
44
  }
40
45
 
41
- table tbody tr:nth-of-type(2n) {
46
+ table th {
47
+ text-align: left;
48
+ }
49
+
50
+ table tbody tr:nth-of-type(2n), table tbody tr:hover {
42
51
  background: #DDD;
43
52
  }
44
53
 
54
+ table tbody tr:nth-of-type(2n):hover {
55
+ background: #CCC;
56
+ }
57
+
58
+ table p {
59
+ margin: 0;
60
+ }
61
+
62
+ .actions {
63
+ display: flex;
64
+ }
65
+
45
66
  .dashboard-stat {
46
67
  text-align: center;
47
68
  display: table;
@@ -67,18 +88,14 @@ table tbody tr:nth-of-type(2n) {
67
88
  margin: 0;
68
89
  }
69
90
 
70
- .dashboard-stat.total {
71
- background: #828E8C;
91
+ .dashboard-stat.running {
92
+ background: #CFD0C1;
72
93
  }
73
94
 
74
95
  .dashboard-stat.scheduled {
75
96
  background: #828E8C;
76
97
  }
77
98
 
78
- .dashboard-stat.running {
79
- background: #CFD0C1;
80
- }
81
-
82
99
  .dashboard-stat.failing {
83
100
  background: #E8866C;
84
101
  }
@@ -88,3 +105,12 @@ table tbody tr:nth-of-type(2n) {
88
105
  line-height: 5rem;
89
106
  color: black;
90
107
  }
108
+
109
+ .btn-danger {
110
+ cursor: pointer;
111
+ border: none;
112
+ border-radius: .25rem;
113
+ background: #E8866C;
114
+ padding: .25rem .5rem;
115
+ margin-right: .5rem;
116
+ }
@@ -3,6 +3,20 @@
3
3
  module Que
4
4
  module View
5
5
  class ApplicationController < ActionController::Base
6
+ http_basic_authenticate_with name: ::Que::Web.configuration.ui_username,
7
+ password: ::Que::Web.configuration.ui_password,
8
+ if: -> { basic_auth_enabled? }
9
+
10
+ private
11
+
12
+ def basic_auth_enabled?
13
+ configuration = ::Que::Web.configuration
14
+
15
+ return false if configuration.ui_username.blank?
16
+ return false if configuration.ui_password.blank?
17
+
18
+ configuration.ui_secured_environments.include?(Rails.env)
19
+ end
6
20
  end
7
21
  end
8
22
  end
@@ -5,15 +5,36 @@ module Que
5
5
  class JobsController < Que::View::ApplicationController
6
6
  PER_PAGE = 20
7
7
 
8
+ before_action :find_job, only: %i[show]
9
+
8
10
  def index
9
11
  @jobs = find_jobs(params[:status])
10
12
  @jobs_amount = find_jobs_amount(params[:status])
11
13
  end
12
14
 
15
+ def show; end
16
+
17
+ def update
18
+ updated_rows = ::Que::View.reschedule_job(params[:id], Time.now)
19
+ redirect_to(
20
+ root_path,
21
+ notice: updated_rows.empty? ? 'Job is not rescheduled' : 'Job is rescheduled'
22
+ )
23
+ end
24
+
13
25
  def destroy
26
+ updated_rows = ::Que::View.delete_job(params[:id])
27
+ redirect_to(
28
+ root_path,
29
+ notice: updated_rows.empty? ? 'Job is not deleted' : 'Job is deleted'
30
+ )
31
+ end
32
+
33
+ def reschedule_all
34
+ updated_rows = reschedule_all_jobs(params[:status], Time.now)
14
35
  redirect_to(
15
36
  jobs_path(status: params[:status]),
16
- notice: 'Removing single job is not ready yet'
37
+ notice: updated_rows.empty? ? 'No jobs rescheduled' : "#{updated_rows.count} jobs rescheduled"
17
38
  )
18
39
  end
19
40
 
@@ -27,24 +48,54 @@ module Que
27
48
 
28
49
  private
29
50
 
51
+ def find_job
52
+ @job = ::Que::View.fetch_job(params[:id])[0]
53
+ return if @job
54
+
55
+ redirect_to root_path, notice: 'Job is not found'
56
+ end
57
+
30
58
  def find_jobs(status)
31
59
  case status&.to_sym
32
- when :failing then ::Que::View.failing_jobs(PER_PAGE, offset, '%')
60
+ when :failed then ::Que::View.fetch_failed_jobs(PER_PAGE, offset, search)
61
+ when :scheduled then ::Que::View.fetch_scheduled_jobs(PER_PAGE, offset, search)
33
62
  else []
34
63
  end
35
64
  end
36
65
 
37
66
  def find_jobs_amount(status)
38
- ::Que::View.dashboard_stats('%')[0][status&.to_sym]
67
+ ::Que::View.fetch_dashboard_stats(search)[0][status&.to_sym]
68
+ end
69
+
70
+ def reschedule_all_jobs(status, time)
71
+ case status&.to_sym
72
+ when :failed then ::Que::View.reschedule_failed_jobs(time)
73
+ when :scheduled then ::Que::View.reschedule_scheduled_jobs(time)
74
+ else 0
75
+ end
39
76
  end
40
77
 
41
78
  def destroy_all_jobs(status)
42
79
  case status&.to_sym
43
- when :failing then ::Que::View.delete_all_failing_jobs
80
+ when :failed then ::Que::View.delete_failed_jobs
81
+ when :scheduled then ::Que::View.delete_scheduled_jobs
44
82
  else 0
45
83
  end
46
84
  end
47
85
 
86
+ def search
87
+ return '%' unless search_param
88
+
89
+ "%#{search_param}%"
90
+ end
91
+
92
+ def search_param
93
+ sanitised = (params[:search] || '').gsub(/[^0-9a-z:]/i, '')
94
+ return if sanitised.empty?
95
+
96
+ sanitised
97
+ end
98
+
48
99
  def page
49
100
  (params[:page] || 1).to_i
50
101
  end
@@ -4,7 +4,7 @@ module Que
4
4
  module View
5
5
  class WelcomeController < Que::View::ApplicationController
6
6
  def index
7
- @dashboard_stats = ::Que::View.dashboard_stats('%')[0]
7
+ @dashboard_stats = ::Que::View.fetch_dashboard_stats('%')[0]
8
8
  end
9
9
  end
10
10
  end
@@ -3,8 +3,6 @@
3
3
  module Que
4
4
  module View
5
5
  module ApplicationHelper
6
- include ::Pagy::Frontend
7
-
8
6
  def humanized_job_class(job)
9
7
  case job[:job_class]
10
8
  when 'ActiveJob::QueueAdapters::QueAdapter::JobWrapper' then job.dig(:args, 0, :job_class)
@@ -4,24 +4,41 @@
4
4
  <thead>
5
5
  <tr>
6
6
  <th>ID</th>
7
- <th>Failures</th>
7
+ <th>Run at</th>
8
8
  <th>Job</th>
9
+ <th>Arguments</th>
9
10
  <th>Queue</th>
10
- <th>Error</th>
11
- <th></th>
11
+ <% if params[:status] == 'failing' %>
12
+ <th>Failures</th>
13
+ <th>Error</th>
14
+ <% end %>
15
+ <% if %w[failing scheduled].include?(params[:status]) %>
16
+ <th></th>
17
+ <% end %>
12
18
  </tr>
13
19
  </thead>
14
20
  <tbody>
15
21
  <% @jobs.each do |job| %>
16
22
  <tr>
17
- <td><%= job[:id] %></td>
18
- <td><%= job[:error_count] %></td>
23
+ <td><%= link_to job[:id], job_path(job[:id]) %></td>
24
+ <td><%= job[:run_at].utc %></td>
19
25
  <td><%= humanized_job_class(job) %></td>
20
26
  <td><%= job[:queue] %></td>
21
- <td><%= format_error(job) %></td>
22
27
  <td>
23
- <%= button_to 'Delete', job_path(job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
28
+ <% job.dig(:args, 0, :arguments).each do |argument| %>
29
+ <p><%= argument %></p>
30
+ <% end %>
24
31
  </td>
32
+ <% if params[:status] == 'failing' %>
33
+ <td><%= job[:error_count] %></td>
34
+ <td><%= format_error(job) %></td>
35
+ <% end %>
36
+ <% if %w[failing scheduled].include?(params[:status]) %>
37
+ <td class="actions">
38
+ <%= button_to 'Run', job_path(job[:id]), class: 'btn-danger', method: :patch, onclick: "return confirm('Are you sure you wish to reschedule job?')" %>
39
+ <%= button_to 'Delete', job_path(job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
40
+ </td>
41
+ <% end %>
25
42
  </tr>
26
43
  <% end %>
27
44
  </tbody>
@@ -30,6 +47,9 @@
30
47
  <p>No jobs found</p>
31
48
  <% end %>
32
49
  </div>
33
- <% if @jobs_amount.positive? %>
34
- <%= button_to 'Delete All', destroy_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete all jobs?')" %>
50
+ <% if %w[failing scheduled].include?(params[:status]) && @jobs_amount.positive? %>
51
+ <div class="row">
52
+ <%= button_to 'Run All', reschedule_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :post, onclick: "return confirm('Are you sure you wish to reschedule all jobs?')" %>
53
+ <%= button_to 'Delete All', destroy_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete all jobs?')" %>
54
+ </div>
35
55
  <% end %>
@@ -0,0 +1,51 @@
1
+ <div class="row">
2
+ <div class="columns">
3
+ <h1>Job <%= @job[:id] %></h1>
4
+ </div>
5
+ </div>
6
+ <div class="row">
7
+ <table cellspacing="0">
8
+ <tbody>
9
+ <tr>
10
+ <th>Job</th>
11
+ <td><%= humanized_job_class(@job) %></td>
12
+ </tr>
13
+ <tr>
14
+ <th>Queue</th>
15
+ <td><%= @job[:queue] %></td>
16
+ </tr>
17
+ <tr>
18
+ <th>Priority</th>
19
+ <td><%= @job[:priority] %></td>
20
+ </tr>
21
+ <tr>
22
+ <th>Run at</th>
23
+ <td><%= @job[:run_at].utc %></td>
24
+ </tr>
25
+ <tr>
26
+ <th>Failures</th>
27
+ <td><%= @job[:error_count] %></td>
28
+ </tr>
29
+ <tr>
30
+ <th>Args</th>
31
+ <td>
32
+ <% @job.dig(:args, 0, :arguments).each do |argument| %>
33
+ <p><%= argument %></p>
34
+ <% end %>
35
+ </td>
36
+ </tr>
37
+ <tr>
38
+ <th>Last Error</th>
39
+ <td>
40
+ <%= format_error(@job) %>
41
+ </td>
42
+ </tr>
43
+ </tbody>
44
+ </table>
45
+ </div>
46
+ <div class="row">
47
+ <div class="actions">
48
+ <%= button_to 'Run', job_path(@job[:id]), class: 'btn-danger', method: :patch, onclick: "return confirm('Are you sure you wish to reschedule job?')" %>
49
+ <%= button_to 'Delete', job_path(@job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
50
+ </div>
51
+ </div>
@@ -1,22 +1,26 @@
1
1
  <div class="row">
2
2
  <div class="column">
3
3
  <div class="dashboard-stat running">
4
- <div class="cell">
5
- <h2>Running</h2>
6
- <span class="dashboard-value">
7
- <%= @dashboard_stats[:running] %>
8
- </span>
9
- </div>
4
+ <%= link_to jobs_path(status: 'running') do %>
5
+ <div class="cell">
6
+ <h2>Running</h2>
7
+ <span class="dashboard-value">
8
+ <%= @dashboard_stats[:running] %>
9
+ </span>
10
+ </div>
11
+ <% end %>
10
12
  </div>
11
13
  </div>
12
14
  <div class="column">
13
15
  <div class="dashboard-stat scheduled">
14
- <div class="cell">
15
- <h2>Scheduled</h2>
16
- <span class="dashboard-value">
17
- <%= @dashboard_stats[:scheduled] %>
18
- </span>
19
- </div>
16
+ <%= link_to jobs_path(status: 'scheduled') do %>
17
+ <div class="cell">
18
+ <h2>Scheduled</h2>
19
+ <span class="dashboard-value">
20
+ <%= @dashboard_stats[:scheduled] %>
21
+ </span>
22
+ </div>
23
+ <% end %>
20
24
  </div>
21
25
  </div>
22
26
  <div class="column">
data/config/routes.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Que::View::Engine.routes.draw do
4
- resources :jobs, only: %i[index destroy] do
4
+ resources :jobs, only: %i[index show update destroy] do
5
+ post :reschedule_all, on: :collection
5
6
  delete :destroy_all, on: :collection
6
7
  end
7
8
 
data/lib/que/view/dsl.rb CHANGED
@@ -2,23 +2,52 @@
2
2
 
3
3
  module Que
4
4
  module View
5
+ # rubocop: disable Metrics/ClassLength
5
6
  class DSL
6
- def dashboard_stats(...)
7
- execute(dashboard_stats_sql(...))
7
+ def fetch_dashboard_stats(...)
8
+ execute(fetch_dashboard_stats_sql(...))
8
9
  end
9
10
 
10
- def failing_jobs(...)
11
- execute(failing_jobs_sql(...))
11
+ def fetch_failing_jobs(...)
12
+ execute(fetch_failing_jobs_sql(...))
12
13
  end
13
14
 
14
- def delete_all_failing_jobs
15
- execute(delete_jobs_query(lock_all_failing_jobs_sql))
15
+ def fetch_scheduled_jobs(...)
16
+ execute(fetch_scheduled_jobs_sql(...))
17
+ end
18
+
19
+ def fetch_job(...)
20
+ execute(fetch_job_sql(...))
21
+ end
22
+
23
+ def delete_failing_jobs
24
+ execute(delete_jobs_sql(lock_failing_jobs_sql))
25
+ end
26
+
27
+ def delete_scheduled_jobs
28
+ execute(delete_jobs_sql(lock_scheduled_jobs_sql))
29
+ end
30
+
31
+ def delete_job(...)
32
+ execute(delete_jobs_sql(lock_job_sql(...)))
33
+ end
34
+
35
+ def reschedule_scheduled_jobs(time)
36
+ execute(reschedule_jobs_sql(lock_scheduled_jobs_sql, time))
37
+ end
38
+
39
+ def reschedule_failing_jobs(time)
40
+ execute(reschedule_jobs_sql(lock_failing_jobs_sql, time))
41
+ end
42
+
43
+ def reschedule_job(job_id, time)
44
+ execute(reschedule_jobs_sql(lock_job_sql(job_id), time))
16
45
  end
17
46
 
18
47
  private
19
48
 
20
49
  # rubocop: disable Metrics/MethodLength
21
- def dashboard_stats_sql(search)
50
+ def fetch_dashboard_stats_sql(search)
22
51
  <<-SQL.squish
23
52
  SELECT count(*) AS total,
24
53
  count(locks.job_id) AS running,
@@ -36,7 +65,7 @@ module Que
36
65
  SQL
37
66
  end
38
67
 
39
- def failing_jobs_sql(per_page, offset, search)
68
+ def fetch_failing_jobs_sql(per_page, offset, search)
40
69
  <<-SQL.squish
41
70
  SELECT que_jobs.*
42
71
  FROM que_jobs
@@ -57,6 +86,36 @@ module Que
57
86
  SQL
58
87
  end
59
88
 
89
+ def fetch_scheduled_jobs_sql(per_page, offset, search)
90
+ <<-SQL.squish
91
+ SELECT que_jobs.*
92
+ FROM que_jobs
93
+ LEFT JOIN (
94
+ SELECT (classid::bigint << 32) + objid::bigint AS job_id
95
+ FROM pg_locks
96
+ WHERE locktype = 'advisory'
97
+ ) locks ON (que_jobs.id=locks.job_id)
98
+ WHERE locks.job_id IS NULL
99
+ AND error_count = 0
100
+ AND (
101
+ job_class ILIKE ('#{search}')
102
+ OR que_jobs.args #>> '{0, job_class}' ILIKE ('#{search}')
103
+ )
104
+ ORDER BY run_at, id
105
+ LIMIT #{per_page}::int
106
+ OFFSET #{offset}::int
107
+ SQL
108
+ end
109
+
110
+ def fetch_job_sql(job_id)
111
+ <<-SQL.squish
112
+ SELECT *
113
+ FROM que_jobs
114
+ WHERE id = #{job_id}::bigint
115
+ LIMIT 1
116
+ SQL
117
+ end
118
+
60
119
  def delete_jobs_sql(scope)
61
120
  <<-SQL.squish
62
121
  WITH target AS (#{scope})
@@ -68,18 +127,47 @@ module Que
68
127
  SQL
69
128
  end
70
129
 
71
- def lock_all_failing_jobs_sql
130
+ def reschedule_jobs_sql(scope, time)
131
+ <<-SQL.squish
132
+ WITH target AS (#{scope})
133
+ UPDATE que_jobs
134
+ SET run_at = '#{time}'::timestamptz, expired_at = NULL
135
+ FROM target
136
+ WHERE target.locked
137
+ AND target.id = que_jobs.id
138
+ RETURNING pg_advisory_unlock(target.id)
139
+ SQL
140
+ end
141
+
142
+ def lock_failing_jobs_sql
72
143
  <<-SQL.squish
73
144
  SELECT id, pg_try_advisory_lock(id) AS locked
74
145
  FROM que_jobs
75
146
  WHERE error_count > 0
76
147
  SQL
77
148
  end
149
+
150
+ def lock_scheduled_jobs_sql
151
+ <<-SQL.squish
152
+ SELECT id, pg_try_advisory_lock(id) AS locked
153
+ FROM que_jobs
154
+ WHERE error_count = 0
155
+ SQL
156
+ end
157
+
158
+ def lock_job_sql(job_id)
159
+ <<-SQL.squish
160
+ SELECT id, pg_try_advisory_lock(id) AS locked
161
+ FROM que_jobs
162
+ WHERE id = #{job_id}::bigint
163
+ SQL
164
+ end
78
165
  # rubocop: enable Metrics/MethodLength
79
166
 
80
167
  def execute(sql)
81
168
  Que.execute(sql)
82
169
  end
83
170
  end
171
+ # rubocop: enable Metrics/ClassLength
84
172
  end
85
173
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Que
4
4
  module View
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
data/lib/que/view.rb CHANGED
@@ -34,6 +34,8 @@ module Que
34
34
 
35
35
  # Public: All the methods delegated to instance. These should match the interface of Que::View::DSL.
36
36
  def_delegators :instance,
37
- :dashboard_stats, :failing_jobs, :delete_all_failing_jobs
37
+ :fetch_dashboard_stats, :fetch_failing_jobs, :fetch_scheduled_jobs, :fetch_job,
38
+ :delete_failing_jobs, :delete_scheduled_jobs, :delete_job,
39
+ :reschedule_scheduled_jobs, :reschedule_failing_jobs, :reschedule_job
38
40
  end
39
41
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que-view
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
  - Bogdanov Anton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-14 00:00:00.000000000 Z
11
+ date: 2023-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: que
@@ -168,6 +168,7 @@ files:
168
168
  - app/helpers/que/view/application_helper.rb
169
169
  - app/views/layouts/que/view/application.html.erb
170
170
  - app/views/que/view/jobs/index.html.erb
171
+ - app/views/que/view/jobs/show.html.erb
171
172
  - app/views/que/view/welcome/index.html.erb
172
173
  - config/routes.rb
173
174
  - lib/que/view.rb