2026-04-29 08:16:15 +07:00
<!DOCTYPE html>
< html lang = "en" >
2026-04-29 09:31:32 +07:00
2026-04-29 08:16:15 +07:00
< head >
2026-04-29 09:31:32 +07:00
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Log Viewer< / title >
< link rel = "stylesheet" href = "css/fonts.css" >
< script src = "js/chart.umd.min.js" > < / script >
< script src = "js/chartjs-plugin-zoom.min.js" > < / script >
< script src = "js/hammer.min.js" > < / script >
< style >
* ,
* :: before ,
* :: after {
box-sizing : border-box ;
margin : 0 ;
padding : 0
}
: root {
--bg : #0d0f12 ;
--surface : #131619 ;
--surface2 : #191c21 ;
--surface3 : #1e2128 ;
--border : #ffffff 0 f ;
--border2 : #ffffff 18 ;
--border3 : #ffffff 28 ;
--text : #e8eaf0 ;
--text2 : #8b8f9a ;
--text3 : #555966 ;
--accent : #4f8ef7 ;
--accent-dim : #4f8ef7 20 ;
2026-04-29 09:43:59 +07:00
--trace : #808080 ;
2026-04-29 09:31:32 +07:00
--debug : #555966 ;
--info : #4f8ef7 ;
--warn : #f5a623 ;
--error : #e8504a ;
--fatal : #c72b2b ;
--trace-bg : #555966 10 ;
--debug-bg : #555966 10 ;
--info-bg : #4f8ef7 10 ;
--warn-bg : #f5a623 10 ;
--error-bg : #e8504a 10 ;
--fatal-bg : #c72b2b 18 ;
--radius : 6 px ;
--radius-lg : 10 px ;
--font-mono : 'JetBrains Mono' , monospace ;
--font-sans : 'DM Sans' , sans-serif ;
}
html ,
body {
height : 100 % ;
overflow : hidden
}
body {
background : var ( - - bg ) ;
color : var ( - - text ) ;
font-family : var ( - - font - sans ) ;
font-size : 13 px ;
display : flex ;
flex-direction : column
}
/* Drop overlay */
# drop-overlay {
position : fixed ;
inset : 0 ;
z-index : 9999 ;
background : #0d0f12 f0 ;
backdrop-filter : blur ( 8 px ) ;
display : none ;
align-items : center ;
justify-content : center ;
flex-direction : column ;
gap : 20 px ;
border : 2 px dashed var ( - - accent ) ;
pointer-events : none ;
}
# drop-overlay . active {
display : flex ;
pointer-events : all
}
# drop-overlay . drop-icon {
width : 64 px ;
height : 64 px ;
opacity : .6 ;
animation : pulse 1.4 s ease-in-out infinite
}
@ keyframes pulse {
0 % ,
100 % {
transform : scale ( 1 ) ;
opacity : .6
}
50 % {
transform : scale ( 1.08 ) ;
opacity : 1
}
}
# drop-overlay . drop-label {
font-size : 18 px ;
font-weight : 500 ;
color : var ( - - text ) ;
letter-spacing : .01 em
}
# drop-overlay . drop-sub {
font-size : 13 px ;
color : var ( - - text2 )
}
/* Header */
# header {
display : flex ;
align-items : center ;
gap : 16 px ;
padding : 10 px 20 px ;
border-bottom : 1 px solid var ( - - border ) ;
background : var ( - - surface ) ;
flex-shrink : 0 ;
}
# logo {
font-family : var ( - - font - mono ) ;
font-size : 15 px ;
font-weight : 500 ;
color : var ( - - text ) ;
letter-spacing : -.02 em
}
# logo span {
color : var ( - - accent )
}
# file-info {
font-family : var ( - - font - mono ) ;
font-size : 11 px ;
color : var ( - - text3 ) ;
flex : 1 ;
white-space : nowrap ;
overflow : hidden ;
text-overflow : ellipsis
}
# file-btn {
display : flex ;
align-items : center ;
gap : 7 px ;
background : var ( - - surface3 ) ;
border : 1 px solid var ( - - border2 ) ;
border-radius : var ( - - radius ) ;
color : var ( - - text2 ) ;
font-family : var ( - - font - sans ) ;
font-size : 12 px ;
font-weight : 500 ;
padding : 6 px 12 px ;
cursor : pointer ;
transition : all .15 s ;
white-space : nowrap ;
}
# file-btn : hover {
border-color : var ( - - border3 ) ;
color : var ( - - text ) ;
background : var ( - - surface2 )
}
# file-input {
display : none
}
. hdr-sep {
width : 1 px ;
height : 20 px ;
background : var ( - - border )
}
/* Filter bar */
# filter-bar {
display : flex ;
align-items : center ;
gap : 10 px ;
padding : 8 px 20 px ;
border-bottom : 1 px solid var ( - - border ) ;
background : var ( - - surface ) ;
flex-shrink : 0 ;
}
# search-wrap {
position : relative ;
flex : 1 ;
max-width : 420 px
}
# search-wrap svg {
position : absolute ;
left : 10 px ;
top : 50 % ;
transform : translateY ( -50 % ) ;
opacity : .4 ;
pointer-events : none
}
# search {
width : 100 % ;
background : var ( - - surface3 ) ;
border : 1 px solid var ( - - border2 ) ;
border-radius : var ( - - radius ) ;
color : var ( - - text ) ;
font-family : var ( - - font - mono ) ;
font-size : 12 px ;
padding : 6 px 10 px 6 px 32 px ;
outline : none ;
transition : border-color .15 s ;
}
# search : focus {
border-color : var ( - - accent ) ;
box-shadow : 0 0 0 3 px var ( - - accent - dim )
}
# search . invalid {
border-color : var ( - - error ) ;
box-shadow : 0 0 0 3 px #e8504a 18
}
# search :: placeholder {
color : var ( - - text3 )
}
# regex-badge {
position : absolute ;
right : 8 px ;
top : 50 % ;
transform : translateY ( -50 % ) ;
font-family : var ( - - font - mono ) ;
font-size : 10 px ;
padding : 2 px 5 px ;
border-radius : 3 px ;
background : var ( - - accent - dim ) ;
color : var ( - - accent ) ;
opacity : .7 ;
}
/* Level pills */
. level-pills {
display : flex ;
gap : 6 px ;
flex-wrap : wrap
}
. level-pill {
display : flex ;
align-items : center ;
gap : 6 px ;
padding : 5 px 10 px ;
border-radius : 20 px ;
border : 1 px solid var ( - - border2 ) ;
background : transparent ;
cursor : pointer ;
font-family : var ( - - font - sans ) ;
font-size : 11 px ;
font-weight : 500 ;
color : var ( - - text2 ) ;
transition : all .15 s ;
white-space : nowrap ;
}
. level-pill : hover {
border-color : var ( - - border3 ) ;
color : var ( - - text )
}
. level-pill . active {
border-color : currentColor
}
. level-pill [ data-level = "TRACE" ] {
--lc : var ( - - trace )
}
. level-pill [ data-level = "DEBUG" ] {
--lc : var ( - - debug )
}
. level-pill [ data-level = "INFO" ] {
--lc : var ( - - info )
}
. level-pill [ data-level = "WARN" ] {
--lc : var ( - - warn )
}
. level-pill [ data-level = "ERROR" ] {
--lc : var ( - - error )
}
. level-pill [ data-level = "FATAL" ] {
--lc : var ( - - fatal )
}
. level-pill . active {
color : var ( - - lc ) ;
border-color : var ( - - lc ) ;
background : color-mix ( in srgb , var ( - - lc ) 10 % , transparent )
}
. pill-dot {
width : 6 px ;
height : 6 px ;
border-radius : 50 % ;
background : var ( - - lc , var ( - - text3 ) ) ;
flex-shrink : 0
}
. pill-count {
font-family : var ( - - font - mono ) ;
font-size : 10 px ;
opacity : .7
}
. filter-actions {
display : flex ;
gap : 6 px ;
margin-left : auto
}
# clear-filters {
padding : 5 px 10 px ;
border-radius : var ( - - radius ) ;
border : 1 px solid var ( - - border2 ) ;
background : transparent ;
color : var ( - - text3 ) ;
font-size : 11 px ;
font-family : var ( - - font - sans ) ;
cursor : pointer ;
transition : all .15 s ;
}
# clear-filters : hover {
color : var ( - - text ) ;
border-color : var ( - - border3 )
}
# time-badge {
display : none ;
align-items : center ;
gap : 5 px ;
padding : 5 px 10 px ;
border-radius : var ( - - radius ) ;
background : var ( - - accent - dim ) ;
border : 1 px solid var ( - - accent ) ;
color : var ( - - accent ) ;
font-size : 11 px ;
font-family : var ( - - font - mono ) ;
cursor : pointer ;
}
# time-badge . visible {
display : flex
}
# time-badge : hover {
background : #4f8ef7 30
}
# time-badge . tx-close {
opacity : .6 ;
margin-left : 2 px
}
/* Main layout */
# main {
display : flex ;
flex-direction : column ;
flex : 1 ;
overflow : hidden
}
/* Chart panel */
# chart-panel {
flex-shrink : 0 ;
padding : 16 px 20 px 12 px ;
border-bottom : 1 px solid var ( - - border ) ;
}
# chart-wrap {
position : relative ;
height : 100 px
}
# chart-hint {
position : absolute ;
top : 4 px ;
right : 0 ;
font-size : 10 px ;
color : var ( - - text3 ) ;
font-family : var ( - - font - mono )
}
/* Table area */
# table-area {
flex : 1 ;
overflow : hidden ;
display : flex ;
flex-direction : column
}
/* DataTables custom styles */
# dt-wrap {
flex : 1 ;
overflow : auto ;
padding : 0 20 px 20 px
}
table # log-table {
width : 100 % ;
border-collapse : collapse ;
font-family : var ( - - font - mono ) ;
font-size : 11.5 px
}
table # log-table thead th {
position : sticky ;
top : 0 ;
z-index : 10 ;
background : var ( - - surface ) ;
border-bottom : 1 px solid var ( - - border2 ) ;
padding : 10 px 12 px ;
text-align : left ;
font-weight : 500 ;
color : var ( - - text2 ) ;
font-size : 11 px ;
letter-spacing : .05 em ;
text-transform : uppercase ;
white-space : nowrap ;
cursor : pointer ;
user-select : none ;
}
table # log-table thead th : hover {
color : var ( - - text )
}
table # log-table thead th . sort-asc :: after {
content : ' ↑'
}
table # log-table thead th . sort-desc :: after {
content : ' ↓'
}
table # log-table tbody tr {
border-bottom : 1 px solid var ( - - border ) ;
cursor : pointer ;
transition : background .1 s
}
table # log-table tbody tr : hover > td {
background : var ( - - surface2 )
}
table # log-table tbody td {
padding : 7 px 12 px ;
vertical-align : top
}
. col-time {
width : 180 px ;
color : var ( - - text3 ) ;
white-space : nowrap
}
. col-level {
width : 72 px
}
. col-msg {
word-break : break-word
}
/* Level badges */
. lv {
display : inline-block ;
padding : 2 px 7 px ;
border-radius : 3 px ;
font-size : 10 px ;
font-weight : 500 ;
letter-spacing : .04 em
}
. lv-TRACE {
background : var ( - - trace - bg ) ;
color : var ( - - trace )
}
. lv-DEBUG {
background : var ( - - debug - bg ) ;
color : var ( - - debug )
}
. lv-INFO {
background : var ( - - info - bg ) ;
color : var ( - - info )
}
. lv-WARN {
background : var ( - - warn - bg ) ;
color : var ( - - warn )
}
. lv-ERROR {
background : var ( - - error - bg ) ;
color : var ( - - error )
}
. lv-FATAL {
background : var ( - - fatal - bg ) ;
color : var ( - - fatal )
}
/* Row details (expanded) */
. row-detail {
background : var ( - - surface2 ) !important ;
border-left : 2 px solid var ( - - accent ) !important
}
. row-detail td {
padding : 0 !important
}
. detail-inner {
padding : 12 px 16 px 16 px ;
display : flex ;
flex-direction : column ;
gap : 12 px
}
. detail-section {
display : flex ;
flex-direction : column ;
gap : 6 px
}
. detail-label {
font-size : 10 px ;
font-weight : 500 ;
letter-spacing : .08 em ;
text-transform : uppercase ;
color : var ( - - text3 )
}
. detail-val {
background : var ( - - surface3 ) ;
border : 1 px solid var ( - - border ) ;
border-radius : var ( - - radius ) ;
padding : 10 px 12 px ;
font-family : var ( - - font - mono ) ;
font-size : 11 px ;
color : var ( - - text2 ) ;
white-space : pre-wrap ;
word-break : break-all ;
max-height : 200 px ;
overflow-y : auto ;
line-height : 1.6 ;
}
/* Empty state */
# empty-state {
flex : 1 ;
display : flex ;
flex-direction : column ;
align-items : center ;
justify-content : center ;
gap : 16 px ;
color : var ( - - text3 ) ;
}
# empty-state svg {
opacity : .3
}
# empty-state . es-title {
font-size : 15 px ;
font-weight : 500 ;
color : var ( - - text2 )
}
# empty-state . es-sub {
font-size : 12 px ;
text-align : center ;
max-width : 280 px ;
line-height : 1.6
}
# empty-state . es-btn {
margin-top : 8 px ;
padding : 8 px 20 px ;
border-radius : var ( - - radius ) ;
background : var ( - - accent - dim ) ;
border : 1 px solid var ( - - accent ) ;
color : var ( - - accent ) ;
font-size : 13 px ;
font-family : var ( - - font - sans ) ;
cursor : pointer ;
transition : all .15 s ;
}
# empty-state . es-btn : hover {
background : #4f8ef7 30
}
/* Table status bar */
# status-bar {
display : flex ;
align-items : center ;
gap : 12 px ;
padding : 7 px 20 px ;
border-top : 1 px solid var ( - - border ) ;
background : var ( - - surface ) ;
flex-shrink : 0 ;
font-size : 11 px ;
color : var ( - - text3 ) ;
font-family : var ( - - font - mono ) ;
}
# status-bar . status-count {
color : var ( - - text2 )
}
. sb-sep {
color : var ( - - border3 )
}
/* Scrollbar */
:: -webkit-scrollbar {
width : 6 px ;
height : 6 px
}
:: -webkit-scrollbar-track {
background : transparent
}
:: -webkit-scrollbar-thumb {
background : var ( - - border2 ) ;
border-radius : 3 px
}
:: -webkit-scrollbar-thumb : hover {
background : var ( - - border3 )
}
/* Animations */
@ keyframes fadeIn {
from {
opacity : 0 ;
transform : translateY ( 4 px )
}
to {
opacity : 1 ;
transform : translateY ( 0 )
}
}
. fade-in {
animation : fadeIn .2 s ease forwards
}
2026-04-29 10:30:49 +07:00
. log-container {
position : relative ;
/* Crucial: pins the loader to this div */
width : 100 % ;
height : 400 px ;
border : 1 px solid #333 ;
}
/* The Overlay */
. loading-overlay {
position : absolute ;
inset : 0 ;
/* Modern shorthand for top, left, bottom, right: 0 */
background : rgba ( 0 , 0 , 0 , 0.7 ) ;
/* Dim the background */
display : flex ;
flex-direction : column ;
align-items : center ;
justify-content : center ;
z-index : 10 ;
/* Ensures it stays on top of the chart */
color : white ;
transition : opacity 0.3 s ease ;
visibility : hidden ;
/* Hidden by default */
opacity : 0 ;
}
/* Show state */
. loading-overlay . is-active {
visibility : visible ;
opacity : 1 ;
}
/* Simple Spinner Animation */
. spinner {
width : 40 px ;
height : 40 px ;
border : 4 px solid rgba ( 255 , 255 , 255 , 0.3 ) ;
border-top-color : #3498db ;
border-radius : 50 % ;
animation : spin 1 s linear infinite ;
margin-bottom : 10 px ;
}
@ keyframes spin {
to {
transform : rotate ( 360 deg ) ;
}
}
2026-04-29 09:31:32 +07:00
< / style >
2026-04-29 08:16:15 +07:00
< / head >
2026-04-29 09:31:32 +07:00
2026-04-29 08:16:15 +07:00
< body >
2026-04-29 09:31:32 +07:00
<!-- Drop Overlay -->
< div id = "drop-overlay" >
< svg class = "drop-icon" width = "64" height = "64" viewBox = "0 0 64 64" fill = "none" >
< rect x = "8" y = "8" width = "48" height = "48" rx = "8" stroke = "#4f8ef7" stroke-width = "2" stroke-dasharray = "6 4" / >
< path d = "M32 20v24M22 34l10 10 10-10" stroke = "#4f8ef7" stroke-width = "2" stroke-linecap = "round"
stroke-linejoin = "round" / >
< / svg >
< div class = "drop-label" > Drop log file here< / div >
< div class = "drop-sub" > Supports .log, .txt, .json and plain text< / div >
2026-04-29 08:16:15 +07:00
< / div >
2026-04-29 09:31:32 +07:00
<!-- Header -->
< div id = "header" >
2026-04-29 10:30:49 +07:00
< div id = "logo" > log< span > viewer< / span > < / div >
2026-04-29 09:31:32 +07:00
< div class = "hdr-sep" > < / div >
< div id = "file-info" > No file loaded — drag & drop or select a file< / div >
< button id = "file-btn" onclick = "document.getElementById('file-input').click()" >
< svg width = "13" height = "13" viewBox = "0 0 16 16" fill = "none" >
< path d = "M2 12V4a1 1 0 011-1h5l2 2h3a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1z" stroke = "currentColor"
stroke-width = "1.4" / >
< / svg >
Open File
< / button >
< input type = "file" id = "file-input" accept = ".log,.txt,.json,text/plain" >
2026-04-29 08:16:15 +07:00
< / div >
2026-04-29 09:31:32 +07:00
2026-04-29 10:30:49 +07:00
< div id = "log-container" >
<!-- Filter bar -->
< div id = "filter-bar" >
< div id = "search-wrap" >
< svg width = "13" height = "13" viewBox = "0 0 16 16" fill = "none" >
< circle cx = "6.5" cy = "6.5" r = "4.5" stroke = "currentColor" stroke-width = "1.4" / >
< path d = "M10.5 10.5L14 14" stroke = "currentColor" stroke-width = "1.4" stroke-linecap = "round" / >
2026-04-29 09:31:32 +07:00
< / svg >
2026-04-29 10:30:49 +07:00
< input type = "text" id = "search" placeholder = "Filter messages… (regex supported)" autocomplete = "off" >
< span id = "regex-badge" > .*< / span >
< / div >
< div class = "hdr-sep" > < / div >
< div class = "level-pills" id = "level-pills" >
< button class = "level-pill" data-level = "TRACE" > < span class = "pill-dot" > < / span > Trace< span class = "pill-count"
id = "cnt-TRACE" > 0< / span > < / button >
< button class = "level-pill" data-level = "DEBUG" > < span class = "pill-dot" > < / span > Debug< span class = "pill-count"
id = "cnt-DEBUG" > 0< / span > < / button >
< button class = "level-pill" data-level = "INFO" > < span class = "pill-dot" > < / span > Info< span class = "pill-count"
id = "cnt-INFO" > 0< / span > < / button >
< button class = "level-pill" data-level = "WARN" > < span class = "pill-dot" > < / span > Warn< span class = "pill-count"
id = "cnt-WARN" > 0< / span > < / button >
< button class = "level-pill" data-level = "ERROR" > < span class = "pill-dot" > < / span > Error< span class = "pill-count"
id = "cnt-ERROR" > 0< / span > < / button >
< button class = "level-pill" data-level = "FATAL" > < span class = "pill-dot" > < / span > Fatal< span class = "pill-count"
id = "cnt-FATAL" > 0< / span > < / button >
< / div >
< div class = "filter-actions" >
< div id = "time-badge" >
< svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" >
< circle cx = "8" cy = "8" r = "6" stroke = "currentColor" stroke-width = "1.4" / >
< path d = "M8 5v3l2 2" stroke = "currentColor" stroke-width = "1.4" stroke-linecap = "round" / >
< / svg >
< span id = "time-badge-label" > time range< / span >
< span class = "tx-close" > ✕< / span >
< / div >
< button id = "clear-filters" > Clear filters< / button >
2026-04-29 09:31:32 +07:00
< / div >
2026-04-29 08:16:15 +07:00
< / div >
2026-04-29 10:30:49 +07:00
<!-- Main -->
< div id = "main" >
<!-- Chart -->
< div id = "chart-panel" >
< div id = "chart-wrap" >
< canvas id = "intensity-chart" > < / canvas >
< span id = "chart-hint" > drag to zoom · right-click to reset< / span >
< / div >
2026-04-29 09:31:32 +07:00
< / div >
2026-04-29 08:16:15 +07:00
2026-04-29 10:30:49 +07:00
<!-- Empty state / Table area -->
< div id = "empty-state" >
< svg width = "48" height = "48" viewBox = "0 0 48 48" fill = "none" >
< rect x = "6" y = "6" width = "36" height = "36" rx = "6" stroke = "currentColor" stroke-width = "1.5"
stroke-dasharray = "5 3" / >
< path d = "M16 20h16M16 24h10M16 28h12" stroke = "currentColor" stroke-width = "1.5" stroke-linecap = "round" / >
< / svg >
< div class = "es-title" > No log file loaded< / div >
< div class = "es-sub" > Drag & drop a log file anywhere on the page or click below to browse< / div >
< button class = "es-btn" onclick = "document.getElementById('file-input').click()" > Select log file< / button >
< / div >
< div id = "table-area" style = "display:none" >
< div id = "dt-wrap" >
< table id = "log-table" >
< thead >
< tr >
< th class = "col-time" data-col = "time" > Time< / th >
< th class = "col-level" data-col = "level" > Level< / th >
< th class = "col-msg" data-col = "message" > Message< / th >
< / tr >
< / thead >
< tbody id = "log-tbody" > < / tbody >
< / table >
< / div >
< / div >
2026-04-29 09:31:32 +07:00
2026-04-29 10:30:49 +07:00
<!-- Status bar -->
< div id = "status-bar" style = "display:none" >
< span class = "status-count" id = "status-showing" > 0 entries< / span >
< span class = "sb-sep" > ·< / span >
< span id = "status-total" > 0 total< / span >
< span class = "sb-sep" > ·< / span >
< span id = "status-file" > no file< / span >
2026-04-29 09:31:32 +07:00
< / div >
2026-04-29 08:16:15 +07:00
< / div >
2026-04-29 10:30:49 +07:00
< div class = "loading-overlay" id = "loader" >
< div class = "spinner" > < / div >
< p > Loading Data...< / p >
2026-04-29 09:31:32 +07:00
< / div >
2026-04-29 08:16:15 +07:00
< / div >
2026-04-29 09:31:32 +07:00
< script >
// ---- State ----
let allLogs = [ ] ;
let filteredLogs = [ ] ;
let levelFilter = null ;
let textFilter = '' ;
let timeFilter = null ; // {start, end} timestamps
let sortCol = 'time' ;
let sortDir = 'asc' ;
let expandedRows = new Set ( ) ;
let chart = null ;
let chartBuckets = [ ] ;
2026-04-29 10:30:49 +07:00
const msg _len _limit = 100 ;
2026-04-29 09:31:32 +07:00
// ---- Demo data generator ----
function generateDemoLogs ( ) {
const levels = [ 'DEBUG' , 'DEBUG' , 'DEBUG' , 'INFO' , 'INFO' , 'INFO' , 'INFO' , 'WARN' , 'WARN' , 'ERROR' , 'ERROR' , 'FATAL' ] ;
const msgs = [
{ level : 'INFO' , msg : 'Server started on port 3000' , props : { pid : 1234 , env : 'production' } } ,
{ level : 'DEBUG' , msg : 'Database connection pool initialized' , props : { pool _size : 10 , timeout : 5000 } } ,
{ level : 'INFO' , msg : 'Request received GET /api/users' , props : { method : 'GET' , path : '/api/users' , ip : '10.0.0.5' } } ,
{ level : 'DEBUG' , msg : 'Cache hit for key user:session:8f2a' , props : { ttl : 3540 } } ,
{ level : 'INFO' , msg : 'User authentication successful' , props : { user _id : 'u_9f3a' , roles : [ 'admin' , 'viewer' ] } } ,
{ level : 'WARN' , msg : 'Response time exceeded threshold 500ms' , props : { actual _ms : 621 , endpoint : '/api/reports' } } ,
{ level : 'ERROR' , msg : 'Failed to connect to Redis' , props : { host : 'redis:6379' , attempt : 3 } , exception : 'RedisConnectionError: Connection refused\n at connect (redis.js:42)\n at retry (pool.js:87)' } ,
{ level : 'DEBUG' , msg : 'Worker thread spawned' , props : { worker _id : 'w_003' , queue : 'email' } } ,
{ level : 'WARN' , msg : 'Memory usage above 80%' , props : { used _mb : 812 , total _mb : 1024 , percent : 79.3 } } ,
{ level : 'INFO' , msg : 'Scheduled job completed: nightly-backup' , props : { duration _ms : 12430 , records : 8912 } } ,
{ level : 'ERROR' , msg : 'Unhandled exception in payment processor' , props : { order _id : 'ord_7f8a' , amount : 149.99 } , exception : 'TypeError: Cannot read property \'charge\' of undefined\n at PaymentService.process (payment.js:133)\n at OrderController.checkout (orders.js:78)' } ,
{ level : 'FATAL' , msg : 'Out of memory — process terminating' , props : { heap _used _mb : 2048 , heap _limit _mb : 2048 } , exception : 'FatalError: ENOMEM\n at allocate (heap.js:21)' } ,
{ level : 'INFO' , msg : 'Health check passed' , props : { db : 'ok' , cache : 'ok' , queue : 'ok' } } ,
{ level : 'DEBUG' , msg : 'Config reloaded from environment' , props : { changed _keys : [ 'LOG_LEVEL' , 'DB_POOL' ] } } ,
{ level : 'WARN' , msg : 'Deprecated API endpoint /v1/search called' , props : { new _endpoint : '/v2/search' , caller _ip : '192.168.1.55' } } ,
] ;
const now = Date . now ( ) ;
const logs = [ ] ;
for ( let i = 0 ; i < 200 ; i ++ ) {
const template = msgs [ Math . floor ( Math . random ( ) * msgs . length ) ] ;
logs . push ( {
time : new Date ( now - ( 200 - i ) * 18000 + Math . random ( ) * 15000 ) ,
level : template . level ,
message : template . msg ,
props : template . props || null ,
exception : template . exception || null ,
} ) ;
}
return logs . sort ( ( a , b ) => a . time - b . time ) ;
}
// ---- Log parsing ----
2026-04-29 09:43:59 +07:00
function parseLogs ( text , filedate = null ) {
const lines = text . split ( /\r?\n/ ) . filter ( l => l . trim ( ) ) ;
2026-04-29 09:31:32 +07:00
const logs = [ ] ;
// Try JSON Lines
const jsonLogs = tryParseJSONLines ( lines ) ;
if ( jsonLogs . length > lines . length * 0.5 ) return jsonLogs ;
// Try common log formats
const patterns = [
// 2024-01-15T10:23:45.123Z INFO message
/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|TRACE)\s+(.+)$/i ,
// [2024-01-15 10:23:45] [INFO] message
/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]\s*\[(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|TRACE)\]\s*(.+)$/i ,
// Jan 15 10:23:45 INFO message
/^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL)\s+(.+)$/i ,
// INFO 2024-01-15 10:23:45 message
/^(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL)\s+(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.+)$/i ,
2026-04-29 09:43:59 +07:00
// 10:23:43.333 [INF] message
/^(\d{2}:\d{2}:\d{2}\.\d{3})\s\[(INF|WRN|ERR|CRT|DBG|TRC)\]\s(.*)$/
2026-04-29 09:31:32 +07:00
] ;
let i = 0 ;
2026-04-29 10:30:49 +07:00
//debugger
2026-04-29 09:31:32 +07:00
while ( i < lines . length ) {
const line = lines [ i ] . trim ( ) ;
let matched = false ;
for ( const pat of patterns ) {
const m = line . match ( pat ) ;
if ( m ) {
const [ , g1 , g2 , g3 ] = m ;
let timeStr , levelStr , msg ;
if ( /^(DEBUG|INFO|WARN|ERROR|FATAL)/i . test ( g1 ) ) {
timeStr = g2 ; levelStr = g1 . toUpperCase ( ) . replace ( 'WARNING' , 'WARN' ) ; msg = g3 ;
} else {
timeStr = g1 ; levelStr = g2 . toUpperCase ( ) . replace ( 'WARNING' , 'WARN' ) ; msg = g3 ;
}
2026-04-29 09:43:59 +07:00
levelStr = translateLogLevel ( levelStr )
let time
if ( filedate === null ) {
time = new Date ( timeStr )
}
else {
const [ hours , mins , secs , ms ] = timeStr . split ( /[:.]/ ) . map ( Number ) ;
let temp _date = filedate . setHours ( hours , mins , secs , ms )
time = new Date ( filedate )
}
2026-04-29 09:31:32 +07:00
if ( ! isNaN ( time ) ) {
// Collect continuation lines (stack traces etc.)
let exception = '' ;
let j = i + 1 ;
while ( j < lines . length && ! /\d{4}-\d{2}-\d{2}/ . test ( lines [ j ] ) && ! patterns . some ( p => p . test ( lines [ j ] ) ) ) {
const cl = lines [ j ] . trim ( ) ;
if ( cl ) exception += ( exception ? '\n' : '' ) + cl ;
j ++ ;
}
i = j - 1 ;
2026-04-29 10:30:49 +07:00
logs . push ( {
time ,
level : levelStr ,
message : msg . length > msg _len _limit ? msg . slice ( 0 , msg _len _limit ) + "..." : msg . trim ( ) ,
fullmessage : msg . length > msg _len _limit ? msg : null ,
props : null ,
exception : exception || null
} ) ;
2026-04-29 09:31:32 +07:00
matched = true ;
break ;
}
}
}
if ( ! matched ) {
// Plain line, treat as INFO
if ( line . length > 0 ) {
logs . push ( { time : new Date ( ) , level : 'INFO' , message : line , props : null , exception : null } ) ;
2026-04-29 08:16:15 +07:00
}
}
2026-04-29 09:31:32 +07:00
i ++ ;
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
return logs . sort ( ( a , b ) => a . time - b . time ) ;
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
function tryParseJSONLines ( lines ) {
const logs = [ ] ;
for ( const line of lines ) {
try {
const obj = JSON . parse ( line . trim ( ) ) ;
const timeKeys = [ 'timestamp' , 'time' , 'ts' , 'date' , '@timestamp' ] ;
const levelKeys = [ 'level' , 'severity' , 'lvl' , 'log_level' ] ;
const msgKeys = [ 'message' , 'msg' , 'text' , 'body' , 'log' ] ;
const timeVal = timeKeys . map ( k => obj [ k ] ) . find ( v => v ) ;
const levelRaw = levelKeys . map ( k => obj [ k ] ) . find ( v => v ) || 'INFO' ;
const msgVal = msgKeys . map ( k => obj [ k ] ) . find ( v => v ) || JSON . stringify ( obj ) ;
const level = String ( levelRaw ) . toUpperCase ( ) . replace ( 'WARNING' , 'WARN' ) ;
const time = timeVal ? new Date ( timeVal ) : new Date ( ) ;
const usedKeys = new Set ( [ ... timeKeys , ... levelKeys , ... msgKeys ] . filter ( k => obj [ k ] !== undefined ) ) ;
const props = Object . fromEntries ( Object . entries ( obj ) . filter ( ( [ k ] ) => ! usedKeys . has ( k ) ) ) ;
const exception = obj . exception || obj . error || obj . stack || null ;
2026-04-29 10:30:49 +07:00
logs . push ( {
time ,
level : [ 'DEBUG' , 'INFO' , 'WARN' , 'ERROR' , 'FATAL' ] . includes ( level ) ? level : 'INFO' ,
message : String ( msgVal ) , props : Object . keys ( props ) . length ? props : null ,
exception : exception ? String ( exception ) : null
} ) ;
2026-04-29 09:31:32 +07:00
} catch { }
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
return logs ;
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
// ---- Chart ----
function buildChart ( logs ) {
if ( ! logs . length ) return ;
const canvas = document . getElementById ( 'intensity-chart' ) ;
const BUCKETS = 60 ;
const times = logs . map ( l => l . time . getTime ( ) ) ;
const minT = Math . min ( ... times ) , maxT = Math . max ( ... times ) ;
const span = maxT - minT || 1 ;
const bucketSize = span / BUCKETS ;
const buckets = Array . from ( { length : BUCKETS } , ( _ , i ) => ( {
t : new Date ( minT + i * bucketSize ) ,
2026-04-29 09:43:59 +07:00
TRACE : 0 , DEBUG : 0 , INFO : 0 , WARN : 0 , ERROR : 0 , FATAL : 0 , total : 0
2026-04-29 09:31:32 +07:00
} ) ) ;
for ( const log of logs ) {
const idx = Math . min ( Math . floor ( ( log . time . getTime ( ) - minT ) / bucketSize ) , BUCKETS - 1 ) ;
buckets [ idx ] [ log . level ] = ( buckets [ idx ] [ log . level ] || 0 ) + 1 ;
buckets [ idx ] . total ++ ;
}
chartBuckets = buckets ;
if ( chart ) chart . destroy ( ) ;
const colorMap = {
2026-04-29 09:43:59 +07:00
TRACE : '#80808080' , DEBUG : '#55596640' , INFO : '#4f8ef760' , WARN : '#f5a62370' , ERROR : '#e8504a80' , FATAL : '#c72b2b90'
2026-04-29 09:31:32 +07:00
} ;
chart = new Chart ( canvas , {
type : 'bar' ,
data : {
labels : buckets . map ( b => b . t ) ,
2026-04-29 09:43:59 +07:00
datasets : [ 'FATAL' , 'ERROR' , 'WARN' , 'INFO' , 'DEBUG' , 'TRACE' ] . map ( level => ( {
2026-04-29 09:31:32 +07:00
label : level ,
data : buckets . map ( b => b [ level ] ) ,
backgroundColor : colorMap [ level ] ,
borderColor : colorMap [ level ] . replace ( /[0-9a-f]{2}$/ , 'ff' ) ,
borderWidth : 0 ,
borderRadius : 1 ,
} ) )
2026-04-29 08:16:15 +07:00
} ,
2026-04-29 09:31:32 +07:00
options : {
responsive : true , maintainAspectRatio : false ,
animation : false ,
plugins : {
legend : { display : false } ,
tooltip : {
backgroundColor : '#131619' ,
borderColor : '#ffffff18' ,
borderWidth : 1 ,
titleColor : '#8b8f9a' ,
bodyColor : '#e8eaf0' ,
titleFont : { family : 'JetBrains Mono' , size : 10 } ,
bodyFont : { family : 'JetBrains Mono' , size : 11 } ,
callbacks : {
title : items => {
const d = new Date ( items [ 0 ] . label ) ;
return d . toLocaleString ( ) ;
}
}
} ,
zoom : {
zoom : {
drag : { enabled : true , backgroundColor : '#4f8ef718' , borderColor : '#4f8ef760' , borderWidth : 1 } ,
mode : 'x' ,
onZoomComplete ( { chart } ) {
const { min , max } = chart . scales . x ;
const minT = chart . data . labels [ Math . round ( min ) ] ? . getTime ( ) ;
const maxT = chart . data . labels [ Math . min ( Math . round ( max ) , chart . data . labels . length - 1 ) ] ? . getTime ( ) ;
if ( minT && maxT ) applyTimeFilter ( minT , maxT ) ;
}
}
}
} ,
scales : {
x : {
type : 'category' ,
stacked : true ,
2026-04-29 10:30:49 +07:00
ticks : {
// Grouping: Limit the number of labels shown to prevent overlapping
maxTicksLimit : 15 ,
// Formatting
// callback: function (val, index) {
// // Assuming your data labels are Date objects or Date strings
// const date = new Date(this.getLabelForValue(val));
// // Returns "HH:mm:ss" in local time
// return date.toLocaleTimeString('sv-SE');
// }
callback : function ( val , index ) {
const labels = this . chart . data . labels ;
const date = new Date ( labels [ index ] ) ;
// Check if the first and last labels are on the same day
const firstDate = new Date ( labels [ 0 ] ) . toDateString ( ) ;
const lastDate = new Date ( labels [ labels . length - 1 ] ) . toDateString ( ) ;
const isSameDay = firstDate === lastDate ;
// Format based on the result
if ( isSameDay ) {
// Returns: HH:mm:ss
return date . toLocaleTimeString ( 'sv-SE' ) ;
} else {
// Returns: DD/MM HH:mm:ss
const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ;
const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const time = date . toLocaleTimeString ( 'sv-SE' ) ;
return ` ${ day } / ${ month } ${ time } ` ;
}
}
} ,
2026-04-29 09:31:32 +07:00
grid : { color : '#ffffff08' , drawBorder : false } ,
border : { color : '#ffffff10' }
} ,
y : {
stacked : true ,
ticks : { color : '#555966' , font : { family : 'JetBrains Mono' , size : 10 } , maxTicksLimit : 4 } ,
grid : { color : '#ffffff08' , drawBorder : false } ,
border : { display : false }
2026-04-29 08:16:15 +07:00
}
}
}
2026-04-29 09:31:32 +07:00
} ) ;
canvas . addEventListener ( 'contextmenu' , e => {
e . preventDefault ( ) ;
2026-04-29 10:30:49 +07:00
showLoading ( )
2026-04-29 09:31:32 +07:00
clearTimeFilter ( ) ;
} ) ;
}
function updateChartHighlight ( ) {
if ( ! chart || ! timeFilter ) return ;
// highlight handled by zoom plugin selection
}
// ---- Filtering ----
function applyTimeFilter ( start , end ) {
2026-04-29 10:30:49 +07:00
showLoading ( )
2026-04-29 09:31:32 +07:00
timeFilter = { start , end } ;
const startD = new Date ( start ) , endD = new Date ( end ) ;
document . getElementById ( 'time-badge-label' ) . textContent =
` ${ fmtShort ( startD ) } – ${ fmtShort ( endD ) } ` ;
document . getElementById ( 'time-badge' ) . classList . add ( 'visible' ) ;
applyFilters ( ) ;
2026-04-29 10:30:49 +07:00
updateFilteredLevelCounts ( ) ;
hideLoading ( )
2026-04-29 09:31:32 +07:00
}
function clearTimeFilter ( ) {
2026-04-29 10:30:49 +07:00
showLoading ( )
2026-04-29 09:31:32 +07:00
timeFilter = null ;
document . getElementById ( 'time-badge' ) . classList . remove ( 'visible' ) ;
if ( chart ) chart . resetZoom ( ) ;
applyFilters ( ) ;
2026-04-29 10:30:49 +07:00
updateLevelCounts ( ) ;
hideLoading ( )
2026-04-29 09:31:32 +07:00
}
function applyFilters ( ) {
let regex = null ;
const searchVal = document . getElementById ( 'search' ) . value ;
const searchEl = document . getElementById ( 'search' ) ;
if ( searchVal ) {
try {
regex = new RegExp ( searchVal , 'i' ) ;
searchEl . classList . remove ( 'invalid' ) ;
} catch {
searchEl . classList . add ( 'invalid' ) ;
regex = null ;
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
} else {
searchEl . classList . remove ( 'invalid' ) ;
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
filteredLogs = allLogs . filter ( log => {
if ( levelFilter && log . level !== levelFilter ) return false ;
if ( timeFilter && ( log . time . getTime ( ) < timeFilter . start || log . time . getTime ( ) > timeFilter . end ) ) return false ;
if ( regex && ! regex . test ( log . message ) ) return false ;
return true ;
} ) ;
renderTable ( ) ;
updateStatus ( ) ;
2026-04-29 08:16:15 +07:00
}
2026-04-29 09:31:32 +07:00
// ---- Table rendering ----
function renderTable ( ) {
const sorted = [ ... filteredLogs ] . sort ( ( a , b ) => {
let av = a [ sortCol ] , bv = b [ sortCol ] ;
if ( sortCol === 'time' ) { av = av . getTime ( ) ; bv = bv . getTime ( ) ; }
else { av = String ( av ) . toLowerCase ( ) ; bv = String ( bv ) . toLowerCase ( ) ; }
return sortDir === 'asc' ? ( av > bv ? 1 : - 1 ) : ( av < bv ? 1 : - 1 ) ;
} ) ;
expandedRows . clear ( ) ;
const tbody = document . getElementById ( 'log-tbody' ) ;
tbody . innerHTML = '' ;
sorted . forEach ( ( log , idx ) => {
const tr = document . createElement ( 'tr' ) ;
tr . className = 'fade-in' ;
tr . dataset . idx = idx ;
tr . innerHTML = `
2026-04-29 10:30:49 +07:00
<td class="col-time"> ${ fmtTimeLocal ( log . time ) } </td>
2026-04-29 08:16:15 +07:00
<td class="col-level"><span class="lv lv- ${ log . level } "> ${ log . level } </span></td>
2026-04-29 10:30:49 +07:00
<td class="col-msg"> ${ esc ( log . message ) } ${ log . exception || log . props || log . fullmessage ? ' <span style="color:var(--text3);font-size:10px">▸ details</span>' : '' } </td>
2026-04-29 08:16:15 +07:00
` ;
2026-04-29 09:31:32 +07:00
tr . addEventListener ( 'click' , ( ) => toggleDetail ( tr , log , idx ) ) ;
tbody . appendChild ( tr ) ;
} ) ;
// Sort indicators
document . querySelectorAll ( 'thead th' ) . forEach ( th => {
th . classList . remove ( 'sort-asc' , 'sort-desc' ) ;
if ( th . dataset . col === sortCol ) th . classList . add ( sortDir === 'asc' ? 'sort-asc' : 'sort-desc' ) ;
} ) ;
}
function toggleDetail ( tr , log , idx ) {
if ( expandedRows . has ( idx ) ) {
expandedRows . delete ( idx ) ;
const detailRow = tr . nextSibling ;
if ( detailRow && detailRow . classList . contains ( 'row-detail' ) ) detailRow . remove ( ) ;
return ;
}
expandedRows . add ( idx ) ;
const detailTr = document . createElement ( 'tr' ) ;
detailTr . className = 'row-detail' ;
let sections = '' ;
2026-04-29 10:30:49 +07:00
if ( log . fullmessage ) {
sections += ` <div class="detail-section"><div class="detail-label">Full Message</div><div class="detail-val"> ${ esc ( log . fullmessage ) } </div></div> ` ;
}
2026-04-29 09:31:32 +07:00
if ( log . exception ) {
sections += ` <div class="detail-section"><div class="detail-label">Exception / Stack Trace</div><div class="detail-val" style="color:var(--error)"> ${ esc ( log . exception ) } </div></div> ` ;
}
if ( log . props ) {
sections += ` <div class="detail-section"><div class="detail-label">Properties</div><div class="detail-val"> ${ esc ( JSON . stringify ( log . props , null , 2 ) ) } </div></div> ` ;
}
if ( ! sections ) return ;
detailTr . innerHTML = ` <td colspan="3"><div class="detail-inner"> ${ sections } </div></td> ` ;
tr . after ( detailTr ) ;
}
// ---- Level counts ----
function updateLevelCounts ( ) {
const counts = { TRACE : 0 , DEBUG : 0 , INFO : 0 , WARN : 0 , ERROR : 0 , FATAL : 0 } ;
allLogs . forEach ( l => { if ( counts [ l . level ] !== undefined ) counts [ l . level ] ++ ; } ) ;
Object . entries ( counts ) . forEach ( ( [ lv , cnt ] ) => {
document . getElementById ( 'cnt-' + lv ) . textContent = cnt . toLocaleString ( ) ;
} ) ;
}
2026-04-29 10:30:49 +07:00
function updateFilteredLevelCounts ( ) {
const counts = { TRACE : 0 , DEBUG : 0 , INFO : 0 , WARN : 0 , ERROR : 0 , FATAL : 0 } ;
filteredLogs . forEach ( l => { if ( counts [ l . level ] !== undefined ) counts [ l . level ] ++ ; } ) ;
Object . entries ( counts ) . forEach ( ( [ lv , cnt ] ) => {
document . getElementById ( 'cnt-' + lv ) . textContent = cnt . toLocaleString ( ) ;
} ) ;
}
2026-04-29 09:31:32 +07:00
// ---- Status bar ----
function updateStatus ( ) {
const s = document . getElementById ( 'status-showing' ) ;
const t = document . getElementById ( 'status-total' ) ;
s . textContent = ` ${ filteredLogs . length . toLocaleString ( ) } entries ` ;
t . textContent = ` ${ allLogs . length . toLocaleString ( ) } total ` ;
}
// ---- Helpers ----
function fmtTime ( d ) {
return d . toISOString ( ) . replace ( 'T' , ' ' ) . replace ( /\.\d+Z$/ , '' ) ;
}
2026-04-29 10:30:49 +07:00
function fmtTimeLocal ( d ) {
return d . toLocaleString ( 'sv-SE' ) . replace ( 'T' , ' ' ) . replace ( /\.\d+Z$/ , '' ) ;
}
2026-04-29 09:31:32 +07:00
function fmtShort ( d ) {
return d . toLocaleTimeString ( 'en' , { hour : '2-digit' , minute : '2-digit' , second : '2-digit' } ) ;
}
function esc ( s ) {
return String ( s ) . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) ;
}
// ---- Load file ----
function loadFile ( file ) {
//debugger
2026-04-29 10:30:49 +07:00
showLoading ( ) ;
2026-04-29 09:31:32 +07:00
const reader = new FileReader ( ) ;
reader . onload = e => {
const text = e . target . result ;
2026-04-29 09:43:59 +07:00
var date = parseDateFromFilename ( file . name ) ;
allLogs = parseLogs ( text , date ) ;
2026-04-29 09:31:32 +07:00
if ( ! allLogs . length ) { allLogs = generateDemoLogs ( ) ; }
initView ( file . name ) ;
} ;
reader . readAsText ( file ) ;
2026-04-29 09:43:59 +07:00
}
function parseDateFromFilename ( filename ) {
const formats = [
{ regex : /(\d{4})[-_](\d{2})[-_](\d{2})/ , map : ( m ) => ` ${ m [ 1 ] } - ${ m [ 2 ] } - ${ m [ 3 ] } ` } , // YYYY-MM-DD
{ regex : /(\d{2})[-_](\d{2})[-_](\d{4})/ , map : ( m ) => ` ${ m [ 3 ] } - ${ m [ 2 ] } - ${ m [ 1 ] } ` } , // DD-MM-YYYY
{ regex : /(\d{4})(\d{2})(\d{2})/ , map : ( m ) => ` ${ m [ 1 ] } - ${ m [ 2 ] } - ${ m [ 3 ] } ` } // YYYYMMDD
] ;
for ( const { regex , map } of formats ) {
const match = filename . match ( regex ) ;
if ( match ) {
const isoString = map ( match ) ;
const date = new Date ( isoString ) ;
if ( ! isNaN ( date . getTime ( ) ) ) return date ;
}
}
return null ;
}
function translateLogLevel ( level ) {
//DEBUG: 0, INFO: 0, WARN: 0, ERROR: 0, FATAL: 0, total: 0
// Normalize input to handle lowercase or uppercase variations
switch ( level . toLowerCase ( ) ) {
case 'trc' :
return "TRACE" ;
case 'dbg' :
return "DEBUG" ;
case 'inf' :
return "INFO" ;
case 'wrn' :
case 'warning' : // Multiple cases can map to the same result
return "WARN" ;
case 'error' :
return "ERROR" ;
case 'fatal' :
case 'crt' :
return "FATAL" ;
default :
return level ; // Unknown level
2026-04-29 09:31:32 +07:00
}
2026-04-29 09:43:59 +07:00
}
2026-04-29 09:31:32 +07:00
function loadDemo ( ) {
allLogs = generateDemoLogs ( ) ;
initView ( 'demo.log' ) ;
}
function initView ( filename ) {
2026-04-29 08:16:15 +07:00
levelFilter = null ;
2026-04-29 09:31:32 +07:00
textFilter = '' ;
timeFilter = null ;
expandedRows . clear ( ) ;
document . getElementById ( 'search' ) . value = '' ;
document . getElementById ( 'time-badge' ) . classList . remove ( 'visible' ) ;
2026-04-29 08:16:15 +07:00
document . querySelectorAll ( '.level-pill' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
2026-04-29 09:31:32 +07:00
document . getElementById ( 'file-info' ) . textContent = filename + ' — ' + allLogs . length . toLocaleString ( ) + ' entries' ;
document . getElementById ( 'status-file' ) . textContent = filename ;
document . getElementById ( 'empty-state' ) . style . display = 'none' ;
document . getElementById ( 'table-area' ) . style . display = 'flex' ;
document . getElementById ( 'status-bar' ) . style . display = 'flex' ;
updateLevelCounts ( ) ;
filteredLogs = [ ... allLogs ] ;
buildChart ( allLogs ) ;
renderTable ( ) ;
updateStatus ( ) ;
2026-04-29 10:30:49 +07:00
hideLoading ( ) ;
2026-04-29 09:31:32 +07:00
}
2026-04-29 10:30:49 +07:00
const loader = document . getElementById ( 'loader' ) ;
function showLoading ( ) {
if ( ! loader . classList . contains ( 'is-active' ) ) {
loader . classList . add ( 'is-active' ) ;
}
}
function hideLoading ( ) {
loader . classList . remove ( 'is-active' ) ;
}
2026-04-29 09:31:32 +07:00
// ---- Events ----
document . getElementById ( 'file-input' ) . addEventListener ( 'change' , e => {
if ( e . target . files [ 0 ] ) loadFile ( e . target . files [ 0 ] ) ;
} ) ;
document . getElementById ( 'search' ) . addEventListener ( 'input' , ( ) => applyFilters ( ) ) ;
document . querySelectorAll ( '.level-pill' ) . forEach ( pill => {
pill . addEventListener ( 'click' , ( ) => {
const lv = pill . dataset . level ;
if ( levelFilter === lv ) {
levelFilter = null ;
pill . classList . remove ( 'active' ) ;
} else {
levelFilter = lv ;
document . querySelectorAll ( '.level-pill' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
pill . classList . add ( 'active' ) ;
}
applyFilters ( ) ;
} ) ;
} ) ;
document . getElementById ( 'time-badge' ) . addEventListener ( 'click' , ( ) => clearTimeFilter ( ) ) ;
document . getElementById ( 'clear-filters' ) . addEventListener ( 'click' , ( ) => {
2026-04-29 10:30:49 +07:00
//showLoading()
2026-04-29 09:31:32 +07:00
levelFilter = null ;
document . getElementById ( 'search' ) . value = '' ;
document . querySelectorAll ( '.level-pill' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
clearTimeFilter ( ) ;
applyFilters ( ) ;
} ) ;
// Sort headers
document . querySelectorAll ( 'thead th[data-col]' ) . forEach ( th => {
th . addEventListener ( 'click' , ( ) => {
const col = th . dataset . col ;
if ( sortCol === col ) sortDir = sortDir === 'asc' ? 'desc' : 'asc' ;
else { sortCol = col ; sortDir = 'asc' ; }
renderTable ( ) ;
} ) ;
} ) ;
// Drag & drop
const dropOverlay = document . getElementById ( 'drop-overlay' ) ;
let dragCount = 0 ;
document . addEventListener ( 'dragenter' , e => {
e . preventDefault ( ) ;
dragCount ++ ;
if ( dragCount === 1 ) dropOverlay . classList . add ( 'active' ) ;
} ) ;
document . addEventListener ( 'dragleave' , e => {
dragCount -- ;
if ( dragCount === 0 ) dropOverlay . classList . remove ( 'active' ) ;
} ) ;
document . addEventListener ( 'dragover' , e => e . preventDefault ( ) ) ;
document . addEventListener ( 'drop' , e => {
e . preventDefault ( ) ;
dragCount = 0 ;
dropOverlay . classList . remove ( 'active' ) ;
const file = e . dataTransfer . files [ 0 ] ;
if ( file ) loadFile ( file ) ;
} ) ;
2026-04-29 09:43:59 +07:00
2026-04-29 09:31:32 +07:00
// Load demo on start
2026-04-29 10:30:49 +07:00
//loadDemo();
2026-04-29 09:31:32 +07:00
< / script >
2026-04-29 08:16:15 +07:00
< / body >
2026-04-29 09:31:32 +07:00
< / html >