#!/bin/bash
###############################################################################
# forensic-host.sh — host + MariaDB state capture
#
# TWO MODES
#   ./forensic-host.sh              post-mortem: what happened, what's configured
#   ./forensic-host.sh --live       IN-FLIGHT capture: run this WHILE the box is
#                                   sick. Grabs processlist, memory, process RSS
#                                   FIRST (before anything recovers), then the
#                                   slow stuff. This is the evidence Phase 0 of
#                                   MARIADB_OOM_INVESTIGATION.md has been blocked
#                                   on since 2026-07-10.
#
# Read-only. No config is changed, no service is touched, nothing is killed.
# Degrades gracefully without root (says what it couldn't read).
#
# MySQL auth: tries socket/root (~/.my.cnf) first, then $MYSQL_USER/$MYSQL_PW,
# then the ERP .env claude account. Override: MYSQL_USER=x MYSQL_PW=y ./...
###############################################################################

LIVE=0
OUT=""
ENVFILE="/home/evolution/my.evolutionerp.com.au/.env"

while [ $# -gt 0 ]; do
	case "$1" in
		--live)   LIVE=1; shift ;;
		-o|--out) OUT="$2"; shift 2 ;;
		--env)    ENVFILE="$2"; shift 2 ;;
		-h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
		*) echo "Unknown option: $1" >&2; exit 1 ;;
	esac
done

[ -z "$OUT" ] && OUT="forensic-host_$(date +%Y%m%d-%H%M%S)$([ $LIVE = 1 ] && echo '_LIVE').txt"

exec > "$OUT" 2>&1

hr()  { echo "======================================================================"; }
sec() { echo; hr; echo "$1"; hr; }
have() { command -v "$1" >/dev/null 2>&1; }

echo "EVOLUTION ERP — HOST / MARIADB FORENSICS"
hr
echo "Mode      : $([ $LIVE = 1 ] && echo 'LIVE (in-flight)' || echo 'post-mortem')"
echo "Host      : $(hostname)"
echo "Time      : $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo "Uptime    : $(uptime)"
echo "Running as: $(id -un)"

###############################################################################
# MySQL connection helper
###############################################################################

MYSQL_ARGS=""
mysql_probe() {
	if mysql $1 -N -B -e "SELECT 1" >/dev/null 2>&1; then MYSQL_ARGS="$1"; return 0; fi
	return 1
}

if have mysql; then
	mysql_probe "" \
	|| { [ -n "$MYSQL_USER" ] && mysql_probe "-u$MYSQL_USER -p$MYSQL_PW"; } \
	|| {
		if [ -r "$ENVFILE" ]; then
			EU=$(grep -E '^\s*sqlClaudeUser' "$ENVFILE" | head -1 | cut -d= -f2- | tr -d ' "'"'")
			EP=$(grep -E '^\s*sqlClaudePasswd' "$ENVFILE" | head -1 | cut -d= -f2- | tr -d ' "'"'")
			EH=$(grep -E '^\s*sqlClaudeHost' "$ENVFILE" | head -1 | cut -d= -f2- | tr -d ' "'"'")
			[ -n "$EU" ] && mysql_probe "-h${EH:-localhost} -u$EU -p$EP"
		fi
	}
fi
MQ() { mysql $MYSQL_ARGS -N -B -e "$1" 2>&1; }
MQT() { mysql $MYSQL_ARGS -t -e "$1" 2>&1; }

###############################################################################
# LIVE BLOCK — grab volatile state FIRST, before the box recovers
###############################################################################

