funktor 0.4.7 → 0.6.2

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/.tool-versions +2 -0
  3. data/Gemfile.lock +31 -12
  4. data/funktor-testapp/.envrc +1 -0
  5. data/funktor-testapp/.gitignore +7 -0
  6. data/funktor-testapp/Gemfile +25 -0
  7. data/funktor-testapp/Gemfile.lock +51 -0
  8. data/funktor-testapp/app/services/job_flood.rb +38 -0
  9. data/funktor-testapp/app/workers/audit_worker.rb +49 -0
  10. data/funktor-testapp/app/workers/greetings_worker.rb +3 -0
  11. data/funktor-testapp/app/workers/hello_worker.rb +18 -0
  12. data/funktor-testapp/app/workers/single_thread_audit_worker.rb +3 -0
  13. data/funktor-testapp/deploy-dev.sh +5 -0
  14. data/funktor-testapp/funktor_config/boot.rb +17 -0
  15. data/funktor-testapp/funktor_config/environment.yml +15 -0
  16. data/funktor-testapp/funktor_config/function_definitions/default_queue_handler.yml +13 -0
  17. data/funktor-testapp/funktor_config/function_definitions/incoming_job_handler.yml +13 -0
  18. data/funktor-testapp/funktor_config/function_definitions/job_activator.yml +7 -0
  19. data/funktor-testapp/funktor_config/function_definitions/low_concurrency_queue_handler.yml +13 -0
  20. data/funktor-testapp/funktor_config/function_definitions/random_job_generator.yml +18 -0
  21. data/funktor-testapp/funktor_config/funktor.yml +114 -0
  22. data/funktor-testapp/funktor_config/iam_permissions/activity_table.yml +5 -0
  23. data/funktor-testapp/funktor_config/iam_permissions/default_queue.yml +8 -0
  24. data/funktor-testapp/funktor_config/iam_permissions/incoming_job_queue.yml +8 -0
  25. data/funktor-testapp/funktor_config/iam_permissions/jobs_table.yml +5 -0
  26. data/funktor-testapp/funktor_config/iam_permissions/jobs_table_secondary_index.yml +8 -0
  27. data/funktor-testapp/funktor_config/iam_permissions/low_concurrency_queue.yml +8 -0
  28. data/funktor-testapp/funktor_config/iam_permissions/ssm.yml +5 -0
  29. data/funktor-testapp/funktor_config/package.yml +11 -0
  30. data/funktor-testapp/funktor_config/resources/activity_table.yml +22 -0
  31. data/funktor-testapp/funktor_config/resources/cloudwatch_dashboard.yml +809 -0
  32. data/funktor-testapp/funktor_config/resources/default_queue.yml +22 -0
  33. data/funktor-testapp/funktor_config/resources/incoming_job_queue.yml +22 -0
  34. data/funktor-testapp/funktor_config/resources/incoming_job_queue_user.yml +26 -0
  35. data/funktor-testapp/funktor_config/resources/jobs_table.yml +56 -0
  36. data/funktor-testapp/funktor_config/resources/low_concurrency_queue.yml +22 -0
  37. data/funktor-testapp/funktor_config/ruby_layer.yml +11 -0
  38. data/funktor-testapp/funktor_init.yml +69 -0
  39. data/funktor-testapp/lambda_event_handlers/default_queue_handler.rb +8 -0
  40. data/funktor-testapp/lambda_event_handlers/incoming_job_handler.rb +8 -0
  41. data/funktor-testapp/lambda_event_handlers/job_activator.rb +8 -0
  42. data/funktor-testapp/lambda_event_handlers/low_concurrency_queue_handler.rb +8 -0
  43. data/funktor-testapp/lambda_event_handlers/random_job_generator.rb +35 -0
  44. data/funktor-testapp/package-lock.json +248 -0
  45. data/funktor-testapp/package.json +8 -0
  46. data/funktor-testapp/serverless.yml +66 -0
  47. data/funktor.gemspec +4 -1
  48. data/lib/active_job/queue_adapters/funktor_adapter.rb +3 -3
  49. data/lib/funktor/activity_tracker.rb +106 -0
  50. data/lib/funktor/cli/bootstrap.rb +0 -1
  51. data/lib/funktor/cli/init.rb +13 -0
  52. data/lib/funktor/cli/templates/app/workers/hello_worker.rb +1 -1
  53. data/lib/funktor/cli/templates/funktor_config/environment.yml +4 -0
  54. data/lib/funktor/cli/templates/funktor_config/function_definitions/incoming_job_handler.yml +3 -1
  55. data/lib/funktor/cli/templates/funktor_config/function_definitions/job_activator.yml +7 -0
  56. data/lib/funktor/cli/templates/funktor_config/function_definitions/work_queue_handler.yml +3 -1
  57. data/lib/funktor/cli/templates/funktor_config/funktor.yml +32 -6
  58. data/lib/funktor/cli/templates/funktor_config/iam_permissions/activity_table.yml +5 -0
  59. data/lib/funktor/cli/templates/funktor_config/iam_permissions/jobs_table.yml +5 -0
  60. data/lib/funktor/cli/templates/funktor_config/iam_permissions/jobs_table_secondary_index.yml +8 -0
  61. data/lib/funktor/cli/templates/funktor_config/resources/activity_table.yml +22 -0
  62. data/lib/funktor/cli/templates/funktor_config/resources/cloudwatch_dashboard.yml +13 -12
  63. data/lib/funktor/cli/templates/funktor_config/resources/incoming_job_queue.yml +2 -2
  64. data/lib/funktor/cli/templates/funktor_config/resources/jobs_table.yml +56 -0
  65. data/lib/funktor/cli/templates/funktor_config/resources/work_queue.yml +2 -2
  66. data/lib/funktor/cli/templates/funktor_init.yml.tt +16 -16
  67. data/lib/funktor/cli/templates/lambda_event_handlers/job_activator.rb +8 -0
  68. data/lib/funktor/cli/templates/lambda_event_handlers/work_queue_handler.rb +1 -1
  69. data/lib/funktor/cli/templates/serverless.yml +3 -2
  70. data/lib/funktor/counter.rb +4 -1
  71. data/lib/funktor/incoming_job_handler.rb +54 -18
  72. data/lib/funktor/job.rb +57 -7
  73. data/lib/funktor/job_activator.rb +124 -0
  74. data/lib/funktor/job_pusher.rb +0 -2
  75. data/lib/funktor/middleware/metrics.rb +8 -3
  76. data/lib/funktor/shard_utils.rb +6 -0
  77. data/lib/funktor/testing.rb +50 -47
  78. data/lib/funktor/version.rb +1 -1
  79. data/lib/funktor/web/application.rb +139 -0
  80. data/lib/funktor/web/views/index.erb +3 -0
  81. data/lib/funktor/web/views/layout.erb +58 -0
  82. data/lib/funktor/web/views/processing.erb +29 -0
  83. data/lib/funktor/web/views/queued.erb +29 -0
  84. data/lib/funktor/web/views/retries.erb +35 -0
  85. data/lib/funktor/web/views/scheduled.erb +26 -0
  86. data/lib/funktor/web/views/stats.erb +9 -0
  87. data/lib/funktor/web/views/table_stats_with_buttons.erb +11 -0
  88. data/lib/funktor/web.rb +1 -0
  89. data/lib/funktor/work_queue_handler.rb +101 -0
  90. data/lib/funktor/worker/funktor_options.rb +3 -1
  91. data/lib/funktor/worker.rb +8 -18
  92. data/lib/funktor.rb +52 -20
  93. metadata +109 -3
  94. data/lib/funktor/active_job_handler.rb +0 -58
