#!/bin/sh
### sysreport.sh  -*- Sh -*-
## Produce a system report in XHTML.

### Ivan Shmakov, 2022, 2026

## To the extent possible under law, the author(s) have dedicated
## all copyright and related and neighboring rights to this software
## to the public domain worldwide.  This software is distributed
## without any warranty.

## You should have received a copy of the CC0 Public Domain Dedication
## along with this software.  If not, see
## <http://creativecommons.org/publicdomain/zero/1.0/>.

### History:

## 0.3  2026-07-16
##      Use s- prefix (was: s.) for XHTML section identifiers for
##      better CSS 2.1 compatibility.
##      (usage): Fixed: use ntp_drift in the example (was: non-existent
##      ntp-drift.)  Updated.
##      (sysreport_df_rw): Use awk (was: gawk), as /proc/mounts
##      device names cannot contain newlines and so do not need
##      to be \0-separated (except for xargs.)
##      (envstat): New function.
##      (netstat_i): Likewise.
##      (netstat_ibd): Likewise.
##      (sysreport_ntp_drift): Look for the file in several alternative
##      locations.
##      (sysreport_sensors): Remove Miscellaneous from section title.

## 0.2  2022-05-01 06:57:13Z
##      (sfn.h1NNHNAerNdEcRO9LIC9esrFIRokAWh6dNTXuXsuFjE.sh)
##      New file:FILENAME, free and uptime options, and a leading
##      blank argument to specify the heading for the immediately
##      succeeding section.
##      (usage): Changed accordingly.
##      (heading): New global variable.
##      (xhtml_rep_header): Use heading, if any, instead of the second
##      positional argument.
##      (xhtml_simple_cmd): New function; report non-zero exit status.
##      (sysreport_apcaccess, sysreport_ntpq_pn): Use xhtml_simple_cmd.
##      (sysreport_ip_s_link, sysreport_sensors): Likewise.
##      (file_counter): New global variable.
##      (sysreport_file): New function.
##      (sysreport_free): New function, based on xhtml_simple_cmd.
##      (sysreport_uptime): Likewise.

## 0.1  2022-04-10 21:15:59Z
##      (sfn.GhzqKy8cI8t2Mcqz_AsQ87vH6Wp91VEzF0bIqHPHZXo.sh)
##      Initial revision.

### Code:

set -e
set -C -u

## For tests:
# set -- apcaccess df_rw df_tmpfs " Swap usage" file:/proc/swaps free hddtemp ip_s_link ntp_drift ntpq_pn sensors uptime
# set -- uptime " Swap usage" file:/proc/swaps free

usage () {
    if [ "$1" != 0 ] ; then
        exec >&2
    fi
    cat <<EOF
Usage:
  \$ ${0} [--] \
        {" "TITLE|apcaccess|df_rw|df_tmpfs|envstat|file:FILENAME|free
         |hddtemp|ip_s_link|netstat_i|netstat_ibd
         |ntp_drift|ntpq_pn|sensors|uptime}... 
  \$ ${0} {--help|-h} 

EOF

    ## .
    test "$1" != 0 \
        && exit "$1"

cat <<EOF
Example:
  \$ ${0} -- ntp_drift | lynx --dump --stdin 
                    System report for 2022-04-10

  NTP drift file

     Started: 2022-04-10 20:50:55 UTC
  58.472

     Finished: 2022-04-10 20:50:55 UTC
  \$ 

Environment:

  SYSREPORT_CSS     href= value for <link rel="stylesheet" />;
  SYSREPORT_TITLE   the contents of <title /> and <h1 />.

EOF

    ## .
    exit "$1"
}

## GNU error function look-alike (for possible future i18n)
gerr () {
    gerr_exit=${1}
    shift
    printf "$@" >&2

    ## .
    test "$gerr_exit" = 0 \
        || exit "$gerr_exit"
}

xhtml_escape () {
    sed -e "s/&/\\&amp;/g; s/</\\&lt;/g;"
}

css=$(  printf %s\\n "${SYSREPORT_CSS:-default.css}" \
            | xhtml_escape)
title=$(printf %s\\n "${SYSREPORT_TITLE:-System report for $(date -u +%F)}" \
            | xhtml_escape)

xhtml_header () {
cat <<EOF
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title >${title}</title>
<link rel="stylesheet" href="${SYSREPORT_CSS:-default.css}" />
<!-- FIXME: should be superseded by @viewport in .css -->
<meta name="viewport" content="initial-scale=1.0" />
</head>
<body>

<article class="h-entry">
<h1 class="p-name" >${title}</h1>
EOF
}

xhtml_footer () {
cat <<EOF
</article>
</body>
</html>
EOF
}

heading=
xhtml_rep_header () {
    local x h
    if test "${1:-XXX}" = "${1#*[!.0-9a-zA-Z-]}" ; then
        x=" id=\"${1}\""
    else
        x=
    fi
    if test -n "$heading" ; then
        h=$(printf %s\\n "$heading" | xhtml_escape)
    fi

cat <<EOF

<section${x}>
<header>
<h2 >${h:-${2}}</h2>
</header>

<p >Started: <time >$(date -u +%F\ %T\ UTC | xhtml_escape)</time></p>

<pre
EOF
    ## .
    printf \>
}