if [ $LIVE = 1 ]; then
	sec "L1. MARIADB PROCESSLIST  *** THE MONEY SHOT ***"
	if [ -n "$MYSQL_ARGS" ] || have mysql; then
		echo "-- running threads (not Sleep), longest first:"
		MQT "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO,400) AS QUERY
		     FROM information_schema.PROCESSLIST
		     WHERE COMMAND <> 'Sleep' ORDER BY TIME DESC LIMIT 60"
		echo
		echo "-- thread state summary:"
		MQT "SELECT COMMAND, STATE, COUNT(*) n, MAX(TIME) max_secs
		     FROM information_schema.PROCESSLIST GROUP BY COMMAND, STATE ORDER BY n DESC"
		echo
		echo "-- connections by db:"
		MQT "SELECT DB, COUNT(*) n FROM information_schema.PROCESSLIST GROUP BY DB ORDER BY n DESC"
		echo
		echo "-- table locks held/waiting (MyISAM read/write lock pileup):"
		MQT "SELECT ID, USER, DB, TIME, STATE, LEFT(INFO,200) q
		     FROM information_schema.PROCESSLIST
		     WHERE STATE LIKE '%lock%' OR STATE LIKE '%Waiting for table%' ORDER BY TIME DESC LIMIT 40"
	else
		echo "  mysql client unavailable or auth failed — see section 5."
	fi

	sec "L2. MEMORY RIGHT NOW"
	free -m 2>/dev/null || cat /proc/meminfo | head -20
	echo
	echo "-- swap activity:"
	have vmstat && vmstat 1 3

	sec "L3. TOP PROCESSES BY RSS"
	ps -eo pid,ppid,user,rss,vsz,pcpu,etime,comm,args --sort=-rss 2>/dev/null | head -40 \
		| cut -c1-200

	sec "L4. PROCESS COUNTS BY CLASS"
	for p in mysqld mariadbd php-cgi php-fpm lsphp httpd apache2 wkhtmltopdf wkhtmltoimage; do
		n=$(pgrep -c -f "$p" 2>/dev/null || echo 0)
		[ "$n" -gt 0 ] && printf "  %-14s %4d procs  %8s MB total RSS\n" "$p" "$n" \
			"$(ps -eo rss,args 2>/dev/null | grep -F "$p" | grep -v grep | awk '{s+=$1} END {printf "%.0f", s/1024}')"
	done

	sec "L5. LOAD / IO / APACHE WORKERS"
	cat /proc/loadavg
	echo
	have iostat && iostat -x 1 2 | tail -25
	echo
	if have curl; then
		echo "-- apache server-status (busy workers):"
		curl -s --max-time 5 "http://127.0.0.1/server-status?auto" 2>/dev/null | head -25 \
			|| echo "  server-status not reachable"
	fi

	sec "L6. ESTABLISHED CONNECTION COUNTS"
	(ss -tan 2>/dev/null || netstat -tan 2>/dev/null) | awk '{print $1, $NF}' | sort | uniq -c | sort -nr | head -15
	echo
	echo "-- connections to mysql port:"
	(ss -tan 2>/dev/null || netstat -tan 2>/dev/null) | grep -c ':3306'
fi

###############################################################################
# OOM / crash evidence
###############################################################################

sec "1. OOM KILLER EVENTS"
FOUND=0
for src in "dmesg -T" "journalctl -k --no-pager --since '7 days ago'"; do
	if have "${src%% *}"; then
		o=$(eval "$src" 2>/dev/null | grep -iE 'out of memory|oom-kill|oom_reaper|killed process' | tail -40)
		if [ -n "$o" ]; then echo "-- via ${src%% *}:"; echo "$o"; FOUND=1; echo; fi
	fi
done
for f in /var/log/messages /var/log/syslog /var/log/kern.log; do
	[ -r "$f" ] || continue
	o=$(grep -iE 'out of memory|oom-kill|oom_reaper|killed process' "$f" 2>/dev/null | tail -40)
	if [ -n "$o" ]; then echo "-- via $f:"; echo "$o"; FOUND=1; echo; fi
done
[ $FOUND = 0 ] && echo "  No OOM-kill records found (or logs unreadable without root)."

sec "1b. WHOLE-OS HANG EVIDENCE  (no-SSH outages are NOT a plain MariaDB OOM)"
echo "A MariaDB OOM-kill leaves SSH working — that is the OOM killer's entire job."
echo "If sshd was unreachable too, the whole OS stalled. Discriminators below."
echo
echo "-- kernel hung-task / soft-lockup / stall warnings:"
HK=0
for src in "dmesg -T" "journalctl -k --no-pager --since '3 days ago'"; do
	if have "${src%% *}"; then
		o=$(eval "$src" 2>/dev/null | grep -iE 'hung_task|blocked for more than|soft lockup|rcu_sched|watchdog|BUG: |call trace|task .* blocked' | tail -30)
		[ -n "$o" ] && { echo "$o"; HK=1; }
	fi
done
[ $HK = 0 ] && echo "  none found"
echo
echo "-- storage / IO errors (a stalled disk hangs every process incl. sshd):"
IOE=0
for src in "dmesg -T" "journalctl -k --no-pager --since '3 days ago'"; do
	if have "${src%% *}"; then
		o=$(eval "$src" 2>/dev/null | grep -iE 'I/O error|blk_update_request|task abort|nvme|ata[0-9]+\.[0-9]+|scsi.*error|EXT4-fs error|xfs.*error|reset_controller|timeout.*device' | tail -30)
		[ -n "$o" ] && { echo "$o"; IOE=1; }
	fi
