litestack 0.2.2 → 0.2.6

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: bb0952d46398aae021145cdfbadbcd7293da4d0eab2be6008c1d4d027b26d18d
4
- data.tar.gz: 59bd635a7fe85b0d9604eb6e4a781bf0cda462f257e1a43418eb527a965240e0
3
+ metadata.gz: d6db833e61264ea2036338102d0ed1d3e9eb0a5d94a235db5a8f8d4cf390c65f
4
+ data.tar.gz: 5a48dd08c18ab39f2a19462025e351da8f3e2eedf55f779019cef3ed277e12bc
5
5
  SHA512:
6
- metadata.gz: 97e5bf71f98f30ba2c4b46ede2d472fbde3676b38fc2eaa5fda2d0edcb6eb048b05712a227b1b9a1b34183ec34154d7e472f9fe5eec7fb27be5102828a2c9a78
7
- data.tar.gz: '041950f5996b88e5c60a7800172150199d273ab071c906cc608acb0bb2947b2c3c95ca018ea9862c07395ad4c6da730b1439450dc04112b5e70432fcd122101f'
6
+ metadata.gz: a6dd503c63cb0ca49092f9f612a6c087d07b13eefa083ca930931b956e5cf81357f130932e2514b340d2e240b73df377bf3f5a55516b7bf444bbe4149c60656c
7
+ data.tar.gz: a1749b7eba0f8d155fc5e18b7a2da8d26df25ee4bf954e8c15724b8cb8164fb9f23aeab9387793420714e27fd2e13d357a91ece891dfca49afb7dbf6f8a91564
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.6] - 2023-07-16
4
+
5
+ - Much improved database location setting (thanks Brad Gessler)
6
+ - A Rails generator for better Rails Litestack defaults (thanks Brad Gessler)
7
+ - Revamped Litemetric, now much faster and more accurate (still experimental)
8
+ - Introduced Liteboard, a dashboard for viewing Litemetric data
9
+
10
+ ## [0.2.3] - 2023-05-20
11
+
12
+ - Cut back on options defined in the Litejob Rails adapter
13
+
14
+ ## [0.2.2] - 2023-05-18
15
+
16
+ - Fix default queue location in Litejob
17
+
18
+
3
19
  ## [0.2.1] - 2023-05-08
4
20
 
5
21
  - Fix a race condition in Litecable
data/Gemfile CHANGED
@@ -6,3 +6,5 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
 
9
+
10
+ gem "rack", "~> 3.0"
data/README.md CHANGED
@@ -31,19 +31,13 @@ Litestack is still pretty young and under heavy development, but you are welcome
31
31
 
32
32
  ## Installation
33
33
 
34
- Add this line to your application's Gemfile:
34
+ Add the `litestack` gem line to your application's Gemfile:
35
35
 
36
- ```ruby
37
- gem 'litestack'
38
- ```
39
-
40
- And then execute:
41
-
42
- $ bundle install
36
+ $ bundle add litestack
43
37
 
44
- Or install it yourself as:
38
+ To configure a Rails application to run the full litestack, run:
45
39
 
46
- $ gem install litestack
40
+ $ rails generate litestack:install
47
41
 
48
42
  ## Usage
49
43
 
@@ -29,7 +29,7 @@ if env == "a" # threaded
29
29
  end
30
30
 
31
31
  require_relative '../lib/active_job/queue_adapters/litejob_adapter'
32
- puts Litesupport.environment
32
+ puts Litesupport.scheduler
33
33
 
34
34
  RailsJob.queue_adapter = :litejob
35
35
  t = Time.now.to_f
@@ -28,7 +28,7 @@ end
28
28
 
29
29
  require './uljob.rb'
30
30
 
31
- STDERR.puts "litejob started in #{Litesupport.environment} environmnet"
31
+ STDERR.puts "litejob started in #{Litesupport.scheduler} environmnet"
32
32
 
33
33
  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
