diff --git a/action.php b/action.php
index 95d3980..6d790a5 100644
--- a/action.php
+++ b/action.php
@@ -54,26 +54,11 @@ class action_plugin_monitor extends DokuWiki_Action_Plugin {
];
/* Write out client info to a server log: */
-
- // what is the session identifier?
- $sessionId = $_COOKIE['DokuWiki'] ?? null;
- if (!$sessionId) {
- if (session_id()) {
- // if a session ID is set, use it
- $sessionId = 'P:' . session_id();
- } else {
- // if no session ID is set, use an empty string
- $sessionId = '';
- }
- } else {
- // if no cookie is set, use the session ID
- $sessionId = 'D:' . $sessionId;
- }
-
$logArr = Array(
$_SERVER['REMOTE_ADDR'] ?? '', /* remote IP */
$INFO['id'] ?? '', /* page ID */
- $sessionId, /* Session ID */
+ $_COOKIE['DokuWiki'] ?? null, /* Dokuwiki Session ID */
+ session_id(), /* PHP Session ID */
$username,
$_SERVER['HTTP_USER_AGENT'] ?? '' /* User agent */
);
diff --git a/admin.php b/admin.php
new file mode 100644
index 0000000..9354857
--- /dev/null
+++ b/admin.php
@@ -0,0 +1,56 @@
+
+*/
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+**/
+class admin_plugin_monitor extends AdminPlugin {
+
+ /**
+ * Return the path to the icon being displayed in the main admin menu.
+ *
+ * @return string full path to the icon file
+ **/
+ public function getMenuIcon() {
+ $plugin = $this->getPluginName();
+ return DOKU_PLUGIN . $plugin . '/img/admin.svg';
+ }
+
+ /**
+ * output appropriate html
+ */
+ public function html() {
+
+ $svg = ' ';
+
+ /* Plugin Headline */
+ echo '
';
+ echo '
Monitor Plugin ';
+
+ /* tab navigation */
+ echo '
';
+ echo '';
+ echo '';
+ echo 'Today ';
+ echo ' ';
+
+ /* Live tab */
+ echo '
';
+ echo 'Today ';
+ echo '';
+ echo '
';
+ echo '' . $svg . 'Starting up … ';
+ echo ' ';
+ echo '
';
+
+ }
+}
\ No newline at end of file
diff --git a/data/known-bots.json b/data/known-bots.json
new file mode 100644
index 0000000..bbfddf1
--- /dev/null
+++ b/data/known-bots.json
@@ -0,0 +1,50 @@
+[
+ {"id": "google",
+ "n": "GoogleBot",
+ "r": ["Googlebot"],
+ "rx": ["Googlebot\\/(\\d+\\.\\d+);"],
+ "url": "http://www.google.com/bot.html"
+ },
+ {"id": "googlead",
+ "n": "Google AdsBot",
+ "r": ["AdsBot-Google", "AdsBot-Google-Mobile", "Mediapartners-Google"],
+ "rx": ["AdsBot-Google;","AdsBot-Google-Mobile;", "Mediapartners-Google\\/(\\d+\\.\\d+);"],
+ "url": "http://www.google.com/mobile/adsbot.html"
+ },
+ {"id": "googleapi",
+ "n": "Google APIs",
+ "r": ["APIs-Google"],
+ "rx": ["APIs-Google"],
+ "url": "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers"
+ },
+ {"id": "bing",
+ "n": "Bingbot",
+ "r": ["bingbot"],
+ "rx": ["bingbot\\/(\\d+\\.\\d+);"],
+ "url": "http://www.bing.com/bingbot.htm"
+ },
+ {"id": "apple",
+ "n": "Applebot",
+ "r": ["Applebot"],
+ "rx": ["Applebot\\/(\\d+\\.\\d+);"],
+ "url": "http://www.apple.com/go/applebot"
+ },
+ {"id": "openai",
+ "n": "OpenAI/ChatGPT Bots",
+ "r": ["OAI-SearchBot", "ChatGPT-User", "GPTBot"],
+ "rx": ["OAI-SearchBot\\/(\\d+\\.\\d+);", "ChatGPT-User\\/(\\d+\\.\\d+);", "GPTBot\\/(\\d+\\.\\d+);"],
+ "url": "https://platform.openai.com/docs/bots/"
+ },
+ {"id": "yandex",
+ "n": "Yandex Bots",
+ "r": ["YandexBot", "YandexAdNet", "YandexBlogs", "YandexImages", "YandexImageResizer", "YandexMarket", "YandexMedia", "YandexOntoDB", "YandexSitelinks","YandexSpravBot", "YandexVertis", "YandexVerticals", "YandexVideo", "YandexWebmaster", "YandexComBot"],
+ "rx": ["Yandex\\w+\\/(\\d+\\.\\d+);"],
+ "url": "http://yandex.com/bots"
+ },
+ {"id": "seznam",
+ "n": "SeznamBot Crawler",
+ "r": ["SeznamBot"],
+ "rx": ["SeznamBot\\/(\\d+\\.\\d+);"],
+ "url": "https://o-seznam.cz/napoveda/vyhledavani/en/seznambot-crawler/"
+ }
+]
\ No newline at end of file
diff --git a/img/admin.svg b/img/admin.svg
new file mode 100644
index 0000000..031843f
--- /dev/null
+++ b/img/admin.svg
@@ -0,0 +1 @@
+Monitor
\ No newline at end of file
diff --git a/img/spinner.svg b/img/spinner.svg
new file mode 100644
index 0000000..733fb91
--- /dev/null
+++ b/img/spinner.svg
@@ -0,0 +1,168 @@
+
\ No newline at end of file
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..d276ce2
--- /dev/null
+++ b/script.js
@@ -0,0 +1,233 @@
+/* DokuWiki Monitor Plugin Script file */
+/* 29.08.2025 - 1.0.5 - initial release */
+/* Authors: Sascha Leib */
+
+const Monitor = {
+
+ init: function() {
+ //console.info('Monitor.init()');
+
+ // find the plugin basedir:
+ this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/'))
+ + '/plugins/monitor/';
+
+ // read the page language from the DOM:
+ this._lang = document.getRootNode().documentElement.lang || this._lang;
+
+ // get the time offset:
+ this._timeDiff = Monitor.t._getTimeOffset();
+
+ // init the sub-objects:
+ Monitor.t._callInit(this);
+ },
+
+ _baseDir: null,
+ _lang: 'en',
+ _today: (new Date()).toISOString().slice(0, 10),
+ _timeDiff: '',
+
+ /* internal tools */
+ t: {
+
+ /* helper function to call inits of sub-objects */
+ _callInit: function(obj) {
+ //console.info('Monitor.t._callInit(obj=',obj,')');
+
+ /* call init / _init on each sub-object: */
+ Object.keys(obj).forEach( (key,i) => {
+ const sub = obj[key];
+ let init = null;
+ if (typeof sub === 'object' && sub.init) {
+ init = sub.init;
+ }
+
+ // bind to object
+ if (typeof init == 'function') {
+ const init2 = init.bind(sub);
+ init2(obj);
+ }
+ });
+ },
+
+ /* helper function to calculate the time difference to UTC: */
+ _getTimeOffset: function() {
+ const now = new Date();
+ let offset = now.getTimezoneOffset(); // in minutes
+ const sign = Math.sign(offset); // +1 or -1
+ offset = Math.abs(offset); // always positive
+
+ let hours = 0;
+ while (offset >= 60) {
+ hours += 1;
+ offset -= 60;
+ }
+ return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : '');
+ }
+ }
+}
+
+/* everything specific to the "Today" tab is self-contained here: */
+Monitor.today = {
+ init: function() {
+ //console.info('Monitor.today.init()');
+
+ // set the title:
+ const tDiff = (Monitor._timeDiff != '' ? ` (${Monitor._timeDiff})` : ' (UTC )' );
+ Monitor.today.status.setTitle(`Showing visits for ${Monitor._today} ${tDiff}`);
+
+ // init sub-objects:
+ Monitor.t._callInit(this);
+ },
+
+ data: {
+ init: function() {
+ //console.info('Monitor.today.data.init()');
+
+ // call sub-inits:
+ Monitor.t._callInit(this);
+
+ // load the first log file:
+ Monitor.today.data.loadLogFile('srv');
+ },
+
+ bots: {
+ // loads the list of known bots from a JSON file:
+ init: async function() {
+ //console.info('Monitor.today.data.bots.init()');
+
+ // Load the list of known bots:
+ Monitor.today.status.showBusy("Loading known bots …");
+ const url = Monitor._baseDir + 'data/known-bots.json';
+
+ console.log(url);
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`${response.status} ${response.statusText}`);
+ }
+
+ Monitor.today.data.bots._list = await response.json();
+ Monitor.today.data.bots._ready = true;
+
+ // TODO: allow using the bots list...
+ } catch (error) {
+ Monitor.today.status.setError("Error while loading the ’known bots’ file: " + error.message);
+ } finally {
+ Monitor.today.status.hideBusy("Done.");
+ }
+ },
+
+ // returns bot info if the clientId matches a known bot, null otherwise:
+ match: function(clientId) {
+
+ // TODO!
+ },
+
+ // indicates if the list is loaded and ready to use:
+ _ready: false,
+
+ // the actual bot list is stored here:
+ _list: []
+ },
+
+ loadLogFile: async function(type) {
+ console.info('Monitor.today.data.loadLogFile(',type,')');
+
+ let typeName = '';
+ let columns = [];
+
+ switch (type) {
+ case "srv":
+ typeName = "Server";
+ columns = ['ts','ip','pg','id','usr','client'];
+ break;
+ break;
+ case "log":
+ typeName = "Page load";
+ columns = ['ts','ip','pg','id','usr'];
+ break;
+ case "tck":
+ typeName = "Ticker";
+ columns = ['ts','ip','pg','id'];
+ break;
+ default:
+ console.warn(`Unknown log type ${type}.`);
+ return;
+ }
+
+ // Load the list of known bots:
+ Monitor.today.status.showBusy(`Loading ${typeName} log file …`);
+
+ const url = Monitor._baseDir + `logs/${Monitor._today}.${type}`;
+ console.log("Loading:",url);
+
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`${response.status} ${response.statusText}`);
+ }
+
+ const events = await response.text();
+ console.log(events);
+ //Monitor.today.data.serverEvents._ready = true;
+
+ // TODO: parse the file...
+ } catch (error) {
+ Monitor.today.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`);
+ } finally {
+ Monitor.today.status.hideBusy("Done.");
+ }
+ }
+ },
+
+ status: {
+ setText: function(txt) {
+ const el = document.getElementById('monitor__today__status');
+ if (el && Monitor.today.status._errorCount <= 0) {
+ el.innerText = txt;
+ }
+ },
+
+ setTitle: function(html) {
+ const el = document.getElementById('monitor__today__title');
+ if (el) {
+ el.innerHTML = html;
+ }
+ },
+
+ setError: function(txt) {
+ console.error(txt);
+ Monitor.today.status._errorCount += 1;
+ const el = document.getElementById('monitor__today__status');
+ if (el) {
+ el.innerText = "An error occured. See the browser log for details!";
+ el.classList.add('error');
+ }
+ },
+ _errorCount: 0,
+
+ showBusy: function(txt = null) {
+ Monitor.today.status._busyCount += 1;
+ const el = document.getElementById('monitor__today__busy');
+ if (el) {
+ el.style.display = 'inline-block';
+ }
+ if (txt) Monitor.today.status.setText(txt);
+ },
+ _busyCount: 0,
+
+ hideBusy: function(txt = null) {
+ const el = document.getElementById('monitor__today__busy');
+ Monitor.today.status._busyCount -= 1;
+ if (Monitor.today.status._busyCount <= 0) {
+ if (el) el.style.display = 'none';
+ if (txt) Monitor.today.status.setText(txt);
+ }
+ }
+ }
+}
+
+/* check if the nustat admin panel is open: */
+if (document.getElementById('monitor__admin')) {
+ Monitor.init();
+}
\ No newline at end of file
diff --git a/style.less b/style.less
new file mode 100644
index 0000000..54e5365
--- /dev/null
+++ b/style.less
@@ -0,0 +1,49 @@
+#monitor__admin {
+
+ section[role="tabpanel"] {
+ margin: .25rem 0;
+ }
+
+ #monitor__today {
+
+ header {
+ & {
+ background-color: #FBFAF9;
+ color: #333;
+ border: #EEE solid 1px;
+ border-radius: .25rem .25rem 0 0;
+ margin: 0 0 .25rem 0;
+ }
+ h3 {
+ font-size: 1rem;
+ padding: .25em .5em; margin: 0;
+ }
+ }
+ footer {
+ & {
+ display: flex;
+ align-items: center;
+ column-gap: .25rem;
+ background-color: #FBFAF9;
+ color: #333;
+ border: #EEE solid 1px;
+ border-radius: 0 0 .25rem .25rem;
+ margin: .25rem 0 0 0;
+ padding: .25rem .5rem;
+ }
+ & > svg {
+ width: 1.25em; height: 1.25em;
+ fill: #333;
+ flex-shrink: 0;
+ }
+ & > span {
+ font-size: .96rem;
+ line-height: 1.25rem;
+ }
+ & > span.error {
+ color: #961D1B;
+ font-weight: bold;
+ }
+ }
+ }
+}
\ No newline at end of file