@@ -0,0 +1,124 @@
1
+ require 'aws-sdk-dynamodb'
2
+ require 'aws-sdk-sqs'
3
+
4
+ module Funktor
5
+ class JobActivator
6
+
7
+ def initialize
8
+ @tracker = Funktor::ActivityTracker.new
9
+ end
10
+
11
+ def dynamodb_client
12
+ @dynamodb_client ||= ::Aws::DynamoDB::Client.new
13
+ end
14
+
15
+ def sqs_client
16
+ @sqs_client ||= ::Aws::SQS::Client.new
17
+ end
18
+
19
+ def delayed_job_table
20
+ ENV['FUNKTOR_JOBS_TABLE']
21
+ end
22
+
23
+ def jobs_to_activate
24
+ # TODO : The lookahead time here should be configurable
25
+ # If this doesn't match the setting in the IncomingJobHandler some jobs
26
+ # might be activated and then immediately re-scheduled instead of being
27
+ # queued, which leads to kind of confusing stats for the "incoming" stat.
28
+ # (Come to think of it, the incoming stat is kind of confusting anyway since
29
+ # it reflects retries and scheduled jobs activations...)
30
+ target_time = (Time.now + 60).utc
31
+ query_params = {
32
+ expression_attribute_values: {
33
+ ":queueable" => "true",
34
+ ":targetTime" => target_time.iso8601
35
+ },
36
+ key_condition_expression: "queueable = :queueable AND performAt < :targetTime",
37
+ projection_expression: "jobId, jobShard, category",
38
+ table_name: delayed_job_table,
39
+ index_name: "performAtIndex"
40
+ }
41
+ resp = dynamodb_client.query(query_params)
42
+ return resp.items
43
+ end
44
+
45
+ def queue_for_job(job)
46
+ queue_name = job.queue || 'default'
47
+ queue_constant = "FUNKTOR_#{queue_name.underscore.upcase}_QUEUE"
48
+ Funktor.logger.debug "queue_constant = #{queue_constant}"
49
+ Funktor.logger.debug "ENV value = #{ENV[queue_constant]}"
50
+ ENV[queue_constant] || ENV['FUNKTOR_DEFAULT_QUEUE']
51
+ end
52
+
53
+ def handle_item(item)
54
+ job_shard = item["jobShard"]
55
+ job_id = item["jobId"]
56
+ current_category = item["category"]
57
+ Funktor.logger.debug "jobShard = #{item['jobShard']}"
58
+ Funktor.logger.debug "jobId = #{item['jobId']}"
59
+ Funktor.logger.debug "current_category = #{current_category}"
60
+ activate_job(job_shard, job_id, current_category)
61
+ end
62
+
63
+ def activate_job(job_shard, job_id, current_category, queue_immediately = false)
64
+ # First we conditionally update the item in Dynamo to be sure that another scheduler hasn't gotten
65
+ # to it, and if that works then send to SQS. This is basically how Sidekiq scheduler works.
66
+ response = dynamodb_client.update_item({
67
+ key: {
68
+ "jobShard" => job_shard,
69
+ "jobId" => job_id
70
+ },
71
+ update_expression: "SET category = :category, queueable = :queueable",
72
+ condition_expression: "category = :current_category",
73
+ expression_attribute_values: {
74
+ ":current_category" => current_category,
75
+ ":queueable" => "false",
76
+ ":category" => "queued"
77
+ },
78
+ table_name: delayed_job_table,
79
+ return_values: "ALL_OLD"
80
+ })
81
+ if response.attributes # this means the record was still there in the state we expected
82
+ Funktor.logger.debug "response.attributes ====== "
83
+ Funktor.logger.debug response.attributes
84
+ job = Funktor::Job.new(response.attributes["payload"])
85
+ Funktor.logger.debug "we created a job from payload"
86
+ Funktor.logger.debug response.attributes["payload"]
87
+ Funktor.logger.debug "queueing to #{job.retry_queue_url}"
88
+ if queue_immediately
89
+ job.delay = 0
90
+ end
91
+ sqs_client.send_message({
92
+ queue_url: job.retry_queue_url,
93
+ message_body: job.to_json
94
+ #delay_seconds: job.delay
95
+ })
96
+ if job.is_retry?
97
+ # We don't track here because we send stuff back to the incoming job queue and we track the
98
+ # :retryActivated even there.
99
+ # TODO - Once we're sure this is all working right we can delete the commented out line.
100
+ #@tracker.track(:retryActivated, job)
101
+ else
102
+ @tracker.track(:scheduledJobActivated, job)
103
+ end
104
+ end
105
+ rescue ::Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
106
+ # This means that a different instance of the JobActivator (or someone doing stuff in the web UI)
107
+ # got to the job first.
108
+ Funktor.logger.debug "#{e.to_s} : #{e.message}"
109
+ Funktor.logger.debug e.backtrace.join("\n")
110
+ end
111
+
112
+ def call(event:, context:)
113
+ handled_item_count = 0
114
+ jobs_to_activate.each do |item|
115
+ if context.get_remaining_time_in_millis < 5_000 # This lets us exit gracefully and resume on the next round instead of getting forcibly killed.
116
+ puts "Bailing out due to milliseconds remaining #{context.get_remaining_time_in_millis}"
117
+ break
118
+ end
119
+ handle_item(item)
120
+ handled_item_count += 1
121
+ end
122
+ end
123
+ end
124
+ end
@@ -4,8 +4,6 @@ module Funktor
4
4
  class JobPusher
