Changeset 1794fac in Klonkt


Ignore:
Timestamp:
06/19/2026 04:07:24 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
6be57b4
Parents:
37edecd
Message:

Pro stats (premium feature #5): referrers + period selector + all-time

Extension of the cookie-free StatsService.

  • DB: stat_referrer (site_id, host, count).
  • StatsService: recordPageview now also counts the external referrer host (Referer header; empty/own-site/invalid skipped, www- normalised). getStats(days) clamps to 7/14/30/90 + returns top-10 referrers (instance-wide) + all-time (pageviews, visitor days).
  • admin-stats route: ?days=7|14|30|90.
  • admin-stats view: period buttons, dynamic labels, Sources section, all-time row.

node --check passed.

Co-Authored-By: Claude <noreply@…>

Location:
src
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r37edecd r1794fac  
    146146    );
    147147    CREATE INDEX IF NOT EXISTS idx_stat_visitor_day ON stat_visitor_day(site_id, day);
     148    CREATE TABLE IF NOT EXISTS stat_referrer (
     149      site_id TEXT NOT NULL,
     150      host TEXT NOT NULL,
     151      count INTEGER NOT NULL DEFAULT 0,
     152      PRIMARY KEY (site_id, host)
     153    );
    148154  `);
    149155
  • src/routes/admin-stats.js

    r37edecd r1794fac  
    3131    } catch { linkClicks = []; }
    3232  }
     33  const days = [7, 14, 30, 90].includes(parseInt(req.query.days, 10)) ? parseInt(req.query.days, 10) : 14;
    3334  renderPage(req, res, 'pages/admin-stats', {
    3435    pageTitle: 'Statistieken',
    3536    bodyClass: 'on-admin',
    36     stats: getStats(14),
     37    stats: getStats(days),
    3738    linkClicks,
    3839  });
  • src/services/StatsService.js

    r37edecd r1794fac  
    5858    bumpPost: db.prepare('UPDATE posts SET view_count = COALESCE(view_count, 0) + 1 WHERE id = ?'),
    5959    bumpTrack: db.prepare('UPDATE audio_tracks SET play_count = COALESCE(play_count, 0) + 1 WHERE id = ?'),
     60    bumpReferrer: db.prepare(`
     61      INSERT INTO stat_referrer (site_id, host, count) VALUES (?, ?, 1)
     62      ON CONFLICT(site_id, host) DO UPDATE SET count = count + 1
     63    `),
    6064  };
    6165  return _s;
     66}
     67
     68// Externe referrer-host uit de Referer-header (pro-stats #5). Lege/eigen-site/
     69// ongeldige referrers worden overgeslagen → alleen echte externe bronnen tellen.
     70function recordReferrer(siteId, req) {
     71  try {
     72    const ref = req && req.headers && (req.headers.referer || req.headers.referrer);
     73    if (!ref) return;
     74    const host = new URL(ref).host.replace(/^www\./, '').toLowerCase();
     75    if (!host) return;
     76    const own = ((req.headers && req.headers.host) || '').replace(/^www\./, '').toLowerCase();
     77    if (host === own) return; // interne navigatie telt niet als bron
     78    stmts().bumpReferrer.run(siteId, host.slice(0, 120));
     79  } catch { /* geen geldige referrer-URL → overslaan */ }
    6280}
    6381
     
    6886    stmts().bumpDaily.run(siteId, d);
    6987    stmts().addVisitor.run(siteId, d, visitorHash(req));
     88    recordReferrer(siteId, req);
    7089  } catch { /* statistieken mogen nooit een request breken */ }
    7190}
     
    86105// Instance-brede statistieken (solo = de site, hub = alle sites samen).
    87106export function getStats(days = 14) {
     107  days = [7, 14, 30, 90].includes(Number(days)) ? Number(days) : 14;
    88108  const pvMap = Object.fromEntries(
    89109    db.prepare('SELECT day, SUM(pageviews) AS pv FROM stat_daily GROUP BY day').all().map((r) => [r.day, r.pv]),
     
    113133    ORDER BY play_count DESC LIMIT 5
    114134  `).all();
    115   return { totals, series, topPosts, topTracks, days };
     135  // Top externe bronnen (pro #5) — instance-breed geaggregeerd per host.
     136  let referrers = [];
     137  try {
     138    referrers = db.prepare(
     139      'SELECT host, SUM(count) AS n FROM stat_referrer GROUP BY host ORDER BY n DESC LIMIT 10'
     140    ).all();
     141  } catch { referrers = []; }
     142  // All-time totalen (cookieloze unieke bezoekers = som van dag-uniques).
     143  const allTime = {
     144    pageviews: db.prepare('SELECT COALESCE(SUM(pageviews),0) AS n FROM stat_daily').get().n,
     145    visitorDays: db.prepare('SELECT COUNT(*) AS n FROM stat_visitor_day').get().n,
     146  };
     147  return { totals, series, topPosts, topTracks, referrers, allTime, days };
    116148}
  • src/views/pages/admin-stats.ejs

    r37edecd r1794fac  
    99
    1010  <div class="stats-cards">
    11     <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label">Bezoekers (14d)</div></div>
    12     <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label">Weergaven (14d)</div></div>
     11    <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label">Bezoekers (<%= s.days %>d)</div></div>
     12    <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label">Weergaven (<%= s.days %>d)</div></div>
    1313    <div class="stat-card"><div class="stat-num"><%= s.totals.plays %></div><div class="stat-label">Plays (totaal)</div></div>
    1414    <div class="stat-card"><div class="stat-num"><%= s.totals.postViews %></div><div class="stat-label">Post-weergaven (totaal)</div></div>
    1515  </div>
    1616
     17  <% if (typeof s.allTime !== 'undefined') { %>
     18    <p class="stats-note">All-time: <strong><%= s.allTime.pageviews %></strong> weergaven · <strong><%= s.allTime.visitorDays %></strong> bezoeker-dagen.</p>
     19  <% } %>
     20
     21  <div class="stats-period">
     22    Periode:
     23    <% [7,14,30,90].forEach(function(n){ %>
     24      <a href="/admin/stats?days=<%= n %>" class="<%= s.days === n ? 'is-on' : '' %>"><%= n %>d</a>
     25    <% }); %>
     26  </div>
     27
    1728  <section class="stats-chart-wrap">
    1829    <div class="stats-chart-head">
    19       <h2>Laatste 14 dagen</h2>
     30      <h2>Laatste <%= s.days %> dagen</h2>
    2031      <div class="stats-legend">
    2132        <span class="lg lg-vis">Bezoekers</span>
     
    6677  </div>
    6778
     79  <% if (typeof s.referrers !== 'undefined' && s.referrers.length) { %>
     80    <section class="stats-top" style="margin-top:1.2rem">
     81      <h2>🌐 Bronnen (waar bezoekers vandaan komen)</h2>
     82      <ol>
     83        <% s.referrers.forEach(function(r){ %>
     84          <li><span><%= r.host %></span><span class="stats-count"><%= r.n %></span></li>
     85        <% }); %>
     86      </ol>
     87    </section>
     88  <% } %>
     89
    6890  <% if (typeof linkClicks !== 'undefined' && linkClicks.length) { %>
    6991    <section class="stats-top" style="margin-top:1.2rem">
     
    112134.stats-count { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
    113135.stats-empty { color: var(--ink-muted); font-size: .88rem; margin: 0; }
     136.stats-period { margin: .2rem 0 1.2rem; font-size: .9rem; color: var(--ink-muted); }
     137.stats-period a { display: inline-block; margin-left: .4rem; padding: .2rem .6rem; border: 1px solid var(--rule); border-radius: 999px; text-decoration: none; color: var(--ink); }
     138.stats-period a.is-on { background: var(--accent); color: #fff; border-color: var(--accent); }
    114139</style>
Note: See TracChangeset for help on using the changeset viewer.