xhtml_rep_footer () {
cat <<EOF
</pre>

<p >Finished: <time >$(date -u +%F\ %T\ UTC | xhtml_escape)</time></p>

</section>
EOF
}

xhtml_simple_cmd () {
    xhtml_rep_header "$1" "$2"
    shift 2
    local x
    x=
    ("$@" | xhtml_escape) || x=$?
    xhtml_rep_footer
    if test -n "$x" ; then
        cat <<EOF
<p >Command terminated with non-zero exit status: $?</p>
EOF
    fi
}

sysreport_apcaccess () {
    xhtml_simple_cmd s-apcaccess "APC UPS daemon status" \
        /sbin/apcaccess
}

sysreport_df_rw () {
    xhtml_rep_header s-df-rw "Disk usage, writable, device-backed filesystems"
    awk '! seen_p[$1] && /^\/dev/ && $4 ~ /(^|[ \t,])?rw([ \t,]|$)/ {
             print $1; seen_p[$1] = 1; }' \
        < /proc/mounts \
        | tr \\n \\0 | xargs -r0 -- df -- \
        | xhtml_escape
    ## .
    xhtml_rep_footer
}

sysreport_df_tmpfs () {
    xhtml_rep_header s-df-tmpfs "Disk usage, in-memory filesystems"
    ## FIXME: handle mfs as well?
    df -t tmpfs \
        | sed -e "1d; / 0 /d;" \
        | LC_ALL=C sort -srnk3 \
        | xhtml_escape
    ## .
    xhtml_rep_footer
}

file_counter=0
sysreport_file () {
    xhtml_rep_header s-file-${file_counter} \
        "Contents of <code >$(printf "$1" | xhtml_escape)</code>"
    file_counter=$((1 + file_counter))
    xhtml_escape < "$1"
    ## .
    xhtml_rep_footer
}

sysreport_envstat () {
    xhtml_simple_cmd s-envstat "Sensor readings" \
        envstat
}

sysreport_free () {
    xhtml_simple_cmd s-free "System memory usage" \
        free
}

sysreport_hddtemp () {
    xhtml_rep_header s-hddtemp "Storage devices temperature"
    ## FIXME: GNU Sed for now
    socat  -u  TCP4:localhost:7634  STDOUT \
        | head -c1024 \
        | cat -v \
        | sed -e "s/\$/\\n/; s/||/|\\n|/g" \
        | xhtml_escape
    xhtml_rep_footer
}

sysreport_netstat_i () {
    xhtml_simple_cmd s-netstat-i "Network interface packet counters" \
        netstat -i
}

sysreport_netstat_ibd () {
    xhtml_simple_cmd s-netstat-ibd "Network interface byte counters" \
        netstat -ibd
}

sysreport_ntp_drift () {
    xhtml_rep_header s-ntp-drift "NTP drift file"
    local f
    for f in \
        /var/lib/openntpd/db/ntp.drift /var/db/ntp.drift \
        /var/lib/ntpsec/ntp.drift /var/lib/ntp/ntp.drift ; do
        test -e "$f" && break
    done
    head -c 64 < "$f" \
        | xhtml_escape
    ## .
    xhtml_rep_footer
}

sysreport_ntpq_pn () {
    ## .
    xhtml_simple_cmd s-ntpq-pn "NTP peers, numeric IP addresses" \
        ntpq -pn
}

sysreport_ip_s_link () {
    xhtml_simple_cmd s-ip_s_link "IP links counters" \
        ip -s link
}

sysreport_sensors () {
    xhtml_simple_cmd s-sensors "Sensor readings" \
        sensors
}

sysreport_uptime () {
    xhtml_simple_cmd s-uptime "System uptime" \
        uptime
}

## FIXME: use getopt or such for command-line arguments parsing?

overplay=
while [ "$#" -ge 1 ] ; do
    case "$1" in
        (-h | --help)   usage 0 ;;
        (--)    shift ; break ;;
        (-*)    usage 1 ;;
        (*)     break ;;
    esac
    shift
done

if ! [ "$#" -ge 1 ] ; then
    ## .
    usage 1
fi

shown_header_p=

for sec ; do
    com_arg=
    case "$sec" in
        (" "*) heading=${sec# } ; continue ;;
        (file:*)
            com=sysreport_${sec%%:*} ; com_arg=${sec#*:} ;;
        (*) com=sysreport_${sec} ;;
    esac
    if ! command -v -- "$com" > /dev/null ; then
        gerr 0 "Warning: %s: Unknown section; ignored" "$sec"
        continue
    fi
    if test -z "$shown_header_p" ; then
        shown_header_p=yes
        xhtml_header
    fi
    ## FIXME: only allowing a single argument for now
    "$com" "$com_arg"
    heading=
done

if test -n "$shown_header_p" ; then
    xhtml_footer
fi

### sysreport.sh ends here
