diff --git a/admin.php b/admin.php index 11c6f1e..9ce6a51 100644 --- a/admin.php +++ b/admin.php @@ -46,15 +46,15 @@ class admin_plugin_botmon extends AdminPlugin { echo ''; // Beta warning message: - echo '
Please note: This plugin is still in the early stages of development and does not (yet) clean up its logs directory.
To avoid taking up too much space on your server, please remove older logs manually!
'; + echo '
Please note: This plugin is still in the early stages of development and does not (yet) clean up its logs directory.
You can clean up the old log files by clicking here, or by adding the cleanup script to your cron jobs.
'; /* Live tab */ echo '
'; echo '

Today

'; echo '
Loading …
'; echo '
'; - echo '
Visitor log'; - echo '
    '; + echo '
    Visitor logs'; + echo '
    '; echo '
    '; echo ''; echo '
    '; diff --git a/cleanup.php b/cleanup.php new file mode 100644 index 0000000..2f68919 --- /dev/null +++ b/cleanup.php @@ -0,0 +1,26 @@ +

    BotMon Cleanup Script

    + diff --git a/data/known-clients.json b/data/known-clients.json index 5b69c86..aa93358 100644 --- a/data/known-clients.json +++ b/data/known-clients.json @@ -3,6 +3,18 @@ "id": "opera", "rx": [ "\\sOpera\\/.*?Version\\/(\\S+)", "Opera\\/(\\S+)" ] }, + {"n": "Samsung Internet", + "id": "samsung", + "rx": [ "\\sSamsungBrowser\\/(\\S+)" ] + }, + {"n": "UC Browser", + "id": "uc", + "rx": [ "\\sUCBrowser\\/(\\d+\\.\\d+)[\\s\\.]"] + }, + {"n": "HuaweiBrowser", + "id": "huawei", + "rx": [ "\\sHuaweiBrowser\\/(\\d+\\.\\d+)[\\s\\.]", "\\/harmony360Browser\\/(\\d+\\.\\d+)[\\s\\.]"] + }, {"n": "Internet Explorer", "id": "msie", "rx": [ "\\sMSIE\\s(\\d+\\.\\d+b?);" ], @@ -16,9 +28,13 @@ "id": "msedge", "rx": [ "\\sEdg\\/(\\S+)", "\\sEdge\\/(\\S+)" ] }, + {"n": "Old Chrome", + "id": "chromeold", + "rx": [ "\\sChrome\\/(\\d\\d\\.\\d+)[\\.\\s;]" ] + }, {"n": "Chrome", "id": "chrome", - "rx": [ "\\sChrome\\/(\\S+)" ] + "rx": [ "\\sChrome\\/(1\\d\\d\\.\\d+)[\\.\\s;]" ] }, {"n": "Safari", "id": "safari", diff --git a/data/known-platforms.json b/data/known-platforms.json index 7231cf3..3148353 100644 --- a/data/known-platforms.json +++ b/data/known-platforms.json @@ -21,7 +21,18 @@ }, {"n": "Vintage Windows", "id": "winold", - "rx": [ "\\(Windows NT (\\d\\.\\d)[;\\s\\)]","Windows (\\d\\.\\d)[;\\s]" ], - "bot": 0.4 + "rx": [ "\\(Windows NT (\\d\\.\\d)[;\\s\\)]","Windows (\\d\\.\\d)[;\\s]" ] + }, + {"n": "Tizen", + "id": "tizen", + "rx": [ "\\sTizen\\s(\\d+\\.\\d+)[;\\s]" ] + }, + {"n": "HarmonyOS", + "id": "hmos", + "rx": [ "\\sHarmonyOS[;\\/\\s](\\d+\\.\\d)?[\\.\\)\\s]*" ] + }, + {"n": "Chromium", + "id": "chromium", + "rx": [ "Chromebook", "\\sCrOS\\s" ] } ] \ No newline at end of file diff --git a/data/rules.json b/data/rules.json new file mode 100644 index 0000000..c4368d8 --- /dev/null +++ b/data/rules.json @@ -0,0 +1,11 @@ +{ + "ip-ranges": [ + {"range": "191.177.0.0", "mask": 16, "bot": 50} + ], + "rules": [ + {"field": "_client", "item": "id", "op": "equals", "value": "chromeold", "bot": 30} + ], + "thresholds": { + "bot": 50 + } +} \ No newline at end of file diff --git a/img/android.svg b/img/android.svg new file mode 100644 index 0000000..4e58649 --- /dev/null +++ b/img/android.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/bing.svg b/img/bing.svg new file mode 100644 index 0000000..f50b683 --- /dev/null +++ b/img/bing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/brave.svg b/img/brave.svg new file mode 100644 index 0000000..8206e81 --- /dev/null +++ b/img/brave.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/chevron.svg b/img/chevron.svg new file mode 100644 index 0000000..5f0fd51 --- /dev/null +++ b/img/chevron.svg @@ -0,0 +1 @@ +chevron \ No newline at end of file diff --git a/img/chrome.svg b/img/chrome.svg new file mode 100644 index 0000000..f8987d7 --- /dev/null +++ b/img/chrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/chromeold.svg b/img/chromeold.svg new file mode 100644 index 0000000..31a88aa --- /dev/null +++ b/img/chromeold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/chromium.svg b/img/chromium.svg new file mode 100644 index 0000000..9d3bcf0 --- /dev/null +++ b/img/chromium.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/cogs.svg b/img/cogs.svg new file mode 100644 index 0000000..1595449 --- /dev/null +++ b/img/cogs.svg @@ -0,0 +1 @@ +cogs \ No newline at end of file diff --git a/img/ddg.svg b/img/ddg.svg new file mode 100644 index 0000000..5721e8c --- /dev/null +++ b/img/ddg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/ecosia.svg b/img/ecosia.svg new file mode 100644 index 0000000..fc60eed --- /dev/null +++ b/img/ecosia.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/firefox.png b/img/firefox.png new file mode 100644 index 0000000..d1f0292 Binary files /dev/null and b/img/firefox.png differ diff --git a/img/firefox.svg b/img/firefox.svg new file mode 100644 index 0000000..4b14151 --- /dev/null +++ b/img/firefox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/google.svg b/img/google.svg new file mode 100644 index 0000000..9004205 --- /dev/null +++ b/img/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/hmos.svg b/img/hmos.svg new file mode 100644 index 0000000..2a553c7 --- /dev/null +++ b/img/hmos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/huawei.png b/img/huawei.png new file mode 100644 index 0000000..89f3e5a Binary files /dev/null and b/img/huawei.png differ diff --git a/img/incognito.svg b/img/incognito.svg new file mode 100644 index 0000000..52eb818 --- /dev/null +++ b/img/incognito.svg @@ -0,0 +1 @@ +incognito \ No newline at end of file diff --git a/img/info.svg b/img/info.svg new file mode 100644 index 0000000..f1daa8f --- /dev/null +++ b/img/info.svg @@ -0,0 +1 @@ +information \ No newline at end of file diff --git a/img/ios.svg b/img/ios.svg new file mode 100644 index 0000000..81eab92 --- /dev/null +++ b/img/ios.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/ip4.svg b/img/ip4.svg new file mode 100644 index 0000000..cd42250 --- /dev/null +++ b/img/ip4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/ip6.svg b/img/ip6.svg new file mode 100644 index 0000000..36f09d1 --- /dev/null +++ b/img/ip6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/linux.svg b/img/linux.svg new file mode 100644 index 0000000..ad51cb7 --- /dev/null +++ b/img/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/localhost.svg b/img/localhost.svg new file mode 100644 index 0000000..3fed84c --- /dev/null +++ b/img/localhost.svg @@ -0,0 +1 @@ +localhost \ No newline at end of file diff --git a/img/macos.svg b/img/macos.svg new file mode 100644 index 0000000..04dc1c8 --- /dev/null +++ b/img/macos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/msedge.svg b/img/msedge.svg new file mode 100644 index 0000000..f56c0f7 --- /dev/null +++ b/img/msedge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/msie.svg b/img/msie.svg new file mode 100644 index 0000000..9c71290 --- /dev/null +++ b/img/msie.svg @@ -0,0 +1,16 @@ + + + + + + + image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/img/opera.svg b/img/opera.svg new file mode 100644 index 0000000..c038666 --- /dev/null +++ b/img/opera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/placeholder.svg b/img/placeholder.svg new file mode 100644 index 0000000..5c162e9 --- /dev/null +++ b/img/placeholder.svg @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/img/qwant.svg b/img/qwant.svg new file mode 100644 index 0000000..1893307 --- /dev/null +++ b/img/qwant.svg @@ -0,0 +1,53 @@ + + + + + + + + + diff --git a/img/robot.svg b/img/robot.svg new file mode 100644 index 0000000..6070578 --- /dev/null +++ b/img/robot.svg @@ -0,0 +1 @@ +robot \ No newline at end of file diff --git a/img/safari.png b/img/safari.png new file mode 100644 index 0000000..fbe7f7f Binary files /dev/null and b/img/safari.png differ diff --git a/img/safari.svg b/img/safari.svg new file mode 100644 index 0000000..8489163 --- /dev/null +++ b/img/safari.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/samsung.svg b/img/samsung.svg new file mode 100644 index 0000000..d3314de --- /dev/null +++ b/img/samsung.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/tizen.png b/img/tizen.png new file mode 100644 index 0000000..236e128 Binary files /dev/null and b/img/tizen.png differ diff --git a/img/uc.svg b/img/uc.svg new file mode 100644 index 0000000..5eba2f2 --- /dev/null +++ b/img/uc.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/user.svg b/img/user.svg new file mode 100644 index 0000000..d2459f5 --- /dev/null +++ b/img/user.svg @@ -0,0 +1 @@ +User Account \ No newline at end of file diff --git a/img/win11.svg b/img/win11.svg new file mode 100644 index 0000000..aaf6a7a --- /dev/null +++ b/img/win11.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/winold.png b/img/winold.png new file mode 100644 index 0000000..bac4773 Binary files /dev/null and b/img/winold.png differ diff --git a/img/winold.svg b/img/winold.svg new file mode 100644 index 0000000..afdb142 --- /dev/null +++ b/img/winold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/script.js b/script.js index a52dfcc..5ced5d2 100644 --- a/script.js +++ b/script.js @@ -1,11 +1,11 @@ /* DokuWiki BotMon Plugin Script file */ -/* 30.08.2025 - 0.1.6 - pre-release */ +/* 03.09.2025 - 0.1.7 - pre-release */ /* Authors: Sascha Leib */ const BotMon = { init: function() { - console.info('BotMon.init()'); + //console.info('BotMon.init()'); // find the plugin basedir: this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) @@ -62,6 +62,25 @@ const BotMon = { offset -= 60; } return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); + }, + + /* helper function to create a new element with all attributes and text content */ + _makeElement: function(name, atlist = undefined, text = undefined) { + var r = null; + try { + r = document.createElement(name); + if (atlist) { + for (let attr in atlist) { + r.setAttribute(attr, atlist[attr]); + } + } + if (text) { + r.textContent = text.toString(); + } + } catch(e) { + console.error(e); + } + return r; } } }; @@ -130,7 +149,7 @@ BotMon.live = { // event callback, after the client log has been loaded: _onClientLogLoaded: function() { - console.info('BotMon.live.data._onClientLogLoaded()'); + //console.info('BotMon.live.data._onClientLogLoaded()'); // chain the ticks file to load: BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded); @@ -139,7 +158,7 @@ BotMon.live = { // event callback, after the tiker log has been loaded: _onTicksLogLoaded: function() { - console.info('BotMon.live.data._onTicksLogLoaded()'); + //console.info('BotMon.live.data._onTicksLogLoaded()'); // analyse the data: BotMon.live.data.analytics.analyseAll(); @@ -150,7 +169,7 @@ BotMon.live = { // display the data: BotMon.live.gui.overview.make(); - console.log(BotMon.live.data.model._visitors); + //console.log(BotMon.live.data.model._visitors); }, @@ -199,7 +218,7 @@ BotMon.live = { // check if it already exists: let visitor = model.findVisitor(dat.id); if (!visitor) { - const bot = BotMon.live.data.bots.match(dat.client); + const bot = BotMon.live.data.bots.match(dat.agent); model._visitors.push(dat); visitor = dat; @@ -209,8 +228,8 @@ BotMon.live = { visitor._pageViews = []; // array of page views visitor._hasReferrer = false; // has at least one referrer visitor._jsClient = false; // visitor has been seen logged by client js as well - visitor._client = bot ?? BotMon.live.data.clients.match(dat.client) ?? null; // client info (browser, bot, etc.) - visitor._platform = BotMon.live.data.platforms.match(dat.client); // platform info + visitor._client = bot ?? BotMon.live.data.clients.match(dat.agent) ?? null; // client info (browser, bot, etc.) + visitor._platform = BotMon.live.data.platforms.match(dat.agent); // platform info // known bots get the bot ID as identifier: if (bot) visitor.id = bot.id; @@ -343,7 +362,7 @@ BotMon.live = { totalPageViews: 0, bots: { known: 0, - likely: 0, + suspected: 0, human: 0, users: 0 } @@ -352,7 +371,7 @@ BotMon.live = { // sort the visits by type: groups: { knownBots: [], - likelyBots: [], + suspectedBots: [], humans: [], users: [] }, @@ -406,8 +425,8 @@ BotMon.live = { // decide based on the score: if (botScore >= 0.5) { - this.data.bots.likely += 1; - this.groups.likelyBots.push(v); + this.data.bots.suspected += 1; + this.groups.suspectedBots.push(v); } else { this.data.bots.human += 1; this.groups.humans.push(v); @@ -496,7 +515,7 @@ BotMon.live = { } }, - // returns bot info if the clientId matches a known bot, null otherwise: + // returns bot info if the user-agent matches a known bot, null otherwise: match: function(cid) { //console.info('BotMon.live.data.clients.match(',cid,')'); @@ -598,15 +617,15 @@ BotMon.live = { switch (type) { case "srv": typeName = "Server"; - columns = ['ts','ip','pg','id','typ','usr','client','ref']; + columns = ['ts','ip','pg','id','typ','usr','agent','ref']; break; case "log": typeName = "Page load"; - columns = ['ts','ip','pg','id','usr','lt','ref','client']; + columns = ['ts','ip','pg','id','usr','lt','ref','agent']; break; case "tck": typeName = "Ticker"; - columns = ['ts','ip','pg','id','client']; + columns = ['ts','ip','pg','id','agent']; break; default: console.warn(`Unknown log type ${type}.`); @@ -671,6 +690,10 @@ BotMon.live = { }, gui: { + init: function() { + // init the lists view: + this.lists.init(); + }, overview: { make: function() { @@ -694,7 +717,7 @@ BotMon.live = {
    Bots vs. Humans
    Known bots:${data.bots.known}
    -
    Likely bots:${data.bots.likely}
    +
    Suspected bots:${data.bots.suspected}
    Probably humans:${data.bots.human}
    Registered users:${data.bots.users}
    @@ -751,8 +774,190 @@ BotMon.live = { if (txt) BotMon.live.gui.status.setText(txt); } } + }, + + lists: { + init: function() { + + const parent = document.getElementById('botmon__today__visitorlists'); + if (parent) { + + for (let i=0; i < 4; i++) { + + // change the id and title by number: + let listTitle = ''; + let listId = ''; + switch (i) { + case 0: + listTitle = "Registered users"; + listId = 'users'; + break; + case 1: + listTitle = "Probably humans"; + listId = 'humans'; + break; + case 2: + listTitle = "Suspected bots"; + listId = 'suspectedBots'; + break; + case 3: + listTitle = "Known bots"; + listId = 'knownBots'; + break; + default: + console.warn('Unknwon list number.'); + } + + const details = BotMon.t._makeElement('details', { + 'data-group': listId, + 'data-loaded': false + }); + details.appendChild(BotMon.t._makeElement('summary', + undefined, + listTitle + )); + details.addEventListener("toggle", this._onDetailsToggle); + + parent.appendChild(details); + + } + } + }, + + _onDetailsToggle: function(e) { + console.info('BotMon.live.gui.lists._onDetailsToggle()'); + + const target = e.target; + + if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet + target.setAttribute('data-loaded', 'loading'); + + const fillType = target.getAttribute('data-group'); + const fillList = BotMon.live.data.analytics.groups[fillType]; + if (fillList && fillList.length > 0) { + + const ul = BotMon.t._makeElement('ul'); + + fillList.forEach( (it) => { + ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); + }); + + target.appendChild(ul); + target.setAttribute('data-loaded', 'true'); + } else { + target.setAttribute('data-loaded', 'false'); + } + + } + }, + + _makeVisitorItem: function(data, type) { + + // shortcut for neater code: + const make = BotMon.t._makeElement; + + const li = make('li'); // root list item + const details = make('details'); + const summary = make('summary'); + details.appendChild(summary); + + const span1 = make('span'); /* left-hand group */ + + let typeDescr = "Seen by: " + ( data.typ == 'php' ? "Server-only": "Server + Client"); + span1.appendChild(make('span', { /* Type */ + 'class': 'icon type type_' + data.typ, + 'title': typeDescr + }, data.typ)); + + span1.appendChild(make('span', { /* ID */ + 'class': 'id' + }, data.id)); + + const platformName = (data._platform ? data._platform.n : 'Unknown'); + span1.appendChild(make('span', { /* Platform */ + 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), + 'title': "Platform: " + platformName + }, platformName)); + + const clientName = (data._client ? data._client.n: 'Unknown') + span1.appendChild(make('span', { /* Client */ + 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), + 'title': "Client: " + clientName + }, clientName)); + + let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); + if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; + span1.appendChild(make('span', { /* IP-Address */ + 'class': 'icon ipaddr ip' + ipType, + 'title': "IP-Address: " + data.ip + }, data.ip)); + + summary.appendChild(span1); + const span2 = make('span'); /* right-hand group */ + + span2.appendChild(make('time', { /* Last seen */ + 'data-field': 'last-seen', + 'datetime': (data._lastSeen ? data._lastSeen : 'unknown') + }, (data._lastSeen ? data._lastSeen.getHours() + ':' + data._lastSeen.getMinutes() + ':' + data._lastSeen.getSeconds() : 'Unknown'))); + + summary.appendChild(span2); + + // create expanable section: + + const dl = make('dl', {'class': 'visitor_details'}); + + dl.appendChild(make('dt', {}, "Client:")); /* client */ + dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, + clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); + + dl.appendChild(make('dt', {}, "Platform:")); /* platform */ + dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, + platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); + + dl.appendChild(make('dt', {}, "IP-Address:")); + dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); + + if ((data._lastSeen - data._firstSeen) < 1) { + dl.appendChild(make('dt', {}, "Seen:")); + dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); + } else { + dl.appendChild(make('dt', {}, "First seen:")); + dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); + dl.appendChild(make('dt', {}, "Last seen:")); + dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); + } + + dl.appendChild(make('dt', {}, "User-Agent:")); + dl.appendChild(make('dd', {'class': 'agent' + ipType}, data.agent)); + + dl.appendChild(make('dt', {}, "Visited pages:")); + const pagesDd = make('dd', {'class': 'pages'}); + const pageList = make('ul'); + + data._pageViews.forEach( (page) => { + const pgLi = make('li'); + + let visitTimeStr = "Bounce"; + const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); + if (visitDuration > 0) { + visitTimeStr = Math.floor(visitDuration / 1000) + "s"; + } + + pgLi.appendChild(make('span', {}, page.pg)); + pgLi.appendChild(make('span', {}, page.ref)); + pgLi.appendChild(make('span', {}, visitTimeStr)); + pageList.appendChild(pgLi); + }); + + pagesDd.appendChild(pageList); + dl.appendChild(pagesDd); + + details.appendChild(dl); + + li.appendChild(details); + return li; + } } - } }; diff --git a/style.less b/style.less index e78fad2..a2a6331 100644 --- a/style.less +++ b/style.less @@ -4,8 +4,24 @@ margin: .25rem 0; } + /* grid layout classes (taken from the Ad-Hoc Wrap plugin) */ + .grid-2-columns, + .grid-3-columns { + display: grid; + } + .grid-2-columns { + grid-template-columns: 1fr 1fr; + grid-gap: 0 .5em; + } + .grid-3-columns { + grid-template-columns: 1fr 1fr 1fr; + grid-gap: 0 .33em; + } + + /* the "today" tab: */ #botmon__today { + /* item header */ header { background-color: #F0F0F0; color: #333; @@ -43,47 +59,181 @@ } } + /* Content */ #botmon__today__content { - & > details { + & details { & { margin: 0 0 1pt 0; text-align: left; } summary { & { + display: flex; + justify-content: flex-start; + align-items: center; + column-gap: .25em; font-weight: bold; font-size: 1rem; line-height: 1.5; - padding: .25rem .5rem; - background-color: #F0F0F0; - color: #333; - border: #CCC solid 1px; - display: flex; - justify-content: space-between; margin: 0; + padding: .25em; + color: #333; + cursor: pointer; } &::marker, &::before { - content: ''; + content: none; display: none; } - &::after { - content: '+'; + &::before { + content: ''; display: inline-block; - color: @ini_link; - background-color: transparent; + width: 1.25em; height: 1.25em; + background: transparent url('img/chevron.svg') center no-repeat; + background-size: 1.25em; + transform: rotate(-90deg); + transition-duration: .25s; } } &[open] { - summary::after { - content: '﹘'; + summary::before { + transform: rotate(0deg); } } & > div { padding: .5rem; + border: #CCC solid 1px; + border-top-width: 0; + border-radius: 0 0 .25rem .25rem; } + & details summary { + background-color: transparent; + border: transparent none 0; + } + } + & > details > summary { + background-color: #F0F0F0; + border: #CCC solid 1px; } } + /* visitor lists: */ + #botmon__today__visitorlists { + details ul { + margin: 0; + padding: 0; + list-style: none; + } + details ul > li { + margin: 0 0 0 .75rem; + padding: 0; + color: #000; + } + details ul > li > details { + border: red dotted 1px; + } + details ul > li > details > summary { + display: flex; + justify-content: space-between; + align-items: center; + column-gap: .5em; + font-weight: normal; + font-size: 1rem; + line-height: 1.5; + border: blue dashed 1px; + } + + details ul > li > details > summary > span { + display: flex; + column-gap: .25em; + } + details ul > li > details > summary > span:first-child { + flex-grow: 1; + } + details ul > li > details > summary > span > span { + height: 1.5em; + overflow: hidden; + } + details ul > li > details > summary > span > span::before { + content: ''; + display: inline-block; + width: 1.25em; height: 1em; + text-align: center; + background: transparent url('img/placeholder.svg') center no-repeat; + background-size: 1em; + } + details ul > li > details > summary > span > span.icon { + width: 1.25em; + overflow: hidden; + } + details ul > li > details > summary > span > span[title] { + cursor: help; + } + + dl.visitor_details { + & { + border: green dotted 1px; + display: grid; + grid-template-columns: min-content auto; + } + dt { + grid-column: 1; + white-space: nowrap; + } + dd { + grid-column: 2; + display: inline-block; + background-color: transparent; + } + } + dd.has_icon::before { + content: ''; + display: inline-block; + width: 1.25em; height: 1.25em; + background: transparent url('img/placeholder.svg') center no-repeat; + background-size: 1em; + } + + /* type icons */ + span.type_dw::before, dd.type_dw::before { background-image: url('img/incognito.svg'); } + span.type_php::before, dd.type_php::before { background-image: url('img/cogs.svg'); } + + /* platform icons */ + span.platform_macos::before, dd.platform_macos::before { background-image: url('img/macos.svg'); } + span.platform_win10::before, dd.platform_win10::before { background-image: url('img/win11.svg'); } + span.platform_linux::before, dd.platform_linux::before { background-image: url('img/linux.svg'); } + span.platform_ios::before, dd.platform_ios::before { background-image: url('img/ios.svg'); } + span.platform_android::before, dd.platform_android::before { background-image: url('img/android.svg'); } + span.platform_winold::before, dd.platform_winold::before { background-image: url('img/winold.svg'); } + span.platform_tizen::before, dd.platform_tizen::before { background-image: url('img/tizen.png'); } + span.platform_hmos::before, dd.platform_hmos::before { background-image: url('img/hmos.svg'); } + span.platform_chromium::before, dd.platform_chromium::before { background-image: url('img/chromium.svg'); } + + + /* browser icons */ + span.client_opera::before, dd.client_opera::before { background-image: url('img/opera.svg'); } + span.client_msie::before, dd.client_msie::before { background-image: url('img/msie.svg'); } + span.client_brave::before, dd.client_brave::before { background-image: url('img/brave.svg'); } + span.client_msedge::before, dd.client_msedge::before { background-image: url('img/msedge.svg'); } + span.client_chrome::before, dd.client_chrome::before { background-image: url('img/chrome.svg'); } + span.client_chromeold::before, dd.client_chromeold::before { background-image: url('img/chromeold.svg'); } + span.client_safari::before, dd.client_safari::before { background-image: url('img/safari.svg'); } + span.client_ddg::before, dd.client_ddg::before { background-image: url('img/ddg.svg'); } + span.client_firefox::before, dd.client_firefox::before { background-image: url('img/firefox.svg'); } + span.client_samsung::before, dd.client_samsung::before { background-image: url('img/samsung.svg'); } + span.client_uc::before, dd.client_uc::before { background-image: url('img/uc.svg'); } + span.client_huawei::before, dd.client_huawei::before { background-image: url('img/huawei.png'); } + + /* ip address type */ + span.ip6::before, dd.ip6::before { background-image: url('img/ip6.svg'); } + span.ip4::before, dd.ip4::before { background-image: url('img/ip4.svg'); } + span.ip0::before, dd.ip0::before { background-image: url('img/localhost.svg'); } + + /* user agent */ + span.agent::before { background-image: url('img/info.svg'); } + + } + + /* item footer */ footer { & { display: flex; @@ -135,18 +285,23 @@ color: #adadb3; } dd:nth-child(even) { - background-color: #4E4E50; + background-color: #333337; } } - #botmon__today__content > details summary { - & { - background-color: #0c0c0d; - color: #adadb3; - border-color: #666; + #botmon__today__content > details { + summary { + & { + background-color: #0c0c0d; + color: #adadb3; + border-color: #666; + } + &::after { + color: #76b0fd; + } } - &::after { - color: #76b0fd; + & > div { + border-color: #666; } } @@ -165,4 +320,13 @@ } } } -} \ No newline at end of file +} +/* layout overrides for narrow screens: */ +@media (max-width: 670px) { + #botmon__admin { + .grid-2-columns, + .grid-3-columns { + grid-template-columns: 100%; + } + } +}