caffeinate_webui 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8b252d1fca4c8a0cdc033379fe1ce1a40825be92ad378c102e13a225b977b69f
4
+ data.tar.gz: 347c84a885db57a12d2aa3d95e259342c4c67ced5f47a71d9062dcc7ff78e27b
5
+ SHA512:
6
+ metadata.gz: 978ce8a1f526486b13e894428c9afb5489b912b733fcf232474a399fdeca0377e22cd3f2c3096d2ae5dab44f15fa641a016f45b959d2956451d05ab60b7d3e03
7
+ data.tar.gz: c7d521079ffb5dd87ee87e26315ecdb566bb939a0dc11848aa034d5cafd12901f2726eb78e9550b8c50a1fb44fc0a992e8bef47059167182486438fd965bc7c0
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Caffeinate WebUI
2
+
3
+ Provides a simple UI to view and manage some aspects of Caffeinate.
4
+
5
+ <div align="center">
6
+ <img width="450" src="https://github.com/joshmn/caffeinate/raw/master/logo.png" alt="Caffeinate logo" />
7
+ </div>
8
+
9
+ <div align="center">
10
+ <img width="100%" src="https://github.com/joshmn/caffeinate-webui/raw/master/dashboard.png" alt="Caffeinate WebUI Example" />
11
+ </div>
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'caffeinate_webui'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle install
24
+
25
+ Drop it into your routes:
26
+
27
+ ```ruby
28
+ mount Caffeinate::Webui => '/admin/caffeinate'
29
+ ```
30
+
31
+ ## Protect it
32
+
33
+ If you're using Devise, you can simply:
34
+
35
+ ```ruby
36
+ authenticate :user, ->(user) { user.admin? } do
37
+ mount Caffeinate::Webui => '/admin/caffeinate'
38
+ end
39
+ ```
40
+
41
+ Otherwise, protect it with your preferred rack-based strategy.
42
+
43
+ ## Features
44
+
45
+ * Some lightweight dashboard stuff
46
+ * View campaigns and their steps
47
+ * View subscriptions
48
+ * Unsubscribe a subscription
49
+ * View mailings
50
+
51
+ ## Dependencies
52
+
53
+ Doesn't need Sprockets, so I guess that's nice.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Caffeinate WebUI'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+ load 'rails/tasks/statistics.rake'
20
+ require 'bundler/gem_tasks'
@@ -0,0 +1,20 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class ApplicationController < ActionController::Base
4
+ layout 'caffeinate/webui/layouts/application'
5
+
6
+ helper_method :page_title
7
+ def page_title
8
+ if @page_title
9
+ "#{@page_title} - Caffeinate"
10
+ else
11
+ "Caffeinate"
12
+ end
13
+ end
14
+
15
+ def set_page_title(val)
16
+ @page_title = val
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class CampaignsController < ApplicationController
4
+ def index
5
+ @campaigns = Caffeinate::Campaign.all
6
+ set_page_title "Campaigns"
7
+ end
8
+
9
+ def show
10
+ @campaign = Caffeinate::Campaign.find_by(id: params[:id])
11
+ @subscriptions = @campaign.caffeinate_campaign_subscriptions.preload(:subscriber).paginate(per_page: 30, page: params[:page])
12
+ set_page_title "Viewing #{@campaign.name}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class DashboardController < ApplicationController
4
+ def show
5
+ @stats ||= OpenStruct.new(
6
+ subscribers: ::Caffeinate::CampaignSubscription.count,
7
+ delivered: ::Caffeinate::Mailing.sent.count,
8
+ skipped: ::Caffeinate::Mailing.skipped.count,
9
+ active: ::Caffeinate::Campaign.active.count
10
+ )
11
+
12
+ @all_stats = Caffeinate::Mailing.all.sent.group_by_day(:send_at).count
13
+ @upcoming_mailings ||= ::Caffeinate::Mailing.unsent
14
+ .joins(:caffeinate_campaign_subscription)
15
+ .preload(:caffeinate_campaign, caffeinate_campaign_subscription: [:subscriber])
16
+ .merge(::Caffeinate::CampaignSubscription.active)
17
+ .order(send_at: :asc)
18
+ .paginate(per_page: 30, page: params[:page])
19
+
20
+ set_page_title "Dashboard"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class MailingsController < ApplicationController
4
+ before_action :set_status, only: [:index]
5
+
6
+ def index
7
+ @campaigns = ::Caffeinate::Campaign.all
8
+ @mailings = ::Caffeinate::Mailing.preload(:caffeinate_campaign, caffeinate_campaign_subscription: [:subscriber])
9
+ if params[:campaign_id]
10
+ @campaign = ::Caffeinate::Campaign.find_by(id: params[:campaign_id])
11
+ end
12
+ if @campaign
13
+ @mailings = @mailings.joins(:caffeinate_campaign).where(caffeinate_campaign: { id: @campaign.id })
14
+ end
15
+ if @status
16
+ @mailings = @mailings.public_send(@status)
17
+ end
18
+ @mailings = @mailings.paginate(per_page: 30, page: params[:page])
19
+ set_page_title "Mailings"
20
+
21
+ end
22
+
23
+ def show
24
+ @mailing = ::Caffeinate::Mailing.find_by(id: params[:id])
25
+ set_page_title "Mailing Details"
26
+
27
+ end
28
+
29
+ private
30
+
31
+ def set_status
32
+ if ['sent', 'unsent', 'skipped'].include?(params[:status])
33
+ @status = params[:status]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ class Caffeinate::Webui::Subscriptions::UnsubscribesController < ApplicationController
2
+ def create
3
+ @subscription = ::Caffeinate::CampaignSubscription.find_by(id: params[:subscription_id])
4
+ if @subscription
5
+ begin
6
+ @subscription.unsubscribe!
7
+ flash[:notice] = "Unsubscribed."
8
+ rescue Caffeinate::InvalidState => e
9
+ flash[:notice] = e.message
10
+ end
11
+ end
12
+
13
+ redirect_to subscription_path(@subscription)
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class SubscriptionsController < ApplicationController
4
+ def index
5
+ @subscriptions = Caffeinate::CampaignSubscription.preload(:caffeinate_campaign, :subscriber)
6
+ if params[:campaign_id]
7
+ @campaign = ::Caffeinate::Campaign.find_by(id: params[:campaign_id])
8
+ end
9
+ if @campaign
10
+ @subscriptions = @subscriptions.where(caffeinate_campaign: { id: @campaign.id })
11
+ end
12
+ @subscriptions = @subscriptions.order(created_at: :desc).paginate(page: params[:page], per_page: 30)
13
+ set_page_title "Subscriptions"
14
+ end
15
+
16
+ def show
17
+ @subscription = Caffeinate::CampaignSubscription.find(params[:id])
18
+ set_page_title "Viewing Subscription"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module Caffeinate::Webui::ApplicationHelper
2
+ def active_link_to(label, href, active: false)
3
+ link_to(label, href, class: "nav-link #{'active' if active}")
4
+ end
5
+
6
+ def time(datetime)
7
+ label = [time_ago_in_words(datetime)]
8
+ if datetime.past?
9
+ label << "ago"
10
+ else
11
+ label << "from now"
12
+ end
13
+
14
+ content_tag(:abbr, label.join(" "), title: datetime)
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ <h2>Campaigns</h2>
2
+ <ul>
3
+
4
+ </ul>
5
+
6
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
7
+ <h6 class="border-bottom pb-2 mb-0">Campaigns</h6>
8
+ <% @campaigns.each do |campaign| %>
9
+ <div class="d-flex w-100 text-muted pt-3 flex-grow-1 justify-content-between border-bottom">
10
+ <div class="pb-3 mb-0 ">
11
+ <strong class="d-block text-gray-dark"><%= link_to campaign.name, campaign_path(campaign) %></strong>
12
+ </div>
13
+ <div>
14
+ <%= pluralize(campaign.subscriptions.active.count, "subscription") %>
15
+ </div>
16
+ </div>
17
+ <% end %>
18
+
19
+ </div>
@@ -0,0 +1,56 @@
1
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
2
+ <h6 class="border-bottom pb-2 mb-0"><%= @campaign.name %></h6>
3
+ <table class="table">
4
+ <thead>
5
+ <tr>
6
+ <td>Mail</td>
7
+ <td>Delay</td>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <% @campaign.to_dripper.drips.each do |step| %>
12
+ <tr>
13
+ <td>
14
+ <%= step.options[:mailer_class] %>#<%= step.action %>
15
+ </td>
16
+ <td>
17
+ <%= step.options[:delay].inspect %>
18
+ </td>
19
+ </tr>
20
+ <% end %>
21
+ </tbody>
22
+ </table>
23
+ </div>
24
+
25
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
26
+ <h6 class="border-bottom pb-2 mb-0">Subscriptions</h6>
27
+ <table class="table">
28
+ <thead>
29
+ <tr>
30
+ <td>Who</td>
31
+ <td>Created</td>
32
+ <td>Status</td>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @subscriptions.each do |subscriber| %>
37
+ <tr>
38
+ <td>
39
+ <%= link_to ::Caffeinate::Webui::Name.for(subscriber.subscriber), subscription_path(subscriber) %>
40
+ </td>
41
+ <td>
42
+ <%= time_ago_in_words(subscriber.created_at) %> ago
43
+ </td>
44
+ <td>
45
+ <%= "Ended" if subscriber.ended? %>
46
+ <%= "Active" if subscriber.subscribed? %>
47
+ <%= "Unsubscribed" if subscriber.unsubscribed? %>
48
+ </td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
53
+ <div class="d-flex justify-content-end">
54
+ <%= will_paginate @subscriptions %>
55
+ </div>
56
+ </div>
@@ -0,0 +1,64 @@
1
+ <div class="row align-items-center text-center">
2
+ <div class="col">
3
+ <h5>Subscribers</h5>
4
+ <span><%= @stats.subscribers %></span>
5
+ </div>
6
+ <div class="col">
7
+ <h5>Delivered</h5>
8
+ <span><%= @stats.delivered %></span>
9
+ </div>
10
+ <div class="col">
11
+ <h5>Skipped</h5>
12
+ <span><%= @stats.skipped %></span>
13
+ </div>
14
+ <div class="col">
15
+ <h5>Active</h5>
16
+ <span><%= @stats.active %></span>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
21
+ <div class="d-flex justify-content-between border-bottom pb-2 mb-0">
22
+ <h6 class="">History</h6>
23
+ <div class="d-flex gap-3">
24
+ <%= link_to "One week", root_path(duration: :week) %>
25
+ <%= link_to "One month", root_path(duration: :month) %>
26
+ <%= link_to "One year", root_path(duration: :year) %>
27
+ <%= link_to "All-time", root_path %>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="d-flex text-muted pt-3">
32
+ <%= line_chart(@all_stats) %>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
37
+ <h6 class="border-bottom pb-2 mb-0">Upcoming</h6>
38
+ <div class="d-flex text-muted pt-3">
39
+
40
+ <table class="table">
41
+ <thead>
42
+ <tr>
43
+ <td>Who</td>
44
+ <td>Campaign</td>
45
+ <td>Send at</td>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ <% @upcoming_mailings.each do |mailing| %>
50
+ <tr>
51
+ <td><%= link_to Caffeinate::Webui::Name.for(mailing.subscriber), subscription_path(mailing.caffeinate_campaign_subscription) %></td>
52
+ <td><%= link_to mailing.campaign.name, mailing.campaign %></td>
53
+ <td><%= time(mailing.send_at) %></td>
54
+ </tr>
55
+ <% end %>
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+
60
+ <div class="d-flex justify-content-end">
61
+ <%= will_paginate @upcoming_mailings %>
62
+ </div>
63
+ </div>
64
+
@@ -0,0 +1,170 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
7
+ <script src="https://code.highcharts.com/highcharts.js"></script>
8
+ <script src="https://unpkg.com/chartjs-adapter-date-fns@2.0.0/dist/chartjs-adapter-date-fns.bundle.js"></script>
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
10
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
11
+ <title><%= page_title %></title>
12
+ <script type="text/javascript">
13
+ /*!
14
+ * Chartkick.js
15
+ * Create beautiful charts with one line of JavaScript
16
+ * https://github.com/ankane/chartkick.js
17
+ * v4.2.0
18
+ * MIT License
19
+ */ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chartkick=e()}(this,function(){"use strict";function t(t){return"[object Array]"===Object.prototype.toString.call(t)}function e(t){return t instanceof Function}function r(t){return"[object Object]"===Object.prototype.toString.call(t)&&!e(t)&&t instanceof Object}function o(e,n){var a;for(a in n)"__proto__"!==a&&(r(n[a])||t(n[a])?(r(n[a])&&!r(e[a])&&(e[a]={}),t(n[a])&&!t(e[a])&&(e[a]=[]),o(e[a],n[a])):void 0!==n[a]&&(e[a]=n[a]))}function n(t,e){var r={};return o(r,t),o(r,e),r}var a=/^(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)$/i;function i(t){return""+t}function s(t){return parseFloat(t)}function l(t){var e,r,o,n;if("object"!=typeof t){if("number"==typeof t)t=new Date(1e3*t);else{if(e=(t=""+(i=t)).match(a))return r=parseInt(e[1],10),o=parseInt(e[3],10)-1,n=parseInt(e[5],10),new Date(r,o,n);var i,s=t.replace(/ /,"T").replace(" ","").replace("UTC","Z");t=new Date(Date.parse(s)||t)}}return t}function c(e){if(!t(e)){var r,o=[];for(r in e)e.hasOwnProperty(r)&&o.push([r,e[r]]);e=o}return e}function p(t,e,r,o,a,i,s,l){return function(c,p,u){var d=c.data,h=n({},t);return h=n(h,u||{}),(c.singleSeriesFormat||"legend"in p)&&e(h,p.legend,c.singleSeriesFormat),p.title&&r(h,p.title),"min"in p?o(h,p.min):!function t(e){var r,o,n;for(r=0;r<e.length;r++)for(o=0,n=e[r].data;o<n.length;o++)if(n[o][1]<0)return!0;return!1}(d)&&o(h,0),p.max&&a(h,p.max),"stacked"in p&&i(h,p.stacked),p.colors&&(h.colors=p.colors),p.xtitle&&s(h,p.xtitle),p.ytitle&&l(h,p.ytitle),h=n(h,p.library||{})}}function u(t,e){return t[0].getTime()-e[0].getTime()}function d(t,e){return t[0]-e[0]}function h(t,e){return t-e}function f(t){return 0===t.getMilliseconds()&&0===t.getSeconds()}function y(t){return f(t)&&0===t.getMinutes()}function m(t){return y(t)&&0===t.getHours()}function $(t,e){return m(t)&&t.getDay()===e}function g(t){return m(t)&&1===t.getDate()}function v(t){return g(t)&&0===t.getMonth()}function z(t){var e;return!isNaN(l(t))&&(""+(e=t)).length>=6}function b(t){return"number"==typeof t}var M=["bytes","KB","MB","GB","TB","PB","EB"];function x(t,e,r,o){t=t||"",r.prefix&&(e<0&&(e*=-1,t+="-"),t+=r.prefix);var n=r.suffix||"",a=r.precision,i=r.round;if(r.byteScale){var s,l=o?r.byteScale:e;l>=0x1000000000000000?(e/=0x1000000000000000,s=6):l>=0x4000000000000?(e/=0x4000000000000,s=5):l>=1099511627776?(e/=1099511627776,s=4):l>=1073741824?(e/=1073741824,s=3):l>=1048576?(e/=1048576,s=2):l>=1024?(e/=1024,s=1):s=0,void 0===a&&void 0===i&&(e>=1023.5&&s<M.length-1&&(e=1,s+=1),a=e>=1e3?4:3),n=" "+M[s]}if(void 0!==a&&void 0!==i)throw Error("Use either round or precision, not both");if(!o&&(void 0===a||(e=e.toPrecision(a),r.zeros||(e=parseFloat(e))),void 0!==i)){if(i<0){var c=Math.pow(10,-1*i);e=parseInt((1*e/c).toFixed(0))*c}else e=e.toFixed(i),r.zeros||(e=parseFloat(e))}if(r.thousands||r.decimal){var p,u=(e=""+(p=e)).split(".");e=u[0],r.thousands&&(e=e.replace(/\B(?=(\d{3})+(?!\d))/g,r.thousands)),u.length>1&&(e+=(r.decimal||".")+u[1])}return t+e+n}function _(t,e,r){return r in e?e[r]:r in t.options?t.options[r]:null}var C={maintainAspectRatio:!1,animation:!1,plugins:{legend:{},tooltip:{displayColors:!1,callbacks:{}},title:{font:{size:20},color:"#333"}},interaction:{}},w={scales:{y:{ticks:{maxTicksLimit:4},title:{font:{size:16},color:"#333"},grid:{}},x:{grid:{drawOnChartArea:!1},title:{font:{size:16},color:"#333"},time:{},ticks:{}}}},A=["#3366CC","#DC3912","#FF9900","#109618","#990099","#3B3EAC","#0099C6","#DD4477","#66AA00","#B82E2E","#316395","#994499","#22AA99","#AAAA11","#6633CC","#E67300","#8B0707","#329262","#5574A6","#651067"],k=function(t,e,r){void 0!==e?(t.plugins.legend.display=!!e,e&&!0!==e&&(t.plugins.legend.position=e)):r&&(t.plugins.legend.display=!1)},S=function(t,e){t.plugins.title.display=!0,t.plugins.title.text=e},T=function(t,e){null!==e&&(t.scales.y.min=s(e))},D=function(t,e){t.scales.y.max=s(e)},E=function(t,e){null!==e&&(t.scales.x.min=s(e))},L=function(t,e){t.scales.x.max=s(e)},B=function(t,e){t.scales.x.stacked=!!e,t.scales.y.stacked=!!e},O=function(t,e){t.scales.x.title.display=!0,t.scales.x.title.text=e},F=function(t,e){t.scales.y.title.display=!0,t.scales.y.title.text=e},H=function(t,e){var r=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return r?"rgba("+parseInt(r[1],16)+", "+parseInt(r[2],16)+", "+parseInt(r[3],16)+", "+e+")":t},R=function(t){return null!=t},N=function(t,e,r){var o=Math.ceil(t.element.offsetWidth/4/e.labels.length);o>25?o=25:o<10&&(o=10),r.scales.x.ticks.callback||(r.scales.x.ticks.callback=function(t){var e;return(t=""+(e=this.getLabelForValue(t))).length>o?t.substring(0,o-2)+"...":t})},j=function(e,r,o){var n={prefix:e.options.prefix,suffix:e.options.suffix,thousands:e.options.thousands,decimal:e.options.decimal,precision:e.options.precision,round:e.options.round,zeros:e.options.zeros};if(e.options.bytes){var a=e.data;"pie"===o&&(a=[{data:a}]);for(var i=0,s=0;s<a.length;s++)for(var l=a[s],c=0;c<l.data.length;c++)l.data[c][1]>i&&(i=l.data[c][1]);for(var p=1;i>=1024;)p*=1024,i/=1024;n.byteScale=p}if("pie"!==o){var u=r.scales.y;"bar"===o&&(u=r.scales.x),n.byteScale&&(u.ticks.stepSize||(u.ticks.stepSize=n.byteScale/2),u.ticks.maxTicksLimit||(u.ticks.maxTicksLimit=4)),u.ticks.callback||(u.ticks.callback=function(t){return x("",t,n,!0)})}if(!r.plugins.tooltip.callbacks.label){if("scatter"===o)r.plugins.tooltip.callbacks.label=function(t){var e=t.dataset.label||"";return e&&(e+=": "),e+"("+t.label+", "+t.formattedValue+")"};else if("bubble"===o)r.plugins.tooltip.callbacks.label=function(t){var e=t.dataset.label||"";e&&(e+=": ");var r=t.raw;return e+"("+r.x+", "+r.y+", "+r.v+")"};else if("pie"===o)r.plugins.tooltip.callbacks.label=function(e){var r=e.label;return t(r)?(r=r.slice(),r[0]+=": "):r+=": ",x(r,e.parsed,n)};else{var d="bar"===o?"x":"y";r.plugins.tooltip.callbacks.label=function(t){if(null!==t.parsed[d]){var e=t.dataset.label||"";return e&&(e+=": "),x(e,t.parsed[d],n)}}}}},P=p(n(C,w),k,S,T,D,B,O,F),U=function(e,r,o){var a=[],i=[],c=e.options.colors||A,p=!0,u=!0,d=!0,z=!0,b=!0,M=!0,x=e.data,C=0;if("bubble"===o)for(var w=0;w<x.length;w++)for(var k=x[w],S=0;S<k.data.length;S++)k.data[S][2]>C&&(C=k.data[S][2]);var T,D,E,L,B,O,F=[],N=[];if("bar"===o||"column"===o||"number"!==e.xtype&&"bubble"!==e.xtype){var j,P,U=[];for(D=0;D<x.length;D++)for(E=0,L=x[D];E<L.data.length;E++)B=L.data[E],F[O="datetime"==e.xtype?B[0].getTime():B[0]]||(F[O]=Array(x.length)),F[O][D]=s(B[1]),-1===U.indexOf(O)&&U.push(O);for(("datetime"===e.xtype||"number"===e.xtype)&&U.sort(h),E=0;E<x.length;E++)N.push([]);for(P=0;P<U.length;P++)for(D=U[P],"datetime"===e.xtype?(j=new Date(s(D)),p=p&&m(j),T||(T=j.getDay()),u=u&&$(j,T),d=d&&g(j),z=z&&v(j),b=b&&y(j),M=M&&f(j)):j=D,i.push(j),E=0;E<x.length;E++)N[E].push(void 0===F[D][E]?null:F[D][E])}else for(var W=0;W<x.length;W++){for(var V=x[W],Q=[],I=0;I<V.data.length;I++){var G={x:s(V.data[I][0]),y:s(V.data[I][1])};"bubble"===o&&(G.r=20*s(V.data[I][2])/C,G.v=V.data[I][2]),Q.push(G)}N.push(Q)}for(D=0;D<x.length;D++){if(L=x[D],e.options.colors&&e.singleSeriesFormat&&("bar"===o||"column"===o)&&!L.color&&t(e.options.colors)&&!t(e.options.colors[0])){q=c,tt=[];for(var J=0;J<c.length;J++)tt[J]=H(q[J],.5)}else q=L.color||c[D],tt="line"!==o?H(q,.5):q;var K={label:L.name||"",data:N[D],fill:"area"===o,borderColor:q,backgroundColor:tt,borderWidth:2},Y="line"===o||"area"===o||"scatter"===o||"bubble"===o;Y&&(K.pointBackgroundColor=q,K.pointHoverBackgroundColor=q,K.pointHitRadius=50),"bubble"===o&&(K.pointBackgroundColor=tt,K.pointHoverBackgroundColor=tt,K.pointHoverBorderWidth=2),L.stack&&(K.stack=L.stack),!1===_(e,L,"curve")?K.tension=0:Y&&(K.tension=.4),!1===_(e,L,"points")&&(K.pointRadius=0,K.pointHoverRadius=0),K=n(K,e.options.dataset||{}),K=n(K,L.library||{}),K=n(K,L.dataset||{}),a.push(K)}var X=e.options.xmin,Z=e.options.xmax;if("datetime"===e.xtype?(R(X)&&(r.scales.x.min=l(X).getTime()),R(Z)&&(r.scales.x.max=l(Z).getTime())):"number"===e.xtype&&(R(X)&&(r.scales.x.min=X),R(Z)&&(r.scales.x.max=Z)),"datetime"===e.xtype&&0===i.length&&(R(X)&&i.push(l(X)),R(Z)&&i.push(l(Z)),p=!1,u=!1,d=!1,z=!1,b=!1,M=!1),"datetime"===e.xtype&&i.length>0){var q,tt,te,tr=(R(X)?l(X):i[0]).getTime(),to=(R(Z)?l(Z):i[0]).getTime();for(D=1;D<i.length;D++){var tn=i[D].getTime();tn<tr&&(tr=tn),tn>to&&(to=tn)}var ta=(to-tr)/864e5;if(!r.scales.x.time.unit&&(z||ta>3650?(r.scales.x.time.unit="year",te=365):d||ta>300?(r.scales.x.time.unit="month",te=30):p||ta>10?(r.scales.x.time.unit="day",te=1):b||ta>.5?(r.scales.x.time.displayFormats={hour:"MMM d, h a"},r.scales.x.time.unit="hour",te=1/24):M&&(r.scales.x.time.displayFormats={minute:"h:mm a"},r.scales.x.time.unit="minute",te=1/24/60),te&&ta>0)){var ti=e.element.offsetWidth;if(ti>0){var ts=Math.ceil(ta/te/(ti/100));u&&1===te&&(ts=7*Math.ceil(ts/7)),r.scales.x.time.stepSize=ts}}!r.scales.x.time.tooltipFormat&&(p?r.scales.x.time.tooltipFormat="PP":b?r.scales.x.time.tooltipFormat="MMM d, h a":M&&(r.scales.x.time.tooltipFormat="h:mm a"))}return{labels:i,datasets:a}},W=function t(e){this.name="chartjs",this.library=e};W.prototype.renderLineChart=function t(e,r){var o={};!e.options.max&&function t(e){var r,o,n;for(r=0;r<e.length;r++)for(o=0,n=e[r].data;o<n.length;o++)if(0!=n[o][1])return!1;return!0}(e.data)&&(o.max=1);var a=P(e,n(o,e.options));j(e,a,r);var i=U(e,a,r||"line");"number"===e.xtype?(a.scales.x.type=a.scales.x.type||"linear",a.scales.x.position=a.scales.x.position||"bottom"):a.scales.x.type="string"===e.xtype?"category":"time",this.drawChart(e,"line",i,a)},W.prototype.renderPieChart=function t(e){var r=n({},C);e.options.donut&&(r.cutout="50%"),"legend"in e.options&&k(r,e.options.legend),e.options.title&&S(r,e.options.title),r=n(r,e.options.library||{}),j(e,r,"pie");for(var o=[],a=[],i=0;i<e.data.length;i++){var s=e.data[i];o.push(s[0]),a.push(s[1])}var l={data:a,backgroundColor:e.options.colors||A};l=n(l,e.options.dataset||{});var c={labels:o,datasets:[l]};this.drawChart(e,"pie",c,r)},W.prototype.renderColumnChart=function t(e,r){if("bar"===r){var o,a=n(C,w);a.indexAxis="y",a.scales.x.grid.drawOnChartArea=!0,a.scales.y.grid.drawOnChartArea=!1,delete a.scales.y.ticks.maxTicksLimit,o=p(a,k,S,E,L,B,O,F)(e,e.options)}else o=P(e,e.options);j(e,o,r);var i=U(e,o,"column");"bar"!==r&&N(e,i,o),this.drawChart(e,"bar",i,o)},W.prototype.renderAreaChart=function t(e){this.renderLineChart(e,"area")},W.prototype.renderBarChart=function t(e){this.renderColumnChart(e,"bar")},W.prototype.renderScatterChart=function t(e,r){r=r||"scatter";var o=P(e,e.options);j(e,o,r),"showLine"in o||(o.showLine=!1);var n=U(e,o,r);o.scales.x.type=o.scales.x.type||"linear",o.scales.x.position=o.scales.x.position||"bottom","mode"in o.interaction||(o.interaction.mode="nearest"),this.drawChart(e,r,n,o)},W.prototype.renderBubbleChart=function t(e){this.renderScatterChart(e,"bubble")},W.prototype.destroy=function t(e){e.chart&&e.chart.destroy()},W.prototype.drawChart=function t(e,r,o,n){if(this.destroy(e),!e.destroyed){var a={type:r,data:o,options:n};e.options.code&&window.console.log("new Chart(ctx, "+JSON.stringify(a)+");"),e.element.innerHTML="<canvas></canvas>";var i=e.element.getElementsByTagName("CANVAS")[0];e.chart=new this.library(i,a)}};var V={chart:{},xAxis:{title:{text:null},labels:{style:{fontSize:"12px"}}},yAxis:{title:{text:null},labels:{style:{fontSize:"12px"}}},title:{text:null},credits:{enabled:!1},legend:{borderWidth:0},tooltip:{style:{fontSize:"12px"}},plotOptions:{areaspline:{},area:{},series:{marker:{}}},time:{useUTC:!1}},Q=function(t,e,r){void 0!==e?(t.legend.enabled=!!e,e&&!0!==e&&("top"===e||"bottom"===e?t.legend.verticalAlign=e:(t.legend.layout="vertical",t.legend.verticalAlign="middle",t.legend.align=e))):r&&(t.legend.enabled=!1)},I=function(t,e){t.title.text=e},G=p(V,Q,I,function(t,e){t.yAxis.min=e},function(t,e){t.yAxis.max=e},function(t,e){var r=e?!0===e?"normal":e:null;t.plotOptions.series.stacking=r,t.plotOptions.area.stacking=r,t.plotOptions.areaspline.stacking=r},function(t,e){t.xAxis.title.text=e},function(t,e){t.yAxis.title.text=e}),J=function(e,r,o){var n={prefix:e.options.prefix,suffix:e.options.suffix,thousands:e.options.thousands,decimal:e.options.decimal,precision:e.options.precision,round:e.options.round,zeros:e.options.zeros};"pie"===o||t(r.yAxis)||r.yAxis.labels.formatter||(r.yAxis.labels.formatter=function(){return x("",this.value,n)}),r.tooltip.pointFormatter||r.tooltip.pointFormat||(r.tooltip.pointFormatter=function(){return'<span style="color:'+this.color+'">●</span> '+x(this.series.name+": <b>",this.y,n)+"</b><br/>"})},K=function t(e){this.name="highcharts",this.library=e};K.prototype.renderLineChart=function t(e,r){var o={};"areaspline"===(r=r||"spline")&&(o={plotOptions:{areaspline:{stacking:"normal"},area:{stacking:"normal"},series:{marker:{enabled:!1}}}}),!1===e.options.curve&&("areaspline"===r?r="area":"spline"===r&&(r="line"));var n,a,i,s=G(e,e.options,o);"number"===e.xtype?s.xAxis.type=s.xAxis.type||"linear":s.xAxis.type="string"===e.xtype?"category":"datetime",s.chart.type||(s.chart.type=r),J(e,s,r);var l=e.data;for(a=0;a<l.length;a++){if(l[a].name=l[a].name||"Value",n=l[a].data,"datetime"===e.xtype)for(i=0;i<n.length;i++)n[i][0]=n[i][0].getTime();l[a].marker={symbol:"circle"},!1===e.options.points&&(l[a].marker.enabled=!1)}this.drawChart(e,l,s)},K.prototype.renderScatterChart=function t(e){var r=G(e,e.options,{});r.chart.type="scatter",this.drawChart(e,e.data,r)},K.prototype.renderPieChart=function t(e){var r=n(V,{});e.options.colors&&(r.colors=e.options.colors),e.options.donut&&(r.plotOptions={pie:{innerSize:"50%"}}),"legend"in e.options&&Q(r,e.options.legend),e.options.title&&I(r,e.options.title);var o=n(r,e.options.library||{});J(e,o,"pie");var a=[{type:"pie",name:e.options.label||"Value",data:e.data}];this.drawChart(e,a,o)},K.prototype.renderColumnChart=function t(e,r){r=r||"column";var o,n,a,i,s=e.data,l=G(e,e.options),c=[],p=[];for(l.chart.type=r,J(e,l,r),o=0;o<s.length;o++)for(n=0,a=s[o];n<a.data.length;n++)c[(i=a.data[n])[0]]||(c[i[0]]=Array(s.length),p.push(i[0])),c[i[0]][o]=i[1];"number"===e.xtype&&p.sort(h),l.xAxis.categories=p;var u,d=[];for(o=0;o<s.length;o++){for(n=0,i=[];n<p.length;n++)i.push(c[p[n]][o]||0);u={name:s[o].name||"Value",data:i},s[o].stack&&(u.stack=s[o].stack),d.push(u)}this.drawChart(e,d,l)},K.prototype.renderBarChart=function t(e){this.renderColumnChart(e,"bar")},K.prototype.renderAreaChart=function t(e){this.renderLineChart(e,"areaspline")},K.prototype.destroy=function t(e){e.chart&&e.chart.destroy()},K.prototype.drawChart=function t(e,r,o){this.destroy(e),!e.destroyed&&(o.chart.renderTo=e.element.id,o.series=r,e.options.code&&window.console.log("new Highcharts.Chart("+JSON.stringify(o)+");"),e.chart=new this.library.Chart(o))};var Y={},X=[],Z={chartArea:{},fontName:"'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif",pointSize:6,legend:{textStyle:{fontSize:12,color:"#444"},alignment:"center",position:"right"},curveType:"function",hAxis:{textStyle:{color:"#666",fontSize:12},titleTextStyle:{},gridlines:{color:"transparent"},baselineColor:"#ccc",viewWindow:{}},vAxis:{textStyle:{color:"#666",fontSize:12},titleTextStyle:{},baselineColor:"#ccc",viewWindow:{}},tooltip:{textStyle:{color:"#666",fontSize:12}}},q=function(t,e,r){if(void 0!==e){var o;o=e?!0===e?"right":e:"none",t.legend.position=o}else r&&(t.legend.position="none")},tt=function(t,e){t.title=e,t.titleTextStyle={color:"#333",fontSize:"20px"}},te=function(t,e){t.hAxis.viewWindow.min=e},tr=function(t,e){t.hAxis.viewWindow.max=e},to=function(t,e){t.isStacked=!!e&&e},tn=function(t,e){t.hAxis.title=e,t.hAxis.titleTextStyle.italic=!1},ta=function(t,e){t.vAxis.title=e,t.vAxis.titleTextStyle.italic=!1},ti=p(Z,q,tt,function(t,e){t.vAxis.viewWindow.min=e},function(t,e){t.vAxis.viewWindow.max=e},to,tn,ta),ts=function(t){window.attachEvent?window.attachEvent("onresize",t):window.addEventListener&&window.addEventListener("resize",t,!0),t()},tl=function t(e){this.name="google",this.library=e};function tc(t,e){var r,o,n=[];if(o="number"===e?s:"datetime"===e?l:i,"bubble"===e)for(r=0;r<t.length;r++)n.push([s(t[r][0]),s(t[r][1]),s(t[r][2])]);else for(r=0;r<t.length;r++)n.push([o(t[r][0]),s(t[r][1])]);return"datetime"===e?n.sort(u):"number"===e&&n.sort(d),n}function tp(t,e){var r,o,n;for(r=0;r<t.length;r++)for(o=0,n=c(t[r].data);o<n.length;o++)if(!e(n[o][0]))return!1;return!0}function tu(e,r,o){var n,a,i,s,l=e.options,p=e.rawData;for(e.singleSeriesFormat=!t(p)||"object"!=typeof p[0]||t(p[0]),e.singleSeriesFormat&&(p=[{name:l.label,data:p}]),p=function t(e){var r,o,n=[];for(r=0;r<e.length;r++){var a={};for(o in e[r])e[r].hasOwnProperty(o)&&(a[o]=e[r][o]);n.push(a)}return n}(p),s=0;s<p.length;s++)p[s].data=c(p[s].data);for(s=0,e.xtype=r||(l.discrete?"string":(n=p,a=o,i=l,th(n)?(i.xmin||i.xmax)&&(!i.xmin||z(i.xmin))&&(!i.xmax||z(i.xmax))?"datetime":"number":tp(n,b)?"number":!a&&tp(n,z)?"datetime":"string"));s<p.length;s++)p[s].data=tc(p[s].data,e.xtype);return p}function td(t){var e,r,o=c(t.rawData);for(r=0;r<o.length;r++)o[r]=[""+(e=o[r][0]),s(o[r][1])];return o}function th(t,e){if("PieChart"===e||"GeoChart"===e||"Timeline"===e)return 0===t.length;for(var r=0;r<t.length;r++)if(t[r].data.length>0)return!1;return!0}function tf(t,e,r){if(t.addEventListener)return t.addEventListener(e,r,!1),r;var o=function(){return r.call(t,window.event)};return t.attachEvent("on"+e,o),o}function ty(t,e,r){t.removeEventListener?t.removeEventListener(e,r,!1):t.detachEvent("on"+e,r)}function tm(t,e){if(t===e)return!1;for(;e&&e!==t;)e=e.parentNode;return e===t}tl.prototype.renderLineChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t={};!1===e.options.curve&&(t.curveType="none"),!1===e.options.points&&(t.pointSize=0);var o=ti(e,e.options,t),n=r.createDataTable(e.data,e.xtype);r.drawChart(e,"LineChart",n,o)})},tl.prototype.renderPieChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t={chartArea:{top:"10%",height:"80%"},legend:{}};e.options.colors&&(t.colors=e.options.colors),e.options.donut&&(t.pieHole=.5),"legend"in e.options&&q(t,e.options.legend),e.options.title&&tt(t,e.options.title);var o=n(n(Z,t),e.options.library||{}),a=new r.library.visualization.DataTable;a.addColumn("string",""),a.addColumn("number","Value"),a.addRows(e.data),r.drawChart(e,"PieChart",a,o)})},tl.prototype.renderColumnChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t=ti(e,e.options),o=r.createDataTable(e.data,e.xtype);r.drawChart(e,"ColumnChart",o,t)})},tl.prototype.renderBarChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t=p(Z,q,tt,te,tr,to,tn,ta)(e,e.options,{hAxis:{gridlines:{color:"#ccc"}}}),o=r.createDataTable(e.data,e.xtype);r.drawChart(e,"BarChart",o,t)})},tl.prototype.renderAreaChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t=ti(e,e.options,{isStacked:!0,pointSize:0,areaOpacity:.5}),o=r.createDataTable(e.data,e.xtype);r.drawChart(e,"AreaChart",o,t)})},tl.prototype.renderGeoChart=function t(e){var r=this;this.waitForLoaded(e,"geochart",function(){var t=n(n(Z,{legend:"none",colorAxis:{colors:e.options.colors||["#f6c7b6","#ce502d"]}}),e.options.library||{}),o=new r.library.visualization.DataTable;o.addColumn("string",""),o.addColumn("number",e.options.label||"Value"),o.addRows(e.data),r.drawChart(e,"GeoChart",o,t)})},tl.prototype.renderScatterChart=function t(e){var r=this;this.waitForLoaded(e,function(){var t,o,n,a,i=ti(e,e.options,{}),s=e.data,l=[];for(t=0;t<s.length;t++)for(o=0,s[t].name=s[t].name||"Value",a=s[t].data;o<a.length;o++){var c=Array(s.length+1);c[0]=a[o][0],c[t+1]=a[o][1],l.push(c)}for((n=new r.library.visualization.DataTable).addColumn("number",""),t=0;t<s.length;t++)n.addColumn("number",s[t].name);n.addRows(l),r.drawChart(e,"ScatterChart",n,i)})},tl.prototype.renderTimeline=function t(e){var r=this;this.waitForLoaded(e,"timeline",function(){var t={legend:"none"};e.options.colors&&(t.colors=e.options.colors);var o=n(n(Z,t),e.options.library||{}),a=new r.library.visualization.DataTable;a.addColumn({type:"string",id:"Name"}),a.addColumn({type:"date",id:"Start"}),a.addColumn({type:"date",id:"End"}),a.addRows(e.data),e.element.style.lineHeight="normal",r.drawChart(e,"Timeline",a,o)})},tl.prototype.destroy=function t(e){e.chart&&e.chart.clearChart()},tl.prototype.drawChart=function t(e,r,o,n){this.destroy(e),!e.destroyed&&(e.options.code&&window.console.log("var data = new google.visualization.DataTable("+o.toJSON()+");\nvar chart = new google.visualization."+r+"(element);\nchart.draw(data, "+JSON.stringify(n)+");"),e.chart=new this.library.visualization[r](e.element),ts(function(){e.chart.draw(o,n)}))},tl.prototype.waitForLoaded=function t(e,r,o){var n=this;if(o||(o=r,r="corechart"),X.push({pack:r,callback:o}),Y[r])this.runCallbacks();else{Y[r]=!0;var a={packages:[r],callback:function(){n.runCallbacks()}},i=e.__config();i.language&&(a.language=i.language),"geochart"===r&&i.mapsApiKey&&(a.mapsApiKey=i.mapsApiKey),this.library.charts.load("current",a)}},tl.prototype.runCallbacks=function t(){for(var e,r,o=0;o<X.length;o++)e=X[o],(r=this.library.visualization&&("corechart"===e.pack&&this.library.visualization.LineChart||"timeline"===e.pack&&this.library.visualization.Timeline||"geochart"===e.pack&&this.library.visualization.GeoChart))&&(e.callback(),X.splice(o,1),o--)},tl.prototype.createDataTable=function t(e,r){var o,n,a,i,l,c,p,h=[],f=[];for(a=0;a<e.length;a++)for(i=0,l=e[a],e[a].name=e[a].name||"Value";i<l.data.length;i++)c=l.data[i],h[p="datetime"===r?c[0].getTime():c[0]]||(h[p]=Array(e.length),f.push(p)),h[p][a]=s(c[1]);var y=[],$=!0;for(i=0;i<f.length;i++)a=f[i],"datetime"===r?(n=new Date(s(a)),$=$&&m(n)):n="number"===r?s(a):a,y.push([n].concat(h[a]));if("datetime"===r)y.sort(u);else if("number"===r){for(y.sort(d),a=0;a<y.length;a++)y[a][0]=""+(o=y[a][0]);r="string"}var g=new this.library.visualization.DataTable;for(r="datetime"===r&&$?"date":r,g.addColumn(r,""),a=0;a<e.length;a++)g.addColumn("number",e[a].name);return g.addRows(y),g};var t$=[],tg=0;function tv(){if(tg<4){var t,e,r,o=t$.shift();o&&(tg++,t=o[0],e=o[1],r=o[2],function t(e,r,o){var n=window.jQuery||window.Zepto||window.$;if(n&&n.ajax)n.ajax({dataType:"json",url:e,success:r,error:o,complete:tz});else{var a=new XMLHttpRequest;a.open("GET",e,!0),a.setRequestHeader("Content-Type","application/json"),a.onload=function(){tz(),200===a.status?r(JSON.parse(a.responseText),a.statusText,a):o(a,"error",a.statusText)},a.send()}}(t,e,function(t,e,o){r("string"==typeof o?o:o.message)}),tv())}}function tz(){tg--,tv()}var tb={},tM=[];function tx(t,e){document.body.innerText?t.innerText=e:t.textContent=e}function t_(t,e,r){r||(e="Error Loading Chart: "+e),tx(t,e),t.style.color="#ff0000"}function tC(t){try{t.__render()}catch(e){throw t_(t.element,e.message),e}}function t8(t,e,r){if(r&&t.options.loading&&("string"==typeof e||"function"==typeof e)&&tx(t.element,t.options.loading),"string"==typeof e){var o,n,a;o=e,n=function(e){t.rawData=e,tC(t)},a=function(e){t_(t.element,e)},t$.push([o,n,a]),tv()}else if("function"==typeof e)try{e(function(e){t.rawData=e,tC(t)},function(e){t_(t.element,e,!0)})}catch(i){t_(t.element,i,!0)}else t.rawData=e,tC(t)}function tw(t){var r=new(function t(r){if(r){if("Highcharts"===r.product)return K;if(r.charts)return tl;if(e(r))return W}throw Error("Unknown adapter")}(t))(t);-1===tM.indexOf(r)&&tM.push(r)}var tA=function t(e,r,o){var a;if("string"==typeof e&&(a=e,!(e=document.getElementById(e))))throw Error("No element with id "+a);this.element=e,this.options=n(tB.options,o||{}),this.dataSource=r,tB.charts[e.id]=this,t8(this,r,!0),this.options.refresh&&this.startRefresh()};tA.prototype.getElement=function t(){return this.element},tA.prototype.getDataSource=function t(){return this.dataSource},tA.prototype.getData=function t(){return this.data},tA.prototype.getOptions=function t(){return this.options},tA.prototype.getChartObject=function t(){return this.chart},tA.prototype.getAdapter=function t(){return this.adapter},tA.prototype.updateData=function t(e,r){this.dataSource=e,r&&this.__updateOptions(r),t8(this,e,!0)},tA.prototype.setOptions=function t(e){this.__updateOptions(e),this.redraw()},tA.prototype.redraw=function t(){t8(this,this.rawData)},tA.prototype.refreshData=function t(){if("string"==typeof this.dataSource){var e=-1===this.dataSource.indexOf("?")?"?":"&",r=this.dataSource+e+"_="+new Date().getTime();t8(this,r)}else"function"==typeof this.dataSource&&t8(this,this.dataSource)},tA.prototype.startRefresh=function t(){var e=this,r=this.options.refresh;if(r&&"string"!=typeof this.dataSource&&"function"!=typeof this.dataSource)throw Error("Data source must be a URL or callback for refresh");if(!this.intervalId){if(r)this.intervalId=setInterval(function(){e.refreshData()},1e3*r);else throw Error("No refresh interval")}},tA.prototype.stopRefresh=function t(){this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null)},tA.prototype.toImage=function t(e){if("chartjs"===this.adapter){if(!e||!e.background||"transparent"===e.background)return this.chart.toBase64Image();var r=this.chart.canvas,o=this.chart.ctx,n=document.createElement("canvas"),a=n.getContext("2d");return n.width=o.canvas.width,n.height=o.canvas.height,a.fillStyle=e.background,a.fillRect(0,0,n.width,n.height),a.drawImage(r,0,0),n.toDataURL("image/png")}throw Error("Feature only available for Chart.js")},tA.prototype.destroy=function t(){this.destroyed=!0,this.stopRefresh(),this.__adapterObject&&this.__adapterObject.destroy(this),this.__enterEvent&&ty(this.element,"mouseover",this.__enterEvent),this.__leaveEvent&&ty(this.element,"mouseout",this.__leaveEvent)},tA.prototype.__updateOptions=function t(e){var r=e.refresh&&e.refresh!==this.options.refresh;this.options=n(tB.options,e),r&&(this.stopRefresh(),this.startRefresh())},tA.prototype.__render=function t(){this.data=this.__processData(),function t(r,o){if(th(o.data,r)){var n,a,i,s,l,c=o.options.empty||o.options.messages&&o.options.messages.empty||"No data";tx(o.element,c)}else(function t(r,o){var n,a,i,s;for(i="render"+r,s=o.options.adapter,("Chart"in window)&&tw(window.Chart),("Highcharts"in window)&&tw(window.Highcharts),window.google&&window.google.charts&&tw(window.google),n=0;n<tM.length;n++)if(a=tM[n],(!s||s===a.name)&&e(a[i]))return o.adapter=a.name,o.__adapterObject=a,a[i](o);if(tM.length>0)throw Error("No charting library found for "+r);throw Error("No charting libraries found - be sure to include one before your charts")})(r,o),o.options.download&&!o.__downloadAttached&&"chartjs"===o.adapter&&(a=(n=o).element,i=document.createElement("a"),!0===(s=n.options.download)?s={}:"string"==typeof s&&(s={filename:s}),i.download=s.filename||"chart.png",i.style.position="absolute",i.style.top="20px",i.style.right="20px",i.style.zIndex=1e3,i.style.lineHeight="20px",i.target="_blank",(l=document.createElement("img")).alt="Download",l.style.border="none",l.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAABCFBMVEUAAADMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMywEsqxAAAAV3RSTlMAAQIDBggJCgsMDQ4PERQaHB0eISIjJCouLzE0OTo/QUJHSUpLTU5PUllhYmltcHh5foWLjI+SlaCio6atr7S1t7m6vsHHyM7R2tze5Obo7fHz9ff5+/1hlxK2AAAA30lEQVQYGUXBhVYCQQBA0TdYWAt2d3d3YWAHyur7/z9xgD16Lw0DW+XKx+1GgX+FRzM3HWQWrHl5N/oapW5RPe0PkBu+UYeICvozTWZVK23Ao04B79oJrOsJDOoxkZoQPWgX29pHpCZEk7rEvQYiNSFq1UMqvlCjJkRBS1R8hb00Vb/TajtBL7nTHE1X1vyMQF732dQhyF2o6SAwrzP06iUQzvwsArlnzcOdrgBhJyHa1QOgO9U1GsKuvjUTjavliZYQ8nNPapG6sap/3nrIdJ6bOWzmX/fy0XVpfzZP3S8OJT3g9EEiJwAAAABJRU5ErkJggg==",i.appendChild(l),a.style.position="relative",n.__downloadAttached=!0,n.__enterEvent=tf(a,"mouseover",function(t){var e=t.relatedTarget;e&&(e===this||tm(this,e))||!n.options.download||(i.href=n.toImage(s),a.appendChild(i))}),n.__leaveEvent=tf(a,"mouseout",function(t){var e=t.relatedTarget;e&&(e===this||tm(this,e))||!i.parentNode||i.parentNode.removeChild(i)}))}(this.__chartName(),this)},tA.prototype.__config=function t(){return tb};var tk=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this)},e.prototype.__chartName=function t(){return"LineChart"},e}(tA),t0=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return td(this)},e.prototype.__chartName=function t(){return"PieChart"},e}(tA),tS=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,null,!0)},e.prototype.__chartName=function t(){return"ColumnChart"},e}(tA),tT=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,null,!0)},e.prototype.__chartName=function t(){return"BarChart"},e}(tA),t4=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this)},e.prototype.__chartName=function t(){return"AreaChart"},e}(tA),tD=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return td(this)},e.prototype.__chartName=function t(){return"GeoChart"},e}(tA),tE=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,"number")},e.prototype.__chartName=function t(){return"ScatterChart"},e}(tA),t1=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){return tu(this,"bubble")},e.prototype.__chartName=function t(){return"BubbleChart"},e}(tA),tL=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.__processData=function t(){var e,r=this.rawData;for(e=0;e<r.length;e++)r[e][1]=l(r[e][1]),r[e][2]=l(r[e][2]);return r},e.prototype.__chartName=function t(){return"Timeline"},e}(tA),tB={LineChart:tk,PieChart:t0,ColumnChart:tS,BarChart:tT,AreaChart:t4,GeoChart:tD,ScatterChart:tE,BubbleChart:t1,Timeline:tL,charts:{},configure:function(t){for(var e in t)t.hasOwnProperty(e)&&(tb[e]=t[e])},setDefaultOptions:function(t){tB.options=t},eachChart:function(t){for(var e in tB.charts)tB.charts.hasOwnProperty(e)&&t(tB.charts[e])},destroyAll:function(){for(var t in tB.charts)tB.charts.hasOwnProperty(t)&&(tB.charts[t].destroy(),delete tB.charts[t])},config:tb,options:{},adapters:tM,addAdapter:tw,use:function(t){return tw(t),tB}};return"undefined"==typeof window||window.Chartkick||(window.Chartkick=tB,document.addEventListener("turbolinks:before-render",function(){!1!==tb.autoDestroy&&tB.destroyAll()}),document.addEventListener("turbo:before-render",function(){!1!==tb.autoDestroy&&tB.destroyAll()}),setTimeout(function(){window.dispatchEvent(new Event("chartkick:load"))},0)),tB.default=tB,tB});
20
+ </script>
21
+
22
+ <script type="text/javascript">
23
+ window.onload = () => {
24
+ 'use strict'
25
+ document.querySelector('#navbarSideCollapse').addEventListener('click', () => {
26
+ document.querySelector('.offcanvas-collapse').classList.toggle('open')
27
+ })
28
+ }
29
+ </script>
30
+ <style type="text/css">
31
+ .bd-placeholder-img {
32
+ font-size: 1.125rem;
33
+ text-anchor: middle;
34
+ -webkit-user-select: none;
35
+ -moz-user-select: none;
36
+ user-select: none;
37
+ }
38
+
39
+ @media (min-width: 768px) {
40
+ .bd-placeholder-img-lg {
41
+ font-size: 3.5rem;
42
+ }
43
+ }
44
+
45
+ .b-example-divider {
46
+ height: 3rem;
47
+ background-color: rgba(0, 0, 0, .1);
48
+ border: solid rgba(0, 0, 0, .15);
49
+ border-width: 1px 0;
50
+ box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
51
+ }
52
+
53
+ .b-example-vr {
54
+ flex-shrink: 0;
55
+ width: 1.5rem;
56
+ height: 100vh;
57
+ }
58
+
59
+ .bi {
60
+ vertical-align: -.125em;
61
+ fill: currentColor;
62
+ }
63
+
64
+ .nav-scroller {
65
+ position: relative;
66
+ z-index: 2;
67
+ height: 2.75rem;
68
+ overflow-y: hidden;
69
+ }
70
+
71
+ .nav-scroller .nav {
72
+ display: flex;
73
+ flex-wrap: nowrap;
74
+ padding-bottom: 1rem;
75
+ margin-top: -1px;
76
+ overflow-x: auto;
77
+ text-align: center;
78
+ white-space: nowrap;
79
+ -webkit-overflow-scrolling: touch;
80
+ }
81
+ html,
82
+ body {
83
+ overflow-x: hidden; /* Prevent scroll on narrow devices */
84
+ }
85
+
86
+ body {
87
+ padding-top: 56px;
88
+ }
89
+
90
+ @media (max-width: 991.98px) {
91
+ .offcanvas-collapse {
92
+ position: fixed;
93
+ top: 56px; /* Height of navbar */
94
+ bottom: 0;
95
+ left: 100%;
96
+ width: 100%;
97
+ padding-right: 1rem;
98
+ padding-left: 1rem;
99
+ overflow-y: auto;
100
+ visibility: hidden;
101
+ background-color: #343a40;
102
+ transition: transform .3s ease-in-out, visibility .3s ease-in-out;
103
+ }
104
+ .offcanvas-collapse.open {
105
+ visibility: visible;
106
+ transform: translateX(-100%);
107
+ }
108
+ }
109
+
110
+ .nav-scroller .nav {
111
+ color: rgba(255, 255, 255, .75);
112
+ }
113
+
114
+ .nav-scroller .nav-link {
115
+ padding-top: .75rem;
116
+ padding-bottom: .75rem;
117
+ font-size: .875rem;
118
+ color: #6c757d;
119
+ }
120
+
121
+ .nav-scroller .nav-link:hover {
122
+ color: #007bff;
123
+ }
124
+
125
+ .nav-scroller .active {
126
+ font-weight: 500;
127
+ color: #343a40;
128
+ }
129
+
130
+ .bg-purple {
131
+ background-color: #6f42c1;
132
+ }
133
+ </style>
134
+ </head>
135
+ <body class="bg-light">
136
+
137
+ <nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark" aria-label="Main navigation">
138
+ <div class="container-fluid">
139
+ <a class="navbar-brand" href="#">Caffeinate</a>
140
+ <button class="navbar-toggler p-0 border-0" type="button" id="navbarSideCollapse" aria-label="Toggle navigation">
141
+ <span class="navbar-toggler-icon"></span>
142
+ </button>
143
+
144
+ <div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault">
145
+ <ul class="navbar-nav me-auto mb-2 mb-lg-0">
146
+ <li class="nav-item">
147
+ <%= active_link_to "Dashboard", root_path, active: controller_name == 'dashboard' %>
148
+ </li>
149
+ <li class="nav-item">
150
+ <%= active_link_to "Campaigns", campaigns_path, active: controller_name == 'campaigns' %>
151
+ </li>
152
+ <li class="nav-item">
153
+ <%= active_link_to "Subscriptions", subscriptions_path, active: controller_name == 'subscriptions' %>
154
+ </li>
155
+ <li class="nav-item">
156
+ <%= active_link_to "Mailings", mailings_path, active: controller_name == 'mailings' %>
157
+ </li>
158
+ </ul>
159
+ </div>
160
+ </div>
161
+ </nav>
162
+
163
+ <main class="container my-5">
164
+ <%= yield %>
165
+ </main>
166
+ <footer class="text-center small mb-3">
167
+ Running <a href="https://github.com/joshmn/caffeinate">Caffeinate <%= ::Caffeinate::VERSION %></a> and <a href="https://github.com/joshmn/caffeinate-webui">Caffeinate WebUI <%= ::Caffeinate::Webui::VERSION %></a>
168
+ </footer>
169
+ </body>
170
+ </html>
@@ -0,0 +1,58 @@
1
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
2
+ <div class="d-flex justify-content-between border-bottom pb-2 mb-0">
3
+ <h6 class="">Mailings</h6>
4
+ <div class="d-flex gap-4">
5
+ <div class="dropdown">
6
+ <a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
7
+ <%= @campaign ? @campaign.name : "All campaigns" %>
8
+ </a>
9
+ <ul class="dropdown-menu">
10
+ <% @campaigns.each do |campaign| %>
11
+ <li><a class="dropdown-item <%= 'active' if params[:campaign_id] == campaign.id.to_s %>" href="<%= mailings_path(campaign_id: campaign.id, status: @status) %>"><%= campaign.name %></a></li>
12
+ <% end %>
13
+ <% if @campaign %>
14
+ <li><a class="dropdown-item" href="<%= mailings_path(status: @status) %>">All campaigns</a></li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
18
+ <div class="dropdown">
19
+ <a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
20
+ <%= @status.try(:titleize) || "All" %>
21
+ </a>
22
+ <ul class="dropdown-menu">
23
+ <li><a class="dropdown-item <%= 'active' if params[:status] == 'unsent' %>" href="<%= mailings_path(campaign_id: @campaign.try(:id), status: :unsent) %>">Unsent</a></li>
24
+ <li><a class="dropdown-item <%= 'active' if params[:status] == 'sent' %>" href="<%= mailings_path(campaign_id: @campaign.try(:id), status: :sent) %>">Sent</a></li>
25
+ <li><a class="dropdown-item <%= 'active' if params[:status] == 'skipped' %>" href="<%= mailings_path(campaign_id: @campaign.try(:id), status: :skipped) %>">Skipped</a></li>
26
+ <% if @status %>
27
+ <li><a class="dropdown-item" href="<%= mailings_path(campaign_id: @campaign.try(:id)) %>">All states</a></li>
28
+ <% end %>
29
+ </ul>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="d-flex text-muted pt-3">
35
+ <table class="table">
36
+ <thead>
37
+ <tr>
38
+ <td>Who</td>
39
+ <td>Campaign</td>
40
+ <td>Send date</td>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <% @mailings.each do |mailing| %>
45
+ <tr>
46
+ <td><%= link_to Caffeinate::Webui::Name.for(mailing.subscriber), subscription_path(mailing.subscriber) %></td>
47
+ <td><%= link_to mailing.campaign.name, mailing.campaign %></td>
48
+ <td><%= time(mailing.send_at) %></td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
53
+ </div>
54
+ <div class="d-flex justify-content-end">
55
+ <%= will_paginate @mailings %>
56
+ </div>
57
+ </div>
58
+
@@ -0,0 +1 @@
1
+ <h2><%= @mailing.mailer_class %>#<%= @mailing.mailer_action %> for <%= ::Caffeinate::Webui::Name.for(@mailing.subscriber) %></h2>
@@ -0,0 +1,34 @@
1
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
2
+ <h6 class="border-bottom pb-2 mb-0">Subscriptions</h6>
3
+ <table class="table">
4
+ <thead>
5
+ <tr>
6
+ <td>Campaign</td>
7
+ <td>Who</td>
8
+ <td>Created</td>
9
+ <td>Status</td>
10
+ </tr>
11
+ </thead>
12
+ <tbody>
13
+ <% @subscriptions.each do |subscriber| %>
14
+ <tr>
15
+ <td><%= link_to subscriber.campaign.name, campaign_path(subscriber.campaign) %></td>
16
+ <td>
17
+ <%= link_to ::Caffeinate::Webui::Name.for(subscriber.subscriber), subscription_path(subscriber) %>
18
+ </td>
19
+ <td>
20
+ <%= time(subscriber.created_at) %>
21
+ </td>
22
+ <td>
23
+ <%= "Ended" if subscriber.ended? %>
24
+ <%= "Active" if subscriber.subscribed? %>
25
+ <%= "Unsubscribed" if subscriber.unsubscribed? %>
26
+ </td>
27
+ </tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
31
+ <div class="d-flex justify-content-end">
32
+ <%= will_paginate @subscriptions %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,50 @@
1
+ <div class="d-flex justify-content-between">
2
+ <div>
3
+ <h3><%= ::Caffeinate::Webui::Name.for(@subscription.subscriber) %></h3>
4
+ <p>
5
+ Subscribed: <%= time_ago_in_words @subscription.created_at %> ago
6
+ </p>
7
+ </div>
8
+ <div class="dropdown">
9
+ <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
10
+ <%= "Active" if @subscription.subscribed? %><%= "Completed" if @subscription.ended? %><%= "Unsubscribed" if @subscription.unsubscribed? %>
11
+ </button>
12
+ <ul class="dropdown-menu">
13
+ <% if @subscription.subscribed? %>
14
+ <li><%= button_to "Unsubscribe", subscription_unsubscribe_path(@subscription), method: :post, class: "dropdown-item" if @subscription.subscribed? %></li>
15
+ <% else %>
16
+ <li>No actions</li>
17
+ <% end %>
18
+ </ul>
19
+ </div>
20
+ </div>
21
+ <div class="my-3 p-3 bg-body rounded shadow-sm">
22
+ <h6 class="border-bottom pb-2 mb-0">Mailings</h6>
23
+ <table class="table">
24
+ <thead>
25
+ <tr>
26
+ <td>Mail</td>
27
+ <td>Status</td>
28
+ <td></td>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <% @subscription.mailings.each do |mailing| %>
33
+ <tr>
34
+ <td>
35
+ <%= mailing.mailer_class %>#<%= mailing.mailer_action %>
36
+ </td>
37
+ <td>
38
+ <%= "Skipped" if mailing.skipped? %>
39
+ <% if @subscription.subscribed? %>
40
+ Sends in <%= distance_of_time_in_words_to_now(mailing.send_at) if mailing.unsent? %> from now
41
+ <%= time_ago_in_words(mailing.sent_at) if mailing.sent? %>
42
+ <% else %>
43
+ Unsubscribed
44
+ <% end %>
45
+ </td>
46
+ </tr>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ Caffeinate::Webui::Engine.routes.draw do
2
+ root to: 'dashboard#show'
3
+
4
+ resources :campaigns, only: [:index, :show]
5
+ resources :subscriptions, only: [:index, :show, :destroy] do
6
+ scope module: :subscriptions do
7
+ resource :unsubscribe, only: [:create]
8
+ end
9
+ end
10
+ resources :mailings, only: [:index, :show, :destroy]
11
+ end
@@ -0,0 +1,13 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace ::Caffeinate::Webui
5
+
6
+ config.generators do |g|
7
+ g.test_framework :rspec, fixture: false
8
+ end
9
+
10
+ config.autoload_paths += Dir["#{config.root}/lib/**/"]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module Caffeinate
2
+ module Webui
3
+ class Name
4
+ def self.for(object)
5
+ if object.respond_to?(:name)
6
+ object.name
7
+ elsif object.respond_to?(:full_name)
8
+ object.full_name
9
+ elsif object.respond_to?(:display_name)
10
+ object.display_name
11
+ elsif object.respond_to?(:email)
12
+ object.email
13
+ elsif object.respond_to?(:to_label)
14
+ object.to_label
15
+ else
16
+ "#{object.class.name}##{object.to_param}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Caffeinate
2
+ module Webui
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+
2
+ require 'active_support'
3
+ require 'groupdate'
4
+ require 'chartkick'
5
+ require 'will_paginate'
6
+ require 'will_paginate-bootstrap-style'
7
+ require 'caffeinate/version'
8
+ require 'caffeinate/webui/version'
9
+ require 'caffeinate/webui/engine'
10
+ require 'caffeinate/webui/name'
11
+
12
+ module Caffeinate
13
+ module Webui
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ require 'caffeinate/webui'
metadata ADDED
@@ -0,0 +1,222 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: caffeinate_webui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Brody
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: caffeinate
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sprockets-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: groupdate
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: chartkick
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: will_paginate-bootstrap-style
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.2.4
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.2.4
97
+ - !ruby/object:Gem::Dependency
98
+ name: will_paginate
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sqlite3
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: codecov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: Create, manage, and send scheduled email sequences and drip campaigns
168
+ from your Rails app.
169
+ email:
170
+ - josh@josh.mn
171
+ executables: []
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - README.md
176
+ - Rakefile
177
+ - app/controllers/caffeinate/webui/application_controller.rb
178
+ - app/controllers/caffeinate/webui/campaigns_controller.rb
179
+ - app/controllers/caffeinate/webui/dashboard_controller.rb
180
+ - app/controllers/caffeinate/webui/mailings_controller.rb
181
+ - app/controllers/caffeinate/webui/subscriptions/unsubscribes_controller.rb
182
+ - app/controllers/caffeinate/webui/subscriptions_controller.rb
183
+ - app/helpers/caffeinate/webui/application_helper.rb
184
+ - app/views/caffeinate/webui/campaigns/index.html.erb
185
+ - app/views/caffeinate/webui/campaigns/show.html.erb
186
+ - app/views/caffeinate/webui/dashboard/show.html.erb
187
+ - app/views/caffeinate/webui/layouts/application.html.erb
188
+ - app/views/caffeinate/webui/mailings/index.html.erb
189
+ - app/views/caffeinate/webui/mailings/show.html.erb
190
+ - app/views/caffeinate/webui/subscriptions/index.html.erb
191
+ - app/views/caffeinate/webui/subscriptions/show.html.erb
192
+ - config/routes.rb
193
+ - lib/caffeinate/webui.rb
194
+ - lib/caffeinate/webui/engine.rb
195
+ - lib/caffeinate/webui/name.rb
196
+ - lib/caffeinate/webui/version.rb
197
+ - lib/caffeinate_webui.rb
198
+ homepage: https://github.com/joshmn/caffeinate_webui
199
+ licenses:
200
+ - MIT
201
+ metadata: {}
202
+ post_install_message:
203
+ rdoc_options: []
204
+ require_paths:
205
+ - lib
206
+ required_ruby_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ required_rubygems_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ requirements: []
217
+ rubygems_version: 3.1.4
218
+ signing_key:
219
+ specification_version: 4
220
+ summary: Create, manage, and send scheduled email sequences and drip campaigns from
221
+ your Rails app.
222
+ test_files: []