Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_dashboard.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_dashboard.html

added on local at 2026-07-01 16:01:48

Added
+197
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 2e29877d123b
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1[if:!$has_sites]
2<div class="card" style="text-align:center; padding: 60px 24px;">
3 <h2 style="font-family:'Space Grotesk',sans-serif; font-size:24px; margin:0 0 6px;">Welcome to ABForge.</h2>
4 <p class="muted" style="margin:0 0 22px;">Add your first website to start tracking visitors, running experiments and watching heatmaps light up.</p>
5 <a class="btn btn-brand btn-lg" href="/sites.cgi?new=1">+ Add my first site</a>
6</div>
7[/if]
8
9[if:$has_sites]
10<div class="page-header">
11 <div>
12 <h1>$site_name</h1>
13 <p>$domain &middot; <span style="color: var(--good);">&#9679; $live live now</span></p>
14 </div>
15 <div class="flex gap-8">
16 <a class="btn btn-ghost btn-sm" href="/sites.cgi?id=$site_id">Site settings</a>
17 <a class="btn btn-secondary btn-sm" href="/realtime.cgi?site_id=$site_id">Real-time &rarr;</a>
18 </div>
19</div>
20
21<div class="kpi-grid">
22 <div class="kpi" data-kpi-detail='{"description":"Total pageviews recorded for <strong>$site_name</strong> over the last 30 days. Every page load (including reloads and navigations within the same session) counts as one pageview.","methodology":"Each time <code>/t.js</code> fires on a page in your tracking domain, it sends a beacon to <code>/c.cgi</code>. The pageviews counter is <code>COUNT(*)</code> over the <code>pageviews</code> table for this site, filtered to the trailing 30 days.","interpretation":"Pageviews answer <em>how much traffic is the site getting</em>. To understand <em>quality</em> of that traffic, pair this with Bounce Rate (are people engaging?) and Pages/Session (are they exploring?). A pageview-heavy + low-bounce site is healthy; pageview-heavy + high-bounce often means a single landing page is doing all the work.","insights":[{"label":"Right now","value":"$pv pageviews from $visitors unique visitors across $sessions sessions.","tone":"info"},{"label":"Per visitor","value":"On average each visitor saw multiple pages — see Pages/Session for the ratio.","tone":"info"}],"drilldowns":[{"label":"Top pages","href":"/pages.cgi?site_id=$site_id"},{"label":"Traffic sources","href":"/sources.cgi?site_id=$site_id"},{"label":"Real-time","href":"/realtime.cgi?site_id=$site_id"}],"note":"Counts every load — reloads and back-button navigations included."}'>
23 <div class="kpi-label">Pageviews / 30d</div><div class="kpi-value">$pv</div>
24 </div>
25
26 <div class="kpi" data-kpi-detail='{"description":"Distinct people who visited <strong>$site_name</strong> in the last 30 days, identified by an anonymous <code>client_id</code> cookie set by <code>/t.js</code>. One visitor can have many sessions over the period; they’re only counted once here.","methodology":"<code>COUNT(DISTINCT client_id)</code> over the <code>pageviews</code> table for this site, filtered to the trailing 30 days. The client_id is a UUID stored in a 1st-party cookie with a 2-year TTL — if a visitor clears cookies or switches browsers, they’ll be counted as new.","interpretation":"This is the closest thing to <em>reach</em> we can measure without identifying users by email. Compare against your acquisition spend to estimate cost-per-visitor; compare against signups (in the funnel report) for visitor-to-customer conversion.","insights":[{"label":"Right now","value":"$visitors unique visitors generated $sessions sessions — each visitor averages about $pv_per pages per visit.","tone":"info"},{"label":"Cookie-based","value":"Visitors who block cookies or use private browsing get re-counted on each visit. The true number is typically 5–15% lower.","tone":"warn"}],"drilldowns":[{"label":"Geography","href":"/geo.cgi?site_id=$site_id"},{"label":"Devices","href":"/devices.cgi?site_id=$site_id"},{"label":"Sources","href":"/sources.cgi?site_id=$site_id"}],"note":"Cookie-based — cookie clears or browser changes count as new."}'>
27 <div class="kpi-label">Unique visitors</div><div class="kpi-value">$visitors</div>
28 </div>
29
30 <div class="kpi" data-kpi-detail='{"description":"A <em>session</em> is one continuous browsing visit. A new session starts when the same visitor returns after <strong>30 minutes of inactivity</strong>, or when they come back the next day. One visitor with two coffee-break gaps in their browsing = 3 sessions.","methodology":"On every <code>/c.cgi</code> beacon, the tracker checks <code>last_seen_at</code> for the client_id. If the gap is &gt; 30 minutes (or the date rolled over), a new <code>session_id</code> is minted. Sessions counter = <code>COUNT(DISTINCT session_id)</code> for the site, trailing 30 days.","interpretation":"Sessions are the unit most analytics tools agree on. Use them as the denominator for conversion rate (signups / sessions), bounce rate, and engagement metrics. If sessions are flat but pageviews are up, your existing visitors are exploring more — a good sign.","insights":[{"label":"Sessions per visitor","value":"$visitors visitors produced $sessions sessions — average visitor came back multiple times in the window.","tone":"good"},{"label":"30-min rule","value":"The 30-minute inactivity window is the industry standard. A user who reads one page for an hour then clicks a link starts a new session.","tone":"info"}],"drilldowns":[{"label":"Real-time","href":"/realtime.cgi?site_id=$site_id"},{"label":"Funnels","href":"/funnels.cgi?site_id=$site_id"}],"note":"30-minute inactivity window opens a new session."}'>
31 <div class="kpi-label">Sessions</div><div class="kpi-value">$sessions</div>
32 </div>
33
34 <div class="kpi" data-kpi-detail='{"description":"Percentage of sessions in which the visitor saw exactly <strong>one page</strong> and left without further interaction. <strong>Lower is generally better</strong>, but context matters: blog posts and landing pages naturally have higher bounce because the visitor got what they came for on page one.","methodology":"<code>(sessions with exactly 1 pageview) / (total sessions) × 100</code>, computed over the trailing 30 days for this site.","interpretation":"For a SaaS dashboard or app, bounce should be &lt; 40% — visitors come to <em>do</em> something across multiple pages. For a blog or article-driven site, 60–80% is normal. If your bounce spiked recently, check Top Pages for a high-traffic page that’s collecting bounces.","insights":[{"label":"Current","value":"$bounce% of your $sessions sessions ended on the first page.","tone":"info"},{"label":"Benchmark","value":"Industry averages: SaaS 30–50% · E-commerce 20–40% · Blogs 60–80% · Landing pages 70–90%.","tone":"info"},{"label":"Tip","value":"Bounce is per-session, not per-visitor. A visitor who bounces once and converts later still counts as one bounce.","tone":"info"}],"drilldowns":[{"label":"Top pages","href":"/pages.cgi?site_id=$site_id"},{"label":"Sources","href":"/sources.cgi?site_id=$site_id"}],"note":"Single-pageview sessions only. Lower is usually better."}'>
35 <div class="kpi-label">Bounce rate</div><div class="kpi-value">$bounce%</div>
36 </div>
37
38 <div class="kpi" data-kpi-detail='{"description":"Average length of a session, measured as the gap between the <strong>first</strong> and <strong>last</strong> event the visitor sent during that session. Each session contributes its own duration; the value shown is the mean across all sessions.","methodology":"For each session, time-on-site = <code>MAX(event_time) - MIN(event_time)</code>. The dashboard averages this across all sessions in the 30-day window. <strong>Single-pageview sessions have no measurable duration</strong> (only one event = no gap), so they’re counted as 0 seconds — which is why a high bounce rate drags this number down hard.","interpretation":"A value of <strong>0s</strong> almost always means most of your sessions are single-pageview bounces. To get a real Avg Time, you need multi-page sessions or instrumented events (scroll depth, button clicks) within a page. Once you have those, anything above 30s for a content site or 2 minutes for an app is healthy.","insights":[{"label":"Right now","value":"Current value: $avg_time. With $sessions sessions and a $bounce% bounce rate, single-pv sessions are pulling this average toward 0.","tone":"warn"},{"label":"Fix","value":"Add explicit events (button clicks, scroll-depth markers) on key pages — they give sessions multiple events even when the user stays on one URL.","tone":"info"}],"drilldowns":[{"label":"Goals","href":"/goals.cgi?site_id=$site_id"},{"label":"Funnels","href":"/funnels.cgi?site_id=$site_id"}],"note":"Single-pageview sessions count as 0s — high bounce drags this down."}'>
39 <div class="kpi-label">Avg time on site</div><div class="kpi-value">$avg_time</div>
40 </div>
41
42 <div class="kpi" data-kpi-detail='{"description":"Average pageviews per session. Tells you <strong>how deep into the site</strong> a typical visitor browses before leaving. 1.0 = everyone visits exactly one page; 3.0 = the average visitor explores 3 pages.","methodology":"<code>total pageviews / total sessions</code> over the trailing 30 days. Bounce sessions (1 pv) pull this number toward 1.0; deep-exploration sessions pull it up.","interpretation":"This is the inverse signal of Bounce Rate — they’ll usually move together (high bounce → low pages/session). For content sites, target 2.0+; for app dashboards, 4–10+ is normal because the user navigates between many internal views.","insights":[{"label":"Current","value":"$pv_per pages per session over $sessions sessions.","tone":"info"},{"label":"Benchmark","value":"Content sites: aim for 2.0+. SaaS apps: 4–10. Single-purpose landing pages: 1.0–1.3 is expected.","tone":"info"},{"label":"Move it up","value":"Add clear next-action CTAs on each page. Use internal links in body copy. Surface related content at the bottom of articles.","tone":"good"}],"drilldowns":[{"label":"Top pages","href":"/pages.cgi?site_id=$site_id"},{"label":"Funnels","href":"/funnels.cgi?site_id=$site_id"}],"note":"Inverse of bounce — these two metrics move together."}'>
43 <div class="kpi-label">Pages / session</div><div class="kpi-value">$pv_per</div>
44 </div>
45</div>
46
47<div class="card">
48 <div class="card-header">
49 <h3 class="card-title">Last 30 days &mdash; pageviews &amp; visitors</h3>
50 </div>
51 <div class="chart-wrap" style="height:240px;">
52 <svg id="dash-chart" class="chart-canvas" viewBox="0 0 1000 240" preserveAspectRatio="none"></svg>
53 </div>
54</div>
55
56<div class="row-2">
57 <div class="card">
58 <div class="card-header"><h3 class="card-title">Top pages</h3><a href="/pages.cgi?site_id=$site_id" class="text-sm muted">See all &rarr;</a></div>
59 <table class="table">
60 <thead><tr><th>Page</th><th class="right">Pageviews</th><th class="right">Avg time</th></tr></thead>
61 <tbody>
62 [loop:@pages]
63 <tr><td style="max-width:380px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">$page_path</td><td class="num">$pageviews</td><td class="num">$avg_time<span class="muted text-sm">s</span></td></tr>
64 [/loop]
65 </tbody>
66 </table>
67 </div>
68
69 <div class="card">
70 <div class="card-header"><h3 class="card-title">Traffic sources</h3><a href="/sources.cgi?site_id=$site_id" class="text-sm muted">See all &rarr;</a></div>
71 <table class="table">
72 <thead><tr><th>Source</th><th class="right">Sessions</th></tr></thead>
73 <tbody>
74 [loop:@sources]
75 <tr><td>$referrer_kind</td><td class="num">$sessions</td></tr>
76 [/loop]
77 </tbody>
78 </table>
79 </div>
80</div>
81
82<div class="row-2">
83 <div class="card">
84 <div class="card-header"><h3 class="card-title">Top countries</h3><a href="/geo.cgi?site_id=$site_id" class="text-sm muted">See map &rarr;</a></div>
85 <table class="table">
86 <thead><tr><th>Country</th><th class="right">Visitors</th><th class="right">Sessions</th></tr></thead>
87 <tbody>
88 [loop:@countries]
89 <tr><td>$country</td><td class="num">$visitors</td><td class="num">$sessions</td></tr>
90 [/loop]
91 </tbody>
92 </table>
93 </div>
94
95 <div class="card">
96 <div class="card-header"><h3 class="card-title">Devices</h3><a href="/devices.cgi?site_id=$site_id" class="text-sm muted">See all &rarr;</a></div>
97 <table class="table">
98 <thead><tr><th>Device</th><th class="right">Sessions</th><th class="right">Visitors</th></tr></thead>
99 <tbody>
100 [loop:@devices]
101 <tr><td>$device_type</td><td class="num">$sessions</td><td class="num">$visitors</td></tr>
102 [/loop]
103 </tbody>
104 </table>
105 </div>
106</div>
107
108<script>
109(function() {
110 var labels = $series_labels;
111 var pv = $series_pv;
112 var vs = $series_vs;
113 var svg = document.getElementById('dash-chart');
114 if (!svg || !pv.length) return;
115 var W = 1000, H = 240, P = 24;
116 var max = Math.max.apply(null, pv.concat(vs, [1]));
117 function pt(arr, i) {
118 var x = P + (i / (arr.length - 1 || 1)) * (W - 2*P);
119 var y = H - P - (arr[i] / max) * (H - 2*P);
120 return x + ',' + y;
121 }
122 function polyline(arr) {
123 return Array.from({length: arr.length}, function(_, i) { return pt(arr, i); }).join(' ');
124 }
125 function area(arr) {
126 var pts = polyline(arr).split(' ');
127 return pts.join(' ') + ' ' + (W-P) + ',' + (H-P) + ' ' + P + ',' + (H-P) + ' Z';
128 }
129 var gridY = '';
130 for (var i = 0; i < 5; i++) {
131 var y = P + i * (H - 2*P) / 4;
132 gridY += '<line x1="'+P+'" y1="'+y+'" x2="'+(W-P)+'" y2="'+y+'" stroke="#20273b" stroke-width="1"/>';
133 }
134 var html = gridY +
135 '<defs>' +
136 '<linearGradient id="gp" x1="0" x2="0" y1="0" y2="1">' +
137 '<stop offset="0%" stop-color="#ff7a3d" stop-opacity="0.45"/>' +
138 '<stop offset="100%" stop-color="#ff7a3d" stop-opacity="0"/></linearGradient>' +
139 '<linearGradient id="gv" x1="0" x2="0" y1="0" y2="1">' +
140 '<stop offset="0%" stop-color="#5aa9ff" stop-opacity="0.32"/>' +
141 '<stop offset="100%" stop-color="#5aa9ff" stop-opacity="0"/></linearGradient>' +
142 '</defs>' +
143 '<path d="M ' + area(pv) + '" fill="url(#gp)" stroke="none"/>' +
144 '<polyline points="' + polyline(pv) + '" fill="none" stroke="#ff7a3d" stroke-width="2"/>' +
145 '<path d="M ' + area(vs) + '" fill="url(#gv)" stroke="none"/>' +
146 '<polyline points="' + polyline(vs) + '" fill="none" stroke="#5aa9ff" stroke-width="2"/>' +
147 '<line id="dash-cross" x1="0" y1="'+P+'" x2="0" y2="'+(H-P)+'" stroke="rgba(255,255,255,0.35)" stroke-width="1" stroke-dasharray="3 3" style="display:none"/>' +
148 '<circle id="dash-dot-pv" r="4" fill="#ff7a3d" stroke="#fff" stroke-width="1.5" style="display:none"/>' +
149 '<circle id="dash-dot-vs" r="4" fill="#5aa9ff" stroke="#fff" stroke-width="1.5" style="display:none"/>';
150 svg.innerHTML = html;
151
152 // Hover crosshair + tooltip wired through window.ABForgeGraphs (loaded
153 // by the shared wrapper). Falls back gracefully if graphs.js is absent.
154 var cross = document.getElementById('dash-cross');
155 var dotPv = document.getElementById('dash-dot-pv');
156 var dotVs = document.getElementById('dash-dot-vs');
157 function fmtDate(s) {
158 var m = String(s||'').match(/^(\d{4})-(\d{2})-(\d{2})$/);
159 if (!m) return s || '';
160 var mn = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
161 return mn[parseInt(m[2],10)-1] + ' ' + parseInt(m[3],10) + ', ' + m[1];
162 }
163 svg.addEventListener('mousemove', function(e) {
164 var rect = svg.getBoundingClientRect();
165 var xpct = (e.clientX - rect.left) / rect.width;
166 var i = Math.round(xpct * (pv.length - 1));
167 if (i < 0) i = 0; if (i > pv.length - 1) i = pv.length - 1;
168 var xc = P + (i / (pv.length - 1 || 1)) * (W - 2*P);
169 var yp = H - P - (pv[i] / max) * (H - 2*P);
170 var yv = H - P - (vs[i] / max) * (H - 2*P);
171 cross.setAttribute('x1', xc); cross.setAttribute('x2', xc); cross.style.display = '';
172 dotPv.setAttribute('cx', xc); dotPv.setAttribute('cy', yp); dotPv.style.display = '';
173 dotVs.setAttribute('cx', xc); dotVs.setAttribute('cy', yv); dotVs.style.display = '';
174 if (window.ABForgeGraphs && window.ABForgeGraphs.showTip) {
175 var pct = pv[i] && pv.reduce(function(s,v){return s+v;},0) ? (pv[i] / pv.reduce(function(s,v){return s+v;},0) * 100) : 0;
176 window.ABForgeGraphs.showTip(
177 '<div class="wg-tip-eyebrow">Last 30 days &middot; ' + fmtDate(labels[i]) + '</div>' +
178 '<div class="wg-tip-main">' + pv[i].toLocaleString() + '</div>' +
179 '<div class="wg-tip-sub">' + pct.toFixed(1) + '% of period pageviews</div>' +
180 '<div class="wg-tip-divider"></div>' +
181 '<div class="wg-tip-row"><span style="display:inline-flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:2px;background:#ff7a3d"></span>Pageviews</span><span class="wg-tip-val">' + pv[i].toLocaleString() + '</span></div>' +
182 '<div class="wg-tip-row"><span style="display:inline-flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:2px;background:#5aa9ff"></span>Visitors</span><span class="wg-tip-val">' + vs[i].toLocaleString() + '</span></div>' +
183 '<div class="wg-tip-row"><span>Pages / visitor</span><span class="wg-tip-val">' + (vs[i] ? (pv[i]/vs[i]).toFixed(2) : '0') + '</span></div>' +
184 '<div class="wg-tip-row"><span>Position</span><span class="wg-tip-val">Day ' + (i+1) + ' of ' + pv.length + '</span></div>'
185 );
186 }
187 });
188 svg.addEventListener('mouseleave', function() {
189 cross.style.display = 'none';
190 dotPv.style.display = 'none';
191 dotVs.style.display = 'none';
192 if (window.ABForgeGraphs && window.ABForgeGraphs.hideTip) window.ABForgeGraphs.hideTip();
193 });
194 svg.style.cursor = 'crosshair';
195})();
196</script>
197[/if]