maintenance_tasks 1.0.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.
- checksums.yaml +7 -0
- data/README.md +316 -0
- data/Rakefile +29 -0
- data/app/controllers/maintenance_tasks/application_controller.rb +26 -0
- data/app/controllers/maintenance_tasks/runs_controller.rb +44 -0
- data/app/controllers/maintenance_tasks/tasks_controller.rb +42 -0
- data/app/helpers/maintenance_tasks/application_helper.rb +67 -0
- data/app/helpers/maintenance_tasks/task_helper.rb +110 -0
- data/app/jobs/maintenance_tasks/task_job.rb +98 -0
- data/app/models/maintenance_tasks/application_record.rb +10 -0
- data/app/models/maintenance_tasks/progress.rb +74 -0
- data/app/models/maintenance_tasks/run.rb +180 -0
- data/app/models/maintenance_tasks/runner.rb +52 -0
- data/app/models/maintenance_tasks/task_data.rb +137 -0
- data/app/models/maintenance_tasks/ticker.rb +58 -0
- data/app/tasks/maintenance_tasks/task.rb +83 -0
- data/app/validators/maintenance_tasks/run_status_validator.rb +86 -0
- data/app/views/layouts/maintenance_tasks/_navbar.html.erb +11 -0
- data/app/views/layouts/maintenance_tasks/application.html.erb +54 -0
- data/app/views/maintenance_tasks/runs/_info.html.erb +10 -0
- data/app/views/maintenance_tasks/runs/_run.html.erb +11 -0
- data/app/views/maintenance_tasks/runs/index.html.erb +15 -0
- data/app/views/maintenance_tasks/runs/info/_cancelled.html.erb +4 -0
- data/app/views/maintenance_tasks/runs/info/_cancelling.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_enqueued.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_errored.html.erb +25 -0
- data/app/views/maintenance_tasks/runs/info/_interrupted.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_paused.html.erb +8 -0
- data/app/views/maintenance_tasks/runs/info/_pausing.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_running.html.erb +7 -0
- data/app/views/maintenance_tasks/runs/info/_succeeded.html.erb +5 -0
- data/app/views/maintenance_tasks/tasks/_task.html.erb +8 -0
- data/app/views/maintenance_tasks/tasks/actions/_cancelled.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/actions/_cancelling.html.erb +4 -0
- data/app/views/maintenance_tasks/tasks/actions/_enqueued.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_errored.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/actions/_interrupted.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_new.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/actions/_paused.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_pausing.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_running.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_succeeded.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/index.html.erb +22 -0
- data/app/views/maintenance_tasks/tasks/show.html.erb +25 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +22 -0
- data/exe/maintenance_tasks +13 -0
- data/lib/generators/maintenance_tasks/install_generator.rb +22 -0
- data/lib/generators/maintenance_tasks/task_generator.rb +74 -0
- data/lib/generators/maintenance_tasks/templates/task.rb.tt +21 -0
- data/lib/generators/maintenance_tasks/templates/task_spec.rb.tt +14 -0
- data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +12 -0
- data/lib/maintenance_tasks.rb +58 -0
- data/lib/maintenance_tasks/cli.rb +40 -0
- data/lib/maintenance_tasks/engine.rb +28 -0
- data/lib/maintenance_tasks/integrations/bugsnag_handler.rb +4 -0
- data/lib/tasks/maintenance_tasks_tasks.rake +5 -0
- metadata +187 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# This class is responsible for running a given Task.
|
5
|
+
class Runner
|
6
|
+
# Exception raised when a Task Job couldn't be enqueued.
|
7
|
+
class EnqueuingError < StandardError
|
8
|
+
# Initializes a Enqueuing Error.
|
9
|
+
#
|
10
|
+
# @param run [Run] the Run which failed to be enqueued.
|
11
|
+
# @return [EnqueuingError] an Enqueuing Error instance.
|
12
|
+
def initialize(run)
|
13
|
+
super("The job to perform #{run.task_name} could not be enqueued")
|
14
|
+
@run = run
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :run
|
18
|
+
end
|
19
|
+
|
20
|
+
# Runs a Task.
|
21
|
+
#
|
22
|
+
# This method creates a Run record for the given Task name and enqueues the
|
23
|
+
# Run.
|
24
|
+
#
|
25
|
+
# @param name [String] the name of the Task to be run.
|
26
|
+
#
|
27
|
+
# @return [Task] the Task that was run.
|
28
|
+
#
|
29
|
+
# @raise [EnqueuingError] if an error occurs while enqueuing the Run.
|
30
|
+
# @raise [ActiveRecord::RecordInvalid] if validation errors occur while
|
31
|
+
# creating the Run.
|
32
|
+
def run(name:)
|
33
|
+
run = Run.active.find_by(task_name: name) || Run.new(task_name: name)
|
34
|
+
|
35
|
+
run.enqueued!
|
36
|
+
enqueue(run)
|
37
|
+
Task.named(name)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def enqueue(run)
|
43
|
+
unless MaintenanceTasks.job.perform_later(run)
|
44
|
+
raise "The job to perform #{run.task_name} could not be enqueued. "\
|
45
|
+
'Enqueuing has been prevented by a callback.'
|
46
|
+
end
|
47
|
+
rescue => error
|
48
|
+
run.persist_error(error)
|
49
|
+
raise EnqueuingError, run
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module MaintenanceTasks
|
3
|
+
# Class that represents the data related to a Task. Such information can be
|
4
|
+
# sourced from a Task or from existing Run records for a Task that was since
|
5
|
+
# deleted.
|
6
|
+
#
|
7
|
+
# Instances of this class replace a Task class instance in cases where we
|
8
|
+
# don't need the actual Task subclass.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class TaskData
|
12
|
+
class << self
|
13
|
+
# Initializes a Task Data by name, raising if the Task does not exist.
|
14
|
+
#
|
15
|
+
# For the purpose of this method, a Task does not exist if it's deleted
|
16
|
+
# and doesn't have a Run. While technically, it could have existed and
|
17
|
+
# been deleted since, if it never had a Run we may as well consider it
|
18
|
+
# non-existent since we don't have interesting data to show.
|
19
|
+
#
|
20
|
+
# @param name [String] the name of the Task subclass.
|
21
|
+
# @return [TaskData] a Task Data instance.
|
22
|
+
# @raise [Task::NotFoundError] if the Task does not exist and doesn't have
|
23
|
+
# a Run.
|
24
|
+
def find(name)
|
25
|
+
task_data = new(name)
|
26
|
+
task_data.last_run || Task.named(name)
|
27
|
+
task_data
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a list of sorted Task Data objects that represent the
|
31
|
+
# available Tasks.
|
32
|
+
#
|
33
|
+
# Tasks are sorted by category, and within a category, by Task name.
|
34
|
+
# Determining a Task's category require its latest Run record.
|
35
|
+
# To optimize calls to the database, a single query is done to get the
|
36
|
+
# last Run for each Task, and Task Data instances are initialized with
|
37
|
+
# these last_run values.
|
38
|
+
#
|
39
|
+
# @return [Array<TaskData>] the list of Task Data.
|
40
|
+
def available_tasks
|
41
|
+
task_names = Task.available_tasks.map(&:name)
|
42
|
+
available_task_runs = Run.where(task_name: task_names)
|
43
|
+
last_runs = Run.where(
|
44
|
+
id: available_task_runs.select('MAX(id) as id').group(:task_name)
|
45
|
+
)
|
46
|
+
|
47
|
+
task_names.map do |task_name|
|
48
|
+
last_run = last_runs.find { |run| run.task_name == task_name }
|
49
|
+
TaskData.new(task_name, last_run)
|
50
|
+
end.sort_by!(&:name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Initializes a Task Data with a name and optionally a last_run.
|
55
|
+
#
|
56
|
+
# @param name [String] the name of the Task subclass.
|
57
|
+
# @param last_run [MaintenanceTasks::Run] optionally, a Run record to
|
58
|
+
# set for the Task.
|
59
|
+
def initialize(name, last_run = :none_passed)
|
60
|
+
@name = name
|
61
|
+
@last_run = last_run unless last_run == :none_passed
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [String] the name of the Task.
|
65
|
+
attr_reader :name
|
66
|
+
|
67
|
+
alias_method :to_s, :name
|
68
|
+
|
69
|
+
# The Task's source code.
|
70
|
+
#
|
71
|
+
# @return [String] the contents of the file which defines the Task.
|
72
|
+
# @return [nil] if the Task file was deleted.
|
73
|
+
def code
|
74
|
+
return if deleted?
|
75
|
+
task = Task.named(name)
|
76
|
+
file = task.instance_method(:collection).source_location.first
|
77
|
+
File.read(file)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Retrieves the latest Run associated with the Task.
|
81
|
+
#
|
82
|
+
# @return [MaintenanceTasks::Run] the Run record.
|
83
|
+
# @return [nil] if there are no Runs associated with the Task.
|
84
|
+
def last_run
|
85
|
+
return @last_run if defined?(@last_run)
|
86
|
+
@last_run = runs.first
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the set of Run records associated with the Task previous to the
|
90
|
+
# last Run. This collection represents a historic of past Runs for
|
91
|
+
# information purposes, since the base for Task Data information comes
|
92
|
+
# primarily from the last Run.
|
93
|
+
#
|
94
|
+
# @return [ActiveRecord::Relation<MaintenanceTasks::Run>] the relation of
|
95
|
+
# record previous to the last Run.
|
96
|
+
def previous_runs
|
97
|
+
return Run.none unless last_run
|
98
|
+
runs.where.not(id: last_run.id)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [Boolean] whether the Task has been deleted.
|
102
|
+
def deleted?
|
103
|
+
Task.named(name)
|
104
|
+
false
|
105
|
+
rescue Task::NotFoundError
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
# The Task status. It returns the status of the last Run, if present. If the
|
110
|
+
# Task does not have any Runs, the Task status is `new`.
|
111
|
+
#
|
112
|
+
# @return [String] the Task status.
|
113
|
+
def status
|
114
|
+
last_run&.status || 'new'
|
115
|
+
end
|
116
|
+
|
117
|
+
# Retrieves the Task's category, which is one of active, new, or completed.
|
118
|
+
#
|
119
|
+
# @return [Symbol] the category of the Task.
|
120
|
+
def category
|
121
|
+
if last_run.present? && last_run.active?
|
122
|
+
:active
|
123
|
+
elsif last_run.nil?
|
124
|
+
:new
|
125
|
+
else
|
126
|
+
:completed
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def runs
|
133
|
+
Run.where(task_name: name).order(created_at: :desc)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
private_constant :TaskData
|
137
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# This class encapsulates the logic behind updating the tick counter.
|
5
|
+
#
|
6
|
+
# It's initialized with a duration for the throttle, and a block to persist
|
7
|
+
# the number of ticks to increment.
|
8
|
+
#
|
9
|
+
# When +tick+ is called, the block will be called with the increment,
|
10
|
+
# provided the duration since the last update (or initialization) has been
|
11
|
+
# long enough.
|
12
|
+
#
|
13
|
+
# To not lose any increments, +persist+ should be used, which may call the
|
14
|
+
# block with any leftover ticks.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
class Ticker
|
18
|
+
# Creates a Ticker that will call the block each time +tick+ is called,
|
19
|
+
# unless the tick is being throttled.
|
20
|
+
#
|
21
|
+
# @param throttle_duration [ActiveSupport::Duration, Numeric] Duration
|
22
|
+
# since initialization or last call that will cause a throttle.
|
23
|
+
# @yieldparam ticks [Integer] the increment in ticks to be persisted.
|
24
|
+
def initialize(throttle_duration, &persist)
|
25
|
+
@throttle_duration = throttle_duration
|
26
|
+
@persist = persist
|
27
|
+
@last_persisted = Time.now
|
28
|
+
@ticks_recorded = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
# Increments the tick count by one, and may persist the new value if the
|
32
|
+
# threshold duration has passed since initialization or the tick count was
|
33
|
+
# last persisted.
|
34
|
+
def tick
|
35
|
+
@ticks_recorded += 1
|
36
|
+
persist if persist?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Persists the tick increments by calling the block passed to the
|
40
|
+
# initializer. This is idempotent in the sense that calling it twice in a
|
41
|
+
# row will call the block at most once (if it had been throttled).
|
42
|
+
def persist
|
43
|
+
return if @ticks_recorded == 0
|
44
|
+
now = Time.now
|
45
|
+
duration = now - @last_persisted
|
46
|
+
@last_persisted = now
|
47
|
+
@persist.call(@ticks_recorded, duration)
|
48
|
+
@ticks_recorded = 0
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def persist?
|
54
|
+
Time.now - @last_persisted >= @throttle_duration
|
55
|
+
end
|
56
|
+
end
|
57
|
+
private_constant :Ticker
|
58
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# Base class that is inherited by the host application's task classes.
|
5
|
+
class Task
|
6
|
+
extend ActiveSupport::DescendantsTracker
|
7
|
+
|
8
|
+
class NotFoundError < NameError; end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Finds a Task with the given name.
|
12
|
+
#
|
13
|
+
# @param name [String] the name of the Task to be found.
|
14
|
+
#
|
15
|
+
# @return [Task] the Task with the given name.
|
16
|
+
#
|
17
|
+
# @raise [NotFoundError] if a Task with the given name does not exist.
|
18
|
+
def named(name)
|
19
|
+
name.constantize
|
20
|
+
rescue NameError
|
21
|
+
raise NotFoundError.new("Task #{name} not found.", name)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a list of concrete classes that inherit from the Task
|
25
|
+
# superclass.
|
26
|
+
#
|
27
|
+
# @return [Array<Class>] the list of classes.
|
28
|
+
def available_tasks
|
29
|
+
load_constants
|
30
|
+
descendants
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_constants
|
36
|
+
namespace = MaintenanceTasks.tasks_module.safe_constantize
|
37
|
+
return unless namespace
|
38
|
+
namespace.constants.map { |constant| namespace.const_get(constant) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Placeholder method to raise in case a subclass fails to implement the
|
43
|
+
# expected instance method.
|
44
|
+
#
|
45
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
46
|
+
# implement an override for this method.
|
47
|
+
def collection
|
48
|
+
raise NotImplementedError,
|
49
|
+
"#{self.class.name} must implement `collection`."
|
50
|
+
end
|
51
|
+
|
52
|
+
# Placeholder method to raise in case a subclass fails to implement the
|
53
|
+
# expected instance method.
|
54
|
+
#
|
55
|
+
# @param _item [Object] the current item from the enumerator being iterated.
|
56
|
+
#
|
57
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
58
|
+
# implement an override for this method.
|
59
|
+
def process(_item)
|
60
|
+
raise NotImplementedError,
|
61
|
+
"#{self.class.name} must implement `process`."
|
62
|
+
end
|
63
|
+
|
64
|
+
# Total count of iterations to be performed.
|
65
|
+
#
|
66
|
+
# Tasks override this method to define the total amount of iterations
|
67
|
+
# expected at the start of the run. Return +nil+ if the amount is
|
68
|
+
# undefined, or counting would be prohibitive for your database.
|
69
|
+
#
|
70
|
+
# @return [Integer, nil]
|
71
|
+
def count
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convenience method to allow tasks define enumerators with cursors for
|
75
|
+
# compatibility with Job Iteration.
|
76
|
+
#
|
77
|
+
# @return [JobIteration::EnumeratorBuilder] instance of an enumerator
|
78
|
+
# builder available to tasks.
|
79
|
+
def enumerator_builder
|
80
|
+
JobIteration.enumerator_builder.new(nil)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module MaintenanceTasks
|
3
|
+
# Custom validator class responsible for ensuring that transitions between
|
4
|
+
# Run statuses are valid.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class RunStatusValidator < ActiveModel::Validator
|
8
|
+
# Valid status transitions a Run can make.
|
9
|
+
VALID_STATUS_TRANSITIONS = {
|
10
|
+
# enqueued -> running occurs when the task starts performing.
|
11
|
+
# enqueued -> pausing occurs when the task is paused before starting.
|
12
|
+
# enqueued -> cancelling occurs when the task is cancelled
|
13
|
+
# before starting.
|
14
|
+
# enqueued -> errored occurs when the task job fails to be enqueued.
|
15
|
+
'enqueued' => ['running', 'pausing', 'cancelling', 'errored'],
|
16
|
+
# pausing -> paused occurs when the task actually halts performing and
|
17
|
+
# occupies a status of paused.
|
18
|
+
# pausing -> cancelling occurs when the user cancels a task immediately
|
19
|
+
# after it was paused, such that the task had not actually halted yet.
|
20
|
+
# pausing -> succeeded occurs when the task completes immediately after
|
21
|
+
# being paused. This can happen if the task is on its last iteration
|
22
|
+
# when it is paused, or if the task is paused after enqueue but has
|
23
|
+
# nothing in its collection to process.
|
24
|
+
# pausing -> errored occurs when the job raises an exception after the
|
25
|
+
# user has paused it.
|
26
|
+
'pausing' => ['paused', 'cancelling', 'succeeded', 'errored'],
|
27
|
+
# cancelling -> cancelled occurs when the task actually halts performing
|
28
|
+
# and occupies a status of cancelled.
|
29
|
+
# cancelling -> succeeded occurs when the task completes immediately after
|
30
|
+
# being cancelled. See description for pausing -> succeeded.
|
31
|
+
# cancelling -> errored occurs when the job raises an exception after the
|
32
|
+
# user has cancelled it.
|
33
|
+
'cancelling' => ['cancelled', 'succeeded', 'errored'],
|
34
|
+
# running -> succeeded occurs when the task completes successfully.
|
35
|
+
# running -> pausing occurs when a user pauses the task as
|
36
|
+
# it's performing.
|
37
|
+
# running -> cancelling occurs when a user cancels the task as
|
38
|
+
# it's performing.
|
39
|
+
# running -> interrupted occurs when the job infra shuts down the task as
|
40
|
+
# it's performing.
|
41
|
+
# running -> errored occurs when the job raises an exception when running.
|
42
|
+
'running' => [
|
43
|
+
'succeeded',
|
44
|
+
'pausing',
|
45
|
+
'cancelling',
|
46
|
+
'interrupted',
|
47
|
+
'errored',
|
48
|
+
],
|
49
|
+
# paused -> enqueued occurs when the task is resumed after being paused.
|
50
|
+
# paused -> cancelling when the user cancels the task after it is paused.
|
51
|
+
# paused -> cancelled when the user cancels the task after it is paused.
|
52
|
+
'paused' => ['enqueued', 'cancelling', 'cancelled'],
|
53
|
+
# interrupted -> running occurs when the task is resumed after being
|
54
|
+
# interrupted by the job infrastructure.
|
55
|
+
# interrupted -> pausing occurs when the task is paused by the user while
|
56
|
+
# it is interrupted.
|
57
|
+
# interrupted -> cancelling occurs when the task is cancelled by the user
|
58
|
+
# while it is interrupted.
|
59
|
+
'interrupted' => ['running', 'pausing', 'cancelling'],
|
60
|
+
}
|
61
|
+
|
62
|
+
# Validate whether a transition from one Run status
|
63
|
+
# to another is acceptable.
|
64
|
+
#
|
65
|
+
# @param record [MaintenanceTasks::Run] the Run object being validated.
|
66
|
+
def validate(record)
|
67
|
+
return unless (previous_status, new_status = record.status_change)
|
68
|
+
|
69
|
+
valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
|
70
|
+
|
71
|
+
unless valid_new_statuses.include?(new_status)
|
72
|
+
add_invalid_status_error(record, previous_status, new_status)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def add_invalid_status_error(record, previous_status, new_status)
|
79
|
+
record.errors.add(
|
80
|
+
:status,
|
81
|
+
"Cannot transition run from status #{previous_status} to #{new_status}"
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
private_constant :RunStatusValidator
|
86
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<nav class="navbar is-dark is-spaced" role="navigation" aria-label="main navigation">
|
2
|
+
<div class="navbar-brand">
|
3
|
+
<%= link_to 'Maintenance Tasks', root_path, class: 'navbar-item is-size-4 has-text-weight-semibold has-text-danger' %>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<div id="navbarMenu" class="navbar-menu is-active">
|
7
|
+
<div class="navbar-start">
|
8
|
+
<%= link_to "Runs", runs_path, class: 'navbar-item' %>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
</nav>
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="<%= I18n.locale %>">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6
|
+
|
7
|
+
<title>
|
8
|
+
<% if content_for?(:page_title) %>
|
9
|
+
<%= content_for :page_title %> -
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
Maintenance Tasks
|
13
|
+
</title>
|
14
|
+
|
15
|
+
<%= csrf_meta_tags %>
|
16
|
+
|
17
|
+
<%=
|
18
|
+
stylesheet_link_tag URI.join(controller.class::BULMA_CDN, 'npm/bulma@0.9.1/css/bulma.css'),
|
19
|
+
media: :all,
|
20
|
+
integrity: 'sha256-67AR2JVjhMZCLVxapLuBSMap5RrXbksv4vlllenHBSE=',
|
21
|
+
crossorigin: 'anonymous'
|
22
|
+
%>
|
23
|
+
|
24
|
+
<style nonce="<%= content_security_policy_nonce %>">
|
25
|
+
.ruby-comment { color: #6a737d;}
|
26
|
+
.ruby-const { color: #e36209; }
|
27
|
+
.ruby-embexpr-beg, .ruby-embexpr-end, .ruby-period { color: #24292e; }
|
28
|
+
.ruby-ident, .ruby-symbeg { color: #6f42c1; }
|
29
|
+
.ruby-ivar, .ruby-cvar, .ruby-gvar, .ruby-int, .ruby-imaginary, .ruby-float, .ruby-rational { color: #005cc5; }
|
30
|
+
.ruby-kw { color: #d73a49; }
|
31
|
+
.ruby-label, .ruby-tstring-beg, .ruby-tstring-content, .ruby-tstring-end { color: #032f62; }
|
32
|
+
</style>
|
33
|
+
|
34
|
+
<% if defined?(@refresh) %>
|
35
|
+
<meta http-equiv="refresh" content="<%= @refresh %>">
|
36
|
+
<% end %>
|
37
|
+
</head>
|
38
|
+
|
39
|
+
<body>
|
40
|
+
<%= render 'layouts/maintenance_tasks/navbar' %>
|
41
|
+
|
42
|
+
<section class="section">
|
43
|
+
<div class="container">
|
44
|
+
<% if notice %>
|
45
|
+
<div class="notification is-success"><%= notice %></div>
|
46
|
+
<% elsif alert %>
|
47
|
+
<div class="notification is-warning"><%= alert %></div>
|
48
|
+
<% end %>
|
49
|
+
|
50
|
+
<%= yield %>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
</body>
|
54
|
+
</html>
|