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:
+177
@@ -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: —</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════ WEBSITE ════════ -->
|
||||
<div class="section">
|
||||
<div class="section-header" onclick="toggleSection('website-section', this)">
|
||||
<h2>🌐 Website — 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>
|
||||
|
||||
Reference in New Issue
Block a user