#!/bin/sh
# Rapporte a ISACore ce qui est REELLEMENT installe sur cet appareil.
#
# POURQUOI CE SERVICE EXISTE :
# le cloud ne connaissait du parc que la version de l'algo. Et encore : l'algo lit ses
# versions UNE FOIS au lancement de son process, et ne les renvoie qu'au demarrage, a la
# reconnexion ou sur changement d'IP. Un `opkg upgrade` sans redemarrage de l'algo restait
# donc invisible indefiniment. « Quels appareils n'ont pas ha-roamd a jour » n'avait pas
# d'autre reponse que de s'y connecter un par un — vecu le 2026-09-24, ou trois capteurs
# sont partis chez un client avec un paquet vieux d'une semaine sans que rien ne le dise.
#
# CE QU'IL N'EST PAS : un agent de deploiement. Il LIT et il rapporte, il n'installe rien
# et ne modifie rien. C'est ce qui lui permet d'etre le temoin independant de ce que font
# les autres — un inventaire qui se remplirait des commandes qu'on a emises ne saurait
# plus dire lesquelles ont echoue.
#
# CAPTEURS ET KIOSQUES : rien ici ne suppose un Persee. Ce qui manque est absent du
# rapport, pas remplace par une valeur par defaut. Une case vide se lit « pas mesure » ;
# une case fausse se lit comme un fait.

TAG="isa-softinvd"

# --- reglages, surchargeables pour les tests puis par /etc/default -----------------
APIS="${APIS:-https://isacore-fr.mintt.cloud}"
STATE_DIR="${STATE_DIR:-/var/lib/isa-softinvd}"
# LA CONFIGURATION N'EST PAS AU MEME ENDROIT PARTOUT. Sur un capteur, le paquet opkg
# s'installe sous `dest oem` et la pose donc dans /opt/oem/etc. Sur un kiosque MyISA il
# n'y a pas de /opt/oem du tout : c'est un Ubuntu standard, le .deb la pose dans /etc.
# On prend le premier chemin lisible, et a defaut celui du capteur — de loin le cas le
# plus frequent, et c'est son absence qu'on veut voir citee dans le journal.
CONF_CANDIDATES="${CONF_CANDIDATES:-/opt/oem/etc/isa-softinvd.conf /etc/isa-softinvd.conf}"
if [ -z "${CONF:-}" ]; then
    for _c in $CONF_CANDIDATES; do
        [ -r "$_c" ] && { CONF="$_c"; break; }
    done
    # Aucun lisible : on retient le premier candidat, pour que le journal cite un chemin
    # plutot qu'une chaine vide quand il dira qu'il n'a pas d'identifiants.
    CONF="${CONF:-${CONF_CANDIDATES%% *}}"
fi
OPKG="${OPKG:-/opt/oem/usr/bin/opkg}"
OPKG_CONF="${OPKG_CONF:-/opt/oem/etc/opkg/opkg.conf}"
PERSEE_RELEASE="${PERSEE_RELEASE:-/etc/persee-release}"
# Surchargeables pour les tests : on rejoue des appareils entiers sans toucher au script.
MINTT_SERIAL="${MINTT_SERIAL:-/root/.mintt_serial}"
SERIAL_FILES="${SERIAL_FILES:-/opt/oem/etc/serial /root/serial /etc/serial}"
DMI_DIR="${DMI_DIR:-/sys/class/dmi/id}"
OS_RELEASE="${OS_RELEASE:-/etc/os-release}"
TIMEOUT="${TIMEOUT:-20}"
# Les identifiants que l'appareil detient DEJA : la reponse du discover, chiffree pour la
# cle RSA commune du parc. ISA2 les lit pour parler a l'API ; on s'en sert pour signer le
# rapport. ha_cloud_isacore porte en principe ISACore et ha_cloud OpenAPI, mais selon la
# version de l'algo et l'etat de migration ce n'est pas garanti : on essaie les deux, et
# c'est le serveur qui dit lequel il reconnait.
# Ce que le site du client laisse passer, mesure DEPUIS le capteur : prealable a la
# migration OpenAPI -> ISACore (le filtrage est chez le client), et a la fermeture de su
# et mirror. nom=url ; une requete HEAD par cible, sans identifiant : un 401 prouve que
# la cible est joignable, ce qui est la seule question posee ici.
NET_TARGETS="${NET_TARGETS-isacore=https://isacore-fr.mintt.cloud/alive isacore_opkg=https://isacore-fr.mintt.cloud/su/opkg/master-mt/Packages.gz openapi=https://openapi.mintt.cloud/ su=https://su.mintt.cloud/master-mt/Packages.gz mirror=https://mirror.mintt.cloud/}"
NET_TIMEOUT="${NET_TIMEOUT:-8}"
APT_DIR="${APT_DIR:-/etc/apt}"
SYSDATA="${SYSDATA:-/etc/sysdata}"
CLOUD_KEY="${CLOUD_KEY:-/opt/oem/etc/certs/apicred.crt}"

