Files
shopify-ai-backup/chat/public/admin-tracking.html

991 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin - Visitor Tracking</title>
<link rel="stylesheet" href="/styles.css" />
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: var(--text);
margin: 8px 0;
}
.stat-label {
color: var(--muted);
font-size: 14px;
}
.chart-container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.chart-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.bar-chart {
display: flex;
flex-direction: column;
gap: 12px;
}
.bar-item {
display: flex;
align-items: center;
gap: 12px;
}
.bar-label {
min-width: 200px;
font-size: 14px;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bar-wrapper {
flex: 1;
height: 24px;
background: var(--background);
border-radius: 4px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s ease;
}
.bar-count {
min-width: 60px;
text-align: right;
font-weight: 600;
color: var(--text);
}
.table-container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
margin-bottom: 24px;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: var(--background);
padding: 12px;
text-align: left;
font-weight: 600;
color: var(--muted);
border-bottom: 1px solid var(--border);
}
.data-table td {
padding: 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:hover {
background: var(--background);
}
.time-cell {
color: var(--muted);
font-size: 13px;
}
.path-cell {
font-family: monospace;
font-size: 13px;
}
.empty-state {
text-align: center;
padding: 48px 24px;
color: var(--muted);
}
.line-chart {
display: flex;
align-items: flex-end;
height: 200px;
gap: 4px;
padding: 20px 0;
}
.line-bar {
flex: 1;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
border-radius: 4px 4px 0 0;
min-width: 8px;
position: relative;
transition: all 0.3s ease;
}
.line-bar:hover {
opacity: 0.8;
}
.line-bar-label {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%) rotate(-45deg);
font-size: 11px;
color: var(--muted);
white-space: nowrap;
transform-origin: center;
}
.line-bar-value {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
font-weight: 600;
color: var(--text);
}
</style>
<!-- PostHog Analytics -->
<script src="/posthog.js"></script>
<script src="/admin.js"></script>
</head>
<body>
<div class="sidebar-overlay"></div>
<div class="app-shell">
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">A</div>
<div>
<div class="brand-title">Admin</div>
<div class="brand-sub">Site management</div>
</div>
<button id="close-sidebar" class="ghost" style="margin-left: auto; display: none;">&times;</button>
</div>
<div class="sidebar-section">
<div class="section-heading">Navigation</div>
<a class="ghost" href="/admin/build">Build models</a>
<a class="ghost" href="/admin/plan">Plan models</a>
<a class="ghost" href="/admin/plans">Plans</a>
<a class="ghost" href="/admin/accounts">Accounts</a>
<a class="ghost" href="/admin/affiliates">Affiliates</a>
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>
</aside>
<main class="main">
<div class="admin-shell">
<div class="topbar" style="margin-bottom: 24px;">
<button id="menu-toggle">
<span></span><span></span><span></span>
</button>
<div>
<div class="pill">Admin</div>
<div class="title" style="margin-top: 6px;">Visitor Tracking</div>
<div class="crumb">Analytics and visitor statistics for your application.</div>
</div>
<div class="admin-actions">
<button id="admin-refresh" class="ghost">Refresh</button>
<button id="admin-logout" class="primary">Logout</button>
</div>
</div>
<!-- Enhanced Analytics Overview -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">DAU (Daily Active)</div>
<div class="stat-value" id="dau">-</div>
</div>
<div class="stat-card">
<div class="stat-label">WAU (Weekly Active)</div>
<div class="stat-value" id="wau">-</div>
</div>
<div class="stat-card">
<div class="stat-label">MAU (Monthly Active)</div>
<div class="stat-value" id="mau">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Avg Session Duration</div>
<div class="stat-value" id="avg-session-duration">-</div>
</div>
</div>
<!-- Business Metrics Overview -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">MRR (Monthly Recurring)</div>
<div class="stat-value" id="mrr">$0</div>
</div>
<div class="stat-card">
<div class="stat-label">LTV (Lifetime Value)</div>
<div class="stat-value" id="ltv">$0</div>
</div>
<div class="stat-card">
<div class="stat-label">Churn Rate</div>
<div class="stat-value" id="churn-rate">0%</div>
</div>
<div class="stat-card">
<div class="stat-label">ARPU</div>
<div class="stat-value" id="arpu">$0</div>
</div>
</div>
<!-- Product Usage Metrics -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Project Completion Rate</div>
<div class="stat-value" id="project-completion-rate">0%</div>
</div>
<div class="stat-card">
<div class="stat-label">Return User Rate</div>
<div class="stat-value" id="return-user-rate">0%</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Sessions</div>
<div class="stat-value" id="total-sessions">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Projects</div>
<div class="stat-value" id="total-projects">0</div>
</div>
</div>
<!-- Technical Metrics Overview -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Avg Queue Time</div>
<div class="stat-value" id="avg-queue-time">0ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Exports</div>
<div class="stat-value" id="total-exports">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Errors</div>
<div class="stat-value" id="total-errors">0</div>
</div>
<div class="stat-card">
<div class="stat-label">System Uptime</div>
<div class="stat-value" id="system-uptime">0h</div>
</div>
</div>
<!-- Feature Usage Chart -->
<div class="chart-container">
<div class="chart-title">Feature Usage (Most Popular)</div>
<div class="bar-chart" id="feature-usage-chart"></div>
</div>
<!-- Model Usage Chart -->
<div class="chart-container">
<div class="chart-title">AI Model Usage</div>
<div class="bar-chart" id="model-usage-chart"></div>
</div>
<!-- Error Rates Chart -->
<div class="chart-container">
<div class="chart-title">Error Rates by Type</div>
<div class="bar-chart" id="error-rates-chart"></div>
</div>
<!-- Plan Upgrade Patterns Chart -->
<div class="chart-container">
<div class="chart-title">Plan Upgrade Patterns</div>
<div class="bar-chart" id="plan-upgrades-chart"></div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px;">
<!-- Retention Cohorts Table -->
<div class="chart-container">
<div class="chart-title">Retention Cohorts</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Cohort Month</th>
<th>Size</th>
<th>1 Week</th>
<th>1 Month</th>
<th>3 Month</th>
</tr>
</thead>
<tbody id="retention-cohorts-table">
<tr>
<td colspan="5" class="empty-state">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Conversion Funnels -->
<div class="chart-container">
<div class="chart-title">Conversion Funnels</div>
<div id="conversion-funnels-chart"></div>
</div>
</div>
<!-- Resource Utilization Chart -->
<div class="chart-container">
<div class="chart-title">Resource Utilization (Last 24 Hours)</div>
<div class="line-chart" id="resource-utilization-chart"></div>
</div>
<!-- AI Response Times Chart -->
<div class="chart-container">
<div class="chart-title">AI Response Times (Last 100 Requests)</div>
<div class="line-chart" id="ai-response-times-chart"></div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px;">
<!-- Top Referrers -->
<div class="chart-container">
<div class="chart-title">Top Referrers</div>
<div class="bar-chart" id="referrers-chart"></div>
</div>
<!-- Top Referrers to Upgrade -->
<div class="chart-container">
<div class="chart-title">Top Referrers to Upgrade Page</div>
<div class="bar-chart" id="upgrade-referrers-chart"></div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px;">
<!-- Top Pages -->
<div class="chart-container">
<div class="chart-title">Most Visited Pages</div>
<div class="bar-chart" id="pages-chart"></div>
</div>
<!-- Conversion Sources -->
<div class="chart-container">
<div class="chart-title">Signup Conversion Sources</div>
<div class="bar-chart" id="conversion-sources-chart"></div>
</div>
</div>
<!-- Upgrade Popup Sources -->
<div class="chart-container">
<div class="chart-title">Upgrade Popup Sources (Where users clicked upgrade)</div>
<div class="bar-chart" id="upgrade-sources-chart"></div>
</div>
<!-- Recent Visits Table -->
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Time</th>
<th>Path</th>
<th>Referrer</th>
<th>IP Address</th>
</tr>
</thead>
<tbody id="recent-visits-table">
<tr>
<td colspan="4" class="empty-state">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
<script>
let trackingData = null;
const byId = (id) => document.getElementById(id);
function setText(id, value) {
const el = byId(id);
if (!el) return;
el.textContent = value;
}
function formatNumber(value) {
return (Number(value) || 0).toLocaleString();
}
function formatMoney(value) {
const num = Number(value) || 0;
return `$${num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function formatPercent(value, digits = 0) {
const num = Number(value) || 0;
return `${num.toFixed(digits)}%`;
}
function formatDuration(seconds) {
const num = Number(seconds) || 0;
if (num < 60) return `${Math.round(num)}s`;
const minutes = Math.floor(num / 60);
const remainingSeconds = Math.round(num % 60);
return `${minutes}m ${remainingSeconds}s`;
}
function formatUptime(seconds) {
const num = Number(seconds) || 0;
const hours = Math.floor(num / 3600);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${Math.floor((num % 3600) / 60)}m`;
return `${Math.floor(num / 60)}m`;
}
async function loadTrackingData() {
try {
// Ensure credentials are included so admin session cookie is sent
const response = await fetch('/api/admin/tracking', { credentials: 'same-origin' });
if (!response.ok) {
if (response.status === 401) {
window.location.href = '/admin/login';
return;
}
throw new Error('Failed to load tracking data');
}
const data = await response.json();
trackingData = (data && data.stats) ? data.stats : null;
renderStats();
renderFeatureUsageChart();
renderModelUsageChart();
renderErrorRatesChart();
renderPlanUpgradesChart();
renderRetentionCohorts();
renderConversionFunnels();
renderResourceUtilization();
renderAIResponseTimes();
renderReferrersChart();
renderUpgradeReferrersChart();
renderPagesChart();
renderConversionSourcesChart();
renderUpgradeSourcesChart();
renderRecentVisits();
} catch (error) {
console.error('Error loading tracking data:', error);
}
}
function renderStats() {
if (!trackingData) return;
const engagement = trackingData.userEngagement || {};
setText('dau', formatNumber(engagement.dau));
setText('wau', formatNumber(engagement.wau));
setText('mau', formatNumber(engagement.mau));
setText('avg-session-duration', formatDuration(engagement.averageSessionDuration));
const business = trackingData.businessMetrics || {};
setText('mrr', formatMoney(business.mrr));
setText('ltv', formatMoney(business.ltv));
setText('churn-rate', formatPercent(business.churnRate, 1));
setText('arpu', formatMoney(business.averageRevenuePerUser));
setText('project-completion-rate', formatPercent(engagement.projectCompletionRate));
setText('return-user-rate', formatPercent(engagement.returnUserRate));
const sessions = trackingData.sessionInsights || {};
setText('total-sessions', formatNumber(sessions.totalSessions));
setText('total-projects', formatNumber(sessions.totalProjectsCreated));
setText('total-exports', formatNumber(sessions.totalExports));
setText('total-errors', formatNumber(sessions.totalErrors));
const tech = trackingData.technicalMetrics || {};
setText('avg-queue-time', `${formatNumber(tech.averageQueueTime)}ms`);
setText('system-uptime', formatUptime(tech.systemHealth && tech.systemHealth.uptime));
}
function renderBarChart({
containerId,
entries,
maxItems = 10,
emptyText = 'No data available',
barGradient = 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
labelFormatter = (label) => label,
countFormatter = (count) => String(count),
}) {
const chartEl = byId(containerId);
if (!chartEl) return;
chartEl.innerHTML = '';
const sliced = entries.slice(0, maxItems);
if (sliced.length === 0) {
chartEl.innerHTML = `<div class="empty-state">${emptyText}</div>`;
return;
}
const maxCount = Math.max(...sliced.map(([, count]) => Number(count) || 0), 1);
sliced.forEach(([labelRaw, countRaw]) => {
const count = Number(countRaw) || 0;
const item = document.createElement('div');
item.className = 'bar-item';
const label = document.createElement('div');
label.className = 'bar-label';
label.textContent = labelFormatter(labelRaw);
const wrapper = document.createElement('div');
wrapper.className = 'bar-wrapper';
const fill = document.createElement('div');
fill.className = 'bar-fill';
fill.style.background = barGradient;
fill.style.width = `${(count / maxCount) * 100}%`;
const countEl = document.createElement('div');
countEl.className = 'bar-count';
countEl.textContent = countFormatter(count);
wrapper.appendChild(fill);
item.appendChild(label);
item.appendChild(wrapper);
item.appendChild(countEl);
chartEl.appendChild(item);
});
}
function renderFeatureUsageChart() {
if (!trackingData || !trackingData.featureUsage) return;
renderBarChart({
containerId: 'feature-usage-chart',
entries: Object.entries(trackingData.featureUsage).sort((a, b) => (b[1] || 0) - (a[1] || 0)),
emptyText: 'No feature usage data available',
barGradient: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
labelFormatter: (feature) => String(feature).replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
});
}
function renderModelUsageChart() {
if (!trackingData || !trackingData.modelUsage) return;
renderBarChart({
containerId: 'model-usage-chart',
entries: Object.entries(trackingData.modelUsage).sort((a, b) => (b[1] || 0) - (a[1] || 0)),
emptyText: 'No model usage data available',
barGradient: 'linear-gradient(90deg, #008060 0%, #004c3f 100%)',
labelFormatter: (model) => String(model).split('/').pop(),
});
}
function renderErrorRatesChart() {
if (!trackingData || !trackingData.errorRates) return;
renderBarChart({
containerId: 'error-rates-chart',
entries: Object.entries(trackingData.errorRates).sort((a, b) => (b[1] || 0) - (a[1] || 0)),
emptyText: 'No error data available',
barGradient: 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)',
labelFormatter: (error) => String(error).replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
});
}
function renderPlanUpgradesChart() {
if (!trackingData || !trackingData.planUpgradePatterns) return;
const upgrades = trackingData.planUpgradePatterns;
const upgradeEntries = [];
Object.entries(upgrades).forEach(([fromPlan, toPlans]) => {
Object.entries(toPlans || {}).forEach(([toPlan, count]) => {
upgradeEntries.push([`${fromPlan}${toPlan}`, Number(count) || 0]);
});
});
upgradeEntries.sort((a, b) => (b[1] || 0) - (a[1] || 0));
renderBarChart({
containerId: 'plan-upgrades-chart',
entries: upgradeEntries,
emptyText: 'No upgrade data available',
barGradient: 'linear-gradient(90deg, #3b82f6 0%, #2563eb 100%)',
});
}
function renderRetentionCohorts() {
if (!trackingData || !trackingData.retentionCohorts) return;
const tableEl = byId('retention-cohorts-table');
if (!tableEl) return;
tableEl.innerHTML = '';
const cohorts = Object.entries(trackingData.retentionCohorts)
.sort((a, b) => b[0].localeCompare(a[0]))
.slice(0, 12);
if (cohorts.length === 0) {
tableEl.innerHTML = '<tr><td colspan="5" class="empty-state">No cohort data available</td></tr>';
return;
}
cohorts.forEach(([month, cohort]) => {
const row = document.createElement('tr');
const tdMonth = document.createElement('td');
tdMonth.textContent = month;
const tdSize = document.createElement('td');
tdSize.textContent = formatNumber(cohort && cohort.cohortSize);
const retention = (cohort && cohort.retention) || {};
const td1w = document.createElement('td');
td1w.textContent = formatPercent(retention['1week'] || 0, 1);
const td1m = document.createElement('td');
td1m.textContent = formatPercent(retention['1month'] || 0, 1);
const td3m = document.createElement('td');
td3m.textContent = formatPercent(retention['3month'] || 0, 1);
row.appendChild(tdMonth);
row.appendChild(tdSize);
row.appendChild(td1w);
row.appendChild(td1m);
row.appendChild(td3m);
tableEl.appendChild(row);
});
}
function renderConversionFunnels() {
if (!trackingData || !trackingData.conversionFunnels) return;
const chartEl = byId('conversion-funnels-chart');
if (!chartEl) return;
chartEl.innerHTML = '';
const funnels = trackingData.conversionFunnels;
const funnelNames = Object.keys(funnels || {});
if (funnelNames.length === 0) {
chartEl.innerHTML = '<div class="empty-state">No funnel data available</div>';
return;
}
funnelNames.forEach((funnelName) => {
const funnel = funnels[funnelName] || {};
const steps = Object.keys(funnel).sort();
if (steps.length === 0) return;
const funnelDiv = document.createElement('div');
funnelDiv.className = 'chart-container';
funnelDiv.style.marginBottom = '20px';
const title = document.createElement('div');
title.className = 'chart-title';
title.textContent = String(funnelName).replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
const stepsList = document.createElement('div');
stepsList.className = 'bar-chart';
const maxCount = Math.max(
...steps.map((step) => Number((funnel[step] && funnel[step].count) || 0)),
1
);
steps.forEach((step) => {
const stepData = funnel[step] || {};
const count = Number(stepData.count) || 0;
const item = document.createElement('div');
item.className = 'bar-item';
const label = document.createElement('div');
label.className = 'bar-label';
label.textContent = String(step).replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
const wrapper = document.createElement('div');
wrapper.className = 'bar-wrapper';
const fill = document.createElement('div');
fill.className = 'bar-fill';
fill.style.background = 'linear-gradient(90deg, #10b981 0%, #059669 100%)';
fill.style.width = `${(count / maxCount) * 100}%`;
const countEl = document.createElement('div');
countEl.className = 'bar-count';
countEl.textContent = formatNumber(count);
wrapper.appendChild(fill);
item.appendChild(label);
item.appendChild(wrapper);
item.appendChild(countEl);
stepsList.appendChild(item);
});
funnelDiv.appendChild(title);
funnelDiv.appendChild(stepsList);
chartEl.appendChild(funnelDiv);
});
}
function getResourceUsageSeries() {
if (!trackingData || !trackingData.technicalMetrics) return [];
const tech = trackingData.technicalMetrics;
if (Array.isArray(tech.resourceUsage)) {
return tech.resourceUsage;
}
const raw = tech.resourceUtilization;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
if (typeof raw === 'object') {
return Object.entries(raw)
.map(([timestamp, data]) => ({ timestamp: Number(timestamp), ...(data || {}) }))
.filter((d) => Number.isFinite(d.timestamp))
.sort((a, b) => a.timestamp - b.timestamp);
}
return [];
}
function renderResourceUtilization() {
const chartEl = byId('resource-utilization-chart');
if (!chartEl) return;
chartEl.innerHTML = '';
const all = getResourceUsageSeries();
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
const data = all.filter((d) => (Number(d.timestamp) || 0) >= cutoff).slice(-288); // ~24h @ 5m intervals
if (data.length === 0) {
chartEl.innerHTML = '<div class="empty-state">No resource data available</div>';
return;
}
const maxMemory = Math.max(...data.map((d) => Number(d.memory) || 0), 1);
const labelEvery = Math.max(1, Math.floor(data.length / 8));
data.forEach((d, idx) => {
const bar = document.createElement('div');
bar.className = 'line-bar';
const height = ((Number(d.memory) || 0) / maxMemory) * 100;
bar.style.height = `${height}%`;
const ts = Number(d.timestamp) || 0;
const memMb = ((Number(d.memory) || 0) / (1024 * 1024)).toFixed(1);
const cpu = Number(d.cpu) || 0;
bar.title = `${new Date(ts).toLocaleString()}: ${memMb}MB, load: ${cpu.toFixed(2)}`;
const label = document.createElement('div');
label.className = 'line-bar-label';
label.textContent = idx % labelEvery === 0 ? new Date(ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '';
bar.appendChild(label);
chartEl.appendChild(bar);
});
}
function renderAIResponseTimes() {
const chartEl = byId('ai-response-times-chart');
if (!chartEl) return;
if (!trackingData || !trackingData.technicalMetrics || !Array.isArray(trackingData.technicalMetrics.aiResponseTimes)) return;
chartEl.innerHTML = '';
const responseTimes = trackingData.technicalMetrics.aiResponseTimes.slice(-100);
if (responseTimes.length === 0) {
chartEl.innerHTML = '<div class="empty-state">No response time data available</div>';
return;
}
const maxTime = Math.max(...responseTimes.map((r) => Number(r.responseTime) || 0), 1);
responseTimes.forEach((data, index) => {
const bar = document.createElement('div');
bar.className = 'line-bar';
const height = ((Number(data.responseTime) || 0) / maxTime) * 100;
bar.style.height = `${height}%`;
bar.style.background = data.success
? 'linear-gradient(180deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(180deg, #ef4444 0%, #dc2626 100%)';
bar.title = `${data.provider}: ${data.responseTime}ms ${data.success ? '(success)' : '(error)'}`;
const label = document.createElement('div');
label.className = 'line-bar-label';
label.textContent = index % 10 === 0 ? `${Math.round(Number(data.responseTime) || 0)}ms` : '';
bar.appendChild(label);
chartEl.appendChild(bar);
});
}
function renderReferrersChart() {
if (!trackingData) return;
const referrers = Array.isArray(trackingData.topReferrers) ? trackingData.topReferrers.slice(0, 10) : [];
renderBarChart({
containerId: 'referrers-chart',
entries: referrers.map((r) => [r.domain, r.count]),
emptyText: 'No referrer data available',
barGradient: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
countFormatter: (count) => formatNumber(count),
});
}
function renderUpgradeReferrersChart() {
if (!trackingData) return;
const referrers = Array.isArray(trackingData.referrersToUpgrade) ? trackingData.referrersToUpgrade : [];
renderBarChart({
containerId: 'upgrade-referrers-chart',
entries: referrers.map((r) => [r.domain, r.count]),
maxItems: 10,
emptyText: 'No data available',
barGradient: 'linear-gradient(90deg, #008060 0%, #004c3f 100%)',
countFormatter: (count) => formatNumber(count),
});
}
function renderPagesChart() {
if (!trackingData) return;
const pages = Array.isArray(trackingData.topPages) ? trackingData.topPages.slice(0, 10) : [];
renderBarChart({
containerId: 'pages-chart',
entries: pages.map((p) => [p.path, p.count]),
emptyText: 'No page data available',
barGradient: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
countFormatter: (count) => formatNumber(count),
});
}
function renderConversionSourcesChart() {
if (!trackingData || !trackingData.conversionSources) return;
const sources = trackingData.conversionSources.signup || {};
const entries = Object.entries(sources).sort((a, b) => (b[1] || 0) - (a[1] || 0));
renderBarChart({
containerId: 'conversion-sources-chart',
entries,
emptyText: 'No data available',
barGradient: 'linear-gradient(90deg, #3b82f6 0%, #2563eb 100%)',
labelFormatter: (source) => {
const s = String(source);
return s.charAt(0).toUpperCase() + s.slice(1);
},
});
}
function renderUpgradeSourcesChart() {
if (!trackingData || !trackingData.upgradeSources) return;
const sources = trackingData.upgradeSources || {};
const entries = Object.entries(sources).sort((a, b) => (b[1] || 0) - (a[1] || 0));
const sourceLabels = {
apps_page: 'Apps Page',
builder_model: 'Builder Model Selection',
usage_limit: 'Usage Limit Reached',
other: 'Other',
};
renderBarChart({
containerId: 'upgrade-sources-chart',
entries,
emptyText: 'No upgrade popup data available',
barGradient: 'linear-gradient(90deg, #f59e0b 0%, #d97706 100%)',
labelFormatter: (source) => sourceLabels[source] || String(source).replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
});
}
function renderRecentVisits() {
if (!trackingData) return;
const tableEl = byId('recent-visits-table');
if (!tableEl) return;
tableEl.innerHTML = '';
const visits = Array.isArray(trackingData.recentVisits) ? trackingData.recentVisits.slice(0, 50) : [];
if (visits.length === 0) {
tableEl.innerHTML = '<tr><td colspan="4" class="empty-state">No recent visits</td></tr>';
return;
}
visits.forEach((visit) => {
const row = document.createElement('tr');
const timeCell = document.createElement('td');
timeCell.className = 'time-cell';
timeCell.textContent = new Date(visit.timestamp).toLocaleString();
const pathCell = document.createElement('td');
pathCell.className = 'path-cell';
pathCell.textContent = visit.path;
const referrerCell = document.createElement('td');
referrerCell.textContent = visit.referrer === 'direct' ? 'Direct' : visit.referrer;
referrerCell.style.maxWidth = '200px';
referrerCell.style.overflow = 'hidden';
referrerCell.style.textOverflow = 'ellipsis';
referrerCell.style.whiteSpace = 'nowrap';
referrerCell.title = visit.referrer;
const ipCell = document.createElement('td');
ipCell.textContent = visit.ip;
ipCell.style.fontFamily = 'monospace';
ipCell.style.fontSize = '13px';
row.appendChild(timeCell);
row.appendChild(pathCell);
row.appendChild(referrerCell);
row.appendChild(ipCell);
tableEl.appendChild(row);
});
}
// Admin controls
byId('admin-refresh')?.addEventListener('click', () => {
loadTrackingData();
});
byId('admin-logout')?.addEventListener('click', async () => {
try {
await fetch('/api/admin/logout', { method: 'POST', credentials: 'same-origin' });
window.location.href = '/admin/login';
} catch (error) {
console.error('Logout error:', error);
window.location.href = '/admin/login';
}
});
// Load data on page load
loadTrackingData();
</script>
</body>
</html>