Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/admin_visitors.cgi
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
to 8edbf8b678b7
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 | #====================================================================== | |
| 8 | use strict; | |
| 9 | use warnings; | |
| 10 | ||
| 11 | use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com'; | |
| 12 | use CGI; | |
| 13 | use MODS::Template; | |
| 14 | use MODS::DBConnect; | |
| 15 | use MODS::Login; | |
| 16 | use MODS::ContactForge::Config; | |
| 17 | use MODS::ContactForge::Wrapper; | |
| 18 | use MODS::ContactForge::Stats; | |
| 19 | ||
| 20 | my $q = CGI->new; | |
| 21 | my $form = $q->Vars; | |
| 22 | my $auth = MODS::Login->new; | |
| 23 | my $wrap = MODS::ContactForge::Wrapper->new; | |
| 24 | my $tfile = MODS::Template->new; | |
| 25 | my $db = MODS::DBConnect->new; | |
| 26 | my $cfg = MODS::ContactForge::Config->new; | |
| 27 | my $stats = MODS::ContactForge::Stats->new; | |
| 28 | my $DB = $cfg->settings('database_name'); | |
| 29 | ||
| 30 | $|=1; | |
| 31 | my $userinfo = $auth->login_verify(); | |
| 32 | if (!$userinfo) { | |
| 33 | print "Status: 302 Found\nLocation: /login.cgi?return=/admin_visitors.cgi\n\n"; | |
| 34 | exit; | |
| 35 | } | |
| 36 | unless ($userinfo->{is_admin}) { | |
| 37 | print "Status: 403 Forbidden\nContent-Type: text/plain\n\nAdmin access required.\n"; | |
| 38 | exit; | |
| 39 | } | |
| 40 | ||
| 41 | sub _h { | |
| 42 | my $s = shift; $s = '' unless defined $s; | |
| 43 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; | |
| 44 | $s =~ s/"/"/g; $s =~ s/'/'/g; | |
| 45 | return $s; | |
| 46 | } | |
| 47 | ||
| 48 | my $dbh = $db->db_connect(); | |
| 49 | ||
| 50 | # Tables present? Soft-fail to a friendly banner if the migration | |
| 51 | # hasn't been run yet. | |
| 52 | my $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 ------------------------------------------------ | |
| 63 | my $kpi_today = $schema_ready ? $stats->kpis($db, $dbh, $DB, 1) : {}; | |
| 64 | my $kpi_30d = $schema_ready ? $stats->kpis($db, $dbh, $DB, 30) : {}; | |
| 65 | my $live_count = $schema_ready ? $stats->live_visitors($db, $dbh, $DB) : 0; | |
| 66 | ||
| 67 | # ---- Live sessions table -------------------------------------------- | |
| 68 | my @live = $schema_ready ? $stats->live_sessions($db, $dbh, $DB, 60) : (); | |
| 69 | my @live_rows; | |
| 70 | foreach 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 -------------------------------------- | |
| 94 | my @recent = $schema_ready ? $stats->recent_pageviews($db, $dbh, $DB, 40) : (); | |
| 95 | my @recent_rows; | |
| 96 | foreach 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) --------------------------------------- | |
| 115 | my @top_pages = $schema_ready ? $stats->by_page($db, $dbh, $DB, 30, 12) : (); | |
| 116 | my @top_page_rows; | |
| 117 | foreach 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 | ||
| 127 | my $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 | ||
| 153 | print "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 | ||
| 155 | my $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 | }); |