# Les paquets apt qui comptent pour le parc. PAS tout `dpkg -l` : le reste est du
# userland Ubuntu standard, sans interet ici, et son volume noierait le rapport.
APT_WATCH="${APT_WATCH:-network-manager wpasupplicant systemd linux-firmware openssh-server curl avahi-daemon cron}"

# ... PLUS tout ce qui est a nous, quel que soit son nom. Sur un capteur les paquets
# maison sont en opkg ; sur un kiosque ils sont en dpkg — releve le 2026-09-24 sur
# MSACD3L1S0102674 : myisa-b2b-kiosk, myisa-package-manager, ha-robin-kiosk,
# ha-robindesk, mintt-isacore-mirror. Les enumerer un par un condamnerait a modifier ce
# script a chaque nouveau paquet ; le motif les prend tous, y compris ceux qui n'existent
# pas encore.
APT_WATCH_PATTERN="${APT_WATCH_PATTERN:-^(ha-|myisa-|mintt-|isa-)}"

[ -r /etc/default/isa-softinvd ] && . /etc/default/isa-softinvd

log() { echo "$*"; }

# --- lecture de la configuration ----------------------------------------------------
# Meme forme que RobinDesk (cle=valeur, commentaires en #) : un fichier depose sur
# l'appareil, et non un secret compile. C'est ce qui permet de tourner un jeton sans
# reconstruire le paquet — et, le jour ou l'on saura en poser un par appareil, de le
# faire sans toucher a ce script.
conf_value() {
    key="$1"; def="$2"
    [ -r "$CONF" ] || { echo "$def"; return; }
    v=$(sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//p" "$CONF" | head -1 \
        | sed 's/[[:space:]]*$//')
    [ -n "$v" ] && echo "$v" || echo "$def"
}

# --- collecte -------------------------------------------------------------------------

# Le numero de serie identifie l'appareil aupres du cloud. On le cherche la ou chaque
# image le pose ; l'ordre va du plus specifique au plus general.
read_serial() {
    # 1. LE KIOSQUE A DEJA TRANCHE, ON NE RETRANCHE PAS. start-myisa-kiosk.sh mene une
    #    cascade a cinq niveaux (override, dmidecode systeme, dmidecode carte mere,
    #    /tmp/hw, lshw) et ECRIT son verdict dans /root/.mintt_serial. Refaire la
    #    detection de notre cote, c'est risquer que deux composants de la meme machine
    #    se declarent sous deux identites — et ignorer /root/.mintt_serial_override, qui
    #    existe precisement pour corriger a la main les cas ou le materiel ment.
    if [ -r "$MINTT_SERIAL" ]; then
        v=$(head -1 "$MINTT_SERIAL" | tr -d ' \t\r\n')
        serial_plausible "$v" && { echo "$v"; return; }
    fi

    # 2. Les capteurs, eux, posent leur serial au provisionnement.
    for f in $SERIAL_FILES; do
        [ -r "$f" ] || continue
        v=$(head -1 "$f" | tr -d ' \t\r\n')
        serial_plausible "$v" && { echo "$v"; return; }
    done

    # 3. Repli DMI, dans l'ORDRE de MyISA : systeme d'abord, carte mere ensuite.
    #    Mesure le 2026-09-24 sur le kiosque de production 77737884840007 :
    #    product_serial rend « To be filled by O.E.M. » et c'est board_serial qui porte
    #    le vrai numero. Prendre le premier venu aurait declare 35 kiosques sous un
    #    numero de serie qui est une phrase d'excuse du fabricant.
    for f in "$DMI_DIR/product_serial" "$DMI_DIR/board_serial"; do
        [ -r "$f" ] || continue
        v=$(tr -d ' \t\r\n' < "$f")
        serial_plausible "$v" && { echo "$v"; return; }
    done

    # 4. Dernier repli : sur un capteur provisionne le nom d'hote porte le serial. Sur un
    #    kiosque il vaut « myisa-kiosk » et n'identifie rien — d'ou l'ordre de cette liste.
    hostname 2>/dev/null | tr -d ' \t\r\n'
}