34
  bench("enqueuing litejobs", count) do |i|
data/bin/liteboard ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative '../lib/litestack/liteboard/liteboard'
6
+ DEFAULTS = {
7
+ config_path: Litemetric::DEFAULT_OPTIONS[:config_path],
8
+ path: Litemetric::DEFAULT_OPTIONS[:path]
9
+ }
10
+
11
+ options = {
12
+ config_path: nil,
13
+ path: nil,
14
+ deamonize: false,
15
+ Port: 9292,
16
+ Host: 'localhost',
17
+ environment: 'production',
18
+ quiet: false
19
+ }
20
+
21
+ OptionParser.new do |parser|
22
+ parser.banner = "Usage: liteboard [options]"
23
+ parser.on("-d", "--database PATH", "path to SQLite file (default: #{DEFAULTS[:path]})") { |v| options[:path] = v }
24
+ parser.on("-c", "--config PATH", "path to a litemetric config file (default: #{DEFAULTS[:config_path]}) ") { |v| options[:config_path] = v }
25
+ parser.on("-s", "--server SERVER", "use SERVER (e.g. puma/falcon/iodine)") { |v| options[:port] = v }
26
+ parser.on("-H", "--host HOST", "listen on HOST (default: #{options[:Host]})") { |v| options[:Host] = v }
27
+ parser.on("-p", "--port PORT", "use PORT (default: #{options[:Port]})") { |v| options[:Port] = v.to_i rescue options[:Port] }
28
+ parser.on("-D", "--deamonize", "run in the background") { |v| options[:deamonize] = true }
29
+ parser.on("-E", "--env ENVIRONMENT", "which environment to use (default: #{options[:environment]})") { |v| options[:environment] = v }
30
+ parser.on("-q", "--quiet", "turn off logging") { |v| options[:quiet] = true }
31
+ parser.on("-h", "--help", "print this message") do
32
+ puts parser
33
+ exit
34
+ end
35
+ end.parse!
36
+
37
+ def check_database(path)
38
+ unless File.exist?(path)
39
+ puts "liteboard: missing database file, please ensure the db path is correct"
40
+ puts "liteboard: exiting"
41
+ exit
42
+ end
43
+ end
44
+
45
+ check_database(options[:path]) if options[:path]
46
+
47
+ # if there is a config file then we need to check it
48
+ config = nil
49
+ if options[:config_path]
50
+ begin
51
+ config = Yaml.load_file(options[:config_path])
52
+ rescue
53
+ puts "liteboard: missing or bad config file, please ensure the config file path is correct"
54
+ puts "liteboard: exiting"
55
+ exit
56
+ end
57
+ else # no config path! use the default
58
+ config = Yaml.load_file(DEFAULTS[:config_path]) rescue nil
59
+ end
60
+
61
+ if config
62
+ if options[:path].nil?
63
+ path = config['path'] || config[options[:environment]]['path']
64
+ options[:path] = path
65
+ end
66
+ end
67
+
68
+ # if still no path we assume a default db path
69
+ options[:path] = DEFAULTS[:path] if options[:path].nil?
70
+
71
+
72
+ # check the validity of the path before starting the server
73
+ check_database(options[:path])
74
+ Litemetric.options = options
75
+ litemetric = Litemetric.instance
76
+ options[:app] = Liteboard.app
77
+
78
+ require_relative '../lib/litestack'
79
+ puts "Starting Liteboard version #{Litestack::VERSION}"
80
+
81
+ Rack::Server.start(options)
@@ -33,11 +33,7 @@ module ActiveJob
33
33
 
34
34
  DEFAULT_OPTIONS = {
35
35
  config_path: "./config/litejob.yml",
36
- path: "./queue.db",
37
- queues: [["default", 1]],
38
36
  logger: nil, # Rails performs its logging already
39
- retries: 5, # It is recommended to stop retries at the Rails level
40
- workers: 5
41
37
  }