5
5
 
6
6
  def push(payload)
7
- puts "payload ============"
8
- pp payload
9
7
  job_id = SecureRandom.uuid
10
8
  payload[:job_id] = job_id
11
9
 
@@ -10,7 +10,12 @@ module Funktor
10
10
  end
11
11
 
12
12
  def put_metric_to_stdout(time_diff, job)
13
- puts Funktor.dump_json(metric_hash(time_diff, job))
13
+ # NOTE : We use raw_logger here instead of Funktor.loggert o avoid getting extra
14
+ # timestamps or log level information in the log line. We need this specific format to
15
+ # be the only thing in the line so that CloudWatch can parse the logs and use the data.
16
+ # 'unknown' is a log level that will always be logged, no matter what is set in the
17
+ # runtime environment as far as log level.
18
+ Funktor.raw_logger.unknown Funktor.dump_json(metric_hash(time_diff, job))
14
19
  end
15
20
 
16
21
  def metric_hash(time_diff_in_seconds, job)
@@ -43,8 +48,8 @@ module Funktor
43
48
  end
44
49
  end
45
50
 
46
- Funktor.configure_active_job_handler do |config|
47
- config.active_job_handler_middleware do |chain|
51
+ Funktor.configure_work_queue_handler do |config|
52
+ config.work_queue_handler_middleware do |chain|
48
53
  chain.add Funktor::Middleware::Metrics