done
[ $IOE = 0 ] && echo "  none found"
echo
echo "-- reboots / unclean shutdowns (did the box actually reset?):"
have last && last -x reboot shutdown 2>/dev/null | head -12
echo "  current uptime: $(uptime -p 2>/dev/null || uptime)"
echo
echo "-- virtualisation / steal time (hypervisor contention starves the whole VM):"
have systemd-detect-virt && echo "  virt: $(systemd-detect-virt 2>/dev/null)"
grep -m1 'model name' /proc/cpuinfo 2>/dev/null | sed 's/^/  /'
echo "  cpus: $(getconf _NPROCESSORS_ONLN 2>/dev/null)"
if have vmstat; then
	echo "  vmstat 1 5 (watch 'st' column — sustained >10 = hypervisor is starving us,"
	echo "              'b' column = procs blocked on IO, 'si/so' = swap thrash):"
	vmstat 1 5 | sed 's/^/    /'
fi
echo
echo "-- fork/PID exhaustion (can't spawn sshd = looks identical to a hang):"
echo "  pids in use : $(ls /proc 2>/dev/null | grep -c '^[0-9]')"
echo "  pid_max     : $(cat /proc/sys/kernel/pid_max 2>/dev/null)"
echo "  threads-max : $(cat /proc/sys/kernel/threads-max 2>/dev/null)"
echo "  nofile limit: $(ulimit -n)"
echo
echo "-- disk full (a full / takes down every service):"
df -h 2>/dev/null | awk 'NR==1 || $5+0 >= 80' | sed 's/^/  /'
df -h 2>/dev/null | awk 'NR>1 && $5+0 >= 80 {n++} END {if (!n) print "  (no filesystem above 80% — not a factor)"}'
echo "  inodes:"
df -i 2>/dev/null | awk 'NR==1 || $5+0 >= 80' | sed 's/^/  /'
df -i 2>/dev/null | awk 'NR>1 && $5+0 >= 80 {n++} END {if (!n) print "  (no filesystem above 80% inode use — not a factor)"}'

sec "1c. SYSSTAT / sar HISTORY  *** RUN THIS FIRST AFTER AN OUTAGE ***"
echo "sar samples every 10 min by default and PERSISTS TO DISK, so it has almost"
echo "certainly already recorded today's outages — retroactively, with no prep."
echo
if have sar; then
	echo "-- memory (kbmemfree / %memused / kbswpused) today:"
	sar -r 2>/dev/null | tail -40
	echo
	echo "-- load & run queue (runq-sz, blocked) today:"
	sar -q 2>/dev/null | tail -40
	echo
	echo "-- cpu (%iowait, %steal) today:"
	sar -u 2>/dev/null | tail -40
	echo
	echo "-- swapping (pswpin/s, pswpout/s) today:"
	sar -W 2>/dev/null | tail -25
	echo
	echo "-- paging / page faults today:"
	sar -B 2>/dev/null | tail -25
	echo
	echo "-- block IO (tps, await) today:"
	sar -d -p 2>/dev/null | tail -30
	echo
	echo "NOTE: gaps in the sar output = the box was too wedged to even sample."
	echo "      The last line BEFORE a gap is the diagnostic one."
	echo
	echo "Yesterday / specific day:  sar -r -f /var/log/sa/sa\$(date -d yesterday +%d)"
	echo "Narrow to a window:        sar -r -s 17:45:00 -e 18:05:00"
else
	echo "  !! sysstat/sar NOT INSTALLED — this is the single cheapest thing you can"
	echo "     add. It would have already answered today's question."
	echo
	echo "     Install (cPanel/CentOS/AlmaLinux):"
	echo "       yum install -y sysstat && systemctl enable --now sysstat"
	echo "     Install (Debian/Ubuntu):"
	echo "       apt-get install -y sysstat"
	echo "       sed -i 's/ENABLED=\"false\"/ENABLED=\"true\"/' /etc/default/sysstat"
	echo "       systemctl enable --now sysstat"
	echo "     Then raise resolution from 10min to 1min:"
	echo "       sed -i 's|5,15,25,35,45,55|*/1|' /etc/cron.d/sysstat   # (EL)"
	echo "       # Debian: edit /etc/cron.d/sysstat, change 5-55/10 to */1"
	echo
	echo "     History then lives in /var/log/sa/ (EL) or /var/log/sysstat/ (Debian)."