42
38
 
43
39
  include ::Litejob
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Installs litestack gem and configures the project to use litestack
3
+ in development, test, and production environments.
4
+
5
+ Example:
6
+ bin/rails generate litestack:install
7
+
8
+ This will modify:
9
+ config/environments/production.rb
10
+ config/database.yml
11
+ config/cable.yml
@@ -0,0 +1,35 @@
1
+ class Litestack::InstallGenerator < Rails::Generators::Base
2
+ source_root File.expand_path("templates", __dir__)
3
+
4
+ # Force copy configuratioon files so Rails installs don't ask questions
5
+ # that less experienced people might not understand. The more Sr folks.
6
+ # will know to check git to look at what changed.
7
+ def modify_database_adapater
8
+ copy_file "database.yml", "config/database.yml", force: true
9
+ end
10
+
11
+ def modify_action_cable_adapter
12
+ copy_file "cable.yml", "config/cable.yml", force: true
13
+ end
14
+
15
+ def modify_cache_store_adapter
16
+ gsub_file "config/environments/production.rb",
17
+ "# config.cache_store = :mem_cache_store",
18
+ "config.cache_store = :litecache"
19
+ end
20
+
21
+ def modify_active_job_adapter
22
+ gsub_file "config/environments/production.rb",
23
+ "# config.active_job.queue_adapter = :resque",
24
+ "config.active_job.queue_adapter = :litejob"
25
+ end
26
+
27
+ def modify_gitignore
28
+ append_file ".gitignore", <<~TEXT
29
+
30
+ # Ignore default Litestack SQLite databases.
31
+ /db/**/*.sqlite3
32
+ /db/**/*.sqlite3-*
33
+ TEXT
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ development:
2
+ adapter: litecable
3
+
4
+ test:
5
+ adapter: test
6
+
7
+ staging:
8
+ adapter: litecable
9
+
10
+ production:
11
+ adapter: litecable
@@ -0,0 +1,30 @@
1
+ # SQLite. Versions 3.8.0 and up are supported.
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem "sqlite3"
6
+ #
7
+ # `Litesupport.root.join("data.sqlite3")` stores
8
+ # application data in the path `./db/#{Rails.env}/data.sqlite3`
9
+ default: &default
10
+ adapter: litedb
11
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
12
+ timeout: 5000
13
+ database: <%= Litesupport.root.join("data.sqlite3") %>
14
+
15
+ development:
16
+ <<: *default
17
+
18
+ # Warning: The database defined as "test" will be erased and
19
+ # re-generated from your development database when you run "rake".
20
+ # Do not set this db to the same as development or production.
21
+ test:
22
+ <<: *default
23
+
24
+ # Warning: Make sure your production database path is on a persistent
25
+ # volume, otherwise your application data could be deleted between deploys.
26
+ #
27
+ # You may also set the Litesupport.root in production via the
28
+ # `LITESTACK_DATA_PATH` environment variable.
29
+ production:
30
+ <<: *default
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ require 'hanami/router'
3
+ require 'tilt'
4
+ require 'erubi'
5
+
6
+ # require relative so we pick the gem version that corresponds to the liteboard binary
7
+ require_relative '../../litestack/litemetric'
8
+
9
+ class Liteboard
10
+
11
+ @@resolutions = {'minute' => [300, 12], 'hour' => [3600, 24], 'day' => [3600*24, 7], 'week' => [3600*24*7, 53], 'year' => [3600*24*365, 100] }
12
+ @@res_mapping = {'hour' => 'minute', 'day' => 'hour', 'week' => 'day', 'year' => 'week'}
13
+ @@templates = {}
14
+ @@app = Hanami::Router.new do
15
+
16
+ get "/", to: ->(env) do
17
+ Liteboard.new(env).call(:index)
18
+ end
19
+
20
+ get "/topics/:topic", to: ->(env) do
21
+ Liteboard.new(env).call(:topic)
22
+ end
23
+
24
+ get "/topics/:topic/events/:event", to: ->(env) do
25
+ Liteboard.new(env).call(:event)
26
+ end
27
+
28
+ end
29
+
30
+
31
+ def initialize(env)
32
+ @env = env
33
+ @params = @env["router.params"]
34
+ @running = true
35
+ @lm = Litemetric.instance
36
+ end
37
+
38
+ def params(key)
39
+ URI.decode_uri_component("#{@params[key]}")
40
+ end
41
+
42
+ def call(method)
43
+ before
44
+ res = send(method)
45
+ after(res)
46
+ end
47
+
48
+ def render(tpl_name)
49
+ layout = Tilt.new("#{__dir__}/views/layout.erb")
50
+ tpl = Tilt.new("#{__dir__}/views/#{tpl_name.to_s}.erb")
51
+ res = layout.render(self){tpl.render(self)}
52
+ end
53
+
54
+ def after(body=nil)
55
+ [200, {'Cache-Control' => 'no-cache'}, [body]]
56
+ end
57
+
58
+ def before
59
+ @res = params(:res) || 'day'
60
+ @resolution = @@res_mapping[@res]
61
+ if not @resolution
62
+ @res = 'day'
63
+ @resolution = @@res_mapping[@res]
64
+ end
65
+ @step = @@resolutions[@resolution][0]
66
+ @count = @@resolutions[@resolution][1]
67
+ @order = params(:order)
68
+ @order = nil if @order == ''
69
+ @dir = params(:dir)
70
+ @dir = 'desc' if @dir.nil? || @dir == ''
71
+ @dir = @dir.downcase
72
+ @idir = if @dir == "asc"
73
+ "desc"
74
+ else
75
+ "asc"
76
+ end
77
+ @search = params(:search)
78
+ @search = nil if @search == ''
79
+ end
80
+
81
+ def index
82
+ @order = 'topic' unless @order
83
+ topics = @lm.topics
84
+ @topics = @lm.topic_summaries(@resolution, @step * @count, @order, @dir, @search)
85
+ @topics.each do |topic|
86
+ data_points = @lm.topic_data_points(@step, @count, @resolution, topic[0])
87
+ topic << data_points.collect{|r| [r[0],r[2]]}
88
+ end
89
+ render :index
90
+ end
91
+
92
+ def topic
93
+ @order = 'rcount' unless @order
94
+ @topic = params(:topic)
95
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
96
+ @events.each do |event|
97
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event[0])
98
+ event << data_points.collect{|r| [r[0],r[2]]}
99
+ event << data_points.collect{|r| [r[0],r[3]]}
100
+ end
101
+ @snapshot = @lm.snapshot(@topic)
102
+ if @snapshot.empty?
103
+ @snapshot = []
104
+ else
105
+ @snapshot[0] = Oj.load(@snapshot[0]) unless @snapshot[0].nil?
106
+ end
107
+ render :topic
108
+ end
109
+
110
+ def event
111
+ @order = 'rcount' unless @order
112
+ @topic = params(:topic)
113
+ @event = params(:event)
114
+ @keys = @lm.keys_summaries(@topic, @event, @resolution, @order, @dir, @search, @step * @count)
115
+ @keys.each do |key|
116
+ data_points = @lm.key_data_points(@step, @count, @resolution, @topic, @event, key[0])
117
+ key << data_points.collect{|r| [r[0],r[2]]}
118
+ key << data_points.collect{|r| [r[0],r[3]]}
119
+ end
120
+ render :event
121
+ end
122
+
123
+ def index_sort_url(field)
124
+ "/?#{compose_query(field)}"
125
+ end
126
+
127
+ def topic_sort_url(field)
128
+ "/topics/#{encode(@topic)}?#{compose_query(field)}"
129
+ end
130
+
131
+ def event_sort_url(field)
132
+ "/topics/#{encode(@topic)}/events/#{encode(@event)}?#{compose_query(field)}"
133
+ end
134
+
135
+ def compose_query(field)
136
+ field.downcase!
137
+ "res=#{@res}&order=#{field}&dir=#{@order == field ? @idir : @dir}&search=#{@search}"
138
+ end
139
+
140
+ def sorted?(field)
141
+ @order == field
142
+ end
143
+
144
+ def dir(field)
145
+ if sorted?(field)
146
+ if @dir == 'asc'
147
+ return "<span class='material-icons'>arrow_drop_up</span>"
148
+ else
149
+ return "<span class='material-icons'>arrow_drop_down</span>"
150
+ end
151
+ end
152
+ '&nbsp;&nbsp;'
153
+ end
154
+
155
+ def encode(text)
156
+ URI.encode_uri_component(text)
157
+ end
158
+
159
+ def self.app
160
+ @@app
161
+ end
162
+
163
+ end
164
+
165
+
166
+
167
+
168
+ #Rack::Server.start({app: Litebaord.app, daemonize: false})
@@ -0,0 +1,32 @@
1
+ <h5><a href="/?res=<%=@res%>">All Topics</a> > <a href="/topics/<%=@topic%>?res=<%=@res%>"><%=@topic%></a> > <%= @event %></h5>
2
+ <div id="search"><form><input id="search-field" type="text" placeholder="Search keys" onkeydown="search_kd(this)" onkeyup="search_ku(this)" value="<%=@search%>"/></form></div>
3
+ <table class="table sortable">
4
+ <tr>
5
+ <th width="16%" class="<%='sorted' if @order == 'key'%>"><a href="<%=event_sort_url('key')%>">Key</a> <%=dir('key')%></th>
6
+ <th width="8%" class="<%='sorted' if @order == 'rcount'%>"><a href="<%=event_sort_url('rcount')%>">Event Count</a> <%=dir('rcount')%></th>
7
+ <th width="8%" class="<%='sorted' if @order == 'ravg'%>"><a href="<%=event_sort_url('ravg')%>">Avg Value</a> <%=dir('ravg')%></th>
8
+ <th width="8%" class="<%='sorted' if @order == 'rtotal'%>"><a href="<%=event_sort_url('rtotal')%>">Total Value</a> <%=dir('rtotal')%></th>
9
+ <th width="8%" class="<%='sorted' if @order == 'rmin'%>"><a href="<%=event_sort_url('rmin')%>">Min Value</a> <%=dir('rmin')%></th>
10
+ <th width="8%" class="<%='sorted' if @order == 'rmax'%>"><a href="<%=event_sort_url('rmax')%>">Max Value</a> <%=dir('rmax')%></th>
11
+ <th width="22%">Events over time</th>
12
+ <th width="22%">Average value over time</th>
13
+ </tr>
14
+ <% @keys.each do |key|%>
15
+ <tr>
16
+ <td title="<%=key[0]%>"><div class="label"><span><%=key[0]%></span></div></td>
17
+ <td><%=key[1]%></td>
18
+ <td><%="%0.2f" % [key[2]] if key[2]%></td>
19
+ <td><%="%0.2f" % [key[3]] if key[3]%></td>
20
+ <td><%="%0.2f" % [key[4]] if key[4]%></td>
21
+ <td><%="%0.2f" % [key[5]] if key[5]%></td>
22
+ <td class="chart"><span class="inlinecolumn hidden" data-label="Count"><%=Oj.dump(key[6]) if key[7]%></span></td>
23
+ <td class="chart"><span class="inlinecolumn hidden" data-label="Avg Value"><%=Oj.dump(key[7]) if key[7]%></span></td>
24
+ </tr>
25
+ <% end %>
26
+ <% if @keys.empty? %>
27
+ <tr>
28
+ <td class="empty" colspan="8">No data to display</td>
29
+ </tr>
30
+ <% end %>
31
+ </table>
32
+
@@ -0,0 +1,22 @@
1
+
2
+ <h5>All topics</h5>
3
+ <div id="search"><form><input id="search-field" type="text" placeholder="Search topics" onkeydown="search_kd(this)" onkeyup="search_ku(this)" value="<%=@search%>"/></form></div>
4
+ <table class="sortable table">
5
+ <tr>
6
+ <th width="20%" class="<%='sorted' if @order == 'topic'%>"><a href="<%=index_sort_url('topic')%>">Topics</a> <%=dir('topic')%></th>
7
+ <th width="20%" class="<%='sorted' if @order == 'rcount'%>"><a href="<%=index_sort_url('rcount')%>">Total events</a> <%=dir('rcount')%></th>
8
+ <th width="40%">Events over time</th>
9
+ </tr>
10
+ <% @topics.each do |topic|%>
11
+ <tr>
12
+ <td title="<%=topic[0]%>"><div class="label"><a href="./topics/<%=encode(topic[0])%>?res=<%=@res%>"><%=topic[0]%></a></div></td>
13
+ <td><%=topic[3]%></td>
14
+ <td class="chart"><span class="inlinecolumn hidden" data-label="Count"><%=Oj.dump(topic[4]) if topic[4]%></span></td>
15
+ </tr>
16
+ <%end%>
17
+ <% if @topics.empty? %>
18
+ <tr>
19
+ <td class="empty" colspan="5">No data to display</td>
20
+ </tr>
21
+ <% end %>
22
+
@@ -0,0 +1,152 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>liteboard</title>
5
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
6
+ <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Antonio">
9
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
+ <style>
11
+ body {background-color: #fff}
12
+ input, select { border-radius: 3px}
13
+ div#header { width: 100%; border-bottom: 1px solid #089;}
14
+ div.label {max-width:400px;overflow:hidden}
15
+ h1 { font-family: antonio }
16
+ #content{ padding-right: 12px; padding-left: 12px; padding-bottom: 60px;}
17
+ table.head { margin-top: 12px; margin-bottom:12px}
18
+ table.head select { color: #078; background-color: #fff; font-weight: normal }
19
+ .table th { color: #078; font-weight: normal; }
20
+ .table th.sorted { font-weight: bold }
21
+ .table td { color: #444; vertical-align:middle; font-size: 18px}
22
+ .table td:first-child { color: #444; vertical-align:middle; font-size: 15px; font-weight:normal}
23
+ .table td.empty { text-align:center}
24
+ a { color: #078; }
25
+ a .logo { color: #000;}
26
+ a:visited { color: #078; }
27
+ //table.summary { width: 50% }
28
+ span.hidden { display: none}
29
+ div#search {margin-bottom: 8px}
30
+ div#footer {position:fixed; left:0px; height: 40px; width:100%; background-color:#0891; border-top: #0893 1px solid; padding: 8px; bottom: 0; text-align: right}
31
+ .logo{font-family: antonio}
32
+ .logo-half{ color: #078 }
33
+ .smaller { font-size: 24px; font-weight: normal}
34
+ .token {background-color: #ed9}
35
+ svg > g > g.google-visualization-tooltip { pointer-events : none }
36
+ .material-icons { vertical-align: middle}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div id="content">
41
+ <div id="header">
42
+ <h1><span class="logo"><span class="logo-half">lite</span>board | </span> <span class="logo smaller">the <span class="logo-half">lite</span>metric dashboard</span></span></h1>
43
+ </div>
44
+ <table class="head">
45
+ <tr>
46
+ <td>
47
+ Showing data for the last <select onchange="window.location = locationWithParam('res', this.value)">
48
+ <%= mapping = {'hour' => '60 minutes', 'day' => '24 hours', 'week' => '7 days', 'year' => '52 weeks'}%>
49
+ <% ['hour', 'day', 'week', 'year'].each do |res| %>
50
+ <option value=<%=res%> <%='selected' if res == @res%>><%=mapping[res]%></option>
51
+ <% end %>
52
+ </select>
53
+ </td>
54
+ <td></td>
55
+ </tr>
56
+ <tr>
57
+ <td></td><td></td>
58
+ </tr>
59
+ </table>
60
+ <%= yield %>
61
+ </div>
62
+ <div id="footer">
63
+ Powered by <a href="https://www.github.com/oldmoe/litestack" target="_blank"><span class="logo"><span class="logo-half">lite</span>stack</span></a>
64
+ </div>
65
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
66
+ </body>
67
+ <script>
68
+ google.charts.load('current', {'packages':['corechart']});
69
+
70
+ google.charts.setOnLoadCallback(drawChart);
71
+
72
+ function drawChart() {
73
+ elements = document.querySelectorAll(".inlinecolumn")
74
+ elements.forEach(element => {
75
+ //console.log(element)
76
+ var label = element.dataset.label;
77
+ var mydata = eval(element.innerText)
78
+ //console.log(mydata)
79
+ if(mydata.length > 0) {
80
+ var data = new google.visualization.DataTable();
81
+
82
+ data.addColumn('string', 'Time');
83
+ data.addColumn('number', label);
84
+ mydata.forEach(row => {
85
+ row[1] = Number(row[1])
86
+ data.addRows([row])
87
+ })
88
+ //data.addRows(mydata)
89
+ var options = {
90
+ animation: {'startup': true, 'duration': 300},
91
+ width: 400,
92
+ height: 40,
93
+ backgroundColor: 'none',
94
+ curveType: 'function',
95
+ colors : ['#089'],
96
+ vAxis: {'gridlines': {'count' : 0}, 'textPosition' : 'none', 'baselineColor' : 'none', 'minValue' : 0 , 'maxValue' : 50},
97
+ hAxis: { 'count' : 0, 'textPosition' : 'none', 'baselineColor' : 'none'},
98
+ legend: {'position': 'none'}
99
+ }
100
+ var chart = new google.visualization.AreaChart(element);
101
+ chart.draw(data, options);
102
+ element.classList.remove("hidden")
103
+ }
104
+ })
105
+ }
106
+ function search_kd(el){
107
+ //store the current value
108
+ el.oldvalue = el.value
109
+ }
110
+ function search_ku(el){
111
+ //check if the value has changed and if so
112
+ // set a new timer to fire a request in 300ms
113
+ // removing any existing timer first
114
+ if(el.value == el.oldvalue){
115
+ return
116
+ }else{
117
+ el.oldvalue = null
118
+ }
119
+ if(el.timeout){
120
+ window.clearTimeout(el.timeout)
121
+ }
122
+ el.timeout = window.setTimeout(function(){
123
+ el.timeout = null
124
+ window.location = locationWithParam('search', el.value)
125
+ }, 300)
126
+ }
127
+
128
+ $(document).ready(function(){
129
+ el = $('#search-field')[0]
130
+ el.focus()
131
+ if(el.value && el.value.length > 0){
132
+ el.setSelectionRange(el.value.length, el.value.length)
133
+ var list = $("table.sortable div.label") //[0].children[0].children
134
+ for(var i=0; i < list.length; i++){
135
+ //console.log(list[i])
136
+ var link = list[i].children[0] //.children[0].children[0]
137
+ var re = new RegExp("("+el.value+")", "giu")
138
+ link.innerHTML = link.innerHTML.replaceAll(re, "<span class='token'>$1</span>") ;
139
+ }
140
+ }
141
+ })
142
+
143
+ function locationWithParam(param, value){
144
+ var query = window.location.search
145
+ var params = new URLSearchParams(query)
146
+ params.set(param, value)
147
+ var l = window.location
148
+ return l.origin + l.pathname + '?' + params.toString()
149
+ }
150
+ </script>
151
+ </html>
152
+