litestack 0.2.3 → 0.3.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Gemfile +2 -0
  4. data/README.md +5 -11
  5. data/assets/event_page.png +0 -0
  6. data/assets/index_page.png +0 -0
  7. data/assets/topic_page.png +0 -0
  8. data/bench/bench_jobs_rails.rb +1 -1
  9. data/bench/bench_jobs_raw.rb +1 -1
  10. data/bin/liteboard +81 -0
  11. data/lib/action_cable/subscription_adapter/litecable.rb +1 -11
  12. data/lib/generators/litestack/install/USAGE +11 -0
  13. data/lib/generators/litestack/install/install_generator.rb +35 -0
  14. data/lib/generators/litestack/install/templates/cable.yml +11 -0
  15. data/lib/generators/litestack/install/templates/database.yml +34 -0
  16. data/lib/litestack/liteboard/liteboard.rb +305 -0
  17. data/lib/litestack/liteboard/views/event.erb +32 -0
  18. data/lib/litestack/liteboard/views/index.erb +54 -0
  19. data/lib/litestack/liteboard/views/layout.erb +303 -0
  20. data/lib/litestack/liteboard/views/litecable.erb +118 -0
  21. data/lib/litestack/liteboard/views/litecache.erb +144 -0
  22. data/lib/litestack/liteboard/views/litedb.erb +168 -0
  23. data/lib/litestack/liteboard/views/litejob.erb +151 -0
  24. data/lib/litestack/liteboard/views/topic.erb +48 -0
  25. data/lib/litestack/litecable.rb +25 -35
  26. data/lib/litestack/litecable.sql.yml +1 -1
  27. data/lib/litestack/litecache.rb +31 -28
  28. data/lib/litestack/litedb.rb +124 -1
  29. data/lib/litestack/litejob.rb +2 -2
  30. data/lib/litestack/litejobqueue.rb +8 -8
  31. data/lib/litestack/litemetric.rb +177 -88
  32. data/lib/litestack/litemetric.sql.yml +312 -42
  33. data/lib/litestack/litemetric_collector.sql.yml +56 -0
  34. data/lib/litestack/litequeue.rb +28 -29
  35. data/lib/litestack/litequeue.sql.yml +11 -0
  36. data/lib/litestack/litesupport.rb +137 -57
  37. data/lib/litestack/railtie.rb +10 -0
  38. data/lib/litestack/version.rb +1 -1
  39. data/lib/litestack.rb +1 -0
  40. data/lib/sequel/adapters/litedb.rb +1 -1
  41. data/template.rb +7 -0
  42. metadata +81 -5
  43. data/lib/litestack/metrics_app.rb +0 -5
