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
+85 -9
View File
@@ -1,18 +1,94 @@
{
"week": "",
"updated": "",
"visitors": 0,
"period": "2026-06-09 to 2026-06-10",
"updated": "2026-06-11 08:30",
"visitors": 9,
"visitors_change": "",
"pageviews": 0,
"pageviews": 37,
"pageviews_change": "",
"bounce_rate": "",
"bounce_rate": "\u2014",
"bounce_rate_change": "",
"visit_duration": "",
"visit_duration": "\u2014",
"visit_duration_change": "",
"agents_sold": 2,
"top_pages": [],
"top_referrers": [],
"events": [],
"what_worked": [],
"top_referrers": [
{
"source": "Direct / None",
"visitors": 7
},
{
"source": "Youtube",
"visitors": 2
}
],
"visitors_by_day": [
{
"date": "2026-06-09",
"visitors": 4
},
{
"date": "2026-06-10",
"visitors": 5
}
],
"countries": [
{
"country": "US",
"visitors": 4
},
{
"country": "PY",
"visitors": 2
},
{
"country": "AU",
"visitors": 1
},
{
"country": "DE",
"visitors": 1
},
{
"country": "TR",
"visitors": 1
}
],
"events": [
{
"name": "Scroll Depth",
"total": 78,
"unique": 3
},
{
"name": "pageview",
"total": 37,
"unique": 9
},
{
"name": "Outbound Link: Click",
"total": 1,
"unique": 1
},
{
"name": "engagement",
"total": 0,
"unique": 6
}
],
"utm_sources": [
{
"source": "(not set)",
"visitors": 7
},
{
"source": "youtube",
"visitors": 2
}
],
"what_worked": [
"9 unique visitors across 2 days of tracking",
"37 pageviews with 4 event types tracked"
],
"what_didnt": [],
"recommendations": []
}
+72 -27
View File
@@ -1,6 +1,6 @@
{
"date": "Wednesday, June 10, 2026",
"updated": "2026-06-10T09:02:00-04:00",
"updated": "2026-06-11T08:30:10-04:00",
"goals": [
{
"current": 1,
@@ -12,15 +12,15 @@
},
{
"current": 0,
"note": "60 YT creators identified 0 contacted"
"note": "60 YT creators identified \u2014 0 contacted"
},
{
"current": 9,
"note": "Plausible: 9 visitors in first 2 days (Jun 9-10). 7 direct, 2 from YouTube."
},
{
"current": 0,
"note": "No analytics traffic yet. Plausible set up."
},
{
"current": 0,
"note": "0 cold emails sent — templates and coupons ready"
"note": "0 cold emails sent \u2014 templates and coupons ready"
},
{
"current": 89,
@@ -29,42 +29,87 @@
],
"campaigns": [
{
"metrics": [0, 0, 0],
"metrics": [
0,
0,
0
],
"items": [
{ "text": "Coupon codes ready: friends950, creator950", "ok": true },
{ "text": "Template signed as Mark — Customer Success Agent", "ok": true },
{ "text": "No Cold contacts with real emails yet", "ok": false }
{
"text": "Coupon codes ready: friends950, creator950",
"ok": true
},
{
"text": "Template signed as Mark \u2014 Customer Success Agent",
"ok": true
},
{
"text": "No Cold contacts with real emails yet",
"ok": false
}
]
},
{
"metrics": [60, 0, 0],
"metrics": [
60,
0,
0
],
"items": [
{ "text": "60 YouTube creators collected on Hold", "ok": true },
{ "text": "Need business emails for top 20", "ok": false },
{ "text": "Affiliate program TBD", "ok": false }
{
"text": "60 YouTube creators collected on Hold",
"ok": true
},
{
"text": "Need business emails for top 20",
"ok": false
},
{
"text": "Affiliate program TBD",
"ok": false
}
]
},
{
"metrics": [0, 0, 0],
"metrics": [
0,
0,
0
],
"items": [
{ "text": "No outreach started yet", "ok": false },
{ "text": "Profile / company page TBD", "ok": false },
{ "text": "No content strategy defined", "ok": false }
{
"text": "No outreach started yet",
"ok": false
},
{
"text": "Profile / company page TBD",
"ok": false
},
{
"text": "No content strategy defined",
"ok": false
}
]
}
],
"state_of_play": "Day 2 of prelaunch. CRM grew to 89 contacts (+27 since yesterday) 88 on Hold, 1 Cold. 60 YT creators identified. 0 subscriptions, 0 influencers onboarded, 0 cold emails sent. Site live at derez.ai. SEO basics: sitemap.xml and robots.txt created today. Plausible analytics live. Next milestone: first paying customer.",
"state_of_play": "Day 2 of prelaunch. CRM grew to 89 contacts (+27 since yesterday) \u2014 88 on Hold, 1 Cold. 60 YT creators identified. 0 subscriptions, 0 influencers onboarded, 0 cold emails sent. Site live at derez.ai. SEO basics: sitemap.xml and robots.txt created today. Plausible analytics live. Next milestone: first paying customer.",
"comms": {
"inbox": { "total": 0, "last35h": 0 },
"sent": { "total": 8, "last35h": 0 },
"inbox": {
"total": 0,
"last35h": 0
},
"sent": {
"total": 8,
"last35h": 0
},
"alerts": 0
},
"recommendations": [
"Search Console submission still the #1 blocker without it the site can't index. Oliver needs to add domain verification TXT record or upload verification file.",
"Prioritise the top 20 YouTube creators the most winnable influencer channel. Need business emails first.",
"Start drafting cornerstone blog content (4 posts planned) even without Search Console, these build the content base for when indexing starts.",
"Search Console submission still the #1 blocker \u2014 without it the site can't index. Oliver needs to add domain verification TXT record or upload verification file.",
"Prioritise the top 20 YouTube creators \u2014 the most winnable influencer channel. Need business emails first.",
"Start drafting cornerstone blog content (4 posts planned) \u2014 even without Search Console, these build the content base for when indexing starts.",
"Email capture (newsletter signup or lead magnet) needed before 10K/mo path is realistic."
],
"day": 2,
"notes": "Day 2 brief session — 09:02 PYST. sitemap.xml + robots.txt created. Blog post updated (integration/leads split). CRM growth accelerating."
"day": 3,
"notes": "Day 3 auto-update at 08:30 UTC"
}
+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>