49
54
  end
50
55
  end
@@ -0,0 +1,6 @@
1
+ module ShardUtils
2
+ def calculate_shard(job_id)
3
+ # TODO - Should the number of shards be configurable?
4
+ job_id.sum % 64
5
+ end
6
+ end
@@ -1,7 +1,9 @@
1
1
  require 'funktor/worker'
2
+ require 'funktor/job_pusher'
2
3
  require 'funktor/fake_job_queue'
3
4
 
4
5
  module Funktor
6
+
5
7
  module Worker
6
8
  def self.clear_all
7
9
  Funktor::FakeJobQueue.clear_all
@@ -25,67 +27,68 @@ module Funktor
25
27
  end
26
28
  end
27
29
  end
30
+
28
31
  class Testing
32
+ class << self
33
+ attr_accessor :mode
29
34
 
30
- # TODO : We probably shouldn't use middleware for this. We should alter the behavior
31
- # of the JobPusher in the testing module isntead.
32
- def self.inline!(&block)
33
- unless block_given?
34
- raise "Funktor inline testing mode can only be called in block form."
35
+ def inline?
36
+ mode == :inline
35
37
  end
36
- Funktor.configure_job_pusher do |config|
37
- config.job_pusher_middleware do |chain|
38
- chain.add Funktor::InlineJobPusherMiddleware
39
- end
38
+
39
+ def fake?
40
+ mode == :fake
40
41
  end
41
- yield
42
- Funktor.configure_job_pusher do |config|
43
- config.job_pusher_middleware do |chain|
44
- chain.remove Funktor::InlineJobPusherMiddleware
42
+
43
+ def inline!(&block)
44
+ unless block_given?
45
+ raise "Funktor inline testing mode can only be called in block form."
45
46
  end
47
+ set_mode(:inline, &block)
46
48
  end
47
- end
48
- def self.fake!(&block)
49
- if block_given?
50
- Funktor.configure_job_pusher do |config|
51
- config.job_pusher_middleware do |chain|
52
- chain.add Funktor::FakeJobPusherMiddleware
53
- end
54
- end
55
- yield
56
- Funktor.configure_job_pusher do |config|
57
- config.job_pusher_middleware do |chain|
58
- chain.remove Funktor::FakeJobPusherMiddleware
59
- end
60
- end
61
- else
62
- Funktor.configure_job_pusher do |config|
63
- config.job_pusher_middleware do |chain|
64
- chain.add Funktor::FakeJobPusherMiddleware
49
+
50
+ def fake!(&block)
51
+ set_mode(:fake, &block)
52
+ end
53
+
54
+ def disable!
55
+ set_mode(:disabled)
56
+ end
57
+
58
+ def set_mode(new_mode, &block)
59
+ if block_given?
60
+ original_mode = mode
61
+ self.mode = new_mode
62
+ begin
63
+ yield
64
+ ensure
65
+ self.mode = original_mode
65
66
  end
