caffeinate_webui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []