1/* DokuWiki Monitor Plugin Script file */ 2/* 30.08.2025 - 0.1.5 - pre-release */ 3/* Authors: Sascha Leib <ad@hominem.info> */ 4 5const Monitor = { 6 7 init: function() { 8 //console.info('Monitor.init()'); 9 10 // find the plugin basedir: 11 this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) 12 + '/plugins/monitor/'; 13 14 // read the page language from the DOM: 15 this._lang = document.getRootNode().documentElement.lang || this._lang; 16 17 // get the time offset: 18 this._timeDiff = Monitor.t._getTimeOffset(); 19 20 // init the sub-objects: 21 Monitor.t._callInit(this); 22 }, 23 24 _baseDir: null, 25 _lang: 'en', 26 _today: (new Date()).toISOString().slice(0, 10), 27 _timeDiff: '', 28 29 /* internal tools */ 30 t: { 31 32 /* helper function to call inits of sub-objects */ 33 _callInit: function(obj) { 34 //console.info('Monitor.t._callInit(obj=',obj,')'); 35 36 /* call init / _init on each sub-object: */ 37 Object.keys(obj).forEach( (key,i) => { 38 const sub = obj[key]; 39 let init = null; 40 if (typeof sub === 'object' && sub.init) { 41 init = sub.init; 42 } 43 44 // bind to object 45 if (typeof init == 'function') { 46 const init2 = init.bind(sub); 47 init2(obj); 48 } 49 }); 50 }, 51 52 /* helper function to calculate the time difference to UTC: */ 53 _getTimeOffset: function() { 54 const now = new Date(); 55 let offset = now.getTimezoneOffset(); // in minutes 56 const sign = Math.sign(offset); // +1 or -1 57 offset = Math.abs(offset); // always positive 58 59 let hours = 0; 60 while (offset >= 60) { 61 hours += 1; 62 offset -= 60; 63 } 64 return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); 65 } 66 } 67}; 68 69/* everything specific to the "Today" tab is self-contained in the "live" object: */ 70Monitor.live = { 71 init: function() { 72 //console.info('Monitor.live.init()'); 73 74 // set the title: 75 const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (Monitor._timeDiff != '' ? `, ${Monitor._timeDiff}` : '' ) + ')'; 76 Monitor.live.status.setTitle(`Showing data for <time datetime=${Monitor._today}>${Monitor._today}</time> ${tDiff}`); 77 78 // init sub-objects: 79 Monitor.t._callInit(this); 80 }, 81 82 data: { 83 init: function() { 84 //console.info('Monitor.live.data.init()'); 85 86 // call sub-inits: 87 Monitor.t._callInit(this); 88 }, 89 90 // this will be called when the known json files are done loading: 91 _dispatch: function(file) { 92 //console.info('Monitor.live.data._dispatch(,',file,')'); 93 94 // shortcut to make code more readable: 95 const data = Monitor.live.data; 96 97 // set the flags: 98 switch(file) { 99 case 'bots': 100 data._dispatchBotsLoaded = true; 101 break; 102 case 'clients': 103 data._dispatchClientsLoaded = true; 104 break; 105 case 'platforms': 106 data._dispatchPlatformsLoaded = true; 107 break; 108 default: 109 // ignore 110 } 111 112 // are all the flags set? 113 if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded) { 114 // chain the log files loading: 115 Monitor.live.data.loadLogFile('srv', Monitor.live.data._onServerLogLoaded); 116 } 117 }, 118 // flags to track which data files have been loaded: 119 _dispatchBotsLoaded: false, 120 _dispatchClientsLoaded: false, 121 _dispatchPlatformsLoaded: false, 122 123 // event callback, after the server log has been loaded: 124 _onServerLogLoaded: function() { 125 //console.info('Monitor.live.data._onServerLogLoaded()'); 126 127 // chain the client log file to load: 128 Monitor.live.data.loadLogFile('log', Monitor.live.data._onClientLogLoaded); 129 }, 130 131 // event callback, after the client log has been loaded: 132 _onClientLogLoaded: function() { 133 console.info('Monitor.live.data._onClientLogLoaded()'); 134 135 // chain the ticks file to load: 136 // Monitor.live.data.loadLogFile('tck', Monitor.live.data._onTicksLogLoaded); 137 //Monitor.live.data.loadLogFile('tck', Monitor.live.data._onTicksLogLoaded); 138 139 console.log(Monitor.live.data.model._visitors); 140 //Monitor.live.data._onTicksLogLoaded(); 141 }, 142 143 // event callback, after the tiker log has been loaded: 144 _onTicksLogLoaded: function() { 145 console.info('Monitor.live.data._onTicksLogLoaded()'); 146 147 // analyse the data: 148 // #TODO 149 150 // sort the data: 151 // #TODO 152 153 // display the data: 154 // #TODO 155 156 console.log(Monitor.live.data.model._visitors); 157 158 }, 159 160 model: { 161 // visitors storage: 162 _visitors: [], 163 164 // find an already existing visitor record: 165 findVisitor: function(id) { 166 167 // shortcut to make code more readable: 168 const model = Monitor.live.data.model; 169 170 // loop over all visitors already registered: 171 for (let i=0; i<model._visitors.length; i++) { 172 const v = model._visitors[i]; 173 if (v && v.id == id) return v; 174 } 175 return null; // nothing found 176 }, 177 178 /* if there is already this visit registered, return it (used for updates) */ 179 _getVisit: function(visit, view) { 180 181 for (let i=0; i<visit._pageViews.length; i++) { 182 const v = visit._pageViews[i]; 183 if (v.pg == view.pg && // same page id, and 184 view.ts.getTime() - v._firstSeen.getTime() < 600000) { // seen less than 10 minutes ago 185 return v; // it is the same visit. 186 } 187 } 188 return null; // not found 189 }, 190 191 // register a new visitor (or update if already exists) 192 registerVisit: function(dat) { 193 //console.info('registerVisit', dat); 194 195 // shortcut to make code more readable: 196 const model = Monitor.live.data.model; 197 198 // check if it already exists: 199 let visitor = model.findVisitor(dat.id); 200 if (!visitor) { 201 const bot = Monitor.live.data.bots.match(dat.client); 202 203 model._visitors.push(dat); 204 visitor = dat; 205 visitor._firstSeen = dat.ts; 206 visitor._lastSeen = dat.ts; 207 visitor._isBot = ( bot ? 1.0 : 0.0 ); // likelihood of being a bot; primed to 0% or 100% in case of a known bot 208 visitor._pageViews = []; // array of page views 209 visitor._hasReferrer = false; // has at least one referrer 210 visitor._jsClient = false; // visitor has been seen logged by client js as well 211 visitor._client = bot ?? Monitor.live.data.clients.match(dat.client) ?? null; // client info (browser, bot, etc.) 212 visitor._platform = Monitor.live.data.platforms.match(dat.client); // platform info 213 214 // known bots get the bot ID as identifier: 215 if (bot) visitor.id = bot.id; 216 } 217 218 // find browser 219 220 // is this visit already registered? 221 let prereg = model._getVisit(visitor, dat); 222 if (!prereg) { 223 // add the page view to the visitor: 224 prereg = { 225 _by: 'srv', 226 ip: dat.ip, 227 pg: dat.pg, 228 ref: dat.ref || '', 229 _firstSeen: dat.ts, 230 _lastSeen: dat.ts, 231 _jsClient: false 232 }; 233 visitor._pageViews.push(prereg); 234 } 235 236 // update referrer state: 237 visitor._hasReferrer = visitor._hasReferrer || 238 (prereg.ref !== undefined && prereg.ref !== ''); 239 240 // update time stamp for last-seen: 241 visitor._lastSeen = dat.ts; 242 243 // if needed: 244 return visitor; 245 }, 246 247 // updating visit data from the client-side log: 248 updateVisit: function(dat) { 249 //console.info('updateVisit', dat); 250 251 // shortcut to make code more readable: 252 const model = Monitor.live.data.model; 253 254 let visitor = model.findVisitor(dat.id); 255 if (!visitor) { 256 visitor = model.registerVisit(dat); 257 } 258 if (visitor) { 259 visitor._lastSeen = dat.ts; 260 visitor._jsClient = true; // seen by client js 261 } else { 262 console.warn(`No visit with ID ${dat.id}.`); 263 return; 264 } 265 266 // find the page view: 267 let prereg = model._getVisit(visitor, dat); 268 if (prereg) { 269 // update the page view: 270 prereg._lastSeen = dat.ts; 271 prereg._jsClient = true; // seen by client js 272 } else { 273 // add the page view to the visitor: 274 prereg = { 275 _by: 'log', 276 ip: dat.ip, 277 pg: dat.pg, 278 ref: dat.ref || '', 279 _firstSeen: dat.ts, 280 _lastSeen: dat.ts, 281 _jsClient: true 282 }; 283 visitor._pageViews.push(prereg); 284 } 285 }, 286 287 // updating visit data from the ticker log: 288 updateTicks: function(dat) { 289 console.info('updateTicks', dat); 290 291 // shortcut to make code more readable: 292 const model = Monitor.live.data.model; 293 294 // find the visit info: 295 let visitor = model.findVisitor(dat.id); 296 if (visitor) { 297 console.log("Visitor:", visitor); 298 } else { 299 //model.registerVisit(dat); 300 console.warn(`Unknown visitor with ID ${dat.id}!`); 301 } 302 303 } 304 }, 305 306 bots: { 307 // loads the list of known bots from a JSON file: 308 init: async function() { 309 //console.info('Monitor.live.data.bots.init()'); 310 311 // Load the list of known bots: 312 Monitor.live.status.showBusy("Loading known bots …"); 313 const url = Monitor._baseDir + 'data/known-bots.json'; 314 try { 315 const response = await fetch(url); 316 if (!response.ok) { 317 throw new Error(`${response.status} ${response.statusText}`); 318 } 319 320 Monitor.live.data.bots._list = await response.json(); 321 Monitor.live.data.bots._ready = true; 322 323 // TODO: allow using the bots list... 324 } catch (error) { 325 Monitor.live.status.setError("Error while loading the ’known bots’ file: " + error.message); 326 } finally { 327 Monitor.live.status.hideBusy("Done."); 328 Monitor.live.data._dispatch('bots') 329 } 330 }, 331 332 // returns bot info if the clientId matches a known bot, null otherwise: 333 match: function(client) { 334 //console.info('Monitor.live.data.bots.match(',client,')'); 335 336 if (client) { 337 for (let i=0; i<Monitor.live.data.bots._list.length; i++) { 338 const bot = Monitor.live.data.bots._list[i]; 339 for (let j=0; j<bot.rx.length; j++) { 340 if (client.match(new RegExp(bot.rx[j]))) { 341 return bot; // found a match 342 } 343 } 344 return null; // not found! 345 } 346 } 347 }, 348 349 // indicates if the list is loaded and ready to use: 350 _ready: false, 351 352 // the actual bot list is stored here: 353 _list: [] 354 }, 355 356 clients: { 357 // loads the list of known clients from a JSON file: 358 init: async function() { 359 //console.info('Monitor.live.data.clients.init()'); 360 361 // Load the list of known bots: 362 Monitor.live.status.showBusy("Loading known clients"); 363 const url = Monitor._baseDir + 'data/known-clients.json'; 364 try { 365 const response = await fetch(url); 366 if (!response.ok) { 367 throw new Error(`${response.status} ${response.statusText}`); 368 } 369 370 Monitor.live.data.clients._list = await response.json(); 371 Monitor.live.data.clients._ready = true; 372 373 } catch (error) { 374 Monitor.live.status.setError("Error while loading the known clients file: " + error.message); 375 } finally { 376 Monitor.live.status.hideBusy("Done."); 377 Monitor.live.data._dispatch('clients') 378 } 379 }, 380 381 // returns bot info if the clientId matches a known bot, null otherwise: 382 match: function(cid) { 383 //console.info('Monitor.live.data.clients.match(',cid,')'); 384 385 let match = {"n": "Unknown", "v": -1, "id": null}; 386 387 if (cid) { 388 Monitor.live.data.clients._list.find(client => { 389 let r = false; 390 for (let j=0; j<client.rx.length; j++) { 391 const rxr = cid.match(new RegExp(client.rx[j])); 392 if (rxr) { 393 match.n = client.n; 394 match.v = (rxr.length > 1 ? rxr[1] : -1); 395 match.id = client.id || null; 396 r = true; 397 break; 398 } 399 } 400 return r; 401 }); 402 } 403 404 return match; 405 }, 406 407 // indicates if the list is loaded and ready to use: 408 _ready: false, 409 410 // the actual bot list is stored here: 411 _list: [] 412 413 }, 414 415 platforms: { 416 // loads the list of known platforms from a JSON file: 417 init: async function() { 418 //console.info('Monitor.live.data.platforms.init()'); 419 420 // Load the list of known bots: 421 Monitor.live.status.showBusy("Loading known platforms"); 422 const url = Monitor._baseDir + 'data/known-platforms.json'; 423 try { 424 const response = await fetch(url); 425 if (!response.ok) { 426 throw new Error(`${response.status} ${response.statusText}`); 427 } 428 429 Monitor.live.data.platforms._list = await response.json(); 430 Monitor.live.data.platforms._ready = true; 431 432 } catch (error) { 433 Monitor.live.status.setError("Error while loading the known platforms file: " + error.message); 434 } finally { 435 Monitor.live.status.hideBusy("Done."); 436 Monitor.live.data._dispatch('platforms') 437 } 438 }, 439 440 // returns bot info if the browser id matches a known platform: 441 match: function(cid) { 442 //console.info('Monitor.live.data.platforms.match(',cid,')'); 443 444 let match = {"n": "Unknown", "id": null}; 445 446 if (cid) { 447 Monitor.live.data.platforms._list.find(platform => { 448 let r = false; 449 for (let j=0; j<platform.rx.length; j++) { 450 const rxr = cid.match(new RegExp(platform.rx[j])); 451 if (rxr) { 452 match.n = platform.n; 453 match.v = (rxr.length > 1 ? rxr[1] : -1); 454 match.id = platform.id || null; 455 r = true; 456 break; 457 } 458 } 459 return r; 460 }); 461 } 462 463 return match; 464 }, 465 466 // indicates if the list is loaded and ready to use: 467 _ready: false, 468 469 // the actual bot list is stored here: 470 _list: [] 471 472 }, 473 474 loadLogFile: async function(type, onLoaded = undefined) { 475 // console.info('Monitor.live.data.loadLogFile(',type,')'); 476 477 let typeName = ''; 478 let columns = []; 479 480 switch (type) { 481 case "srv": 482 typeName = "Server"; 483 columns = ['ts','ip','pg','id','typ','usr','client','ref']; 484 break; 485 case "log": 486 typeName = "Page load"; 487 columns = ['ts','ip','pg','id','usr','lt','ref','client']; 488 break; 489 case "tck": 490 typeName = "Ticker"; 491 columns = ['ts','ip','pg','id']; 492 break; 493 default: 494 console.warn(`Unknown log type ${type}.`); 495 return; 496 } 497 498 // Show the busy indicator and set the visible status: 499 Monitor.live.status.showBusy(`Loading ${typeName} log file …`); 500 501 // compose the URL from which to load: 502 const url = Monitor._baseDir + `logs/${Monitor._today}.${type}`; 503 //console.log("Loading:",url); 504 505 // fetch the data: 506 try { 507 const response = await fetch(url); 508 if (!response.ok) { 509 throw new Error(`${response.status} ${response.statusText}`); 510 } 511 512 const logtxt = await response.text(); 513 514 logtxt.split('\n').forEach((line) => { 515 if (line.trim() === '') return; // skip empty lines 516 const cols = line.split('\t'); 517 518 // assign the columns to an object: 519 const data = {}; 520 cols.forEach( (colVal,i) => { 521 colName = columns[i] || `col${i}`; 522 const colValue = (colName == 'ts' ? new Date(colVal) : colVal); 523 data[colName] = colValue; 524 }); 525 526 // register the visit in the model: 527 switch(type) { 528 case 'srv': 529 Monitor.live.data.model.registerVisit(data); 530 break; 531 case 'log': 532 Monitor.live.data.model.updateVisit(data); 533 break; 534 case 'tck': 535 Monitor.live.data.model.updateTicks(data); 536 break; 537 default: 538 console.warn(`Unknown log type ${type}.`); 539 return; 540 } 541 }); 542 543 if (onLoaded) { 544 onLoaded(); // callback after loading is finished. 545 } 546 547 } catch (error) { 548 Monitor.live.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 549 } finally { 550 Monitor.live.status.hideBusy("Done."); 551 } 552 } 553 }, 554 555 status: { 556 setText: function(txt) { 557 const el = document.getElementById('monitor__today__status'); 558 if (el && Monitor.live.status._errorCount <= 0) { 559 el.innerText = txt; 560 } 561 }, 562 563 setTitle: function(html) { 564 const el = document.getElementById('monitor__today__title'); 565 if (el) { 566 el.innerHTML = html; 567 } 568 }, 569 570 setError: function(txt) { 571 console.error(txt); 572 Monitor.live.status._errorCount += 1; 573 const el = document.getElementById('monitor__today__status'); 574 if (el) { 575 el.innerText = "An error occured. See the browser log for details!"; 576 el.classList.add('error'); 577 } 578 }, 579 _errorCount: 0, 580 581 showBusy: function(txt = null) { 582 Monitor.live.status._busyCount += 1; 583 const el = document.getElementById('monitor__today__busy'); 584 if (el) { 585 el.style.display = 'inline-block'; 586 } 587 if (txt) Monitor.live.status.setText(txt); 588 }, 589 _busyCount: 0, 590 591 hideBusy: function(txt = null) { 592 const el = document.getElementById('monitor__today__busy'); 593 Monitor.live.status._busyCount -= 1; 594 if (Monitor.live.status._busyCount <= 0) { 595 if (el) el.style.display = 'none'; 596 if (txt) Monitor.live.status.setText(txt); 597 } 598 } 599 } 600}; 601 602/* launch only if the Monitor admin panel is open: */ 603if (document.getElementById('monitor__admin')) { 604 Monitor.init(); 605}