# Les cartes non serialisees ne rendent pas une chaine vide, elles rendent une PHRASE, et
# pas toujours avec la meme casse : « To be filled by O.E.M. » sur le materiel de
# production, « To Be Filled By O.E.M. » dans la documentation. On compare donc en
# minuscules — une comparaison sensible a la casse laisserait passer exactement le cas
# qu'on veut ecarter.
serial_plausible() {
    [ -n "$1" ] || return 1
    case "$(printf '%s' "$1" | tr 'A-Z' 'a-z')" in
        *"to be filled"*|*"not specified"*|*"not available"*|*"default string"*|\
        *"system serial number"*|*"o.e.m."*|"none"|"0"|"null"|"unknown") return 1 ;;
    esac
    # Un numero de serie utile fait plus de quelques caracteres : `lshw` de MyISA ecarte
    # d'ailleurs tout ce qui fait 8 caracteres ou moins.
    [ "${#1}" -gt 4 ] || return 1
    return 0
}

# `opkg list-installed` rend « paquet - version ». On en fait un objet JSON.
# Silencieux et sans erreur si opkg n'existe pas : c'est le cas d'un kiosque.
collect_opkg() {
    [ -x "$OPKG" ] && [ -r "$OPKG_CONF" ] || return 1
    "$OPKG" --conf "$OPKG_CONF" list-installed 2>/dev/null | awk '
        BEGIN { printf "{" ; sep="" }
        NF >= 3 && $2 == "-" { gsub(/"/,"",$1); gsub(/"/,"",$3);
                               printf "%s\"%s\":\"%s\"", sep, $1, $3; sep="," }
        END { printf "}" }'
}

collect_apt() {
    command -v dpkg-query >/dev/null 2>&1 || return 1
    # Une seule interrogation de dpkg, filtree ensuite : la liste blanche pour le
    # systeme, le motif pour tout ce qui est a nous. Trier rend le rapport stable d'un
    # passage a l'autre — sans cela l'empreinte changerait au gre de l'ordre de dpkg et
    # l'on croirait a un changement d'inventaire a chaque reveil.
    dpkg-query -W -f='${db:Status-Abbrev}|${Package}|${Version}\n' 2>/dev/null \
    | awk -F'|' -v liste="$APT_WATCH" -v motif="$APT_WATCH_PATTERN" '
        BEGIN { n=split(liste, a, " "); for (i=1;i<=n;i++) garde[a[i]]=1 }
        $1 ~ /^ii/ && ($2 in garde || $2 ~ motif) { print $2 "|" $3 }
      ' \
    | sort \
    | awk -F'|' '
        BEGIN { printf "{"; sep="" }
        { gsub(/"/,"",$1); gsub(/"/,"",$2); printf "%s\"%s\":\"%s\"", sep, $1, $2; sep="," }
        END { printf "}" }'
}