67
+ else
68
+ self.mode = new_mode
66
69
  end
67
70
  end
68
71
  end
69
- def self.disable!
70
- Funktor.configure_job_pusher do |config|
71
- config.job_pusher_middleware do |chain|
72
- chain.remove Funktor::FakeJobPusherMiddleware
72
+ end
73
+
74
+ module TestingPusher
75
+ def push(payload)
76
+ if Funktor::Testing.inline?
77
+ Funktor.job_pusher_middleware.invoke(payload) do
78
+ payload = payload.with_indifferent_access
79
+ worker = Object.const_get payload["worker"]
80
+ worker.new.perform(*payload["worker_params"])
73
81
  end
82
+ elsif Funktor::Testing.fake?
83
+ Funktor.job_pusher_middleware.invoke(payload) do
84
+ Funktor::FakeJobQueue.push(payload)
85
+ end
86
+ else
87
+ super
74
88
  end
75
89
  end
76
90
  end
77
91
 
78
- class InlineJobPusherMiddleware
79
- def call(payload)
80
- payload = payload.with_indifferent_access
81
- worker = Object.const_get payload["worker"]
82
- worker.new.perform(*payload["worker_params"])
83
- end
84
- end
92
+ Funktor::JobPusher.prepend TestingPusher
85
93
 
86
- class FakeJobPusherMiddleware
87
- def call(payload)
88
- Funktor::FakeJobQueue.push(payload)
89
- end
90
- end
91
94
  end
@@ -1,3 +1,3 @@
1
1
  module Funktor
2
- VERSION = "0.4.7"
2
+ VERSION = "0.6.2"
3
3
  end
