From 6c5ce7e8f8cdf377a8b5982442f8e5f9ba21cf26 Mon Sep 17 00:00:00 2001 From: chromoxdor <33860956+chromoxdor@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:33:30 +0100 Subject: [PATCH] [LOG] refining the filter - Added highlight filter phrases - Added ordered AND operator (&) - Added exclusion (!) --- src/src/CustomBuild/define_plugin_sets.h | 3 + src/src/Static/WebStaticData.h | 77 +++++++++++-- static/fetch_and_parse_log.js | 138 ++++++++++++++++++++--- static/github_clipboard.js | 11 +- 4 files changed, 201 insertions(+), 28 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 14983ba0a..79b90d6b6 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -464,6 +464,9 @@ To create/register a plugin, you have to : #ifndef FEATURE_RULES_EASY_COLOR_CODE #define FEATURE_RULES_EASY_COLOR_CODE 1 #endif + #ifndef WEBSERVER_INCLUDE_JS + #define WEBSERVER_INCLUDE_JS + #endif #endif #if FEATURE_FHEM diff --git a/src/src/Static/WebStaticData.h b/src/src/Static/WebStaticData.h index a0995dbed..f5ea29d48 100644 --- a/src/src/Static/WebStaticData.h +++ b/src/src/Static/WebStaticData.h @@ -341,12 +341,12 @@ static const char jsClipboardCopyPart2[] PROGMEM = { // "if(t==null){i= max_loop + 1;" "if(t==null){i=101;" "}else{" - "cb+=t.innerHTML.replace(/<[Bb][Rr]\\s*\\/?>/gim,'\\n')+'" + "const visibleLines=Array.from(t.children).filter((e=>null!==e.offsetParent)).map((e=>e.innerHTML));visibleLines.length>0&&(cb+=visibleLines.join('\\n').replace(/<[Bb][Rr]\\s*\\/?>/gim,'\\n')+'" }; //Fix HTML static const char jsClipboardCopyPart3[] PROGMEM = { - "';}}" + "');}}" "cb=cb.replace(/<\\/[Dd][Ii][Vv]\\s*\\/?>/gim,'\\n');" "cb=cb.replace(/<[^>]*>/gim,'');" "var ti=document.createElement('textarea');" @@ -405,14 +405,71 @@ static const char DATA_UPDATE_SENSOR_VALUES_DEVICE_PAGE_JS[] PROGMEM = { #ifdef WEBSERVER_INCLUDE_JS static const char DATA_FETCH_AND_PARSE_LOG_JS[] PROGMEM = { - "function elId(e){return document.getElementById(e)}" - "function getBrowser(){var e,o=navigator.userAgent,r=o.match(/(opera|chrome|safari|firefox|msie|trident(?=\\/))\\/?\\s*(\\d+)/i)||[];return/trident/i.test(r[1])?{name:'IE',version:(e=/\brv[ :]+(\\d+)/g.exec(o)||[])[1]||''}:'Chrome'===r[1]&&null!=(e=o.match(/\bOPR|Edge\\/(\\d+)/))?{name:'Opera',version:e[1]}:(r=r[2]?[r[1],r[2]]:[navigator.appName,navigator.appVersion,'-?'],null!=(e=o.match(/version\\/(\\d+)/i))&&r.splice(1,1,e[1]),{name:r[0],version:r[1]})}" - "var browser=getBrowser(),currentBrowser=browser.name+browser.version;textToDisplay=(browser.name=browser.version<12)?'Error: '+currentBrowser+' is not supported! Please try a modern web browser.':'Fetching log entries...',elId('copyText_1').innerHTML=textToDisplay,loopDeLoop(1e3,0);var logLevel=['Unused','Error','Info','Debug','Debug More','Undefined','Undefined','Undefined','Undefined','Debug Dev'];" - "function loopDeLoop(e,o){let r='copyText_1';isNaN(o)&&(o=1),null==e&&(e=1e3),scrolling_type=e<=500?'auto':'smooth';var n,t,i='',l=0," - "s=setInterval(function(){if(l>0){clearInterval(s);return}++o>1?l=1:fetch('/logjson').then(function(o){if(200!==o.status){console.log('Looks like there was a problem. Status Code: '+o.status);return}o.json()" - ".then(function(o){var l;for(null==t&&(t=''),n=0;n'+o.Log.Entries[n].timestamp+': '+o.Log.Entries[n].text+'')}e=o.Log.TTL,''!==t&&('Fetching log entries...'==elId(r).innerHTML&&(elId(r).innerHTML=''),elId(r).innerHTML+=t),t='',!0==(autoscroll_on=elId('autoscroll').checked)&&''!==i&&elId(i).scrollIntoView({behavior:scrolling_type}),elId('current_loglevel').innerHTML='Logging: '+logLevel[o.Log.SettingsWebLogLevel]+' ('+o.Log.SettingsWebLogLevel+')',clearInterval(s)," - "loopDeLoop(e,0)})}).catch(function(o){elId(r).innerHTML+='
>> '+o.message+' <<
',autoscroll_on=elId('autoscroll').checked,elId(r).scrollTop=elId(r).scrollHeight,e=5e3,clearInterval(s)," - "loopDeLoop(e,0)}),l=1},e)}" +"function elId(e){return document.getElementById(e)}" +"function getBrowser(){var e,t=navigator.userAgent,o=t.match(/(opera|chrome|safari|firefox|msie|trident(?=\\/))\\/?\\s*(\\d+)/i)||[];" +"return/trident/i.test(o[1])?{name:'IE',version:(e=/\\brv[ :]+(\\d+)/g.exec(t)||[])[1]||''}:" +"'Chrome'===o[1]&&null!=(e=t.match(/\\bOPR|Edge\\/(\\d+)/))?" +"{name:'Opera',version:e[1]}:" +"(o=o[2]?[o[1],o[2]]:[navigator.appName,navigator.appVersion,'-?']," +"null!=(e=t.match(/version\\/(\\d+)/i))&&o.splice(1,1,e[1]),{name:o[0],version:o[1]})}" +"var browser=getBrowser(),currentBrowser=browser.name+browser.version;" +"textToDisplay=(browser.name=browser.version<12)?" +"'Error: '+currentBrowser+' is not supported! Please try a modern web browser.':" +"'Fetching log entries...';" +"elId('copyText_1').innerHTML=textToDisplay;" +"loopDeLoop(1000,0);" +"var logLevel=['Unused','Error','Info','Debug','Debug More','Undefined','Undefined','Undefined','Undefined','Debug Dev'];" +"function loopDeLoop(e,t){let o='copyText_1';" +"isNaN(t)&&(t=1),null==e&&(e=1000),scrolling_type=e<=500?'auto':'smooth';" +"var r,n,i='',l=0,s=setInterval(function(){" +"if(l>0){clearInterval(s);return}" +"++t>1?l=1:fetch('/logjson').then(function(t){" +"if(200!==t.status){console.log('Looks like there was a problem. Status Code: '+t.status);return}" +"t.json().then(function(t){var l;" +"for(null==n&&(n=''),r=0;r'+t.Log.Entries[r].timestamp+': '+" +"t.Log.Entries[r].text+'')}" +"e=t.Log.TTL," +"''!==n&&('Fetching log entries...'==elId(o).innerHTML&&(elId(o).innerHTML='')," +"elId(o).innerHTML+=n)," +"n='';" +"!0==(autoscroll_on=elId('autoscroll').checked)&&''!==i&&elId(i).scrollIntoView({behavior:scrolling_type});" +"applyLogFilter();" +"elId('current_loglevel').innerHTML='Logging: '+logLevel[t.Log.SettingsWebLogLevel]+' ('+t.Log.SettingsWebLogLevel+')';" +"clearInterval(s);loopDeLoop(e,0)" +"})}).catch(function(t){" +"elId(o).innerHTML+='
>> '+t.message+' <<
';" +"autoscroll_on=elId('autoscroll').checked;" +"elId(o).scrollTop=elId(o).scrollHeight;" +"e=5000;clearInterval(s);loopDeLoop(e,0)" +"}),l=1},e)}" +"function applyLogFilter(){let e=elId('logfilter').value.split(';').map(e=>e.trim()).filter(Boolean)," +"t=document.querySelectorAll('#copyText_1 > div')," +"o=e.some(e=>!e.startsWith('!'));" +"for(let r of t){r.dataset.originalHtml||(r.dataset.originalHtml=r.innerHTML);" +"let n=r.dataset.originalHtml,i=r.textContent.toLowerCase(),l=[],s=!1,a=!o;" +"for(let d of e){" +"if(d.startsWith('!')){let c=d.slice(1);" +"if(!c.includes('&')||c.startsWith('&')||c.endsWith('&')){" +"if(c&&i.includes(c.toLowerCase())){s=!0;break}}else{" +"let g=c.split('&').map(e=>e.trim()).filter(Boolean),f=-1,u=!0;" +"for(let p of g){let L=i.indexOf(p.toLowerCase(),f+1);if(-1===L){u=!1;break}f=L}if(u){s=!0;break}}continue}" +"if(d.includes('&')&&!d.startsWith('&')&&!d.endsWith('&')){" +"let h=d.split('&').map(e=>e.trim()).filter(Boolean);" +"if(h.length>=2){let m=-1,$=!0;for(let _ of h){let b=i.indexOf(_.toLowerCase(),m+1);if(-1===b){$=!1;break}m=b}if($){l.push(...h),a=!0;break}}continue}" +"if(i.includes(d.toLowerCase())){l.push(d),a=!0;break}}" +"if(s||!a){r.style.display='none';continue}r.style.display='';r.innerHTML=highlightOutsideTags(n,l)}}" +"function highlightOutsideTags(e,t){if(!t.length)return e;" +"let o='',r=!1,n=0;" +"for(;n'===i){r=!1;o+=i;n++;continue}" +"if(!r){let l=!1;for(let s of t){let a=e.slice(n,n+s.length);" +"if(a.toLowerCase()===s.toLowerCase()){o+=`${a}`;n+=s.length;l=!0;break}}if(l)continue}o+=i;n++}return o}" +"const input=document.getElementById('logfilter');" +"input&&(input.placeholder='\"!\"=exclude \";\"=or \"&\"=and (e.g. !act)');" }; #endif // WEBSERVER_INCLUDE_JS diff --git a/static/fetch_and_parse_log.js b/static/fetch_and_parse_log.js index c1a0f7b85..db2bb6577 100644 --- a/static/fetch_and_parse_log.js +++ b/static/fetch_and_parse_log.js @@ -96,7 +96,6 @@ function loopDeLoop(timeForNext, activeRequests) { elId(ct1).innerHTML = ''; } elId(ct1).innerHTML += logEntriesChunk; - applyLogFilter(); } logEntriesChunk = ''; autoscroll_on = elId('autoscroll').checked; @@ -105,6 +104,7 @@ function loopDeLoop(timeForNext, activeRequests) { behavior: scrolling_type }); } + applyLogFilter(); elId('current_loglevel').innerHTML = 'Logging: ' + logLevel[data.Log.SettingsWebLogLevel] + ' (' + data.Log.SettingsWebLogLevel + ')'; clearInterval(i); loopDeLoop(timeForNext, 0); @@ -125,24 +125,128 @@ function loopDeLoop(timeForNext, activeRequests) { } function applyLogFilter() { - const filtername = elId('logfilter').value; + const filterGroups = elId('logfilter').value + .split(';').map(s => s.trim()).filter(Boolean); - // prepare filters: "string1;string2" - const filters = filtername - .split(';') - .map(s => s.trim().toLowerCase()) - .filter(Boolean); + const entries = document.querySelectorAll('#copyText_1 > div'); + const hasPositiveFilter = filterGroups.some(g => !g.startsWith('!')); - const container = document.getElementById('copyText_1'); - const entries = container.querySelectorAll('div'); + for (const entry of entries) { + if (!entry.dataset.originalHtml) entry.dataset.originalHtml = entry.innerHTML; + let html = entry.dataset.originalHtml; - entries.forEach(entry => { - const text = entry.textContent.toLowerCase(); + const textLower = entry.textContent.toLowerCase(); + let matched = []; + let hiddenByExclusion = false; - const matches = - filters.length === 0 || filters.some(f => text.includes(f)); + let isMatch = !hasPositiveFilter; - // show only matching entries - entry.style.display = matches ? '' : 'none'; - }); -} \ No newline at end of file + for (const group of filterGroups) { + + // exclusion rule with ordered AND + if (group.startsWith('!')) { + const g = group.slice(1); + + if (g.includes('&') && !g.startsWith('&') && !g.endsWith('&')) { + const parts = g.split('&').map(s => s.trim()).filter(Boolean); + let lastIndex = -1; + let ok = true; + + for (const p of parts) { + const idx = textLower.indexOf(p.toLowerCase(), lastIndex + 1); + if (idx === -1) { + ok = false; + break; + } + lastIndex = idx; + } + + if (ok) { + hiddenByExclusion = true; + break; + } + } else { + if (g && textLower.includes(g.toLowerCase())) { + hiddenByExclusion = true; + break; + } + } + continue; + } + + // ordered AND: a&b&c + if (group.includes('&') && !group.startsWith('&') && !group.endsWith('&')) { + const parts = group.split('&').map(s => s.trim()).filter(Boolean); + if (parts.length >= 2) { + let lastIndex = -1; + let ok = true; + + for (const p of parts) { + const idx = textLower.indexOf(p.toLowerCase(), lastIndex + 1); + if (idx === -1) { + ok = false; + break; + } + lastIndex = idx; + } + if (ok) { + matched.push(...parts); + isMatch = true; + break; + } + } + continue; + } + // normal include + if (textLower.includes(group.toLowerCase())) { + matched.push(group); + isMatch = true; + break; + } + } + if (hiddenByExclusion || !isMatch) { + entry.style.display = 'none'; + continue; + } + entry.style.display = ''; + entry.innerHTML = highlightOutsideTags(html, matched); + } +} + +function highlightOutsideTags(html, terms) { + if (!terms.length) return html; + let result = ''; + let insideTag = false; + let i = 0; + + while (i < html.length) { + const char = html[i]; + if (char === '<') insideTag = true; + if (char === '>') { + insideTag = false; + result += char; + i++; + continue; + } + + if (!insideTag) { + let matched = false; + for (const term of terms) { + const slice = html.slice(i, i + term.length); + if (slice.toLowerCase() === term.toLowerCase()) { + result += `${slice}`; + i += term.length; + matched = true; + break; + } + } + if (matched) continue; + } + result += char; + i++; + } + return result; +} + +const input = document.getElementById("logfilter"); +input && (input.placeholder = '"!"=exclude ";"=or "&"=and (e.g. !act)'); \ No newline at end of file diff --git a/static/github_clipboard.js b/static/github_clipboard.js index bb2c1a8f8..a8aab1818 100644 --- a/static/github_clipboard.js +++ b/static/github_clipboard.js @@ -11,7 +11,16 @@ function setGithubClipboard() { if (i % 2 == 0) { separatorSymbol += '\n' } - clipboard += test.innerHTML.replace(/<[Bb][Rr]\s*\/?>/gim, '\n') + separatorSymbol; + // collect only visible log entry divs + const visibleLines = Array.from(test.children) + .filter(div => div.offsetParent !== null) // visible only + .map(div => div.innerHTML); + + if (visibleLines.length > 0) { + clipboard += + visibleLines.join('\n').replace(/<[Bb][Rr]\s*\/?>/gim, '\n') + + separatorSymbol; + } } } clipboard = clipboard.replace(/<\/[Dd][Ii][Vv]\s*\/?>/gim, '\n');