Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/admin_visitors.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/admin_visitors.cgi

added on local at 2026-07-11 18:31:49

Added
+162
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 8edbf8b678b7
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# ContactForge -- Admin Live Visitors
4#
5# Admin-only. KPI tiles + live visitor table + recent pageviews
6# timeline. Single-tenant: every row is traffic to crm.3dshawn.com.
7#======================================================================
8use strict;
9use warnings;
10
11use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com';
12use CGI;
13use MODS::Template;
14use MODS::DBConnect;
15use MODS::Login;
16use MODS::ContactForge::Config;
17use MODS::ContactForge::Wrapper;
18use MODS::ContactForge::Stats;
19
20my $q = CGI->new;
21my $form = $q->Vars;
22my $auth = MODS::Login->new;
23my $wrap = MODS::ContactForge::Wrapper->new;
24my $tfile = MODS::Template->new;
25my $db = MODS::DBConnect->new;
26my $cfg = MODS::ContactForge::Config->new;
27my $stats = MODS::ContactForge::Stats->new;
28my $DB = $cfg->settings('database_name');
29
30$|=1;
31my $userinfo = $auth->login_verify();
32if (!$userinfo) {
33 print "Status: 302 Found\nLocation: /login.cgi?return=/admin_visitors.cgi\n\n";
34 exit;
35}
36unless ($userinfo->{is_admin}) {
37 print "Status: 403 Forbidden\nContent-Type: text/plain\n\nAdmin access required.\n";
38 exit;
39}
40
41sub _h {
42 my $s = shift; $s = '' unless defined $s;
43 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
44 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
45 return $s;
46}
47
48my $dbh = $db->db_connect();
49
50# Tables present? Soft-fail to a friendly banner if the migration
51# hasn't been run yet.
52my $schema_ready = 0;
53{
54 my $r = $db->db_readwrite($dbh, qq~
55 SELECT COUNT(*) AS n FROM information_schema.tables
56 WHERE table_schema='$DB'
57 AND table_name IN ('visitors','tracking_sessions','pageviews')
58 ~, $ENV{SCRIPT_NAME}, __LINE__);
59 $schema_ready = ($r && $r->{n} && $r->{n} >= 3) ? 1 : 0;
60}
61
62# ---- KPIs + KPI tiles ------------------------------------------------
63my $kpi_today = $schema_ready ? $stats->kpis($db, $dbh, $DB, 1) : {};
64my $kpi_30d = $schema_ready ? $stats->kpis($db, $dbh, $DB, 30) : {};
65my $live_count = $schema_ready ? $stats->live_visitors($db, $dbh, $DB) : 0;
66
67# ---- Live sessions table --------------------------------------------
68my @live = $schema_ready ? $stats->live_sessions($db, $dbh, $DB, 60) : ();
69my @live_rows;
70foreach my $v (@live) {
71 my $idle = $v->{idle_seconds} || 0;
72 my $idle_label = $idle < 10 ? 'just now'
73 : $idle < 60 ? $idle . 's ago'
74 : $idle < 3600 ? int($idle/60) . 'm ago'
75 : int($idle/3600) . 'h ago';
76 push @live_rows, {
77 session_id => _h($v->{session_id}),
78 country => _h($v->{country} || ''),
79 device_type => _h($v->{device_type} || 'unknown'),
80 browser_name => _h($v->{browser_name}|| 'Unknown'),
81 os_name => _h($v->{os_name} || 'Unknown'),
82 current_page => _h($v->{current_page}|| '/'),
83 entry_page => _h($v->{entry_page} || '/'),
84 ref_host => _h($v->{referrer_host}|| '(direct)'),
85 pageviews => $v->{pageviews} || 0,
86 duration => $v->{duration_seconds} || 0,
87 idle_label => $idle_label,
88 idle_epoch => (time() - $idle),
89 is_active => ($idle < 30) ? 1 : 0,
90 };
91}
92
93# ---- Recent pageviews timeline --------------------------------------
94my @recent = $schema_ready ? $stats->recent_pageviews($db, $dbh, $DB, 40) : ();
95my @recent_rows;
96foreach my $r (@recent) {
97 my $ago = $r->{ago_seconds} || 0;
98 my $when = $ago < 60 ? $ago . 's ago'
99 : $ago < 3600 ? int($ago/60) . 'm ago'
100 : $ago < 86400 ? int($ago/3600) . 'h ago'
101 : int($ago/86400) . 'd ago';
102 push @recent_rows, {
103 path => _h($r->{path} || '/'),
104 title => _h($r->{title} || ''),
105 country => _h($r->{country} || ''),
106 device_type => _h($r->{device_type} || ''),
107 browser => _h($r->{browser_name} || ''),
108 os => _h($r->{os_name} || ''),
109 when => $when,
110 when_epoch => (time() - $ago),
111 };
112}
113
114# ---- Top pages (last 30 days) ---------------------------------------
115my @top_pages = $schema_ready ? $stats->by_page($db, $dbh, $DB, 30, 12) : ();
116my @top_page_rows;
117foreach my $p (@top_pages) {
118 push @top_page_rows, {
119 path => _h($p->{page_path} || '/'),
120 pageviews => $p->{pageviews} || 0,
121 sessions => $p->{sessions} || 0,
122 };
123}
124
125$db->db_disconnect($dbh);
126
127my $tvars = {
128 schema_ready => $schema_ready ? 1 : 0,
129 schema_missing => $schema_ready ? 0 : 1,
130
131 kpi_live_now => $live_count,
132 kpi_pageviews_today => $kpi_today->{pageviews} || 0,
133 kpi_visitors_today => $kpi_today->{visitors} || 0,
134 kpi_sessions_today => $kpi_today->{sessions} || 0,
135 kpi_pageviews_30d => $kpi_30d->{pageviews} || 0,
136 kpi_visitors_30d => $kpi_30d->{visitors} || 0,
137 kpi_sessions_30d => $kpi_30d->{sessions} || 0,
138 kpi_avg_seconds_30d => $kpi_30d->{avg_seconds} || 0,
139 kpi_bounce_30d => $kpi_30d->{bounce_rate} || 0,
140 kpi_pv_per_session => $kpi_30d->{pv_per_session} || 0,
141
142 live_rows => \@live_rows,
143 has_live => scalar(@live_rows) ? 1 : 0,
144 live_count => scalar(@live_rows),
145
146 recent_rows => \@recent_rows,
147 has_recent => scalar(@recent_rows) ? 1 : 0,
148
149 top_pages => \@top_page_rows,
150 has_top_pages => scalar(@top_page_rows) ? 1 : 0,
151};
152
153print "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";
154
155my $body = join('', $tfile->template('cf_admin_visitors.html', $tvars, $userinfo));
156
157$wrap->render({
158 userinfo => $userinfo,
159 page_key => 'admin_visitors',
160 title => 'Live Visitors',
161 body => $body,
162});