1919import shutil
2020import html as html_module
2121import time
22- from web_helper import get_header_html , get_plan_css , get_editor_js , get_editor_css , get_log_css , get_charts_css , get_apps_css , get_html_config_css , get_apps_js , get_components_css
22+ from web_helper import get_header_html , get_plan_css , get_editor_js , get_editor_css , get_log_css , get_charts_css , get_apps_css , get_html_config_css , get_apps_js , get_components_css , get_logfile_js
2323
2424from utils import calc_percent_limit , str2time , dp0 , dp2
2525from config import TIME_FORMAT , TIME_FORMAT_DAILY
@@ -141,6 +141,7 @@ async def start(self):
141141 app .router .add_get ("/api/ping" , self .html_api_ping )
142142 app .router .add_post ("/api/state" , self .html_api_post_state )
143143 app .router .add_post ("/api/service" , self .html_api_post_service )
144+ app .router .add_get ("/api/log" , self .html_api_get_log )
144145 app .router .add_post ("/api/login" , self .html_api_login )
145146
146147 # Notify plugin system that web interface is ready
@@ -926,6 +927,73 @@ def render_chart(self, series_data, yaxis_name, chart_name, now_str, tagname="ch
926927 text += "</script>\n "
927928 return text
928929
930+ async def html_api_get_log (self , request ):
931+ """
932+ JSON API to get log data with filtering
933+ """
934+ try :
935+ logfile = "predbat.log"
936+ logdata = ""
937+
938+ if os .path .exists (logfile ):
939+ with open (logfile , "r" ) as f :
940+ logdata = f .read ()
941+
942+ # Get query parameters
943+ args = request .query
944+ filter_type = args .get ("filter" , "warnings" ) # all, warnings, errors
945+ since_line = int (args .get ("since" , 0 )) # Line number to start from
946+ max_lines = int (args .get ("max_lines" , 1024 )) # Maximum lines to return
947+
948+ loglines = logdata .split ("\n " )
949+ total_lines = len (loglines )
950+
951+ # Process log lines with filtering
952+ result_lines = []
953+ count_lines = 0
954+ lineno = total_lines - 1
955+
956+ while count_lines < max_lines and lineno >= 0 :
957+ line = loglines [lineno ]
958+ line_lower = line .lower ()
959+
960+ # Skip empty lines
961+ if not line .strip ():
962+ lineno -= 1
963+ continue
964+
965+ # Apply filtering
966+ include_line = False
967+ line_type = "info"
968+
969+ if "error" in line_lower :
970+ line_type = "error"
971+ include_line = True
972+ elif "warn" in line_lower :
973+ line_type = "warning"
974+ include_line = filter_type in ["all" , "warnings" ]
975+ else :
976+ line_type = "info"
977+ include_line = filter_type == "all"
978+
979+ if include_line and (since_line == 0 or lineno > since_line ):
980+ start_line = line [0 :27 ] if len (line ) >= 27 else line
981+ rest_line = line [27 :] if len (line ) >= 27 else ""
982+
983+ result_lines .append ({"line_number" : lineno , "timestamp" : start_line , "message" : rest_line , "type" : line_type , "full_line" : line })
984+ count_lines += 1
985+
986+ lineno -= 1
987+
988+ # Reverse to get reverse chronological order (newest first)
989+ result_lines .reverse ()
990+
991+ return web .json_response ({"status" : "success" , "total_lines" : total_lines , "returned_lines" : len (result_lines ), "lines" : result_lines , "filter" : filter_type })
992+
993+ except Exception as e :
994+ self .log (f"Error in html_api_get_log: { e } " )
995+ return web .json_response ({"status" : "error" , "message" : str (e )}, status = 500 )
996+
929997 async def html_api_post_state (self , request ):
930998 """
931999 JSON API
@@ -1186,16 +1254,11 @@ def add_button_to_load(match):
11861254
11871255 async def html_log (self , request ):
11881256 """
1189- Return the Predbat log as an HTML page
1257+ Return the Predbat log as an HTML page with dynamic updates
11901258 """
1191- logfile = "predbat.log"
1192- logdata = ""
11931259 self .default_page = "./log"
1194- if os .path .exists (logfile ):
1195- with open (logfile , "r" ) as f :
1196- logdata = f .read ()
11971260
1198- # Decode method get arguments
1261+ # Decode method get arguments to determine filter type
11991262 args = request .query
12001263 errors = False
12011264 warnings = False
@@ -1212,59 +1275,48 @@ async def html_log(self, request):
12121275 self .default_log = "warnings"
12131276 warnings = True
12141277
1215- loglines = logdata . split ( " \n " )
1216- text = self .get_header ("Predbat Log" , refresh = 10 )
1278+ # Remove refresh from header since we'll update dynamically
1279+ text = self .get_header ("Predbat Log" , refresh = 0 )
12171280 text += """<body>"""
12181281 text += get_log_css ()
12191282
12201283 if errors :
12211284 active_all = ""
12221285 active_warnings = ""
12231286 active_errors = "active"
1287+ filter_type = "errors"
12241288 elif warnings :
12251289 active_all = ""
12261290 active_warnings = "active"
12271291 active_errors = ""
1292+ filter_type = "warnings"
12281293 else :
12291294 active_all = "active"
12301295 active_warnings = ""
12311296 active_errors = ""
1297+ filter_type = "all"
12321298
12331299 text += '<div class="log-menu">'
12341300 text += "<h3>Logfile</h3> "
12351301 text += f'<a href="./log?all" class="{ active_all } ">All</a>'
12361302 text += f'<a href="./log?warnings" class="{ active_warnings } ">Warnings</a>'
12371303 text += f'<a href="./log?errors" class="{ active_errors } ">Errors</a>'
12381304 text += '<a href="./debug_log">Download</a>'
1305+ text += '<label class="auto-scroll-toggle"><input type="checkbox" id="autoScroll"> Auto-scroll to new</label>'
1306+ text += '<button class="scroll-to-bottom" onclick="scrollToBottom()">Scroll to Bottom</button>'
1307+ text += '<button id="pauseResumeBtn" onclick="toggleUpdates()" style="margin-left: 10px; padding: 4px 8px;">Pause</button>'
12391308 text += "</div>"
12401309
1241- text += "<table width=100%>\n "
1242-
1243- total_lines = len (loglines )
1244- count_lines = 0
1245- lineno = total_lines - 1
1246- while count_lines < 1024 and lineno >= 0 :
1247- line = loglines [lineno ]
1248- line_lower = line .lower ()
1249- lineno -= 1
1250-
1251- start_line = line [0 :27 ]
1252- rest_line = line [27 :]
1253-
1254- if "error" in line_lower :
1255- text += "<tr><td>{}</td><td nowrap><font color=#ff3333>{}</font> {}</td></tr>\n " .format (lineno , start_line , rest_line )
1256- count_lines += 1
1257- continue
1258- elif (not errors ) and ("warn" in line_lower ):
1259- text += "<tr><td>{}</td><td nowrap><font color=#ffA500>{}</font> {}</td></tr>\n " .format (lineno , start_line , rest_line )
1260- count_lines += 1
1261- continue
1310+ text += '<div id="logStatus" class="log-status">Loading log data...</div>'
1311+ text += "<table width=100% id='logTable'>\n "
1312+ text += "<tbody id='logTableBody'>\n "
1313+ text += "<!-- Log entries will be loaded dynamically via JavaScript -->\n "
1314+ text += "</tbody>\n "
1315+ text += "</table>"
12621316
1263- if line and (not errors ) and (not warnings ):
1264- text += "<tr><td>{}</td><td nowrap><font color=#33cc33>{}</font> {}</td></tr>\n " .format (lineno , start_line , rest_line )
1265- count_lines += 1
1317+ # Add JavaScript for dynamic updates
1318+ text += get_logfile_js (filter_type )
12661319
1267- text += "</table>"
12681320 text += "</body></html>\n "
12691321 return web .Response (content_type = "text/html" , text = text )
12701322
0 commit comments