@@ -778,12 +778,22 @@ _lmd_render_json_list() {
778778 # produced a visible hang on large indexes (~20K+ sessions).
779779 local -A _seen_ids
780780
781+ # Buffer each entry as "epoch\tJSON_LITERAL" then sort-desc for global
782+ # ordering across index + legacy passes. Mirrors lmd_session.sh text path
783+ # (see _view_session_list in lmd_session.sh:329-418). Fixes issue #483:
784+ # pre-fix code emitted index entries in append-order, then legacy entries
785+ # after, producing a misordered reports[] array (e.g. Mar 29 legacy after
786+ # Apr 18 TSV). Tab never appears in emitted JSON — _json_escape_var
787+ # replaces real tabs with \t.
788+ local _sortbuf
789+ _sortbuf=$( mktemp " $tmpdir /.jsonlist.XXXXXX" )
790+
781791 # Rebuild index from TSV files if missing (first call on upgraded server)
782792 if [ ! -f " $_index_file " ]; then
783793 _session_index_rebuild
784794 fi
785795
786- # Fast path : read from session.index (covers all TSV sessions)
796+ # Pass 1 : read from session.index (covers all TSV sessions)
787797 if [ -f " $_index_file " ]; then
788798 local _ix_scanid _ix_epoch _ix_started_hr _ix_elapsed
789799 local _ix_tot_files _ix_tot_hits _ix_tot_cl _ix_tot_quar _ix_path
@@ -796,32 +806,36 @@ _lmd_render_json_list() {
796806 _ix_tot_quar=" 0"
797807 fi
798808 _seen_ids[" $_ix_scanid " ]=1
799- if [ " $_first " != " 1" ]; then printf " ," ; fi
800- _first=0
801- printf ' \n {'
802- printf ' "scan_id": "%s", ' " $_ix_scanid "
803- if [ " $_ix_started_hr " = " -" ]; then printf ' "started": null, '
804- else printf ' "started": "%s", ' " $_ix_started_hr " ; fi
805- if [ " $_ix_tot_files " = " -" ]; then printf ' "total_files": null, '
806- else printf ' "total_files": %s, ' " $_ix_tot_files " ; fi
807- if [ " $_ix_tot_hits " = " -" ]; then printf ' "total_hits": null, '
808- else printf ' "total_hits": %s, ' " $_ix_tot_hits " ; fi
809- local _jval=" ${_ix_tot_cl:- 0} "
810- [ " $_jval " = " -" ] && _jval=" 0"
811- printf ' "total_cleaned": %s, ' " $_jval "
812- local _jquar=" ${_ix_tot_quar:- 0} "
813- [ " $_jquar " = " -" ] && _jquar=" 0"
814- printf ' "total_quarantined": %s, ' " $_jquar "
815- if [ " $_ix_elapsed " = " -" ]; then printf ' "elapsed_seconds": null, '
816- else printf ' "elapsed_seconds": %s, ' " $_ix_elapsed " ; fi
817- if [ -z " $_ix_path " ] || [ " $_ix_path " = " -" ]; then
818- printf ' "path": null'
819- else
820- # Out-param form avoids a subshell fork per report
821- _json_escape_var " $_ix_path "
822- printf ' "path": "%s"' " $_JSON_ESC_OUT "
823- fi
824- printf ' }'
809+ # Normalize epoch for sort key and started_epoch field
810+ local _ep=" ${_ix_epoch:- 0} "
811+ [ " $_ep " = " -" ] && _ep=" 0"
812+ {
813+ printf ' %s\t{' " $_ep "
814+ printf ' "scan_id": "%s", ' " $_ix_scanid "
815+ if [ " $_ix_started_hr " = " -" ]; then printf ' "started": null, '
816+ else printf ' "started": "%s", ' " $_ix_started_hr " ; fi
817+ printf ' "started_epoch": %s, ' " $_ep "
818+ if [ " $_ix_tot_files " = " -" ]; then printf ' "total_files": null, '
819+ else printf ' "total_files": %s, ' " $_ix_tot_files " ; fi
820+ if [ " $_ix_tot_hits " = " -" ]; then printf ' "total_hits": null, '
821+ else printf ' "total_hits": %s, ' " $_ix_tot_hits " ; fi
822+ local _jval=" ${_ix_tot_cl:- 0} "
823+ [ " $_jval " = " -" ] && _jval=" 0"
824+ printf ' "total_cleaned": %s, ' " $_jval "
825+ local _jquar=" ${_ix_tot_quar:- 0} "
826+ [ " $_jquar " = " -" ] && _jquar=" 0"
827+ printf ' "total_quarantined": %s, ' " $_jquar "
828+ if [ " $_ix_elapsed " = " -" ]; then printf ' "elapsed_seconds": null, '
829+ else printf ' "elapsed_seconds": %s, ' " $_ix_elapsed " ; fi
830+ if [ -z " $_ix_path " ] || [ " $_ix_path " = " -" ]; then
831+ printf ' "path": null'
832+ else
833+ # Out-param form avoids a subshell fork per report
834+ _json_escape_var " $_ix_path "
835+ printf ' "path": "%s"' " $_JSON_ESC_OUT "
836+ fi
837+ printf ' }\n'
838+ } >> " $_sortbuf "
825839 done < " $_index_file "
826840 fi
827841
@@ -838,31 +852,51 @@ _lmd_render_json_list() {
838852 _parse_session_metadata " $_file "
839853 [ -z " $scanid " ] && continue
840854 _seen_ids[" $scanid " ]=1
841- if [ " $_first " != " 1" ]; then printf " ," ; fi
842- _first=0
843- printf ' \n {'
844- printf ' "scan_id": "%s", ' " $scanid "
845- if [ -z " $scan_start_hr " ]; then printf ' "started": null, '
846- else printf ' "started": "%s", ' " $scan_start_hr " ; fi
847- if [ -z " $tot_files " ]; then printf ' "total_files": null, '
848- else printf ' "total_files": %s, ' " $tot_files " ; fi
849- if [ -z " $tot_hits " ]; then printf ' "total_hits": null, '
850- else printf ' "total_hits": %s, ' " $tot_hits " ; fi
851- local _jval=" ${tot_cl:- 0} "
852- [ -z " $_jval " ] && _jval=" 0"
853- printf ' "total_cleaned": %s, ' " $_jval "
854- printf ' "total_quarantined": null, '
855- if [ -z " $scan_et " ]; then printf ' "elapsed_seconds": null, '
856- else printf ' "elapsed_seconds": %s, ' " $scan_et " ; fi
857- if [ -z " $hrspath " ]; then
858- printf ' "path": null, '
859- else
860- _json_escape_var " $hrspath "
861- printf ' "path": "%s", ' " $_JSON_ESC_OUT "
862- fi
863- printf ' "source": "legacy"'
864- printf ' }'
855+ # Derive epoch from scan_start_hr; fall back to 0 on parse failure
856+ # (matches _session_index_rebuild at lmd_lifecycle.sh:564). On FreeBSD,
857+ # `date -d` is unsupported; the 2>/dev/null || fallback yields 0 and
858+ # these entries sort to the end, preserving prior FreeBSD behavior.
859+ local _leg_ep
860+ _leg_ep=$( command date -d " $scan_start_hr " " +%s" 2> /dev/null) || _leg_ep=0 # safe: date parse failure sorts entry to end
861+ [ -z " $_leg_ep " ] && _leg_ep=0
862+ {
863+ printf ' %s\t{' " $_leg_ep "
864+ printf ' "scan_id": "%s", ' " $scanid "
865+ if [ -z " $scan_start_hr " ]; then printf ' "started": null, '
866+ else printf ' "started": "%s", ' " $scan_start_hr " ; fi
867+ printf ' "started_epoch": %s, ' " $_leg_ep "
868+ if [ -z " $tot_files " ]; then printf ' "total_files": null, '
869+ else printf ' "total_files": %s, ' " $tot_files " ; fi
870+ if [ -z " $tot_hits " ]; then printf ' "total_hits": null, '
871+ else printf ' "total_hits": %s, ' " $tot_hits " ; fi
872+ local _jval=" ${tot_cl:- 0} "
873+ [ -z " $_jval " ] && _jval=" 0"
874+ printf ' "total_cleaned": %s, ' " $_jval "
875+ printf ' "total_quarantined": null, '
876+ if [ -z " $scan_et " ]; then printf ' "elapsed_seconds": null, '
877+ else printf ' "elapsed_seconds": %s, ' " $scan_et " ; fi
878+ if [ -z " $hrspath " ]; then
879+ printf ' "path": null, '
880+ else
881+ _json_escape_var " $hrspath "
882+ printf ' "path": "%s", ' " $_JSON_ESC_OUT "
883+ fi
884+ printf ' "source": "legacy"'
885+ printf ' }\n'
886+ } >> " $_sortbuf "
865887 done
888+
889+ # Drain sorted buffer: newest-first (desc), mirrors text path preference
890+ if [ -s " $_sortbuf " ]; then
891+ local _e _jl
892+ while IFS=$' \t ' read -r _e _jl; do
893+ if [ " $_first " != " 1" ]; then printf " ," ; fi
894+ _first=0
895+ printf ' \n %s' " $_jl "
896+ done < <( command sort -k1 -rn " $_sortbuf " )
897+ fi
898+ command rm -f " $_sortbuf "
899+
866900 printf ' \n ]\n}\n'
867901}
868902
0 commit comments