Skip to content

Commit 2a32ce5

Browse files
committed
[Fix] --json-report list: merge-sort reports[] + add started_epoch; issue #483
[Fix] _lmd_render_json_list() reports[]: buffer both TSV-index and legacy-session passes into a sort buffer and emit in started_epoch-descending order (newest first). Pre-fix code emitted index entries in append-order followed by legacy entries, producing a misordered reports[] array (e.g. Mar 29 legacy after Apr 18 TSV). Mirrors the text-path merge-sort in lmd_session.sh. [New] _lmd_render_json_list() reports[]: additive started_epoch integer field alongside the existing started string. Pass 1 uses session.index field 2 (already stored); pass 2 derives via command date -d with fallback to 0, matching _session_index_rebuild at lmd_lifecycle.sh. No schema break -- existing consumers parsing started remain unaffected. [Change] CHANGELOG + CHANGELOG.RELEASE: append v2.0.1 Bug Fixes entries for both fixes (issue #483 joins the existing section alongside #480/#482).
1 parent ef3b4b4 commit 2a32ce5

3 files changed

Lines changed: 101 additions & 51 deletions

File tree

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ v2.0.1 | Mar 25 2026:
326326
[Fix] RPM/DEB %pre: defensive detection for dir-vs-symlink cpio conflict; install
327327
no longer fails with "rename failed - Is a directory" when prior install.sh
328328
or partial install left real directories at symlink-target paths
329+
[Fix] --json-report list: reports[] now globally sorted by started_epoch
330+
(newest first) across TSV index + legacy-session passes; pre-fix
331+
code emitted index entries in append-order then legacy entries,
332+
producing visible date-order inversions (e.g. Mar 29 legacy after
333+
Apr 18 TSV); issue #483
334+
[New] --json-report list: reports[] entries include started_epoch integer
335+
field for machine-readable absolute timestamps; additive schema
336+
change (existing "started" string preserved); issue #483
329337

330338
v1.6.6.1 | Feb 25 2025:
331339
[Fix] find_recentopts incorrectly escaping find options to the right of ( -mtime .. -ctime ); previously normalized by eval; issue #440, pr#442

CHANGELOG.RELEASE

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,11 @@ v2.0.1 | Mar 25 2026:
326326
[Fix] RPM/DEB %pre: defensive detection for dir-vs-symlink cpio conflict; install
327327
no longer fails with "rename failed - Is a directory" when prior install.sh
328328
or partial install left real directories at symlink-target paths
329+
[Fix] --json-report list: reports[] now globally sorted by started_epoch
330+
(newest first) across TSV index + legacy-session passes; pre-fix
331+
code emitted index entries in append-order then legacy entries,
332+
producing visible date-order inversions (e.g. Mar 29 legacy after
333+
Apr 18 TSV); issue #483
334+
[New] --json-report list: reports[] entries include started_epoch integer
335+
field for machine-readable absolute timestamps; additive schema
336+
change (existing "started" string preserved); issue #483

files/internals/lmd_alert.sh

Lines changed: 85 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)