funktor 0.4.6 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.tool-versions +2 -0
- data/Gemfile.lock +24 -5
- data/funktor-testapp/.envrc +1 -0
- data/funktor-testapp/.gitignore +7 -0
- data/funktor-testapp/Gemfile +25 -0
- data/funktor-testapp/Gemfile.lock +51 -0
- data/funktor-testapp/app/services/job_flood.rb +38 -0
- data/funktor-testapp/app/workers/audit_worker.rb +49 -0
- data/funktor-testapp/app/workers/greetings_worker.rb +3 -0
- data/funktor-testapp/app/workers/hello_worker.rb +18 -0
- data/funktor-testapp/app/workers/single_thread_audit_worker.rb +3 -0
- data/funktor-testapp/deploy-dev.sh +5 -0
- data/funktor-testapp/funktor_config/boot.rb +17 -0
- data/funktor-testapp/funktor_config/environment.yml +15 -0
- data/funktor-testapp/funktor_config/function_definitions/default_queue_handler.yml +13 -0
- data/funktor-testapp/funktor_config/function_definitions/incoming_job_handler.yml +13 -0
- data/funktor-testapp/funktor_config/function_definitions/job_activator.yml +7 -0
- data/funktor-testapp/funktor_config/function_definitions/low_concurrency_queue_handler.yml +13 -0
- data/funktor-testapp/funktor_config/function_definitions/random_job_generator.yml +18 -0
- data/funktor-testapp/funktor_config/funktor.yml +114 -0
- data/funktor-testapp/funktor_config/iam_permissions/activity_table.yml +5 -0
- data/funktor-testapp/funktor_config/iam_permissions/default_queue.yml +8 -0
- data/funktor-testapp/funktor_config/iam_permissions/incoming_job_queue.yml +8 -0
- data/funktor-testapp/funktor_config/iam_permissions/jobs_table.yml +5 -0
- data/funktor-testapp/funktor_config/iam_permissions/jobs_table_secondary_index.yml +8 -0
- data/funktor-testapp/funktor_config/iam_permissions/low_concurrency_queue.yml +8 -0
- data/funktor-testapp/funktor_config/iam_permissions/ssm.yml +5 -0
- data/funktor-testapp/funktor_config/package.yml +11 -0
- data/funktor-testapp/funktor_config/resources/activity_table.yml +22 -0
- data/funktor-testapp/funktor_config/resources/cloudwatch_dashboard.yml +809 -0
- data/funktor-testapp/funktor_config/resources/default_queue.yml +22 -0
- data/funktor-testapp/funktor_config/resources/incoming_job_queue.yml +22 -0
- data/funktor-testapp/funktor_config/resources/incoming_job_queue_user.yml +26 -0
- data/funktor-testapp/funktor_config/resources/jobs_table.yml +56 -0
- data/funktor-testapp/funktor_config/resources/low_concurrency_queue.yml +22 -0
- data/funktor-testapp/funktor_config/ruby_layer.yml +11 -0
- data/funktor-testapp/funktor_init.yml +69 -0
- data/funktor-testapp/lambda_event_handlers/default_queue_handler.rb +8 -0
- data/funktor-testapp/lambda_event_handlers/incoming_job_handler.rb +8 -0
- data/funktor-testapp/lambda_event_handlers/job_activator.rb +8 -0
- data/funktor-testapp/lambda_event_handlers/low_concurrency_queue_handler.rb +8 -0
- data/funktor-testapp/lambda_event_handlers/random_job_generator.rb +35 -0
- data/funktor-testapp/package-lock.json +248 -0
- data/funktor-testapp/package.json +8 -0
- data/funktor-testapp/serverless.yml +66 -0
- data/funktor.gemspec +4 -1
- data/lib/active_job/queue_adapters/funktor_adapter.rb +3 -3
- data/lib/funktor/activity_tracker.rb +106 -0
- data/lib/funktor/cli/bootstrap.rb +0 -1
- data/lib/funktor/cli/init.rb +13 -0
- data/lib/funktor/cli/templates/app/workers/hello_worker.rb +1 -1
- data/lib/funktor/cli/templates/funktor_config/environment.yml +4 -0
- data/lib/funktor/cli/templates/funktor_config/function_definitions/incoming_job_handler.yml +3 -1
- data/lib/funktor/cli/templates/funktor_config/function_definitions/job_activator.yml +7 -0
- data/lib/funktor/cli/templates/funktor_config/function_definitions/work_queue_handler.yml +3 -1
- data/lib/funktor/cli/templates/funktor_config/funktor.yml +32 -6
- data/lib/funktor/cli/templates/funktor_config/iam_permissions/activity_table.yml +5 -0
- data/lib/funktor/cli/templates/funktor_config/iam_permissions/jobs_table.yml +5 -0
- data/lib/funktor/cli/templates/funktor_config/iam_permissions/jobs_table_secondary_index.yml +8 -0
- data/lib/funktor/cli/templates/funktor_config/resources/activity_table.yml +22 -0
- data/lib/funktor/cli/templates/funktor_config/resources/cloudwatch_dashboard.yml +13 -12
- data/lib/funktor/cli/templates/funktor_config/resources/incoming_job_queue.yml +2 -2
- data/lib/funktor/cli/templates/funktor_config/resources/jobs_table.yml +56 -0
- data/lib/funktor/cli/templates/funktor_config/resources/work_queue.yml +2 -2
- data/lib/funktor/cli/templates/funktor_init.yml.tt +16 -16
- data/lib/funktor/cli/templates/lambda_event_handlers/job_activator.rb +8 -0
- data/lib/funktor/cli/templates/lambda_event_handlers/work_queue_handler.rb +1 -1
- data/lib/funktor/cli/templates/serverless.yml +3 -2
- data/lib/funktor/counter.rb +4 -1
- data/lib/funktor/incoming_job_handler.rb +54 -18
- data/lib/funktor/job.rb +57 -7
- data/lib/funktor/job_activator.rb +124 -0
- data/lib/funktor/job_pusher.rb +0 -2
- data/lib/funktor/middleware/metrics.rb +8 -3
- data/lib/funktor/shard_utils.rb +6 -0
- data/lib/funktor/testing.rb +52 -29
- data/lib/funktor/version.rb +1 -1
- data/lib/funktor/web/application.rb +139 -0
- data/lib/funktor/web/views/index.erb +3 -0
- data/lib/funktor/web/views/layout.erb +58 -0
- data/lib/funktor/web/views/processing.erb +29 -0
- data/lib/funktor/web/views/queued.erb +29 -0
- data/lib/funktor/web/views/retries.erb +35 -0
- data/lib/funktor/web/views/scheduled.erb +26 -0
- data/lib/funktor/web/views/stats.erb +9 -0
- data/lib/funktor/web/views/table_stats_with_buttons.erb +11 -0
- data/lib/funktor/web.rb +1 -0
- data/lib/funktor/work_queue_handler.rb +101 -0
- data/lib/funktor/worker/funktor_options.rb +3 -1
- data/lib/funktor/worker.rb +8 -18
- data/lib/funktor.rb +52 -20
- metadata +109 -3
- 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
|
data/lib/funktor/job_pusher.rb
CHANGED
@@ -10,7 +10,12 @@ module Funktor
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def put_metric_to_stdout(time_diff, job)
|
13
|
-
|
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.
|
47
|
-
config.
|
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
|
data/lib/funktor/testing.rb
CHANGED
@@ -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,47 +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
|
-
|
31
|
-
|
32
|
-
config.job_pusher_middleware do |chain|
|
33
|
-
chain.add Funktor::InlineJobPusherMiddleware
|
34
|
-
end
|
35
|
+
def inline?
|
36
|
+
mode == :inline
|
35
37
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
chain.remove Funktor::InlineJobPusherMiddleware
|
40
|
-
end
|
38
|
+
|
39
|
+
def fake?
|
40
|
+
mode == :fake
|
41
41
|
end
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
chain.add Funktor::FakeJobPusherMiddleware
|
42
|
+
|
43
|
+
def inline!(&block)
|
44
|
+
unless block_given?
|
45
|
+
raise "Funktor inline testing mode can only be called in block form."
|
47
46
|
end
|
47
|
+
set_mode(:inline, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fake!(&block)
|
51
|
+
set_mode(:fake, &block)
|
48
52
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
66
|
+
end
|
67
|
+
else
|
68
|
+
self.mode = new_mode
|
53
69
|
end
|
54
70
|
end
|
55
71
|
end
|
56
72
|
end
|
57
73
|
|
58
|
-
|
59
|
-
def
|
60
|
-
|
61
|
-
|
62
|
-
|
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"])
|
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
|
88
|
+
end
|
63
89
|
end
|
64
90
|
end
|
65
91
|
|
66
|
-
|
67
|
-
|
68
|
-
Funktor::FakeJobQueue.push(payload)
|
69
|
-
end
|
70
|
-
end
|
92
|
+
Funktor::JobPusher.prepend TestingPusher
|
93
|
+
|
71
94
|
end
|
data/lib/funktor/version.rb
CHANGED
@@ -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,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>
|