fi

sec "2. MARIADB ERROR LOG  (crashes, restarts, aborted connections)"
MYERR=""
for c in /var/lib/mysql/*.err /var/log/mysql/error.log /var/log/mysqld.log \
         /var/log/mariadb/mariadb.log /usr/local/var/mysql/*.err; do
	[ -r "$c" ] && MYERR="$c" && break
done
if [ -n "$MYERR" ]; then
	echo "File: $MYERR"
	echo
	echo "-- startup/crash/recovery markers (last 60):"
	grep -iE 'ready for connections|shutdown|crash|recover|signal|Out of memory|cannot allocate|aborted|too many connections|table is marked as crashed' \
		"$MYERR" 2>/dev/null | tail -60
	echo
	echo "-- last 40 lines:"
	tail -40 "$MYERR"
else
	echo "  MariaDB error log not found/readable. Locate with:"
	echo "    mysql -e \"SHOW VARIABLES LIKE 'log_error'\""
fi

sec "3. MEMORY / RESOURCE BASELINE"
free -m 2>/dev/null
echo
echo "-- MemTotal / swap:"
grep -E 'MemTotal|MemAvailable|SwapTotal|SwapFree|Committed_AS' /proc/meminfo 2>/dev/null
echo
echo "-- top 25 by RSS:"
ps -eo pid,user,rss,pcpu,etime,comm --sort=-rss 2>/dev/null | head -26

sec "4. MARIADB MEMORY CONFIG  (the OOM amplifier)"
if [ -n "$MYSQL_ARGS" ] || have mysql; then
	MQT "SHOW GLOBAL VARIABLES WHERE Variable_name IN (
		'max_connections','thread_cache_size','table_open_cache','open_files_limit',
		'tmp_table_size','max_heap_table_size','sort_buffer_size','join_buffer_size',
		'read_buffer_size','read_rnd_buffer_size','myisam_sort_buffer_size',
		'key_buffer_size','innodb_buffer_pool_size','innodb_log_buffer_size',
		'query_cache_size','max_allowed_packet','net_buffer_length',
		'slow_query_log','slow_query_log_file','long_query_time','log_queries_not_using_indexes',
		'performance_schema','version','default_storage_engine')"
	echo
	echo "-- WORST-CASE MEMORY MATH (per-connection buffers x max_connections + global):"
	MQ "SELECT CONCAT('  key_buffer_size        : ', ROUND(@@key_buffer_size/1048576), ' MB  (MyISAM index cache)')
	    UNION ALL SELECT CONCAT('  innodb_buffer_pool     : ', ROUND(@@innodb_buffer_pool_size/1048576), ' MB')
	    UNION ALL SELECT CONCAT('  innodb_log_buffer      : ', ROUND(@@innodb_log_buffer_size/1048576), ' MB')
	    UNION ALL SELECT CONCAT('  GLOBAL SUBTOTAL        : ', ROUND((@@key_buffer_size + @@innodb_buffer_pool_size + @@innodb_log_buffer_size)/1048576), ' MB  <- resident all the time')
	    UNION ALL SELECT '  --'
	    UNION ALL SELECT CONCAT('  per-connection buffers : ', ROUND((@@sort_buffer_size + @@read_buffer_size + @@read_rnd_buffer_size + @@join_buffer_size + @@max_heap_table_size)/1048576), ' MB  (incl. max_heap_table_size)')
	    UNION ALL SELECT CONCAT('  x max_connections (', @@max_connections, ')  : ', ROUND(((@@sort_buffer_size + @@read_buffer_size + @@read_rnd_buffer_size + @@join_buffer_size + @@max_heap_table_size) * @@max_connections)/1048576), ' MB')
	    UNION ALL SELECT '  --'
	    UNION ALL SELECT CONCAT('  THEORETICAL PEAK       : ', ROUND(((@@key_buffer_size + @@innodb_buffer_pool_size + @@innodb_log_buffer_size) + (@@sort_buffer_size + @@read_buffer_size + @@read_rnd_buffer_size + @@join_buffer_size + @@max_heap_table_size) * @@max_connections)/1048576), ' MB')"
	echo
	MEMTOTAL_MB=$(awk '/^MemTotal:/ {printf "%d", $2/1024}' /proc/meminfo)
	PEAK_MB=$(MQ "SELECT ROUND(((@@key_buffer_size + @@innodb_buffer_pool_size + @@innodb_log_buffer_size) + (@@sort_buffer_size + @@read_buffer_size + @@read_rnd_buffer_size + @@join_buffer_size + @@max_heap_table_size) * @@max_connections)/1048576)" 2>/dev/null | tr -d ' ')
	echo "  PHYSICAL RAM           : ${MEMTOTAL_MB} MB"
	if [ -n "$PEAK_MB" ] && [ "$PEAK_MB" -gt "$MEMTOTAL_MB" ] 2>/dev/null; then
		echo "  ==> OVERCOMMITTED BY CONFIGURATION: peak is $(awk -v p="$PEAK_MB" -v m="$MEMTOTAL_MB" 'BEGIN{printf "%.1f", p/m}')x physical RAM."
		echo "      MariaDB alone can request more than the box has. Any spike from ANY"
		echo "      tenant on this shared host makes mysqld the fattest OOM target."
	fi
	echo
	echo "  ^ compare THEORETICAL PEAK against MemTotal above. If peak > RAM,"
	echo "    the box is one concurrency spike away from an OOM kill by design."
else
	echo "  Could not connect to MariaDB. Try:  mysql -e 'SHOW GLOBAL VARIABLES'"
	echo "  or set MYSQL_USER / MYSQL_PW env vars."
fi

sec "5. MARIADB RUNTIME COUNTERS  (scan + temp-table fingerprint)"
if [ -n "$MYSQL_ARGS" ] || have mysql; then
	MQT "SHOW GLOBAL STATUS WHERE Variable_name IN (
		'Uptime','Threads_connected','Threads_running','Max_used_connections','Connections',
		'Aborted_clients','Aborted_connects','Slow_queries',
		'Created_tmp_tables','Created_tmp_disk_tables','Created_tmp_files',
		'Handler_read_rnd_next','Handler_read_first','Handler_read_key',
		'Select_scan','Select_full_join','Sort_merge_passes','Sort_scan',
		'Table_locks_immediate','Table_locks_waited',
		'Key_reads','Key_read_requests','Opened_tables','Open_tables',
		'Innodb_buffer_pool_reads','Innodb_buffer_pool_read_requests')"
	echo
	echo "-- derived:"
	MQ "SELECT CONCAT('  disk tmp table ratio  : ', ROUND(100*
			(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Created_tmp_disk_tables') /
			GREATEST(1,(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Created_tmp_tables')),1), '%   (>25% = queries are spilling to disk)')
	    UNION ALL SELECT CONCAT('  table lock wait ratio : ', ROUND(100*
			(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Table_locks_waited') /
			GREATEST(1,(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Table_locks_immediate')),2), '%   (MyISAM read/write lock pileup)')
	    UNION ALL SELECT CONCAT('  full-scan rows/sec    : ', ROUND(
			(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Handler_read_rnd_next') /
			GREATEST(1,(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Uptime'))), '   (sequential row reads = missing indexes)')" 2>/dev/null
fi

sec "6. SLOW QUERY LOG  (Phase 0 blocker)"
if [ -n "$MYSQL_ARGS" ] || have mysql; then
	SQL_ON=$(MQ "SELECT @@slow_query_log")
	SQL_F=$(MQ "SELECT @@slow_query_log_file")
	SQL_T=$(MQ "SELECT @@long_query_time")
	echo "  slow_query_log      : $SQL_ON"
	echo "  slow_query_log_file : $SQL_F"
	echo "  long_query_time     : $SQL_T"
	echo
	if [ "$SQL_ON" = "1" ] && [ -r "$SQL_F" ]; then
		echo "  size: $(du -h "$SQL_F" 2>/dev/null | cut -f1)   entries: $(grep -c '^# Time:' "$SQL_F" 2>/dev/null)"
		echo
		if have pt-query-digest; then
			echo "-- pt-query-digest (top 15):"
			pt-query-digest --limit 15 "$SQL_F" 2>/dev/null | head -120
		else
			echo "-- slowest 20 individual queries (crude; install percona-toolkit for a real digest):"
			awk '/^# Query_time:/ {qt=$3; getline; getline; q=""; while ((getline line) > 0) {
					if (line ~ /^# (Time|User)/) break; q = q " " line }
				printf "%.2f\t%s\n", qt, substr(q,1,220) }' "$SQL_F" 2>/dev/null \
				| sort -rn | head -20 | awk -F'\t' '{printf "  %8.2fs  %s\n", $1, $2}'
			echo
			echo "-- most frequent slow query shapes:"
			grep -vE '^#|^SET timestamp|^use ' "$SQL_F" 2>/dev/null \
				| sed -E "s/[0-9]+/N/g; s/'[^']*'/'S'/g" | cut -c1-160 \
				| sort | uniq -c | sort -nr | head -20 | awk '{printf "  %5d  %s\n", $1, substr($0, index($0,$2))}'
		fi
	else
		echo "  !! NOT ENABLED (or unreadable). This is why we still cannot confirm H1/H2/H3."
		echo "     Enable at runtime, no restart needed:"
		echo "       SET GLOBAL slow_query_log = 1;"
		echo "       SET GLOBAL long_query_time = 2;"
		echo "       SET GLOBAL log_queries_not_using_indexes = 1;"
		echo "     (add to my.cnf to survive restart; watch disk with log_queries_not_using_indexes on)"
	fi
fi

sec "7. STORAGE ENGINE / TABLE SIZE AUDIT  (top 25 tables, all tenants)"
if [ -n "$MYSQL_ARGS" ] || have mysql; then
	MQT "SELECT TABLE_SCHEMA db, TABLE_NAME tbl, ENGINE,
	            ROUND(DATA_LENGTH/1048576,1) data_mb,
	            ROUND(INDEX_LENGTH/1048576,1) idx_mb,
	            TABLE_ROWS rows_est
	     FROM information_schema.TABLES
	     WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys')
	     ORDER BY DATA_LENGTH DESC LIMIT 25"
	echo
	echo "-- engine mix:"
	MQT "SELECT ENGINE, COUNT(*) tables, ROUND(SUM(DATA_LENGTH)/1048576) data_mb
	     FROM information_schema.TABLES
	     WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys')
	     GROUP BY ENGINE ORDER BY data_mb DESC"
fi

sec "8. SCHEDULER / CRON INVENTORY  (Phase 0 blocker)"
echo "-- system crontab:"
[ -r /etc/crontab ] && grep -vE '^\s*#|^\s*$' /etc/crontab || echo "  unreadable"
echo
echo "-- /etc/cron.d:"
[ -d /etc/cron.d ] && grep -rhvE '^\s*#|^\s*$' /etc/cron.d/ 2>/dev/null | head -40 || echo "  none"
echo
echo "-- per-user crontabs:"
if [ -d /var/spool/cron ]; then
	for f in /var/spool/cron/* /var/spool/cron/crontabs/*; do
		[ -r "$f" ] || continue
		echo "  [$(basename "$f")]"
		grep -vE '^\s*#|^\s*$' "$f" 2>/dev/null | sed 's/^/    /'
	done
else
	echo "  /var/spool/cron unreadable (need root)"
fi
echo
echo "-- systemd timers:"
have systemctl && systemctl list-timers --all --no-pager 2>/dev/null | head -20
echo
echo "NOTE: MARIADB_OOM_INVESTIGATION.md records the production scheduler as"
echo "      living OFF-BOX. If the above is empty, the external scheduler's job"
echo "      list is still an outstanding Phase 0 item."

sec "9. APACHE / PHP LIMITS"
have httpd && httpd -V 2>/dev/null | grep -E 'MPM|Server version'
have apache2ctl && apache2ctl -V 2>/dev/null | grep -E 'MPM|Server version'
echo
grep -rhE '^\s*(MaxRequestWorkers|MaxClients|ServerLimit|ThreadsPerChild|MaxConnectionsPerChild|Timeout)' \
	/etc/apache2/conf.d/ /etc/apache2/conf/ /usr/local/apache/conf/ 2>/dev/null | head -20
echo
echo "-- PHP limits:"
have php && php -r 'echo "  memory_limit      : ".ini_get("memory_limit")."\n  max_execution_time: ".ini_get("max_execution_time")."\n  post_max_size     : ".ini_get("post_max_size")."\n";' 2>/dev/null
echo
echo "-- disk:"
df -h 2>/dev/null | grep -vE '^tmpfs|^devtmpfs'

echo
hr
echo "Report written to: $OUT"
[ $LIVE = 1 ] && echo "LIVE capture — send this to Claude alongside the forensic-web.sh digest."
hr
