Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_traffic.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_traffic.cgi

added on local at 2026-07-06 01:09:36

Added
+545
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 7685a73837ea
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2
3##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26use CGI; my $query = new CGI; my $form = $query->Vars;
27use MODS::Template; my $tfile = new MODS::Template;
28use MODS::DBConnect; my $db = new MODS::DBConnect;
29use MODS::Login; my $login = new MODS::Login;
30use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name');
31use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper;
32use MODS::PTMatrix::Stats; my $stats = new MODS::PTMatrix::Stats;
33
34
35
36#############################################################
37## Get form data
38#############################################################
39
40
41
42############################################################
43# Program Controls
44############################################################
45$|=1;
46my $userinfo = $login->login_verify();
47if(!$userinfo){
48 print qq~Status: 302 Found\nLocation: /login.cgi?return=/admin_traffic.cgi\n\n~;
49 exit;
50}
51if(!$userinfo->{is_admin} && !$userinfo->{is_super_admin}){
52 print qq~Status: 403 Forbidden\nContent-Type: text/plain\n\nAdmin access required.\n~;
53 exit;
54}
55
56&admin_traffic;
57
58
59############################################################
60# Subroutines
61############################################################
62sub h{
63 my $s = shift;
64 $s = '' unless defined $s;
65 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
66 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
67 return $s;
68}
69
70sub jstr{
71 my $s = shift;
72 $s = '' unless defined $s;
73 $s =~ s/\\/\\\\/g; $s =~ s/"/\\"/g; $s =~ s/[\x00-\x1f]//g;
74 return $s;
75}
76
77sub parse_dt{
78 my $s = shift;
79 return undef unless defined($s) && length($s);
80 $s =~ s/[^0-9 :T\-]//g;
81 if($s =~ /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(?::\d{2})?$/){return "$1 $2:00";}
82 if($s =~ /^(\d{4}-\d{2}-\d{2})$/){return "$1 00:00:00";}
83 return undef;
84}
85
86sub rows_with_pct{
87 my($aref, $label_key, $count_key) = @_;
88 my $total = 0;
89 $total += ($_->{$count_key} || 0) for @$aref;
90 my @out;
91 foreach my $r(@$aref){
92 my $n = $r->{$count_key} || 0;
93 my $pct = $total ? sprintf('%.1f', ($n / $total) * 100) : '0.0';
94 push @out, { label => &h($r->{$label_key} || 'Unknown'), count => $n, pct => $pct };
95 }
96 return \@out;
97}
98
99sub build_globe_json{
100 my($dbh) = @_;
101 my @globe = $stats->by_country_geo($db, $dbh, $database_name, 80);
102 my @json_rows;
103 foreach my $g(@globe){
104 push @json_rows, sprintf('{"code":"%s","name":"%s","lat":%s,"lng":%s,"count":%d}',
105 $g->{code}, &jstr($g->{name}), $g->{lat}+0, $g->{lng}+0, $g->{visitors}+0);
106 }
107 return ('[' . join(',', @json_rows) . ']', scalar(@globe));
108}
109
110sub build_state_json{
111 my($dbh) = @_;
112 my $sr = $stats->state_rollup($db, $dbh, $database_name);
113 my @sparts;
114 foreach my $code(sort keys %$sr){
115 my @rp;
116 foreach my $region(sort keys %{ $sr->{$code} }){
117 my $rj = $region; $rj =~ s/\\/\\\\/g; $rj =~ s/"/\\"/g; $rj =~ s/[\x00-\x1f]//g;
118 push @rp, sprintf('"%s":%d', $rj, $sr->{$code}{$region}+0);
119 }
120 push @sparts, sprintf('"%s":{%s}', $code, join(',', @rp));
121 }
122 return '{' . join(',', @sparts) . '}';
123}
124
125sub build_tree_json{
126 #Full country -> state -> city drill tree for the geo modal
127 my($dbh) = @_;
128 my $tree2 = $stats->full_drill_tree($db, $dbh, $database_name);
129 my @tparts;
130 foreach my $cc(@{ $tree2->{countries} }){
131 my @sparts2;
132 foreach my $st(@{ $cc->{states} }){
133 my @cparts;
134 foreach my $ct(@{ $st->{cities} }){
135 my $cj = $ct->{city}; $cj =~ s/\\/\\\\/g; $cj =~ s/"/\\"/g; $cj =~ s/[\x00-\x1f]//g;
136 push @cparts, sprintf('{"city":"%s","visitors":%d,"sessions":%d}', $cj, $ct->{visitors}+0, $ct->{sessions}+0);
137 }
138 my $rj = $st->{region}; $rj =~ s/\\/\\\\/g; $rj =~ s/"/\\"/g; $rj =~ s/[\x00-\x1f]//g;
139 push @sparts2, sprintf('"%s":{"visitors":%d,"sessions":%d,"cities":[%s]}', $rj, $st->{visitors}+0, $st->{sessions}+0, join(',', @cparts));
140 }
141 push @tparts, sprintf('"%s":{"visitors":%d,"sessions":%d,"states":{%s}}', $cc->{code}, $cc->{visitors}+0, $cc->{sessions}+0, join(',', @sparts2));
142 }
143 return '{' . join(',', @tparts) . '}';
144}
145
146sub build_dims_json{
147 my($dbh) = @_;
148 my $dims = $stats->country_dimensions_rollup($db, $dbh, $database_name);
149 my @dparts;
150 foreach my $code(sort keys %$dims){
151 my @dx;
152 foreach my $dim(qw(browsers os devices resolutions viewports languages timezones)){
153 my $rh = $dims->{$code}{$dim} || {};
154 my @vp;
155 foreach my $v(sort { $rh->{$b} <=> $rh->{$a} } keys %$rh){
156 my $vj = $v; $vj =~ s/\\/\\\\/g; $vj =~ s/"/\\"/g; $vj =~ s/[\x00-\x1f]//g;
157 push @vp, sprintf('"%s":%d', $vj, $rh->{$v}+0);
158 }
159 push @dx, sprintf('"%s":{%s}', $dim, join(',', @vp));
160 }
161 push @dparts, sprintf('"%s":{%s}', $code, join(',', @dx));
162 }
163 return ('{' . join(',', @dparts) . '}', $dims);
164}
165
166sub build_overall_dims_json{
167 my($dims) = @_;
168 my %overall;
169 foreach my $code(keys %$dims){
170 foreach my $dim(qw(browsers os devices resolutions viewports languages timezones)){
171 my $rh = $dims->{$code}{$dim} || {};
172 foreach my $v(keys %$rh){$overall{$dim}{$v} += $rh->{$v};}
173 }
174 }
175 my @oparts;
176 foreach my $dim(qw(browsers os devices resolutions viewports languages timezones)){
177 my $rh = $overall{$dim} || {};
178 my @vp;
179 foreach my $v(sort { $rh->{$b} <=> $rh->{$a} } keys %$rh){
180 my $vj = $v; $vj =~ s/\\/\\\\/g; $vj =~ s/"/\\"/g; $vj =~ s/[\x00-\x1f]//g;
181 push @vp, sprintf('"%s":%d', $vj, $rh->{$v}+0);
182 }
183 push @oparts, sprintf('"%s":{%s}', $dim, join(',', @vp));
184 }
185 return '{' . join(',', @oparts) . '}';
186}
187
188sub build_sdims_json{
189 my($dbh) = @_;
190 my $sdims = $stats->state_dimensions_rollup($db, $dbh, $database_name);
191 my @sdparts;
192 foreach my $code(sort keys %$sdims){
193 my @rparts;
194 foreach my $region(sort keys %{ $sdims->{$code} }){
195 my @dx;
196 foreach my $dim(qw(browsers os devices resolutions viewports languages timezones)){
197 my $rh = $sdims->{$code}{$region}{$dim} || {};
198 my @vp;
199 foreach my $v(sort { $rh->{$b} <=> $rh->{$a} } keys %$rh){
200 my $vj = $v; $vj =~ s/\\/\\\\/g; $vj =~ s/"/\\"/g; $vj =~ s/[\x00-\x1f]//g;
201 push @vp, sprintf('"%s":%d', $vj, $rh->{$v}+0);
202 }
203 push @dx, sprintf('"%s":{%s}', $dim, join(',', @vp));
204 }
205 my $rj = $region; $rj =~ s/\\/\\\\/g; $rj =~ s/"/\\"/g; $rj =~ s/[\x00-\x1f]//g;
206 push @rparts, sprintf('"%s":{%s}', $rj, join(',', @dx));
207 }
208 push @sdparts, sprintf('"%s":{%s}', $code, join(',', @rparts));
209 }
210 return '{' . join(',', @sdparts) . '}';
211}
212
213sub build_accordion_html{
214 #Country > state > city accordion under the globe on Overview
215 my($dbh) = @_;
216 my $tree = $stats->full_drill_tree($db, $dbh, $database_name);
217 my $has_countries = ($tree && scalar @{ $tree->{countries} } > 0) ? 1 : 0;
218
219 my $acc_html = q~
220<style>
221.accord-scope { position: relative; --acc-brand: var(--col-brand, var(--col-brand-2, var(--col-accent, #5aa9ff))); }
222.accord-collapse-all { background: var(--col-bg-2, #0e1822); color: var(--col-text-muted); border: 1px solid var(--col-border); padding: 5px 11px; border-radius: 7px; font-size: 11px; font-weight: 600; cursor: pointer; transition: all .12s ease; font-family: inherit; margin-left: auto; }
223.accord-collapse-all:hover { color: var(--col-text); border-color: var(--acc-brand); }
224.accord-tbl { width:100%; border-collapse: collapse; margin-bottom: 8px; }
225.accord-tbl th { background: rgba(255,255,255,.03); padding: 10px 14px; text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 1.4px; color: var(--col-text-muted); font-weight: 700; border-bottom: 1px solid var(--col-border); }
226.accord-tbl td { padding: 11px 14px; border-bottom: 1px solid var(--col-border); font-size: 13px; vertical-align: middle; }
227.accord-tbl tr.expandable { cursor: pointer; transition: background .12s ease; }
228.accord-tbl tr.expandable:hover { background: color-mix(in srgb, var(--acc-brand) 6%, rgba(255,255,255,.02)); }
229.accord-tbl tr.expandable.is-open { background: color-mix(in srgb, var(--acc-brand) 10%, transparent); box-shadow: inset 3px 0 0 0 var(--acc-brand); }
230.accord-tbl .chev { display:inline-block; width:14px; color: var(--col-text-muted); transition: transform .15s ease, color .15s ease; margin-right:6px; }
231.accord-tbl tr.expandable.is-open .chev { transform: rotate(90deg); color: var(--acc-brand); }
232.accord-tbl tr.kids > td { padding: 0 !important; background: rgba(0,0,0,.18); border-bottom: 1px solid var(--col-border); }
233.accord-tbl tr.kids[hidden] { display: none; }
234.accord-tbl .nested { width:100%; border-collapse: collapse; }
235.accord-tbl .nested td { padding: 9px 14px 9px 36px; font-size: 12.5px; border-bottom: 1px solid rgba(255,255,255,.05); }
236.accord-tbl .nested .nested td { padding-left: 58px; font-size: 12px; }
237.accord-tbl .num { text-align:right; font-variant-numeric: tabular-nums; }
238.accord-tbl .leaf td { padding-left: 80px; color: var(--col-text-2); }
239.accord-tbl tr.accord-empty td { text-align:center; color: var(--col-text-muted); padding: 26px 14px; font-size: 12.5px; font-style: italic; }
240</style>
241<div class="accord-scope">
242<div style="display:flex;align-items:center;margin-bottom:8px;gap:8px"><span style="font-size:11px;color:var(--col-text-dim)">click any row with the chevron to expand</span><button type="button" class="accord-collapse-all">Collapse all</button></div>
243<table class="accord-tbl">
244 <thead><tr><th>Country / state / city</th><th class="num">Visitors</th><th class="num">Sessions</th></tr></thead>
245 <tbody>
246~;
247
248 if($has_countries){
249 my $ci = 0;
250 foreach my $cc(@{ $tree->{countries} }){
251 $ci++;
252 my $cid = "atcnt-$ci";
253 my $cname = &h($cc->{name});
254 my $has_states = scalar @{ $cc->{states} } > 0 ? 1 : 0;
255 my $chev = $has_states ? '<span class="chev">&rsaquo;</span>' : '<span class="chev" style="opacity:0">&rsaquo;</span>';
256 $acc_html .= qq~<tr class="expandable" data-target="$cid"><td><strong>$chev$cname</strong></td><td class="num">$cc->{visitors}</td><td class="num">$cc->{sessions}</td></tr>~;
257 if($has_states){
258 $acc_html .= qq~<tr class="kids" id="$cid" hidden><td colspan="3"><table class="nested"><tbody>~;
259 my $si = 0;
260 foreach my $st(@{ $cc->{states} }){
261 $si++;
262 my $sid = "${cid}-st-$si";
263 my $rname = &h($st->{region});
264 my $has_cities = scalar @{ $st->{cities} } > 0 ? 1 : 0;
265 my $schev = $has_cities ? '<span class="chev">&rsaquo;</span>' : '<span class="chev" style="opacity:0">&rsaquo;</span>';
266 $acc_html .= qq~<tr class="expandable" data-target="$sid"><td>$schev$rname</td><td class="num">$st->{visitors}</td><td class="num">$st->{sessions}</td></tr>~;
267 if($has_cities){
268 $acc_html .= qq~<tr class="kids" id="$sid" hidden><td colspan="3"><table class="nested"><tbody>~;
269 foreach my $ct(@{ $st->{cities} }){
270 my $cn = &h($ct->{city});
271 $acc_html .= qq~<tr class="leaf"><td>$cn</td><td class="num">$ct->{visitors}</td><td class="num">$ct->{sessions}</td></tr>~;
272 }
273 $acc_html .= '</tbody></table></td></tr>';
274 }
275 }
276 $acc_html .= '</tbody></table></td></tr>';
277 }
278 }
279 }else{
280 $acc_html .= q~<tr class="accord-empty"><td colspan="3">No geo data yet &mdash; this table fills in as visitors are recorded.</td></tr>~;
281 }
282 $acc_html .= q~ </tbody>
283</table>
284</div>
285<script>
286(function(){
287 document.addEventListener('click', function(e){
288 var btn = e.target.closest('.accord-collapse-all');
289 if (btn) {
290 var scope = btn.closest('.accord-scope') || document;
291 scope.querySelectorAll('tr.kids').forEach(function(k){ k.hidden = true; });
292 scope.querySelectorAll('tr.expandable.is-open').forEach(function(t){ t.classList.remove('is-open'); });
293 return;
294 }
295 var tr = e.target.closest('tr.expandable');
296 if (!tr) return;
297 var id = tr.dataset.target;
298 if (!id) return;
299 var kids = document.getElementById(id);
300 if (!kids) return;
301 var open = !kids.hidden;
302 kids.hidden = open;
303 tr.classList.toggle('is-open', !open);
304 });
305})();
306</script>
307~;
308 return $acc_html;
309}
310
311sub populate_overview_tab{
312 my($dbh, $tvars, $days, $is_absolute) = @_;
313 if(!$is_absolute){
314 my $series = $stats->daily_series($db, $dbh, $database_name, $days);
315 $tvars->{series_labels_json} = '["' . join('","', @{$series->{labels}}) . '"]';
316 $tvars->{series_pv_json} = '[' . join(',', @{$series->{pageviews}}) . ']';
317 $tvars->{series_vs_json} = '[' . join(',', @{$series->{visitors}}) . ']';
318 $tvars->{has_series} = scalar(@{$series->{labels}}) ? 1 : 0;
319 }else{
320 $tvars->{has_series} = 0;
321 }
322
323 my @src = $stats->by_source($db, $dbh, $database_name, 8);
324 my @br = $stats->by_browser($db, $dbh, $database_name, 6);
325 my @top = $stats->by_page($db, $dbh, $database_name, 10);
326
327 $tvars->{src_rows} = &rows_with_pct(\@src, 'source_kind', 'sessions');
328 $tvars->{has_src_rows} = scalar(@src) ? 1 : 0;
329 $tvars->{br_rows} = &rows_with_pct(\@br, 'browser', 'sessions');
330 $tvars->{has_br_rows} = scalar(@br) ? 1 : 0;
331
332 my @top_rows;
333 foreach my $p(@top){
334 push @top_rows, { path => &h($p->{page_path} || '/'), pageviews => $p->{pageviews} || 0, sessions => $p->{sessions} || 0 };
335 }
336 $tvars->{top_pages} = \@top_rows;
337 $tvars->{has_top_pages} = scalar(@top_rows) ? 1 : 0;
338
339 my $acc_html = &build_accordion_html($dbh);
340 $tvars->{accordion_html} = $acc_html;
341 $tvars->{has_accordion} = 1;
342}
343
344sub populate_sources_tab{
345 my($dbh, $tvars) = @_;
346 my @src = $stats->by_source($db, $dbh, $database_name, 12);
347 my @refs = $stats->by_referrer_host($db, $dbh, $database_name, 25);
348 $tvars->{src_rows} = &rows_with_pct(\@src, 'source_kind', 'sessions');
349 $tvars->{has_src_rows} = scalar(@src) ? 1 : 0;
350 $tvars->{ref_rows} = &rows_with_pct(\@refs, 'host', 'sessions');
351 $tvars->{has_ref_rows} = scalar(@refs) ? 1 : 0;
352 $tvars->{utm_rows} = [];
353 $tvars->{has_utm_rows} = 0;
354}
355
356sub populate_devices_tab{
357 my($dbh, $tvars) = @_;
358 my @dev = $stats->by_device ($db, $dbh, $database_name);
359 my @br = $stats->by_browser ($db, $dbh, $database_name, 12);
360 my @os = $stats->by_os ($db, $dbh, $database_name, 12);
361 my @res = $stats->by_resolution($db, $dbh, $database_name, 12);
362 $tvars->{dev_rows} = &rows_with_pct(\@dev, 'device_type', 'sessions');
363 $tvars->{has_dev_rows} = scalar(@dev) ? 1 : 0;
364 $tvars->{br_rows} = &rows_with_pct(\@br, 'browser', 'sessions');
365 $tvars->{has_br_rows} = scalar(@br) ? 1 : 0;
366 $tvars->{os_rows} = &rows_with_pct(\@os, 'os', 'sessions');
367 $tvars->{has_os_rows} = scalar(@os) ? 1 : 0;
368 $tvars->{res_rows} = &rows_with_pct(\@res, 'resolution', 'sessions');
369 $tvars->{has_res_rows} = scalar(@res) ? 1 : 0;
370}
371
372sub populate_geo_tab{
373 my($dbh, $tvars) = @_;
374 my @c = $stats->by_country_enriched($db, $dbh, $database_name, 30);
375 my @rows;
376 my $total = 0;
377 $total += ($_->{visitors} || 0) for @c;
378 foreach my $r(@c){
379 my $n = $r->{visitors} || 0;
380 my $sec = int($r->{avg_duration_seconds} || 0);
381 my $dur = $sec >= 60 ? sprintf('%dm %02ds', int($sec/60), $sec % 60) : "${sec}s";
382 push @rows, {
383 country => &h($r->{country} || '(unknown)'),
384 visitors => $n,
385 sessions => $r->{sessions} || 0,
386 pct => $total ? sprintf('%.1f', ($n / $total) * 100) : '0.0',
387 avg_duration => $dur,
388 bounce_rate => sprintf('%.1f%%', 100 * ($r->{bounce_rate} || 0)),
389 pv_per_session => sprintf('%.2f', ($r->{pv_per_session} || 0)),
390 };
391 }
392 $tvars->{country_rows} = \@rows;
393 $tvars->{has_country_rows} = scalar(@rows) ? 1 : 0;
394
395 my @tz = $stats->by_timezone($db, $dbh, $database_name, 15);
396 $tvars->{tz_rows} = &rows_with_pct(\@tz, 'timezone', 'sessions');
397 $tvars->{has_tz_rows} = scalar(@tz) ? 1 : 0;
398
399 my $drill = $form->{country} || '';
400 $drill =~ s/[^A-Za-z]//g;
401 $drill = uc(substr($drill, 0, 2));
402 $tvars->{has_country_drill} = 0;
403 if(length($drill) == 2){
404 my $d = $stats->country_drill($db, $dbh, $database_name, $drill);
405 if($d && $d->{name}){
406 $tvars->{has_country_drill} = 1;
407 $tvars->{drill_country_name} = &h($d->{name});
408 $tvars->{drill_total_visitors} = $d->{visitors};
409 $tvars->{drill_total_sessions} = $d->{sessions};
410 my @dr;
411 foreach my $r(@{ $d->{regions} || [] }){
412 push @dr, { region => &h($r->{region} || '(unknown)'), visitors => $r->{visitors} || 0, sessions => $r->{sessions} || 0 };
413 }
414 $tvars->{drill_regions} = \@dr;
415 $tvars->{has_drill_regions} = scalar(@dr) ? 1 : 0;
416 my @dc;
417 foreach my $c2(@{ $d->{cities} || [] }){
418 push @dc, { city => &h($c2->{city} || '(unknown)'), region => &h($c2->{region} || ''), visitors => $c2->{visitors} || 0, sessions => $c2->{sessions} || 0 };
419 }
420 $tvars->{drill_cities} = \@dc;
421 $tvars->{has_drill_cities} = scalar(@dc) ? 1 : 0;
422 }
423 }
424}
425
426sub populate_pages_tab{
427 my($dbh, $tvars) = @_;
428 my @pages = $stats->by_page($db, $dbh, $database_name, 50);
429 my @landing = $stats->by_landing_page($db, $dbh, $database_name, 25);
430 my @top_rows;
431 foreach my $p(@pages){
432 push @top_rows, { path => &h($p->{page_path} || '/'), pageviews => $p->{pageviews} || 0, sessions => $p->{sessions} || 0 };
433 }
434 $tvars->{top_pages} = \@top_rows;
435 $tvars->{has_top_pages} = scalar(@top_rows) ? 1 : 0;
436 my @land_rows;
437 foreach my $l(@landing){
438 push @land_rows, { path => &h($l->{entry_page} || '/'), sessions => $l->{sessions} || 0, bounce => sprintf('%.1f', 100 * ($l->{bounce_rate} || 0)) };
439 }
440 $tvars->{landing_rows} = \@land_rows;
441 $tvars->{has_landing_rows} = scalar(@land_rows) ? 1 : 0;
442}
443
444sub admin_traffic{
445 my $tab = lc($form->{tab} || 'overview');
446 $tab =~ s/[^a-z]//g;
447 $tab = 'overview' unless $tab =~ /^(overview|sources|devices|geo|pages)$/;
448
449 my $days = $form->{days};
450 $days =~ s/[^0-9]//g if defined $days;
451 $days = 30 unless $days && $days =~ /^\d+$/;
452 $days = 730 if $days > 730;
453 $days = 1 if $days < 1;
454
455 my $abs_from = &parse_dt($form->{from});
456 my $abs_to = &parse_dt($form->{to});
457 my $range;
458 my $is_absolute = 0;
459 if($abs_from && $abs_to){$range = { from => $abs_from, to => $abs_to }; $is_absolute = 1;}
460 else {$range = $days;}
461 $stats->set_range($range);
462
463 my $dbh = $db->db_connect();
464
465 my $schema_ready = 0;
466 {
467 my $r = $db->db_readwrite($dbh, qq~SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema='$database_name' AND table_name IN ('tracking_sessions','tracking_events')~, $ENV{SCRIPT_NAME}, __LINE__);
468 $schema_ready = ($r && $r->{n} && $r->{n} >= 2) ? 1 : 0;
469 }
470
471 my $kpis = $schema_ready ? $stats->kpis($db, $dbh, $database_name) : {};
472
473 #Create all template variables
474 my $tvars;
475 $tvars->{schema_ready} = $schema_ready ? 1 : 0;
476 $tvars->{schema_missing} = $schema_ready ? 0 : 1;
477 $tvars->{tab_overview} = ($tab eq 'overview') ? 1 : 0;
478 $tvars->{tab_sources} = ($tab eq 'sources') ? 1 : 0;
479 $tvars->{tab_devices} = ($tab eq 'devices') ? 1 : 0;
480 $tvars->{tab_geo} = ($tab eq 'geo') ? 1 : 0;
481 $tvars->{tab_pages} = ($tab eq 'pages') ? 1 : 0;
482 $tvars->{days} = $days;
483 $tvars->{is_absolute} = $is_absolute;
484 $tvars->{is_rolling} = $is_absolute ? 0 : 1;
485 $tvars->{range_from} = $is_absolute ? &h($abs_from) : '';
486 $tvars->{range_to} = $is_absolute ? &h($abs_to) : '';
487 $tvars->{range_from_input} = $is_absolute ? do { (my $x = $abs_from) =~ s/ /T/; substr($x, 0, 16) } : '';
488 $tvars->{range_to_input} = $is_absolute ? do { (my $x = $abs_to) =~ s/ /T/; substr($x, 0, 16) } : '';
489 $tvars->{range_label} = $is_absolute ? "$abs_from -- $abs_to" : "Last $days days";
490 $tvars->{preset_24h} = (!$is_absolute && $days == 1) ? 1 : 0;
491 $tvars->{preset_7d} = (!$is_absolute && $days == 7) ? 1 : 0;
492 $tvars->{preset_30d} = (!$is_absolute && $days == 30) ? 1 : 0;
493 $tvars->{preset_90d} = (!$is_absolute && $days == 90) ? 1 : 0;
494 $tvars->{preset_1y} = (!$is_absolute && $days == 365) ? 1 : 0;
495 $tvars->{preset_custom} = $is_absolute ? 1 : 0;
496 $tvars->{kpi_pageviews} = $kpis->{pageviews} || 0;
497 $tvars->{kpi_visitors} = $kpis->{visitors} || 0;
498 $tvars->{kpi_sessions} = $kpis->{sessions} || 0;
499 $tvars->{kpi_avg_seconds} = $kpis->{avg_seconds} || 0;
500 $tvars->{kpi_bounce_rate} = $kpis->{bounce_rate} || 0;
501 $tvars->{kpi_pv_per_session} = $kpis->{pv_per_session} || 0;
502
503 if($schema_ready && ($tab eq 'overview' || $tab eq 'geo')){
504 my($globe_json, $country_count) = &build_globe_json($dbh);
505 $tvars->{globe_json} = $globe_json;
506 #Always render the globe even with zero countries - the empty wireframe
507 #tells users what to expect when visitors start flowing.
508 $tvars->{has_globe} = 1;
509 $tvars->{state_json} = &build_state_json($dbh);
510 $tvars->{tree_json} = &build_tree_json($dbh);
511 my($dims_json, $dims) = &build_dims_json($dbh);
512 $tvars->{dims_json} = $dims_json;
513 $tvars->{overall_dims_json} = &build_overall_dims_json($dims);
514 $tvars->{sdims_json} = &build_sdims_json($dbh);
515 $tvars->{kpi_countries} = $country_count;
516 }
517
518 if($schema_ready && $tab eq 'overview'){
519 &populate_overview_tab($dbh, $tvars, $days, $is_absolute);
520 }elsif($schema_ready && $tab eq 'sources'){
521 &populate_sources_tab($dbh, $tvars);
522 }elsif($schema_ready && $tab eq 'devices'){
523 &populate_devices_tab($dbh, $tvars);
524 }elsif($schema_ready && $tab eq 'geo'){
525 &populate_geo_tab($dbh, $tvars);
526 }elsif($schema_ready && $tab eq 'pages'){
527 &populate_pages_tab($dbh, $tvars);
528 }
529
530 $db->db_disconnect($dbh);
531
532 print qq~Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n~;
533
534 my $body = join('', $tfile->template('pt_admin_traffic.html', $tvars, $userinfo));
535 $load->render({
536 userinfo => $userinfo,
537 page_key => 'admin_traffic',
538 title => 'Traffic Reports',
539 body => $body,
540 extra_js => [
541 '/assets/javascript/globe.js?v=1783314562',
542 '/assets/javascript/datepicker.js?v=3',
543 ],
544 });
545}