@@ -7,6 +7,7 @@ const openLogsButton = document.getElementById("open-logs");
77const pauseButton = document . getElementById ( "pause" ) ;
88const clearButton = document . getElementById ( "clear" ) ;
99const autoScrollToggle = document . getElementById ( "autoscroll" ) ;
10+ const colorMessagesToggle = document . getElementById ( "color-messages" ) ;
1011const filterCheckboxes = Array . from ( document . querySelectorAll ( ".filters input[type='checkbox']" ) ) ;
1112
1213const desktopBridge = window . desktop ?? {
@@ -22,21 +23,147 @@ if (!window.desktop) {
2223 console . error ( "Desktop preload bridge unavailable. Renderer controls will be no-ops." ) ;
2324}
2425
26+ const PREFERENCES_STORAGE_KEY = "desktop-log-preferences" ;
27+
28+ function readStoredPreferences ( ) {
29+ try {
30+ const raw = window . localStorage ?. getItem ( PREFERENCES_STORAGE_KEY ) ;
31+ if ( ! raw ) {
32+ return { } ;
33+ }
34+ const parsed = JSON . parse ( raw ) ;
35+ return typeof parsed === "object" && parsed ? parsed : { } ;
36+ } catch ( error ) {
37+ console . warn ( "[preferences] Failed to read stored preferences" , error ) ;
38+ return { } ;
39+ }
40+ }
41+
42+ let storedPreferences = readStoredPreferences ( ) ;
43+
44+ function persistPreferences ( patch ) {
45+ storedPreferences = { ...storedPreferences , ...patch } ;
46+ try {
47+ window . localStorage ?. setItem (
48+ PREFERENCES_STORAGE_KEY ,
49+ JSON . stringify ( storedPreferences ) ,
50+ ) ;
51+ } catch ( error ) {
52+ console . warn ( "[preferences] Failed to persist preferences" , error ) ;
53+ }
54+ }
55+
56+ const INFO_MESSAGE_COLORS = [ "#aaa" , "#fff" ] ;
57+
58+ const infoMessageColorState = {
59+ lastIdentifier : null ,
60+ colorIndex : 0 ,
61+ } ;
62+
2563const state = {
2664 paused : false ,
27- autoScroll : true ,
28- filters : new Set ( [ "messages" , "desktop" ] ) ,
65+ autoScroll :
66+ typeof storedPreferences . autoScroll === "boolean"
67+ ? storedPreferences . autoScroll
68+ : true ,
69+ colorMessages :
70+ typeof storedPreferences . colorMessages === "boolean"
71+ ? storedPreferences . colorMessages
72+ : false ,
73+ filters : new Set ( [ "messages" , "http" , "desktop" ] ) ,
2974 logs : [ ] ,
3075} ;
3176
32- function scrollToBottom ( ) {
33- if ( ! state . autoScroll ) return ;
34- logContainer . scrollTop = logContainer . scrollHeight ;
77+ function scrollToBottom ( reason = "unknown" ) {
78+ if ( ! logContainer ) {
79+ console . warn ( "[scrollToBottom] Missing log container" , { reason } ) ;
80+ return ;
81+ }
82+
83+ if ( ! state . autoScroll ) {
84+ console . debug ( "[scrollToBottom] Skipped because auto-scroll is disabled" , {
85+ reason,
86+ scrollTop : logContainer . scrollTop ,
87+ scrollHeight : logContainer . scrollHeight ,
88+ clientHeight : logContainer . clientHeight ,
89+ } ) ;
90+ return ;
91+ }
92+
93+ const before = {
94+ scrollTop : logContainer . scrollTop ,
95+ scrollHeight : logContainer . scrollHeight ,
96+ clientHeight : logContainer . clientHeight ,
97+ } ;
98+
99+ requestAnimationFrame ( ( ) => {
100+ const lastEntry = logContainer . lastElementChild ;
101+ if ( lastEntry instanceof HTMLElement ) {
102+ lastEntry . scrollIntoView ( { block : "end" } ) ;
103+ }
104+ logContainer . scrollTop = logContainer . scrollHeight ;
105+ const after = {
106+ scrollTop : logContainer . scrollTop ,
107+ scrollHeight : logContainer . scrollHeight ,
108+ clientHeight : logContainer . clientHeight ,
109+ } ;
110+ console . debug ( "[scrollToBottom] Applied" , { reason, before, after, hasEntry : Boolean ( lastEntry ) } ) ;
111+ } ) ;
35112}
36113
37114function formatTimestamp ( timestamp ) {
38115 if ( ! timestamp ) return "" ;
39- return timestamp ;
116+ const normalized = timestamp . includes ( "T" ) ? timestamp : timestamp . replace ( " " , "T" ) ;
117+ const date = new Date ( normalized ) ;
118+ if ( Number . isNaN ( date . getTime ( ) ) ) {
119+ return timestamp ;
120+ }
121+ const hours = `${ date . getHours ( ) } ` . padStart ( 2 , "0" ) ;
122+ const minutes = `${ date . getMinutes ( ) } ` . padStart ( 2 , "0" ) ;
123+ const seconds = `${ date . getSeconds ( ) } ` . padStart ( 2 , "0" ) ;
124+ return `${ hours } :${ minutes } :${ seconds } ` ;
125+ }
126+
127+ function resetInfoMessageColorState ( ) {
128+ infoMessageColorState . lastIdentifier = null ;
129+ infoMessageColorState . colorIndex = 0 ;
130+ }
131+
132+ function extractInfoMessageIdentifier ( trimmed ) {
133+ if ( ! trimmed ) {
134+ return null ;
135+ }
136+ const firstWhitespaceIndex = trimmed . search ( / \s / ) ;
137+ return firstWhitespaceIndex === - 1 ? trimmed : trimmed . slice ( 0 , firstWhitespaceIndex ) ;
138+ }
139+
140+ function enrichLogEntry ( entry ) {
141+ const enriched = { ...entry } ;
142+ if ( entry . level !== "info" && entry . level !== "verbose" ) {
143+ return enriched ;
144+ }
145+
146+ const trimmed = entry . message ?. trim ( ) ;
147+ if ( ! trimmed ) {
148+ return enriched ;
149+ }
150+
151+ const identifier = extractInfoMessageIdentifier ( trimmed ) ;
152+ if ( ! identifier ) {
153+ return enriched ;
154+ }
155+
156+ if ( infoMessageColorState . lastIdentifier === null ) {
157+ infoMessageColorState . colorIndex = 0 ;
158+ } else if ( identifier !== infoMessageColorState . lastIdentifier ) {
159+ infoMessageColorState . colorIndex =
160+ ( infoMessageColorState . colorIndex + 1 ) % INFO_MESSAGE_COLORS . length ;
161+ }
162+
163+ infoMessageColorState . lastIdentifier = identifier ;
164+ enriched . infoColor = INFO_MESSAGE_COLORS [ infoMessageColorState . colorIndex ] ;
165+ enriched . infoIdentifier = identifier ;
166+ return enriched ;
40167}
41168
42169function renderEntry ( entry ) {
@@ -45,12 +172,20 @@ function renderEntry(entry) {
45172 }
46173 const node = template . content . firstElementChild . cloneNode ( true ) ;
47174 node . dataset . source = entry . source ;
48- node . querySelector ( ".log-source" ) . textContent = entry . source ;
49- node . querySelector ( ".log-timestamp" ) . textContent = formatTimestamp ( entry . timestamp ) ;
175+ const timestampElement = node . querySelector ( ".log-timestamp" ) ;
176+ timestampElement . textContent = formatTimestamp ( entry . timestamp ) ;
177+ timestampElement . dataset . level = entry . level ;
50178 const levelElement = node . querySelector ( ".log-level" ) ;
51179 levelElement . textContent = entry . level . toUpperCase ( ) ;
52180 levelElement . dataset . level = entry . level ;
53- node . querySelector ( ".log-message" ) . textContent = entry . message ;
181+ const messageElement = node . querySelector ( ".log-message" ) ;
182+ messageElement . textContent = entry . message ;
183+ messageElement . dataset . level = entry . level ;
184+ if ( ( entry . level === "info" || entry . level === "verbose" ) && entry . infoColor ) {
185+ messageElement . style . setProperty ( "--message-color" , entry . infoColor ) ;
186+ } else {
187+ messageElement . style . removeProperty ( "--message-color" ) ;
188+ }
54189 logContainer . appendChild ( node ) ;
55190}
56191
@@ -63,17 +198,23 @@ function renderAll() {
63198 logContainer . appendChild ( empty ) ;
64199 return ;
65200 }
201+ resetInfoMessageColorState ( ) ;
202+ state . logs = state . logs . map ( ( entry ) => enrichLogEntry ( entry ) ) ;
66203 state . logs . forEach ( ( entry ) => {
67204 renderEntry ( entry ) ;
68205 } ) ;
69- scrollToBottom ( ) ;
206+ scrollToBottom ( "renderAll" ) ;
70207}
71208
72209function handleLog ( entry ) {
73- state . logs . push ( entry ) ;
74- if ( state . paused ) return ;
75- renderEntry ( entry ) ;
76- scrollToBottom ( ) ;
210+ const enriched = enrichLogEntry ( entry ) ;
211+ state . logs . push ( enriched ) ;
212+ if ( state . paused ) {
213+ console . debug ( "[handleLog] Received log while paused" , entry ) ;
214+ return ;
215+ }
216+ renderEntry ( enriched ) ;
217+ scrollToBottom ( "handleLog" ) ;
77218}
78219
79220function updateStatus ( running ) {
@@ -117,11 +258,25 @@ pauseButton.addEventListener("click", () => {
117258
118259clearButton . addEventListener ( "click" , ( ) => {
119260 state . logs = [ ] ;
261+ resetInfoMessageColorState ( ) ;
120262 renderAll ( ) ;
121263} ) ;
122264
123265autoScrollToggle . addEventListener ( "change" , ( ) => {
124266 state . autoScroll = autoScrollToggle . checked ;
267+ if ( state . autoScroll ) {
268+ console . debug ( "[autoscroll] Enabled" ) ;
269+ scrollToBottom ( "autoScrollToggle" ) ;
270+ } else {
271+ console . debug ( "[autoscroll] Disabled" ) ;
272+ }
273+ persistPreferences ( { autoScroll : state . autoScroll } ) ;
274+ } ) ;
275+
276+ colorMessagesToggle . addEventListener ( "change" , ( ) => {
277+ state . colorMessages = colorMessagesToggle . checked ;
278+ logContainer . classList . toggle ( "color-messages" , state . colorMessages ) ;
279+ persistPreferences ( { colorMessages : state . colorMessages } ) ;
125280} ) ;
126281
127282desktopBridge . onLog ( handleLog ) ;
@@ -132,5 +287,8 @@ desktopBridge.onBotState((stateInfo) => {
132287window . addEventListener ( "DOMContentLoaded" , ( ) => {
133288 updateStatus ( false ) ;
134289 renderAll ( ) ;
290+ autoScrollToggle . checked = state . autoScroll ;
291+ colorMessagesToggle . checked = state . colorMessages ;
292+ logContainer . classList . toggle ( "color-messages" , state . colorMessages ) ;
135293 desktopBridge . notifyReady ( ) ;
136294} ) ;
0 commit comments