@@ -5,6 +5,7 @@ const state = {
55 counts : new Map ( ) ,
66 names : new Map ( ) ,
77 series : [ ] ,
8+ clientLog : [ ] ,
89 remote : {
910 user : "" ,
1011 host : "" ,
@@ -49,6 +50,8 @@ const els = {
4950 optionsSectionBody : document . getElementById ( "optionsSectionBody" ) ,
5051 toggleReportsSection : document . getElementById ( "toggleReportsSection" ) ,
5152 reportsSectionBody : document . getElementById ( "reportsSectionBody" ) ,
53+ toggleLogSection : document . getElementById ( "toggleLogSection" ) ,
54+ logSectionBody : document . getElementById ( "logSectionBody" ) ,
5255 uncollapseBtn : document . getElementById ( "uncollapseBtn" ) ,
5356 uncollapseTipsBtn : document . getElementById ( "uncollapseTipsBtn" ) ,
5457 subtreeReportBtn : document . getElementById ( "subtreeReportBtn" ) ,
@@ -105,6 +108,9 @@ const els = {
105108 sourceLegend : document . getElementById ( "sourceLegend" ) ,
106109 topTableWrap : document . getElementById ( "topTableWrap" ) ,
107110 topTable : document . getElementById ( "topTable" ) ,
111+ clientLogMeta : document . getElementById ( "clientLogMeta" ) ,
112+ clientLog : document . getElementById ( "clientLog" ) ,
113+ clearClientLogBtn : document . getElementById ( "clearClientLogBtn" ) ,
108114} ;
109115
110116els . renderBtn . addEventListener ( "click" , loadAndRender ) ;
@@ -156,6 +162,9 @@ els.clearSelectionBtn.addEventListener("click", clearSelection);
156162if ( els . exportMatrixBtn ) {
157163 els . exportMatrixBtn . addEventListener ( "click" , exportCurrentSubtreeMatrix ) ;
158164}
165+ if ( els . clearClientLogBtn ) {
166+ els . clearClientLogBtn . addEventListener ( "click" , clearClientLog ) ;
167+ }
159168initControlsResize ( ) ;
160169initSummaryResize ( ) ;
161170initTablePanel ( ) ;
@@ -190,6 +199,7 @@ function initRemotePanel() {
190199 updateConnectionState ( false , "Not connected" ) ;
191200 renderRemoteDatasets ( ) ;
192201 updateRenderAvailability ( ) ;
202+ renderClientLog ( ) ;
193203}
194204
195205function initFileInputs ( ) {
@@ -340,6 +350,7 @@ function initSidebarPanel() {
340350 initSectionToggle ( "filesSection" , els . toggleFilesSection , els . filesSectionBody ) ;
341351 initSectionToggle ( "optionsSection" , els . toggleOptionsSection , els . optionsSectionBody ) ;
342352 initSectionToggle ( "reportsSection" , els . toggleReportsSection , els . reportsSectionBody ) ;
353+ initSectionToggle ( "logSection" , els . toggleLogSection , els . logSectionBody ) ;
343354}
344355
345356function initSectionToggle ( key , button , body ) {
@@ -396,6 +407,7 @@ async function connectRemote() {
396407
397408 updateConnectionState ( false , "Connecting..." ) ;
398409 setStatus ( `Testing local tunnel endpoint for ${ user } @${ host } ...` ) ;
410+ addClientLog ( "info" , "tunnel" , `Testing tunnel endpoint for ${ user } @${ host } .` , "GET http://localhost:8000/ping" ) ;
399411 try {
400412 const controller = new AbortController ( ) ;
401413 const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 2500 ) ;
@@ -410,10 +422,17 @@ async function connectRemote() {
410422 const ping = await response . json ( ) ;
411423 updateRemoteServerStatus ( ping ) ;
412424 updateConnectionState ( true , "Connected" ) ;
425+ addClientLog (
426+ "success" ,
427+ "tunnel" ,
428+ `Tunnel check succeeded for ${ user } @${ host } .` ,
429+ `backend taxonomy: nodes=${ ping ?. taxonomy ?. nodes_file || "missing" } , names=${ ping ?. taxonomy ?. names_file || "missing" } ` ,
430+ ) ;
413431 try {
414432 await refreshRemoteDatasets ( { selectAll : true } ) ;
415433 setStatus ( remoteConnectionReadyMessage ( user , host ) ) ;
416434 } catch ( error ) {
435+ addClientLog ( "error" , "datasets" , `Remote dataset refresh failed after tunnel check.` , errorToDetail ( error ) ) ;
417436 setStatus ( `Tunnel check succeeded for ${ user } @${ host } , but the remote dataset list could not be loaded yet. ${ error . message || error } ` ) ;
418437 }
419438 } catch ( error ) {
@@ -422,6 +441,7 @@ async function connectRemote() {
422441 state . remote . selectedDatasets . clear ( ) ;
423442 updateRemoteServerStatus ( null ) ;
424443 renderRemoteDatasets ( ) ;
444+ addClientLog ( "error" , "tunnel" , `Tunnel check failed for ${ user } @${ host } .` , errorToDetail ( error ) ) ;
425445 setStatus ( `Tunnel check failed for ${ user } @${ host } . Make sure the SSH tunnel is open and the remote HTTP server is running on port 8000.` ) ;
426446 }
427447}
@@ -466,12 +486,20 @@ async function uploadLoadedFiles() {
466486 }
467487
468488 els . uploadBtn . disabled = true ;
489+ addClientLog (
490+ "info" ,
491+ "upload" ,
492+ `Uploading ${ localFiles . length . toLocaleString ( ) } local file${ localFiles . length === 1 ? "" : "s" } to the backend.` ,
493+ localFiles . map ( ( file ) => file . name ) . join ( "\n" ) ,
494+ ) ;
469495 try {
470496 const uploaded = await uploadFilesToRemote ( localFiles ) ;
471497 await refreshRemoteServerStatus ( ) ;
472498 await refreshRemoteDatasets ( ) ;
499+ addClientLog ( "success" , "upload" , `Uploaded ${ uploaded . toLocaleString ( ) } file(s) successfully.` ) ;
473500 setStatus ( `Uploaded ${ uploaded . toLocaleString ( ) } file(s) to the remote server for ${ user } @${ host } .` ) ;
474501 } catch ( error ) {
502+ addClientLog ( "error" , "upload" , "Upload stopped." , errorToDetail ( error ) ) ;
475503 setStatus ( `Upload stopped. ${ error . message || error } ` ) ;
476504 } finally {
477505 els . uploadBtn . disabled = false ;
@@ -531,6 +559,7 @@ async function loadAndRender() {
531559 redraw ( ) ;
532560 } catch ( error ) {
533561 console . error ( error ) ;
562+ addClientLog ( "error" , state . remote . connected ? "tree" : "local" , "Could not render tree." , errorToDetail ( error ) ) ;
534563 setStatus ( `Could not render tree: ${ error . message || error } ` ) ;
535564 }
536565}
@@ -553,13 +582,22 @@ async function uploadFilesToRemote(files) {
553582 for ( const file of files ) {
554583 const form = new FormData ( ) ;
555584 form . append ( "file" , file , file . name ) ;
556- const response = await fetch ( "http://localhost:8000/upload" , {
557- method : "POST" ,
558- body : form ,
559- } ) ;
585+ addClientLog ( "info" , "upload" , `Uploading file ${ file . name } .` ) ;
586+ let response ;
587+ try {
588+ response = await fetch ( "http://localhost:8000/upload" , {
589+ method : "POST" ,
590+ body : form ,
591+ } ) ;
592+ } catch ( error ) {
593+ addClientLog ( "error" , "upload" , `Upload fetch failed for ${ file . name } .` , errorToDetail ( error ) ) ;
594+ throw error ;
595+ }
560596 if ( ! response . ok ) {
597+ addClientLog ( "error" , "upload" , `Upload failed for ${ file . name } .` , `HTTP ${ response . status } ` ) ;
561598 throw new Error ( `Upload failed for ${ file . name } with HTTP ${ response . status } ` ) ;
562599 }
600+ addClientLog ( "success" , "upload" , `Upload finished for ${ file . name } .` ) ;
563601 uploaded ++ ;
564602 }
565603 return uploaded ;
@@ -586,14 +624,22 @@ async function loadAndRenderRemote() {
586624 }
587625
588626 state . remote . expandedTaxids . clear ( ) ;
627+ addClientLog (
628+ "info" ,
629+ "tree" ,
630+ `Requesting remote root tree view for ${ files . length . toLocaleString ( ) } dataset${ files . length === 1 ? "" : "s" } .` ,
631+ files . join ( "\n" ) ,
632+ ) ;
589633 const payload = await fetchRemoteVisibleTree ( ) ;
590634 if ( ! payload . tree ) {
635+ addClientLog ( "error" , "tree" , "The remote backend returned no tree payload." ) ;
591636 setStatus ( "The remote backend returned no tree to render." ) ;
592637 return ;
593638 }
594639
595640 applyRemoteVisiblePayload ( payload ) ;
596641 state . centerOnNextRender = true ;
642+ addClientLog ( "success" , "tree" , `Loaded backend tree with ${ Number ( payload . direct_taxa || 0 ) . toLocaleString ( ) } direct taxa.` ) ;
597643 setStatus ( `Loaded backend tree for ${ state . series . length . toLocaleString ( ) } remote dataset${ state . series . length === 1 ? "" : "s" } and ${ state . remote . directTaxa . toLocaleString ( ) } direct taxa.` ) ;
598644 redraw ( ) ;
599645}
@@ -760,13 +806,23 @@ function buildRemoteContextUrl(path, options = {}) {
760806
761807async function fetchRemoteVisibleTree ( options = { } ) {
762808 const endpoint = options . taxid != null ? "expand-node" : "root-view" ;
763- const response = await fetch ( buildRemoteContextUrl ( endpoint , {
809+ const url = buildRemoteContextUrl ( endpoint , {
764810 expandedTaxids : options . expandedTaxids || Array . from ( state . remote . expandedTaxids ) ,
765811 query : options . taxid != null ? { taxid : options . taxid } : { } ,
766- } ) . toString ( ) , { method : "GET" } ) ;
812+ } ) . toString ( ) ;
813+ addClientLog ( "info" , "tree" , `GET /${ endpoint } ` , url ) ;
814+ let response ;
815+ try {
816+ response = await fetch ( url , { method : "GET" } ) ;
817+ } catch ( error ) {
818+ addClientLog ( "error" , "tree" , `Remote ${ endpoint } fetch failed.` , errorToDetail ( error ) ) ;
819+ throw error ;
820+ }
767821 if ( ! response . ok ) {
822+ addClientLog ( "error" , "tree" , `Remote ${ endpoint } request failed.` , `HTTP ${ response . status } ` ) ;
768823 throw new Error ( `Remote ${ endpoint } request failed with HTTP ${ response . status } ` ) ;
769824 }
825+ addClientLog ( "success" , "tree" , `Remote ${ endpoint } request succeeded.` ) ;
770826 return response . json ( ) ;
771827}
772828
@@ -826,11 +882,20 @@ async function fetchRemoteRankReport(taxids) {
826882}
827883
828884async function fetchRemoteFullTreeModel ( ) {
829- const response = await fetch ( buildRemoteContextUrl ( "tree-model" ) . toString ( ) , { method : "GET" } ) ;
885+ addClientLog ( "info" , "tree" , "GET /tree-model" ) ;
886+ let response ;
887+ try {
888+ response = await fetch ( buildRemoteContextUrl ( "tree-model" ) . toString ( ) , { method : "GET" } ) ;
889+ } catch ( error ) {
890+ addClientLog ( "error" , "tree" , "Remote tree-model fetch failed." , errorToDetail ( error ) ) ;
891+ throw error ;
892+ }
830893 const payload = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
831894 if ( ! response . ok ) {
895+ addClientLog ( "error" , "tree" , "Remote tree-model request failed." , payload ?. detail ?. message || `HTTP ${ response . status } ` ) ;
832896 throw new Error ( payload ?. detail ?. message || `Remote tree-model request failed with HTTP ${ response . status } ` ) ;
833897 }
898+ addClientLog ( "success" , "tree" , "Remote tree-model request succeeded." ) ;
834899 return payload ;
835900}
836901
@@ -865,10 +930,18 @@ async function refreshRemoteDatasets(options = {}) {
865930 }
866931
867932 const { selectAll = false } = options ;
868- const response = await fetch ( "http://localhost:8000/datasets" , {
869- method : "GET" ,
870- } ) ;
933+ addClientLog ( "info" , "datasets" , "Refreshing remote dataset list." , "GET http://localhost:8000/datasets" ) ;
934+ let response ;
935+ try {
936+ response = await fetch ( "http://localhost:8000/datasets" , {
937+ method : "GET" ,
938+ } ) ;
939+ } catch ( error ) {
940+ addClientLog ( "error" , "datasets" , "Remote datasets fetch failed." , errorToDetail ( error ) ) ;
941+ throw error ;
942+ }
871943 if ( ! response . ok ) {
944+ addClientLog ( "error" , "datasets" , "Remote datasets request failed." , `HTTP ${ response . status } ` ) ;
872945 throw new Error ( `Remote datasets request failed with HTTP ${ response . status } ` ) ;
873946 }
874947
@@ -896,6 +969,7 @@ async function refreshRemoteDatasets(options = {}) {
896969 }
897970
898971 renderRemoteDatasets ( ) ;
972+ addClientLog ( "success" , "datasets" , `Loaded ${ state . remote . datasets . length . toLocaleString ( ) } remote dataset${ state . remote . datasets . length === 1 ? "" : "s" } .` ) ;
899973}
900974
901975function renderRemoteDatasets ( ) {
@@ -2363,6 +2437,70 @@ function setStatus(message) {
23632437 els . status . textContent = message ;
23642438}
23652439
2440+ function addClientLog ( level , stage , message , detail = "" ) {
2441+ const entry = {
2442+ timestamp : new Date ( ) . toISOString ( ) ,
2443+ level : String ( level || "info" ) ,
2444+ stage : String ( stage || "general" ) ,
2445+ message : String ( message || "" ) ,
2446+ detail : String ( detail || "" ) ,
2447+ } ;
2448+ state . clientLog . unshift ( entry ) ;
2449+ if ( state . clientLog . length > 200 ) {
2450+ state . clientLog . length = 200 ;
2451+ }
2452+ renderClientLog ( ) ;
2453+ }
2454+
2455+ function clearClientLog ( ) {
2456+ state . clientLog = [ ] ;
2457+ renderClientLog ( ) ;
2458+ setStatus ( "Cleared client log." ) ;
2459+ }
2460+
2461+ function renderClientLog ( ) {
2462+ if ( ! els . clientLog || ! els . clientLogMeta ) return ;
2463+ const entries = state . clientLog ;
2464+ els . clientLogMeta . textContent = entries . length
2465+ ? `${ entries . length . toLocaleString ( ) } recent client event${ entries . length === 1 ? "" : "s" } `
2466+ : "No client log entries yet" ;
2467+ if ( ! entries . length ) {
2468+ els . clientLog . innerHTML = `<div class="remote-datasets-empty">Client-side upload, dataset, and tree requests will appear here.</div>` ;
2469+ return ;
2470+ }
2471+ els . clientLog . innerHTML = entries . map ( ( entry ) => `
2472+ <div class="client-log-entry">
2473+ <div class="client-log-entry-head">
2474+ <div class="client-log-badges">
2475+ <span class="client-log-badge stage-${ escapeHtml ( entry . stage ) } ">${ escapeHtml ( entry . stage ) } </span>
2476+ <span class="client-log-badge level-${ escapeHtml ( entry . level ) } ">${ escapeHtml ( entry . level ) } </span>
2477+ </div>
2478+ <span class="client-log-time">${ escapeHtml ( formatLogTime ( entry . timestamp ) ) } </span>
2479+ </div>
2480+ <div class="client-log-message">${ escapeHtml ( entry . message ) } </div>
2481+ ${ entry . detail ? `<pre class="client-log-detail">${ escapeHtml ( entry . detail ) } </pre>` : "" }
2482+ </div>
2483+ ` ) . join ( "" ) ;
2484+ }
2485+
2486+ function formatLogTime ( timestamp ) {
2487+ const parsed = new Date ( timestamp ) ;
2488+ if ( Number . isNaN ( parsed . getTime ( ) ) ) return timestamp ;
2489+ return parsed . toLocaleTimeString ( [ ] , {
2490+ hour : "2-digit" ,
2491+ minute : "2-digit" ,
2492+ second : "2-digit" ,
2493+ } ) ;
2494+ }
2495+
2496+ function errorToDetail ( error ) {
2497+ if ( ! error ) return "" ;
2498+ if ( typeof error === "string" ) return error ;
2499+ const message = error . message || String ( error ) ;
2500+ const stack = typeof error . stack === "string" ? error . stack : "" ;
2501+ return stack && ! stack . startsWith ( message ) ? `${ message } \n${ stack } ` : message ;
2502+ }
2503+
23662504function initChartPan ( ) {
23672505 let pointerId = null ;
23682506 let startX = 0 ;
0 commit comments