@@ -0,0 +1,139 @@
1
+ require 'sinatra/base'
2
+ require 'aws-sdk-dynamodb'
3
+ require_relative '../../funktor'
4
+ require_relative '../../funktor/shard_utils'
5
+ require_relative '../../funktor/activity_tracker'
6
+
7
+ module Funktor
8
+ module Web
9
+ class Application < Sinatra::Base
10
+ include ShardUtils
11
+
12
+ get '/' do
13
+ erb :index, layout: :layout, locals: {
14
+ activity_data: get_activity_data
15
+ }
16
+ end
17
+
18
+ get '/scheduled' do
19
+ erb :scheduled, layout: :layout, locals: {
20
+ activity_data: get_activity_data,
21
+ jobs: get_jobs('scheduled')
22
+ }
23
+ end
24
+
25
+ get '/retries' do
26
+ erb :retries, layout: :layout, locals: {
27
+ activity_data: get_activity_data,
28
+ jobs: get_jobs('retry')
29
+ }
30
+ end
31
+
32
+ get '/queued' do
33
+ erb :queued, layout: :layout, locals: {
34
+ activity_data: get_activity_data,
35
+ jobs: get_jobs('queued')
36
+ }
37
+ end
38
+
39
+ get '/processing' do
40
+ erb :processing, layout: :layout, locals: {
41
+ activity_data: get_activity_data,
42
+ jobs: get_jobs('processing')
43
+ }
44
+ end
45
+
46
+ post '/update_jobs' do
47
+ job_ids = params[:job_id]
48
+ if job_ids.is_a?(String)
49
+ job_ids = [job_ids]
50
+ end
51
+ job_ids ||= []
52
+ puts "params[:submit] = #{params[:submit]}"
53
+ puts "job_ids = #{job_ids}"
54
+ puts "params[:source] = #{params[:source]}"
55
+ if params[:submit] == "Delete Selected Jobs"
56
+ delete_jobs(job_ids, params[:source])
57
+ elsif params[:submit] == "Queue Selected Jobs"
58
+ queue_jobs(job_ids, params[:source])
59
+ end
60
+ redirect request.referrer
61
+ end
62
+
63
+ def get_jobs(category)
64
+ "Jobs of type #{category}"
65
+ query_params = {
66
+ expression_attribute_values: {
67
+ ":category" => category
68
+ },
69
+ key_condition_expression: "category = :category",
70
+ projection_expression: "payload, performAt, jobId, jobShard",
71
+ table_name: ENV['FUNKTOR_JOBS_TABLE'],
72
+ index_name: "categoryIndex"
73
+ }
74
+ resp = dynamodb_client.query(query_params)
75
+ @items = resp.items
76
+ @jobs = @items.map{ |item| Funktor::Job.new(item["payload"]) }
77
+ return @jobs
78
+ end
79
+
80
+ def get_activity_data
81
+ query_params = {
82
+ expression_attribute_values: {
83
+ ":category" => "stat"
84
+ },
85
+ key_condition_expression: "category = :category",
86
+ projection_expression: "statName, stat_value",
87
+ table_name: ENV['FUNKTOR_ACTIVITY_TABLE']
88
+ }
89
+ resp = dynamodb_client.query(query_params)
90
+ @activity_stats = {}
91
+ resp.items.each do |item|
92
+ @activity_stats[item["statName"]] = item["stat_value"].to_i
93
+ end
94
+ return @activity_stats
95
+ end
96
+
97
+ def queue_jobs(job_ids, source)
98
+ job_activator = Funktor::JobActivator.new
99
+ job_ids.each do |job_id|
100
+ job_shard = calculate_shard(job_id)
101
+ job_activator.activate_job(job_shard, job_id, source, true)
102
+ end
103
+ end
104
+
105
+ def delete_jobs(job_ids, source)
106
+ @tracker = Funktor::ActivityTracker.new
107
+ job_ids.each do |job_id|
108
+ delete_single_job(job_id, source)
109
+ end
110
+ end
111
+
112
+ def delete_single_job(job_id, source)
113
+ response = dynamodb_client.delete_item({
114
+ key: {
115
+ "jobShard" => calculate_shard(job_id),
116
+ "jobId" => job_id
117
+ },
118
+ table_name: ENV['FUNKTOR_JOBS_TABLE'],
119
+ return_values: "ALL_OLD"
120
+ })
121
+ if response.attributes # this means the record was still there
122
+ if source == "scheduled"
123
+ @tracker.track(:scheduledJobDeleted, nil)
124
+ elsif source == "retry"
125
+ @tracker.track(:retryDeleted, nil)
126
+ end
127
+ end
128
+ end
129
+
130
+ def dynamodb_client
131
+ @dynamodb_client ||= ::Aws::DynamoDB::Client.new
132
+ end
133
+
134
+ # start the server if ruby file executed directly
135
+ run! if app_file == $0
136
+ end
137
+ end
138
+ end
139
+
@@ -0,0 +1,3 @@
1
+ <p>Hellerrrrrrrrr!</p>
2
+ <p>Activity data = <%= activity_data %></p>
3
+
@@ -0,0 +1,58 @@
1
+ <html>
2
+ <head>
3
+ <link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
4
+ <style type="text/css" media="screen">
5
+ /* Green Light scheme (Default) */
6
+ /* Can be forced with data-theme="light" */
7
+ [data-theme="light"],
8
+ :root:not([data-theme="dark"]) {
9
+ --primary: #43a047;
10
+ --primary-hover: #388e3c;
11
+ --primary-focus: rgba(67, 160, 71, 0.125);
12
+ --primary-inverse: #FFF;
13
+ }
14
+
15
+ /* Green Dark scheme (Auto) */
16
+ /* Automatically enabled if user has Dark mode enabled */
17
+ @media only screen and (prefers-color-scheme: dark) {
18
+ :root:not([data-theme="light"]) {
19
+ --primary: #43a047;
20
+ --primary-hover: #4caf50;
21
+ --primary-focus: rgba(67, 160, 71, 0.25);
22
+ --primary-inverse: #FFF;
23
+ }
24
+ }
25
+
26
+ /* Green Dark scheme (Forced) */
27
+ /* Enabled if forced with data-theme="dark" */
28
+ [data-theme="dark"] {
29
+ --primary: #43a047;
30
+ --primary-hover: #4caf50;
31
+ --primary-focus: rgba(67, 160, 71, 0.25);
32
+ --primary-inverse: #FFF;
33
+ }
34
+
35
+ /* Green (Common styles) */
36
+ :root {
37
+ --form-element-active-border-color: var(--primary);
38
+ --form-element-focus-color: var(--primary-focus);
39
+ --switch-color: var(--primary-inverse);
40
+ --switch-checked-background-color: var(--primary);
41
+ }
42
+
43
+ /* custom stuff for the funktor dashboard */
44
+ table.header h5{
45
+ margin-bottom: 0;
46
+ }
47
+ body > main{
48
+ padding: 0;
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <%= erb :stats %>
54
+ <main class="container-fluid">
55
+ <%= yield %>
56
+ </main>
57
+ </body>
58
+ </html>