xref: /plugin/botmon/script.js (revision 0c03961572c9f505f27467430b8d67632cbd6df3)
143d9de6bSSascha Leib"use strict";
27bd08c30SSascha Leib/* DokuWiki BotMon Plugin Script file */
35526d629SSascha Leib/* 06.09.2025 - 0.2.0 - beta */
45526d629SSascha Leib/* Author: Sascha Leib <ad@hominem.info> */
5f125bc8dSSascha Leib
6f4417fdeSSascha Leib// enumeration of user types:
7f4417fdeSSascha Leibconst BM_USERTYPE = Object.freeze({
8f4417fdeSSascha Leib	'UNKNOWN': 'unknown',
9f4417fdeSSascha Leib	'KNOWN_USER': 'user',
10f4417fdeSSascha Leib	'HUMAN': 'human',
11f4417fdeSSascha Leib	'LIKELY_BOT': 'likely_bot',
12f4417fdeSSascha Leib	'KNOWN_BOT': 'known_bot'
13f4417fdeSSascha Leib});
14f4417fdeSSascha Leib
15f4417fdeSSascha Leib/* BotMon root object */
167bd08c30SSascha Leibconst BotMon = {
17f125bc8dSSascha Leib
18f125bc8dSSascha Leib	init: function() {
1993a5b18bSSascha Leib		//console.info('BotMon.init()');
20f125bc8dSSascha Leib
21f125bc8dSSascha Leib		// find the plugin basedir:
22f125bc8dSSascha Leib		this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/'))
237bd08c30SSascha Leib			+ '/plugins/botmon/';
24f125bc8dSSascha Leib
25f125bc8dSSascha Leib		// read the page language from the DOM:
26f125bc8dSSascha Leib		this._lang = document.getRootNode().documentElement.lang || this._lang;
27f125bc8dSSascha Leib
28f125bc8dSSascha Leib		// get the time offset:
297bd08c30SSascha Leib		this._timeDiff = BotMon.t._getTimeOffset();
30f125bc8dSSascha Leib
31f125bc8dSSascha Leib		// init the sub-objects:
327bd08c30SSascha Leib		BotMon.t._callInit(this);
33f125bc8dSSascha Leib	},
34f125bc8dSSascha Leib
35f125bc8dSSascha Leib	_baseDir: null,
36f125bc8dSSascha Leib	_lang: 'en',
37f125bc8dSSascha Leib	_today: (new Date()).toISOString().slice(0, 10),
38f125bc8dSSascha Leib	_timeDiff: '',
39f125bc8dSSascha Leib
40f125bc8dSSascha Leib	/* internal tools */
41f125bc8dSSascha Leib	t: {
42f125bc8dSSascha Leib		/* helper function to call inits of sub-objects */
43f125bc8dSSascha Leib		_callInit: function(obj) {
447bd08c30SSascha Leib			//console.info('BotMon.t._callInit(obj=',obj,')');
45f125bc8dSSascha Leib
46f125bc8dSSascha Leib			/* call init / _init on each sub-object: */
47f125bc8dSSascha Leib			Object.keys(obj).forEach( (key,i) => {
48f125bc8dSSascha Leib				const sub = obj[key];
49f125bc8dSSascha Leib				let init = null;
50f125bc8dSSascha Leib				if (typeof sub === 'object' && sub.init) {
51f125bc8dSSascha Leib					init = sub.init;
52f125bc8dSSascha Leib				}
53f125bc8dSSascha Leib
54f125bc8dSSascha Leib				// bind to object
55f125bc8dSSascha Leib				if (typeof init == 'function') {
56f125bc8dSSascha Leib					const init2 = init.bind(sub);
57f125bc8dSSascha Leib					init2(obj);
58f125bc8dSSascha Leib				}
59f125bc8dSSascha Leib			});
60f125bc8dSSascha Leib		},
61f125bc8dSSascha Leib
62f125bc8dSSascha Leib		/* helper function to calculate the time difference to UTC: */
63f125bc8dSSascha Leib		_getTimeOffset: function() {
64f125bc8dSSascha Leib			const now = new Date();
65f125bc8dSSascha Leib			let offset = now.getTimezoneOffset(); // in minutes
66f125bc8dSSascha Leib			const sign = Math.sign(offset); // +1 or -1
67f125bc8dSSascha Leib			offset = Math.abs(offset); // always positive
68f125bc8dSSascha Leib
69f125bc8dSSascha Leib			let hours = 0;
70f125bc8dSSascha Leib			while (offset >= 60) {
71f125bc8dSSascha Leib				hours += 1;
72f125bc8dSSascha Leib				offset -= 60;
73f125bc8dSSascha Leib			}
74f125bc8dSSascha Leib			return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : '');
7593a5b18bSSascha Leib		},
7693a5b18bSSascha Leib
7793a5b18bSSascha Leib		/* helper function to create a new element with all attributes and text content */
7893a5b18bSSascha Leib		_makeElement: function(name, atlist = undefined, text = undefined) {
7993a5b18bSSascha Leib			var r = null;
8093a5b18bSSascha Leib			try {
8193a5b18bSSascha Leib				r = document.createElement(name);
8293a5b18bSSascha Leib				if (atlist) {
8393a5b18bSSascha Leib					for (let attr in atlist) {
8493a5b18bSSascha Leib						r.setAttribute(attr, atlist[attr]);
8593a5b18bSSascha Leib					}
8693a5b18bSSascha Leib				}
8793a5b18bSSascha Leib				if (text) {
8893a5b18bSSascha Leib					r.textContent = text.toString();
8993a5b18bSSascha Leib				}
9093a5b18bSSascha Leib			} catch(e) {
9193a5b18bSSascha Leib				console.error(e);
9293a5b18bSSascha Leib			}
9393a5b18bSSascha Leib			return r;
945526d629SSascha Leib		},
955526d629SSascha Leib
965526d629SSascha Leib		/* helper to convert an ip address string to a normalised format: */
975526d629SSascha Leib		_ip2Num: function(ip) {
985526d629SSascha Leib			if (ip.indexOf(':') > 0) { /* IP6 */
995526d629SSascha Leib				return (ip.split(':').map(d => ('0000'+d).slice(-4) ).join(''));
1005526d629SSascha Leib			} else { /* IP4 */
1015526d629SSascha Leib				return Number(ip.split('.').map(d => ('000'+d).slice(-3) ).join(''));
1025526d629SSascha Leib			}
103446aa816SSascha Leib		},
104446aa816SSascha Leib
105446aa816SSascha Leib		/* helper function to format a Date object to show only the time. */
106446aa816SSascha Leib		/* returns String */
107446aa816SSascha Leib		_formatTime: function(date) {
108446aa816SSascha Leib
109446aa816SSascha Leib			if (date) {
110446aa816SSascha Leib				return ('0'+date.getHours()).slice(-2) + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2);
111446aa816SSascha Leib			} else {
112446aa816SSascha Leib				return null;
113446aa816SSascha Leib			}
114446aa816SSascha Leib
115446aa816SSascha Leib		},
116446aa816SSascha Leib
117446aa816SSascha Leib		/* helper function to show a time difference in seconds or minutes */
118446aa816SSascha Leib		/* returns String */
119446aa816SSascha Leib		_formatTimeDiff: function(dateA, dateB) {
120446aa816SSascha Leib
121446aa816SSascha Leib			// if the second date is ealier, swap them:
122446aa816SSascha Leib			if (dateA > dateB) dateB = [dateA, dateA = dateB][0];
123446aa816SSascha Leib
124446aa816SSascha Leib			// get the difference in milliseconds:
125446aa816SSascha Leib			let ms = dateB - dateA;
126446aa816SSascha Leib
127446aa816SSascha Leib			if (ms > 50) { /* ignore small time spans */
128446aa816SSascha Leib				const h = Math.floor((ms / (1000 * 60 * 60)) % 24);
129446aa816SSascha Leib				const m = Math.floor((ms / (1000 * 60)) % 60);
130446aa816SSascha Leib				const s = Math.floor((ms / 1000) % 60);
131446aa816SSascha Leib
132446aa816SSascha Leib				return ( h>0 ? h + 'h ': '') + ( m>0 ? m + 'm ': '') + ( s>0 ? s + 's': '');
133446aa816SSascha Leib			}
134446aa816SSascha Leib
135446aa816SSascha Leib			return null;
136446aa816SSascha Leib
137f125bc8dSSascha Leib		}
138f125bc8dSSascha Leib	}
139c7931771SSascha Leib};
140f125bc8dSSascha Leib
1419f1ee8c1SSascha Leib/* everything specific to the "Today" tab is self-contained in the "live" object: */
1427bd08c30SSascha LeibBotMon.live = {
143f125bc8dSSascha Leib	init: function() {
1447bd08c30SSascha Leib		//console.info('BotMon.live.init()');
145f125bc8dSSascha Leib
146f125bc8dSSascha Leib		// set the title:
1477bd08c30SSascha Leib		const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')';
1487bd08c30SSascha Leib		BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</time> ${tDiff}`);
149f125bc8dSSascha Leib
150f125bc8dSSascha Leib		// init sub-objects:
1517bd08c30SSascha Leib		BotMon.t._callInit(this);
152f125bc8dSSascha Leib	},
153f125bc8dSSascha Leib
154f125bc8dSSascha Leib	data: {
155f125bc8dSSascha Leib		init: function() {
1567bd08c30SSascha Leib			//console.info('BotMon.live.data.init()');
157f125bc8dSSascha Leib
158f125bc8dSSascha Leib			// call sub-inits:
1597bd08c30SSascha Leib			BotMon.t._callInit(this);
1609f1ee8c1SSascha Leib		},
161f125bc8dSSascha Leib
1629f1ee8c1SSascha Leib		// this will be called when the known json files are done loading:
1639f1ee8c1SSascha Leib		_dispatch: function(file) {
1647bd08c30SSascha Leib			//console.info('BotMon.live.data._dispatch(,',file,')');
1659f1ee8c1SSascha Leib
1669f1ee8c1SSascha Leib			// shortcut to make code more readable:
1677bd08c30SSascha Leib			const data = BotMon.live.data;
1689f1ee8c1SSascha Leib
1699f1ee8c1SSascha Leib			// set the flags:
1709f1ee8c1SSascha Leib			switch(file) {
1715526d629SSascha Leib				case 'rules':
1725526d629SSascha Leib					data._dispatchRulesLoaded = true;
1735526d629SSascha Leib					break;
1749f1ee8c1SSascha Leib				case 'bots':
1759f1ee8c1SSascha Leib					data._dispatchBotsLoaded = true;
1769f1ee8c1SSascha Leib					break;
1779f1ee8c1SSascha Leib				case 'clients':
1789f1ee8c1SSascha Leib					data._dispatchClientsLoaded = true;
1799f1ee8c1SSascha Leib					break;
1809f1ee8c1SSascha Leib				case 'platforms':
1819f1ee8c1SSascha Leib					data._dispatchPlatformsLoaded = true;
1829f1ee8c1SSascha Leib					break;
1839f1ee8c1SSascha Leib				default:
1849f1ee8c1SSascha Leib					// ignore
1859f1ee8c1SSascha Leib			}
1869f1ee8c1SSascha Leib
1879f1ee8c1SSascha Leib			// are all the flags set?
188b82cba27SSascha Leib			if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded && data._dispatchRulesLoaded) {
1899f1ee8c1SSascha Leib				// chain the log files loading:
1907bd08c30SSascha Leib				BotMon.live.data.loadLogFile('srv', BotMon.live.data._onServerLogLoaded);
1919f1ee8c1SSascha Leib			}
1922f2bc93aSSascha Leib		},
1939f1ee8c1SSascha Leib		// flags to track which data files have been loaded:
1949f1ee8c1SSascha Leib		_dispatchBotsLoaded: false,
1959f1ee8c1SSascha Leib		_dispatchClientsLoaded: false,
1969f1ee8c1SSascha Leib		_dispatchPlatformsLoaded: false,
197b82cba27SSascha Leib		_dispatchRulesLoaded: false,
1982f2bc93aSSascha Leib
1999f1ee8c1SSascha Leib		// event callback, after the server log has been loaded:
2002f2bc93aSSascha Leib		_onServerLogLoaded: function() {
2017bd08c30SSascha Leib			//console.info('BotMon.live.data._onServerLogLoaded()');
2022f2bc93aSSascha Leib
2039f1ee8c1SSascha Leib			// chain the client log file to load:
2047bd08c30SSascha Leib			BotMon.live.data.loadLogFile('log', BotMon.live.data._onClientLogLoaded);
2052f2bc93aSSascha Leib		},
2062f2bc93aSSascha Leib
2079f1ee8c1SSascha Leib		// event callback, after the client log has been loaded:
2082f2bc93aSSascha Leib		_onClientLogLoaded: function() {
20993a5b18bSSascha Leib			//console.info('BotMon.live.data._onClientLogLoaded()');
2102f2bc93aSSascha Leib
2119f1ee8c1SSascha Leib			// chain the ticks file to load:
2127bd08c30SSascha Leib			BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded);
2132f2bc93aSSascha Leib
2149f1ee8c1SSascha Leib		},
2159f1ee8c1SSascha Leib
2169f1ee8c1SSascha Leib		// event callback, after the tiker log has been loaded:
2179f1ee8c1SSascha Leib		_onTicksLogLoaded: function() {
21893a5b18bSSascha Leib			//console.info('BotMon.live.data._onTicksLogLoaded()');
2199f1ee8c1SSascha Leib
2209f1ee8c1SSascha Leib			// analyse the data:
2217bd08c30SSascha Leib			BotMon.live.data.analytics.analyseAll();
2229f1ee8c1SSascha Leib
2239f1ee8c1SSascha Leib			// sort the data:
2249f1ee8c1SSascha Leib			// #TODO
2259f1ee8c1SSascha Leib
2269f1ee8c1SSascha Leib			// display the data:
2277bd08c30SSascha Leib			BotMon.live.gui.overview.make();
2289f1ee8c1SSascha Leib
22993a5b18bSSascha Leib			//console.log(BotMon.live.data.model._visitors);
2309f1ee8c1SSascha Leib
2312f2bc93aSSascha Leib		},
2322f2bc93aSSascha Leib
2332f2bc93aSSascha Leib		model: {
2349f1ee8c1SSascha Leib			// visitors storage:
2352f2bc93aSSascha Leib			_visitors: [],
2362f2bc93aSSascha Leib
2372f2bc93aSSascha Leib			// find an already existing visitor record:
238f4417fdeSSascha Leib			findVisitor: function(visitor) {
239f4417fdeSSascha Leib				//console.info('BotMon.live.data.model.findVisitor()');
240f4417fdeSSascha Leib				//console.log(visitor);
2412f2bc93aSSascha Leib
2422f2bc93aSSascha Leib				// shortcut to make code more readable:
2437bd08c30SSascha Leib				const model = BotMon.live.data.model;
2442f2bc93aSSascha Leib
245446aa816SSascha Leib				const timeout = 60 * 60 * 1000; /* session timeout: One hour */
246446aa816SSascha Leib
2472f2bc93aSSascha Leib				// loop over all visitors already registered:
2482f2bc93aSSascha Leib				for (let i=0; i<model._visitors.length; i++) {
2492f2bc93aSSascha Leib					const v = model._visitors[i];
250f4417fdeSSascha Leib
251f4417fdeSSascha Leib					if (visitor._type == BM_USERTYPE.KNOWN_BOT) { /* known bots */
252f4417fdeSSascha Leib
253f4417fdeSSascha Leib						// bots match when their ID matches:
254f4417fdeSSascha Leib						if (v._bot && v._bot.id == visitor._bot.id) {
255f4417fdeSSascha Leib							return v;
256f4417fdeSSascha Leib						}
257f4417fdeSSascha Leib
258f4417fdeSSascha Leib					} else if (visitor._type == BM_USERTYPE.KNOWN_USER) { /* registered users */
259f4417fdeSSascha Leib
260f4417fdeSSascha Leib						// visitors match when their names match:
261f4417fdeSSascha Leib						if ( v.usr == visitor.usr
262f4417fdeSSascha Leib						&& v.ip == visitor.ip
263f4417fdeSSascha Leib						&& v.agent == visitor.agent) {
264f4417fdeSSascha Leib							return v;
265f4417fdeSSascha Leib						}
266f4417fdeSSascha Leib					} else { /* any other visitor */
267f4417fdeSSascha Leib
2685d78a53bSSascha Leib						if (Math.abs(v._lastSeen - visitor.ts) < timeout) { /* ignore timed out visits */
269f4417fdeSSascha Leib							if ( v.id == visitor.id) { /* match the pre-defined IDs */
270f4417fdeSSascha Leib								return v;
271259d3b85SSascha Leib							} else if (v.ip == visitor.ip && v.agent == visitor.agent) {
272451abfadSSascha Leib								if (v.typ !== 'ip') {
2735526d629SSascha Leib									console.warn(`Visitor ID “${v.id}” not found, using matchin IP + User-Agent instead.`);
274451abfadSSascha Leib								}
275259d3b85SSascha Leib								return v;
276f4417fdeSSascha Leib							}
277f4417fdeSSascha Leib						}
2785d78a53bSSascha Leib
2792f2bc93aSSascha Leib					}
280446aa816SSascha Leib				}
2812f2bc93aSSascha Leib				return null; // nothing found
2822f2bc93aSSascha Leib			},
2832f2bc93aSSascha Leib
284259d3b85SSascha Leib			/* if there is already this visit registered, return the page view item */
285259d3b85SSascha Leib			_getPageView: function(visit, view) {
2862f2bc93aSSascha Leib
287294f6af8SSascha Leib				// shortcut to make code more readable:
2887bd08c30SSascha Leib				const model = BotMon.live.data.model;
289294f6af8SSascha Leib
2902f2bc93aSSascha Leib				for (let i=0; i<visit._pageViews.length; i++) {
291294f6af8SSascha Leib					const pv = visit._pageViews[i];
292259d3b85SSascha Leib					if (pv.pg == view.pg) {
293259d3b85SSascha Leib						return pv;
2942f2bc93aSSascha Leib					}
2952f2bc93aSSascha Leib				}
2962f2bc93aSSascha Leib				return null; // not found
2972f2bc93aSSascha Leib			},
2982f2bc93aSSascha Leib
2992f2bc93aSSascha Leib			// register a new visitor (or update if already exists)
300f4417fdeSSascha Leib			registerVisit: function(nv, type) {
301f4417fdeSSascha Leib				//console.info('registerVisit', nv, type);
3022f2bc93aSSascha Leib
3032f2bc93aSSascha Leib				// shortcut to make code more readable:
3047bd08c30SSascha Leib				const model = BotMon.live.data.model;
3052f2bc93aSSascha Leib
30643d9de6bSSascha Leib				// is it a known bot?
307f4417fdeSSascha Leib				const bot = BotMon.live.data.bots.match(nv.agent);
3089f1ee8c1SSascha Leib
309f4417fdeSSascha Leib				// enrich new visitor with relevant data:
310f4417fdeSSascha Leib				if (!nv._bot) nv._bot = bot ?? null; // bot info
311259d3b85SSascha Leib				nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) );
312f4417fdeSSascha Leib				if (!nv._firstSeen) nv._firstSeen = nv.ts;
313259d3b85SSascha Leib				nv._lastSeen = nv.ts;
31443d9de6bSSascha Leib
315abfc901fSSascha Leib				// check if it already exists:
316f4417fdeSSascha Leib				let visitor = model.findVisitor(nv);
317abfc901fSSascha Leib				if (!visitor) {
318f4417fdeSSascha Leib					visitor = nv;
31943d9de6bSSascha Leib					visitor._seenBy = [type];
3202f2bc93aSSascha Leib					visitor._pageViews = []; // array of page views
3212f2bc93aSSascha Leib					visitor._hasReferrer = false; // has at least one referrer
3222f2bc93aSSascha Leib					visitor._jsClient = false; // visitor has been seen logged by client js as well
323f4417fdeSSascha Leib					visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info
324f4417fdeSSascha Leib					visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info
325f4417fdeSSascha Leib					model._visitors.push(visitor);
326451abfadSSascha Leib				} else { // update existing
327446aa816SSascha Leib					if (visitor._firstSeen > nv.ts) {
328451abfadSSascha Leib						visitor._firstSeen = nv.ts;
329451abfadSSascha Leib					}
3302f2bc93aSSascha Leib				}
3312f2bc93aSSascha Leib
3329f1ee8c1SSascha Leib				// find browser
3339f1ee8c1SSascha Leib
3342f2bc93aSSascha Leib				// is this visit already registered?
335259d3b85SSascha Leib				let prereg = model._getPageView(visitor, nv);
3362f2bc93aSSascha Leib				if (!prereg) {
337259d3b85SSascha Leib					// add new page view:
338259d3b85SSascha Leib					prereg = model._makePageView(nv, type);
3392f2bc93aSSascha Leib					visitor._pageViews.push(prereg);
340259d3b85SSascha Leib				} else {
341259d3b85SSascha Leib					// update last seen date
342259d3b85SSascha Leib					prereg._lastSeen = nv.ts;
343259d3b85SSascha Leib					// increase view count:
344259d3b85SSascha Leib					prereg._viewCount += 1;
345446aa816SSascha Leib					prereg._tickCount += 1;
3462f2bc93aSSascha Leib				}
3472f2bc93aSSascha Leib
3482f2bc93aSSascha Leib				// update referrer state:
3492f2bc93aSSascha Leib				visitor._hasReferrer = visitor._hasReferrer ||
3502f2bc93aSSascha Leib					(prereg.ref !== undefined && prereg.ref !== '');
3512f2bc93aSSascha Leib
3522f2bc93aSSascha Leib				// update time stamp for last-seen:
353451abfadSSascha Leib				if (visitor._lastSeen < nv.ts) {
354f4417fdeSSascha Leib					visitor._lastSeen = nv.ts;
355451abfadSSascha Leib				}
3569f1ee8c1SSascha Leib
3579f1ee8c1SSascha Leib				// if needed:
3589f1ee8c1SSascha Leib				return visitor;
3592f2bc93aSSascha Leib			},
3602f2bc93aSSascha Leib
3612f2bc93aSSascha Leib			// updating visit data from the client-side log:
3622f2bc93aSSascha Leib			updateVisit: function(dat) {
3639f1ee8c1SSascha Leib				//console.info('updateVisit', dat);
3642f2bc93aSSascha Leib
3652f2bc93aSSascha Leib				// shortcut to make code more readable:
3667bd08c30SSascha Leib				const model = BotMon.live.data.model;
3672f2bc93aSSascha Leib
36843d9de6bSSascha Leib				const type = 'log';
36943d9de6bSSascha Leib
370f4417fdeSSascha Leib				let visitor = BotMon.live.data.model.findVisitor(dat);
3719f1ee8c1SSascha Leib				if (!visitor) {
37243d9de6bSSascha Leib					visitor = model.registerVisit(dat, type);
3739f1ee8c1SSascha Leib				}
3742f2bc93aSSascha Leib				if (visitor) {
37543d9de6bSSascha Leib
376446aa816SSascha Leib					if (visitor._lastSeen < dat.ts) {
3772f2bc93aSSascha Leib						visitor._lastSeen = dat.ts;
378446aa816SSascha Leib					}
379259d3b85SSascha Leib					if (!visitor._seenBy.includes(type)) {
380259d3b85SSascha Leib						visitor._seenBy.push(type);
381259d3b85SSascha Leib					}
3822f2bc93aSSascha Leib					visitor._jsClient = true; // seen by client js
3832f2bc93aSSascha Leib				}
3842f2bc93aSSascha Leib
3852f2bc93aSSascha Leib				// find the page view:
386259d3b85SSascha Leib				let prereg = BotMon.live.data.model._getPageView(visitor, dat);
3872f2bc93aSSascha Leib				if (prereg) {
3882f2bc93aSSascha Leib					// update the page view:
3892f2bc93aSSascha Leib					prereg._lastSeen = dat.ts;
390259d3b85SSascha Leib					if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type);
3912f2bc93aSSascha Leib					prereg._jsClient = true; // seen by client js
3922f2bc93aSSascha Leib				} else {
3932f2bc93aSSascha Leib					// add the page view to the visitor:
394259d3b85SSascha Leib					prereg = model._makePageView(dat, type);
3952f2bc93aSSascha Leib					visitor._pageViews.push(prereg);
3962f2bc93aSSascha Leib				}
397446aa816SSascha Leib				prereg._tickCount += 1;
3989f1ee8c1SSascha Leib			},
3999f1ee8c1SSascha Leib
4009f1ee8c1SSascha Leib			// updating visit data from the ticker log:
4019f1ee8c1SSascha Leib			updateTicks: function(dat) {
402294f6af8SSascha Leib				//console.info('updateTicks', dat);
4039f1ee8c1SSascha Leib
4049f1ee8c1SSascha Leib				// shortcut to make code more readable:
4057bd08c30SSascha Leib				const model = BotMon.live.data.model;
4069f1ee8c1SSascha Leib
407259d3b85SSascha Leib				const type = 'tck';
408259d3b85SSascha Leib
4099f1ee8c1SSascha Leib				// find the visit info:
410f4417fdeSSascha Leib				let visitor = model.findVisitor(dat);
411294f6af8SSascha Leib				if (!visitor) {
4125526d629SSascha Leib					console.info(`No visitor with ID “${dat.id}” found, registering as a new one.`);
413259d3b85SSascha Leib					visitor = model.registerVisit(dat, type);
414294f6af8SSascha Leib				}
4159f1ee8c1SSascha Leib				if (visitor) {
41643d9de6bSSascha Leib					// update visitor:
417294f6af8SSascha Leib					if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts;
418259d3b85SSascha Leib					if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type);
419294f6af8SSascha Leib
420294f6af8SSascha Leib					// get the page view info:
421259d3b85SSascha Leib					let pv = model._getPageView(visitor, dat);
422259d3b85SSascha Leib					if (!pv) {
423446aa816SSascha Leib						console.warn(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`);
424259d3b85SSascha Leib						pv = model._makePageView(dat, type);
425259d3b85SSascha Leib						visitor._pageViews.push(pv);
426259d3b85SSascha Leib					}
427294f6af8SSascha Leib
428259d3b85SSascha Leib					// update the page view info:
429259d3b85SSascha Leib					if (!pv._seenBy.includes(type)) pv._seenBy.push(type);
430259d3b85SSascha Leib					if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts;
431259d3b85SSascha Leib					pv._tickCount += 1;
432259d3b85SSascha Leib
433259d3b85SSascha Leib				}
434259d3b85SSascha Leib			},
435259d3b85SSascha Leib
436259d3b85SSascha Leib			// helper function to create a new "page view" item:
437259d3b85SSascha Leib			_makePageView: function(data, type) {
4385526d629SSascha Leib
4395526d629SSascha Leib				// try to parse the referrer:
4405526d629SSascha Leib				let rUrl = null;
4415526d629SSascha Leib				try {
4425526d629SSascha Leib					rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null );
4435526d629SSascha Leib				} catch (e) {
4445526d629SSascha Leib					console.info(`Invalid referer: “${data.ref}”.`);
4455526d629SSascha Leib				}
4465526d629SSascha Leib
447259d3b85SSascha Leib				return {
448259d3b85SSascha Leib					_by: type,
449259d3b85SSascha Leib					ip: data.ip,
450259d3b85SSascha Leib					pg: data.pg,
4515526d629SSascha Leib					_ref: rUrl,
452259d3b85SSascha Leib					_firstSeen: data.ts,
453259d3b85SSascha Leib					_lastSeen: data.ts,
454259d3b85SSascha Leib					_seenBy: [type],
455259d3b85SSascha Leib					_jsClient: ( type !== 'srv'),
456259d3b85SSascha Leib					_viewCount: 1,
457259d3b85SSascha Leib					_tickCount: 0
458294f6af8SSascha Leib				};
4592f2bc93aSSascha Leib			}
460f125bc8dSSascha Leib		},
461f125bc8dSSascha Leib
462294f6af8SSascha Leib		analytics: {
463294f6af8SSascha Leib
464294f6af8SSascha Leib			init: function() {
465f4417fdeSSascha Leib				//console.info('BotMon.live.data.analytics.init()');
466294f6af8SSascha Leib			},
467294f6af8SSascha Leib
468294f6af8SSascha Leib			// data storage:
469294f6af8SSascha Leib			data: {
470294f6af8SSascha Leib				totalVisits: 0,
471294f6af8SSascha Leib				totalPageViews: 0,
472294f6af8SSascha Leib				bots: {
473294f6af8SSascha Leib					known: 0,
47493a5b18bSSascha Leib					suspected: 0,
4759bc80cc5SSascha Leib					human: 0,
4769bc80cc5SSascha Leib					users: 0
477294f6af8SSascha Leib				}
478294f6af8SSascha Leib			},
479294f6af8SSascha Leib
480294f6af8SSascha Leib			// sort the visits by type:
481294f6af8SSascha Leib			groups: {
482294f6af8SSascha Leib				knownBots: [],
48393a5b18bSSascha Leib				suspectedBots: [],
484294f6af8SSascha Leib				humans: [],
485294f6af8SSascha Leib				users: []
486294f6af8SSascha Leib			},
487294f6af8SSascha Leib
488294f6af8SSascha Leib			// all analytics
489294f6af8SSascha Leib			analyseAll: function() {
4907bd08c30SSascha Leib				//console.info('BotMon.live.data.analytics.analyseAll()');
491294f6af8SSascha Leib
492294f6af8SSascha Leib				// shortcut to make code more readable:
4937bd08c30SSascha Leib				const model = BotMon.live.data.model;
494446aa816SSascha Leib				const me = BotMon.live.data.analytics;
495294f6af8SSascha Leib
496451abfadSSascha Leib				BotMon.live.gui.status.showBusy("Analysing data …");
497451abfadSSascha Leib
498294f6af8SSascha Leib				// loop over all visitors:
499294f6af8SSascha Leib				model._visitors.forEach( (v) => {
500294f6af8SSascha Leib
501294f6af8SSascha Leib					// count visits and page views:
502294f6af8SSascha Leib					this.data.totalVisits += 1;
503294f6af8SSascha Leib					this.data.totalPageViews += v._pageViews.length;
504294f6af8SSascha Leib
505294f6af8SSascha Leib					// check for typical bot aspects:
50643d9de6bSSascha Leib					let botScore = 0;
507294f6af8SSascha Leib
508f4417fdeSSascha Leib					if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots
509294f6af8SSascha Leib
510b82cba27SSascha Leib						this.data.bots.known += v._pageViews.length;
511294f6af8SSascha Leib						this.groups.knownBots.push(v);
512294f6af8SSascha Leib
513f4417fdeSSascha Leib					} else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */
514f4417fdeSSascha Leib
515b82cba27SSascha Leib						this.data.bots.users += v._pageViews.length;
516294f6af8SSascha Leib						this.groups.users.push(v);
517294f6af8SSascha Leib
518f4417fdeSSascha Leib					} else {
519294f6af8SSascha Leib
520b82cba27SSascha Leib						// get evaluation:
521b82cba27SSascha Leib						const e = BotMon.live.data.rules.evaluate(v);
522b82cba27SSascha Leib						v._eval = e.rules;
523b82cba27SSascha Leib						v._botVal = e.val;
524b82cba27SSascha Leib
525446aa816SSascha Leib						// add each page view to IP range information (unless it is already from a known bot IP range):
526446aa816SSascha Leib						v._pageViews.forEach( pv => {
527446aa816SSascha Leib							me._addToIPRanges(pv.ip);
528446aa816SSascha Leib						});
529446aa816SSascha Leib
530b82cba27SSascha Leib						if (e.isBot) { // likely bots
531b82cba27SSascha Leib							v._type = BM_USERTYPE.LIKELY_BOT;
532b82cba27SSascha Leib							this.data.bots.suspected += v._pageViews.length;
53393a5b18bSSascha Leib							this.groups.suspectedBots.push(v);
534b82cba27SSascha Leib						} else { // probably humans
535b82cba27SSascha Leib							v._type = BM_USERTYPE.HUMAN;
536b82cba27SSascha Leib							this.data.bots.human += v._pageViews.length;
537b82cba27SSascha Leib							this.groups.humans.push(v);
538b82cba27SSascha Leib						}
539b82cba27SSascha Leib						// TODO: find suspected bots
540f4417fdeSSascha Leib
541294f6af8SSascha Leib					}
542294f6af8SSascha Leib				});
543294f6af8SSascha Leib
5445d78a53bSSascha Leib				BotMon.live.gui.status.hideBusy('Done.');
545446aa816SSascha Leib			},
546446aa816SSascha Leib
547446aa816SSascha Leib			// visits from IP ranges:
548446aa816SSascha Leib			_ipRange: {
549446aa816SSascha Leib				ip4: [],
550446aa816SSascha Leib				ip6: []
551446aa816SSascha Leib			},
552446aa816SSascha Leib			/**
553446aa816SSascha Leib			 * Adds a visit to the IP range statistics.
554446aa816SSascha Leib			 *
555446aa816SSascha Leib			 * This helps to identify IP ranges that are used by bots.
556446aa816SSascha Leib			 *
557446aa816SSascha Leib			 * @param {string} ip The IP address to add.
558446aa816SSascha Leib			 */
559446aa816SSascha Leib			_addToIPRanges: function(ip) {
560446aa816SSascha Leib
561*0c039615SSascha Leib				// #TODO: handle nestled ranges!
562446aa816SSascha Leib				const me = BotMon.live.data.analytics;
563446aa816SSascha Leib				const ipv = (ip.indexOf(':') > 0 ? 6 : 4);
564446aa816SSascha Leib
565446aa816SSascha Leib				const ipArr = ip.split( ipv == 6 ? ':' : '.');
566446aa816SSascha Leib				const maxSegments = (ipv == 6 ? 4 : 3);
567446aa816SSascha Leib
568446aa816SSascha Leib				let arr = (ipv == 6 ? me._ipRange.ip6 : me._ipRange.ip4);
5695d78a53bSSascha Leib
5705d78a53bSSascha Leib				// find any existing segment entry:
5715d78a53bSSascha Leib				it = null;
5725d78a53bSSascha Leib				for (let i=0; i < arr.length; i++) {
5735d78a53bSSascha Leib					const sig = arr[i];
5745d78a53bSSascha Leib					if (sig.seg == ipArr[0]) {
5755d78a53bSSascha Leib						it = sig;
5765d78a53bSSascha Leib						break;
5775d78a53bSSascha Leib					}
5785d78a53bSSascha Leib				}
5795d78a53bSSascha Leib
5805d78a53bSSascha Leib				// create if not found:
581446aa816SSascha Leib				if (!it) {
5825d78a53bSSascha Leib					it = {seg: ipArr[0], count: 1};
5835d78a53bSSascha Leib					//if (i<maxSegments) it.sub = [];
584446aa816SSascha Leib					arr.push(it);
5855d78a53bSSascha Leib
5865d78a53bSSascha Leib				} else { // increase count:
5875d78a53bSSascha Leib
588446aa816SSascha Leib					it.count += 1;
589446aa816SSascha Leib				}
5905d78a53bSSascha Leib
591446aa816SSascha Leib			},
592*0c039615SSascha Leib			getTopBotIPRanges: function(max) {
593*0c039615SSascha Leib
594446aa816SSascha Leib				const me = BotMon.live.data.analytics;
595446aa816SSascha Leib
596*0c039615SSascha Leib				const kMinHits = 2;
597451abfadSSascha Leib
598*0c039615SSascha Leib				// combine the ip lists, removing all lower volume branches:
599*0c039615SSascha Leib				let ipTypes = [4,6];
600*0c039615SSascha Leib				const tmpList = [];
601*0c039615SSascha Leib				for (let i=0; i<ipTypes.length; i++) {
602*0c039615SSascha Leib					const ipType = ipTypes[i];
603*0c039615SSascha Leib					(ipType == 6 ? me._ipRange.ip6 : me._ipRange.ip4).forEach( it => {
604*0c039615SSascha Leib						if (it.count > kMinHits) {
605*0c039615SSascha Leib							it.type = ipType;
606*0c039615SSascha Leib							tmpList.push(it);
607*0c039615SSascha Leib						}
6085d78a53bSSascha Leib						});
609*0c039615SSascha Leib					tmpList.sort( (a,b) => b.count - a.count);
610*0c039615SSascha Leib				}
6115d78a53bSSascha Leib
612*0c039615SSascha Leib				// reduce to only the top (max) items and create the target format:
613*0c039615SSascha Leib				// #TODO: handle nestled ranges!
614*0c039615SSascha Leib				let rList = [];
615*0c039615SSascha Leib				for (let j=0; Math.min(max, tmpList.length) > j; j++) {
616*0c039615SSascha Leib					const rangeInfo = tmpList[j];
617*0c039615SSascha Leib					rList.push({
618*0c039615SSascha Leib						'ip': rangeInfo.seg + ( rangeInfo.type == 4 ? '.x.x.x' : '::x'),
619*0c039615SSascha Leib						'typ': rangeInfo.type,
620*0c039615SSascha Leib						'num': rangeInfo.count
621*0c039615SSascha Leib					});
622*0c039615SSascha Leib				}
623*0c039615SSascha Leib
624*0c039615SSascha Leib				return rList;
625446aa816SSascha Leib			}
626294f6af8SSascha Leib		},
627294f6af8SSascha Leib
628f125bc8dSSascha Leib		bots: {
629f125bc8dSSascha Leib			// loads the list of known bots from a JSON file:
630f125bc8dSSascha Leib			init: async function() {
6317bd08c30SSascha Leib				//console.info('BotMon.live.data.bots.init()');
632f125bc8dSSascha Leib
633f125bc8dSSascha Leib				// Load the list of known bots:
6347bd08c30SSascha Leib				BotMon.live.gui.status.showBusy("Loading known bots …");
635*0c039615SSascha Leib				const url = BotMon._baseDir + 'config/known-bots.json';
636f125bc8dSSascha Leib				try {
637f125bc8dSSascha Leib					const response = await fetch(url);
638f125bc8dSSascha Leib					if (!response.ok) {
639f125bc8dSSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
640f125bc8dSSascha Leib					}
641f125bc8dSSascha Leib
64243d9de6bSSascha Leib					this._list = await response.json();
64343d9de6bSSascha Leib					this._ready = true;
644f125bc8dSSascha Leib
645f125bc8dSSascha Leib				} catch (error) {
6467bd08c30SSascha Leib					BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message);
647f125bc8dSSascha Leib				} finally {
6487bd08c30SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
6497bd08c30SSascha Leib					BotMon.live.data._dispatch('bots')
650f125bc8dSSascha Leib				}
651f125bc8dSSascha Leib			},
652f125bc8dSSascha Leib
653f125bc8dSSascha Leib			// returns bot info if the clientId matches a known bot, null otherwise:
65443d9de6bSSascha Leib			match: function(agent) {
65543d9de6bSSascha Leib				//console.info('BotMon.live.data.bots.match(',agent,')');
656f125bc8dSSascha Leib
65743d9de6bSSascha Leib				const BotList = BotMon.live.data.bots._list;
65843d9de6bSSascha Leib
65943d9de6bSSascha Leib				// default is: not found!
66043d9de6bSSascha Leib				let botInfo = null;
66143d9de6bSSascha Leib
662fa1532fcSSascha Leib				if (!agent) return null;
663fa1532fcSSascha Leib
66443d9de6bSSascha Leib				// check for known bots:
66543d9de6bSSascha Leib				BotList.find(bot => {
66643d9de6bSSascha Leib					let r = false;
6679f1ee8c1SSascha Leib					for (let j=0; j<bot.rx.length; j++) {
66843d9de6bSSascha Leib						const rxr = agent.match(new RegExp(bot.rx[j]));
66943d9de6bSSascha Leib						if (rxr) {
67043d9de6bSSascha Leib							botInfo = {
67143d9de6bSSascha Leib								n : bot.n,
67243d9de6bSSascha Leib								id: bot.id,
67343d9de6bSSascha Leib								url: bot.url,
67443d9de6bSSascha Leib								v: (rxr.length > 1 ? rxr[1] : -1)
6750359f250SSascha Leib							};
67643d9de6bSSascha Leib							r = true;
67743d9de6bSSascha Leib							break;
678446aa816SSascha Leib						};
6790359f250SSascha Leib					};
68043d9de6bSSascha Leib					return r;
68143d9de6bSSascha Leib				});
682259d3b85SSascha Leib
683259d3b85SSascha Leib				// check for unknown bots:
684259d3b85SSascha Leib				if (!botInfo) {
685259d3b85SSascha Leib					const botmatch = agent.match(/[^\s](\w*bot)[\/\s;\),$]/i);
686259d3b85SSascha Leib					if(botmatch) {
687259d3b85SSascha Leib						botInfo = {'id': "other", 'n': "Other", "bot": botmatch[0] };
688259d3b85SSascha Leib					}
6899f1ee8c1SSascha Leib				}
69043d9de6bSSascha Leib
69143d9de6bSSascha Leib				//console.log("botInfo:", botInfo);
69243d9de6bSSascha Leib				return botInfo;
693f125bc8dSSascha Leib			},
694f125bc8dSSascha Leib
69543d9de6bSSascha Leib
696f125bc8dSSascha Leib			// indicates if the list is loaded and ready to use:
697f125bc8dSSascha Leib			_ready: false,
698f125bc8dSSascha Leib
699f125bc8dSSascha Leib			// the actual bot list is stored here:
700f125bc8dSSascha Leib			_list: []
701f125bc8dSSascha Leib		},
702f125bc8dSSascha Leib
7039f1ee8c1SSascha Leib		clients: {
7049f1ee8c1SSascha Leib			// loads the list of known clients from a JSON file:
7059f1ee8c1SSascha Leib			init: async function() {
7067bd08c30SSascha Leib				//console.info('BotMon.live.data.clients.init()');
7079f1ee8c1SSascha Leib
7089f1ee8c1SSascha Leib				// Load the list of known bots:
7097bd08c30SSascha Leib				BotMon.live.gui.status.showBusy("Loading known clients");
710*0c039615SSascha Leib				const url = BotMon._baseDir + 'config/known-clients.json';
7119f1ee8c1SSascha Leib				try {
7129f1ee8c1SSascha Leib					const response = await fetch(url);
7139f1ee8c1SSascha Leib					if (!response.ok) {
7149f1ee8c1SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
7159f1ee8c1SSascha Leib					}
7169f1ee8c1SSascha Leib
7177bd08c30SSascha Leib					BotMon.live.data.clients._list = await response.json();
7187bd08c30SSascha Leib					BotMon.live.data.clients._ready = true;
7199f1ee8c1SSascha Leib
7209f1ee8c1SSascha Leib				} catch (error) {
7217bd08c30SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message);
7229f1ee8c1SSascha Leib				} finally {
7237bd08c30SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
7247bd08c30SSascha Leib					BotMon.live.data._dispatch('clients')
7259f1ee8c1SSascha Leib				}
7269f1ee8c1SSascha Leib			},
7279f1ee8c1SSascha Leib
72893a5b18bSSascha Leib			// returns bot info if the user-agent matches a known bot, null otherwise:
72943d9de6bSSascha Leib			match: function(agent) {
73043d9de6bSSascha Leib				//console.info('BotMon.live.data.clients.match(',agent,')');
7319f1ee8c1SSascha Leib
7329f1ee8c1SSascha Leib				let match = {"n": "Unknown", "v": -1, "id": null};
7339f1ee8c1SSascha Leib
73443d9de6bSSascha Leib				if (agent) {
7357bd08c30SSascha Leib					BotMon.live.data.clients._list.find(client => {
7369f1ee8c1SSascha Leib						let r = false;
7379f1ee8c1SSascha Leib						for (let j=0; j<client.rx.length; j++) {
73843d9de6bSSascha Leib							const rxr = agent.match(new RegExp(client.rx[j]));
7399f1ee8c1SSascha Leib							if (rxr) {
7409f1ee8c1SSascha Leib								match.n = client.n;
7419f1ee8c1SSascha Leib								match.v = (rxr.length > 1 ? rxr[1] : -1);
7429f1ee8c1SSascha Leib								match.id = client.id || null;
7439f1ee8c1SSascha Leib								r = true;
7449f1ee8c1SSascha Leib								break;
7459f1ee8c1SSascha Leib							}
7469f1ee8c1SSascha Leib						}
7479f1ee8c1SSascha Leib						return r;
7489f1ee8c1SSascha Leib					});
7499f1ee8c1SSascha Leib				}
7509f1ee8c1SSascha Leib
75143d9de6bSSascha Leib				//console.log(match)
7529f1ee8c1SSascha Leib				return match;
7539f1ee8c1SSascha Leib			},
7549f1ee8c1SSascha Leib
7559f1ee8c1SSascha Leib			// indicates if the list is loaded and ready to use:
7569f1ee8c1SSascha Leib			_ready: false,
7579f1ee8c1SSascha Leib
7589f1ee8c1SSascha Leib			// the actual bot list is stored here:
7599f1ee8c1SSascha Leib			_list: []
7609f1ee8c1SSascha Leib
7619f1ee8c1SSascha Leib		},
7629f1ee8c1SSascha Leib
7639f1ee8c1SSascha Leib		platforms: {
7649f1ee8c1SSascha Leib			// loads the list of known platforms from a JSON file:
7659f1ee8c1SSascha Leib			init: async function() {
7667bd08c30SSascha Leib				//console.info('BotMon.live.data.platforms.init()');
7679f1ee8c1SSascha Leib
7689f1ee8c1SSascha Leib				// Load the list of known bots:
7697bd08c30SSascha Leib				BotMon.live.gui.status.showBusy("Loading known platforms");
770*0c039615SSascha Leib				const url = BotMon._baseDir + 'config/known-platforms.json';
7719f1ee8c1SSascha Leib				try {
7729f1ee8c1SSascha Leib					const response = await fetch(url);
7739f1ee8c1SSascha Leib					if (!response.ok) {
7749f1ee8c1SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
7759f1ee8c1SSascha Leib					}
7769f1ee8c1SSascha Leib
7777bd08c30SSascha Leib					BotMon.live.data.platforms._list = await response.json();
7787bd08c30SSascha Leib					BotMon.live.data.platforms._ready = true;
7799f1ee8c1SSascha Leib
7809f1ee8c1SSascha Leib				} catch (error) {
7817bd08c30SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message);
7829f1ee8c1SSascha Leib				} finally {
7837bd08c30SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
7847bd08c30SSascha Leib					BotMon.live.data._dispatch('platforms')
7859f1ee8c1SSascha Leib				}
7869f1ee8c1SSascha Leib			},
7879f1ee8c1SSascha Leib
7889f1ee8c1SSascha Leib			// returns bot info if the browser id matches a known platform:
7899f1ee8c1SSascha Leib			match: function(cid) {
7907bd08c30SSascha Leib				//console.info('BotMon.live.data.platforms.match(',cid,')');
7919f1ee8c1SSascha Leib
7929f1ee8c1SSascha Leib				let match = {"n": "Unknown", "id": null};
7939f1ee8c1SSascha Leib
7949f1ee8c1SSascha Leib				if (cid) {
7957bd08c30SSascha Leib					BotMon.live.data.platforms._list.find(platform => {
7969f1ee8c1SSascha Leib						let r = false;
7979f1ee8c1SSascha Leib						for (let j=0; j<platform.rx.length; j++) {
7989f1ee8c1SSascha Leib							const rxr = cid.match(new RegExp(platform.rx[j]));
7999f1ee8c1SSascha Leib							if (rxr) {
8009f1ee8c1SSascha Leib								match.n = platform.n;
8019f1ee8c1SSascha Leib								match.v = (rxr.length > 1 ? rxr[1] : -1);
8029f1ee8c1SSascha Leib								match.id = platform.id || null;
8039f1ee8c1SSascha Leib								r = true;
8049f1ee8c1SSascha Leib								break;
8059f1ee8c1SSascha Leib							}
8069f1ee8c1SSascha Leib						}
8079f1ee8c1SSascha Leib						return r;
8089f1ee8c1SSascha Leib					});
8099f1ee8c1SSascha Leib				}
8109f1ee8c1SSascha Leib
8119f1ee8c1SSascha Leib				return match;
8129f1ee8c1SSascha Leib			},
8139f1ee8c1SSascha Leib
8149f1ee8c1SSascha Leib			// indicates if the list is loaded and ready to use:
8159f1ee8c1SSascha Leib			_ready: false,
8169f1ee8c1SSascha Leib
8179f1ee8c1SSascha Leib			// the actual bot list is stored here:
8189f1ee8c1SSascha Leib			_list: []
8199f1ee8c1SSascha Leib
8209f1ee8c1SSascha Leib		},
8219f1ee8c1SSascha Leib
822b82cba27SSascha Leib		rules: {
823b82cba27SSascha Leib			// loads the list of rules and settings from a JSON file:
824b82cba27SSascha Leib			init: async function() {
825b82cba27SSascha Leib				//console.info('BotMon.live.data.rules.init()');
826b82cba27SSascha Leib
827b82cba27SSascha Leib				// Load the list of known bots:
828b82cba27SSascha Leib				BotMon.live.gui.status.showBusy("Loading list of rules …");
829*0c039615SSascha Leib				const url = BotMon._baseDir + 'config/rules.json';
830b82cba27SSascha Leib				try {
831b82cba27SSascha Leib					const response = await fetch(url);
832b82cba27SSascha Leib					if (!response.ok) {
833b82cba27SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
834b82cba27SSascha Leib					}
835b82cba27SSascha Leib
836b82cba27SSascha Leib					const json = await response.json();
837b82cba27SSascha Leib
838b82cba27SSascha Leib					if (json.rules) {
839b82cba27SSascha Leib						this._rulesList = json.rules;
840b82cba27SSascha Leib					}
841b82cba27SSascha Leib
842b82cba27SSascha Leib					if (json.threshold) {
843b82cba27SSascha Leib						this._threshold = json.threshold;
844b82cba27SSascha Leib					}
845b82cba27SSascha Leib
8465526d629SSascha Leib					if (json.ipRanges) {
8475526d629SSascha Leib						// clean up the IPs first:
8485526d629SSascha Leib						let list = [];
8495526d629SSascha Leib						json.ipRanges.forEach( it => {
8505526d629SSascha Leib							let item = {
8515526d629SSascha Leib								'from': BotMon.t._ip2Num(it.from),
8525526d629SSascha Leib								'to': BotMon.t._ip2Num(it.to),
8536f3fa739SSascha Leib								'label': it.label
8545526d629SSascha Leib							};
8555526d629SSascha Leib							list.push(item);
8565526d629SSascha Leib						});
8575526d629SSascha Leib
8585526d629SSascha Leib						this._botIPs = list;
8595526d629SSascha Leib					}
8605526d629SSascha Leib
861b82cba27SSascha Leib					this._ready = true;
862b82cba27SSascha Leib
863b82cba27SSascha Leib				} catch (error) {
864b82cba27SSascha Leib					BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message);
865b82cba27SSascha Leib				} finally {
866b82cba27SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
867b82cba27SSascha Leib					BotMon.live.data._dispatch('rules')
868b82cba27SSascha Leib				}
869b82cba27SSascha Leib			},
870b82cba27SSascha Leib
871b82cba27SSascha Leib			_rulesList: [], // list of rules to find out if a visitor is a bot
872b82cba27SSascha Leib			_threshold: 100, // above this, it is considered a bot.
873b82cba27SSascha Leib
874b82cba27SSascha Leib			// returns a descriptive text for a rule id
875b82cba27SSascha Leib			getRuleInfo: function(ruleId) {
876b82cba27SSascha Leib				// console.info('getRuleInfo', ruleId);
877b82cba27SSascha Leib
878b82cba27SSascha Leib				// shortcut for neater code:
879b82cba27SSascha Leib				const me = BotMon.live.data.rules;
880b82cba27SSascha Leib
881b82cba27SSascha Leib				for (let i=0; i<me._rulesList.length; i++) {
882b82cba27SSascha Leib					const rule = me._rulesList[i];
883b82cba27SSascha Leib					if (rule.id == ruleId) {
884b82cba27SSascha Leib						return rule;
885b82cba27SSascha Leib					}
886b82cba27SSascha Leib				}
887b82cba27SSascha Leib				return null;
888b82cba27SSascha Leib
889b82cba27SSascha Leib			},
890b82cba27SSascha Leib
891b82cba27SSascha Leib			// evaluate a visitor for lkikelihood of being a bot
892b82cba27SSascha Leib			evaluate: function(visitor) {
893b82cba27SSascha Leib
894b82cba27SSascha Leib				// shortcut for neater code:
895b82cba27SSascha Leib				const me = BotMon.live.data.rules;
896b82cba27SSascha Leib
897b82cba27SSascha Leib				let r =  {	// evaluation result
898b82cba27SSascha Leib					'val': 0,
899b82cba27SSascha Leib					'rules': [],
900b82cba27SSascha Leib					'isBot': false
901b82cba27SSascha Leib				};
902b82cba27SSascha Leib
903b82cba27SSascha Leib				for (let i=0; i<me._rulesList.length; i++) {
904b82cba27SSascha Leib					const rule = me._rulesList[i];
905b82cba27SSascha Leib					const params = ( rule.params ? rule.params : [] );
906b82cba27SSascha Leib
907b82cba27SSascha Leib					if (rule.func) { // rule is calling a function
908b82cba27SSascha Leib						if (me.func[rule.func]) {
909b82cba27SSascha Leib							if(me.func[rule.func](visitor, ...params)) {
910b82cba27SSascha Leib								r.val += rule.bot;
911b82cba27SSascha Leib								r.rules.push(rule.id)
912b82cba27SSascha Leib							}
913b82cba27SSascha Leib						} else {
914b82cba27SSascha Leib							//console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.")
915b82cba27SSascha Leib						}
916b82cba27SSascha Leib					}
917b82cba27SSascha Leib				}
918b82cba27SSascha Leib
919b82cba27SSascha Leib				// is a bot?
920b82cba27SSascha Leib				r.isBot = (r.val >= me._threshold);
921b82cba27SSascha Leib
922b82cba27SSascha Leib				return r;
923b82cba27SSascha Leib			},
924b82cba27SSascha Leib
925b82cba27SSascha Leib			// list of functions that can be called by the rules list to evaluate a visitor:
926b82cba27SSascha Leib			func: {
927b82cba27SSascha Leib
9286f3fa739SSascha Leib				// check if client is on the list passed as parameter:
9296f3fa739SSascha Leib				matchesClient: function(visitor, ...clients) {
930b82cba27SSascha Leib
931b82cba27SSascha Leib					const clientId = ( visitor._client ? visitor._client.id : '');
93213592cacSSascha Leib					return clients.includes(clientId);
933b82cba27SSascha Leib				},
934b82cba27SSascha Leib
935b82cba27SSascha Leib				// check if OS/Platform is one of the obsolete ones:
9366f3fa739SSascha Leib				matchesPlatform: function(visitor, ...platforms) {
937b82cba27SSascha Leib
93813592cacSSascha Leib					const pId = ( visitor._platform ? visitor._platform.id : '');
93913592cacSSascha Leib					return platforms.includes(pId);
940b82cba27SSascha Leib				},
941b82cba27SSascha Leib
942b82cba27SSascha Leib				// are there at lest num pages loaded?
943b82cba27SSascha Leib				smallPageCount: function(visitor, num) {
944b82cba27SSascha Leib					return (visitor._pageViews.length <= Number(num));
945b82cba27SSascha Leib				},
946b82cba27SSascha Leib
947446aa816SSascha Leib				// There was no entry in a specific log file for this visitor:
948b82cba27SSascha Leib				// note that this will also trigger the "noJavaScript" rule:
949446aa816SSascha Leib				noRecord: function(visitor, type) {
950446aa816SSascha Leib					return !visitor._seenBy.includes(type);
951b82cba27SSascha Leib				},
952b82cba27SSascha Leib
9535526d629SSascha Leib				// there are no referrers in any of the page visits:
9545526d629SSascha Leib				noReferrer: function(visitor) {
9555526d629SSascha Leib
9565526d629SSascha Leib					let r = false; // return value
9575526d629SSascha Leib					for (let i = 0; i < visitor._pageViews.length; i++) {
9585526d629SSascha Leib						if (!visitor._pageViews[i]._ref) {
9595526d629SSascha Leib							r = true;
9605526d629SSascha Leib							break;
961b82cba27SSascha Leib						}
962b82cba27SSascha Leib					}
9635526d629SSascha Leib					return r;
9645526d629SSascha Leib				},
9655526d629SSascha Leib
9665526d629SSascha Leib				// test for specific client identifiers:
9676f3fa739SSascha Leib				/*matchesClients: function(visitor, ...list) {
9685526d629SSascha Leib
9695526d629SSascha Leib					for (let i=0; i<list.length; i++) {
9705526d629SSascha Leib						if (visitor._client.id == list[i]) {
9715526d629SSascha Leib								return true
9725526d629SSascha Leib						}
9735526d629SSascha Leib					};
9745526d629SSascha Leib					return false;
9756f3fa739SSascha Leib				},*/
9765526d629SSascha Leib
977451abfadSSascha Leib				// unusual combinations of Platform and Client:
9786f3fa739SSascha Leib				combinationTest: function(visitor, ...combinations) {
9795526d629SSascha Leib
9805526d629SSascha Leib					for (let i=0; i<combinations.length; i++) {
9815526d629SSascha Leib
9825526d629SSascha Leib						if (visitor._platform.id == combinations[i][0]
9835526d629SSascha Leib							&& visitor._client.id == combinations[i][1]) {
9845526d629SSascha Leib								return true
9855526d629SSascha Leib						}
9865526d629SSascha Leib					};
9875526d629SSascha Leib
9885526d629SSascha Leib					return false;
9895526d629SSascha Leib				},
9905526d629SSascha Leib
9915526d629SSascha Leib				// is the IP address from a known bot network?
9925526d629SSascha Leib				fromKnownBotIP: function(visitor) {
9935526d629SSascha Leib
9945526d629SSascha Leib					const ipInfo = BotMon.live.data.rules.getBotIPInfo(visitor.ip);
9955526d629SSascha Leib
996446aa816SSascha Leib					if (ipInfo) {
997446aa816SSascha Leib						visitor._ipInKnownBotRange = true;
998446aa816SSascha Leib					}
999446aa816SSascha Leib
10005526d629SSascha Leib					return (ipInfo !== null);
1001451abfadSSascha Leib				},
1002451abfadSSascha Leib
1003451abfadSSascha Leib				// is the page language mentioned in the client's accepted languages?
1004451abfadSSascha Leib				// the parameter holds an array of exceptions, i.e. page languages that should be ignored.
1005451abfadSSascha Leib				matchLang: function(visitor, ...exceptions) {
1006451abfadSSascha Leib
100717cb08e8SSascha Leib					if (visitor.lang && visitor.accept && exceptions.indexOf(visitor.lang) < 0) {
100817cb08e8SSascha Leib						return (visitor.accept.split(',').indexOf(visitor.lang) < 0);
1009451abfadSSascha Leib					}
1010451abfadSSascha Leib					return false;
1011446aa816SSascha Leib				},
1012446aa816SSascha Leib
101343b05c93SSascha Leib				// Is there an accept-language field defined at all?
101443b05c93SSascha Leib				noAcceptLang: function(visitor) {
101543b05c93SSascha Leib
101643b05c93SSascha Leib					if (!visitor.accept || visitor.accept.length <= 0) { // no accept-languages header
101743b05c93SSascha Leib						return true;
101843b05c93SSascha Leib					}
101943b05c93SSascha Leib					// TODO: parametrize this!
102043b05c93SSascha Leib					return false;
102143b05c93SSascha Leib				},
1022446aa816SSascha Leib				// At least x page views were recorded, but they come within less than y seconds
1023446aa816SSascha Leib				loadSpeed: function(visitor, minItems, maxTime) {
1024446aa816SSascha Leib
1025446aa816SSascha Leib					if (visitor._pageViews.length >= minItems) {
1026446aa816SSascha Leib						//console.log('loadSpeed', visitor._pageViews.length, minItems, maxTime);
1027446aa816SSascha Leib
1028446aa816SSascha Leib						const pvArr = visitor._pageViews.map(pv => pv._lastSeen).sort();
1029446aa816SSascha Leib
1030446aa816SSascha Leib						let totalTime = 0;
1031446aa816SSascha Leib						for (let i=1; i < pvArr.length; i++) {
1032446aa816SSascha Leib							totalTime += (pvArr[i] - pvArr[i-1]);
1033446aa816SSascha Leib						}
1034446aa816SSascha Leib
1035446aa816SSascha Leib						//console.log('     ', totalTime , Math.round(totalTime / (pvArr.length * 1000)), (( totalTime / pvArr.length ) <= maxTime * 1000), visitor.ip);
1036446aa816SSascha Leib
1037446aa816SSascha Leib						return (( totalTime / pvArr.length ) <= maxTime * 1000);
1038446aa816SSascha Leib					}
10395526d629SSascha Leib				}
10405526d629SSascha Leib			},
10415526d629SSascha Leib
10425526d629SSascha Leib			/* known bot IP ranges: */
10435526d629SSascha Leib			_botIPs: [],
10445526d629SSascha Leib
10455526d629SSascha Leib			// return information on a bot IP range:
10465526d629SSascha Leib			getBotIPInfo: function(ip) {
10475526d629SSascha Leib
10485526d629SSascha Leib				// shortcut to make code more readable:
10495526d629SSascha Leib				const me = BotMon.live.data.rules;
10505526d629SSascha Leib
10515526d629SSascha Leib				// convert IP address to easier comparable form:
10525526d629SSascha Leib				const ipNum = BotMon.t._ip2Num(ip);
10535526d629SSascha Leib
10545526d629SSascha Leib				for (let i=0; i < me._botIPs.length; i++) {
10555526d629SSascha Leib					const ipRange = me._botIPs[i];
10565526d629SSascha Leib
10575526d629SSascha Leib					if (ipNum >= ipRange.from && ipNum <= ipRange.to) {
10585526d629SSascha Leib						return ipRange;
10595526d629SSascha Leib					}
10605526d629SSascha Leib
10615526d629SSascha Leib				};
10625526d629SSascha Leib				return null;
10635526d629SSascha Leib
10645526d629SSascha Leib			}
1065b82cba27SSascha Leib
1066b82cba27SSascha Leib		},
1067b82cba27SSascha Leib
10682f2bc93aSSascha Leib		loadLogFile: async function(type, onLoaded = undefined) {
106913592cacSSascha Leib			//console.info('BotMon.live.data.loadLogFile(',type,')');
1070f125bc8dSSascha Leib
1071f125bc8dSSascha Leib			let typeName = '';
1072f125bc8dSSascha Leib			let columns = [];
1073f125bc8dSSascha Leib
1074f125bc8dSSascha Leib			switch (type) {
1075f125bc8dSSascha Leib				case "srv":
1076f125bc8dSSascha Leib					typeName = "Server";
1077451abfadSSascha Leib					columns = ['ts','ip','pg','id','typ','usr','agent','ref','lang','accept'];
1078f125bc8dSSascha Leib					break;
1079f125bc8dSSascha Leib				case "log":
1080f125bc8dSSascha Leib					typeName = "Page load";
108193a5b18bSSascha Leib					columns = ['ts','ip','pg','id','usr','lt','ref','agent'];
1082f125bc8dSSascha Leib					break;
1083f125bc8dSSascha Leib				case "tck":
1084f125bc8dSSascha Leib					typeName = "Ticker";
108593a5b18bSSascha Leib					columns = ['ts','ip','pg','id','agent'];
1086f125bc8dSSascha Leib					break;
1087f125bc8dSSascha Leib				default:
1088f125bc8dSSascha Leib					console.warn(`Unknown log type ${type}.`);
1089f125bc8dSSascha Leib					return;
1090f125bc8dSSascha Leib			}
1091f125bc8dSSascha Leib
10922f2bc93aSSascha Leib			// Show the busy indicator and set the visible status:
10937bd08c30SSascha Leib			BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`);
1094f125bc8dSSascha Leib
10952f2bc93aSSascha Leib			// compose the URL from which to load:
10967bd08c30SSascha Leib			const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`;
10972f2bc93aSSascha Leib			//console.log("Loading:",url);
1098f125bc8dSSascha Leib
10992f2bc93aSSascha Leib			// fetch the data:
1100f125bc8dSSascha Leib			try {
1101f125bc8dSSascha Leib				const response = await fetch(url);
1102f125bc8dSSascha Leib				if (!response.ok) {
1103f125bc8dSSascha Leib					throw new Error(`${response.status} ${response.statusText}`);
1104f125bc8dSSascha Leib				}
1105f125bc8dSSascha Leib
11062f2bc93aSSascha Leib				const logtxt = await response.text();
1107f125bc8dSSascha Leib
11082f2bc93aSSascha Leib				logtxt.split('\n').forEach((line) => {
11092f2bc93aSSascha Leib					if (line.trim() === '') return; // skip empty lines
11102f2bc93aSSascha Leib					const cols = line.split('\t');
11112f2bc93aSSascha Leib
11122f2bc93aSSascha Leib					// assign the columns to an object:
11132f2bc93aSSascha Leib					const data = {};
11142f2bc93aSSascha Leib					cols.forEach( (colVal,i) => {
11152f2bc93aSSascha Leib						colName = columns[i] || `col${i}`;
111643d9de6bSSascha Leib						const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim());
11172f2bc93aSSascha Leib						data[colName] = colValue;
11182f2bc93aSSascha Leib					});
11192f2bc93aSSascha Leib
11202f2bc93aSSascha Leib					// register the visit in the model:
11212f2bc93aSSascha Leib					switch(type) {
11222f2bc93aSSascha Leib						case 'srv':
112343d9de6bSSascha Leib							BotMon.live.data.model.registerVisit(data, type);
11242f2bc93aSSascha Leib							break;
11252f2bc93aSSascha Leib						case 'log':
112643d9de6bSSascha Leib							data.typ = 'js';
11277bd08c30SSascha Leib							BotMon.live.data.model.updateVisit(data);
11282f2bc93aSSascha Leib							break;
11299f1ee8c1SSascha Leib						case 'tck':
113043d9de6bSSascha Leib							data.typ = 'js';
11317bd08c30SSascha Leib							BotMon.live.data.model.updateTicks(data);
11329f1ee8c1SSascha Leib							break;
11332f2bc93aSSascha Leib						default:
11342f2bc93aSSascha Leib							console.warn(`Unknown log type ${type}.`);
11352f2bc93aSSascha Leib							return;
11362f2bc93aSSascha Leib					}
11372f2bc93aSSascha Leib				});
11382f2bc93aSSascha Leib
11392f2bc93aSSascha Leib				if (onLoaded) {
11402f2bc93aSSascha Leib					onLoaded(); // callback after loading is finished.
11412f2bc93aSSascha Leib				}
11422f2bc93aSSascha Leib
1143f125bc8dSSascha Leib			} catch (error) {
11447bd08c30SSascha Leib				BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`);
1145f125bc8dSSascha Leib			} finally {
11467bd08c30SSascha Leib				BotMon.live.gui.status.hideBusy("Status: Done.");
1147f125bc8dSSascha Leib			}
1148f125bc8dSSascha Leib		}
1149f125bc8dSSascha Leib	},
1150f125bc8dSSascha Leib
1151294f6af8SSascha Leib	gui: {
115293a5b18bSSascha Leib		init: function() {
115393a5b18bSSascha Leib			// init the lists view:
115493a5b18bSSascha Leib			this.lists.init();
115593a5b18bSSascha Leib		},
1156294f6af8SSascha Leib
1157294f6af8SSascha Leib		overview: {
1158294f6af8SSascha Leib			make: function() {
1159f4417fdeSSascha Leib
11607bd08c30SSascha Leib				const data = BotMon.live.data.analytics.data;
1161f4417fdeSSascha Leib
1162f4417fdeSSascha Leib				// shortcut for neater code:
1163f4417fdeSSascha Leib				const makeElement = BotMon.t._makeElement;
1164f4417fdeSSascha Leib
1165*0c039615SSascha Leib				const botsVsHumans = document.getElementById('botmon__today__botsvshumans');
1166*0c039615SSascha Leib				if (botsVsHumans) {
1167*0c039615SSascha Leib					botsVsHumans.appendChild(makeElement('dt', {}, "Bots vs. Humans (page views)"));
1168ade4db36SSascha Leib
1169*0c039615SSascha Leib					for (let i = 3; i >= 0; i--) {
1170*0c039615SSascha Leib						const dd = makeElement('dd');
1171*0c039615SSascha Leib						let title = '';
1172*0c039615SSascha Leib						let value = '';
1173*0c039615SSascha Leib						switch(i) {
1174*0c039615SSascha Leib							case 0:
1175*0c039615SSascha Leib								title = "Registered users:";
1176*0c039615SSascha Leib								value = data.bots.users;
1177*0c039615SSascha Leib								break;
1178*0c039615SSascha Leib							case 1:
1179*0c039615SSascha Leib								title = "Probably humans:";
1180*0c039615SSascha Leib								value = data.bots.human;
1181*0c039615SSascha Leib								break;
1182*0c039615SSascha Leib							case 2:
1183*0c039615SSascha Leib								title = "Suspected bots:";
1184*0c039615SSascha Leib								value = data.bots.suspected;
1185*0c039615SSascha Leib								break;
1186*0c039615SSascha Leib							case 3:
1187*0c039615SSascha Leib								title = "Known bots:";
1188*0c039615SSascha Leib								value = data.bots.known;
1189*0c039615SSascha Leib								break;
1190*0c039615SSascha Leib							default:
1191*0c039615SSascha Leib								console.warn(`Unknown list type ${i}.`);
1192*0c039615SSascha Leib						}
1193*0c039615SSascha Leib						dd.appendChild(makeElement('span', {}, title));
1194*0c039615SSascha Leib						dd.appendChild(makeElement('strong', {}, value));
1195*0c039615SSascha Leib						botsVsHumans.appendChild(dd);
1196*0c039615SSascha Leib					}
1197*0c039615SSascha Leib				}
1198f4417fdeSSascha Leib
1199f4417fdeSSascha Leib				// update known bots list:
1200*0c039615SSascha Leib				const botlist = document.getElementById('botmon__botslist');
1201*0c039615SSascha Leib				botlist.innerHTML = "<dt>Top 5 known bots (page views)</dt>";
1202f4417fdeSSascha Leib
1203f4417fdeSSascha Leib				let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
1204f4417fdeSSascha Leib					return b._pageViews.length - a._pageViews.length;
1205f4417fdeSSascha Leib				});
1206f4417fdeSSascha Leib
1207*0c039615SSascha Leib				for (let i=0; i < Math.min(bots.length, 5); i++) {
1208f4417fdeSSascha Leib					const dd = makeElement('dd');
1209f4417fdeSSascha Leib					dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id }, bots[i]._bot.n));
1210b82cba27SSascha Leib					dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length));
1211*0c039615SSascha Leib					botlist.appendChild(dd);
1212*0c039615SSascha Leib				}
1213*0c039615SSascha Leib
1214*0c039615SSascha Leib				// update the suspected bot IP ranges list:
1215*0c039615SSascha Leib				const botIps = document.getElementById('botmon__today__botips');
1216*0c039615SSascha Leib				if (botIps) {
1217*0c039615SSascha Leib					botIps.appendChild(makeElement('dt', {}, "Top 5 suspected bots’ IP ranges"));
1218*0c039615SSascha Leib
1219*0c039615SSascha Leib					const ipList = BotMon.live.data.analytics.getTopBotIPRanges(5);
1220*0c039615SSascha Leib					ipList.forEach( (ipInfo) => {
1221*0c039615SSascha Leib						const li = makeElement('dd');
1222*0c039615SSascha Leib						li.appendChild(makeElement('span', {'class': 'ip ip' + ipInfo.typ }, ipInfo.ip));
1223*0c039615SSascha Leib						li.appendChild(makeElement('span', {'class': 'count' }, ipInfo.num));
1224*0c039615SSascha Leib						botIps.append(li)
1225*0c039615SSascha Leib					})
1226*0c039615SSascha Leib				}
1227*0c039615SSascha Leib
1228*0c039615SSascha Leib				// update the webmetrics overview:
1229*0c039615SSascha Leib				const wmoverview = document.getElementById('botmon__today__wm_overview');
1230*0c039615SSascha Leib				if (wmoverview) {
1231*0c039615SSascha Leib					const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100);
1232*0c039615SSascha Leib
1233*0c039615SSascha Leib					wmoverview.appendChild(makeElement('dt', {}, "Overview"));
1234*0c039615SSascha Leib					for (let i = 0; i < 3; i++) {
1235*0c039615SSascha Leib						const dd = makeElement('dd');
1236*0c039615SSascha Leib						let title = '';
1237*0c039615SSascha Leib						let value = '';
1238*0c039615SSascha Leib						switch(i) {
1239*0c039615SSascha Leib							case 0:
1240*0c039615SSascha Leib								title = "Total page views:";
1241*0c039615SSascha Leib								value = data.totalPageViews;
1242*0c039615SSascha Leib								break;
1243*0c039615SSascha Leib							case 1:
1244*0c039615SSascha Leib								title = "Total visitors (est.):";
1245*0c039615SSascha Leib								value = data.totalVisits;
1246*0c039615SSascha Leib								break;
1247*0c039615SSascha Leib							case 2:
1248*0c039615SSascha Leib								title = "Bounce rate (est.):";
1249*0c039615SSascha Leib								value = bounceRate + '%';
1250*0c039615SSascha Leib								break;
1251*0c039615SSascha Leib							default:
1252*0c039615SSascha Leib								console.warn(`Unknown list type ${i}.`);
1253*0c039615SSascha Leib						}
1254*0c039615SSascha Leib						dd.appendChild(makeElement('span', {}, title));
1255*0c039615SSascha Leib						dd.appendChild(makeElement('strong', {}, value));
1256*0c039615SSascha Leib						wmoverview.appendChild(dd);
1257f4417fdeSSascha Leib					}
1258294f6af8SSascha Leib				}
1259*0c039615SSascha Leib
1260294f6af8SSascha Leib			}
1261294f6af8SSascha Leib		},
1262f4417fdeSSascha Leib
1263f125bc8dSSascha Leib		status: {
1264f125bc8dSSascha Leib			setText: function(txt) {
12657bd08c30SSascha Leib				const el = document.getElementById('botmon__today__status');
12667bd08c30SSascha Leib				if (el && BotMon.live.gui.status._errorCount <= 0) {
1267f125bc8dSSascha Leib					el.innerText = txt;
1268f125bc8dSSascha Leib				}
1269f125bc8dSSascha Leib			},
1270f125bc8dSSascha Leib
1271f125bc8dSSascha Leib			setTitle: function(html) {
12727bd08c30SSascha Leib				const el = document.getElementById('botmon__today__title');
1273f125bc8dSSascha Leib				if (el) {
1274f125bc8dSSascha Leib					el.innerHTML = html;
1275f125bc8dSSascha Leib				}
1276f125bc8dSSascha Leib			},
1277f125bc8dSSascha Leib
1278f125bc8dSSascha Leib			setError: function(txt) {
1279f125bc8dSSascha Leib				console.error(txt);
12807bd08c30SSascha Leib				BotMon.live.gui.status._errorCount += 1;
12817bd08c30SSascha Leib				const el = document.getElementById('botmon__today__status');
1282f125bc8dSSascha Leib				if (el) {
1283f125bc8dSSascha Leib					el.innerText = "An error occured. See the browser log for details!";
1284f125bc8dSSascha Leib					el.classList.add('error');
1285f125bc8dSSascha Leib				}
1286f125bc8dSSascha Leib			},
1287f125bc8dSSascha Leib			_errorCount: 0,
1288f125bc8dSSascha Leib
1289f125bc8dSSascha Leib			showBusy: function(txt = null) {
12907bd08c30SSascha Leib				BotMon.live.gui.status._busyCount += 1;
12917bd08c30SSascha Leib				const el = document.getElementById('botmon__today__busy');
1292f125bc8dSSascha Leib				if (el) {
1293f125bc8dSSascha Leib					el.style.display = 'inline-block';
1294f125bc8dSSascha Leib				}
12957bd08c30SSascha Leib				if (txt) BotMon.live.gui.status.setText(txt);
1296f125bc8dSSascha Leib			},
1297f125bc8dSSascha Leib			_busyCount: 0,
1298f125bc8dSSascha Leib
1299f125bc8dSSascha Leib			hideBusy: function(txt = null) {
13007bd08c30SSascha Leib				const el = document.getElementById('botmon__today__busy');
13017bd08c30SSascha Leib				BotMon.live.gui.status._busyCount -= 1;
13027bd08c30SSascha Leib				if (BotMon.live.gui.status._busyCount <= 0) {
1303f125bc8dSSascha Leib					if (el) el.style.display = 'none';
13047bd08c30SSascha Leib					if (txt) BotMon.live.gui.status.setText(txt);
1305f125bc8dSSascha Leib				}
1306f125bc8dSSascha Leib			}
130793a5b18bSSascha Leib		},
130893a5b18bSSascha Leib
130993a5b18bSSascha Leib		lists: {
131093a5b18bSSascha Leib			init: function() {
131193a5b18bSSascha Leib
131213592cacSSascha Leib				// function shortcut:
131313592cacSSascha Leib				const makeElement = BotMon.t._makeElement;
131413592cacSSascha Leib
131593a5b18bSSascha Leib				const parent = document.getElementById('botmon__today__visitorlists');
131693a5b18bSSascha Leib				if (parent) {
131793a5b18bSSascha Leib
131893a5b18bSSascha Leib					for (let i=0; i < 4; i++) {
131993a5b18bSSascha Leib
132093a5b18bSSascha Leib						// change the id and title by number:
132193a5b18bSSascha Leib						let listTitle = '';
132293a5b18bSSascha Leib						let listId = '';
132393a5b18bSSascha Leib						switch (i) {
132493a5b18bSSascha Leib							case 0:
132593a5b18bSSascha Leib								listTitle = "Registered users";
132693a5b18bSSascha Leib								listId = 'users';
132793a5b18bSSascha Leib								break;
132893a5b18bSSascha Leib							case 1:
132993a5b18bSSascha Leib								listTitle = "Probably humans";
133093a5b18bSSascha Leib								listId = 'humans';
133193a5b18bSSascha Leib								break;
133293a5b18bSSascha Leib							case 2:
133393a5b18bSSascha Leib								listTitle = "Suspected bots";
133493a5b18bSSascha Leib								listId = 'suspectedBots';
133593a5b18bSSascha Leib								break;
133693a5b18bSSascha Leib							case 3:
133793a5b18bSSascha Leib								listTitle = "Known bots";
133893a5b18bSSascha Leib								listId = 'knownBots';
133993a5b18bSSascha Leib								break;
134093a5b18bSSascha Leib							default:
13414c5062c1SSascha Leib								console.warn('Unknown list number.');
1342f125bc8dSSascha Leib						}
1343294f6af8SSascha Leib
134413592cacSSascha Leib						const details = makeElement('details', {
134593a5b18bSSascha Leib							'data-group': listId,
134693a5b18bSSascha Leib							'data-loaded': false
134793a5b18bSSascha Leib						});
134813592cacSSascha Leib						const title = details.appendChild(makeElement('summary'));
13494c5062c1SSascha Leib						title.appendChild(makeElement('span', {'class':'title'}, listTitle));
13504c5062c1SSascha Leib						title.appendChild(makeElement('span', {'class':'counter'}, '–'));
135193a5b18bSSascha Leib						details.addEventListener("toggle", this._onDetailsToggle);
135293a5b18bSSascha Leib
135393a5b18bSSascha Leib						parent.appendChild(details);
135493a5b18bSSascha Leib
135593a5b18bSSascha Leib					}
135693a5b18bSSascha Leib				}
135793a5b18bSSascha Leib			},
135893a5b18bSSascha Leib
135993a5b18bSSascha Leib			_onDetailsToggle: function(e) {
1360f4417fdeSSascha Leib				//console.info('BotMon.live.gui.lists._onDetailsToggle()');
136193a5b18bSSascha Leib
136293a5b18bSSascha Leib				const target = e.target;
136393a5b18bSSascha Leib
136493a5b18bSSascha Leib				if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet
136593a5b18bSSascha Leib					target.setAttribute('data-loaded', 'loading');
136693a5b18bSSascha Leib
136793a5b18bSSascha Leib					const fillType = target.getAttribute('data-group');
136893a5b18bSSascha Leib					const fillList = BotMon.live.data.analytics.groups[fillType];
136993a5b18bSSascha Leib					if (fillList && fillList.length > 0) {
137093a5b18bSSascha Leib
137193a5b18bSSascha Leib						const ul = BotMon.t._makeElement('ul');
137293a5b18bSSascha Leib
137393a5b18bSSascha Leib						fillList.forEach( (it) => {
137493a5b18bSSascha Leib							ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType));
137593a5b18bSSascha Leib						});
137693a5b18bSSascha Leib
137793a5b18bSSascha Leib						target.appendChild(ul);
137893a5b18bSSascha Leib						target.setAttribute('data-loaded', 'true');
137993a5b18bSSascha Leib					} else {
138093a5b18bSSascha Leib						target.setAttribute('data-loaded', 'false');
138193a5b18bSSascha Leib					}
138293a5b18bSSascha Leib
138393a5b18bSSascha Leib				}
138493a5b18bSSascha Leib			},
138593a5b18bSSascha Leib
138693a5b18bSSascha Leib			_makeVisitorItem: function(data, type) {
138793a5b18bSSascha Leib
138893a5b18bSSascha Leib				// shortcut for neater code:
138993a5b18bSSascha Leib				const make = BotMon.t._makeElement;
139093a5b18bSSascha Leib
139143d9de6bSSascha Leib				let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
13925526d629SSascha Leib				const platformName = (data._platform ? data._platform.n : 'Unknown');
13935526d629SSascha Leib				const clientName = (data._client ? data._client.n: 'Unknown');
139443d9de6bSSascha Leib
139593a5b18bSSascha Leib				const li = make('li'); // root list item
139693a5b18bSSascha Leib				const details = make('details');
139793a5b18bSSascha Leib				const summary = make('summary');
139893a5b18bSSascha Leib				details.appendChild(summary);
139993a5b18bSSascha Leib
140093a5b18bSSascha Leib				const span1 = make('span'); /* left-hand group */
1401f4417fdeSSascha Leib				if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */
140293a5b18bSSascha Leib
1403259d3b85SSascha Leib					const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown");
140443d9de6bSSascha Leib					span1.appendChild(make('span', { /* Bot */
140543d9de6bSSascha Leib						'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'),
1406259d3b85SSascha Leib						'title': "Bot: " + botName
1407259d3b85SSascha Leib					}, botName));
140843d9de6bSSascha Leib
1409f4417fdeSSascha Leib				} else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */
141043d9de6bSSascha Leib
141143d9de6bSSascha Leib					span1.appendChild(make('span', { /* User */
1412f4417fdeSSascha Leib						'class': 'user_known',
141343d9de6bSSascha Leib						'title': "User: " + data.usr
141443d9de6bSSascha Leib					}, data.usr));
141543d9de6bSSascha Leib
141643d9de6bSSascha Leib				} else { /* others */
141743d9de6bSSascha Leib
141843d9de6bSSascha Leib					if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
141943d9de6bSSascha Leib					span1.appendChild(make('span', { /* IP-Address */
142043d9de6bSSascha Leib						'class': 'ipaddr ip' + ipType,
142143d9de6bSSascha Leib						'title': "IP-Address: " + data.ip
142243d9de6bSSascha Leib					}, data.ip));
142343d9de6bSSascha Leib
142443d9de6bSSascha Leib				}
142593a5b18bSSascha Leib
1426f4417fdeSSascha Leib				if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */
142793a5b18bSSascha Leib					span1.appendChild(make('span', { /* Platform */
142893a5b18bSSascha Leib						'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'),
142993a5b18bSSascha Leib						'title': "Platform: " + platformName
143093a5b18bSSascha Leib					}, platformName));
143193a5b18bSSascha Leib
143293a5b18bSSascha Leib					span1.appendChild(make('span', { /* Client */
143393a5b18bSSascha Leib						'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'),
143493a5b18bSSascha Leib						'title': "Client: " + clientName
143593a5b18bSSascha Leib					}, clientName));
1436f4417fdeSSascha Leib				}
143793a5b18bSSascha Leib
143893a5b18bSSascha Leib				summary.appendChild(span1);
143993a5b18bSSascha Leib				const span2 = make('span'); /* right-hand group */
144093a5b18bSSascha Leib
1441f4417fdeSSascha Leib				span2.appendChild(make('span', { /* page views */
1442f4417fdeSSascha Leib					'class': 'pageviews'
1443f4417fdeSSascha Leib				}, data._pageViews.length));
144493a5b18bSSascha Leib
144593a5b18bSSascha Leib				summary.appendChild(span2);
144693a5b18bSSascha Leib
14475526d629SSascha Leib				// add details expandable section:
14485526d629SSascha Leib				details.appendChild(BotMon.live.gui.lists._makeVisitorDetails(data, type));
14495526d629SSascha Leib
14505526d629SSascha Leib				li.appendChild(details);
14515526d629SSascha Leib				return li;
14525526d629SSascha Leib			},
14535526d629SSascha Leib
14545526d629SSascha Leib			_makeVisitorDetails: function(data, type) {
14555526d629SSascha Leib
14565526d629SSascha Leib				// shortcut for neater code:
14575526d629SSascha Leib				const make = BotMon.t._makeElement;
14585526d629SSascha Leib
14595526d629SSascha Leib				let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
14605526d629SSascha Leib				if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
14615526d629SSascha Leib				const platformName = (data._platform ? data._platform.n : 'Unknown');
14625526d629SSascha Leib				const clientName = (data._client ? data._client.n: 'Unknown');
146393a5b18bSSascha Leib
146493a5b18bSSascha Leib				const dl = make('dl', {'class': 'visitor_details'});
146593a5b18bSSascha Leib
1466f4417fdeSSascha Leib				if (data._type == BM_USERTYPE.KNOWN_BOT) {
1467f4417fdeSSascha Leib
1468f4417fdeSSascha Leib					dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */
146943d9de6bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')},
147043d9de6bSSascha Leib						(data._bot ? data._bot.n : 'Unknown')));
1471f4417fdeSSascha Leib
1472f4417fdeSSascha Leib					if (data._bot && data._bot.url) {
1473f4417fdeSSascha Leib						dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */
1474f4417fdeSSascha Leib						const botInfoDd = dl.appendChild(make('dd'));
1475f4417fdeSSascha Leib						botInfoDd.appendChild(make('a', {
1476f4417fdeSSascha Leib							'href': data._bot.url,
1477f4417fdeSSascha Leib							'target': '_blank'
1478f4417fdeSSascha Leib						}, data._bot.url)); /* bot info link*/
1479f4417fdeSSascha Leib
148043d9de6bSSascha Leib					}
148143d9de6bSSascha Leib
1482f4417fdeSSascha Leib				} else { /* not for bots */
1483f4417fdeSSascha Leib
148493a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Client:")); /* client */
148593a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')},
148693a5b18bSSascha Leib						clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) ));
148793a5b18bSSascha Leib
148893a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Platform:")); /* platform */
148993a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')},
149093a5b18bSSascha Leib						platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) ));
149193a5b18bSSascha Leib
149293a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "IP-Address:"));
149393a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip));
1494b2e3bd8bSSascha Leib
14955d78a53bSSascha Leib					/*dl.appendChild(make('dt', {}, "ID:"));
14965d78a53bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/
1497259d3b85SSascha Leib				}
149893a5b18bSSascha Leib
1499446aa816SSascha Leib				if (Math.abs(data._lastSeen - data._firstSeen) < 100) {
150093a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Seen:"));
150193a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString()));
150293a5b18bSSascha Leib				} else {
150393a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "First seen:"));
150493a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString()));
150593a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Last seen:"));
150693a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString()));
150793a5b18bSSascha Leib				}
150893a5b18bSSascha Leib
150993a5b18bSSascha Leib				dl.appendChild(make('dt', {}, "User-Agent:"));
15105526d629SSascha Leib				dl.appendChild(make('dd', {'class': 'agent'}, data.agent));
151193a5b18bSSascha Leib
15126f3fa739SSascha Leib				dl.appendChild(make('dt', {}, "Languages:"));
15136f3fa739SSascha Leib				dl.appendChild(make('dd', {'class': 'langs'}, "Client accepts: [" + data.accept + "]; Page: [" + data.lang + ']'));
15146f3fa739SSascha Leib
15155d78a53bSSascha Leib				/*dl.appendChild(make('dt', {}, "Visitor Type:"));
15165d78a53bSSascha Leib				dl.appendChild(make('dd', undefined, data._type ));*/
1517f4417fdeSSascha Leib
1518f4417fdeSSascha Leib				dl.appendChild(make('dt', {}, "Seen by:"));
1519f4417fdeSSascha Leib				dl.appendChild(make('dd', undefined, data._seenBy.join(', ') ));
1520f4417fdeSSascha Leib
152193a5b18bSSascha Leib				dl.appendChild(make('dt', {}, "Visited pages:"));
152293a5b18bSSascha Leib				const pagesDd = make('dd', {'class': 'pages'});
152393a5b18bSSascha Leib				const pageList = make('ul');
15245526d629SSascha Leib
15255526d629SSascha Leib				/* list all page views */
152693a5b18bSSascha Leib				data._pageViews.forEach( (page) => {
152793a5b18bSSascha Leib					const pgLi = make('li');
152893a5b18bSSascha Leib
152993a5b18bSSascha Leib					let visitTimeStr = "Bounce";
153093a5b18bSSascha Leib					const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime();
153193a5b18bSSascha Leib					if (visitDuration > 0) {
153293a5b18bSSascha Leib						visitTimeStr = Math.floor(visitDuration / 1000) + "s";
153393a5b18bSSascha Leib					}
153493a5b18bSSascha Leib
15355526d629SSascha Leib					pgLi.appendChild(make('span', {}, page.pg)); /* DW Page ID */
15365526d629SSascha Leib					if (page._ref) {
15375526d629SSascha Leib						pgLi.appendChild(make('span', {
15385526d629SSascha Leib							'data-ref': page._ref.host,
15395526d629SSascha Leib							'title': "Referrer: " + page._ref.full
15405526d629SSascha Leib						}, page._ref.site));
15415526d629SSascha Leib					} else {
15425526d629SSascha Leib						pgLi.appendChild(make('span', {
15435526d629SSascha Leib						}, "No referer"));
15445526d629SSascha Leib					}
1545259d3b85SSascha Leib					pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount));
1546446aa816SSascha Leib					pgLi.appendChild(make('span', {}, BotMon.t._formatTime(page._firstSeen)));
1547446aa816SSascha Leib
1548446aa816SSascha Leib					// get the time difference:
1549446aa816SSascha Leib					const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen);
1550446aa816SSascha Leib					if (tDiff) {
1551446aa816SSascha Leib						pgLi.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff));
1552446aa816SSascha Leib					} else {
1553446aa816SSascha Leib						pgLi.appendChild(make('span', {'class': 'bounce'}, "Bounce"));
1554446aa816SSascha Leib					}
1555446aa816SSascha Leib
155693a5b18bSSascha Leib					pageList.appendChild(pgLi);
155793a5b18bSSascha Leib				});
155893a5b18bSSascha Leib				pagesDd.appendChild(pageList);
155993a5b18bSSascha Leib				dl.appendChild(pagesDd);
156093a5b18bSSascha Leib
1561446aa816SSascha Leib				/* bot evaluation rating */
15625d78a53bSSascha Leib				if (data._type !== BM_USERTYPE.KNOWN_BOT && data._type !== BM_USERTYPE.KNOWN_USER) {
1563446aa816SSascha Leib					dl.appendChild(make('dt', undefined, "Bot rating:"));
15645d78a53bSSascha Leib					dl.appendChild(make('dd', {'class': 'bot-rating'}, ( data._botVal ? data._botVal : '–' ) + '/' + BotMon.live.data.rules._threshold ));
1565446aa816SSascha Leib
1566446aa816SSascha Leib					/* add bot evaluation details: */
15671c16f1b7SSascha Leib					if (data._eval) {
1568446aa816SSascha Leib						dl.appendChild(make('dt', {}, "Bot evaluation details:"));
1569b82cba27SSascha Leib						const evalDd = make('dd');
1570bc55f6a6SSascha Leib						const testList = make('ul',{
1571bc55f6a6SSascha Leib							'class': 'eval'
1572bc55f6a6SSascha Leib						});
15735526d629SSascha Leib						data._eval.forEach( test => {
1574b82cba27SSascha Leib
1575b82cba27SSascha Leib							const tObj = BotMon.live.data.rules.getRuleInfo(test);
15765526d629SSascha Leib							let tDesc = tObj ? tObj.desc : test;
1577b82cba27SSascha Leib
15785526d629SSascha Leib							// special case for Bot IP range test:
15795526d629SSascha Leib							if (tObj.func == 'fromKnownBotIP') {
15805526d629SSascha Leib								const rangeInfo = BotMon.live.data.rules.getBotIPInfo(data.ip);
15815526d629SSascha Leib								if (rangeInfo) {
15826f3fa739SSascha Leib									tDesc += ' (' + (rangeInfo.label ? rangeInfo.label : 'Unknown') + ')';
15835526d629SSascha Leib								}
15845526d629SSascha Leib							}
15855526d629SSascha Leib
15865526d629SSascha Leib							// create the entry field
1587b82cba27SSascha Leib							const tstLi = make('li');
1588b82cba27SSascha Leib							tstLi.appendChild(make('span', {
15895526d629SSascha Leib								'data-testid': test
15905526d629SSascha Leib							}, tDesc));
1591b82cba27SSascha Leib							tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') ));
1592b82cba27SSascha Leib							testList.appendChild(tstLi);
1593b82cba27SSascha Leib						});
1594b82cba27SSascha Leib
15955526d629SSascha Leib						// add total row
1596bc55f6a6SSascha Leib						const tst2Li = make('li', {
1597bc55f6a6SSascha Leib							'class': 'total'
1598bc55f6a6SSascha Leib						});
1599446aa816SSascha Leib						/*tst2Li.appendChild(make('span', {}, "Total:"));
1600b82cba27SSascha Leib						tst2Li.appendChild(make('span', {}, data._botVal));
1601446aa816SSascha Leib						testList.appendChild(tst2Li);*/
1602b82cba27SSascha Leib
1603b82cba27SSascha Leib						evalDd.appendChild(testList);
1604b82cba27SSascha Leib						dl.appendChild(evalDd);
1605bc55f6a6SSascha Leib					}
16065d78a53bSSascha Leib				}
16075d78a53bSSascha Leib				// return the element to add to the UI:
16085526d629SSascha Leib				return dl;
160993a5b18bSSascha Leib			}
1610f4417fdeSSascha Leib
161193a5b18bSSascha Leib		}
1612294f6af8SSascha Leib	}
1613c7931771SSascha Leib};
1614f125bc8dSSascha Leib
16157bd08c30SSascha Leib/* launch only if the BotMon admin panel is open: */
16167bd08c30SSascha Leibif (document.getElementById('botmon__admin')) {
16177bd08c30SSascha Leib	BotMon.init();
1618f125bc8dSSascha Leib}