{% extends 'base.html' %}
{% block title %}Analytics — {{ site.name }} — AgentForms{% endblock %}
{% block head_extra %}
<style nonce="{{ csp_nonce }}">
nav { background: #1a1a2e; color: white; }.btn { display: inline-block; padding: 8px 16px; background: #1a1a2e; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; font-size: 14px; }
.btn:hover { background: #2d2d44; }
.btn-outline { background: transparent; border: 1px solid #1a1a2e; color: #1a1a2e; }
.btn-outline:hover { background: #f0f0f0; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 16px; }
.stat-card { background: #f8f9fa; border-radius: 8px; padding: 16px; text-align: center; }
.stat-value { font-size: 32px; font-weight: 700; color: #1a1a2e; }
.stat-label { font-size: 13px; color: #666; margin-top: 4px; }.token { font-family: monospace; background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 12px; }.badge-spam { background: #f8d7da; color: #721c24; }
.period-select { padding: 6px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
/* Chart */
.chart-container { position: relative; height: 200px; margin-top: 16px; }
.chart-bar { fill: #4a90d9; }
.chart-bar-spam { fill: #e74c3c; }
.chart-bar-imp { fill: #95a5a6; }
.chart-axis { stroke: #ddd; }
.chart-label { font-size: 11px; fill: #666; }/* Horizontal bar chart */
.h-bar-row { display: flex; align-items: center; margin-bottom: 8px; }
.h-bar-label { width: 100px; font-size: 13px; color: #555; text-align: right; padding-right: 12px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.h-bar-track { flex: 1; height: 22px; background: #f0f0f0; border-radius: 4px; overflow: hidden; position: relative; }
.h-bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; display: flex; align-items: center; padding-left: 6px; }
.h-bar-fill.city { background: #4a90d9; }
.h-bar-fill.country { background: #27ae60; }
.h-bar-fill.device { background: #e67e22; }
.h-bar-fill.browser { background: #8e44ad; }
.h-bar-fill.os { background: #16a085; }
.h-bar-count { font-size: 11px; color: white; font-weight: 600; }
.h-bar-pct { font-size: 11px; color: #999; margin-left: 8px; flex-shrink: 0; width: 42px; text-align: right; }
.geo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.geo-section h3 { font-size: 14px; color: #888; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.device-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.device-section h3 { font-size: 14px; color: #888; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
/* Conversion rate */
.conversion-ring { width: 100px; height: 100px; margin: 0 auto 12px; }
.conversion-ring-bg { fill: none; stroke: #f0f0f0; stroke-width: 8; }
.conversion-ring-fill { fill: none; stroke: #27ae60; stroke-width: 8; stroke-linecap: round; transition: stroke-dashoffset 0.5s ease; }
.conversion-ring-text { font-size: 20px; font-weight: 700; fill: #1a1a2e; }
.legend { display: flex; gap: 16px; justify-content: center; margin-top: 8px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #666; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
.legend-dot.imp { background: #95a5a6; }
.legend-dot.sub { background: #4a90d9; }
/* Tier upgrade prompt */
.tier-upgrade { background: #fff8e1; border: 1px solid #ffe082; border-radius: 8px; padding: 20px; text-align: center; }
.tier-upgrade p { color: #666; margin-bottom: 12px; font-size: 14px; }
.tier-upgrade .btn { background: #f57c00; }
.tier-upgrade .btn:hover { background: #e65100; }
@media (max-width: 768px) {
.geo-grid { grid-template-columns: 1fr; }
.device-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.container { padding: 8px; }
.stats-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
.stat-value { font-size: 24px; }
.header-row { flex-direction: column; align-items: flex-start; gap: 12px; }
.header-row > div:last-child { width: 100%; display: flex; gap: 8px; flex-wrap: wrap; }
.geo-grid { grid-template-columns: 1fr; }
.device-grid { grid-template-columns: 1fr; }
table { display: block; width: 100%; }
thead { display: none; }
tbody { display: block; width: 100%; }
tr {
display: block; width: 100%; padding: 12px; margin-bottom: 12px;
background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
box-sizing: border-box;
}
td {
display: flex; width: 100%; justify-content: space-between; align-items: center;
text-align: right; padding: 6px 0; border: none; box-sizing: border-box;
}
td:before {
content: attr(data-label); font-weight: 600; color: #666;
text-align: left; flex-shrink: 0; margin-right: 8px; font-size: 11px; text-transform: uppercase;
}
}
</style>
{% endblock %}
{% block content %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card">
<div class="header-row">
<div>
<h2>{{ site.name }} — Analytics</h2>
<p style="color:#666; font-size:13px; margin-top:4px;">
Submission analytics and spam tracking
</p>
</div>
<div>
<select class="period-select" id="periodSelect" data-onchange="loadAnalytics(this.value)">
<option value="7" {% if days==7 %}selected{% endif %}>Last 7 days</option>
<option value="14" {% if days==14 %}selected{% endif %}>Last 14 days</option>
<option value="30" {% if days==30 %}selected{% endif %}>Last 30 days</option>
<option value="90" {% if days==90 %}selected{% endif %}>Last 90 days</option>
</select>
<a href="{{ url_for('user_sites.list_sites') }}" class="btn btn-outline btn-sm" style="margin-left:8px;">← Back</a>
</div>
</div>
</div>
<!-- Summary stats -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ summary.total_submissions }}</div>
<div class="stat-label">Total Submissions</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ summary.total_spam }}</div>
<div class="stat-label">Total Spam Blocked</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ summary.today_submissions }}</div>
<div class="stat-label">Today</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ summary.week_submissions }}</div>
<div class="stat-label">This Week</div>
</div>
</div>
<!-- Chart -->
<div class="card">
<h2>Submissions Over Time</h2>
<div class="chart-container" id="chartContainer">
<svg id="chart" width="100%" height="100%"></svg>
</div>
</div>
<!-- Impressions & Conversion -->
<div class="card">
<h2>Form Impressions & Conversions</h2>
{% if analytics_level == 'advanced' %}
{% if impressions %}
<div class="stats-grid" style="margin-bottom: 20px;">
<div class="stat-card">
<div class="stat-value">{{ impressions.impressions }}</div>
<div class="stat-label">Impressions</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ impressions.submissions }}</div>
<div class="stat-label">Submissions</div>
</div>
<div class="stat-card">
<svg class="conversion-ring" viewBox="0 0 100 100">
<circle class="conversion-ring-bg" cx="50" cy="50" r="40"/>
<circle class="conversion-ring-fill" cx="50" cy="50" r="40"
stroke-dasharray="251.3"
stroke-dashoffset="{{ 251.3 - (251.3 * impressions.conversion_rate / 100) | round(1) }}"
transform="rotate(-90 50 50)"/>
<text class="conversion-ring-text" x="50" y="55" text-anchor="middle">{{ impressions.conversion_rate }}%</text>
</svg>
<div class="stat-label" style="text-align:center;">Conversion Rate</div>
</div>
</div>
<div class="chart-container" id="impChartContainer">
<svg id="impChart" width="100%" height="100%"></svg>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot imp"></div>Impressions</div>
<div class="legend-item"><div class="legend-dot sub"></div>Submissions</div>
</div>
{% else %}
<div class="empty"><p>No impression data yet. Install the embed SDK to start tracking.</p></div>
{% endif %}
{% elif analytics_level == 'basic' %}
<div class="tier-upgrade">
<p>📊 Track form views and conversion rates. Available on <strong>Pro</strong> and above.</p>
<a href="{{ url_for('user_sites.list_sites') }}" class="btn">View Plans</a>
</div>
{% else %}
<div class="tier-upgrade">
<p>📊 Track form views and conversion rates. Available on <strong>Pro</strong> and above.</p>
<a href="{{ url_for('user_sites.list_sites') }}" class="btn">View Plans</a>
</div>
{% endif %}
</div>
<!-- Top Locations -->
<div class="card">
<h2>Top Locations</h2>
{% if analytics_level in ['standard', 'advanced'] %}
{% if geo and (geo.top_cities or geo.top_countries) %}
<div class="geo-grid">
{% if geo.top_cities %}
<div class="geo-section">
<h3>Top Cities</h3>
{% set max_city = geo.top_cities[0].count %}
{% for c in geo.top_cities %}
<div class="h-bar-row">
<div class="h-bar-label" title="{{ c.city }}, {{ c.country }}">{{ c.city }}</div>
<div class="h-bar-track">
<div class="h-bar-fill city" style="width: {{ c.count / max_city * 100 if max_city > 0 else 0 }}%">
<span class="h-bar-count">{{ c.count }}</span>
</div>
</div>
<div class="h-bar-pct">{{ c.country }}</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if geo.top_countries %}
<div class="geo-section">
<h3>Top Countries</h3>
{% set max_country = geo.top_countries[0].count %}
{% for c in geo.top_countries %}
<div class="h-bar-row">
<div class="h-bar-label" title="{{ c.country }}">{{ c.country }}</div>
<div class="h-bar-track">
<div class="h-bar-fill country" style="width: {{ c.count / max_country * 100 if max_country > 0 else 0 }}%">
<span class="h-bar-count">{{ c.count }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% else %}
<div class="empty"><p>No location data yet. Geolocation is tracked on submissions.</p></div>
{% endif %}
{% else %}
<div class="tier-upgrade">
<p>🌍 See where your form submissions come from. Available on <strong>Starter</strong> and above.</p>
<a href="{{ url_for('user_sites.list_sites') }}" class="btn">View Plans</a>
</div>
{% endif %}
</div>
<!-- Device Breakdown -->
<div class="card">
<h2>Device Breakdown</h2>
{% if analytics_level in ['standard', 'advanced'] %}
{% if device and (device.devices or device.browsers or device.os) %}
<div class="device-grid">
{% if device.devices %}
<div class="device-section">
<h3>Devices</h3>
{% for d in device.devices %}
<div class="h-bar-row">
<div class="h-bar-label" style="width: 80px;" title="{{ d.label }}">{{ d.label }}</div>
<div class="h-bar-track">
<div class="h-bar-fill device" style="width: {{ d.pct }}%">
<span class="h-bar-count">{{ d.count }}</span>
</div>
</div>
<div class="h-bar-pct">{{ d.pct }}%</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if device.browsers %}
<div class="device-section">
<h3>Browsers</h3>
{% for d in device.browsers %}
<div class="h-bar-row">
<div class="h-bar-label" style="width: 80px;" title="{{ d.label }}">{{ d.label }}</div>
<div class="h-bar-track">
<div class="h-bar-fill browser" style="width: {{ d.pct }}%">
<span class="h-bar-count">{{ d.count }}</span>
</div>
</div>
<div class="h-bar-pct">{{ d.pct }}%</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if device.os %}
<div class="device-section">
<h3>OS</h3>
{% for d in device.os %}
<div class="h-bar-row">
<div class="h-bar-label" style="width: 80px;" title="{{ d.label }}">{{ d.label }}</div>
<div class="h-bar-track">
<div class="h-bar-fill os" style="width: {{ d.pct }}%">
<span class="h-bar-count">{{ d.count }}</span>
</div>
</div>
<div class="h-bar-pct">{{ d.pct }}%</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% else %}
<div class="empty"><p>No device data yet. Device parsing runs on form submissions.</p></div>
{% endif %}
{% else %}
<div class="tier-upgrade">
<p>📱 See what devices and browsers your visitors use. Available on <strong>Starter</strong> and above.</p>
<a href="{{ url_for('user_sites.list_sites') }}" class="btn">View Plans</a>
</div>
{% endif %}
</div>
<!-- Session Insights (Phase 10) -->
<div class="card">
<h2>Session Insights</h2>
{% if analytics_level == 'advanced' %}
<div class="stats-grid" style="margin-bottom: 20px;">
<div class="stat-card">
<div class="stat-value">{{ session_stats.total_sessions if session_stats else 0 }}</div>
<div class="stat-label">Form Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ session_stats.completed_sessions if session_stats else 0 }}</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ session_stats.abandonment_rate if session_stats else 0 }}%</div>
<div class="stat-label">Abandonment Rate</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ "%.0fs"|format(session_stats.avg_completion_time_seconds) if session_stats and session_stats.avg_completion_time_seconds else '—' }}</div>
<div class="stat-label">Avg Completion (sec)</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ session_stats.return_visitor_count if session_stats else 0 }}</div>
<div class="stat-label">Return Visitors</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ session_stats.return_visitor_rate if session_stats else 0 }}%</div>
<div class="stat-label">Return Rate</div>
</div>
</div>
{% if field_dropoff and field_dropoff|length > 0 %}
<h3 style="margin-bottom: 12px; font-size: 15px;">Field Drop-off (where visitors stopped)</h3>
{% set max_drop = field_dropoff[0].drop_off_rate if field_dropoff|length > 0 else 1 %}
{% for f in field_dropoff[:8] %}
<div class="h-bar-row">
<div class="h-bar-label">{{ f.field_key }}</div>
<div class="h-bar-track">
<div class="h-bar-fill dropoff" style="width: {{ f.drop_off_rate / max_drop * 100 if max_drop > 0 else 0 }}%; background: #e74c3c;">
<span class="h-bar-count">{{ f.abandoned }}</span>
</div>
</div>
<div class="h-bar-pct">{{ f.drop_off_rate }}%</div>
</div>
{% endfor %}
{% endif %}
{% else %}
<div class="tier-upgrade">
<p>🎯 Track visitor sessions, abandonment, and field-level drop-off. Available on <strong>Pro</strong> and above.</p>
<a href="{{ url_for('user_sites.list_sites') }}" class="btn">View Plans</a>
</div>
{% endif %}
</div>
<!-- Daily breakdown -->
<div class="card">
<h2>Daily Breakdown</h2>
{% if analytics %}
<table>
<thead>
<tr>
<th>Date</th>
<th>Submissions</th>
<th>Spam</th>
<th>Clean</th>
</tr>
</thead>
<tbody>
{% for day in analytics|reverse %}
<tr>
<td data-label="Date">{{ day.day }}</td>
<td data-label="Submissions">{{ day.submission_count }}</td>
<td data-label="Spam">
{% if day.spam_count > 0 %}
<span class="badge badge-spam">{{ day.spam_count }}</span>
{% else %}
—
{% endif %}
</td>
<td data-label="Clean">{{ day.submission_count - day.spam_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">
<p>No analytics data yet. Submissions will appear here.</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script type="application/json" id="data-site-analytics">{{ {'analytics': analytics, 'days': days, 'impData': impressions.daily if impressions else [], 'analyticsUrl': url_for('user_sites.site_analytics_view', site_id=site.id)} | tojson }}</script>
<script src="/static/js/site-analytics-inline.js"></script>
{% endblock %}