flight_control 2.0.0.beta.2 → 2.0.0.beta.3

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/app/assets/config/flight_control_manifest.js +3 -0
  4. data/app/assets/stylesheets/flight_control/application.css +5 -0
  5. data/app/assets/stylesheets/flight_control/bulma.min.css +21013 -0
  6. data/app/assets/stylesheets/flight_control/forms.css +8 -0
  7. data/app/assets/stylesheets/flight_control/jobs.css +42 -0
  8. data/app/controllers/concerns/flight_control/adapter_features.rb +25 -0
  9. data/app/controllers/concerns/flight_control/application_scoped.rb +39 -0
  10. data/app/controllers/concerns/flight_control/basic_authentication.rb +34 -0
  11. data/app/controllers/concerns/flight_control/failed_jobs_bulk_operations.rb +18 -0
  12. data/app/controllers/concerns/flight_control/job_filters.rb +44 -0
  13. data/app/controllers/concerns/flight_control/job_scoped.rb +17 -0
  14. data/app/controllers/concerns/flight_control/not_found_redirections.rb +36 -0
  15. data/app/controllers/concerns/flight_control/queue_scoped.rb +13 -0
  16. data/app/controllers/flight_control/application_controller.rb +35 -0
  17. data/app/controllers/flight_control/bulk_discards_controller.rb +21 -0
  18. data/app/controllers/flight_control/bulk_retries_controller.rb +10 -0
  19. data/app/controllers/flight_control/discards_controller.rb +19 -0
  20. data/app/controllers/flight_control/dispatches_controller.rb +19 -0
  21. data/app/controllers/flight_control/job_launches_controller.rb +28 -0
  22. data/app/controllers/flight_control/jobs_controller.rb +40 -0
  23. data/app/controllers/flight_control/queues/pauses_controller.rb +15 -0
  24. data/app/controllers/flight_control/queues_controller.rb +17 -0
  25. data/app/controllers/flight_control/recurring_tasks_controller.rb +39 -0
  26. data/app/controllers/flight_control/retries_controller.rb +14 -0
  27. data/app/controllers/flight_control/workers_controller.rb +24 -0
  28. data/app/helpers/flight_control/application_helper.rb +12 -0
  29. data/app/helpers/flight_control/dates_helper.rb +30 -0
  30. data/app/helpers/flight_control/interface_helper.rb +23 -0
  31. data/app/helpers/flight_control/jobs_helper.rb +105 -0
  32. data/app/helpers/flight_control/navigation_helper.rb +61 -0
  33. data/app/javascript/flight_control/application.js +3 -0
  34. data/app/javascript/flight_control/controllers/application.js +9 -0
  35. data/app/javascript/flight_control/controllers/form_controller.js +8 -0
  36. data/app/javascript/flight_control/controllers/index.js +11 -0
  37. data/app/javascript/flight_control/controllers/local_time_controller.js +27 -0
  38. data/app/models/flight_control/application_record.rb +5 -0
  39. data/app/models/flight_control/current.rb +3 -0
  40. data/app/models/flight_control/job_launch.rb +75 -0
  41. data/app/models/flight_control/page.rb +49 -0
  42. data/app/models/flight_control/recurring_task.rb +26 -0
  43. data/app/models/flight_control/worker.rb +18 -0
  44. data/app/views/flight_control/job_launches/new.html.erb +53 -0
  45. data/app/views/flight_control/jobs/_date_range_filter.html.erb +11 -0
  46. data/app/views/flight_control/jobs/_error_information.html.erb +24 -0
  47. data/app/views/flight_control/jobs/_filters.html.erb +49 -0
  48. data/app/views/flight_control/jobs/_general_information.html.erb +77 -0
  49. data/app/views/flight_control/jobs/_job.html.erb +13 -0
  50. data/app/views/flight_control/jobs/_jobs_page.html.erb +13 -0
  51. data/app/views/flight_control/jobs/_raw_data.html.erb +4 -0
  52. data/app/views/flight_control/jobs/_title.html.erb +19 -0
  53. data/app/views/flight_control/jobs/_toolbar.html.erb +18 -0
  54. data/app/views/flight_control/jobs/blocked/_actions.html.erb +3 -0
  55. data/app/views/flight_control/jobs/blocked/_job.html.erb +8 -0
  56. data/app/views/flight_control/jobs/failed/_actions.html.erb +5 -0
  57. data/app/views/flight_control/jobs/failed/_backtrace_toggle.html.erb +14 -0
  58. data/app/views/flight_control/jobs/failed/_job.html.erb +8 -0
  59. data/app/views/flight_control/jobs/finished/_job.html.erb +3 -0
  60. data/app/views/flight_control/jobs/in_progress/_job.html.erb +9 -0
  61. data/app/views/flight_control/jobs/index.html.erb +19 -0
  62. data/app/views/flight_control/jobs/scheduled/_actions.html.erb +5 -0
  63. data/app/views/flight_control/jobs/scheduled/_job.html.erb +13 -0
  64. data/app/views/flight_control/jobs/show.html.erb +6 -0
  65. data/app/views/flight_control/queues/_actions.html.erb +7 -0
  66. data/app/views/flight_control/queues/_job.html.erb +15 -0
  67. data/app/views/flight_control/queues/_queue.html.erb +16 -0
  68. data/app/views/flight_control/queues/_queue_title.html.erb +17 -0
  69. data/app/views/flight_control/queues/index.html.erb +21 -0
  70. data/app/views/flight_control/queues/show.html.erb +25 -0
  71. data/app/views/flight_control/recurring_tasks/_actions.html.erb +5 -0
  72. data/app/views/flight_control/recurring_tasks/_general_information.html.erb +37 -0
  73. data/app/views/flight_control/recurring_tasks/_recurring_task.html.erb +22 -0
  74. data/app/views/flight_control/recurring_tasks/_title.html.erb +10 -0
  75. data/app/views/flight_control/recurring_tasks/index.html.erb +18 -0
  76. data/app/views/flight_control/recurring_tasks/show.html.erb +14 -0
  77. data/app/views/flight_control/shared/_job.html.erb +27 -0
  78. data/app/views/flight_control/shared/_jobs.html.erb +14 -0
  79. data/app/views/flight_control/shared/_pagination_toolbar.html.erb +7 -0
  80. data/app/views/flight_control/workers/_configuration.html.erb +6 -0
  81. data/app/views/flight_control/workers/_raw_data.html.erb +6 -0
  82. data/app/views/flight_control/workers/_title.html.erb +11 -0
  83. data/app/views/flight_control/workers/_worker.html.erb +21 -0
  84. data/app/views/flight_control/workers/_workers_page.html.erb +15 -0
  85. data/app/views/flight_control/workers/index.html.erb +6 -0
  86. data/app/views/flight_control/workers/show.html.erb +14 -0
  87. data/app/views/layouts/flight_control/_application_selection.html.erb +14 -0
  88. data/app/views/layouts/flight_control/_flash.html.erb +9 -0
  89. data/app/views/layouts/flight_control/_navigation.html.erb +9 -0
  90. data/app/views/layouts/flight_control/application.html.erb +28 -0
  91. data/app/views/layouts/flight_control/application_selection/_applications.html.erb +13 -0
  92. data/app/views/layouts/flight_control/application_selection/_servers.html.erb +15 -0
  93. data/config/importmap.rb +5 -0
  94. data/config/routes.rb +34 -0
  95. data/lib/flight_control/version.rb +1 -1
  96. metadata +93 -1
