From 65dd92b769efc6eedfa2fe8458e47620c60c363c Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Mon, 20 Apr 2026 16:25:51 -0300 Subject: [PATCH] =?UTF-8?q?docs=20&=20tools:=20Actualizaci=C3=B3n=20de=20i?= =?UTF-8?q?nformes=20y=20scripts=20de=20migraci=C3=B3n=20DASUTEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adn/tools/bkps/lib/proc_dasuten.rb | 3 +- adn/tools/cli/configurar_zona_horaria_dasu.rb | 93 +++ adn/tools/cli/dron/dispatcher.rb | 6 +- adn/tools/cli/dron/ejecutor.rb | 28 +- .../dasuten_download_drive-dasu-sql4.rb | 71 +- ...dasuten_exportar_diferencial_srvv-fenix.rb | 42 +- .../cli/drones/dasuten_exportar_srvv-fenix.rb | 38 +- .../cli/drones/dasuten_restaurar_dasu-sql4.rb | 55 +- .../drones/dasuten_upload_srv-ns8-drive.rb | 21 +- adn/tools/cli/drones/lib/dasu_executor.rb | 11 +- adn/tools/cli/limpiar_backups_dasu.rb | 78 +++ adn/tools/cli/limpiar_fenix_orig.rb | 61 ++ adn/tools/cli/restaurar_completo_dasu.rb | 157 +++++ .../cli/verificacion_final_comparativa.rb | 205 ++++++ adn/tools/cli/verificar_abril_2026.rb | 68 ++ adn/tools/cli/verificar_backup_dasu.rb | 100 +++ adn/tools/cli/verificar_backup_info.rb | 106 +++ adn/tools/cli/verificar_bonos_fenix_dasu.rb | 214 ++++++ adn/tools/cli/verificar_datos_fenix_dasu.rb | 319 +++++++++ adn/tools/cli/verificar_estructura_tabla.rb | 103 +++ .../cli/verificar_fechas_timezone_dasu.rb | 95 +++ adn/tools/cli/verificar_fenix_actual.rb | 70 ++ adn/tools/cli/verificar_fenix_fechas.rb | 84 +++ adn/tools/cli/verificar_final_dasu.rb | 70 ++ adn/tools/cli/verificar_identity_simple.rb | 78 +++ adn/tools/cli/verificar_ids_fechas_dasu.rb | 85 +++ adn/tools/cli/verificar_pk_dasu.rb | 88 +++ adn/tools/cli/verificar_solo_dasu.rb | 63 ++ adn/tools/cli/verificar_triggers_dasu.rb | 79 +++ adn/tools/cli/verificar_zona_horaria_dasu.rb | 66 ++ adn/tools/db/migrations/004_create_planes.sql | 48 +- .../dtic-DASUTEN/A04.P006_Backups-DASUTEN.md | 57 +- .../dtic-DASUTEN/A04.P007_Bugs-Operativos.md | 273 ++++++++ .../00_caratula_entrega.md | 34 + .../00_caratula_entrega.pdf | Bin 0 -> 19842 bytes .../01_informe_ejecutivo_DASUTEN.md | 66 +- .../01_informe_ejecutivo_DASUTEN.pdf | Bin 131073 -> 53381 bytes .../02_informe_tecnico_DASUTEN.md | 135 +++- .../02_informe_tecnico_DASUTEN.pdf | Bin 0 -> 119761 bytes .../01_informe_ejecutivo_DASUTEN.pdf | Bin 0 -> 131073 bytes .../_versiones/01_v1.0.md | 158 +++++ .../_versiones/02_informe_tecnico_DASUTEN.md | 407 ------------ .../_versiones/02_v1.0.md | 620 ++++++++++++++++++ install_rclone.rb | 35 + 44 files changed, 3822 insertions(+), 568 deletions(-) create mode 100644 adn/tools/cli/configurar_zona_horaria_dasu.rb create mode 100644 adn/tools/cli/limpiar_backups_dasu.rb create mode 100644 adn/tools/cli/limpiar_fenix_orig.rb create mode 100644 adn/tools/cli/restaurar_completo_dasu.rb create mode 100644 adn/tools/cli/verificacion_final_comparativa.rb create mode 100644 adn/tools/cli/verificar_abril_2026.rb create mode 100644 adn/tools/cli/verificar_backup_dasu.rb create mode 100644 adn/tools/cli/verificar_backup_info.rb create mode 100644 adn/tools/cli/verificar_bonos_fenix_dasu.rb create mode 100644 adn/tools/cli/verificar_datos_fenix_dasu.rb create mode 100644 adn/tools/cli/verificar_estructura_tabla.rb create mode 100644 adn/tools/cli/verificar_fechas_timezone_dasu.rb create mode 100644 adn/tools/cli/verificar_fenix_actual.rb create mode 100644 adn/tools/cli/verificar_fenix_fechas.rb create mode 100644 adn/tools/cli/verificar_final_dasu.rb create mode 100644 adn/tools/cli/verificar_identity_simple.rb create mode 100644 adn/tools/cli/verificar_ids_fechas_dasu.rb create mode 100644 adn/tools/cli/verificar_pk_dasu.rb create mode 100644 adn/tools/cli/verificar_solo_dasu.rb create mode 100644 adn/tools/cli/verificar_triggers_dasu.rb create mode 100644 adn/tools/cli/verificar_zona_horaria_dasu.rb create mode 100644 docs/ambito/dtic-DASUTEN/A04.P007_Bugs-Operativos.md create mode 100644 docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.md create mode 100644 docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.pdf create mode 100644 docs/informes/2026-04_DASUTEN-migracion/02_informe_tecnico_DASUTEN.pdf create mode 100644 docs/informes/2026-04_DASUTEN-migracion/_versiones/01_informe_ejecutivo_DASUTEN.pdf create mode 100644 docs/informes/2026-04_DASUTEN-migracion/_versiones/01_v1.0.md delete mode 100644 docs/informes/2026-04_DASUTEN-migracion/_versiones/02_informe_tecnico_DASUTEN.md create mode 100644 docs/informes/2026-04_DASUTEN-migracion/_versiones/02_v1.0.md create mode 100644 install_rclone.rb diff --git a/adn/tools/bkps/lib/proc_dasuten.rb b/adn/tools/bkps/lib/proc_dasuten.rb index 6fd09311..0b72d8ba 100644 --- a/adn/tools/bkps/lib/proc_dasuten.rb +++ b/adn/tools/bkps/lib/proc_dasuten.rb @@ -50,9 +50,10 @@ module ADN nodo = tarea[:nodo] || detectar_nodo(tarea[:id]) nota = tarea[:texto] || "DASUTEN: #{tarea[:id]}" + ambito = tarea[:ambito] || 'dtic-DASUTEN' # Lanzar dron con bitácora automática - cmd = "#{@dron_runner} dron lanzar --nota #{Shellwords.escape(nota)} --nodo #{Shellwords.escape(nodo)} -- ruby #{script_path}" + cmd = "#{@dron_runner} dron lanzar --nota #{Shellwords.escape(nota)} --nodo #{Shellwords.escape(nodo)} --ambito #{Shellwords.escape(ambito)} -- ruby #{script_path}" @logger.info("Ejecutando dron: #{nota}") @logger.debug("Comando: #{cmd}") diff --git a/adn/tools/cli/configurar_zona_horaria_dasu.rb b/adn/tools/cli/configurar_zona_horaria_dasu.rb new file mode 100644 index 00000000..a56bd757 --- /dev/null +++ b/adn/tools/cli/configurar_zona_horaria_dasu.rb @@ -0,0 +1,93 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# Configurar zona horaria Argentina en dasu-sql4 +# ========================================================== + +require_relative 'drones/lib/dasu_executor' + +puts "=== CONFIGURAR ZONA HORARIA - ARGENTINA (UTC-3) ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\timezone_config_output.json" + + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" + + Write-Host "[1/4] Zona horaria ACTUAL:" + $tz = Get-TimeZone + Write-Host " ID: $($tz.Id)" + Write-Host " DisplayName: $($tz.DisplayName)" + Write-Host " BaseUtcOffset: $($tz.BaseUtcOffset)" + + Write-Host "`n[2/4] Configurando zona horaria ARGENTINA (UTC-3)..." + + # Argentina Standard Time es el ID correcto para UTC-3 + try { + Set-TimeZone -Id "Argentina Standard Time" + Write-Host " ✅ Zona horaria configurada correctamente" + } catch { + Write-Host " ERROR: $_" + exit 1 + } + + Write-Host "`n[3/4] Verificando nueva configuración:" + $tz = Get-TimeZone + Write-Host " ID: $($tz.Id)" + Write-Host " DisplayName: $($tz.DisplayName)" + Write-Host " BaseUtcOffset: $($tz.BaseUtcOffset)" + + Write-Host "`n[4/4] Fecha/hora actual:" + Write-Host " Get-Date: $(Get-Date)" + Write-Host " Get-Date -Format 'o': $(Get-Date -Format 'o')" + Write-Host " UTC: $(Get-Date -AsUtc)" + + # Verificar si SQL Server necesita reinicio + Write-Host "`n[5/5] Verificando SQL Server:" + $connString = "Server=localhost;Database=master;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT GETDATE() as fecha_sql, SYSUTCDATETIME() as utc_sql", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " GETDATE(): $($row.fecha_sql)" + Write-Host " SYSUTCDATETIME(): $($row.utc_sql)" + Write-Host " Diferencia con UTC: $([DateTime]::Now - [DateTime]::UtcNow)" + } + + $conn.Close() + + $resultado = @{ + paso = "timezone_configurado" + timezone_id = $tz.Id + timezone_display = $tz.DisplayName + utc_offset = $tz.BaseUtcOffset.ToString() + timestamp = (Get-Date -Format "o") + } + + $resultado | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +puts "\n[1/2] Configurando zona horaria..." +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: 'C:\temp\timezone_config_output.json') + +puts "\n[2/2] Leyendo resultado..." +begin + resultado = DasuExecutor.read_json_from_dasu('C:\temp\timezone_config_output.json') + puts "\n=== RESULTADO ===" + puts " Timezone ID: #{resultado['timezone_id']}" + puts " Display: #{resultado['timezone_display']}" + puts " UTC Offset: #{resultado['utc_offset']}" +rescue => e + puts "[!] Error leyendo resultado: #{e.message}" +end + +puts "\n✅ Configuración completada" +puts "\n⚠️ NOTA: SQL Server puede necesitar reinicio para aplicar completamente la nueva zona horaria." diff --git a/adn/tools/cli/dron/dispatcher.rb b/adn/tools/cli/dron/dispatcher.rb index d384ba20..b1721e6e 100644 --- a/adn/tools/cli/dron/dispatcher.rb +++ b/adn/tools/cli/dron/dispatcher.rb @@ -114,7 +114,8 @@ module Dron evento_id: opciones[:evento_id], timeout: opciones[:timeout], flujo_id: opciones[:flujo_id], - nodo: opciones[:nodo] + nodo: opciones[:nodo], + ambito: opciones[:ambito] ) # Mostrar resultado @@ -136,6 +137,7 @@ module Dron timeout: 3600, flujo_id: nil, nodo: nil, + ambito: nil, cmd: nil } @@ -153,6 +155,8 @@ module Dron opciones[:flujo_id] = args.shift when '--nodo' opciones[:nodo] = args.shift + when '--ambito' + opciones[:ambito] = args.shift when '--help', '-h' Dispatcher.mostrar_ayuda exit 0 diff --git a/adn/tools/cli/dron/ejecutor.rb b/adn/tools/cli/dron/ejecutor.rb index 4b9aa6c2..960cb4ac 100644 --- a/adn/tools/cli/dron/ejecutor.rb +++ b/adn/tools/cli/dron/ejecutor.rb @@ -24,14 +24,14 @@ module Dron HEARTBEAT_INTERVAL = 30 class << self - def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil) + def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil, ambito: nil) Base.logger.info("🛠️ Dron Ejecutor iniciado: #{Process.pid}") inicio = Time.now # 1. Registrar en tablas internas (dron_logs) resultado_inicio = ADN::DB::DronDB.registrar_inicio( tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, - metadata: { nota: nota, evento_id: evento_id, timeout: timeout, nodo: nodo } + metadata: { nota: nota, evento_id: evento_id, timeout: timeout, nodo: nodo, ambito: ambito } ) dron_id = resultado_inicio[:dron_id] @@ -42,7 +42,7 @@ module Dron ) # 2. Crear/vincular evento en Bitácora Web (visible) - evento_id = crear_o_vincular_evento_web(dron_id, nota, evento_id, cmd, nodo) + evento_id = crear_o_vincular_evento_web(dron_id, nota, evento_id, cmd, nodo, ambito) # 3. Ejecutar comando con heartbeat que actualiza AMBAS capas resultado = ejecutar_comando(cmd, dron_id, timeout, evento_id, nota, inicio) @@ -161,10 +161,11 @@ module Dron # ─── Bitácora Web: Crear/Vincular evento ───────────────────── - def crear_o_vincular_evento_web(dron_id, nota, evento_id, cmd, nodo) + def crear_o_vincular_evento_web(dron_id, nota, evento_id, cmd, nodo, ambito = nil) return nil unless nota # Sin nota = sin registro en web nodo_id = detectar_nodo_id(nodo) + ambito_id = detectar_ambito_id(ambito) BitacorasDB::BitacoraDB.with_connection do |db| if evento_id.to_i > 0 @@ -196,12 +197,14 @@ module Dron estado: '⏳', modo: 'P', bitacora_id: bitacora_hoy['id'], - nodo_id: nodo_id + nodo_id: nodo_id, + ambito_id: ambito_id ) entrada['id'] else evento = db.crear_evento( nodo_id: nodo_id, + ambito_id: ambito_id, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳', @@ -307,6 +310,21 @@ module Dron Base.logger.warn("⚠️ Error detectando nodo: #{e.message}, usando default (1)") 1 end + + def detectar_ambito_id(ambito) + return ambito if ambito.is_a?(Integer) + return nil if ambito.nil? || ambito.empty? + + resultado = nil + BitacorasDB::BitacoraDB.with_connection do |db| + resultado = db.execute('SELECT id FROM bitacoras.ambitos WHERE LOWER(nombre) = $1 LIMIT 1', [ambito.downcase]) + end + + resultado&.any? ? resultado.first['id'] : nil + rescue StandardError => e + Base.logger.warn("⚠️ Error detectando ámbito: #{e.message}, usando default (nil)") + nil + end end end end diff --git a/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb b/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb index 8985baaf..564f038f 100755 --- a/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb +++ b/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb @@ -5,8 +5,8 @@ # adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb # ========================================================== # Dron: Download desde Google Drive a dasu-sql4 -# Flujo: Google Drive → F:\BACKUP\sysdasuten_compressed_ADN.bak -# Usa: Invoke-WebRequest (sin rclone) +# Flujo: Google Drive → F:\BACKUP\sysdasuten_FULL_YYYYMMDD_HHMMSS.bak +# Usa: rclone copy (mantiene nombre original del backup) # ========================================================== require 'time' @@ -27,9 +27,12 @@ end.parse!(ARGV) BACKUP_DIR_WINDOWS = "F:\\BACKUP" BACKUP_NAME_DEFAULT = "sysdasuten_compressed_ADN.bak" -JSON_PATH = "C:\temp\dron_download_output.json" +JSON_PATH = "C:\\temp\\dron_download_output.json" # Determinar archivo a descargar +backup_name = nil +file_id = nil + if opciones[:test_file] puts "[*] Dron: Download desde Google Drive" puts " Nodo: dasu-sql4" @@ -52,10 +55,20 @@ else if File.exist?(UPLOAD_OUTPUT) upload_data = JSON.parse(File.read(UPLOAD_OUTPUT)) drive_ruta = upload_data['drive_ruta'] - file_id = File.basename(drive_ruta) + # Extraer el nombre EXACTO del archivo desde drive_ruta (ej: "rmonla-GDrive:drive_bkps-dasu/sysdasuten_FULL_20260415_121943.bak") backup_name = File.basename(drive_ruta) + + puts "[*] Dron: Buscando Google Drive ID real (rclone)..." + raw_json = `rclone lsjson '#{drive_ruta}' 2>/dev/null`.strip + begin + parsed = JSON.parse(raw_json).first + file_id = parsed['ID'] + rescue + puts "[!] Error al parsear ID de Google Drive. rclone regresó vacio." + exit 1 + end + download_url = "https://drive.google.com/uc?export=download&id=#{file_id}" - puts "[*] Dron: Download desde Google Drive" puts " Nodo: dasu-sql4" puts " Drive ruta: #{drive_ruta}" puts " File ID: #{file_id}" @@ -66,55 +79,44 @@ else end end +# Validar que backup_name tenga el formato correcto (FULL o DIF con fecha) +unless backup_name =~ /sysdasuten_(FULL|DIF)_\d{8}_\d{6}\.bak/ + puts "[!] Advertencia: El nombre del backup '#{backup_name}' no sigue el formato esperado (sysdasuten_FULL_YYYYMMDD_HHMMSS.bak)" +end + # PowerShell script para download ps_script = <<~PSCMD.strip $ErrorActionPreference = "Stop" $BACKUP_DIR = "F:\\BACKUP" - $DOWNLOAD_URL = "#{download_url}" $BACKUP_NAME = "#{backup_name}" $LOCAL_BACKUP = "$BACKUP_DIR\\$BACKUP_NAME" $JSON_PATH = "C:\\temp\\dron_download_output.json" + $REMOTE_RCLONE_PATH = "#{drive_ruta}" $null = New-Item -ItemType Directory -Force -Path "C:\\temp" $null = New-Item -ItemType Directory -Force -Path $BACKUP_DIR - Write-Host "[1/3] Iniciando descarga..." - Write-Host " URL: $DOWNLOAD_URL" + Write-Host "[1/3] Iniciando descarga con Rclone Nativo..." + Write-Host " Origen: $REMOTE_RCLONE_PATH" + Write-Host " Destino: $LOCAL_BACKUP" Write-Host "[2/3] Descargando..." $start = Get-Date - try { - $response = Invoke-WebRequest -Uri $DOWNLOAD_URL -UseBasicParsing -Headers @{ - "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - } - $html = $response.Content - $fileId = "" - $uuid = "" - $confirm = "t" - if ($html -match 'name="id" value="([^"]+)"') { $fileId = $matches[1] } - if ($html -match 'name="uuid" value="([^"]+)"') { $uuid = $matches[1] } - if ($html -match 'name="confirm" value="([^"]+)"') { $confirm = $matches[1] } + # Limpiamos el ultimo archivo varado (por si el anterior dejo su esqueleto de 0 bytes) + if (Test-Path $LOCAL_BACKUP) { Remove-Item $LOCAL_BACKUP -Force } - Write-Host " File ID: $fileId" - Write-Host " UUID: $uuid" + # Llamamos al rclone binario local apuntando al config + & C:\\rclone\\rclone.exe copy "$REMOTE_RCLONE_PATH" "$BACKUP_DIR\\" --config "C:\\rclone\\rclone.conf" --stats-one-line --stats 1s - $downloadConfirmUrl = "https://drive.usercontent.google.com/download?id=$fileId&export=download&confirm=$confirm&uuid=$uuid" - Write-Host " URL confirmada: $downloadConfirmUrl" - - $finalResponse = Invoke-WebRequest -Uri $downloadConfirmUrl -UseBasicParsing -Headers @{ - "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - } - - [System.IO.File]::WriteAllBytes($LOCAL_BACKUP, $finalResponse.Content) - - $dur = (New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds - Write-Host "[+] Descarga en $([math]::Round($dur, 2))s" - } catch { - Write-Host "[!] Error: $_" + if ($LASTEXITCODE -ne 0) { + Write-Host "[!] Error: Falló la descarga de rclone (Código exit: $LASTEXITCODE)" exit 1 } + $dur = (New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds + Write-Host "[+] Descarga en $([math]::Round($dur, 2))s" + Write-Host "[3/3] Verificando..." if (Test-Path $LOCAL_BACKUP) { $tam = (Get-Item $LOCAL_BACKUP).Length @@ -131,6 +133,7 @@ ps_script = <<~PSCMD.strip $res = @{ paso = "download_complete" backup_local = $LOCAL_BACKUP + backup_name = $BACKUP_NAME drive_origen = "Google Drive" nodo = "dasu-sql4" duracion = [math]::Round($dur, 2) diff --git a/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb b/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb index 50292dd6..b55f6c7c 100644 --- a/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb +++ b/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb @@ -12,6 +12,7 @@ require 'tiny_tds' require 'time' require 'json' +require 'base64' require_relative 'lib/dasu_executor' # Nombre del archivo con timestamp @@ -33,6 +34,10 @@ begin sql_user = 'UTNlarioja\\monlaricardo' sql_host = '10.0.10.200' + smb_user = 'rmonla' + smb_server = 'srv-ns8' + smb_share = 'bkps-dasuten' + smb_path = "\\\\#{smb_server}\\#{smb_share}" if sql_pass.empty? puts "[!] Error: No se pudo obtener credencial de candados" @@ -47,8 +52,24 @@ begin ) puts "[+] Conectado a SQL Server en #{sql_host} (auth: Windows)" + # Mapear share SMB con credenciales usando PowerShell + puts "\n[0/3] Mapeando share SMB #{smb_path}..." + # Escapar password para PowerShell + sql_pass_escaped = sql_pass.gsub("'", "''") + ps_map = "net use X: #{smb_path} #{sql_pass_escaped} /user:#{smb_user} /persistent:no" + map_result = client.execute("EXEC xp_cmdshell '#{ps_map}'") + map_output = [] + map_result.each { |r| map_output << r['output'] if r['output'] && !r['output'].strip.empty? } + puts " #{map_output.join("\n ")}" + puts "[+] Share mapeado en X: -> #{smb_path}" + + # Crear directorio en share si no existe + puts "\n[1/3] Verificando directorio en share..." + client.execute("EXEC master.sys.xp_create_subdir 'X:\\'").each { |r| } + puts "[+] Directorio share asegurado: X:\\" + # Generar backup diferencial directamente en share X: - puts "\n[0/2] Generando backup diferencial en share X:..." + puts "[2/3] Generando backup diferencial en share X:..." start = Time.now sql = <<~SQL.strip @@ -65,19 +86,22 @@ begin duracion = (Time.now - start).round(2) puts "[+] Backup diferencial generado en #{duracion} segundos" - # Verificar archivo en share usando módulo común - puts "\n[1/2] Verificando archivo en share..." - file_info = DasuExecutor.get_file_info_on_fenix(client, BACKUP_FILE) - - if file_info[:exists] - puts "[+] Archivo verificado: #{BACKUP_FILE} (#{file_info[:size_mb]} MB)" + # Verificar archivo en share directamente en srv-ns8 vía SSH + puts "\n[3/3] Verificando archivo en share..." + backup_unix = BACKUP_FILE.gsub('X:\\', '/mnt/ns8Disco2/bkps-dasuten/') + ls_result = `ssh rmonla@10.0.10.8 "ls -l '#{backup_unix}' 2>&1"`.strip + if $?.success? && ls_result.include?(File.basename(backup_unix)) + # Extraer tamaño del output de ls -l + file_size = ls_result.split[4].to_i + puts "[+] Archivo verificado: #{backup_unix} (#{file_size.to_i / 1024 / 1024} MB)" else - puts "[!] Error: No existe el archivo en #{BACKUP_FILE}" + puts "[!] Error: No existe el archivo en #{backup_unix}" + puts " SSH output: #{ls_result}" exit 1 end # Tamaño aproximado de la BD base - puts "\n[2/2] Información de tamaño..." + puts "\n[4/3] Información de tamaño..." begin size_info = client.execute("SELECT size_bytes = (SELECT size * 8192 FROM sys.database_files WHERE name = 'sysdasuten')") puts " Tamaño BD base: #{size_info.to_a.first['size_bytes'].to_i / 1024 / 1024} MB" diff --git a/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb b/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb index 6512183b..4f2ca3cd 100755 --- a/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb +++ b/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb @@ -12,6 +12,7 @@ require 'tiny_tds' require 'time' require 'json' +require 'base64' require_relative 'lib/dasu_executor' # Nombre del archivo con timestamp @@ -34,6 +35,10 @@ begin sql_user = 'UTNlarioja\\monlaricardo' sql_host = '10.0.10.200' + smb_user = 'rmonla' + smb_server = 'srv-ns8' + smb_share = 'bkps-dasuten' + smb_path = "\\\\#{smb_server}\\#{smb_share}" if sql_pass.empty? puts "[!] Error: No se pudo obtener credencial de candados" @@ -48,13 +53,23 @@ begin ) puts "[+] Conectado a SQL Server en #{sql_host} (auth: Windows)" + # Mapear share SMB con credenciales usando net use + puts "\n[0/3] Mapeando share SMB #{smb_path}..." + sql_pass_escaped = sql_pass.gsub("'", "''") + ps_map = "net use X: #{smb_path} #{sql_pass_escaped} /user:#{smb_user} /persistent:no" + map_result = client.execute("EXEC xp_cmdshell '#{ps_map}'") + map_output = [] + map_result.each { |r| map_output << r['output'] if r['output'] && !r['output'].strip.empty? } + puts " #{map_output.join("\n ")}" + puts "[+] Share mapeado en X: -> #{smb_path}" + # Crear directorio en share si no existe - puts "\n[0/2] Verificando directorio en share..." - client.execute("EXEC master.sys.xp_create_subdir 'X:\\'") + puts "\n[1/3] Verificando directorio en share..." + client.execute("EXEC master.sys.xp_create_subdir 'X:\\'").each { |r| } puts "[+] Directorio share asegurado: X:\\" # Generar backup comprimido directamente en share X: - puts "[1/2] Generando backup en share X: (srv-ns8)..." + puts "[2/3] Generando backup en share X: (srv-ns8)..." start = Time.now result = client.execute("BACKUP DATABASE sysdasuten TO DISK = '#{BACKUP_NS8}' WITH INIT, COMPRESSION, STATS=10") @@ -63,14 +78,17 @@ begin duracion = (Time.now - start).round(2) puts "[+] Backup generado en #{duracion} segundos" - # Verificar archivo en share usando módulo común - puts "[2/2] Verificando archivo en share..." - file_info = DasuExecutor.get_file_info_on_fenix(client, BACKUP_NS8) - - if file_info[:exists] - puts "[+] Archivo verificado: #{BACKUP_NS8} (#{file_info[:size_mb]} MB)" + # Verificar archivo en share directamente en srv-ns8 vía SSH + puts "[3/3] Verificando archivo en share..." + backup_unix = BACKUP_NS8.gsub('X:\\', '/mnt/ns8Disco2/bkps-dasuten/') + ls_result = `ssh rmonla@10.0.10.8 "ls -l '#{backup_unix}' 2>&1"`.strip + if $?.success? && ls_result.include?(File.basename(backup_unix)) + # Extraer tamaño del output de ls -l + file_size = ls_result.split[4].to_i + puts "[+] Archivo verificado: #{backup_unix} (#{file_size.to_i / 1024 / 1024} MB)" else - puts "[!] Error: No existe el archivo en #{BACKUP_NS8}" + puts "[!] Error: No existe el archivo en #{backup_unix}" + puts " SSH output: #{ls_result}" exit 1 end diff --git a/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb b/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb index f9e8f3d8..22b77e91 100644 --- a/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb +++ b/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb @@ -22,48 +22,60 @@ JSON_PATH = 'C:\temp\dron_restore_output.json' if File.exist?(DOWNLOAD_OUTPUT) output = JSON.parse(File.read(DOWNLOAD_OUTPUT)) backup_local = output['backup_local'] - backup_name = File.basename(backup_local) - LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\#{backup_name}" + backup_name = output['backup_name'] || File.basename(backup_local) + LOCAL_BACKUP = backup_local # Usar la ruta EXACTA que viene del download puts "[*] Dron: Restaurar backup DASUTEN" + puts " Nodo: dasu-sql4" + puts " Backup: #{LOCAL_BACKUP} (desde download)" + puts " Nombre: #{backup_name}" else LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\sysdasuten_FULL_*.bak" puts "[*] Dron: Restaurar backup DASUTEN (buscar más reciente)" + puts " Nodo: dasu-sql4" + puts " Backup: #{LOCAL_BACKUP} (búsqueda por patrón)" end -puts " Nodo: dasu-sql4" -puts " Backup: #{LOCAL_BACKUP}" - # PowerShell script para restore -ps_script = <<~'PSCMD'.strip +ps_script = <<~PSCMD.strip $ErrorActionPreference = "Stop" - $BACKUP_DIR = "F:\BACKUP" - $BACKUP_PATTERN = "sysdasuten_FULL_*.bak" - $JSON_PATH = "C:\temp\dron_restore_output.json" + $BACKUP_DIR = "F:\\BACKUP" + $BACKUP_NAME = "#{backup_name}" + $LOCAL_BACKUP = "$BACKUP_DIR\\$BACKUP_NAME" + $JSON_PATH = "C:\\temp\\dron_restore_output.json" - $null = New-Item -ItemType Directory -Force -Path "C:\temp" + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" - Write-Host "[0/1] Buscando backup completo más reciente..." - $backupFile = Get-ChildItem "$BACKUP_DIR\$BACKUP_PATTERN" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 - if (-not $backupFile) { - Write-Host "[!] No se encontró backup completo en $BACKUP_DIR" - exit 1 + Write-Host "[0/1] Verificando backup específico..." + Write-Host " Archivo esperado: $LOCAL_BACKUP" + + if (-not (Test-Path $LOCAL_BACKUP)) { + Write-Host "[!] No se encontró backup en $LOCAL_BACKUP" + Write-Host " Buscando backup más reciente como fallback..." + $BACKUP_PATTERN = "sysdasuten_FULL_*.bak" + $backupFile = Get-ChildItem "$BACKUP_DIR\\$BACKUP_PATTERN" | Sort-Object Name -Descending | Select-Object -First 1 + if (-not $backupFile) { + Write-Host "[!] No se encontró ningún backup en $BACKUP_DIR" + exit 1 + } + $LOCAL_BACKUP = $backupFile.FullName + Write-Host " Fallback: $LOCAL_BACKUP" } - $LOCAL_BACKUP = $backupFile.FullName - Write-Host " Archivo: $LOCAL_BACKUP" + + $backupFile = Get-Item $LOCAL_BACKUP Write-Host " Tamaño: $([math]::Round($backupFile.Length / 1MB, 2)) MB" Write-Host "[1/1] Ejecutando RESTORE DATABASE..." $start = Get-Date - $sqlcmd = "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe" + $sqlcmd = "C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\170\\Tools\\Binn\\sqlcmd.exe" $restoreSql = @" RESTORE DATABASE sysdasuten FROM DISK = '$LOCAL_BACKUP' WITH REPLACE, - MOVE 'sysdasuten' TO 'F:\DATA\sysdasuten.mdf', - MOVE 'sysdasuten_log' TO 'F:\LOG\sysdasuten_log.ldf' - "@ + MOVE 'sysdasuten' TO 'F:\\DATA\\sysdasuten.mdf', + MOVE 'sysdasuten_log' TO 'F:\\LOG\\sysdasuten_log.ldf' +"@ Write-Host " SQL: $restoreSql" @@ -82,6 +94,7 @@ ps_script = <<~'PSCMD'.strip $res = @{ paso = "restore_complete" backup_ruta = $LOCAL_BACKUP + backup_name = $BACKUP_NAME nodo = "dasu-sql4" duracion_total = $duracionRestore duracion_restore = $duracionRestore diff --git a/adn/tools/cli/drones/dasuten_upload_srv-ns8-drive.rb b/adn/tools/cli/drones/dasuten_upload_srv-ns8-drive.rb index c0c55fcd..738c4c40 100644 --- a/adn/tools/cli/drones/dasuten_upload_srv-ns8-drive.rb +++ b/adn/tools/cli/drones/dasuten_upload_srv-ns8-drive.rb @@ -16,17 +16,28 @@ require 'shellwords' REMOTE_DRIVE = "rmonla-GDrive:drive_bkps-dasu" -# Leer ruta del backup desde el output de la transferencia +# Archivos de output de los pasos anteriores TRANSFER_OUTPUT = '/tmp/dron_transfer_output.json' -if File.exist?(TRANSFER_OUTPUT) +EXPORT_OUTPUT = '/tmp/dron_export_output.json' + +# Determinar cuál es el más reciente (si existen) +archivos_output = [TRANSFER_OUTPUT, EXPORT_OUTPUT].select { |f| File.exist?(f) } +output_reciente = archivos_output.max_by { |f| File.mtime(f) } + +if output_reciente == TRANSFER_OUTPUT output = JSON.parse(File.read(TRANSFER_OUTPUT)) - BACKUP_LOCAL = output['backup_local'] + BACKUP_LOCAL = output['backup_local'] || output['backup_ruta_unix'] BACKUP_NAME = File.basename(BACKUP_LOCAL) - puts "[*] Dron: Upload a Google Drive (DIFERENCIAL)" + puts "[*] Dron: Upload a Google Drive (Desde TRANSFERENCIA)" +elsif output_reciente == EXPORT_OUTPUT + output = JSON.parse(File.read(EXPORT_OUTPUT)) + BACKUP_LOCAL = output['backup_ruta_unix'] + BACKUP_NAME = File.basename(BACKUP_LOCAL) + puts "[*] Dron: Upload a Google Drive (Desde EXPORTACIÓN DIRECTA)" else BACKUP_LOCAL = "/var/tmp/sysdasuten_compressed_ADN.bak" BACKUP_NAME = "sysdasuten_compressed_ADN_#{Time.now.strftime('%Y%m%d_%H%M%S')}.bak" - puts "[*] Dron: Upload a Google Drive (COMPLETO)" + puts "[*] Dron: Upload a Google Drive (COMPLETO FALLBACK)" end puts " Nodo: srv-ns8" diff --git a/adn/tools/cli/drones/lib/dasu_executor.rb b/adn/tools/cli/drones/lib/dasu_executor.rb index e468fb09..6ea0bd7b 100644 --- a/adn/tools/cli/drones/lib/dasu_executor.rb +++ b/adn/tools/cli/drones/lib/dasu_executor.rb @@ -108,8 +108,15 @@ module DasuExecutor # Ejecutar script ssh_cmd = "ssh srv-dasu 'bash #{remote_path}'" - output = `#{ssh_cmd}` - output = output.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if output + output_buffer = "" + IO.popen(ssh_cmd, err: [:child, :out]) do |io| + io.each_line do |line| + safe_line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') + print safe_line + output_buffer += safe_line + end + end + output = output_buffer # Limpieza bash_file.unlink diff --git a/adn/tools/cli/limpiar_backups_dasu.rb b/adn/tools/cli/limpiar_backups_dasu.rb new file mode 100644 index 00000000..bb020730 --- /dev/null +++ b/adn/tools/cli/limpiar_backups_dasu.rb @@ -0,0 +1,78 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== LIMPIEZA DE BACKUPS EN DASU-SQL4 ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\limpieza_output.json" + + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" + + # Listar todos los backups + $backups = Get-ChildItem "F:\\BACKUP\\sysdasuten*.bak" | Sort-Object LastWriteTime -Descending + + Write-Host "=== ANTES DE LIMPIEZA ===" + Write-Host "Total archivos: $($backups.Count)" + $totalSize = ($backups | Measure-Object -Property Length -Sum).Sum / 1MB + Write-Host "Total tamaño: $([math]::Round($totalSize, 2)) MB" + + Write-Host "`nArchivos:" + $backups | ForEach-Object { + $sizeMB = [math]::Round($_.Length / 1MB, 2) + Write-Host " $($_.Name) - ${sizeMB} MB - $($_.LastWriteTime)" + } + + # Mantener solo el más reciente + if ($backups.Count -gt 1) { + $aEliminar = $backups | Select-Object -Skip 1 + + Write-Host "`n=== ELIMINANDO $($aEliminar.Count) ARCHIVOS ===" + $aEliminar | ForEach-Object { + Write-Host " Eliminando: $($_.Name)..." + Remove-Item $_.FullName -Force + } + + # Verificar resultado + $restantes = Get-ChildItem "F:\\BACKUP\\sysdasuten*.bak" | Sort-Object LastWriteTime -Descending + Write-Host "`n=== DESPUÉS DE LIMPIEZA ===" + Write-Host "Total archivos: $($restantes.Count)" + $totalSize = ($restantes | Measure-Object -Property Length -Sum).Sum / 1MB + Write-Host "Total tamaño: $([math]::Round($totalSize, 2)) MB" + + $restantes | ForEach-Object { + $sizeMB = [math]::Round($_.Length / 1MB, 2) + Write-Host " $($_.Name) - ${sizeMB} MB - $($_.LastWriteTime)" + } + + $resultado = @{ + eliminados = $aEliminar.Count + restantes = $restantes.Count + espacio_libre_mb = [math]::Round($totalSize, 2) + timestamp = (Get-Date -Format "o") + } + } else { + Write-Host "`n=== NO HAY NADA QUE ELIMINAR ===" + $resultado = @{ + eliminados = 0 + restantes = $backups.Count + timestamp = (Get-Date -Format "o") + } + } + + $resultado | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: 'C:\temp\limpieza_output.json') +resultado = DasuExecutor.read_json_from_dasu('C:\temp\limpieza_output.json') + +puts "\n=== RESUMEN ===" +puts " Archivos eliminados: #{resultado['eliminados']}" +puts " Archivos restantes: #{resultado['restantes']}" +puts " Espacio usado: #{resultado['espacio_libre_mb']} MB" if resultado['espacio_libre_mb'] + +puts "\n✅ Limpieza completada" diff --git a/adn/tools/cli/limpiar_fenix_orig.rb b/adn/tools/cli/limpiar_fenix_orig.rb new file mode 100644 index 00000000..8f031a9b --- /dev/null +++ b/adn/tools/cli/limpiar_fenix_orig.rb @@ -0,0 +1,61 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'tiny_tds' +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICANDO BACKUPS EN FENIX (E:\\BK_SQL\\sysdasuten\\) ===" +puts "" + +pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip +sql_user = 'UTNlarioja\\monlaricardo' +sql_host = '10.0.10.200' + +client = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' +) + +# Listar backups en E:\BK_SQL\sysdasuten\ +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + + $path = "E:\\BK_SQL\\sysdasuten" + + if (Test-Path $path) { + $backups = Get-ChildItem "$path\\sysdasuten*.bak" | Sort-Object LastWriteTime -Descending + + Write-Host "Archivos encontrados:" + $backups | ForEach-Object { + $sizeMB = [math]::Round($_.Length / 1MB, 2) + Write-Host " $($_.Name) - ${sizeMB} MB - $($_.LastWriteTime)" + } + + Write-Host "`nTotal archivos: $($backups.Count)" + $totalSize = ($backups | Measure-Object -Property Length -Sum).Sum / 1MB + Write-Host "Total tamaño: $([math]::Round($totalSize, 2)) MB" + + # Si hay más de 1, eliminar los viejos + if ($backups.Count -gt 1) { + $aEliminar = $backups | Select-Object -Skip 1 + Write-Host "`n=== ELIMINANDO $($aEliminar.Count) ARCHIVOS ===" + $aEliminar | ForEach-Object { + Write-Host " Eliminando: $($_.Name)" + Remove-Item $_.FullName -Force + } + + $restantes = Get-ChildItem "$path\\sysdasuten*.bak" | Sort-Object LastWriteTime -Descending + Write-Host "`nRestantes: $($restantes.Count)" + } + } else { + Write-Host "El directorio no existe" + } +PSCMD + +result = client.execute("EXEC xp_cmdshell 'powershell -Command \"' + ps_script.gsub('\"', '\"\"') + '\"'") +result.each { |r| puts r['output'] if r['output'] } + +client.close diff --git a/adn/tools/cli/restaurar_completo_dasu.rb b/adn/tools/cli/restaurar_completo_dasu.rb new file mode 100644 index 00000000..14dd5d60 --- /dev/null +++ b/adn/tools/cli/restaurar_completo_dasu.rb @@ -0,0 +1,157 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# Restauración COMPLETA desde backup local en dasu-sql4 +# ========================================================== + +require_relative 'drones/lib/dasu_executor' + +puts "=== RESTAURACIÓN COMPLETA DESDE BACKUP LOCAL ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +# Script PowerShell para restore (sin drop, la BD ya no existe) +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\restaurar_completo_output.json" + + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" + + # Encontrar el backup más reciente + Write-Host "[0/5] Buscando backup más reciente..." + $backupFile = Get-ChildItem "F:\\BACKUP\\sysdasuten_FULL_*.bak" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if (-not $backupFile) { + Write-Host "[!] No se encontró backup en F:\\BACKUP\\" + exit 1 + } + $BACKUP_FILE = $backupFile.FullName + Write-Host " Backup: $($backupFile.Name)" + Write-Host " Tamaño: $([math]::Round($backupFile.Length / 1MB, 2)) MB" + + # Ver archivos lógicos en el backup + Write-Host "`n[1/5] Leyendo archivos lógicos del backup..." + $connString = "Server=localhost;Database=master;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $cmd = New-Object System.Data.SqlClient.SqlCommand("RESTORE FILELISTONLY FROM DISK = '$BACKUP_FILE'", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + + $logicalNames = @{} + if ($ds.Tables[0].Rows.Count -gt 0) { + Write-Host " Archivos en el backup:" + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " Tipo: $($_.Type) | Lógico: $($_.LogicalName) | Físico: $($_.PhysicalName)" + if ($_.Type -eq 'D') { + $logicalNames['data'] = $_.LogicalName + } elseif ($_.Type -eq 'L') { + $logicalNames['log'] = $_.LogicalName + } + } + } + + Write-Host "`n[2/5] Restaurando desde backup..." + Write-Host " Backup: $BACKUP_FILE" + Write-Host " LogicalName data: $($logicalNames['data'])" + Write-Host " LogicalName log: $($logicalNames['log'])" + + $restoreSql = @" + RESTORE DATABASE sysdasuten + FROM DISK = '$BACKUP_FILE' + WITH REPLACE, RECOVERY, + MOVE '$($logicalNames['data'])' TO 'F:\\DATA\\sysdasuten.mdf', + MOVE '$($logicalNames['log'])' TO 'F:\\LOG\\sysdasuten_log.ldf' +"@ + + Write-Host " SQL: $restoreSql" + + $cmd = New-Object System.Data.SqlClient.SqlCommand($restoreSql, $conn) + $cmd.CommandTimeout = 600 + + $start = Get-Date + try { + $cmd.ExecuteNonQuery() | Out-Null + $duracion = [math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds, 2) + Write-Host " Restore completado en ${duracion}s" + } catch { + Write-Host " ERROR en restore: $_" + exit 1 + } + + Write-Host "`n[3/5] Poniendo BD en línea..." + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + ALTER DATABASE sysdasuten SET MULTI_USER + ", $conn) + $cmd.ExecuteNonQuery() | Out-Null + Write-Host " BD en línea" + + # Verificar restore + Write-Host "`n[4/5] Verificando cantidad de registros..." + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT COUNT(*) as total FROM sysdasuten.dbo.BonosAfiliado + ", $conn) + $total = $cmd.ExecuteScalar() + Write-Host " Total registros en BonosAfiliado: $total" + + Write-Host "`n[5/5] Verificando IDs y fechas..." + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT MAX(idsolicitud) as max_id FROM sysdasuten.dbo.BonosAfiliado + ", $conn) + $maxId = $cmd.ExecuteScalar() + Write-Host " MAX(idsolicitud): $maxId" + + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT MAX(fecha) as ultima_fecha FROM sysdasuten.dbo.BonosAfiliado + ", $conn) + $ultimaFecha = $cmd.ExecuteScalar() + Write-Host " Última fecha: $ultimaFecha" + + # Verificar registros con idsolicitud = 0 vs > 0 + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + SUM(CASE WHEN idsolicitud = 0 THEN 1 ELSE 0 END) as con_cero, + SUM(CASE WHEN idsolicitud > 0 THEN 1 ELSE 0 END) as con_mayor_cero + FROM sysdasuten.dbo.BonosAfiliado + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " Con idsolicitud=0: $($row.con_cero)" + Write-Host " Con idsolicitud>0: $($row.con_mayor_cero)" + } + + $conn.Close() + + $resultado = @{ + paso = "restore_completo" + backup = $BACKUP_FILE + total_registros = $total + max_idsolicitud = $maxId + ultima_fecha = $ultimaFecha + timestamp = (Get-Date -Format "o") + } + + $resultado | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +puts "\n[1/2] Ejecutando restauración..." +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: 'C:\temp\restaurar_completo_output.json') + +puts "\n[2/2] Leyendo resultado..." +begin + resultado = DasuExecutor.read_json_from_dasu('C:\temp\restaurar_completo_output.json') + puts "\n=== RESULTADO ===" + puts " Backup: #{resultado['backup']}" + puts " Total registros: #{resultado['total_registros']}" + puts " MAX(idsolicitud): #{resultado['max_idsolicitud']}" + puts " Última fecha: #{resultado['ultima_fecha']}" +rescue => e + puts "[!] Error leyendo resultado: #{e.message}" +end + +puts "\n✅ Restauración completada" diff --git a/adn/tools/cli/verificacion_final_comparativa.rb b/adn/tools/cli/verificacion_final_comparativa.rb new file mode 100644 index 00000000..6f704728 --- /dev/null +++ b/adn/tools/cli/verificacion_final_comparativa.rb @@ -0,0 +1,205 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# Verificación Final Comparativa - FENIX vs DASU-SQL4 +# ========================================================== + +require 'tiny_tds' +require_relative 'drones/lib/dasu_executor' + +puts "═" * 70 +puts " VERIFICACIÓN FINAL - FENIX (PRODUCCIÓN) vs DASU-SQL4" +puts " Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "═" * 70 + +# ========================================== +# 1. FENIX - Obtener datos +# ========================================== +puts "\n[1] Conectando a FENIX (PRODUCCIÓN)..." + +begin + pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip + sql_user = 'UTNlarioja\\monlaricardo' + sql_host = '10.0.10.200' + + client_fenix = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' + ) + puts "[+] Conectado a FENIX" + + # Consultas a FENIX + puts "\n[2] Obteniendo datos de FENIX..." + + fenix_data = {} + + # Total BonosAfiliado + result = client_fenix.execute("SELECT COUNT(*) as c FROM BonosAfiliado") + fenix_data[:total_bonos] = result.first['c'] + + # Registros por día (últimos 5 días) + result = client_fenix.execute(" + SELECT TOP 5 + CAST(fecha AS DATE) as dia, + COUNT(*) as c + FROM BonosAfiliado + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ") + fenix_data[:por_dia] = {} + result.each { |r| fenix_data[:por_dia][r['dia']] = r['c'] } + + # Última fecha + result = client_fenix.execute("SELECT MAX(fecha) as ultima FROM BonosAfiliado") + fenix_data[:ultima_fecha] = result.first['ultima'] + + # MAX(idsolicitud) + result = client_fenix.execute("SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado") + fenix_data[:max_idsolicitud] = result.first['max_id'] + + # AutorizacionBonosExcepcion + result = client_fenix.execute("SELECT COUNT(*) as c FROM AutorizacionBonosExcepcion") + fenix_data[:total_aut] = result.first['c'] + + result = client_fenix.execute("SELECT MAX(fechasolicitud) as ultima FROM AutorizacionBonosExcepcion") + fenix_data[:ultima_fecha_aut] = result.first['ultima'] + + client_fenix.close + + puts " Total BonosAfiliado: #{fenix_data[:total_bonos]}" + puts " Última fecha: #{fenix_data[:ultima_fecha]}" + puts " MAX(idsolicitud): #{fenix_data[:max_idsolicitud]}" + puts " Total AutorizacionBonosExcepcion: #{fenix_data[:total_aut]}" + +rescue => e + puts "[!] Error conectando a FENIX: #{e.message}" + fenix_data = {} +end + +# ========================================== +# 2. DASU-SQL4 - Obtener datos +# ========================================== +puts "\n[3] Conectando a DASU-SQL4..." + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\verificacion_final_output.json" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $data = @{} + + # Total BonosAfiliado + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) as c FROM BonosAfiliado", $conn) + $data.total_bonos = $cmd.ExecuteScalar() + + # Por día + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 + CONVERT(varchar, CAST(fecha AS DATE), 120) as dia, + COUNT(*) as c + FROM BonosAfiliado + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $por_dia = @{} + $ds.Tables[0].Rows | ForEach-Object { $por_dia[$($_.dia)] = $($_.c) } + $data.por_dia = $por_dia + + # Última fecha + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fecha) as ultima FROM BonosAfiliado", $conn) + $data.ultima_fecha = $cmd.ExecuteScalar() + + # MAX(idsolicitud) + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado", $conn) + $data.max_idsolicitud = $cmd.ExecuteScalar() + + # AutorizacionBonosExcepcion + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) as c FROM AutorizacionBonosExcepcion", $conn) + $data.total_aut = $cmd.ExecuteScalar() + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fechasolicitud) as ultima FROM AutorizacionBonosExcepcion", $conn) + $data.ultima_fecha_aut = $cmd.ExecuteScalar() + + $conn.Close() + + $data | ConvertTo-Json -Depth 5 | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +begin + DasuExecutor.execute_ps_on_dasu(ps_script, output_path: 'C:\temp\verificacion_final_output.json') + dasu_data = DasuExecutor.read_json_from_dasu('C:\temp\verificacion_final_output.json') + puts "[+] Conectado a DASU-SQL4" + + puts " Total BonosAfiliado: #{dasu_data['total_bonos']}" + puts " Última fecha: #{dasu_data['ultima_fecha']}" + puts " MAX(idsolicitud): #{dasu_data['max_idsolicitud']}" + puts " Total AutorizacionBonosExcepcion: #{dasu_data['total_aut']}" + +rescue => e + puts "[!] Error consultando DASU-SQL4: #{e.message}" + dasu_data = {} +end + +# ========================================== +# 3. COMPARAR +# ========================================== +puts "\n" + "═" * 70 +puts " COMPARACIÓN" +puts "═" * 70 + +def comparar(nombre, fenix, dasu, formato = nil) + fenix_val = fenix.nil? ? "N/A" : (formato ? formato.call(fenix) : fenix.to_s) + dasu_val = dasu.nil? ? "N/A" : (formato ? formato.call(dasu) : dasu.to_s) + + coincide = fenix.to_s == dasu.to_s + estado = coincide ? "✅" : "⚠️" + + puts " #{estado} #{nombre}:" + puts " FENIX: #{fenix_val}" + puts " DASU: #{dasu_val}" + + coincide +end + +todos_coinciden = true + +# Comparar totales +todos_coinciden &= comparar("Total BonosAfiliado", fenix_data[:total_bonos], dasu_data['total_bonos']) + +# Comparar última fecha +todos_coinciden &= comparar("Última fecha", fenix_data[:ultima_fecha], dasu_data['ultima_fecha']) + +# Comparar MAX(idsolicitud) +todos_coinciden &= comparar("MAX(idsolicitud)", fenix_data[:max_idsolicitud], dasu_data['max_idsolicitud']) + +# Comparar total autorizaciones +todos_coinciden &= comparar("Total AutorizacionBonosExcepcion", fenix_data[:total_aut], dasu_data['total_aut']) + +# Comparar por día +puts "\n Registros por día (últimos 5 días):" +if fenix_data[:por_dia] && dasu_data['por_dia'] + fenix_data[:por_dia].each do |dia, count| + dasu_count = dasu_data['por_dia'][dia.to_s] + coincide = count == dasu_count + estado = coincide ? "✅" : "⚠️" + puts " #{estado} #{dia}: FENIX=#{count} | DASU=#{dasu_count}" + todos_coinciden = false unless coincide + end +end + +puts "\n" + "═" * 70 +if todos_coinciden + puts " ✅ TODOS LOS DATOS COINCIDEN - EL SISTEMA ESTÁ ACTUALIZADO" +else + puts " ⚠️ HAY DIFERENCIAS - REVISAR" +end +puts "═" * 70 diff --git a/adn/tools/cli/verificar_abril_2026.rb b/adn/tools/cli/verificar_abril_2026.rb new file mode 100644 index 00000000..1f994229 --- /dev/null +++ b/adn/tools/cli/verificar_abril_2026.rb @@ -0,0 +1,68 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Registros de ABRIL 2026 ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] Registros de abril 2026 ordenados por fecha:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 30 idsolicitud, fecha, codigoafiliado, dependencia + FROM BonosAfiliado + WHERE fecha >= '2026-04-01' + ORDER BY fecha DESC, idsolicitud DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " ID: $($_.idsolicitud) | fecha: $($_.fecha) | afiliado: $($_.codigoafiliado)" + } + + Write-Host "`n[2] Cantidad de registros por día (abril 2026):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT CAST(fecha AS DATE) as dia, COUNT(*) as c + FROM BonosAfiliado + WHERE fecha >= '2026-04-01' + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.dia): $($_.c) registros" + } + + Write-Host "`n[3] IDs mínimos y máximos por día (abril 2026):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + CAST(fecha AS DATE) as dia, + MIN(idsolicitud) as min_id, + MAX(idsolicitud) as max_id, + COUNT(*) as c + FROM BonosAfiliado + WHERE fecha >= '2026-04-01' + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.dia): IDs [$($_.min_id) - $($_.max_id)] | c: $($_.c)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_backup_dasu.rb b/adn/tools/cli/verificar_backup_dasu.rb new file mode 100644 index 00000000..ddbec190 --- /dev/null +++ b/adn/tools/cli/verificar_backup_dasu.rb @@ -0,0 +1,100 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# Verificar backup en DASU-SQL4 - qué archivo hay y qué fecha tiene +# ========================================================== + +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICANDO BACKUP EN DASU-SQL4 ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\backup_info_output.json" + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" + + # Listar backups en F:\\BACKUP + Write-Host "=== ARCHIVOS EN F:\\BACKUP ===" + $backups = Get-ChildItem "F:\\BACKUP\\*.bak" | Sort-Object LastWriteTime -Descending + $backups | ForEach-Object { + Write-Host " Archivo: $($_.Name)" + Write-Host " Tamaño: $([math]::Round($_.Length / 1MB, 2)) MB" + Write-Host " LastWriteTime: $($_.LastWriteTime)" + Write-Host " CreationTime: $($_.CreationTime)" + } + + # Si hay backup, verificar fecha de los datos dentro + if ($backups.Count -gt 0) { + $backupMasReciente = $backups[0] + Write-Host "`n=== INFORMACIÓN DEL BACKUP MÁS RECIENTE ===" + Write-Host " Archivo: $($backupMasReciente.Name)" + + # Conectar a SQL y verificar fecha máxima en BonosAfiliado + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fecha) as ultima_fecha FROM [BonosAfiliado]", $conn) + try { + $ultimaFecha = $cmd.ExecuteScalar() + Write-Host " ÚLTIMA FECHA EN BonosAfiliado: $ultimaFecha" + } catch { + Write-Host " ERROR al obtener fecha: $_" + } + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [BonosAfiliado] WHERE fecha >= '2026-04-15'", $conn) + try { + $count15 = $cmd.ExecuteScalar() + Write-Host " Registros el 2026-04-15: $count15" + } catch { + Write-Host " ERROR al contar registros del 15: $_" + } + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [BonosAfiliado] WHERE fecha >= '2026-04-14'", $conn) + try { + $count14 = $cmd.ExecuteScalar() + Write-Host " Registros desde 2026-04-14: $count14" + } catch { + Write-Host " ERROR al contar registros desde 14: $_" + } + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [BonosAfiliado] WHERE fecha >= '2026-04-13'", $conn) + try { + $count13 = $cmd.ExecuteScalar() + Write-Host " Registros desde 2026-04-13: $count13" + } catch { + Write-Host " ERROR al contar registros desde 13: $_" + } + + $conn.Close() + } + + # Verificar fecha de restore en msdb + Write-Host "`n=== FECHA DE RESTORE EN msdb ===" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 5 destination_database_name, restore_date, backup_start_date FROM msdb.dbo.restorehistory WHERE destination_database_name = 'sysdasuten' ORDER BY restore_date DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + + if ($ds.Tables[0].Rows.Count -gt 0) { + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " Restore: $($_.restore_date)" + Write-Host " Backup de: $($_.backup_start_date)" + Write-Host " BD: $($_.destination_database_name)" + } + } else { + Write-Host " No hay registros de restore" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_backup_info.rb b/adn/tools/cli/verificar_backup_info.rb new file mode 100644 index 00000000..79db56ba --- /dev/null +++ b/adn/tools/cli/verificar_backup_info.rb @@ -0,0 +1,106 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'tiny_tds' +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICANDO BACKUP EN DASU-SQL4 ===" +puts "" + +# ========================================== +# 1. FENIX - Verificar datos actuales +# ========================================== +puts "[1] FENIX - Estado actual de BonosAfiliado:" + +pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip +sql_user = 'UTNlarioja\\monlaricardo' +sql_host = '10.0.10.200' + +client_fenix = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' +) + +# Última fecha y registros del 15 +result = client_fenix.execute("SELECT MAX(fecha) as ultima FROM BonosAfiliado") +puts " Última fecha: #{result.first['ultima']}" + +result = client_fenix.execute("SELECT COUNT(*) as c FROM BonosAfiliado WHERE fecha = '2026-04-15'") +puts " Registros el 2026-04-15: #{result.first['c']}" + +# Ver IDs máximos +result = client_fenix.execute("SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado") +puts " ID máximo en FENIX: #{result.first['max_id']}" + +client_fenix.close + +# ========================================== +# 2. DASU-SQL4 - Verificar datos del backup +# ========================================== +puts "\n[2] DASU-SQL4 - Datos del backup restaurado:" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host " Última fecha en BonosAfiliado:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fecha) as ultima FROM BonosAfiliado", $conn) + try { + $ultima = $cmd.ExecuteScalar() + Write-Host " $ultima" + } catch { + Write-Host " ERROR: $_" + } + + Write-Host " Registros el 2026-04-15:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) as c FROM BonosAfiliado WHERE fecha = '2026-04-15'", $conn) + try { + $c = $cmd.ExecuteScalar() + Write-Host " $c" + } catch { + Write-Host " ERROR: $_" + } + + Write-Host " ID máximo en BonosAfiliado:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado", $conn) + try { + $maxId = $cmd.ExecuteScalar() + Write-Host " $maxId" + } catch { + Write-Host " ERROR: $_" + } + + # Ver fecha de restore en msdb + Write-Host "`n Último restore:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 1 restore_date, backup_start_date FROM msdb.dbo.restorehistory WHERE destination_database_name = 'sysdasuten' ORDER BY restore_date DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " Restore date: $($row.restore_date)" + Write-Host " Backup start: $($row.backup_start_date)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n[3] ARCHIVOS DE BACKUP EN DASU-SQL4:" +ps_script = <<~PSCMD.strip + Get-ChildItem "F:\\BACKUP\\*.bak" | ForEach-Object { + Write-Host " $($_.Name)" + Write-Host " LastWriteTime: $($_.LastWriteTime)" + Write-Host " CreationTime: $($_.CreationTime)" + } +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_bonos_fenix_dasu.rb b/adn/tools/cli/verificar_bonos_fenix_dasu.rb new file mode 100644 index 00000000..07584115 --- /dev/null +++ b/adn/tools/cli/verificar_bonos_fenix_dasu.rb @@ -0,0 +1,214 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# Verificación específica de BONOS - FENIX vs DASU-SQL4 +# ========================================================== + +require 'tiny_tds' +require 'json' +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICACIÓN DE BONOS (venta de bonos) ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +# ========================================== +# 1. FENIX - BonosAfiliado y AutorizacionBonosExcepcion +# ========================================== +puts "\n[1] FENIX (PRODUCCIÓN) - BonosAfiliado..." + +pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip +sql_user = 'UTNlarioja\\monlaricardo' +sql_host = '10.0.10.200' + +client_fenix = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' +) + +# BonosAfiliado - verificar columna de fecha +puts " Buscando columna de fecha en BonosAfiliado..." +cols = client_fenix.execute(" + SELECT COLUMN_NAME, DATA_TYPE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'BonosAfiliado' + AND DATA_TYPE IN ('datetime', 'datetime2', 'date', 'smalldatetime') +").to_a + +puts " Columnas fecha encontradas: #{cols.map { |c| "#{c['COLUMN_NAME']} (#{c['DATA_TYPE']})" }.join(', ')}" + +# Usar la primera columna fecha encontrada +if cols.any? + col_fecha = cols.first['COLUMN_NAME'] + puts " Usando columna: #{col_fecha}" + + # Última fecha exacta + ultima = client_fenix.execute("SELECT MAX(#{col_fecha}) as ultima FROM BonosAfiliado").first['ultima'] + puts " Última fecha: #{ultima}" + + # Registros desde abril 13 + count_13 = client_fenix.execute("SELECT COUNT(*) as c FROM BonosAfiliado WHERE #{col_fecha} >= '2026-04-13'").first['c'] + puts " Registros desde 2026-04-13: #{count_13}" + + # Registros por día desde abril 13 + puts "\n Registros por día (desde 2026-04-13):" + por_dia = client_fenix.execute(" + SELECT CAST(#{col_fecha} AS DATE) as dia, COUNT(*) as cantidad + FROM BonosAfiliado + WHERE #{col_fecha} >= '2026-04-13' + GROUP BY CAST(#{col_fecha} AS DATE) + ORDER BY dia DESC + ") + por_dia.each { |r| puts " #{r['dia']}: #{r['cantidad']} registros" } + + # Últimos 10 registros + puts "\n Últimos 10 registros de BonosAfiliado:" + ultimos = client_fenix.execute("SELECT TOP 10 * FROM BonosAfiliado ORDER BY #{col_fecha} DESC").to_a + ultimos.each_with_index do |r, i| + puts " #{i+1}. #{col_fecha}=#{r[col_fecha]} - #{r.select { |k,v| k != col_fecha }.to_a.first(3).map { |k,v| "#{k}: #{v}" }.join(', ')}" + end +end + +# AutorizacionBonosExcepcion +puts "\n[2] FENIX (PRODUCCIÓN) - AutorizacionBonosExcepcion..." + +cols = client_fenix.execute(" + SELECT COLUMN_NAME, DATA_TYPE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'AutorizacionBonosExcepcion' + AND DATA_TYPE IN ('datetime', 'datetime2', 'date', 'smalldatetime') +").to_a + +if cols.any? + col_fecha = cols.first['COLUMN_NAME'] + puts " Usando columna: #{col_fecha}" + + ultima = client_fenix.execute("SELECT MAX(#{col_fecha}) as ultima FROM AutorizacionBonosExcepcion").first['ultima'] + puts " Última fecha: #{ultima}" + + count_13 = client_fenix.execute("SELECT COUNT(*) as c FROM AutorizacionBonosExcepcion WHERE #{col_fecha} >= '2026-04-13'").first['c'] + puts " Registros desde 2026-04-13: #{count_13}" + + puts "\n Registros por día (desde 2026-04-13):" + por_dia = client_fenix.execute(" + SELECT CAST(#{col_fecha} AS DATE) as dia, COUNT(*) as cantidad + FROM AutorizacionBonosExcepcion + WHERE #{col_fecha} >= '2026-04-13' + GROUP BY CAST(#{col_fecha} AS DATE) + ORDER BY dia DESC + ") + por_dia.each { |r| puts " #{r['dia']}: #{r['cantidad']} registros" } + + puts "\n Últimos 10 registros:" + ultimos = client_fenix.execute("SELECT TOP 10 * FROM AutorizacionBonosExcepcion ORDER BY #{col_fecha} DESC").to_a + ultimos.each_with_index do |r, i| + puts " #{i+1}. #{col_fecha}=#{r[col_fecha]} - #{r.to_a.first(5).map { |k,v| "#{k}: #{v}" }.join(', ')}" + end +end + +client_fenix.close + +# ========================================== +# 2. DASU-SQL4 - Mismas consultas +# ========================================== +puts "\n[3] DASU-SQL4 - BonosAfiliado..." + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\bonos_dasu_output.json" + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" + + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + # BonosAfiliado + Write-Host " Buscando columna de fecha en BonosAfiliado..." + $cmdCol = New-Object System.Data.SqlClient.SqlCommand("SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'BonosAfiliado' AND DATA_TYPE IN ('datetime', 'datetime2', 'date', 'smalldatetime')", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmdCol) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $cols = $ds.Tables[0].Rows + + if ($cols.Count -gt 0) { + $colFecha = $cols[0].COLUMN_NAME + Write-Host " Usando columna: $colFecha" + + # Última fecha + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX([$colFecha]) as ultima FROM [BonosAfiliado]", $conn) + $ultima = $cmd.ExecuteScalar() + Write-Host " Última fecha: $ultima" + + # Registros desde abril 13 + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [BonosAfiliado] WHERE [$colFecha] >= '2026-04-13'", $conn) + $count13 = $cmd.ExecuteScalar() + Write-Host " Registros desde 2026-04-13: $count13" + + # Por día + Write-Host " Registros por día:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT CAST([$colFecha] AS DATE) as dia, COUNT(*) as cantidad FROM [BonosAfiliado] WHERE [$colFecha] >= '2026-04-13' GROUP BY CAST([$colFecha] AS DATE) ORDER BY dia DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { Write-Host " $($_.dia): $($_.cantidad) registros" } + + # Últimos 10 + Write-Host " Últimos 10 registros:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 10 * FROM [BonosAfiliado] ORDER BY [$colFecha] DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + for ($i = 0; $i -lt $ds.Tables[0].Rows.Count; $i++) { + $row = $ds.Tables[0].Rows[$i] + Write-Host " $($i+1). $($colFecha)=$($row.$colFecha)" + } + } + + # AutorizacionBonosExcepcion + Write-Host "`n[4] DASU-SQL4 - AutorizacionBonosExcepcion..." + $cmdCol = New-Object System.Data.SqlClient.SqlCommand("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'AutorizacionBonosExcepcion' AND DATA_TYPE IN ('datetime', 'datetime2', 'date', 'smalldatetime')", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmdCol) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $cols = $ds.Tables[0].Rows + + if ($cols.Count -gt 0) { + $colFecha = $cols[0].COLUMN_NAME + Write-Host " Usando columna: $colFecha" + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX([$colFecha]) as ultima FROM [AutorizacionBonosExcepcion]", $conn) + $ultima = $cmd.ExecuteScalar() + Write-Host " Última fecha: $ultima" + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [AutorizacionBonosExcepcion] WHERE [$colFecha] >= '2026-04-13'", $conn) + $count13 = $cmd.ExecuteScalar() + Write-Host " Registros desde 2026-04-13: $count13" + + Write-Host " Registros por día:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT CAST([$colFecha] AS DATE) as dia, COUNT(*) as cantidad FROM [AutorizacionBonosExcepcion] WHERE [$colFecha] >= '2026-04-13' GROUP BY CAST([$colFecha] AS DATE) ORDER BY dia DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { Write-Host " $($_.dia): $($_.cantidad) registros" } + + Write-Host " Últimos 10 registros:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 10 * FROM [AutorizacionBonosExcepcion] ORDER BY [$colFecha] DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + for ($i = 0; $i -lt $ds.Tables[0].Rows.Count; $i++) { + $row = $ds.Tables[0].Rows[$i] + Write-Host " $($i+1). $($colFecha)=$($row.$colFecha)" + } + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_datos_fenix_dasu.rb b/adn/tools/cli/verificar_datos_fenix_dasu.rb new file mode 100644 index 00000000..17d2d0e6 --- /dev/null +++ b/adn/tools/cli/verificar_datos_fenix_dasu.rb @@ -0,0 +1,319 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/verificar_datos_fenix_dasu.rb +# ========================================================== +# Verificación minuciosa de datos entre FENIX (producción) +# y DASU-SQL4 (nuevo sistema) - desde abril 13 +# ========================================================== + +require 'tiny_tds' +require 'json' +require 'base64' +require_relative 'drones/lib/dasu_executor' + +puts "=== COMPARACIÓN FENIX vs DASU-SQL4 ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +# ========================================== +# 1. CONEXIÓN A FENIX (PRODUCCIÓN) +# ========================================== +puts "\n[1] Conectando a srvv-fenix (PRODUCCIÓN)..." + +# Autorizar candados primero +system("ruby adn/tools/candados/candados.rb authorize >/dev/null 2>&1") + +# Obtener credencial de candados +pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip + +if pass_cmd.empty? + puts "[!] Error: No se pudo obtener credencial de candados" + exit 1 +end + +sql_user = 'UTNlarioja\\monlaricardo' +sql_host = '10.0.10.200' + +begin + client_fenix = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' + ) + puts "[+] Conectado a srvv-fenix" +rescue => e + puts "[!] Error conectando a fenix: #{e.message}" + exit 1 +end + +# ========================================== +# 2. TABLAS PRINCIPALES PARA VERIFICAR +# ========================================== +# El usuario mencionó "venta de bonos" como tarea repetitiva +# Verificamos tablas relacionadas con bonos y afiliados + +tablas_principales = [ + 'Bonos', + 'BonosAfiliado', + 'AutorizacionGasto', + 'AutorizacionBonosExcepcion', + 'afiliados', + 'ControlPrestaciones', + 'usuarios', + 'transacciones' +] + +puts "\n[2] Verificando tablas principales..." + +# ========================================== +# 3. OBTENER DATOS DE FENIX +# ========================================== +datos_fenix = {} + +tablas_principales.each do |tabla| + begin + # Verificar si existe + existe = client_fenix.execute(" + SELECT TOP 1 1 + FROM sys.objects + WHERE name = '#{tabla}' AND type = 'U' + ").to_a.any? + + unless existe + puts " #{tabla}: NO EXISTE" + next + end + + # Buscar columna de fecha + cols = client_fenix.execute(" + SELECT TOP 1 COLUMN_NAME, DATA_TYPE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '#{tabla}' + AND DATA_TYPE IN ('datetime', 'datetime2', 'date', 'smalldatetime') + ").to_a + + if cols.empty? + # Sin columna de fecha, solo contar total + count_total = client_fenix.execute("SELECT COUNT(*) as total FROM #{tabla}").first['total'] + datos_fenix[tabla] = { total: count_total, col_fecha: nil } + puts " #{tabla}: #{count_total} registros (sin fecha)" + next + end + + col_fecha = cols.first['COLUMN_NAME'] + + # Contar total y desde abril 13 + count_total = client_fenix.execute("SELECT COUNT(*) as total FROM #{tabla}").first['total'] + count_desde_13 = client_fenix.execute(" + SELECT COUNT(*) as total + FROM #{tabla} + WHERE #{col_fecha} >= '2026-04-13' + ").first['total'] + + # Última fecha + ultima_fecha = client_fenix.execute(" + SELECT MAX(#{col_fecha}) as ultima + FROM #{tabla} + ").first['ultima'] + + # Últimos 5 registros + ultimos = client_fenix.execute(" + SELECT TOP 5 * + FROM #{tabla} + ORDER BY #{col_fecha} DESC + ").to_a + + datos_fenix[tabla] = { + total: count_total, + desde_13: count_desde_13, + col_fecha: col_fecha, + ultima_fecha: ultima_fecha, + ultimos: ultimos + } + + puts " #{tabla}: #{count_desde_13} desde 2026-04-13 (total: #{count_total}) - última: #{ultima_fecha}" + rescue => e + puts " #{tabla}: ERROR - #{e.message}" + end +end + +# ========================================== +# 4. CONSULTAR DASU-SQL4 +# ========================================== +puts "\n[3] Consultando DASU-SQL4..." + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\\temp\\comparacion_dasu_output.json" + + $null = New-Item -ItemType Directory -Force -Path "C:\\temp" + + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $tablasPrincipales = @('Bonos', 'BonosAfiliado', 'AutorizacionGasto', 'AutorizacionBonosExcepcion', 'afiliados', 'ControlPrestaciones', 'usuarios', 'transacciones') + + $datos = @{} + + foreach ($tabla in $tablasPrincipales) { + try { + # Verificar si existe + $check = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 1 1 FROM sys.objects WHERE name = '$tabla' AND type = 'U'", $conn) + $existe = $check.ExecuteScalar() + + if ($existe) { + # Buscar columna fecha + $cmdCol = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 1 COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$tabla' AND DATA_TYPE IN ('datetime', 'datetime2', 'date', 'smalldatetime')", $conn) + $colFecha = $cmdCol.ExecuteScalar() + + if ($colFecha) { + # Count total + $cmdTotal = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [$tabla]", $conn) + $countTotal = $cmdTotal.ExecuteScalar() + + # Count desde abril 13 + $cmd13 = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [$tabla] WHERE [$colFecha] >= '2026-04-13'", $conn) + $count13 = $cmd13.ExecuteScalar() + + # Última fecha + $cmdMax = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX([$colFecha]) as ultima FROM [$tabla]", $conn) + $ultima = $cmdMax.ExecuteScalar() + + # Últimos 5 + $cmdUltimos = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 5 * FROM [$tabla] ORDER BY [$colFecha] DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmdUltimos) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ultimos = $ds.Tables[0].Rows | ForEach-Object { + $row = @{} + $_.Table.Columns | ForEach-Object { $row[$_.ColumnName] = $_.($_) } + $row + } + + $datos[$tabla] = @{ + total = $countTotal + desde_13 = $count13 + col_fecha = $colFecha + ultima_fecha = $ultima + ultimos = $ultimos + } + + Write-Host " $tabla : $count13 desde 2026-04-13 (total: $countTotal) - última: $ultima" + } else { + $cmdTotal = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM [$tabla]", $conn) + $countTotal = $cmdTotal.ExecuteScalar() + $datos[$tabla] = @{ total = $countTotal; col_fecha = $null } + Write-Host " $tabla : $countTotal registros (sin fecha)" + } + } else { + Write-Host " $tabla : NO EXISTE" + } + } catch { + Write-Host " $tabla : ERROR - $_" + } + } + + $conn.Close() + + $resultado = @{ + datos = $datos + timestamp = (Get-Date -Format "o") + } + + $resultado | ConvertTo-Json -Depth 10 | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +begin + DasuExecutor.execute_ps_on_dasu(ps_script, output_path: 'C:\temp\comparacion_dasu_output.json') + resultado_dasu = DasuExecutor.read_json_from_dasu('C:\temp\comparacion_dasu_output.json') + datos_dasu = resultado_dasu['datos'] + +rescue => e + puts "[!] Error consultando dasu-sql4: #{e.message}" + puts e.backtrace.first(5).join("\n") + datos_dasu = {} +end + +# ========================================== +# 5. COMPARAR RESULTADOS +# ========================================== +puts "\n[4] COMPARACIÓN FENIX vs DASU-SQL4:" +puts "=" * 60 + +tablas_comunes = datos_fenix.keys & (datos_dasu || {}).keys + +if tablas_comunes.empty? + puts " ⚠️ No hay tablas comunes para comparar" +else + puts " Tablas a comparar: #{tablas_comunes.join(', ')}" + puts "" + + todas_coinciden = true + + tablas_comunes.each do |tabla| + fenix = datos_fenix[tabla] + dasu = datos_dasu[tabla] + + puts " === #{tabla} ===" + puts " Columna fecha: #{fenix[:col_fecha] || dasu['col_fecha'] || 'N/A'}" + puts " FENIX total: #{fenix[:total]} | DASU total: #{dasu['total']}" + puts " FENIX desde_13: #{fenix[:desde_13]} | DASU desde_13: #{dasu['desde_13']}" + puts " FENIX última: #{fenix[:ultima_fecha]} | DASU última: #{dasu['ultima_fecha']}" + + # Verificar coincidencia + if fenix[:total] == dasu['total'] && fenix[:desde_13] == dasu['desde_13'] + # Comparar fechas como strings + fenix_fecha = fenix[:ultima_fecha].to_s.strip + dasu_fecha = dasu['ultima_fecha'].to_s.strip + + if fenix_fecha == dasu_fecha + puts " ✅ COINCIDE" + else + puts " ⚠️ FECHAS DIFIEREN" + puts " FENIX: #{fenix_fecha}" + puts " DASU: #{dasu_fecha}" + todas_coinciden = false + end + else + puts " ⚠️ NO COINCIDE - Diferencia en cantidades" + todas_coinciden = false + end + puts "" + end + + puts "=" * 60 + if todas_coinciden + puts " ✅ TODOS LOS DATOS COINCIDEN - El restore fue exitoso" + else + puts " ⚠️ HAY DIFERENCIAS - El restore puede estar incompleto" + end +end + +# ========================================== +# 6. DETALLE ÚLTIMOS REGISTROS (BONOS) +# ========================================== +puts "\n[5] DETALLE ÚLTIMOS REGISTROS DE BONOS:" +puts "=" * 60 + +if datos_fenix['Bonos'] && datos_fenix['Bonos'][:ultimos] + puts " FENIX - Últimos 5 bonos:" + datos_fenix['Bonos'][:ultimos].each_with_index do |registro, i| + puts " #{i+1}. #{registro}" + end +end + +if datos_dasu && datos_dasu['Bonos'] && datos_dasu['Bonos']['ultimos'] + puts "\n DASU-SQL4 - Últimos 5 bonos:" + datos_dasu['Bonos']['ultimos'].each_with_index do |registro, i| + puts " #{i+1}. #{registro}" + end +end + +puts "\n✅ Verificación completada" + +client_fenix.close diff --git a/adn/tools/cli/verificar_estructura_tabla.rb b/adn/tools/cli/verificar_estructura_tabla.rb new file mode 100644 index 00000000..754340ea --- /dev/null +++ b/adn/tools/cli/verificar_estructura_tabla.rb @@ -0,0 +1,103 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Estructura de tabla BonosAfiliado ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] Estructura de la columna idsolicitud:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_DEFAULT, + IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'BonosAfiliado' AND COLUMN_NAME = 'idsolicitud' + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " Columna: $($row.COLUMN_NAME)" + Write-Host " Tipo: $($row.DATA_TYPE)" + Write-Host " Default: $($row.COLUMN_DEFAULT)" + Write-Host " Nullable: $($row.IS_NULLABLE)" + } else { + Write-Host " Columna no encontrada" + } + + Write-Host "`n[2] ¿Es idsolicitud una columna IDENTITY?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + c.name AS column_name, + t.name AS table_name, + COLUMNPROPERTY(OBJECT_ID(t.schema_name + '.' + t.name), c.name, 'IsIdentity') AS is_identity, + IDENT_CURRENT(t.schema_name + '.' + t.name) AS current_identity, + IDENT_INCR(t.schema_name + '.' + t.name) AS identity_increment, + IDENT_SEED(t.schema_name + '.' + t.name) AS identity_seed + FROM sys.columns c + JOIN sys.tables t ON c.object_id = t.object_id + WHERE t.name = 'BonosAfiliado' AND c.name = 'idsolicitud' + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " ¿Es Identity?: $($row.is_identity)" + Write-Host " Seed (valor inicial): $($row.identity_seed)" + Write-Host " Incremento: $($row.identity_increment)" + Write-Host " Valor actual (IDENT_CURRENT): $($row.current_identity)" + } else { + Write-Host " No es columna IDENTITY o no existe" + } + + Write-Host "`n[3] Valores reales de idsolicitud en últimos registros:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 20 idsolicitud, fecha, codigoafiliado + FROM BonosAfiliado + ORDER BY idsolicitud DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " idsolicitud=$($_.idsolicitud) | fecha=$($_.fecha) | afiliado=$($_.codigoafiliado)" + } + + Write-Host "`n[4] ¿Hay registros con idsolicitud > 0?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT COUNT(*) as c FROM BonosAfiliado WHERE idsolicitud > 0 + ", $conn) + $c = $cmd.ExecuteScalar() + Write-Host " Cantidad: $c" + + Write-Host "`n[5] ¿Hay registros con idsolicitud = 0?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT COUNT(*) as c FROM BonosAfiliado WHERE idsolicitud = 0 + ", $conn) + $c = $cmd.ExecuteScalar() + Write-Host " Cantidad: $c" + + Write-Host "`n[6] Máximo valor de idsolicitud:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado + ", $conn) + $max = $cmd.ExecuteScalar() + Write-Host " MAX(idsolicitud): $max" + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_fechas_timezone_dasu.rb b/adn/tools/cli/verificar_fechas_timezone_dasu.rb new file mode 100644 index 00000000..f66d53be --- /dev/null +++ b/adn/tools/cli/verificar_fechas_timezone_dasu.rb @@ -0,0 +1,95 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICAR FECHAS DESPUÉS DEL CAMBIO DE ZONA HORARIA ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] Zona horaria actual del sistema:" + $tz = Get-TimeZone + Write-Host " ID: $($tz.Id)" + Write-Host " DisplayName: $($tz.DisplayName)" + Write-Host " BaseUtcOffset: $($tz.BaseUtcOffset)" + + Write-Host "`n[2] Fecha/hora actual:" + Write-Host " Get-Date: $(Get-Date)" + Write-Host " UTC: $([DateTime]::UtcNow)" + + Write-Host "`n[3] Última fecha en BonosAfiliado:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fecha) as ultima FROM BonosAfiliado", $conn) + $ultima = $cmd.ExecuteScalar() + Write-Host " MAX(fecha): $ultima" + + Write-Host "`n[4] Últimos 10 registros (ordenados por fecha):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 10 + idsolicitud, + CONVERT(varchar, fecha, 103) as fecha_ddmmaaaa, + codigoafiliado, + dependencia, + bono, + CONVERT(varchar, __tiempo, 103) + ' ' + CONVERT(varchar, __tiempo, 108) as tiempo_completo + FROM BonosAfiliado + WHERE fecha IS NOT NULL + ORDER BY fecha DESC, idsolicitud DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + + if ($ds.Tables[0].Rows.Count -gt 0) { + Write-Host " idsol | fecha (dd/mm/aaaa) | afiliado | dep | bono | __tiempo" + Write-Host " " + ("-" * 80) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.idsolicitud.ToString().PadLeft(5)) | $($_.fecha_ddmmaaaa) | $($_.codigoafiliado.ToString().PadLeft(5)) | $($_.dependencia.ToString().PadLeft(2)) | $($_.bono) | $($_.tiempo_completo)" + } + } + + Write-Host "`n[5] Registros por día (últimos 5 días):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 + CONVERT(varchar, CAST(fecha AS DATE), 103) as dia_ddmmaaaa, + COUNT(*) as cantidad, + MIN(idsolicitud) as min_id, + MAX(idsolicitud) as max_id + FROM BonosAfiliado + GROUP BY CAST(fecha AS DATE) + ORDER BY CAST(fecha AS DATE) DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.dia_ddmmaaaa): $($_.cantidad) registros | IDs: [$($_.min_id) - $($_.max_id)]" + } + + Write-Host "`n[6] Verificando formato de fecha ISO:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 3 + fecha, + CONVERT(varchar, fecha, 120) as formato_iso, + __tiempo + FROM BonosAfiliado + ORDER BY fecha DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " fecha=$($_.fecha) | ISO=$($_.formato_iso)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_fenix_actual.rb b/adn/tools/cli/verificar_fenix_actual.rb new file mode 100644 index 00000000..5af79edb --- /dev/null +++ b/adn/tools/cli/verificar_fenix_actual.rb @@ -0,0 +1,70 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# Verificar FENIX - qué datos hay ACTUALMENTE (abril 15) +# ========================================================== + +require 'tiny_tds' + +puts "=== FENIX (PRODUCCIÓN) - ESTADO ACTUAL ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip +sql_user = 'UTNlarioja\\monlaricardo' +sql_host = '10.0.10.200' + +client = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' +) + +puts "[1] BonosAfiliado - Registros por día (últimos 5 días):" +result = client.execute(" + SELECT CAST(fecha AS DATE) as dia, COUNT(*) as cantidad + FROM BonosAfiliado + WHERE fecha >= DATEADD(DAY, -5, GETDATE()) + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC +") + +result.each { |r| puts " #{r['dia']}: #{r['cantidad']} registros" } + +puts "\n[2] BonosAfiliado - Última fecha exacta:" +result = client.execute("SELECT MAX(fecha) as ultima FROM BonosAfiliado") +puts " #{result.first['ultima']}" + +puts "\n[3] BonosAfiliado - Registros del 2026-04-15:" +result = client.execute("SELECT COUNT(*) as c FROM BonosAfiliado WHERE fecha >= '2026-04-15'") +puts " #{result.first['c']} registros" + +puts "\n[4] AutorizacionBonosExcepcion - Registros por día:" +result = client.execute(" + SELECT CAST(fechasolicitud AS DATE) as dia, COUNT(*) as cantidad + FROM AutorizacionBonosExcepcion + WHERE fechasolicitud >= DATEADD(DAY, -5, GETDATE()) + GROUP BY CAST(fechasolicitud AS DATE) + ORDER BY dia DESC +") + +result.each { |r| puts " #{r['dia']}: #{r['cantidad']} registros" } + +puts "\n[5] AutorizacionBonosExcepcion - Última fecha:" +result = client.execute("SELECT MAX(fechasolicitud) as ultima FROM AutorizacionBonosExcepcion") +puts " #{result.first['ultima']}" + +puts "\n[6] AutorizacionBonosExcepcion - Registros del 2026-04-15:" +result = client.execute("SELECT COUNT(*) as c FROM AutorizacionBonosExcepcion WHERE fechasolicitud >= '2026-04-15'") +puts " #{result.first['c']} registros" + +puts "\n[7] IDs más altos en BonosAfiliado (para ver si hay datos nuevos):" +result = client.execute("SELECT TOP 5 idsolicitud FROM BonosAfiliado ORDER BY idsolicitud DESC") +puts " Últimos IDs: #{result.to_a.map { |r| r['idsolicitud'] }.join(', ')}" + +client.close + +puts "\n✅ Verificación FENIX completada" diff --git a/adn/tools/cli/verificar_fenix_fechas.rb b/adn/tools/cli/verificar_fenix_fechas.rb new file mode 100644 index 00000000..71339d38 --- /dev/null +++ b/adn/tools/cli/verificar_fenix_fechas.rb @@ -0,0 +1,84 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'tiny_tds' + +puts "=== FENIX - Verificando fechas de últimos registros ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +pass_cmd = `ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null`.strip +sql_user = 'UTNlarioja\\monlaricardo' +sql_host = '10.0.10.200' + +client = TinyTds::Client.new( + username: sql_user, + password: pass_cmd, + host: sql_host, + timeout: 600, + database: 'sysdasuten' +) + +puts "[1] Últimos 10 registros de BonosAfiliado (por ID):" +result = client.execute(" + SELECT TOP 10 idsolicitud, fecha, codigoafiliado, dependencia, + CONVERT(datetime, fecha) as fecha_dt, + GETDATE() as ahora + FROM BonosAfiliado + ORDER BY idsolicitud DESC +") + +result.each do |r| + puts " ID: #{r['idsolicitud']} | fecha: #{r['fecha']} | afiliado: #{r['codigoafiliado']}" +end + +puts "\n[2] Registros con fecha 2026-04-15:" +result = client.execute(" + SELECT TOP 10 idsolicitud, fecha, codigoafiliado + FROM BonosAfiliado + WHERE fecha >= '2026-04-15' + ORDER BY idsolicitud DESC +") + +if result.to_a.empty? + puts " No hay registros con fecha >= 2026-04-15" +else + result.each do |r| + puts " ID: #{r['idsolicitud']} | fecha: #{r['fecha']}" + end +end + +puts "\n[3] Verificando diferencia entre MAX(idsolicitud) y MAX(fecha):" +result = client.execute(" + SELECT + MAX(idsolicitud) as max_id, + (SELECT MAX(fecha) FROM BonosAfiliado) as max_fecha, + (SELECT fecha FROM BonosAfiliado WHERE idsolicitud = (SELECT MAX(idsolicitud) FROM BonosAfiliado)) as fecha_del_max_id + FROM BonosAfiliado +") + +r = result.first +puts " ID máximo: #{r['max_id']}" +puts " Fecha máxima: #{r['max_fecha']}" +puts " Fecha del ID máximo: #{r['fecha_del_max_id']}" + +puts "\n[4] ¿Hay registros creados el 15/04 con fecha 14/04?" +puts " (comparando IDs altos con fecha 14 vs fecha 15)" + +# IDs del 15 de abril (por fecha de creación del registro, no la columna fecha) +# Usamos una columna de auditoría si existe, sino comparamos por rango de IDs +result = client.execute(" + SELECT + (SELECT COUNT(*) FROM BonosAfiliado WHERE fecha = '2026-04-15') as con_fecha_15, + (SELECT COUNT(*) FROM BonosAfiliado WHERE fecha = '2026-04-14') as con_fecha_14, + (SELECT COUNT(*) FROM BonosAfiliado WHERE idsolicitud > 30000000) as con_id_alto +") + +r = result.first +puts " Registros con fecha=2026-04-15: #{r['con_fecha_15']}" +puts " Registros con fecha=2026-04-14: #{r['con_fecha_14']}" +puts " Registros con ID > 30000000: #{r['con_id_alto']}" + +client.close + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_final_dasu.rb b/adn/tools/cli/verificar_final_dasu.rb new file mode 100644 index 00000000..27567fbe --- /dev/null +++ b/adn/tools/cli/verificar_final_dasu.rb @@ -0,0 +1,70 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICACIÓN FINAL DASU-SQL4 ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] Última fecha en BonosAfiliado (formato ISO):" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT CONVERT(varchar, MAX(fecha), 120) as ultima FROM BonosAfiliado", $conn) + $ultima = $cmd.ExecuteScalar() + Write-Host " $ultima" + + Write-Host "`n[2] Registros por día (últimos 5 días):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 + CONVERT(varchar, CAST(fecha AS DATE), 120) as dia, + COUNT(*) as cantidad, + MIN(idsolicitud) as min_id, + MAX(idsolicitud) as max_id + FROM BonosAfiliado + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.dia): $($_.cantidad) registros | IDs: [$($_.min_id) - $($_.max_id)]" + } + + Write-Host "`n[3] ¿La suma de registros coincide con el total?" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT COUNT(*) FROM BonosAfiliado", $conn) + $total = $cmd.ExecuteScalar() + Write-Host " Total: $total" + + Write-Host "`n[4] Muestra de 5 registros recientes (con todos sus campos):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 idsolicitud, fecha, codigoafiliado, dependencia, bono + FROM BonosAfiliado + ORDER BY fecha DESC, idsolicitud DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " ids=$($_.idsolicitud) | fecha=$($_.fecha) | afiliado=$($_.codigoafiliado) | dep=$($_.dependencia) | bono=$($_.bono)" + } + + Write-Host "`n[5] Verificando si hay datos del 15 de abril de 2026:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT COUNT(*) as c FROM BonosAfiliado + WHERE YEAR(fecha) = 2026 AND MONTH(fecha) = 4 AND DAY(fecha) = 15 + ", $conn) + $c = $cmd.ExecuteScalar() + Write-Host " Registros el 2026-04-15: $c" + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_identity_simple.rb b/adn/tools/cli/verificar_identity_simple.rb new file mode 100644 index 00000000..113944a6 --- /dev/null +++ b/adn/tools/cli/verificar_identity_simple.rb @@ -0,0 +1,78 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Verificar si idsolicitud es IDENTITY ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] ¿Es idsolicitud una columna IDENTITY?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT COLUMNPROPERTY(OBJECT_ID('dbo.BonosAfiliado'), 'idsolicitud', 'IsIdentity') + ", $conn) + $is_identity = $cmd.ExecuteScalar() + if ($is_identity -eq 1) { + Write-Host " SI, es columna IDENTITY" + } else { + Write-Host " NO, no es columna IDENTITY (valor: $is_identity)" + } + + Write-Host "`n[2] Valor actual de IDENT_CURRENT:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT IDENT_CURRENT('dbo.BonosAfiliado') + ", $conn) + try { + $current = $cmd.ExecuteScalar() + Write-Host " IDENT_CURRENT: $current" + } catch { + Write-Host " ERROR: $_" + } + + Write-Host "`n[3] Contar registros con idsolicitud = 0 vs > 0:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + SUM(CASE WHEN idsolicitud = 0 THEN 1 ELSE 0 END) as con_cero, + SUM(CASE WHEN idsolicitud > 0 THEN 1 ELSE 0 END) as con_mayor_cero + FROM BonosAfiliado + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " Con idsolicitud=0: $($row.con_cero)" + Write-Host " Con idsolicitud>0: $($row.con_mayor_cero)" + } + + Write-Host "`n[4] Máximo idsolicitud:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado + ", $conn) + $max = $cmd.ExecuteScalar() + Write-Host " MAX(idsolicitud): $max" + + Write-Host "`n[5] Últimos 5 registros por fecha (ver idsolicitud):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 idsolicitud, fecha, codigoafiliado + FROM BonosAfiliado + ORDER BY fecha DESC, idsolicitud DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " idsolicitud=$($_.idsolicitud) | fecha=$($_.fecha)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_ids_fechas_dasu.rb b/adn/tools/cli/verificar_ids_fechas_dasu.rb new file mode 100644 index 00000000..4a60f5f5 --- /dev/null +++ b/adn/tools/cli/verificar_ids_fechas_dasu.rb @@ -0,0 +1,85 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Relación IDs vs Fechas ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] Últimos 20 registros por ID:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 20 idsolicitud, fecha FROM BonosAfiliado ORDER BY idsolicitud DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " ID: $($_.idsolicitud) | fecha: $($_.fecha)" + } + + Write-Host "`n[2] Máximo ID por fecha (últimos 5 días):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 CAST(fecha AS DATE) as dia, MAX(idsolicitud) as max_id + FROM BonosAfiliado + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.dia): ID máximo = $($_.max_id)" + } + + Write-Host "`n[3] Rango de IDs por fecha:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + CAST(fecha AS DATE) as dia, + MIN(idsolicitud) as min_id, + MAX(idsolicitud) as max_id, + COUNT(*) as cantidad + FROM BonosAfiliado + WHERE fecha >= '2026-04-10' + GROUP BY CAST(fecha AS DATE) + ORDER BY dia DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.dia): IDs [$($_.min_id) - $($_.max_id)] | cantidad: $($_.cantidad)" + } + + Write-Host "`n[4] ¿Hay IDs > 30000000 con fecha 14?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT COUNT(*) as c + FROM BonosAfiliado + WHERE idsolicitud > 30000000 AND fecha = '2026-04-14' + ", $conn) + $c = $cmd.ExecuteScalar() + Write-Host " Cantidad: $c" + + Write-Host "`n[5] Últimos 5 IDs > 30000000:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 idsolicitud, fecha + FROM BonosAfiliado + WHERE idsolicitud > 30000000 + ORDER BY idsolicitud DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " ID: $($_.idsolicitud) | fecha: $($_.fecha)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_pk_dasu.rb b/adn/tools/cli/verificar_pk_dasu.rb new file mode 100644 index 00000000..4ceac0ad --- /dev/null +++ b/adn/tools/cli/verificar_pk_dasu.rb @@ -0,0 +1,88 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Verificar clave primaria en BonosAfiliado ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] ¿Hay clave primaria en BonosAfiliado?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT i.name as index_name, i.is_primary_key, i.is_unique + FROM sys.indexes i + WHERE i.object_id = OBJECT_ID('dbo.BonosAfiliado') AND i.is_primary_key = 1 + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " PK: $($row.index_name) (unique: $($row.is_unique))" + } else { + Write-Host " NO HAY CLAVE PRIMARIA" + } + + Write-Host "`n[2] ¿Hay índices únicos?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT i.name as index_name, i.is_unique + FROM sys.indexes i + WHERE i.object_id = OBJECT_ID('dbo.BonosAfiliado') AND i.is_unique = 1 + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " Índice: $($_.index_name) (unique: $($_.is_unique))" + } + } else { + Write-Host " NO HAY ÍNDICES ÚNICOS" + } + + Write-Host "`n[3] Columnas de la tabla:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT c.name, t.name as type_name, c.is_nullable + FROM sys.columns c + JOIN sys.types t ON c.user_type_id = t.user_type_id + WHERE c.object_id = OBJECT_ID('dbo.BonosAfiliado') + ORDER BY c.column_id + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " $($_.name) ($($_.type_name)) nullable=$($_.is_nullable)" + } + + Write-Host "`n[4] ¿Hay registros duplicados por (fecha, codigoafiliado, dependencia)?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT fecha, codigoafiliado, dependencia, COUNT(*) as duplicados + FROM BonosAfiliado + GROUP BY fecha, codigoafiliado, dependencia + HAVING COUNT(*) > 1 + ORDER BY duplicados DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + Write-Host " Hay $($ds.Tables[0].Rows.Count) combinaciones duplicadas" + $ds.Tables[0].Rows | Select-Object -First 5 | ForEach-Object { + Write-Host " fecha=$($_.fecha) | afiliado=$($_.codigoafiliado) | dep=$($_.dependencia) | count=$($_.duplicados)" + } + } else { + Write-Host " NO HAY DUPLICADOS" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_solo_dasu.rb b/adn/tools/cli/verificar_solo_dasu.rb new file mode 100644 index 00000000..00db5a87 --- /dev/null +++ b/adn/tools/cli/verificar_solo_dasu.rb @@ -0,0 +1,63 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== VERIFICANDO DATOS EN DASU-SQL4 ===" +puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] BonosAfiliado - Última fecha:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fecha) as ultima FROM BonosAfiliado", $conn) + $ultima = $cmd.ExecuteScalar() + Write-Host " $ultima" + + Write-Host "`n[2] BonosAfiliado - ID máximo:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(idsolicitud) as max_id FROM BonosAfiliado", $conn) + $maxId = $cmd.ExecuteScalar() + Write-Host " $maxId" + + Write-Host "`n[3] BonosAfiliado - Registros por fecha:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT CAST(fecha AS DATE) as dia, COUNT(*) as c FROM BonosAfiliado GROUP BY CAST(fecha AS DATE) ORDER BY dia DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { Write-Host " $($_.dia): $($_.c) registros" } + + Write-Host "`n[4] AutorizacionBonosExcepcion - ID máximo:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(idsolicitud) as max_id FROM AutorizacionBonosExcepcion", $conn) + $maxId = $cmd.ExecuteScalar() + Write-Host " $maxId" + + Write-Host "`n[5] AutorizacionBonosExcepcion - Última fecha:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT MAX(fechasolicitud) as ultima FROM AutorizacionBonosExcepcion", $conn) + $ultima = $cmd.ExecuteScalar() + Write-Host " $ultima" + + Write-Host "`n[6] Fecha del último restore:" + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT TOP 1 restore_date FROM msdb.dbo.restorehistory WHERE destination_database_name = 'sysdasuten' ORDER BY restore_date DESC", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + Write-Host " Restore: $($ds.Tables[0].Rows[0].restore_date)" + } + + Write-Host "`n[7] Archivos en F:\\BACKUP:" + Get-ChildItem "F:\\BACKUP\\*.bak" | ForEach-Object { + Write-Host " $($_.Name)" + Write-Host " LastWriteTime: $($_.LastWriteTime)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_triggers_dasu.rb b/adn/tools/cli/verificar_triggers_dasu.rb new file mode 100644 index 00000000..1ca61c4c --- /dev/null +++ b/adn/tools/cli/verificar_triggers_dasu.rb @@ -0,0 +1,79 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Verificar triggers y constraints en BonosAfiliado ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + $connString = "Server=localhost;Database=sysdasuten;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + Write-Host "[1] ¿Hay triggers en BonosAfiliado?" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT name, type_desc + FROM sys.triggers + WHERE parent_id = OBJECT_ID('dbo.BonosAfiliado') + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " Trigger: $($_.name) ($($_.type_desc))" + } + } else { + Write-Host " No hay triggers" + } + + Write-Host "`n[2] Constraints DEFAULT en idsolicitud:" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT dc.name, dc.definition + FROM sys.default_constraints dc + JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id + WHERE OBJECT_NAME(dc.parent_object_id) = 'BonosAfiliado' AND c.name = 'idsolicitud' + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " Constraint: $($_.name) = $($_.definition)" + } + } else { + Write-Host " No hay constraints DEFAULT" + } + + Write-Host "`n[3] Verificar columna idsolicitud (detalles):" + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT + c.name, + c.is_nullable, + c.is_identity, + c.default_object_id, + t.name as type_name + FROM sys.columns c + JOIN sys.types t ON c.user_type_id = t.user_type_id + WHERE c.object_id = OBJECT_ID('dbo.BonosAfiliado') AND c.name = 'idsolicitud' + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + if ($ds.Tables[0].Rows.Count -gt 0) { + $row = $ds.Tables[0].Rows[0] + Write-Host " name: $($row.name)" + Write-Host " is_nullable: $($row.is_nullable)" + Write-Host " is_identity: $($row.is_identity)" + Write-Host " default_object_id: $($row.default_object_id)" + Write-Host " type: $($row.type_name)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/cli/verificar_zona_horaria_dasu.rb b/adn/tools/cli/verificar_zona_horaria_dasu.rb new file mode 100644 index 00000000..d44d76d4 --- /dev/null +++ b/adn/tools/cli/verificar_zona_horaria_dasu.rb @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'drones/lib/dasu_executor' + +puts "=== DASU-SQL4 - Verificar Zona Horaria ===" +puts "" + +ps_script = <<~PSCMD.strip + $ErrorActionPreference = "Stop" + + Write-Host "[1] Zona horaria del sistema:" + $tz = Get-TimeZone + Write-Host " ID: $($tz.Id)" + Write-Host " DisplayName: $($tz.DisplayName)" + Write-Host " BaseUtcOffset: $($tz.BaseUtcOffset)" + + Write-Host "`n[2] Fecha/hora actual del sistema:" + Write-Host " Get-Date: $(Get-Date)" + Write-Host " Get-Date -Format 'o': $(Get-Date -Format 'o')" + Write-Host " [DateTime]::Now: $([DateTime]::Now)" + Write-Host " [DateTime]::UtcNow: $([DateTime]::UtcNow)" + + Write-Host "`n[3] Fecha/hora en SQL Server:" + $connString = "Server=localhost;Database=master;Integrated Security=true;TrustServerCertificate=yes;" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT GETDATE() as fecha_sql", $conn) + $fecha = $cmd.ExecuteScalar() + Write-Host " GETDATE(): $fecha" + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT SYSUTCDATETIME() as utc_sql", $conn) + $utc = $cmd.ExecuteScalar() + Write-Host " SYSUTCDATETIME(): $utc" + + $cmd = New-Object System.Data.SqlClient.SqlCommand("SELECT SYSDATETIMEOFFSET() as offset_sql", $conn) + $offset = $cmd.ExecuteScalar() + Write-Host " SYSDATETIMEOFFSET(): $offset" + + $conn.Close() + + Write-Host "`n[4] Verificando fecha en BonosAfiliado:" + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + $conn.Open() + $conn.ChangeDatabase("sysdasuten") + + $cmd = New-Object System.Data.SqlClient.SqlCommand(" + SELECT TOP 5 fecha, __tiempo, impresion + FROM BonosAfiliado + WHERE fecha IS NOT NULL + ORDER BY fecha DESC + ", $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $ds = New-Object System.Data.DataSet + $adapter.Fill($ds) + $ds.Tables[0].Rows | ForEach-Object { + Write-Host " fecha=$($_.fecha) | __tiempo=$($_.__tiempo) | impresion=$($_.impresion)" + } + + $conn.Close() +PSCMD + +DasuExecutor.execute_ps_on_dasu(ps_script) + +puts "\n✅ Verificación completada" diff --git a/adn/tools/db/migrations/004_create_planes.sql b/adn/tools/db/migrations/004_create_planes.sql index c66866fe..eefc1cc3 100644 --- a/adn/tools/db/migrations/004_create_planes.sql +++ b/adn/tools/db/migrations/004_create_planes.sql @@ -4,6 +4,15 @@ DO $$ BEGIN + -- Agregar columna 'codigo' a ambitos si no existe + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'bitacoras' AND table_name = 'ambitos' AND column_name = 'codigo' + ) THEN + ALTER TABLE bitacoras.ambitos ADD COLUMN codigo VARCHAR(10); + CREATE UNIQUE INDEX IF NOT EXISTS idx_ambitos_codigo ON bitacoras.ambitos(codigo); + END IF; + -- Crear tabla planes (dentro de un ámbito) CREATE TABLE IF NOT EXISTS bitacoras.planes ( id SERIAL PRIMARY KEY, @@ -38,24 +47,23 @@ BEGIN -- Mantener fase_id por compatibilidad temporal (se deprecará en el futuro) -- Los nuevos hitos deben usar plan_id - -- Crear vista resumen_planes para métricas - CREATE OR REPLACE VIEW bitacoras.resumen_planes AS - SELECT - p.id AS plan_id, - p.codigo, - p.titulo, - p.estado, - a.codigo AS ambito_codigo, - a.nombre AS ambito_nombre, - COUNT(h.id) AS total_hitos, - COUNT(CASE WHEN h.estado = '✅' THEN 1 END) AS hitos_completados, - COALESCE(SUM(h.horas_presencial), 0) AS total_horas_presencial, - COALESCE(SUM(h.horas_remoto), 0) AS total_horas_remoto, - MIN(h.fecha) AS fecha_inicio_real, - MAX(h.fecha) AS fecha_fin_real - FROM bitacoras.planes p - LEFT JOIN bitacoras.ambitos a ON p.ambito_id = a.id - LEFT JOIN bitacoras.hitos h ON p.id = h.plan_id - GROUP BY p.id, p.codigo, p.titulo, p.estado, a.codigo, a.nombre; - END $$; + +-- Crear vista resumen_planes para métricas (fuera del bloque DO) +CREATE OR REPLACE VIEW bitacoras.resumen_planes AS +SELECT + p.id AS plan_id, + p.codigo, + p.titulo, + p.estado, + a.nombre AS ambito_nombre, + COUNT(h.id) AS total_hitos, + COUNT(CASE WHEN h.estado = '✅' THEN 1 END) AS hitos_completados, + COALESCE(SUM(h.horas_presencial), 0) AS total_horas_presencial, + COALESCE(SUM(h.horas_remoto), 0) AS total_horas_remoto, + MIN(h.fecha) AS fecha_inicio_real, + MAX(h.fecha) AS fecha_fin_real +FROM bitacoras.planes p +LEFT JOIN bitacoras.ambitos a ON p.ambito_id = a.id +LEFT JOIN bitacoras.hitos h ON p.id = h.plan_id +GROUP BY p.id, p.codigo, p.titulo, p.estado, a.nombre; diff --git a/docs/ambito/dtic-DASUTEN/A04.P006_Backups-DASUTEN.md b/docs/ambito/dtic-DASUTEN/A04.P006_Backups-DASUTEN.md index 70652301..ff15ca3e 100644 --- a/docs/ambito/dtic-DASUTEN/A04.P006_Backups-DASUTEN.md +++ b/docs/ambito/dtic-DASUTEN/A04.P006_Backups-DASUTEN.md @@ -5,8 +5,8 @@ **Código:** A04.P006 **Fecha:** 15 de abril de 2026 **Autor:** Sistema ADN -**Versión:** 1.2 -**Estado:** ✅ COMPLETADO (Pipeline completo, diferencial y latidos acumulados) +**Versión:** 1.3 +**Estado:** ✅ COMPLETADO Y VALIDADO (Pipeline completo + migración + validación usuario) **Dependencia:** A04.P005 (DASUTEN sin DC), A03.P002 (Automatización Backups) ## 📊 Progreso @@ -28,6 +28,13 @@ - Duración total: **13m 15s** (dentro de lo estimado: ~35-40 min) - Todos los drones atómicos funcionaron correctamente en modo orquestado +✅ **Migración y configuración de zona horaria validadas** +- Zona horaria cambiada de "Romance Standard Time" (UTC+2) a "Argentina Standard Time" (UTC-3) +- Datos sincronizados entre FENIX y DASU-SQL4: **1,339,313 registros** +- **Validación del usuario (17:00):** Andrea Almirón confirma que el sistema desktop muestra correctamente los movimientos del día en curso + +**Conclusión:** El pipeline de backups funcionaba correctamente desde el inicio. La causa raíz de la "desactualización" percibida era la zona horaria incorrecta en dasu-sql4, que desplazaba las fechas ~5 horas adelante (UTC+2 en lugar de UTC-3). + ## 🎯 Objetivo Establecer un sistema de backups y refrescos de la base de datos DASUTEN que permita: @@ -191,6 +198,8 @@ El procesador `Dasuten` fue integrado en `bkps.rb` como un nuevo tipo de tarea: | 2026-04-14 16:03 | Diferencial | 2.44s | 0.39 MB | ✅ | | 2026-04-14 16:35 | Upload Drive | 23.79s | 0.39 MB | ✅ | | **2026-04-15 11:01** | **Pipeline FULL** | **13m 15s** | **~1.3 GB** | **✅ COMPLETO** | +| **2026-04-15 16:30** | **Migración + TZ** | **~30 min** | **~1.3 GB** | **✅ COMPLETO** | +| **2026-04-15 17:00** | **Validación Usuario** | **—** | **—** | **✅ VALIDADO (Andrea Almirón)** | ### Detalle Pipeline 2026-04-15 (Test Completo) @@ -203,6 +212,48 @@ El procesador `Dasuten` fue integrado en `bkps.rb` como un nuevo tipo de tarea: | 5 | `dasuten_restaurar_full` | dasu-sql4 | 13s | ✅ | | 6 | `dasuten_verificar` | dasu-sql4 | 4m 11s | ✅ | +### Migración y Corrección de Zona Horaria (2026-04-15 16:30) + +**Problema identificado:** El usuario reportó que el sistema DASUTEN desktop mostraba datos desactualizados (solo hasta el 13 de abril). + +**Causas raíz:** +1. Los backups se exportaban y subían a Drive correctamente, pero el paso de **restore no se ejecutaba** automáticamente +2. **Zona horaria incorrecta** en dasu-sql4: estaba configurada como "Romance Standard Time" (UTC+2, Europa/París) en lugar de "Argentina Standard Time" (UTC-3) + +**Solución aplicada:** +1. Saneamiento de backups viejos en dasu-sql4 (4 archivos eliminados, ~4 GB liberados) +2. Restauración completa desde `F:\BACKUP\sysdasuten_FULL_20260415_130531.bak` +3. Configuración de zona horaria Argentina (UTC-3) en dasu-sql4 + +| Acción | Detalle | Resultado | +|--------|---------|-----------| +| Limpieza F:\BACKUP\ | 5 archivos → 1 archivo | ~4 GB liberados | +| Restore completo | sysdasuten_FULL_20260415_130531.bak | 1,339,313 registros | +| Zona horaria | Romance ST → Argentina ST | UTC+2 → UTC-3 ✅ | + +**Verificación post-migración:** + +| Métrica | FENIX | DASU-SQL4 | Estado | +|---------|-------|-----------|--------| +| Última fecha | 2026-04-15 | 2026-04-15 | ✅ | +| Registros 15/04 | 84 | 84 | ✅ | +| Registros 14/04 | 370 | 370 | ✅ | +| Registros 13/04 | 603 | 603 | ✅ | +| Total BonosAfiliado | - | 1,339,313 | ✅ | +| Zona horaria | UTC-3 | UTC-3 | ✅ | + +**Nota técnica:** La columna `idsolicitud` muestra valor `0` para los registros nuevos (desde ~2026). Esto es un problema heredado de FENIX (no del restore) y **no es crítico** porque: +- `idsolicitud` NO es la clave primaria (la PK es `PK_BONOSAFILIADO`) +- La tabla funciona correctamente con los datos actuales +- No hay duplicados problemáticos + +**Validación del usuario (2026-04-15 17:00):** + +La usuaria **Andrea Almirón (aalmiron)** confirmó que el sistema DASUTEN desktop ahora muestra correctamente los movimientos del día en curso, resolviéndose el problema que se presentaba anteriormente donde los datos no se veían actualizados. + +> **Conclusión:** El problema de la zona horaria era la causa principal de la discrepancia percibida. Los backups se realizaban correctamente, pero la configuración UTC+2 (Europa) en lugar de UTC-3 (Argentina) provocaba que las fechas se interpretaran con un desplazamiento de ~5 horas, afectando la visualización de los datos en el sistema desktop. + + **Throughput:** - Upload a Drive: ~3 MB/s (limitado por ancho de banda de subida) - Download desde Drive: ~25 MB/s (HTTP directo) @@ -336,6 +387,8 @@ file_info = DasuExecutor.get_file_info_on_fenix(client, 'X:\backup.bak') | 2026-04-14 | Un solo destino (X:) vs 3 directorios | Menos puntos de falla, más simple | | 2026-04-14 | Módulo común DasuExecutor: 6 drones, 1 solo patrón | -52% código, mantenibilidad | | 2026-04-15 | Heartbeat con output acumulado en Bitácora Web | Visibilidad completa del progreso de drones multi-paso | +| 2026-04-15 | **Zona horaria incorrecta pasa desapercibida** | Datos "actualizados" pero invisibles para el usuario — validar TZ en todo restore | +| 2026-04-15 | **Validación usuario es crítica** | El pipeline técnicamente exitoso necesita confirmación del usuario final | ## 📈 Mejora de Latidos/Acumulados (2026-04-15) diff --git a/docs/ambito/dtic-DASUTEN/A04.P007_Bugs-Operativos.md b/docs/ambito/dtic-DASUTEN/A04.P007_Bugs-Operativos.md new file mode 100644 index 00000000..4767c9e0 --- /dev/null +++ b/docs/ambito/dtic-DASUTEN/A04.P007_Bugs-Operativos.md @@ -0,0 +1,273 @@ +# Registro de Bugs y Problemas Operativos DASUTEN + +> **Ámbito:** A04 — dtic-DASUTEN +> **Código:** A04.P007 +> **Estado:** 🟢 ACTIVO (operación continua) +> **Responsable:** DTIC - DIIAA + +--- + +## 📊 Tablero de Estado + +| Bug-ID | Título | Prioridad | Estado | Fecha Reporte | Fecha Cierre | +|:------:|--------|:---------:|:------:|:--------------|:-------------| +| #001 | [Error plantillas en Práctica](#bug-001-error-de-plantillas-en-sección-práctica) | Alta | 🔍 Diagnóstico | 2026-04-16 | — | +| #002 | [Impresión - rutas hardcodeadas](#bug-002-impresión-no-funciona-rutas-hardcodeadas) | Alta | ✅ Cerrado | 2026-04-13 | 2026-04-13 | +| #003 | [Zona horaria incorrecta](#bug-003-zona-horaria-incorrecta-en-servidor) | Media | ✅ Cerrado | 2026-04-14 | 2026-04-14 | +| #004 | [Restore diferencial - escape rutas](#bug-004-restore-diferencial-fallaba-por-escape-de-rutas) | Alta | ✅ Cerrado | 2026-04-16 | 2026-04-16 | + +**Leyenda de Estados:** +- 🆕 Reportado → 🔍 En diagnóstico → 🛠️ En solución → ✅ Resuelto → 📚 Cerrado + +--- + +## 🎯 Objetivo del Documento + +Este registro centraliza la **gestión de bugs y problemas operativos** del sistema DASUTEN en producción. + +**Características:** +- **Vivo:** Se actualiza con cada nuevo bug reportado +- **Trazable:** Cada bug tiene ID único y historial completo +- **Acción:** Estado claro y próximos pasos definidos +- **Histórico:** Bugs cerrados permanecen como referencia + +--- + +## 📋 Bugs Activos + +### BUG #001: Error de Plantillas en Sección "Práctica" + +| Metadato | Valor | +|----------|-------| +| **Reportado por** | Andrea Almirón | +| **Fecha** | 2026-04-16 | +| **Prioridad** | Alta | +| **Estado** | 🔍 En diagnóstico | +| **Impacto** | Bloquea sección completa de Práctica | + +#### Descripción + +Al intentar trabajar en la sección **Práctica**, el sistema muestra un error relacionado con **plantillas de documentos**. + +#### Contexto Técnico + +El sistema DASUTEN utiliza plantillas Word/Excel para generar documentos imprimibles: +- `C:\SysDasuten\Sistema\Word\` (97 archivos .doc) +- `C:\SysDasuten\Sistema\Excel\` (Recibo.xls) + +#### Antecedentes Relacionados + +| Bug-ID | Problema | Solución | +|--------|----------|----------| +| #002 | Impresión no funcionaba — rutas a `C:\Sistema\` | Symlink `C:\Sistema → C:\SysDasuten\Sistema` | + +#### Hipótesis + +1. **Ruta de plantillas incorrecta** — El ejecutable VFP busca en una ruta que no existe +2. **Permisos de archivo** — Las plantillas no son legibles desde el contexto de ejecución +3. **Referencia en código** — La sección "Práctica" tiene una ruta hardcodeada diferente + +#### Próximos Pasos + +- [ ] Identificar mensaje de error exacto (captura de pantalla de la usuaria) +- [ ] Verificar ruta de plantillas que busca el sistema +- [ ] Confirmar si las plantillas existen en esa ruta +- [ ] Revisar logs de eventos de Windows (si aplica) + +#### Solución a Implementar + +Pendiente de diagnóstico. Opciones probables: +- Symlink adicional si la ruta es diferente +- Modificación de configuración en `.ini` o registro +- Ajuste de permisos de archivos + +--- + +## 📜 Histórico de Bugs Cerrados + +### BUG #002: Impresión no Funciona - Rutas Hardcodeadas + +| Metadato | Valor | +|----------|-------| +| **Reportado por** | Andrea Almirón | +| **Fecha** | 2026-04-13 | +| **Prioridad** | Alta | +| **Estado** | ✅ Cerrado | +| **Tiempo de resolución** | < 2 horas | + +#### Descripción + +La función de impresión del sistema DASUTEN no funcionaba. Los documentos no se generaban. + +#### Causa Raíz + +El ejecutable de Visual FoxPro tenía hardcodeada la ruta legacy `C:\Sistema\` para las plantillas, pero el directorio real estaba en `C:\SysDasuten\Sistema\`. + +#### Solución Implementada + +```powershell +# Symlink para redirigir ruta legacy a ruta actual +New-Item -ItemType SymbolicLink -Path "C:\Sistema" -Target "C:\SysDasuten\Sistema" +``` + +#### Validación + +- [x] Impresión de documentos funcional +- [x] Validado por usuaria Andrea Almirón + +#### Lección Aprendida + +El sistema VFP asume rutas legacy. Documentar todos los paths hardcodeados antes de migraciones futuras. + +--- + +### BUG #003: Zona Horaria Incorrecta en Servidor + +| Metadato | Valor | +|----------|-------| +| **Reportado por** | Monitoreo DTIC | +| **Fecha** | 2026-04-14 | +| **Prioridad** | Media | +| **Estado** | ✅ Cerrado | +| **Tiempo de resolución** | < 30 minutos | + +#### Descripción + +El servidor `dasu-sql4` tenía configurada una zona horaria incorrecta, lo que afectaba los timestamps de la base de datos y los backups. + +#### Causa Raíz + +La zona horaria configurada era "Romance Standard Time" (UTC+2, Europa del Este) en lugar de "Argentina Standard Time" (UTC-3). + +#### Solución Implementada + +```powershell +# Configurar zona horaria correcta +Set-TimeZone -Id "Argentina Standard Time" +``` + +#### Validación + +- [x] `Get-TimeZone` retorna "Argentina Standard Time" +- [x] Timestamps de SQL Server correctos +- [x] Backups con horarios OK + +#### Lección Aprendida + +Verificar timezone en deploy inicial de cualquier servidor Windows. Incluir en checklist de provisioning. + +--- + +### BUG #004: Restore Diferencial Fallaba por Escape de Rutas + +| Metadato | Valor | +|----------|-------| +| **Reportado por** | Pipeline de Backups | +| **Fecha** | 2026-04-16 | +| **Prioridad** | Alta | +| **Estado** | ✅ Cerrado | +| **Tiempo de resolución** | < 1 hora | + +#### Descripción + +El script Ruby de restore diferencial fallaba al intentar ejecutar comandos en el servidor Windows. + +#### Causa Raíz + +Las rutas Windows en el script Ruby no tenían el escape correcto de backslashes. Ejemplo: +```ruby +# Incorrecto +"C:\Program Files\Microsoft\..." + +# Correcto +"C:\\Program Files\\Microsoft\\..." +``` + +#### Solución Implementada + +Modificación del archivo `dasuten_restaurar_diferencial_dasu-sql4.rb`: +- Escape de todas las rutas Windows con dobles backslashes +- Validación de sintaxis Ruby + +Commit: `faf46be8 [Bugfix] dasuten_restaurar_diferencial_dasu-sql4.rb — Escape rutas Windows` + +#### Validación + +- [x] Restore diferencial ejecuta sin errores de sintaxis +- [x] Backup restaurado correctamente en entorno de prueba + +#### Lección Aprendida + +Los scripts Ruby que interactúan con Windows requieren escape estricto de rutas. Considerar uso de `%q{}` o forward slashes donde sea compatible. + +--- + +## 🔧 Metodología de Gestión de Bugs + +### Flujo de Trabajo + +``` +🆕 Reportado → 🔍 Diagnóstico → 🛠️ En solución → ✅ Resuelto → 📚 Cerrado +``` + +### Criterios de Estado + +| Estado | Criterio | +|--------|----------| +| 🆕 **Reportado** | Bug ingresado en el registro, pendiente de análisis inicial | +| 🔍 **Diagnóstico** | Equipo analizando causa raíz, reproduciendo el error | +| 🛠️ **En solución** | Causa identificada, implementación de fix en curso | +| ✅ **Resuelto** | Fix implementado, pendiente validación del usuario | +| 📚 **Cerrado** | Usuario validó solución, bug se mueve a histórico | + +### Bitácora Web + +Los detalles de cada intervención se registran en: +- **URL:** http://localhost:5174/bitacoras/ +- **Filtro:** Ámbito `dtic-DASUTEN` +- **Búsqueda:** `[A04.P007]` o `BUG-###` + +--- + +## 📝 Lecciones Aprendidas (Consolidado) + +| Fecha | Bug | Causa Raíz | Solución | Prevención | +|-------|-----|------------|----------|------------| +| 2026-04-13 | #002 Impresión | Rutas hardcodeadas legacy | Symlink `C:\Sistema` | Documentar paths en código VFP | +| 2026-04-14 | #003 Timezone | "Romance Standard Time" | `Set-TimeZone` Argentina | Checklist de provisioning | +| 2026-04-16 | #004 Escape rutas | Backslashes sin escape en Ruby | `C:\\Program Files\\...` | Linter/validación de scripts | + +--- + +## 🛠️ Herramientas Disponibles + +```bash +# SSH a dasu-sql4 (vía srv-dasu) +./adn/tools/run nodos info dasu-sql4 + +# SSH a dasu-pc (vía Tailscale) +./adn/tools/run nodos info dasu-pc + +# Ejecutar PowerShell remoto +ruby adn/tools/candados/candados.rb run dasu-sql4:Administrador "" + +# Ver logs de bitácora +./adn/tools/run db evento:listar --ambito dtic-DASUTEN --descripcion "P007" + +# Buscar eventos por Bug-ID +./adn/tools/run db evento:listar --ambito dtic-DASUTEN --descripcion "BUG-001" +``` + +--- + +## 📅 Historial de Versiones del Documento + +| Versión | Fecha | Cambios | +|---------|-------|---------| +| 2.0 | 2026-04-16 | Reestructuración completa: formato escalable para múltiples bugs | +| 1.0 | 2026-04-16 | Versión inicial — Bug #001: Error plantillas Práctica | + +--- + +**Última actualización:** 2026-04-16 +**Próxima revisión:** Al cerrar BUG #001 o al reportarse nuevo bug diff --git a/docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.md b/docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.md new file mode 100644 index 00000000..62d623cc --- /dev/null +++ b/docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.md @@ -0,0 +1,34 @@ +# UNIVERSIDAD TECNOLÓGICA NACIONAL +## Facultad Regional La Rioja + +# INFORME DE MIGRACIÓN +## Sistema DASUTEN a Infraestructura Autónoma + +**Proyecto:** A04.P005 | **Área:** DTIC | **Fecha:** 16 de abril de 2026 | **Versión:** 1.1 + +--- + +### Documentos incluidos + +| N° | Documento | Páginas | +|:---:|:---|:---:| +| 01 | Informe Ejecutivo — Migración del Sistema DASUTEN a Infraestructura Autónoma | 4 | +| 02 | Informe Técnico — Migración de infraestructura DASUTEN (Modo Workgroup sin Domain Controller) | 8 | + +### Estado: ✅ COMPLETADO Y VALIDADO + +El sistema DASUTEN se encuentra 100% operativo en la nueva infraestructura desde el 15 de abril de 2026. + +### Resumen de logros + +| Logro | Impacto | +|-------|---------| +| Simplificación arquitectónica | De 4 componentes a 2 (−50%) | +| Trabajo directo | Sin dependencia de escritorio remoto | +| Autonomía operativa | Infraestructura independiente de la Facultad | +| Backups automatizados | Pipeline de drones atómicos implementado | +| Sin inversión de hardware | Equipos existentes reutilizados | + +--- + +*La Rioja, 16 de abril de 2026* diff --git a/docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.pdf b/docs/informes/2026-04_DASUTEN-migracion/00_caratula_entrega.pdf new file mode 100644 index 0000000000000000000000000000000000000000..29e12cb9a841ea9abe9e14cc22d52918000e5278 GIT binary patch literal 19842 zcma&Nb97~0vo9QVY}>YN+s2M<+qR94ZQD*dw%Kvgv6I`+Io}=UJ>$IJ8F!5RN9{GM z<}9tXepO=?nWCsT9TPnVG})h@X~ocNgp7ptMpn?gybR)&HZG=4gbd;~hAyU}rpERr zrVO&CcIGY?gv`vGT>Sjd&Mr=-hPKci8&_I7@yD#Nem8ZeEXeCyhmsa0dTG%K?X^ZCs6h{6=A>deOo@v@@B`!FE32*u%^6~Jl5ju8(Y+YSLMu=2orYMb z$`g^b&3Z$)T;l2c+0U9a!(Z?H@O#%Xf9ne~Xjfyz@3KkN;@Y=;-loqz9e6yF0E3^b z3Ajg<%g}D^7KspH{?R@**Z+|A}a^A~ihbn#8zo!VqGKL~u($fDEZx$c>Og@i8KVrSnvMqUZU+mS#sfPr1bRX|^Zcz@M!;TBi0ExuS(^(kjPkzMDHO zOo>3@exOwTEhl%yaCfL3sp8m}oYOgu<3<)NeM|=r1dM{P>{v}o&V(UJtGZsbhEH9Y zG8u18Rxmgr4!ull4}})`FMO=zwSt8O(BN}@zX}SaBfEtzkSh(4y^Qdv|C94b?w=iD z)Iim-lT`^D6g{hKoXazmsRe3NLnd)jixuha!W5axRd)`65E*Y3@y4W$ji5==*l zq$w(usQT&BK%u#DDT4|g(G?UbR&q;*UU*knq-czb$g&SgpOZ7jnGNdfoz|vnz`d$m)kHQkmoKi*1%WU#F zFtd(opnhLg5cGcDjm-*N$*oK5?p1#UCQB;;UD16!bL}OsRKtFfUu=hgH?wM z*MTxy$@Hg5dR2E!Byzc~fN0hm8!)cKETp`bTT}?W$2s0gWn@x(u@n!R4DhxG3 za1|1niefOGdiMZNa9Kr74cr$h=d<;cYhQXB2}BQPrAQ(10;!_@>F z=(t`jltXlqI@Hj!-M3Y-4H+iJ42bV;A~q<&u9KVXy$%W`S7vIipR6mxtRqiY2dJh~ zn*34@C$5Q0R^IuENH@YhZ6?AnawWZ{gtb*#+ZI3$1bY^?VR{k~YfYgOPBipEA74QT zpcLb745`*?<#1K54$1{_n;w95udgyKEq1#oW)FVEAiAzB3Dz-s!ZZ$_zKhm@ZZd1b z9Frn#53PFfY=YEoMQg4XqZmywgE)_?R=t(-%}b5DU+k&PPE5kLE8)iFXf4bT^9XzG z8(Ld89F8%!%4S{(Wm5=S_!W*~#9c*8&|`8fM_o(aqUboSQqF`GNIyIdO;1mg~wR&*%I0WS95br3PiP z!rNKxdR#f5?P{_t%5I;#-V-NpbXGWP`h-`vr^~_Z_O@$=GW;a&emUkkwh@2GsG)6W zM&ht>b%0`eNqhhkBZd3;}PmMl+1RD1Y(1e7-y!3*4h z?+EJu!0{v+Cf@LfXOy`yiCd9JMi`pl9-P3EgYL{lIAEh~> z(BPi$ck)xYwN@oyCa29i>iWEImz&H7wQ7%hZWxs>55BuNC`#;owEVunk(`sqp-t^f z{y*&Vm-+`TG5rf}`cI6+M99X%%=o{7C>CaR_J0LYJ(@7Os%qHZz0Nnez@jDrz_-9G zgu!7!%q)me_NW9e!eIBY`Gr9-g$JOj&8hXM+bcfJMz+;Sul?*ayIHSm!)h$|pU_jf zbha(bIb4_S*D)zATAyy4@1NH_Cwcd|H$L&-&$%q1kU)8i2&AVe8FMvV?@1vh3|Tq@?OJvZABg2{5s3}z>0#qLt)*D!=y z5o2k|{6k@1no`{uXY8`lvsftSi@i(%y7Gq-HXWWS!27VV>m!`o$nC(O2nkl!6?8V< z5XnS8!UDxV-@cO0be!0&JE#xKFn-Os?fDi|je&b>{-TZFBA6gNvqFH;Wm`(q<82iB zSVg=ng~vV1G)2CVY0?p08@hy#*KGqkK-=JAkm{Xu3k1opR-bNGH9pbdhBD5K&mKIt zG$p6kYB`6{$esc1N~!^GFUvUqovXeIu#W@T@;=sKuB=fm_9`UfJUs8lJk@)WtklRt zCSoNKW*ks?u&J1d0Ab;)L6n&FAPxaT)OM#3J{1a?gGoAFCjeh?MuaUrVI&Tr5`~0` zKukj+>J|^FkA>9J=A^rI78{K>`a|FIli1fZDie4^*jzFskCYEBFVJN?fQb>`h7rz8 znCvoK#R2YG$z)(;Gu0!Rup`bL_b359{&q!OAaTcAR3P&5#`RS|{3?RHUs)5OYo9(* z1kuX~x73Kqj+lEWtDZ7#+DTRWg@jw`$N;_qZQjH(w0T~P;|ScTJ4d2f_)GFj-14CC zx-XEt!{T~&;l3|Ooi`Bhj@B%;E(2H`L=41U&)vY;=hMI;f}GT|74LX7*U$lhG3nS~ zW(}9GZ3T&iyqReQ7xg1Y>J>+EDn$$`Rp!AUM`~O=%Y>-Ohw(KRAumvJIO-UMLxfA5 z3$wfM&#^C0HYiV?%j^Xcrclyxz>)0S$1oGfdN-*BJI9h}!^{-Z&16lha4J=y32wJd zK~hnzr9Ed$5Ma&^wGy*#EM#jzj-*K+ZL;hQY@|+qu|2G~>cwy+}WYCbKP zv6HIFV-)};W&;|-F9D8z|1QSa1cLClljV|qoOzWm=n|C*Mee|sW_4^f=5(d4*rXTz z20!4s#Kq{Y?JxrHU?b!|*;simPbh@WQJj&VD8;(=z6(6nhM{)KSbi=L+*xM}Gd)Ik z^?6inMx0Is8Fx!ye!GXjhA!D!0ABC;L zrno}ma6&(DjX7fiNwf~pf`uO}4ZWjKpz^mn*QuXMVQP>k=UwlKth_RDEtRui1JRX2E@c+^rcBuX86x0> zCmNI09iQFmb!ar3Rdajb-R)4KI;eVCIp=v2U5aQ5PbeYMasjYj#CR25&?l-~maXZ9 zx205~>YbRbz8+l2+j*9BGUq2J=V?yJa~HmFZEUanDZYpcXB|2Y#ybM#Rtpmw8`h3h zFRGnEI!fUd$heznJw&sqj;-S!r+Goni5j}KW9r*~A*m*fB~|m+g9?i_Xs1n{vJ`cUOOV5yxuy|AV9`yph+)MROkB}2 zGRjnkt_lD08aTs=X|PzLik;_U3h^JO@VdkH*}2~BB$3u6E@8LzPTb8l_*T9f_?Y$H zBclS5Gc(2SXu`Sp8;%mpThF^B=&Wr z!c$RkBEr1z_C9h;cRw}FzOGVsB^=g>Z)#NUg%(&r7MYGL1c6}V6?tH6X-<6$GyXgi z#1j~@esPS+eyn!c>xeIN%<{VbFvaJ&M}l50cRLR)p%?NJh+V1c~ zMvmN1Y+PQLws0^@5W2w#0TxOs+#i#is`F5jvDs)g;?hQm-l3uyzr>FlkOYX=MEW1- z^M>!nH}yEWOs!%1oV|qlLKNB1hSNT8| zh}yT-NVn~G=>3I|ouAFR-fQ<+YFj7sd1d;+FF>(U*dui&q|6JXO^9p(!o47KKB&Oc zD+UwB8n=*L(FM0p>;EUa!d?pG!3aa8nsw?|MFFLncm;{TEZZ>GUjDn{Hevw%|+axm!1RfXWy-> zG;y>Bb%>nYGYNP%fcVMZoNZPzDneo&%k^suYMt-k{o$I4L+QhcsK!XDDomIn(0yS@ zi_CTBXi9S=y5Y$F#rMIhzHPK$;1QSYWvk?C{jotQxDer!F(&9T66qcAjGZx%elKRs zd~srjFSP>QQ6pK40OrTAE>Xr&NlpzrVP#C(2zaKV;QQNx8J)M*e!6pTnLVsCkxi-J zvP~lJ)Kgf1`=I<@tGQe*1o8uHbe0#HCV!tQAp9i2NpM zi`KsUYT3YRXZ$tl{(AC0hJJ^Gc}gBjU+=4WENMnkf0l+kW>*rWFg<|`Nu;GI)R@yn zW(XjxQoW4Fq~wVs@TZ)xCBR2MEh3#yNqi(%5+JBCV+zFyRQ@lsf*a!!wj$V1S^50g>w7j43RF z_Qc|`e=YVg%Yajc^px+kU!T(yAYC7mqVFZTr*?)QrZAtQ0zmEBQ6yG2pl1{Vm3F&W z{9=Ab;=2Qt#oVnHtOLhh?e0VLB}eEFmYnn3>JyE7tTMlp7zux(91yX!)ahpDU)krx zSXa|~C-&0z=6GLfUS{gsL4P$9`q`G{cfw_4V`lcVz!~8;h0#Y=9~}UHrA)<#LdM&7 zDWDLE!QRVKs)#=K*R4_mf2>jRo#k==2E#h@u_Y3OQl?9Rx^uOb{YrQzbrSd#40>8M zrmp9urjLa2JMGT{WOj3e6K~w_)74FD=ZHUpx(j|8Tk0Ob%d#l0YYhDsjbRJ&6vYRV zzQ!Tzw%F1*z?s#d88j7C3wc2}iP%jd2~#cZb_Z z4M-G`;?c{X#>@GyS|n;M)g`NC;%Z`|Fuk>oKkItI09@VfA{&qA;lv`8&bTC%6MO%6 zgbIJ$c3;0|rL*l$(-`!p(h2Kz!sABH{TFRtDy_xK@fUGVTSjc+m#Yo!lSp-(J>wuV zpdD=w90AaLzsKT&CkdW+Es~a9Vf>r|-O{fRh(dpE>?J=og{3bOho@QP?+$4dt2IK~ z0F_|PB7mb+{u0=Igg@{H4v_8)_P?o$*#E1W{C}$=CQep1_W!LcVr6II{MWMRirZ3C zb*ypOcP3NlpQ=a|EXe;W`z0Z4B)pO=-QBO}#O;3gepAg6Z z)LAGX&;IfHzVr~miU;V9?unj&by&b)W&)q>B`@g4cvLP<>S5~o2uP4(|9#{o<4DvY zkbF%XyFU773U}{|iYaXF%dm^D;4cl;Jx>OhXaTktIo~?ZEx2>m&V|cB^7(R z-ZkipF4){d9;>VFbX0Qa6VL&iNt>aQtaRZre9-;Ay@#i4WG3zjd)br?5b$f_!EnXI zA~yVhG%m{@$8NBC5NK*bIJhFgA?yR3cI% zn1m2@0wN`y(pZgT8^Mo;P%zM#?NUyzc2_v&FkVMjtOyjB~{4v!dP=nDy}N-b0Thxn;y7-}he7pL-h3^XPsVr!9^eY0%7M2G==E z;6_q*q%V{12Ow+}IJTGRj>b_MOW5>;s;W5pAkRDpaMb69G?jiCiV5I0q>X)f`vI>- zu}&e;&z|%yVQ|HW;QNCkG0TUJFw`S9VAmqy3kuW*w5S%zHyJzeROzgkZGig3z1uf6!U{C;5Rfw*39O z{Ripk<{wcB;w;DJUFZvD_(xT zXKUQ(_F2oOFx%MacKO`gXw>R+c-&}fbe}ZEW}n^Sky*RjZnlSh+hxpzEgDsT+W_NU zUW&LtS2i}eue>R$ylDWAE|WO2dtXRV$* zLQ~5nNs{Pj;`4;)6B7+#ZUmqAw0u+aCBEYbS~Sq`Z)I@Jdo(s`s&SpNh4N@gULIj^ z8)Bs&M$eli?iYH{3GDcmwucwi}aLoFR^j$?Ghh zIu4si(yx1aK6IVpaC-9ieODK_oSnktvfuY1Dr=T0p2gErQsMxQX29pC@!cXOT$f4` zsp=(A#N4U!atRcc@~ndpC8JbrJDhewTu65juUX7ITi~%<#P?>%Tg_(uc%Rmc71G~jIBY)3?X}n7{e_ID zZP5oKx+2D@^bntAp}qM^B?x**0Cqk zb=7QcUlv3_mGav~CWGA2?5=ux@NnemiAYZ%6flCuiNPq@h=mNy8dbxSMP9R13`m>( z_NyxJaC2IjEwlEY$!j$@W>ZhkOMN%=N^7MQXiMmBwSk)Wyz8oqKfZED)j3mHRU_EW zsdPureOfRRD4CX(`)S8&Vn+9sp!ZASQw)d-xuyz?2PifliP0n*u|zmhjzHh>mfKi+ z!V%jo6C6i^-Ev+^A^XP7l}wCQeX?8z!`}^$Io_4{1{ zDmc|ob7RAip+W|G3ZTHw{xLf2$=m60QmncjC&)X{Y&iq1ba5sYd?|=R$W8MZvF8+z zC3$}%m5nB*bg6ko=H|qgDGKP5`qj^q~dAoPn+?jv-#@NvMdPUXn3|VvOS? z8%@d@hItALhX^GY?i!^S88zk%bgmuT#4SF46)#ay!#nae`Mn?inN&fr$}L${67!}m zX6A{CDxq`mRRtn~Dm}gNHJmF^6W5M(qKF*SKm%vJFp*9-YZlF84c5?{H|_GEu31n; z`bC|BMz@Y;eoBQ)oy*)Z$*4NX_~Nd!w_?_#&AHm%dqnj9!vY)Y9jt)VNTaBRG!;oXpHl5*eqR`ArikHV0$Vk9Cu9R6LYl|U7r`a}4aeqEi2DT?387(D3w?r= zFLY}8IaZ@M#+pskwA*552&_rt;LQx5ca(8(nxD!qr%f3-6TPEVoqK1C_4Q8e#T;qMFoJDy`r&Gh zIh&QXcR)wf`oZ>_`oT7_XM8l1IzWW_!DwPprnM(W<0wqHSfiDx^>lB(2ZA0~b25*s zly8I71QVU0_Uk+Z(OUCu{a*Gc5Wjp{93Rsjo9;Kb&Y*K>aI$lK52$}g2y$U^Z{Pj! zNL`7>5FWbo;M%|#*`?)iT`pf%^Bt)=MaV-zdKnrPqbnwJtEwzfA)>wJ(=TY-JLQ5P z2^~~zHZ5F8?bGq&BPpgB))lhq1bHj1b40e;j9zV>0~|zYFN1=@3^J+PH#m)-Ur8BB z+$Cpz0?d8CZ8q)b*m`!O@G2A^Dq`?8I1ef6fgJ?OnHZU~i;Q4zhwG8~CIZeX~P9Jq!+?qLCc`Q1(=Z}-4#jhl@ zx>Y3_?b^9$qyAAXb9jvs+7}Z;c_De~N7g)SrWw9%ZjY*$fY?(uUZSjR^ENwznjY7) z2dQ3}@;(RX#}h2M-xvorGT8B8v0tLNPEKPcth+_MN^b9E5*+M$j@#DRtcJI`=dZT5 zgQVGo42*-g5mD{wHI1=(9sw6ju&-YsUgK;to6||&fuuT;o#pa0QP z@2`bW$yBjlp!_~*MHy+le|Ppu7iGMbCh_uBZT5f0wJwV1Sq z$Z=?x-aWfw!?1^L6?^WIE+4pYbhSa4tIL$Ttb4fvhJcCk@Y9Ex;r*%~euKXNBScz6 zzku0!<<676L*2d?Cz(noSPV)a(mog>9&a!n1+xpklJIJ(b;Xcxg=&eKhiDFWq+Eis z0KhZbb=CS4PbN!AZ)6(fT|Q50=Uk#@cowy5kR|-6AuNyQ;c!?n9;#Z9=GYYa;h+uTnc`zX_!N?T;Gg;U#--qd$b7p3dKvox!0j0 zh~MGW1GuB4sp=wfs}X>2-lvUbR$0$1SN*^}z&WC&2-WqKo(u?>?W;hp9LnX#E{s!&c#AixTVyF7gL;_GkC)gXeu%iN`wT>$fC|4vB&_z#4n@ zDN7?+bbAOQeq#INxw3Hu^@KrJsWG9mfyX0g6%p-f+|P)^KIiM)Uf*AZ=7bPE6c@q( z{*L*WP`_pohH06hgiz{UJkm>y3yG}3*iroPwo*&$rY!6j)nxX!ltrH!6b=y; zJ4AHuD9>=5k;2-tjzl2>x-PeCH4{I60lZUd0-zY$SY}M zjis>M9aX+ORQM=A6Z{NUriJwGt>beQYg+M~*xGv7H)c^cf9i6uTK%z_^gC0G@Lm1x;YMN6r+*k&B&<5l>5uySRUt0ADLM%S<8goQw0hP9W4aT@)^yx>D1 zO0NS0Ikia2#aX$u*GYQNO0M&(2;*R#O8H47%VzDVi(tW7mpaC0pX12tmvxJ7@lZr? z+^C)r3j&|biq^&Z1L4fg`tP*|tOv*haFS~mxCNfcme1z}{Gim_-Qm=e%I>U}%No-Y zYU)%#rA~O&$JXp+{%8$eD>e@?O{|QkfMIdNNMNY2%xg3*;x&;#9!VCWElM7tN8DU} z)#W5o(m+~XzrYV$`Nk5`>`U_=OK+Kxaw6F@pnem|xm4||+krg{?1&OKg5 z{lV;66t$hrcnZ^c8&0^KCu463BCWQ*Q32iWbpTXP7mNM`jZv! zUwC)I_!Vc}%HD>Nh>{>9&zduG%WHG8|HifU(-{Bf?9Y^EV3z^U>zBMx+Y*<;fd~cj z#S@pURYeSm@{n&bL9D_3UDy{u_klai7fh$&pKruDe2IP#^!k7wvR`2HMgy*}^R&u;Hs1Q0>=wnX6$r`z(!rS@+u#FK~_ufWaWwQ25zbLH9D z_bN8gVzgJ3$4VZKozJY2k33#A3Io=KcnjqUg{9z?$=;gi+rE9JmB@dTcaUZ^0@UzX>YpVkrlmq z^hjf|FKQBa57(e*g|(FYzD>Kk`~Wee^V9w}naKG+Gm(Xno%Mg~b24$UG5u>Qwy66m zs!AZ=WIpfk6E^zzOZ9&O&EwWi(sz+JprdkSWhR43>{x+`56rFxJakaa99Evp4#bM658+88J!Q*->cTz+ll<>g)L zKR-;QT;-}BH& zESZG<+Kc%>bbBsb=p2l*y~PKL8UotNIslRBmB~J_(lTHt`!4Lm0x2m-MS&y%UuB?+Xe00?#A)X&&)8fdL=$PdtXyBJ2}(UbtjM9+s( zM!ujoN=V>iW>GAoZYgMGN{k5O_!1kA_M-PA^2HZDQV<$Bkd<_veF&e( zJ8;O?BX&`Q5h}%xB~zgw!He7Rf$4_~m1}8rjAGjV?erV1g3y8&N{@DJ648BSW-q!ZeV zEH3}u4}<2{PLG9ar=bj6Zw39 zAie@!nHD|IfZ@LWBv7f7(CkR{Fw)+G1?{IytMIzokMn|p*H7|BqhZn9j% zt$-B4b?Vk8f=2aJ>=6bXU-$bytAUbgkbcHSw8kfADxH;X{J5YM?cAZJTrEZ!ZCd2a zhbl`9^B3{afl?L7iHu_p^fRymLv3ZozEXIb;537Fv>?5hc!RJu9=&N1Zi`F1d%+-> z7S!sn`>2dHtEHS5Kni|!6};N>z&G{8$4j7B3~>1}5^)qhndM zx(EqeQ)FT!qn9}LZ!0$F^TR6{iMQ_svMtPW9Dw(!y3?4Tsa(a&{>DwrnvkNu!f8gV zD$4UV%6GRg_zusUtC(h({uw;@Z_w@^dO#24Eg0s|%Pdy>%o(VkYNvBaork-LF?;gr z99HjR#r_n7h_htbVdZ~(7qX_iLBR;j$h|~QYe+HS>VBKQW@|sW_bw&a+Gy&p;B6Rp zKkwX(K?`8nT`bPe1_7qMI>?_prUyIKI$YV<3i$fE)>^(4%GykNItx(_YX{q_=7ww_ zhmtA5FR_9~KRxgU{$Q=6s3F?x`-Uii;FL>piX6$Xg-n#rL?& z%+5gZv3(Ggl|a44d`a`eoMw{;cZ+2BVdX$qz7O3CWF`K~B2s&}crP~+G%?E%v=8lo z0~Qe>>ziZ$+>}|S{wdefv@bzj5T*33W3=G#L_DZ8ZgG~!P1CyUxAOozbm01dXz#AjGFBQlaJC3TcaS)s4~LhnG9Ro}a2 zwssxU!)xHQBRLo_%bSK(K4hg@fLZ+FN5w(2eIrP;GxzShPD zS6D{Y3uKJz28pL1zJQ6|ae?5=+s9OF4uxiV_;rP!ox<;9y%(B%=Qu0{cejsDi&w8veb<{0 zeTDr=xY0j%%!VBNKSnkvpaIacM-(@SszvP#hU8=7NQ#VdztUERbWnOZKju&Ih2wpk z7QYcUHCML?q_3@#v!1GDVmGvT-cI5rm+Zr5Q;*te*P5@6gqVbVP{ujM$M;}>!0aJ| z5)-+wv&AJlO}eHlyu2y+&3|Tn0G0D0elKq&y6~Ck$z@0hhgqkDlrOVCF6||O2@K_w zrF;39zMIX^JAOWsn?^65pR5}v&)0E8s`>AhX+Da|sj?fU;AAlp=3&P02Faz^6WgCd zu2Yuex~DtG{E%Fa*zI;|c|4GFTkZLh@&6v`xsqVvu&1>e_sh!LBKCRgJ$@P!F|mAZ z5kl@(d2!nO0(G?I5ZirOnfEs|<8ve=i=X!r+xKVd(Y*9?;ndZ*Q+Md7;puOZn4;h9 z)^5uwu)eCQwP{nt9@bYv=w&!n^!*9f4%bmmq{5`#{Y&l^^&Koh8PjWp1+{! zHpS{~E)oNnQe2Y72w1Nm#uqr*SR0jc#J+kPYb6UnaNzITS&Ec81b7KP+UxK5WS=Bq zaKvvw^h&MkhrPICd&{kUi+7nw`+KuHD~;|QUNz3W?uA!a=&1XN9-2aNJpDN;&BKqr zqvM1w5@-mo`((Pi<(?IAA@5@ZxZpXn4vR9F`LUTBnL=;k>Ndwu*`gSK@$%cVG;lkH z8eJZpGULb$yr7`b#g`dhAu4DjxPM;;kyI00wn0^SH^pA($gGo9B11{5>de8n;_;%{ zM4)iV52^>FmIs!Fl>tF1rbci?TE%gCOkPIPf@o!7v-YzG*c9R|>8-HTTOwAXmv`KPzYs~~I;!#G(T zp(<)9-YA_UJUSRdbCfE%{^SwP#dei`sGaK)qDxt3VhPP;n@gTc{+638mzm}NEx26&?E_xDl&Ppq!N0+M zivq}_2QM+Sz=!wtC ze6CBy`AuJ+OOj17>&}m#H%$+xjPt(71s*T7ZDKnJp+UbgQnxC%iYS^We1s{qUGb|Wl9w?hjpV0rpZh_96TJRJ55&dq`=7M z1^FgfS(>J07iSj~76;}AlFn4fwQ03^ouQ^j_O?slUz%PPDVxsd1Z5(b;k3q+v5d?T ze1psw(ug!1fq9nYNC&)uZHJ;Vx0)3==pL+31BwEQf}pS~Eh;rDQQO%j=;DN3Tq4}9 zW5iv5k&@YS3KG^_sl5?HLDejy2j$;Rs+lT0i^Z ziNC&-PA_)<-0=4YUd%wQ|2J9lFZJmE@`kDZ&{Z$~qWO zbbp{3?8t4a*YQ|fm;n2Hk>haQ4OeI>=-0?ep>6nVH5lMcBrs0MB`|pXzTmve!1T2j z!Y@R&fex#U@1l0ACkNSR5I$3Xz2If=d-k?PJf!nsOac=Rc1FpP^jHE_Ze&^5xI;T| z=VOt>s{XB(vP++~>%!TU`ud8``PF+?(TnJS*A#+GGH#ETe-%|^T9v;~Q_gHq2GVqyI6Zc6%PHB5*a`ph3Xb!HUt+nyT{#I$pwP=NIc6thP@bTq;d-SfSYL$*LT zgy94~9zaJwbZ;P+KhmXt1Q}>$oEq-h;R)puRMilYu8M`_HrbRDN+QM@zlm*tUk#94 zoj)Lnvi^g^$6H|<1fksR`D15|hR;WDbo}sF2SLUF_@v~n$`5Y(2jZ=&j6n2tH(b>v zxwW|YdyU%;u6$Foxa`E}pk*j1z#2_L+{pZ*{MbB7{rS%s1&i$F4|M+2bnY}3^(lTO zJ##Bha&=Q`tUAPIA?Z{#i-7H~G`5MEpc-QdxVZ z9wF{4?|~3E1;HBO*h4Jpw9ST}JZ$9KyFC|ozkO{ydXKiwmW++}zu>nHl4yMURGD)U z|Bc)K6@4w$6lqCruTdOie6}UF@9*nb`h0$<@}*`9Bo+k5ioqS@`)G zROAeutqEEFxy6+KYYdhBUrXqmt6V(`R1MTIiiyjaYtn2vT}R_JVRR`}4b72QLSQ5` zYNi&3iWg#3-&XY~g90&W7__LAg|P+V7lQ`RhSpYu8iW-CCYmc{^(9uje%6UFyJr!1 zGXe4f1eI2iMjjsLn+|t=e+BND>7Gpg>+k8FldXos@7YN;~Lug}VO+9ma zf?p6I0Aalbz6qY~4TTlb={VolJbZfz35X$w@*XGR$2aX7^2kEcZ#HZUtgwC0c+wia z%rlL?)}B-{_*78f+M*)Rz9RXd!?VllC-7kYVPzk^GUb+k0nuc_kVkA3s-MRe+goYj@^kAa7t3BYg`aMH=vvj+Q7jda_Faqq^94)M>2H^qLSlA z9PnrYG-rCp4R(fW388}#&v8`Ah!|+mhS83AIDt_OJ39!qjcZ9agpf+dx}+<+IH0GAKjl$(c=ZSObakYqH4*w#^Xcnh*hby zJ!ld6p(d3vTXM;i`P|Tru4V%69UdQ(18j-mH~d%4WSMod84qelts-h>W07RvFS?hj zbM(>0inOP6wSm+44o+q#vwDqZ5Q&4-)j!!Qbg*S_7fq@;Bd0MPlA@zyVdO~kF?JxD z!XeF##|t^OxU{$t`1oA0Zghi$n~)idYEtzBCSrfxNa3!(fTrC)PbaHJ&B2$4kyg)R zj4n?`|NJ^M`UFfnx@Oj%|HFsu2UGMe945^a%oO1>Qx4w#rvIK1>{0<7l}d+ZUX`Sn z8uHfzCCT)Pz~9+R5`>>hK}p)RYeu#N)O)*!s+`};C`g}prIga-FaqElsO z6_nOM!dr@}(IUrR^2U`)jA_%%SiXysX6>=kITA?&#qIHJ88!k8s^+0azcZm@#tPV{;-<7XhL-pa9;lHN!9?O7)BVld2_D zs2;P(cM=_jAfS?D8AS4*rR*~rR+)x@5U!5*q^IP+@h=j@v0e_GL|=q@kv0gC5*>Zy ze%Tr4lw70+GK)SN6T>t_d`cY~gz#D0v|a|*;-!z!Dz|B%Hwalr--vctt_=((tl>Bw zAIJLWFcG8n|9poCGXXIS5o&@dzu~X}qu%@ec%UhI0$_37QyiK7O;APPpIZlxI+BC$rk$|qu^~s@xK&0Pop#<~vA+9VaY2iemB-1}TNbgz{?aXHG z7)|=E(qmm&2&S@|cww>X_w1{)g-oNV>S`;2$YTQSz*FhidG~05QAQ~_Kxq+J{?dUt zV{`5-_vwFkqJlN$NNyS#r(c9Y)g?8*nJm!c0K|B3(a;X-67;K^_ z78FO5g(oJnXvkj+FaBB}B8sWW5loFxq>ca&QjsH&pDmOco++?LLqrCfk{w8kkE8il zkjhZ-A02EA{)&Q4r892Bo*NU7{v&Q8kqKLxeHDq@BJ|Ii1Z-S39zK`twwD9qxVjN> z7uI;0D;>t4)C4r}v0sc#Qh%9DLa8EX2zNV|qhB%NH-)>)d2>HQT~IC<)&4FHhrSx0 z!Jm<*WYqk%_h8UO-^8Lb6YG3HD8E4Bn%#}$#oael_u4m`3s~M*=z7`AV2?u= zxrp{?m;^J9?sdEv8tndxfJ3SvYLuRoJ>=N$|ILY**TLN$^HsJL5bcJ)F*6RSkQ{6| z8fo1?9ll)7gqiu9+mI{tRVs-g41T^eNLU2Xd28*vEySq=^dAm4>#h)SXjnrbiBj}i z^7LDAVF#PSk)s2l2Su;ClMPAZn6xpnRn56*yMK;}xR&{k+^)1I>sh-}y!~P9osl1i4Ig8hp&D#G1esPvd{ubn>Y(}5OXe}A&2oH2&* z7U}wbYB|$zDAYENhpFtx8nVk)#9+oQ96ODD88k9u7-B4A>@h+jrV=9|A|f5rVGId{ zv1F|o6i3#`mR*cv>DBd~_d4gjPS<(+@_u@*=f1x^_lM_q|L^~Gcg*4ndO^7UD{zV^ zsD$PEe&wMBBwFb~Yj8~cCF*so zwo1N{O=w3rh^X}KG7~(PF5AxEQS!VsCkw~$gsevS4#)kEF5@5B?gzo^h_`-_0?jb+ zs2{?>|7;G?_-o6YzdTwM(rUnEKn26@7&ezPus-CtEwwXc5NjJhYAe*sm15nWDy=Kd zkP*bcUzm@j^)^k?@V^qaukHA_FU=gb*VQV6bu`|jI|-F$(JI@j6t(0uE&a+q&h`9u zY24%4*ZEp;arJ?Im3Hoih1UK%4xG{#I>VAh?n*f1?YtVaVCHR20!m)|KO6zcTd652 z`gi(Dunry;% z3jPEFdY;&ultTG?7cRpSV+F`-9@uJw|25H^1`DPam;x5eal4J))0MXH8h+Set!9wUuN#7{tJbx{X6-~%pI=1 zCEj;SbI+dg@8OX(6n@Oxr%3uxomHBk_B%`9T4z#?Cqgz0(gK7h9j7GWWzwkLVciG@ zTKw%IXM|X}4G#huz%iacRTq=zHm;e<-XM4tTS2L*8 z5IcAhnighQP+tgMvqTEVk*hVwXPZ6-=oSvT_A8CS?5s&Fdv~gOiuW^?S5?~tNpR+% zGLyj9al%$O{Tw9mmsILVWuEWhypC~ZX#u;l0`eRo!} z-&<+vdw|gM4Fo)K^j-E?OX^4b)gp3phEVW>|vemP`F9w@U=O0){dd$oQpx!mV zCtBAbA-uRk@~};9$y>{$qp87~n-3phcuhSPnW$iFv>9(UL^Mr=E$+}Ec@Z>3Fz?)A zILeqzg^?EoOWk?jmgQ#&bDIzzrBr`V`D`^5)cVA0fM1n-C6pE!N(0()sr6mSn?;l@ zh2?c?$%?zQ8^N3l^m8p z3oai!KslP)n5&Y=Pm@=&ZrNz$JUium^1aPXuZ&i2sGGWzbPn>k}6xp5% z7PeGR$BR-KeC10;sHVCA4>N3=WI{8W2!oZmqEWFW*elDh;Nj48){G2Zri*((l^w>; zwv{q917Zhc`9Da{nL5v1m&EF}us$&PT%+W;zwr7)ZZpADQ}z}~NODKbkfbf-h5@&H z{`mxb#?FSK2m|SA0;$zT{dA?bxTti5k~Dc8MVxfn^>a(N^1PTdwm!xua()sjSwBus%cJm+FmJ#zjbj$;dOBIv-ta1seTj4%(|sl>DvNnw z3t@7W0Np%hj%jKEb(y~2NHJ%3XL@(K+&6&fT)YJ+CyLR@H!13gJ)80MsH(X09Hmyd zM5D;^!f{aN%}O@l?qRryFK@>&&zXF=W3MzNuL@BSUGwU)wdgc0=d0$s0Jk<|v6D?a zq=`MieFL6Kg1!Wxe&5w()5z99dBei@V@ozyUhdlkte8l*Gy*nx+$WPg)1cS)1_ykN z-teI9iShX8@Pxib)&L#YKIskx!i7ue-{5KbVeMtd4c6_#D2mnuRK=b4H9gN)9+EZjZWd!%YXj$YbREPplRoL`( z$Z7dtUFXXN{W)+*!Hq!yO6lm=zx?v*bOZVK<++2i2MiDGehT^fD}-X<7;GfQ+lTeY P`~lWvm60*AF=hQ1LraW- literal 0 HcmV?d00001 diff --git a/docs/informes/2026-04_DASUTEN-migracion/01_informe_ejecutivo_DASUTEN.md b/docs/informes/2026-04_DASUTEN-migracion/01_informe_ejecutivo_DASUTEN.md index 9632a1e1..4b64cc0f 100644 --- a/docs/informes/2026-04_DASUTEN-migracion/01_informe_ejecutivo_DASUTEN.md +++ b/docs/informes/2026-04_DASUTEN-migracion/01_informe_ejecutivo_DASUTEN.md @@ -1,20 +1,20 @@ -# INFORME DE AVANCE +# INFORME EJECUTIVO ## Migración del Sistema DASUTEN a Infraestructura Autónoma > **Área responsable:** Departamento de Tecnología de la Información y Comunicaciones (DTIC) > **Facultad Regional La Rioja — Universidad Tecnológica Nacional** -> **Fecha:** 13 de abril de 2026 +> **Fecha:** 16 de abril de 2026 > **Elaborado por:** Ricardo Monla — DTIC, UTN-FRLR -> **Destinatarios:** [Completar] -> **Versión:** 0.3 (Borrador) +> **Destinatarios:** Autoridades DASUTEN, Autoridades DTIC, Autoridades FR La Rioja +> **Versión:** 1.1 — REVISADO > -> *Este documento es de carácter ejecutivo y narrativo. El detalle técnico completo se encuentra en el documento complementario [02_informe_tecnico_DASUTEN.md](02_informe_tecnico_DASUTEN.md).* +> *Este documento es de carácter ejecutivo y narrativo. Se adjunta también el detalle técnico completo en el documento [02_informe_tecnico_DASUTEN.md](02_informe_tecnico_DASUTEN.md).* --- ## 1. Contexto y Punto de Partida -El sistema de gestión administrativa de **DASUTEN** (Departamento de Acción Social Universitaria Tecnológica Nacional) en la Facultad Regional La Rioja funcionaba desde hace años sobre la infraestructura informática compartida de la Facultad: servidores, red interna y servicios de dominio que atienden a toda la institución. +El sistema de gestión administrativa de **D.A.S.U.Te.N** (Dirección de Acción Social de la Universidad Tecnológica Nacional) en la Facultad Regional La Rioja funcionaba desde hace años sobre la infraestructura informática compartida de la Facultad: servidores, red interna y servicios de dominio que atienden a toda la institución. Si bien esta configuración fue funcional durante un tiempo, con el correr de los años fue manifestando limitaciones concretas: @@ -23,7 +23,7 @@ Si bien esta configuración fue funcional durante un tiempo, con el correr de lo - **Dificultad de mantenimiento:** Los respaldos de datos y las actualizaciones periódicas que envía Rectorado requerían coordinación con la infraestructura general de la Facultad, generando demoras y dependencias cruzadas. - **Limitación de autonomía:** DASUTEN, si bien opera dentro de las instalaciones de la Facultad, responde funcionalmente a **Rectorado (Buenos Aires)**. La dependencia de la infraestructura local dificultaba una eventual gestión directa desde la sede central. -A esta situación se sumó que, al reubicarse la oficina en un espacio propio alejado de los servidores, las fallas de conectividad se agravaron por la cantidad de puntos intermedios en la red. Para dar continuidad al servicio, se gestionó una línea de internet propia y se implementó un esquema de trabajo indirecto donde las usuarias — Andrea Almirón y Romina Molina — accedían mediante escritorio remoto a una PC virtual para poder operar el sistema. Esto implicaba mantener cuatro componentes simultáneamente (servidor de dominio, servidor de base de datos, PC virtual y PC física), multiplicando los puntos de falla. +A esta situación se sumó que, al reubicarse la oficina en un espacio propio alejado de los servidores, las fallas de conectividad se agravaron por la cantidad de puntos intermedios en la red. Para dar continuidad al servicio, se gestionó una línea de internet propia y se implementó un esquema de trabajo indirecto donde las usuarias — **Andrea Almirón (aalmiron)** y **Romina Molina (rmolina)** — accedían mediante escritorio remoto a una PC virtual para poder operar el sistema. Esto implicaba mantener cuatro componentes simultáneamente (servidor de dominio, servidor de base de datos, PC virtual y PC física), multiplicando los puntos de falla. --- @@ -59,7 +59,7 @@ Las usuarias ahora operan el sistema **directamente desde la PC de la oficina**. Si bien el proyecto llevó más tiempo del inicialmente estimado, esto responde a una decisión deliberada: se planificó una **etapa de testeo exhaustivo** antes de llevar el sistema a producción. Cada paso fue validado primero en un entorno de pruebas antes de aplicarse al entorno real, minimizando riesgos para la operación de la oficina. -**A la fecha, el sistema se encuentra a un paso del 100%**, restando únicamente un ajuste de compatibilidad en la impresión de documentos, para el cual ya se dispone de una solución provisoria. +**El sistema se encuentra 100% operativo y validado.** Todos los ajustes de compatibilidad (incluyendo impresión de documentos) fueron resueltos exitosamente. ### 3.3 Valor agregado: herramientas y metodología @@ -68,13 +68,28 @@ Más allá del objetivo central, el trabajo realizado **generó herramientas y m - **Dashboard web de seguimiento** del proyecto, con visibilidad en tiempo real del progreso. - **Sistema de bitácoras** con registro detallado y auditable de cada intervención técnica. - **Herramientas de automatización** (Ecosistema ADN) para respaldos, gestión remota y tareas repetitivas. -- **Metodología de trabajo con IA generativa**, altamente productiva y replicable en futuros proyectos. - **Pipeline de Drones Atómicos** para backups: 6 drones independientes que se encadenan secuencialmente, comunicándose vía JSON. - **Módulo común DasuExecutor** que centraliza la ejecución remota de PowerShell, eliminando código repetido (-52% líneas). +### 3.4 Metodología de trabajo con IA generativa + +Un factor diferencial en este proyecto fue la **incorporación de IA generativa como herramienta de análisis y diseño**, lo que permitió: + +- **Ingeniería inversa acelerada:** La IA asistió en el análisis del sistema DASUTEN (desarrollado en Visual FoxPro, tecnología legacy sin documentación oficial disponible), identificando que la dependencia del Active Directory era **configuracional y no de código** — un hallazgo que permitió eliminar el Domain Controller. + +- **Diseño de arquitectura simplificada:** El enfoque de "red plana" sin subnetting privado fue validado mediante análisis asistido por IA, que identificó los puntos de falla innecesarios en la arquitectura original. + +- **Automatización de código repetitivo:** La IA generativa permitió diseñar el patrón de "Drones Atómicos" y el módulo común `DasuExecutor`, reduciendo ~52% las líneas de código necesarias. + +- **Resolución de problemas complejos:** El problema de impresión (plantillas hardcodeadas en `C:\Sistema\`) se resolvió mediante análisis asistido que identificó el symlink como solución sin necesidad de recompilar el ejecutable VFP. + +- **Velocidad de implementación:** Lo que tradicionalmente hubiera requerido semanas de prueba y error se completó en días, gracias a que la IA permitió **simular mentalmente** múltiples enfoques antes de implementarlos. + +**En síntesis:** La IA actuó como un **multiplicador cognitivo** que permitió estudiar, diseñar e implementar más rápido y mejor, con mayor certeza en las decisiones técnicas. + --- -## 4. Estado Actual +## 5. Estado Actual | Aspecto | Estado | |:---|:---:| @@ -84,27 +99,28 @@ Más allá del objetivo central, el trabajo realizado **generó herramientas y m | Administración remota por DTIC | ✅ | | Respaldos automatizados | ✅ Pipeline implementado | | Impresión de documentos | ✅ Resuelto (symlink) | +| Zona horaria del servidor | ✅ UTC-3 Argentina (configurada 15/04/2026) | +| Validación del usuario | ✅ Confirmada (Andrea Almirón, 15/04/2026 17:00) | -El sistema se encuentra **100% operativo**. La usuaria accede directamente al sistema desde la PC de la oficina. DTIC administra y respalda de forma remota sin interferir con la operación diaria. +El sistema se encuentra **100% operativo y validado**. La usuaria accede directamente al sistema desde la PC de la oficina. DTIC administra y respalda de forma remota sin interferir con la operación diaria. -### 4.1 Pipeline de Backups Implementado +### 5.1 Soluciones técnicas clave -Se implementó un sistema automatizado de backups con las siguientes características: +- **Impresión resuelta:** Mediante un **symlink** que redirige `C:\Sistema` → `C:\SysDasuten\Sistema`, permitiendo que el sistema encuentre las plantillas Word/Excel sin modificar el ejecutable compilado. -- **Backup completo semanal** (~1.3 GB, 35-40 minutos): `./adn/tools/run bkps run dasuten_full` -- **Backup diferencial diario** (~0.4-10 MB, 5-10 minutos): `./adn/tools/run bkps run dasuten_diferencial` -- **Arquitectura de Drones Atómicos**: 6 drones independientes (exportar → transferir → upload → download → restaurar → verificar) -- **Comunicación vía JSON**: Cada dron escribe su output en `/tmp/dron_*_output.json` para el siguiente paso -- **Google Drive como intermediario**: Elimina dependencia de Tailscale inestable -- **Integrado en bkps.rb**: Ejecutable desde el ecosistema ADN de herramientas +- **Backups automatizados:** Pipeline de drones atómicos con backup completo semanal (~1.3 GB, 35-40 min) y diferencial diario (~0.4-10 MB, 5-10 min). Ver detalle técnico en [02_informe_tecnico_DASUTEN.md](02_informe_tecnico_DASUTEN.md#G-pipeline-de-backups). -### 4.2 Solución de Impresión +- **Zona horaria corregida:** El problema de datos "desactualizados" se debía a la zona horaria incorrecta (UTC+2 en lugar de UTC-3). Los backups se realizaban correctamente, pero las fechas se interpretaban con ~5 horas de desplazamiento. -El problema de impresión se resolvió mediante un **symlink** que redirige `C:\Sistema` → `C:\SysDasuten\Sistema`, permitiendo que el sistema encuentre las plantillas Word/Excel sin modificar el ejecutable compilado. +### 5.2 Validación Final con la Usuaria (2026-04-15) + +La usuaria **Andrea Almirón (aalmiron)** confirmó que el sistema desktop muestra correctamente los movimientos del día en curso tras la corrección de la zona horaria en `dasu-sql4` (UTC-3 Argentina). + +**Nota:** Ambas usuarias (Andrea Almirón y Romina Molina) operan el sistema DASUTEN. La validación del 15/04/2026 fue realizada por Andrea Almirón. --- -## 5. Beneficios Alcanzados +## 6. Beneficios Alcanzados - **Trabajo directo:** Las usuarias operan el sistema sin intermediarios. - **Autonomía operativa:** Infraestructura propia, desvinculada de la Facultad, preparada para gestión desde Rectorado. @@ -115,7 +131,7 @@ El problema de impresión se resolvió mediante un **symlink** que redirige `C:\ --- -## 6. Próximos Pasos +## 7. Próximos Pasos 1. ✅ **Impresión resuelta** (symlink `C:\Sistema` → `C:\SysDasuten\Sistema`). 2. ✅ **Configuración de red estabilizada** (red plana DHCP del ISP). @@ -125,7 +141,7 @@ El problema de impresión se resolvió mediante un **symlink** que redirige `C:\ --- -## 7. Conclusión +## 8. Conclusión La migración del sistema DASUTEN demuestra que es posible **lograr una mejora sustancial en estabilidad, autonomía y experiencia de trabajo** partiendo de recursos limitados y sin inversión adicional. @@ -143,5 +159,7 @@ DASUTEN opera hoy de forma independiente, administrable remotamente por DTIC y p | 0.2 | 13/04/2026 | Se amplía contexto de situación inicial | | 0.3 | 13/04/2026 | Se retoma enfoque narrativo, se integra contexto como complemento | | 0.4 | 14/04/2026 | Impresión resuelta + Pipeline de backups implementado | +| 1.0 | 15/04/2026 | COMPLETADO Y VALIDADO — Zona horaria corregida (UTC-3), validación usuaria Andrea Almirón confirma sistema operativo | +| **1.1** | **16/04/2026** | **REVISADO** — Se agrega sección sobre metodología de trabajo con IA generativa como factor diferencial | *Fin del informe.* diff --git a/docs/informes/2026-04_DASUTEN-migracion/01_informe_ejecutivo_DASUTEN.pdf b/docs/informes/2026-04_DASUTEN-migracion/01_informe_ejecutivo_DASUTEN.pdf index 02f11de22e59b85a01d8df7d43a9a9203c9ced7f..461405441d94fb51a3658cb79e191d3b8227f661 100644 GIT binary patch literal 53381 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