Changeset e3a5aed in Klonkt
- Timestamp:
- 06/30/2026 10:11:03 PM (2 months ago)
- Branches:
- main
- Children:
- d9c5eb8
- Parents:
- 65a0697
- Location:
- src
- Files:
-
- 4 edited
-
routes/admin-stats.js (modified) (3 diffs)
-
services/StatsService.js (modified) (3 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/admin-stats.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/admin-stats.js
r65a0697 re3a5aed 14 14 import { requireGod } from '../middleware/auth.js'; 15 15 import { premiumUnlocked } from '../services/PatreonService.js'; 16 import { getStats } from '../services/StatsService.js';16 import { getStats, currentIp, getExcludedIps, setExcludedIps } from '../services/StatsService.js'; 17 17 18 18 const router = express.Router(); … … 32 32 } 33 33 const days = [7, 14, 30, 90].includes(parseInt(req.query.days, 10)) ? parseInt(req.query.days, 10) : 14; 34 const myIp = currentIp(req); 34 35 renderPage(req, res, 'pages/admin-stats', { 35 36 pageTitleKey: 'admin.t_stats', … … 37 38 stats: getStats(days), 38 39 linkClicks, 40 myIp, 41 ipExcluded: !!myIp && getExcludedIps().includes(myIp), 39 42 }); 40 43 }); 41 44 45 // Toggle whether the admin's current IP is counted in statistics. 46 router.post('/exclude-ip', requireGod, (req, res) => { 47 const ip = currentIp(req); 48 if (ip) { 49 const list = getExcludedIps(); 50 const i = list.indexOf(ip); 51 if (i >= 0) list.splice(i, 1); else list.push(ip); 52 setExcludedIps(list); 53 } 54 res.redirect('/admin/stats'); 55 }); 56 42 57 export default router; -
src/services/StatsService.js
r65a0697 re3a5aed 54 54 } 55 55 56 // The client IP (trust-proxy gives the real one), normalised: drop an IPv4-mapped-IPv6 prefix 57 // and a trailing :port so it matches what the admin sees + stores. 58 function clientIp(req) { 59 let ip = (req && (req.ip || (req.socket && req.socket.remoteAddress))) || ''; 60 if (ip.startsWith('::ffff:')) ip = ip.slice(7); 61 if (/^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(ip)) ip = ip.split(':')[0]; 62 return ip; 63 } 64 export function currentIp(req) { return clientIp(req); } 65 66 // Admin-configured IPs to skip — so an owner browsing logged-OUT (incognito, another browser) 67 // doesn't inflate their own stats. Stored as a comma-separated app_setting. 68 export function getExcludedIps() { 69 return (getSetting('stats_exclude_ips', '') || '').split(',').map((s) => s.trim()).filter(Boolean); 70 } 71 export function setExcludedIps(list) { 72 const clean = [...new Set((list || []).map((s) => String(s).trim()).filter(Boolean))].slice(0, 20); 73 setSetting('stats_exclude_ips', clean.join(',')); 74 } 75 function isExcludedIp(req) { 76 try { const ip = clientIp(req); return !!ip && getExcludedIps().includes(ip); } catch { return false; } 77 } 78 56 79 // Lazy prepares — tables only exist after initializeDatabase(); this module is 57 80 // imported before that call. … … 90 113 91 114 export function recordPageview(siteId, req) { 92 if (!siteId || isOperator(req) || isBot(req) ) return;115 if (!siteId || isOperator(req) || isBot(req) || isExcludedIp(req)) return; 93 116 try { 94 117 const d = today(); … … 100 123 101 124 export function recordPostView(post, req) { 102 if (!post || !post.id || isOperator(req) || isBot(req) ) return;125 if (!post || !post.id || isOperator(req) || isBot(req) || isExcludedIp(req)) return; 103 126 try { 104 127 stmts().bumpPost.run(post.id); -
src/services/i18n.js
r65a0697 re3a5aed 356 356 'astat.title': 'Statistieken', 357 357 'astat.intro': 'Cookievrij gemeten — geen tracking-cookies, geen toestemmingsbanner. Bezoekers worden per dag geteld via een dagelijks roterende, anonieme hash (IP/browser worden niet bewaard). Je eigen beheerder-bezoeken en bekende bots/crawlers tellen niet mee.', 358 'astat.your_ip': 'Jouw IP', 'astat.ip_counted': 'wordt meegeteld.', 'astat.ip_not_counted': 'wordt NIET meegeteld.', 'astat.ip_exclude': 'Tel mijn bezoeken niet mee', 'astat.ip_count': 'Wel meetellen', 358 359 'astat.visitor_days': 'Bezoeker-dagen ({n}d)', 359 360 'astat.pageviews_days': 'Weergaven ({n}d)', … … 1274 1275 'astat.title': 'Statistics', 1275 1276 'astat.intro': 'Measured cookie-free — no tracking cookies, no consent banner. Visitors are counted per day via a daily-rotating, anonymous hash (IP/browser are not stored). Your own admin visits and known bots/crawlers are not counted.', 1277 'astat.your_ip': 'Your IP', 'astat.ip_counted': 'is being counted.', 'astat.ip_not_counted': 'is NOT counted.', 'astat.ip_exclude': "Don't count my visits", 'astat.ip_count': 'Count my visits', 1276 1278 'astat.visitor_days': 'Visitor-days ({n}d)', 1277 1279 'astat.pageviews_days': 'Views ({n}d)', … … 2191 2193 'astat.title': 'Statistiken', 2192 2194 'astat.intro': 'Cookiefrei gemessen — keine Tracking-Cookies, kein Zustimmungsbanner. Besucher werden pro Tag über einen täglich rotierenden, anonymen Hash gezählt (IP/Browser werden nicht gespeichert). Deine eigenen Admin-Besuche und bekannte Bots/Crawler werden nicht mitgezählt.', 2195 'astat.your_ip': 'Deine IP', 'astat.ip_counted': 'wird mitgezählt.', 'astat.ip_not_counted': 'wird NICHT mitgezählt.', 'astat.ip_exclude': 'Meine Besuche nicht zählen', 'astat.ip_count': 'Meine Besuche zählen', 2193 2196 'astat.visitor_days': 'Besuchertage ({n}T)', 2194 2197 'astat.pageviews_days': 'Aufrufe ({n}T)', -
src/views/pages/admin-stats.ejs
r65a0697 re3a5aed 3 3 <h1><%= t('astat.title') %></h1> 4 4 <p class="stats-note"><%= t('astat.intro') %></p> 5 6 <% if (typeof myIp !== 'undefined' && myIp) { %> 7 <form method="post" action="/admin/stats/exclude-ip" class="stats-note" style="margin:.2rem 0 1.2rem;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;"> 8 <span><%= t('astat.your_ip') %> <code><%= myIp %></code> — 9 <% if (ipExcluded) { %><strong><%= t('astat.ip_not_counted') %></strong><% } else { %><%= t('astat.ip_counted') %><% } %></span> 10 <button type="submit" class="btn" style="padding:3px 10px;font-size:.85rem;"><%= ipExcluded ? t('astat.ip_count') : t('astat.ip_exclude') %></button> 11 </form> 12 <% } %> 5 13 6 14 <% var s = stats; var _max = Math.max(1, ...s.series.map(function(d){ return d.pageviews; })); %>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)