@@ -0,0 +1,8 @@
1
+ /* Hide the HTML datalist wonky arrow in Safari and Chrome, since we add it via bulma. */
2
+ [list]::-webkit-calendar-picker-indicator {
3
+ opacity: 0;
4
+ }
5
+
6
+ a[disabled] {
7
+ pointer-events: none;
8
+ }
@@ -0,0 +1,42 @@
1
+ .filter,
2
+ .filter form {
3
+ max-width: 100%;
4
+ min-width: 0;
5
+ }
6
+
7
+ .filter input {
8
+ width: 15rem;
9
+ }
10
+
11
+ table.jobs {
12
+ td {
13
+ word-break: break-all;
14
+ }
15
+ th.job-header{
16
+ width: 30%;
17
+ }
18
+ th.duration-header {
19
+ width: 20%;
20
+ }
21
+ &.queues th.job-header {
22
+ width: 30%;
23
+ }
24
+ &.failed th.job-header {
25
+ width: 35%;
26
+ }
27
+ &.in_progress th.job-header {
28
+ width: 50%;
29
+ }
30
+ &.blocked th.job-header {
31
+ width: 45%;
32
+ }
33
+ &.scheduled th.job-header {
34
+ width: 40%;
35
+ }
36
+ &.finished th.job-header {
37
+ width: 65%;
38
+ }
39
+ &.workers th.job-header {
40
+ width: 40%;
41
+ }
42
+ }
@@ -0,0 +1,25 @@
1
+ module FlightControl::AdapterFeatures
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ helper_method :supported_job_statuses, :queue_pausing_supported?, :workers_exposed?, :recurring_tasks_supported?
6
+ end
7
+
8
+ private
9
+
10
+ def supported_job_statuses
11
+ FlightControl::Current.server.queue_adapter.supported_job_statuses & ActiveJob::JobsRelation::STATUSES
12
+ end
13
+
14
+ def queue_pausing_supported?
15
+ FlightControl::Current.server.queue_adapter.supports_queue_pausing?
16
+ end
17
+
18
+ def workers_exposed?
19
+ FlightControl::Current.server.queue_adapter.exposes_workers?
20
+ end
21
+
22
+ def recurring_tasks_supported?
23
+ FlightControl::Current.server.queue_adapter.supports_recurring_tasks?
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ module FlightControl::ApplicationScoped
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_application
6
+ around_action :activating_job_server
7
+
8
+ delegate :applications, to: FlightControl
9
+ end
10
+
11
+ private
12
+
13
+ def set_application
14
+ @application = find_application or raise FlightControl::Errors::ResourceNotFound, "Application not found"
15
+ FlightControl::Current.application = @application
16
+ end
17
+
18
+ def find_application
19
+ if params[:application_id]
20
+ applications[params[:application_id]]
21
+ else
22
+ applications.first
23
+ end
24
+ end
25
+
26
+ def activating_job_server(&block)
27
+ @server = find_server or raise FlightControl::Errors::ResourceNotFound, "Server not found"
28
+ FlightControl::Current.server = @server
29
+ @server.activating(&block)
30
+ end
31
+
32
+ def find_server
33
+ if params[:server_id]
34
+ FlightControl::Current.application.servers[params[:server_id]]
35
+ else
36
+ @application.servers.first
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ module FlightControl::BasicAuthentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :authenticate_by_http_basic
6
+ end
7
+
8
+ private
9
+
10
+ def authenticate_by_http_basic
11
+ if http_basic_authentication_enabled?
12
+ if http_basic_authentication_configured?
13
+ http_basic_authenticate_or_request_with(**http_basic_authentication_credentials)
14
+ else
15
+ head :unauthorized
16
+ end
17
+ end
18
+ end
19
+
20
+ def http_basic_authentication_enabled?
21
+ FlightControl.http_basic_auth_enabled
22
+ end
23
+
24
+ def http_basic_authentication_configured?
25
+ http_basic_authentication_credentials.values.all?(&:present?)
26
+ end
27
+
28
+ def http_basic_authentication_credentials
29
+ {
30
+ name: FlightControl.http_basic_auth_user,
31
+ password: FlightControl.http_basic_auth_password
32
+ }.transform_values(&:presence)
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module FlightControl::FailedJobsBulkOperations
2
+ extend ActiveSupport::Concern
3
+
4
+ MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS = 3000
5
+
6
+ included do
7
+ include FlightControl::JobFilters
8
+ end
9
+
10
+ private
11
+
12
+ # We set a hard limit to prevent problems with the data store (for example, overloading Redis
13
+ # or causing replication lag in MySQL). This should be enough for most scenarios. For
14
+ # cases where we need to retry a huge sets of jobs, we offer a runbook that uses the API.
15
+ def bulk_limited_filtered_failed_jobs
16
+ ActiveJob.jobs.failed.where(**@job_filters).limit(MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS)
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ module FlightControl::JobFilters
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_filters
6
+
7
+ helper_method :active_filters?, :jobs_filter_param
8
+ end
9
+
10
+ private
11
+
12
+ def set_filters
13
+ @job_filters = {
14
+ job_class_name: params.dig(:filter, :job_class_name).to_s.strip.presence,
15
+ queue_name: params.dig(:filter, :queue_name).to_s.strip.presence,
16
+ finished_at: date_range_params(:finished_at),
17
+ scheduled_at: date_range_params(:scheduled_at),
18
+ enqueued_at: date_range_params(:enqueued_at)
19
+ }.compact
20
+ end
21
+
22
+ def active_filters?
23
+ @job_filters.any?
24
+ end
25
+
26
+ def jobs_filter_param
27
+ if @job_filters&.any?
28
+ {filter: @job_filters}
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ def date_range_params(attribute)
35
+ range_start, range_end = params.dig(:filter, :"#{attribute}_start"), params.dig(:filter, :"#{attribute}_end")
36
+ if range_start || range_end
37
+ (parse_with_time_zone(range_start)..parse_with_time_zone(range_end))
38
+ end
39
+ end
40
+
41
+ def parse_with_time_zone(date)
42
+ Time.zone.parse(date) if date.present?
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ module FlightControl::JobScoped
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_job
6
+ end
7
+
8
+ private
9
+
10
+ def set_job
11
+ @job = jobs_relation.find_by_id!(params[:job_id] || params[:id])
12
+ end
13
+
14
+ def jobs_relation
15
+ raise NotImplementedError
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module FlightControl::NotFoundRedirections
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ rescue_from(ActiveJob::Errors::JobNotFoundError) do |error|
6
+ redirect_to best_location_for_job_relation(error.job_relation), alert: error.message
7
+ end
8
+
9
+ rescue_from(FlightControl::Errors::ResourceNotFound) do |error|
10
+ redirect_to best_location_for_resource_not_found_error(error), alert: error.message
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def best_location_for_job_relation(job_relation)
17
+ if job_relation.failed?
18
+ application_jobs_path(@application, :failed)
19
+ elsif job_relation.queue_name.present?
20
+ application_queue_path(@application, job_relation.queue_name)
21
+ else
22
+ root_path
23
+ end
24
+ end
25
+
26
+ def best_location_for_resource_not_found_error(error)
27
+ case error.message
28
+ when /recurring task/i
29
+ application_recurring_tasks_path(@application)
30
+ when /queue/i
31
+ application_queues_path(@application)
32
+ else
33
+ root_url
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module FlightControl::QueueScoped
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_queue
6
+ end
7
+
8
+ private
9
+
10
+ def set_queue
11
+ @queue = ActiveJob.queues[params[:queue_id]] or raise FlightControl::Errors::ResourceNotFound, "Queue '#{params[:queue_id]}' not found"
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ class FlightControl::ApplicationController < FlightControl.base_controller_class.constantize
2
+ ActionController::Base::MODULES.each do |mod|
3
+ include mod unless self < mod
4
+ end
5
+
6
+ layout "flight_control/application"
7
+
8
+ # Include helpers if not already included
9
+ helper FlightControl::ApplicationHelper unless self < FlightControl::ApplicationHelper
10
+ helper Importmap::ImportmapTagsHelper unless self < Importmap::ImportmapTagsHelper
11
+
12
+ include FlightControl::HostRouteHelpers
13
+ include FlightControl::BasicAuthentication
14
+ include FlightControl::NotFoundRedirections
15
+ include FlightControl::ApplicationScoped
16
+ include FlightControl::AdapterFeatures
17
+ include FlightControl::JobFilters
18
+
19
+ around_action :set_current_locale
20
+
21
+ private
22
+
23
+ def default_url_options
24
+ super.merge(server_id: FlightControl::Current.server)
25
+ end
26
+
27
+ def set_current_locale(&block)
28
+ @previous_config = I18n.config
29
+ I18n.config = FlightControl::I18nConfig.new
30
+ I18n.with_locale(:en, &block)
31
+ ensure
32
+ I18n.config = @previous_config
33
+ @previous_config = nil
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ class FlightControl::BulkDiscardsController < FlightControl::ApplicationController
2
+ include FlightControl::FailedJobsBulkOperations
3
+
4
+ def create
5
+ jobs_to_discard_count = jobs_to_discard.count
6
+ jobs_to_discard.discard_all
7
+
8
+ redirect_to application_jobs_url(@application, :failed), notice: "Discarded #{jobs_to_discard_count} jobs"
9
+ end
10
+
11
+ private
12
+
13
+ def jobs_to_discard
14
+ if active_filters?
15
+ bulk_limited_filtered_failed_jobs
16
+ else
17
+ # we don't want to apply any limit since "discarding all" without parameters can be optimized in the adapter as a much faster operation
18
+ ActiveJob.jobs.failed
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ class FlightControl::BulkRetriesController < FlightControl::ApplicationController
2
+ include FlightControl::FailedJobsBulkOperations
3
+
4
+ def create
5
+ jobs_to_retry_count = bulk_limited_filtered_failed_jobs.count
6
+ bulk_limited_filtered_failed_jobs.retry_all
7
+
8
+ redirect_to application_jobs_url(@application, :failed), notice: "Retried #{jobs_to_retry_count} jobs"
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ class FlightControl::DiscardsController < FlightControl::ApplicationController
2
+ include FlightControl::JobScoped
3
+
4
+ def create
5
+ @job.discard
6
+ redirect_to redirect_location, notice: "Discarded job with id #{@job.job_id}"
7
+ end
8
+
9
+ private
10
+
11
+ def jobs_relation
12
+ ActiveJob.jobs
13
+ end
14
+
15
+ def redirect_location
16
+ status = @job.status.presence_in(supported_job_statuses) || :failed
17
+ application_jobs_url(@application, status, **jobs_filter_param)
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ class FlightControl::DispatchesController < FlightControl::ApplicationController
2
+ include FlightControl::JobScoped
3
+
4
+ def create
5
+ @job.dispatch
6
+ redirect_to redirect_location, notice: "Dispatched job with id #{@job.job_id}"
7
+ end
8
+
9
+ private
10
+
11
+ def jobs_relation
12
+ ActiveJob.jobs
13
+ end
14
+
15
+ def redirect_location
16
+ status = @job.status.presence_in(supported_job_statuses) || :blocked
17
+ application_jobs_url(@application, status)
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ class FlightControl::JobLaunchesController < FlightControl::ApplicationController
2
+ before_action :set_form_options
3
+
4
+ def new
5
+ @job_launch = FlightControl::JobLaunch.new
6
+ end
7
+
8
+ def create
9
+ @job_launch = FlightControl::JobLaunch.new(job_launch_params)
10
+
11
+ if (job = @job_launch.enqueue)
12
+ redirect_to application_job_path(@application, job.job_id), notice: "Enqueued #{@job_launch.job_class_name} with job id #{job.job_id}"
13
+ else
14
+ render :new, status: :unprocessable_entity
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def set_form_options
21
+ @job_classes = FlightControl::JobLaunch.job_classes
22
+ @queue_names = ActiveJob.queues.map(&:name)
23
+ end
24
+
25
+ def job_launch_params
26
+ params.require(:job_launch).permit(:job_class_name, :arguments_json, :queue_name)
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ class FlightControl::JobsController < FlightControl::ApplicationController
2
+ include FlightControl::JobScoped
3
+
4
+ skip_before_action :set_job, only: :index
5
+
6
+ def index
7
+ @job_class_names = jobs_with_status.job_class_names
8
+ @queue_names = ActiveJob.queues.map(&:name)
9
+
10
+ @jobs_page = FlightControl::Page.new(filtered_jobs_with_status, page: params[:page].to_i)
11
+ @jobs_count = @jobs_page.total_count
12
+ end
13
+
14
+ def show
15
+ end
16
+
17
+ private
18
+
19
+ def jobs_relation
20
+ filtered_jobs
21
+ end
22
+
23
+ def filtered_jobs_with_status
24
+ filtered_jobs.with_status(jobs_status)
25
+ end
26
+
27
+ def jobs_with_status
28
+ ActiveJob.jobs.with_status(jobs_status)
29
+ end
30
+
31
+ def filtered_jobs
32
+ ActiveJob.jobs.where(**@job_filters)
33
+ end
34
+
35
+ helper_method :jobs_status
36
+
37
+ def jobs_status
38
+ params[:status].presence&.inquiry
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ class FlightControl::Queues::PausesController < FlightControl::ApplicationController
2
+ include FlightControl::QueueScoped
3
+
4
+ def create
5
+ @queue.pause
6
+
7
+ redirect_back fallback_location: application_queues_url(@application)
8
+ end
9
+
10
+ def destroy
11
+ @queue.resume
12
+
13
+ redirect_back fallback_location: application_queues_url(@application)
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ class FlightControl::QueuesController < FlightControl::ApplicationController
2
+ before_action :set_queue, only: :show
3
+
4
+ def index
5
+ @queues = ActiveJob.queues.sort_by(&:name)
6
+ end
7
+
8
+ def show
9
+ @jobs_page = FlightControl::Page.new(@queue.jobs, page: params[:page].to_i)
10
+ end
11
+
12
+ private
13
+
14
+ def set_queue
15
+ @queue = ActiveJob.queues[params[:id]] or raise FlightControl::Errors::ResourceNotFound, "Queue '#{params[:id]}' not found"
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ class FlightControl::RecurringTasksController < FlightControl::ApplicationController
2
+ before_action :ensure_supported_recurring_tasks
3
+ before_action :set_recurring_task, only: [:show, :update]
4
+ before_action :ensure_recurring_task_can_be_enqueued, only: :update
5
+
6
+ def index
7
+ @recurring_tasks = FlightControl::Current.server.recurring_tasks
8
+ end
9
+
10
+ def show
11
+ @jobs_page = FlightControl::Page.new(@recurring_task.jobs, page: params[:page].to_i)
12
+ end
13
+
14
+ def update
15
+ if (job = @recurring_task.enqueue) && job.successfully_enqueued?
16
+ redirect_to application_job_path(@application, job.job_id), notice: "Enqueued recurring task #{@recurring_task.id}"
17
+ else
18
+ redirect_to application_recurring_task_path(@application, @recurring_task.id), alert: "Something went wrong enqueuing this recurring task"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def ensure_supported_recurring_tasks
25
+ unless recurring_tasks_supported?
26
+ redirect_to root_url, alert: "This server doesn't support recurring tasks"
27
+ end
28
+ end
29
+
30
+ def set_recurring_task
31
+ @recurring_task = FlightControl::Current.server.find_recurring_task(params[:id])
32
+ end
33
+
34
+ def ensure_recurring_task_can_be_enqueued
35
+ unless @recurring_task.runnable?
36
+ redirect_to application_recurring_task_path(@application, @recurring_task.id), alert: "This task can't be enqueued"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ class FlightControl::RetriesController < FlightControl::ApplicationController
2
+ include FlightControl::JobScoped
3
+
4
+ def create
5
+ @job.retry
6
+ redirect_to application_jobs_url(@application, :failed, **jobs_filter_param), notice: "Retried job with id #{@job.job_id}"
7
+ end
8
+
9
+ private
10
+
11
+ def jobs_relation
12
+ ActiveJob.jobs.failed
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ class FlightControl::WorkersController < FlightControl::ApplicationController
2
+ before_action :ensure_exposed_workers
3
+
4
+ def index
5
+ @workers_page = FlightControl::Page.new(workers_relation, page: params[:page].to_i)
6
+ @workers_count = @workers_page.total_count
7
+ end
8
+
9
+ def show
10
+ @worker = FlightControl::Current.server.find_worker(params[:id])
11
+ end
12
+
13
+ private
14
+
15
+ def ensure_exposed_workers
16
+ unless workers_exposed?
17
+ redirect_to root_url, alert: "This server doesn't expose workers"
18
+ end
19
+ end
20
+
21
+ def workers_relation
22
+ FlightControl::Current.server.workers_relation
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ module FlightControl
2
+ module ApplicationHelper
3
+ # Explicit helper inclusion because ApplicationController inherits from the host app.
4
+ #
5
+ # We can't rely on +config.action_controller.include_all_helpers = true+ in the host app.
6
+ include InterfaceHelper
7
+ include NavigationHelper
8
+ include JobsHelper
9
+ include DatesHelper
10
+ include FlightControl::Engine.routes.url_helpers
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ module FlightControl::DatesHelper
2
+ def time_distance_in_words_with_title(time)
3
+ tag.span time_ago_in_words_with_default_options(time), title: "Since #{time.to_fs(:long)}"
4
+ end
5
+
6
+ def bidirectional_time_distance_in_words_with_title(time)
7
+ time_distance = if time.past?
8
+ "#{time_ago_in_words_with_default_options(time)} ago"
9
+ else
10
+ "in #{time_ago_in_words_with_default_options(time)}"
11
+ end
12
+
13
+ tag.span time_distance, title: time.to_fs(:long)
14
+ end
15
+
16
+ # Renders "2026-07-10 12:32:15 UTC (about 4 hours ago)". The local-time
17
+ # Stimulus controller prepends the timestamp in the browser's timezone,
18
+ # which the server can't know.
19
+ def timestamp_with_relative_time(time)
20
+ time = time.to_time.utc
21
+ safe_join [
22
+ tag.span(time.strftime("%Y-%m-%d %H:%M:%S UTC"), data: {controller: "local-time", local_time_datetime_value: time.iso8601}),
23
+ " (#{time_ago_in_words_with_default_options(time)} ago)"
24
+ ]
25
+ end
26
+
27
+ def time_ago_in_words_with_default_options(time)
28
+ time_ago_in_words(time, include_seconds: true, locale: :en)
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ module FlightControl::InterfaceHelper
2
+ def blank_status_notice(message)
3
+ tag.div message, class: "mt-6 has-text-centered is-size-3 has-text-grey"
4
+ end
5
+
6
+ def blank_status_emoji(status)
7
+ case status.to_s
8
+ when "failed", "blocked" then "😌"
9
+ else ""
10
+ end
11
+ end
12
+
13
+ def modifier_for_status(status)
14
+ case status.to_s
15
+ when "failed" then "is-danger"
16
+ when "blocked" then "is-warning"
17
+ when "finished" then "is-success"
18
+ when "scheduled" then "is-info"
19
+ when "in_progress" then "is-primary"
20
+ else "is-primary is-light"
21
+ end
22
+ end
23
+ end