@@ -0,0 +1,54 @@
1
+
2
+
3
+
4
+
5
+ <div class="container">
6
+ <% @topics.each do |topic|%>
7
+
8
+ <div class = "row justify-content-center">
9
+
10
+ <div class = "col-6">
11
+ <div class="card">
12
+ <div class="card-header">
13
+ <a href="./topics/<%=encode(topic[0])%>?res=<%=@res%>"><%=topic[0]%></a>
14
+ </div>
15
+ <div class="card-body">
16
+ <div class="container">
17
+ <div class= "row">
18
+ <div class= "col">
19
+ <h1><%=topic[3]%> <span class="fs-4">events</span></h1>
20
+ </div>
21
+ <div class= "col">
22
+ <span class="inlineminicolumn hidden" data-label="Count"><%=Oj.dump(topic[4].unshift(['Time', 'Count'])) if topic[4]%></span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ </div>
31
+
32
+ <div class = "row">
33
+ &nbsp;<br/>
34
+ </div>
35
+
36
+
37
+ <%end%>
38
+ <% if @topics.empty? %>
39
+ <div class = "row justify-content-center">
40
+
41
+ <div class = "col-6">
42
+ <div class="card">
43
+ <div class="card-header">
44
+ Topics
45
+ </div>
46
+ <div class="card-body justify-content-center">
47
+ No data to display
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ </div>
53
+ <%end%>
54
+ </div>
@@ -0,0 +1,303 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>liteboard</title>
5
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
6
+ <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Antonio">
9
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
+ <style>
11
+ body {background-color: #fff}
12
+ input, select { border-radius: 3px}
13
+ div#header { width: 100%; border-bottom: 1px solid #089;}
14
+ div.label {max-width:400px;overflow:hidden}
15
+ h1 { font-family: antonio }
16
+ #content{ padding-right: 12px; padding-left: 12px; padding-bottom: 60px;}
17
+ table.head { margin-top: 12px; margin-bottom:12px}
18
+ select { color: #078; background-color: #fff; font-weight: normal }
19
+ .table th { color: #078; font-weight: normal; }
20
+ .table th.sorted { font-weight: bold }
21
+ .table td { color: #444; vertical-align:middle; font-size: 18px}
22
+ .table td:first-child { color: #444; vertical-align:middle; font-size: 15px; font-weight:normal}
23
+ .table td.empty { text-align:center}
24
+ a, a.nav-link { color: #078; }
25
+ .nav-pills .nav-link.active { color: #fff; background-color: #078}
26
+ a .logo { color: #000;}
27
+ a:visited { color: #078; }
28
+ .hidden { display: none}
29
+ div#search {margin-bottom: 8px}
30
+ div#footer {position:fixed; left:0px; height: 40px; width:100%; background-color:#0891; border-top: #0893 1px solid; padding: 8px; bottom: 0; text-align: right}
31
+ .logo{font-family: antonio}
32
+ .logo-half{ color: #078 }
33
+ .smaller { font-size: 24px; font-weight: normal}
34
+ .token {background-color: #ed9}
35
+ svg > g > g.google-visualization-tooltip { pointer-events : none }
36
+ .material-icons { vertical-align: middle}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div id="content">
41
+ <div id="header">
42
+ <h1><span class="logo"><span class="logo-half">lite</span>board | </span> <span class="logo smaller">the <span class="logo-half">lite</span>metric dashboard</span></span></h1>
43
+ </div>
44
+ <div class="container">
45
+ <div class = "row">
46
+ &nbsp;<br/>
47
+ </div>
48
+ <div class="row">
49
+ <div class="col">
50
+ <nav class="navbar bg-body-tertiary">
51
+ <div>&nbsp;&nbsp;Showing data for the last <select onchange="window.location = locationWithParam('res', this.value)">
52
+ <%= mapping = {'hour' => '60 minutes', 'day' => '24 hours', 'week' => '7 days', 'year' => '52 weeks'}%>
53
+ <% ['hour', 'day', 'week', 'year'].each do |res| %>
54
+ <option value=<%=res%> <%='selected' if res == @res%>><%=mapping[res]%></option>
55
+ <% end %>
56
+ </select></div>
57
+ </nav>
58
+ </div>
59
+ </div>
60
+ <div class = "row">
61
+ &nbsp;<br/>
62
+ </div>
63
+ <%= yield %>
64
+
65
+ </div>
66
+
67
+ <div class="container" style="position: fixed; left:0px; top: 145px">
68
+ <div class="row justify-content-center">
69
+ <div class="col">
70
+ <div class="card" style="width: 15rem;">
71
+ <div class="card-body">
72
+ <ul class="nav nav-pills nav-fill flex-column list-group list-group-flush">
73
+ <li class="list-group-item"><a class="nav-link <%='active' unless @topic%>" href="<%=index_url%>">Home</a></li>
74
+ <%@topics.each do |topic|%>
75
+ <li class="list-group-item"><a class="nav-link <%='active' if @topic == topic[0]%>" href="<%=topic_url(topic[0])%>"><%=topic[0]%></a></li>
76
+ <%end%>
77
+ </ul>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ </div>
85
+ <div id="footer">
86
+ Powered by <a href="https://www.github.com/oldmoe/litestack" target="_blank"><span class="logo"><span class="logo-half">lite</span>stack</span></a>
87
+ </div>
88
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
89
+ </body>
90
+ <script>
91
+ google.charts.load('current', {'packages':['corechart', 'bar']});
92
+
93
+ google.charts.setOnLoadCallback(drawMiniColumnChart);
94
+ google.charts.setOnLoadCallback(drawColumnChart);
95
+ google.charts.setOnLoadCallback(drawPieChart);
96
+ google.charts.setOnLoadCallback(drawStackedColumnChart);
97
+
98
+
99
+ function drawMiniColumnChart() {
100
+ elements = document.querySelectorAll(".inlineminicolumn")
101
+ elements.forEach(element => {
102
+ var label = element.dataset.label;
103
+ var mydata = eval(element.innerText)
104
+ element.innerText = ''
105
+ element.classList.remove("hidden")
106
+ if(mydata.length > 1) {
107
+ mydata.forEach(row => {
108
+ if(mydata[0].length == 5){ // we are doing custom tooltips
109
+ if(row[0] != "Time"){
110
+ row[1] = Number(row[1].toPrecision(4))
111
+ row[3] = Number(row[2].toPrecision(4))
112
+ row[2] = row[0]+': '+mydata[0][1]+' '+row[1]
113
+ row[4] = row[0]+': '+mydata[0][3]+' '+row[3]
114
+ }
115
+ }
116
+ })
117
+ var data = google.visualization.arrayToDataTable(
118
+ mydata
119
+ )
120
+ var options = {
121
+ animation: {'startup': true, 'duration': 300},
122
+ width: 300,
123
+ height: 70,
124
+ chartArea: {width:'100%', height: '100%'},
125
+ backgroundColor: 'none',
126
+ bar: {groupWidth: "61.8%"},
127
+ colors : ['#089', 'silver' ],
128
+ vAxis: {'gridlines': {'count' : 0}, 'textPosition' : 'none', 'baselineColor' : 'none'},
129
+ hAxis: { 'count' : 0, 'textPosition' : 'none', 'baselineColor' : 'none'},
130
+ legend: {'position': 'none'},
131
+ tooltip: {showColorCode: true, isHtml: true},
132
+ isStacked: true
133
+ }
134
+ var chart = new google.visualization.ColumnChart(element);
135
+ chart.draw(data, options);
136
+ }
137
+ })
138
+ }
139
+
140
+ function drawColumnChart() {
141
+ elements = document.querySelectorAll(".inlinecolumn")
142
+ elements.forEach(element => {
143
+ var label = element.dataset.label;
144
+ var mydata = eval(element.innerText)
145
+ element.innerText = ''
146
+ element.classList.remove("hidden")
147
+ if(mydata.length > 1) {
148
+ mydata.forEach(row => {
149
+ if(mydata[0].length == 5){ // we are doing custom tooltips
150
+ if(row[0] != "Time"){
151
+ row[1] = Number(row[1].toPrecision(4))
152
+ row[3] = Number(row[2].toPrecision(4))
153
+ row[2] = row[0]+': '+mydata[0][1]+' '+row[1]
154
+ row[4] = row[0]+': '+mydata[0][3]+' '+row[3]
155
+ }
156
+ }
157
+ })
158
+ var data = google.visualization.arrayToDataTable(
159
+ mydata
160
+ )
161
+ var options = {
162
+ animation: {'startup': true, 'duration': 300},
163
+ width: 550,
164
+ height: 350,
165
+ chartArea: {width:'100%', height: '80%'},
166
+ backgroundColor: 'none',
167
+ bar: {groupWidth: "61.8%"},
168
+ colors : ['#089', 'silver' ],
169
+ vAxis: {'gridlines': {'count' : 0}, 'textPosition' : 'none', 'baselineColor' : 'none'},
170
+ hAxis: { 'count' : 0, 'textPosition' : 'none', 'baselineColor' : 'none'},
171
+ legend: {'position': 'bottom'},
172
+ tooltip: {showColorCode: true, isHtml: true},
173
+ isStacked: true
174
+ }
175
+ var chart = new google.visualization.ColumnChart(element);
176
+ chart.draw(data, options);
177
+ }
178
+ })
179
+ }
180
+
181
+ function drawPieChart() {
182
+ elements = document.querySelectorAll(".inlinepie")
183
+ elements.forEach(element => {
184
+ var label = element.dataset.label;
185
+ var mydata = eval(element.innerText)
186
+ element.innerText = ''
187
+ element.classList.remove("hidden")
188
+ if(mydata.length >= 2) {
189
+ var data = google.visualization.arrayToDataTable(
190
+ mydata
191
+ )
192
+ var options = {
193
+ animation: {'startup': true, 'duration': 300},
194
+ annotations: {textStyle: {bold: true}, alwaysOutside: false },
195
+ width: 600,
196
+ height: 350,
197
+ backgroundColor: 'none',
198
+ bar: {groupWidth: "80%"},
199
+ colors : ['#089', 'silver', '#545B77' ],
200
+ axisTitlesPosition: 'none',
201
+ chartArea: {width:'90%', height: '85%'},
202
+ tooltip : {showColorCode: true},
203
+ vAxis: {gridlines: {count : 0}, textPosition : 'in', baselineColor : 'none', textStyle: {color: '#089', bold: true} },
204
+ hAxis: {gridlines: {count : 0}, textPosition : 'none', baselineColor : 'none'},
205
+ legend: {'position': 'bottom'},
206
+ isStacked: true,
207
+ bars: 'horizontal'
208
+ }
209
+ var chart = new google.visualization.PieChart(element);
210
+ if(mydata[1][1] == 0 && mydata[2][1] == 0){
211
+ return 0
212
+ }
213
+ chart.draw(data, options);
214
+ }
215
+ })
216
+ }
217
+
218
+
219
+ function drawStackedColumnChart() {
220
+ elements = document.querySelectorAll(".inlinestackedcolumn")
221
+ elements.forEach(element => {
222
+ var label = element.dataset.label;
223
+ var mydata = eval(element.innerText)
224
+ element.innerText = ''
225
+ element.classList.remove("hidden")
226
+ if(mydata.length > 1) {
227
+ console.log(mydata)
228
+ var data = google.visualization.arrayToDataTable(
229
+ mydata
230
+ )
231
+ var options = {
232
+ animation: {'startup': true, 'duration': 300},
233
+ annotations: {textStyle: {bold: true}, alwaysOutside: false },
234
+ width: 600,
235
+ height: 350,
236
+ backgroundColor: 'none',
237
+ bar: {groupWidth: "80%"},
238
+ colors : [ '#089', 'silver', '#545B77', 'silver'],
239
+ axisTitlesPosition: 'none',
240
+ chartArea: {width:'100%', height: '85%'},
241
+ tooltip : {showColorCode: true},
242
+ vAxis: {gridlines: {count : 0}, textPosition : 'none', baselineColor : 'none', textStyle: {color: '#089', bold: true} },
243
+ hAxis: {gridlines: {count : 0}, textPosition : 'none', baselineColor : 'none'},
244
+ legend: {'position': 'bottom'},
245
+ isStacked: true,
246
+ bars: 'horizontal'
247
+ }
248
+ var chart = new google.visualization.AreaChart(element);
249
+ chart.draw(data, options);
250
+ }
251
+ })
252
+ }
253
+
254
+
255
+ function search_kd(el){
256
+ //store the current value
257
+ el.oldvalue = el.value
258
+ }
259
+ function search_ku(el){
260
+ //check if the value has changed and if so
261
+ // set a new timer to fire a request in 300ms
262
+ // removing any existing timer first
263
+ if(el.value == el.oldvalue){
264
+ return
265
+ }else{
266
+ el.oldvalue = null
267
+ }
268
+ if(el.timeout){
269
+ window.clearTimeout(el.timeout)
270
+ }
271
+ el.timeout = window.setTimeout(function(){
272
+ el.timeout = null
273
+ window.location = locationWithParam('search', el.value)
274
+ }, 500)
275
+ }
276
+
277
+ $(document).ready(function(){
278
+ /*
279
+ el = $('#search-field')[0]
280
+ el.focus()
281
+ if(el.value && el.value.length > 0){
282
+ el.setSelectionRange(el.value.length, el.value.length)
283
+ var list = $("table.sortable div.label") //[0].children[0].children
284
+ for(var i=0; i < list.length; i++){
285
+ //console.log(list[i])
286
+ var link = list[i].children[0] //.children[0].children[0]
287
+ var re = new RegExp("("+el.value+")", "giu")
288
+ link.innerHTML = link.innerHTML.replaceAll(re, "<span class='token'>$1</span>") ;
289
+ }
290
+ }
291
+ */
292
+ })
293
+
294
+ function locationWithParam(param, value){
295
+ var query = window.location.search
296
+ var params = new URLSearchParams(query)
297
+ params.set(param, value)
298
+ var l = window.location
299
+ return l.origin + l.pathname + '?' + params.toString()
300
+ }
301
+ </script>
302
+ </html>
303
+
@@ -0,0 +1,118 @@
1
+ <div class = "row">
2
+
3
+ <div class = "col">
4
+ <div class="card">
5
+ <div class="card-header">
6
+ Subscriptions
7
+ </div>
8
+ <div class="card-body">
9
+ <h1><%=format(@subscription_count)%></h1>
10
+ </div>
11
+ </div>
12
+ </div>
13
+
14
+ <div class = "col">
15
+ <div class="card">
16
+ <div class="card-header">
17
+ Messages Received
18
+ </div>
19
+ <div class="card-body">
20
+ <h1><%=format(@broadcast_count)%></h1>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div class = "col">
26
+ <div class="card">
27
+ <div class="card-header">
28
+ Messages delivered
29
+ </div>
30
+ <div class="card-body">
31
+ <h1><%=format(@message_count)%></h1>
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ </div>
37
+
38
+ <div class = "row">
39
+ &nbsp;<br/>
40
+ </div>
41
+
42
+ <div class="row">
43
+
44
+ <div class = "col">
45
+ <div class="card">
46
+ <div class="card-header">
47
+ Subscriptions over time
48
+ </div>
49
+ <div class="card-body">
50
+ <span class="hidden inlinecolumn">
51
+ <%=[["Time", "Count"]] + @subscriptions_over_time.to_a%>
52
+ </span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div class = "col">
58
+ <div class="card">
59
+ <div class="card-header">
60
+ Messages received/delivered over time
61
+ </div>
62
+ <div class="card-body">
63
+ <span class="hidden inlinestackedcolumn">
64
+ <%=[["Time", "Recieved Count", "Delivered Count"]] + @messages_over_time.to_a%>
65
+ </span>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ </div>
71
+
72
+ <div class = "row">
73
+ &nbsp;<br/>
74
+ </div>
75
+
76
+ <div class = "row">
77
+
78
+ <div class = "col-6">
79
+ <div class="card">
80
+ <div class="card-header">
81
+ Channels with most subscriptions
82
+ </div>
83
+ <div class="card-body">
84
+ <table class="table">
85
+ <%@top_subscribed_channels.each do |r| %>
86
+ <tr>
87
+ <td><%=r['key']%></td>
88
+ <td align="right"><h6><%=format(r['rcount'])%>&nbsp;subs</h6></td>
89
+ </tr>
90
+ <% end %>
91
+ </table>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <div class = "col-6">
97
+ <div class="card">
98
+ <div class="card-header">
99
+ Channels with most messages delivered
100
+ </div>
101
+ <div class="card-body">
102
+ <table class="table">
103
+ <%@top_messaged_channels.each do |r| %>
104
+ <tr>
105
+ <td><%=r['key']%></td>
106
+ <td align="right"><h6><%=format(r['rcount'])%>&nbsp;messages</h6></td>
107
+ </tr>
108
+ <% end %>
109
+ </table>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ </div>
115
+
116
+ <div class = "row">
117
+ &nbsp;<br/>
118
+ </div>
@@ -0,0 +1,144 @@
1
+
2
+ <div class = "row">
3
+
4
+ <div class = "col">
5
+ <div class="card">
6
+ <div class="card-header">
7
+ Current size / Max size
8
+ </div>
9
+ <div class="card-body">
10
+ <h1><%=format(round(@size))%>MB / <%=format(round(@max_size))%>MB <span class="fs-4"><%=round(@full)%>% full</span></h1>
11
+ </div>
12
+ </div>
13
+ </div>
14
+
15
+ <div class = "col">
16
+ <div class="card">
17
+ <div class="card-header">
18
+ Number of entries
19
+ </div>
20
+ <div class="card-body">
21
+ <h1><%=format(@entries)%></h1>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ </div>
27
+
28
+ <div class = "row">
29
+ &nbsp;<br/>
30
+ </div>
31
+
32
+ <div class = "row">
33
+
34
+ <div class = "col">
35
+ <div class="card">
36
+ <div class="card-header">
37
+ Reads / Writes
38
+ </div>
39
+ <div class="card-body">
40
+ <h1><%=format(@reads)%> / <%=format(@writes)%> <span class="fs-4"><%=round(@reads.to_f/@writes) rescue 0%> reads/write</span></h1>
41
+ <hr/>
42
+ <span class="hidden inlinepie"><%=[["name", "value"],["reads", @reads],["writes", @writes]]%></span>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div class = "col">
48
+ <div class="card">
49
+ <div class="card-header">
50
+ Read hits / Misses
51
+ </div>
52
+ <div class="card-body">
53
+ <h1><%=format(@hits)%> / <%=format(@misses)%> <span class="fs-4"><%=round(@hitrate*100)%>% hit rate</span></h1>
54
+ <hr/>
55
+ <span class="hidden inlinepie"><%=[["name", "value"],["hits", @hits],["misses", @misses]]%></span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ </div>
61
+
62
+ <div class = "row">
63
+ &nbsp;<br/>
64
+ </div>
65
+
66
+ <div class = "row">
67
+
68
+ <div class = "col">
69
+ <div class="card">
70
+ <div class="card-header">
71
+ Reads, writes over time
72
+ </div>
73
+ <div class="card-body">
74
+ <div class="hidden inlinecolumn">
75
+ <%=Oj.dump([["Time", "Reads", "Writes"]] + @reads_vs_writes)%>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div class = "col">
82
+ <div class="card">
83
+ <div class="card-header">
84
+ Hits vs misses over time
85
+ </div>
86
+ <div class="card-body">
87
+ <span class="hidden inlinecolumn">
88
+ <%=Oj.dump([["Time", "Hits", "Misses"]] + @hits_vs_misses)%>
89
+ </span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ </div>
95
+
96
+ <div class = "row">
97
+ &nbsp;<br/>
98
+ </div>
99
+
100
+ <div class = "row">
101
+
102
+ <div class = "col">
103
+ <div class="card">
104
+ <div class="card-header">
105
+ Most read entries
106
+ </div>
107
+ <div class="card-body">
108
+ <table class="table">
109
+ <%@top_reads.each do |r| %>
110
+ <tr>
111
+ <td><%=r['key']%></td>
112
+ <td align="right"><h5><%=format(r['rcount'])%></h5></td>
113
+ </tr>
114
+ <% end %>
115
+ </table>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class = "col">
121
+ <div class="card">
122
+ <div class="card-header">
123
+ Most written entries
124
+ </div>
125
+ <div class="card-body">
126
+ <table class="table">
127
+ <%@top_writes.each do |r| %>
128
+ <tr>
129
+ <td><%=r['key']%></td>
130
+ <td align="right"><h5><%=format(r['rcount'])%></h5></td>
131
+ </tr>
132
+ <% end %>
133
+ </table>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ </div>
139
+
140
+ <div class = "row">
141
+ &nbsp;<br/>
142
+ </div>
143
+
144
+