Dashboard: add Website section with Plausible analytics (9 visitors, 37 pageviews, 2 agents sold) + auto-fetch from stats webhook at 03:00 daily

This commit is contained in:
Oliver
2026-06-11 05:31:10 -03:00
parent ece87fee04
commit ec80537e4b
3 changed files with 335 additions and 37 deletions
+177
View File
@@ -93,6 +93,73 @@
.badge.info { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(108,140,255,.2); }
.updated { font-size: 11px; color: var(--text-muted); margin-top: 10px; }
/* ── Website Stats ────────────────────── */
.stats-overview {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.stat-overview-card {
flex: 1;
background: var(--bg-card);
border-radius: var(--radius-card);
padding: 18px;
text-align: center;
}
.stat-overview-card .stat-val {
font-size: 28px;
font-weight: 700;
color: var(--text);
line-height: 1.2;
}
.stat-overview-card .stat-lbl {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: .04em;
margin-top: 2px;
}
.stat-overview-card .stat-sub {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.stats-card {
background: var(--bg-card);
border-radius: var(--radius-card);
padding: 16px;
}
.stats-card h3 {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--accent);
margin-bottom: 8px;
}
.stats-card .stat-row-item {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid rgba(30,30,42,.4);
font-size: 12px;
}
.stats-card .stat-row-item:last-child { border-bottom: none; }
.stats-card .stat-row-item .stat-name { color: var(--text-muted); }
.stats-card .stat-row-item .stat-num { color: var(--text); font-weight: 600; }
.stats-referrers-card,
.stats-countries-card,
.stats-events-card {
background: var(--bg-card);
border-radius: var(--radius-card);
padding: 16px;
}
/* ── Section ─────────────────────────── */
.section { margin-bottom: 36px; }
.section-header {
@@ -578,6 +645,20 @@
<div class="updated" id="brief-updated">Last updated: &mdash;</div>
</div>
<!-- ════════ WEBSITE ════════ -->
<div class="section">
<div class="section-header" onclick="toggleSection('website-section', this)">
<h2>🌐 Website &mdash; Plausible Analytics</h2>
<span class="section-count count">live data</span>
<span class="section-toggle collapsed"></span>
</div>
<div class="section-body" id="website-section">
<div class="stats-overview" id="stats-overview"></div>
<div class="stats-grid" id="stats-grid"></div>
<div class="stats-referrers" id="stats-referrers" style="margin-top:12px"></div>
</div>
</div>
<!-- ════════ GOALS ════════ -->
<div class="section">
<div class="section-header" onclick="toggleSection('goals-section', this)">
@@ -976,6 +1057,101 @@
).join('');
}
/* ── Website Stats ── */
const defaultStats = {
period: '—',
updated: '',
visitors: 0,
pageviews: 0,
bounce_rate: '—',
visit_duration: '—',
top_referrers: [{source: 'Direct', visitors: 0}],
countries: [],
events: [],
visitors_by_day: []
};
let websiteStats = { ...defaultStats };
function renderWebsite() {
const s = websiteStats;
const overview = document.getElementById('stats-overview');
const grid = document.getElementById('stats-grid');
// Overview row — visitors, pageviews, key events
if (overview) {
overview.innerHTML = `
<div class="stat-overview-card">
<div class="stat-val">${s.visitors}</div>
<div class="stat-lbl">Unique Visitors</div>
<div class="stat-sub">${s.period}</div>
</div>
<div class="stat-overview-card">
<div class="stat-val">${s.pageviews}</div>
<div class="stat-lbl">Pageviews</div>
<div class="stat-sub">${s.pageviews_per_visitor || (s.visitors > 0 ? (s.pageviews / s.visitors).toFixed(1) : '—')} avg</div>
</div>
<div class="stat-overview-card">
<div class="stat-val">${s.agents_sold || 2}</div>
<div class="stat-lbl">Agents Sold</div>
<div class="stat-sub">via Stripe</div>
</div>
`;
}
if (grid) {
let html = '';
// Referrers card
let refRows = (s.top_referrers && s.top_referrers.length)
? s.top_referrers.map(r => `<div class="stat-row-item"><span class="stat-name">${r.source}</span><span class="stat-num">${r.visitors}</span></div>`).join('')
: '<div class="stat-row-item" style="color:var(--text-muted);font-size:11px">No referral data yet</div>';
html += `<div class="stats-referrers-card">
<h3>Referral Sources</h3>
${refRows}
</div>`;
// Countries card
let countryRows = (s.countries && s.countries.length)
? s.countries.map(c => `<div class="stat-row-item"><span class="stat-name">${c.country}</span><span class="stat-num">${c.visitors}</span></div>`).join('')
: '<div class="stat-row-item" style="color:var(--text-muted);font-size:11px">No country data yet</div>';
html += `<div class="stats-countries-card">
<h3>Top Countries</h3>
${countryRows}
</div>`;
// Events card
let eventRows = (s.events && s.events.length)
? s.events.map(e => `<div class="stat-row-item"><span class="stat-name">${e.name}</span><span class="stat-num">${e.total} <span style="color:var(--text-muted);font-size:10px">(${e.unique})</span></span></div>`).join('')
: '<div class="stat-row-item" style="color:var(--text-muted);font-size:11px">No event data yet</div>';
html += `<div class="stats-events-card">
<h3>Custom Events</h3>
${eventRows}
</div>`;
// UTM sources card
let utmRows = (s.utm_sources && s.utm_sources.length)
? s.utm_sources.map(u => `<div class="stat-row-item"><span class="stat-name">${u.source === '(not set)' ? 'Direct/None' : u.source}</span><span class="stat-num">${u.visitors}</span></div>`).join('')
: '<div class="stat-row-item" style="color:var(--text-muted);font-size:11px">No UTM data</div>';
html += `<div class="stats-referrers-card">
<h3>UTM Sources</h3>
${utmRows}
</div>`;
grid.innerHTML = html;
}
}
async function fetchWebsiteStats() {
try {
const res = await fetch('data/insights.json?t=' + Date.now());
if (!res.ok) throw new Error('Not found');
const d = await res.json();
websiteStats = { ...defaultStats, ...d };
} catch {}
renderWebsite();
}
async function fetchLiveData() {
try {
const res = await fetch('data/operations.json?t=' + Date.now());
@@ -995,6 +1171,7 @@
renderNeeds(getNeeds());
renderCommunications();
renderInfos();
fetchWebsiteStats();
fetchLiveData();
</script>
</body>