# La joignabilite de chaque cible : le code de sortie de curl distingue les causes
# (6 = DNS, 7 = connexion refusee, 28 = delai, 35/60 = TLS), le code HTTP dit que la cible
# a repondu, la latence situe la liaison. AUTONOME a dessein : l'audit des appareils qui
# ne rapportent pas (isa-softinvd absent, ou ISACore bloque) envoie cette meme fonction
# par Robin, pour mesurer la meme chose de la meme facon.
collect_net() {
    command -v curl >/dev/null 2>&1 || return 1
    printf '{'; _sep=''
    for _t in $NET_TARGETS; do
        _n=${_t%%=*}; _u=${_t#*=}
        _o=$(curl -sS -I -o /dev/null -m "$NET_TIMEOUT" -w '%{http_code} %{time_total}' "$_u" 2>/dev/null); _rc=$?
        _h=${_o%% *}; _s=${_o#* }
        [ "$_s" = "$_o" ] && _s=0
        _ms=$(awk -v s="$_s" 'BEGIN{printf "%d", s*1000}')
        printf '%s"%s":{"exit":%d,"http":%d,"ms":%d}' "$_sep" "$_n" "$_rc" "$(( ${_h:-0} + 0 ))" "${_ms:-0}"
        _sep=','
    done
    printf '}'
}

# Par ou passent les paquets : les depots d'opkg.conf et leur authentification
# (http_auth = par le proxy ISACore, mtls = en direct sur su par certificat client, none
# = en anonyme), et les sources apt. C'est ce qu'il faudra reconfigurer avant de fermer
# su et mirror, et ce qui prouvera qu'on peut les fermer.
collect_feeds() {
    printf '{'; _sep=''
    if [ -r "$OPKG_CONF" ]; then
        _auth=none
        grep -q '^[[:space:]]*option[[:space:]][[:space:]]*http_auth' "$OPKG_CONF" && _auth=http_auth
        if grep -q '^[[:space:]]*option[[:space:]][[:space:]]*ssl_cert' "$OPKG_CONF"; then
            [ "$_auth" = none ] && _auth=mtls || _auth="$_auth+mtls"
        fi
        printf '"opkgAuth":"%s","opkg":[' "$_auth"; _s2=''
        for _u in $(sed -n 's/^[[:space:]]*src[^[:space:]]*[[:space:]][[:space:]]*[^[:space:]][^[:space:]]*[[:space:]][[:space:]]*\([^[:space:]]*\).*/\1/p' "$OPKG_CONF" | sort -u); do
            printf '%s"%s"' "$_s2" "$(json_escape "$_u")"; _s2=','
        done
        printf ']'; _sep=','
    fi
    _apt=$(cat "$APT_DIR/sources.list" "$APT_DIR"/sources.list.d/*.list 2>/dev/null \
             | sed -n 's/^[[:space:]]*deb\(-src\)\{0,1\}[[:space:]][[:space:]]*\(\[[^]]*\][[:space:]]*\)\{0,1\}\([^[:space:]]*\).*/\3/p'
           cat "$APT_DIR"/sources.list.d/*.sources 2>/dev/null | sed -n 's/^URIs:[[:space:]]*//p' | tr ' ' '\n')
    # Seulement de vraies URL : une ligne `deb cdrom:[Ubuntu 22.04 …]/` porte des espaces
    # et se debiterait en morceaux.
    _apt=$(printf '%s\n' "$_apt" | tr ' ' '\n' | grep -E '^[a-z][a-z0-9+.-]*://' | sort -u)
    if [ -n "$_apt" ]; then
        printf '%s"apt":[' "$_sep"; _s2=''
        for _u in $_apt; do printf '%s"%s"' "$_s2" "$(json_escape "$_u")"; _s2=','; done
        printf ']'
    fi
    printf '}'
}

# /etc/persee-release est sourcable (PERSEE_VERSION=..., une cle par ligne). Il n'existe
# que sur l'image 6.6 : son absence n'est pas une anomalie, c'est un 3.10 ou un kiosque.
read_release_key() {
    [ -r "$PERSEE_RELEASE" ] || return 1
    sed -n "s/^$1=//p" "$PERSEE_RELEASE" | head -1 | tr -d '"'
}

read_os_key() {
    [ -r "$OS_RELEASE" ] || return 1
    sed -n "s/^$1=//p" "$OS_RELEASE" | head -1 | tr -d '"'
}

# La version du firmware wifi n'est exposee nulle part proprement : le pilote l'annonce
# une fois, au chargement, dans le tampon du noyau. Elle disparait donc du journal quand
# le tampon tourne — on rend une chaine vide, jamais une valeur inventee.
collect_wifi_firmware() {
    command -v dmesg >/dev/null 2>&1 || return 1
    dmesg 2>/dev/null | sed -n 's/.*Firmware: \(BCM[^ ]*\) .*version \([0-9.]*\).*/\1 \2/p' \
        | tail -1
}

collect_regdomain() {
    command -v iw >/dev/null 2>&1 || return 1
    iw reg get 2>/dev/null | sed -n 's/^country \([A-Z0-9]*\):.*/\1/p' | head -1
}

json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }

# Une paire "cle":"valeur" seulement si la valeur existe. C'est la regle de tout ce
# fichier : ce qu'on n'a pas mesure ne figure pas dans le rapport, pour que le serveur
# distingue « vide » de « pas su lire ».
kv() {
    [ -n "$2" ] || return 0
    printf '%s"%s":"%s"' "$3" "$1" "$(json_escape "$2")"
}

build_report() {
    kernel=$(uname -r 2>/dev/null)
    arch=$(uname -m 2>/dev/null)
    os_id=$(read_os_key ID)
    os_codename=$(read_os_key VERSION_CODENAME)
    img_version=$(read_release_key PERSEE_VERSION)
    img_commit=$(read_release_key PERSEE_GIT_COMMIT)
    img_branch=$(read_release_key PERSEE_GIT_BRANCH)
    img_dirty=$(read_release_key PERSEE_GIT_DIRTY)
    img_built=$(read_release_key PERSEE_BUILD_DATE)
    fw=$(collect_wifi_firmware)
    reg=$(collect_regdomain)
    nm=$(command -v nmcli >/dev/null 2>&1 && nmcli --version 2>/dev/null | awk '{print $NF}')
    opkg_json=$(collect_opkg)
    apt_json=$(collect_apt)
    feeds_json=$(collect_feeds)
    net_json=$(collect_net)

    printf '{'
    printf '"serial":"%s"' "$(json_escape "$(read_serial)")"
    kv hostname        "$(hostname 2>/dev/null)" ","
    kv kernelRelease   "$kernel"      ","
    kv arch            "$arch"        ","
    kv osId            "$os_id"       ","
    kv osCodename      "$os_codename" ","
    kv imageVersion    "$img_version" ","
    kv imageCommit     "$img_commit"  ","
    kv imageBranch     "$img_branch"  ","
    kv imageDirty      "$img_dirty"   ","
    kv imageBuiltAt    "$img_built"   ","
    kv wifiFirmware    "$fw"          ","
    kv regdomain       "$reg"         ","
    kv nmVersion       "$nm"          ","
    kv reporterVersion "$VERSION"     ","
    [ -n "$opkg_json" ] && printf ',"opkg":%s' "$opkg_json"
    [ -n "$apt_json" ]  && printf ',"apt":%s'  "$apt_json"
    [ "$feeds_json" != "{}" ] && printf ',"feeds":%s' "$feeds_json"
    [ -n "$net_json" ]  && printf ',"net":%s'  "$net_json"
    printf '}'
}

# --- empreinte ---------------------------------------------------------------------
# On hache le rapport SANS les champs volatils : sans cela, le moindre detail changeant
# a chaque lecture ferait croire a un changement d'inventaire a chaque reveil, et la
# date de dernier changement cote serveur ne voudrait plus rien dire.
report_hash() {
    # Ni la version du rapporteur, ni les latences : elles bougent a chaque passage sans
    # que rien n'ait change. Les codes de joignabilite, eux, comptent : un site qui se
    # met a bloquer ISACore est un changement a remonter tout de suite.
    printf '%s' "$1" | sed 's/"reporterVersion":"[^"]*"//; s/,"ms":[0-9]*//g' \
        | { command -v sha256sum >/dev/null 2>&1 && sha256sum || md5sum; } \
        | awk '{print $1}'
}

# --- identifiants ------------------------------------------------------------------------

# La cle du parc, telle qu'ISA2 la tolere : un PEM parfois ecrit sur une seule ligne. On
# remet alors les sauts de ligne apres l'en-tete et avant le pied, comme crypto.h.
cloud_key_file() {
    [ -r "$CLOUD_KEY" ] || return 1
    if [ "$(wc -l < "$CLOUD_KEY")" -le 1 ]; then
        tmpk="$STATE_DIR/.cle.pem"
        sed -e 's/\(-----BEGIN [A-Z ]*-----\)/\1\n/' -e 's/\(-----END [A-Z ]*-----\)/\n\1/' "$CLOUD_KEY" > "$tmpk"
        chmod 600 "$tmpk" 2>/dev/null
        echo "$tmpk"
    else
        echo "$CLOUD_KEY"
    fi
}

# Les JWT de l'appareil, un par ligne, ha_cloud_isacore d'abord. Rien si la cle ou
# openssl manquent : on retombera sur le jeton partage.
device_jwts() {
    command -v openssl >/dev/null 2>&1 || return 0
    key=$(cloud_key_file) || return 0
    for f in "$SYSDATA/ha_cloud_isacore" "$SYSDATA/ha_cloud"; do
        [ -s "$f" ] || continue
        # Le serveur chiffre en PKCS#1 v1.5 ; ISA2 essaie aussi OAEP, on fait pareil.
        clair=$(base64 -d < "$f" 2>/dev/null | openssl rsautl -decrypt -inkey "$key" 2>/dev/null) \
          || clair=$(base64 -d < "$f" 2>/dev/null | openssl rsautl -decrypt -oaep -inkey "$key" 2>/dev/null) \
          || continue
        printf '%s\n' "$clair" | sed -n 's/.*"jwt"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
    done
}

# --- envoi -----------------------------------------------------------------------------

# Un POST, l'en-tete d'authentification passe par stdin (-K -) : ni le JWT ni le jeton
# n'apparaissent dans `ps`. Rend le code HTTP.
post_report() { # post_report <api> <en-tete d'authentification> <corps>
    printf 'header = "%s"\n' "$2" \
    | curl -sS -m "$TIMEOUT" -o /dev/null -w '%{http_code}' -K - \
           -X POST "$1/inventory" \
           -H "Content-Type: application/json" \
           -H "User-Agent: $serial" \
           --data-binary "$3" 2>/dev/null
}

send() {
    body="$1"
    serial=$(read_serial)
    jwts=$(device_jwts)
    token=$(conf_value token "")
    if [ -z "$jwts" ] && [ -z "$token" ]; then
        log "aucun identifiant (ni $SYSDATA/ha_cloud*, ni jeton dans $CONF) : rien n'est envoye"
        return 1
    fi
    for api in $APIS; do
        # 1. Les identifiants de l'appareil : le serveur sait alors QUI parle, sans croire
        #    le numero de serie sur parole. Un 401 veut dire « pas pour cette instance »
        #    (JWT OpenAPI, appareil jamais decouvert ici) : on essaie le suivant.
        for jwt in $jwts; do
            code=$(post_report "$api" "Authorization: Bearer $jwt" "$body")
            case "$code" in
                2*) log "rapport accepte par $api ($code, identifiants de l'appareil)"; return 0 ;;
                401|403) ;;
                *)  log "echec vers $api (code ${code:-aucun})"; continue 2 ;;
            esac
        done
        # 2. Le jeton partage, pour la transition.
        if [ -n "$token" ]; then
            code=$(post_report "$api" "X-Soft-Inventory: $token" "$body")
            case "$code" in
                2*) log "rapport accepte par $api ($code, jeton partage)"; return 0 ;;
            esac
        fi
        log "echec vers $api (code ${code:-aucun})"
    done
    return 1
}

