#!/usr/bin/env bash # STEP050B - Porting Presenze 215.6 # # Audit diretto DB del difetto di stampa Silvelox: # le ore ORD compaiono nella colonna ASS. # # Correzione rispetto a STEP050: # - NON usa dataReportTable.json per identificare il dipendente; # - risolve AGOSTINI LORENZO / badge 000000038 direttamente da: # ax_user # ax_associazioni_temporali # - confronta: # ax_new_time_day # ax_new_time_causali # ax_new_time # ax_new_time_causali_month # - acquisisce il codice che salva ORD/ABSE. # # Modalita': AUDIT ONLY. # Nessuna modifica a file, database o servizi. # # Server atteso: 215.6 / ax-accessi set -Eeuo pipefail umask 077 STEP_NAME="STEP050B_2156_SILVELOX_PRINT_ORD_AS_ABSE_DB_DIRECT_AUDIT_ONLY_VERIFIED_20260731" EXPECTED_HOST="${EXPECTED_HOST:-ax-accessi}" EXPECTED_IP_FRAGMENT="${EXPECTED_IP_FRAGMENT:-192.168.215.6}" TARGET_ROOT="${TARGET_ROOT:-/var/www/html/presenze.silvelox}" TARGET_DB="${TARGET_DB:-presenze_silvelox}" TIME_HELPER="${TIME_HELPER:-$TARGET_ROOT/components/templates/custom_templates/ax_attendance/ax_schede_presenze_modA/timeCtrl/timeDataHelper.php}" TIME_HELPER_OK="${TIME_HELPER_OK:-$TARGET_ROOT/components/templates/custom_templates/ax_attendance/ax_schede_presenze_modA/timeCtrl_silvelox_ok/timeDataHelper.php}" TIME_HELPER_LAST="${TIME_HELPER_LAST:-$TARGET_ROOT/components/templates/custom_templates/ax_attendance/ax_schede_presenze_modA/timeCtrl_lastonsilvelox/timeDataHelper.php}" BADGE="${BADGE:-000000038}" LAST_NAME="${LAST_NAME:-AGOSTINI}" FIRST_NAME="${FIRST_NAME:-LORENZO}" PERIOD="${PERIOD:-2026-07}" ALLOW_TEST="${ALLOW_TEST:-0}" MYSQL_BIN_OVERRIDE="${MYSQL_BIN_OVERRIDE:-}" STAMP="$(date +%Y%m%d_%H%M%S)" OUTPUT_ZIP="$PWD/${STEP_NAME}_${STAMP}.zip" WORKDIR="$(mktemp -d "$PWD/.${STEP_NAME}.XXXXXX")" REPORT="$WORKDIR/${STEP_NAME}_REPORT.txt" SCHEMA="$WORKDIR/schema_columns.tsv" IDENTITY_CANDIDATES="$WORKDIR/identity_candidates.tsv" IDENTITY="$WORKDIR/resolved_identity.tsv" DAY_ROWS="$WORKDIR/ax_new_time_day.tsv" DAY_CAUS="$WORKDIR/ax_new_time_causali.tsv" MONTH_ROW="$WORKDIR/ax_new_time_month.tsv" MONTH_CAUS="$WORKDIR/ax_new_time_causali_month.tsv" COMPARE="$WORKDIR/day_vs_causali_comparison.tsv" SUMMARY="$WORKDIR/automatic_findings.txt" SOURCE_SNIPPETS="$WORKDIR/source_snippets.txt" SOURCE_COMPARE="$WORKDIR/source_compare.txt" SQL_FILE="$WORKDIR/audit_queries.sql" RESOLVER="$WORKDIR/resolve_identity.php" COMPARATOR="$WORKDIR/compare_data.php" cleanup() { rm -rf -- "$WORKDIR" } on_error() { local rc=$? local line="${1:-?}" trap - ERR printf 'ERRORE linea %s, codice %s\n' "$line" "$rc" >&2 exit "$rc" } trap cleanup EXIT INT TERM trap 'on_error "$LINENO"' ERR progress() { printf '%s\n' "$1" printf '\n[%s] %s\n' "$(date '+%H:%M:%S')" "$1" >> "$REPORT" } HOST_NOW="$(hostname 2>/dev/null || true)" IPS_NOW="$(hostname -I 2>/dev/null || true)" if [[ "$ALLOW_TEST" != "1" ]]; then if [[ "$HOST_NOW" != "$EXPECTED_HOST" && "$IPS_NOW" != *"$EXPECTED_IP_FRAGMENT"* ]]; then echo "ERRORE: server errato; atteso 215.6 / ax-accessi." >&2 exit 10 fi fi [[ -d "$TARGET_ROOT" ]] || { echo "ERRORE: portale non trovato: $TARGET_ROOT" >&2 exit 11 } [[ -f "$TIME_HELPER" ]] || { echo "ERRORE: timeDataHelper.php non trovato: $TIME_HELPER" >&2 exit 12 } if ! [[ "$PERIOD" =~ ^[0-9]{4}-[0-9]{2}$ ]]; then echo "ERRORE: periodo non valido: $PERIOD" >&2 exit 13 fi PHP_BIN="$(command -v php5.6 || command -v php || true)" [[ -n "$PHP_BIN" ]] || { echo "ERRORE: PHP CLI non disponibile." >&2 exit 14 } if [[ -n "$MYSQL_BIN_OVERRIDE" ]]; then MYSQL_BIN="$MYSQL_BIN_OVERRIDE" else MYSQL_BIN="$(command -v mariadb || command -v mysql || true)" fi [[ -n "$MYSQL_BIN" && -x "$MYSQL_BIN" ]] || { echo "ERRORE: client MariaDB/MySQL non disponibile." >&2 exit 15 } YEAR="${PERIOD%-*}" MONTH="${PERIOD#*-}" DATE_FROM="${PERIOD}-01" DATE_TO="$( "$PHP_BIN" -r ' $date = new DateTime($argv[1] . "-01"); $date->modify("last day of this month"); echo $date->format("Y-m-d"); ' "$PERIOD" )" { echo "$STEP_NAME" echo "DATA: $(date --iso-8601=seconds 2>/dev/null || date)" echo "SERVER: $HOST_NOW" echo "IP: $IPS_NOW" echo "PORTALE: $TARGET_ROOT" echo "DATABASE: $TARGET_DB" echo "DIPENDENTE: $LAST_NAME $FIRST_NAME" echo "BADGE: $BADGE" echo "PERIODO: $DATE_FROM -> $DATE_TO" echo "RISOLUZIONE IDENTITA': DATABASE DIRETTO" echo "JSON TRANSITORIO: NON UTILIZZATO" echo "MODALITA': AUDIT ONLY" echo "NESSUNA MODIFICA ESEGUITA" } > "$REPORT" progress "[1/7] Verifica dello schema reale" if "$MYSQL_BIN" \ --batch \ --raw \ --skip-column-names \ -e " SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COALESCE(COLUMN_DEFAULT, '') FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '${TARGET_DB}' AND TABLE_NAME IN ( 'ax_user', 'ax_associazioni_temporali', 'ax_new_time_day', 'ax_new_time_causali', 'ax_new_time', 'ax_new_time_causali_month' ) ORDER BY TABLE_NAME, ORDINAL_POSITION; " > "$SCHEMA" 2>&1; then : else rc=$? cat "$SCHEMA" >&2 exit "$rc" fi cat > "$WORKDIR/required_columns.txt" <<'EOF' ax_user usrId ax_user usrFirst ax_user usrLast ax_associazioni_temporali usrId ax_associazioni_temporali usrBdg ax_associazioni_temporali attuale ax_new_time_day userId ax_new_time_day day ax_new_time_day ordinarie ax_new_time_day straordinarie ax_new_time_day assenze ax_new_time_day maggiorazioni ax_new_time_day overturno ax_new_time_day bancaore ax_new_time_causali userId ax_new_time_causali dayCaus ax_new_time_causali causType ax_new_time_causali causGest ax_new_time_causali causTime ax_new_time userId ax_new_time anno ax_new_time mese ax_new_time ordinarie ax_new_time assenze ax_new_time_causali_month priKeyRif ax_new_time_causali_month causType ax_new_time_causali_month causTime EOF while IFS=$'\t' read -r table column; do if ! awk -F '\t' \ -v table="$table" \ -v column="$column" \ '$1 == table && $2 == column {found=1} END {exit(found ? 0 : 1)}' \ "$SCHEMA"; then echo "ERRORE: colonna richiesta assente: $table.$column" >&2 exit 20 fi done < "$WORKDIR/required_columns.txt" { echo echo "===== SCHEMA VERIFICATO =====" cat "$SCHEMA" } >> "$REPORT" progress "[2/7] Risoluzione dipendente direttamente dal database" cat > "$SQL_FILE" < "$IDENTITY_CANDIDATES" 2>&1; then : else rc=$? cat "$IDENTITY_CANDIDATES" >&2 exit "$rc" fi cat > "$RESOLVER" <<'PHP' (string) $parts[0], 'usrFirst' => (string) $parts[1], 'usrLast' => (string) $parts[2], 'usrBdg' => (string) $parts[3], 'attuale' => (int) $parts[4], 'score' => (int) $parts[5] ); } fclose($handle); if (count($rows) === 0) { fwrite( STDERR, "Nessun dipendente trovato nel database per nome o badge\n" ); exit(5); } usort( $rows, function ($left, $right) { if ($left['score'] === $right['score']) { if ($left['attuale'] === $right['attuale']) { return strcmp($left['usrId'], $right['usrId']); } return $right['attuale'] - $left['attuale']; } return $right['score'] - $left['score']; } ); $best = $rows[0]; if ($best['score'] <= 0) { fwrite(STDERR, "Nessun candidato con corrispondenza valida\n"); exit(6); } foreach ($rows as $index => $row) { if ($index === 0) { continue; } if ( $row['score'] === $best['score'] && $row['usrId'] !== $best['usrId'] ) { fwrite( STDERR, "Identita' ambigua: piu' usrId con punteggio massimo\n" ); exit(7); } } $handle = fopen($output, 'wb'); if ($handle === false) { fwrite(STDERR, "Impossibile scrivere identita'\n"); exit(8); } fwrite( $handle, "usrId\tusrFirst\tusrLast\tusrBdg\tattuale\tscore\n" ); fwrite( $handle, implode( "\t", array( $best['usrId'], $best['usrFirst'], $best['usrLast'], $best['usrBdg'], $best['attuale'], $best['score'] ) ) . "\n" ); fclose($handle); echo $best['usrId'] . "\n"; PHP if "$PHP_BIN" \ "$RESOLVER" \ "$IDENTITY_CANDIDATES" \ "$IDENTITY" \ > "$WORKDIR/resolved_user_id.txt" 2> "$WORKDIR/resolve_error.txt"; then : else rc=$? cat "$WORKDIR/resolve_error.txt" >&2 echo "CANDIDATI TROVATI:" >&2 cat "$IDENTITY_CANDIDATES" >&2 exit "$rc" fi USER_ID="$(tr -d '\r\n' < "$WORKDIR/resolved_user_id.txt")" [[ "$USER_ID" =~ ^[0-9]+$ ]] || { echo "ERRORE: usrId risolto non valido: $USER_ID" >&2 exit 21 } { echo echo "===== CANDIDATI IDENTITA' =====" cat "$IDENTITY_CANDIDATES" echo echo "===== IDENTITA' RISOLTA =====" cat "$IDENTITY" echo "USER_ID=$USER_ID" } >> "$REPORT" progress "[3/7] Lettura dati giornalieri e causali" if "$MYSQL_BIN" \ --batch \ --raw \ --skip-column-names \ -e " SELECT DATE_FORMAT(day, '%Y-%m-%d'), COALESCE(ordinarie, 0), COALESCE(straordinarie, 0), COALESCE(assenze, 0), COALESCE(maggiorazioni, 0), COALESCE(overturno, 0), COALESCE(bancaore, 0), COALESCE(workday, 0), COALESCE(data, '') FROM \`${TARGET_DB}\`.ax_new_time_day WHERE userId = ${USER_ID} AND day BETWEEN '${DATE_FROM}' AND '${DATE_TO}' ORDER BY day; " > "$DAY_ROWS" 2>&1; then : else rc=$? cat "$DAY_ROWS" >&2 exit "$rc" fi if "$MYSQL_BIN" \ --batch \ --raw \ --skip-column-names \ -e " SELECT DATE_FORMAT(dayCaus, '%Y-%m-%d'), COALESCE(causType, ''), COALESCE(causGest, ''), SUM(COALESCE(causTime, 0)) FROM \`${TARGET_DB}\`.ax_new_time_causali WHERE userId = ${USER_ID} AND dayCaus BETWEEN '${DATE_FROM}' AND '${DATE_TO}' GROUP BY dayCaus, causType, causGest ORDER BY dayCaus, causType, causGest; " > "$DAY_CAUS" 2>&1; then : else rc=$? cat "$DAY_CAUS" >&2 exit "$rc" fi [[ -s "$DAY_ROWS" ]] || { echo "ERRORE: nessuna riga ax_new_time_day per usrId=$USER_ID" >&2 exit 22 } [[ -s "$DAY_CAUS" ]] || { echo "ERRORE: nessuna riga ax_new_time_causali per usrId=$USER_ID" >&2 exit 23 } progress "[4/7] Lettura totali mensili" if "$MYSQL_BIN" \ --batch \ --raw \ --skip-column-names \ -e " SELECT priKey, userId, anno, mese, COALESCE(ordinarie, 0), COALESCE(straordinarie, 0), COALESCE(assenze, 0), COALESCE(maggiorazioni, 0), COALESCE(overturno, 0), COALESCE(bancaore, 0) FROM \`${TARGET_DB}\`.ax_new_time WHERE userId = ${USER_ID} AND anno = '${YEAR}' AND LPAD(mese, 2, '0') = '${MONTH}' ORDER BY priKey; " > "$MONTH_ROW" 2>&1; then : else rc=$? cat "$MONTH_ROW" >&2 exit "$rc" fi PRI_KEY_MONTH="${USER_ID}_${YEAR}_${MONTH}" if "$MYSQL_BIN" \ --batch \ --raw \ --skip-column-names \ -e " SELECT COALESCE(causType, ''), SUM(COALESCE(causTime, 0)) FROM \`${TARGET_DB}\`.ax_new_time_causali_month WHERE priKeyRif = '${PRI_KEY_MONTH}' GROUP BY causType ORDER BY causType; " > "$MONTH_CAUS" 2>&1; then : else rc=$? cat "$MONTH_CAUS" >&2 exit "$rc" fi { echo echo "===== AX_NEW_TIME_DAY =====" cat "$DAY_ROWS" echo echo "===== AX_NEW_TIME_CAUSALI =====" cat "$DAY_CAUS" echo echo "===== AX_NEW_TIME MENSILE =====" cat "$MONTH_ROW" echo echo "===== AX_NEW_TIME_CAUSALI_MONTH =====" cat "$MONTH_CAUS" } >> "$REPORT" progress "[5/7] Confronto automatico giornaliero e mensile" cat > "$COMPARATOR" <<'PHP' numberValue($row[1]), 'EXT' => numberValue($row[2]), 'ABSE' => numberValue($row[3]), 'MAGG' => numberValue($row[4]), 'OVER' => numberValue($row[5]), 'BANK' => numberValue($row[6]), 'WORKDAY' => $row[7], 'DATA' => $row[8] ); } foreach ($causRows as $row) { if (count($row) < 4) { continue; } $day = $row[0]; $type = strtoupper(trim($row[1])); $gest = trim($row[2]); $value = numberValue($row[3]); if (!isset($causes[$day])) { $causes[$day] = array( 'ORD' => 0.0, 'EXT' => 0.0, 'ABSE' => 0.0, 'MAGG' => 0.0, 'OVER' => 0.0, 'BANK' => 0.0 ); } if ($gest === 'la_magg') { $causes[$day]['MAGG'] += $value; } elseif (isset($causes[$day][$type])) { $causes[$day][$type] += $value; } } $allDays = array_unique( array_merge( array_keys($days), array_keys($causes) ) ); sort($allDays); $handle = fopen($outputFile, 'wb'); if ($handle === false) { throw new Exception("Impossibile scrivere confronto"); } writeLine( $handle, array( 'day', 'day_ORD', 'day_ABSE', 'caus_ORD', 'caus_ABSE', 'workday', 'flags' ) ); $stats = array( 'days' => 0, 'day_vs_caus_mismatch' => 0, 'caus_ord_day_abse' => 0, 'day_ord_total' => 0.0, 'day_abse_total' => 0.0, 'caus_ord_total' => 0.0, 'caus_abse_total' => 0.0 ); foreach ($allDays as $day) { $stats['days']++; $dayData = isset($days[$day]) ? $days[$day] : array( 'ORD' => 0, 'ABSE' => 0, 'WORKDAY' => '' ); $causData = isset($causes[$day]) ? $causes[$day] : array( 'ORD' => 0, 'ABSE' => 0 ); $stats['day_ord_total'] += $dayData['ORD']; $stats['day_abse_total'] += $dayData['ABSE']; $stats['caus_ord_total'] += $causData['ORD']; $stats['caus_abse_total'] += $causData['ABSE']; $flags = array(); if ( abs($dayData['ORD'] - $causData['ORD']) > 0.001 || abs($dayData['ABSE'] - $causData['ABSE']) > 0.001 ) { $flags[] = 'DAY_VS_CAUS_MISMATCH'; $stats['day_vs_caus_mismatch']++; } if ( $causData['ORD'] > 0.001 && $dayData['ORD'] <= 0.001 && $dayData['ABSE'] > 0.001 ) { $flags[] = 'CAUS_ORD_DAY_ABSE'; $stats['caus_ord_day_abse']++; } writeLine( $handle, array( $day, numberText($dayData['ORD']), numberText($dayData['ABSE']), numberText($causData['ORD']), numberText($causData['ABSE']), $dayData['WORKDAY'], implode(',', $flags) ) ); } fclose($handle); $monthOrd = 0.0; $monthAbse = 0.0; foreach ($monthRows as $row) { if (count($row) >= 10) { $monthOrd += numberValue($row[4]); $monthAbse += numberValue($row[6]); } } $monthCausOrd = 0.0; $monthCausAbse = 0.0; foreach ($monthCausRows as $row) { if (count($row) < 2) { continue; } $type = strtoupper(trim($row[0])); $value = numberValue($row[1]); if ($type === 'ORD') { $monthCausOrd += $value; } elseif ($type === 'ABSE') { $monthCausAbse += $value; } } $cause = 'NON_CLASSIFICATA'; if ( $stats['caus_ord_day_abse'] > 0 && $stats['day_vs_caus_mismatch'] > 0 ) { $cause = 'AX_NEW_TIME_DAY_CLASSIFICA_ORD_COME_ABSE'; } $summary = array( 'DAYS=' . $stats['days'], 'DAY_VS_CAUS_MISMATCH=' . $stats['day_vs_caus_mismatch'], 'CAUS_ORD_DAY_ABSE=' . $stats['caus_ord_day_abse'], 'DAY_ORD_TOTAL=' . numberText($stats['day_ord_total']), 'DAY_ABSE_TOTAL=' . numberText($stats['day_abse_total']), 'DAY_CAUS_ORD_TOTAL=' . numberText($stats['caus_ord_total']), 'DAY_CAUS_ABSE_TOTAL=' . numberText($stats['caus_abse_total']), 'MONTH_ORD_TOTAL=' . numberText($monthOrd), 'MONTH_ABSE_TOTAL=' . numberText($monthAbse), 'MONTH_CAUS_ORD_TOTAL=' . numberText($monthCausOrd), 'MONTH_CAUS_ABSE_TOTAL=' . numberText($monthCausAbse), 'CAUSE=' . $cause, '', 'ESITO: AUDIT COMPLETATO', 'NESSUNA MODIFICA ESEGUITA' ); file_put_contents( $summaryFile, implode("\n", $summary) . "\n" ); PHP if "$PHP_BIN" \ "$COMPARATOR" \ "$DAY_ROWS" \ "$DAY_CAUS" \ "$MONTH_ROW" \ "$MONTH_CAUS" \ "$COMPARE" \ "$SUMMARY" > "$WORKDIR/compare_stdout.txt" 2> "$WORKDIR/compare_error.txt"; then : else rc=$? cat "$WORKDIR/compare_error.txt" >&2 exit "$rc" fi cat "$COMPARE" >> "$REPORT" cat "$SUMMARY" >> "$REPORT" progress "[6/7] Acquisizione del codice che salva ORD e ABSE" { echo "===== FILE ATTIVO =====" stat -c '%A %a %U:%G %s %y %n' "$TIME_HELPER" sha256sum "$TIME_HELPER" "$PHP_BIN" -l "$TIME_HELPER" echo echo "===== insertUpdateTimeMaster =====" START_LINE="$( grep -nF \ 'public function insertUpdateTimeMaster(' \ "$TIME_HELPER" \ | head -n 1 \ | cut -d: -f1 )" [[ "$START_LINE" =~ ^[0-9]+$ ]] END_LINE="$((START_LINE + 135))" nl -ba "$TIME_HELPER" \ | sed -n "${START_LINE},${END_LINE}p" echo echo "===== OCCORRENZE ORD / ABSE =====" grep -nE \ "outTypeDay|outType\\[|ordinarie|assenze|ax_new_time_day|causTime" \ "$TIME_HELPER" \ | head -n 600 } > "$SOURCE_SNIPPETS" 2>&1 { echo "===== HASH FILE ATTIVO =====" sha256sum "$TIME_HELPER" if [[ -f "$TIME_HELPER_OK" ]]; then echo echo "===== HASH timeCtrl_silvelox_ok =====" sha256sum "$TIME_HELPER_OK" echo echo "===== DIFF timeCtrl_silvelox_ok -> ATTIVO =====" diff -u \ "$TIME_HELPER_OK" \ "$TIME_HELPER" \ | head -n 3000 || true else echo "ASSENTE: $TIME_HELPER_OK" fi if [[ -f "$TIME_HELPER_LAST" ]]; then echo echo "===== HASH timeCtrl_lastonsilvelox =====" sha256sum "$TIME_HELPER_LAST" echo echo "===== DIFF timeCtrl_lastonsilvelox -> ATTIVO =====" diff -u \ "$TIME_HELPER_LAST" \ "$TIME_HELPER" \ | head -n 3000 || true else echo "ASSENTE: $TIME_HELPER_LAST" fi } > "$SOURCE_COMPARE" 2>&1 cat "$SOURCE_SNIPPETS" >> "$REPORT" cat "$SOURCE_COMPARE" >> "$REPORT" progress "[7/7] Creazione ZIP" ( cd "$WORKDIR" zip -q -9 "$OUTPUT_ZIP" \ "$(basename "$REPORT")" \ "$(basename "$SCHEMA")" \ "$(basename "$IDENTITY_CANDIDATES")" \ "$(basename "$IDENTITY")" \ "$(basename "$DAY_ROWS")" \ "$(basename "$DAY_CAUS")" \ "$(basename "$MONTH_ROW")" \ "$(basename "$MONTH_CAUS")" \ "$(basename "$COMPARE")" \ "$(basename "$SUMMARY")" \ "$(basename "$SOURCE_SNIPPETS")" \ "$(basename "$SOURCE_COMPARE")" \ "$(basename "$SQL_FILE")" ) zip -T "$OUTPUT_ZIP" >/dev/null echo "STEP050B COMPLETATO" echo "ZIP: $OUTPUT_ZIP"