# --- programme principal -----------------------------------------------------------------
VERSION="${VERSION:-0.1.0-kiosk-1}"
FORCE=0
case "${1:-}" in
    --now)   FORCE=1 ;;            # apres un deploiement : on veut la ligne tout de suite
    --print) build_report; echo; exit 0 ;;   # pour le diagnostic, n'envoie rien
    --net)   collect_net; echo; exit 0 ;;    # la joignabilite seule (audit par Robin)
    --version) echo "$VERSION"; exit 0 ;;
    "")      ;;
    *)       echo "usage: $0 [--now|--print|--net|--version]" >&2; exit 2 ;;
esac

report=$(build_report)
hash=$(report_hash "$report")

mkdir -p "$STATE_DIR" 2>/dev/null
LAST="$STATE_DIR/last-hash"
previous=""
[ -r "$LAST" ] && previous=$(cat "$LAST" 2>/dev/null)

# ON N'ENVOIE QUE CE QUI A CHANGE — sauf quand on nous le demande.
# Le rattrapage periodique, lui, envoie MEME SI rien n'a change, et ce n'est pas un
# gaspillage : c'est ce qui fait avancer la date de reception cote serveur. Sans lui,
# un appareil stable et un appareil muet auraient exactement la meme tete en base, et
# l'inventaire ne saurait plus dire s'il est a jour ou simplement perime.
if [ "$FORCE" = 0 ] && [ "$hash" = "$previous" ]; then
    log "inventaire inchange ($hash), rien a envoyer"
    exit 0
fi

if send "$report"; then
    printf '%s' "$hash" > "$LAST" 2>/dev/null
    exit 0
fi

# Envoi rate : on NE memorise PAS l'empreinte, pour retenter au prochain reveil.
log "rapport non transmis, il sera retente"
exit 1
