diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a1413e5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +splinker/ diff --git a/eslint.config.mts b/eslint.config.mts index d88d489..09cf78b 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -14,7 +14,8 @@ export default defineConfig([ 'mysql/', 'public/', 'coverage/', - 'node_modules/' + 'node_modules/', + 'splinker/' ] }, { diff --git a/splinker/Dockerfile b/splinker/Dockerfile index e15cba2..749e389 100644 --- a/splinker/Dockerfile +++ b/splinker/Dockerfile @@ -1,26 +1,42 @@ FROM eclipse-temurin:21-jre ARG SPLINKER_VERSION=7.2.7 +ARG NODE_VERSION=22.20.0 WORKDIR /app -RUN \ - apt-get update && \ +# Install minimal packages, download splinker.jar and Node.js binary +RUN apt-get update && \ apt-get install -y \ cron \ tzdata \ - curl && \ + curl \ + xz-utils \ + ca-certificates && \ curl -fsSL https://github.com/cria/splinker-javafx/releases/download/v${SPLINKER_VERSION}/splinker.jar \ -o /app/splinker.jar && \ - apt-get remove -y curl && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz -o /tmp/node.tar.xz && \ + tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 && \ + rm -f /tmp/node.tar.xz && \ + corepack enable && \ + apt-get remove -y curl xz-utils && \ + apt autoremove -y && \ rm -rf /var/lib/apt/lists/* +# Copy package.json and yarn.lock from splinker/ and install dependencies +COPY package.json yarn.lock /app/ +RUN yarn install --production && \ + yarn cache clean --force + ENV \ TZ=America/Sao_Paulo \ CRON_SCHEDULE="0 0 * * *" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +COPY save_logs.mjs run_splinker.sh /app/ +RUN chmod +x /app/run_splinker.sh + COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/splinker/compose.yml b/splinker/compose.yml index 6b66e62..93ae4b3 100644 --- a/splinker/compose.yml +++ b/splinker/compose.yml @@ -4,3 +4,5 @@ services: container_name: splinker build: . env_file: .env + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/splinker/entrypoint.sh b/splinker/entrypoint.sh index 07eae8f..a3e09da 100644 --- a/splinker/entrypoint.sh +++ b/splinker/entrypoint.sh @@ -1,6 +1,17 @@ #!/bin/sh +# Export container environment variables to /etc/environment so cron can read them +printenv | grep -v "^\(HOME\|USER\|LOGNAME\|SHELL\|PATH\)=" >> /etc/environment + echo "Generating config from environment variables..." + +# Expect DATABASE_* variables to be provided by the environment/container +: "${DATABASE_HOST:?DATABASE_HOST is required}" +: "${DATABASE_PORT:?DATABASE_PORT is required}" +: "${DATABASE_NAME:?DATABASE_NAME is required}" +: "${DATABASE_USERNAME:?DATABASE_USERNAME is required}" +: "${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}" + cat > /app/splinker.conf < /proc/1/fd/1 2> /proc/1/fd/2" | crontab - +echo "$CRON_SCHEDULE /app/run_splinker.sh 1> /proc/1/fd/1 2> /proc/1/fd/2" | crontab - if ! crontab -l >/dev/null 2>&1; then echo "Failed to configure cron job" diff --git a/splinker/package.json b/splinker/package.json new file mode 100644 index 0000000..c382272 --- /dev/null +++ b/splinker/package.json @@ -0,0 +1,8 @@ +{ + "name": "splinker-helper", + "version": "0.1.0", + "private": true, + "dependencies": { + "pg": "^8.16.3" + } +} diff --git a/splinker/run_splinker.sh b/splinker/run_splinker.sh new file mode 100644 index 0000000..460f968 --- /dev/null +++ b/splinker/run_splinker.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# Roda o jar do splinker, duplica o output para os logs do Docker e envia para o script de log +# Usamos tee para que a saída também seja registrada nos logs do container +# Caminhos absolutos são necessários pois o cron tem um PATH mínimo +/opt/java/openjdk/bin/java -jar /app/splinker.jar /app/splinker.conf 2>&1 | tee /proc/1/fd/1 | /usr/local/bin/node /app/save_logs.mjs diff --git a/splinker/save_logs.mjs b/splinker/save_logs.mjs new file mode 100644 index 0000000..d91183a --- /dev/null +++ b/splinker/save_logs.mjs @@ -0,0 +1,81 @@ +import { Client } from 'pg' + +const DATABASE_HOST = process.env.DATABASE_HOST +const DATABASE_PORT = process.env.DATABASE_PORT +const DATABASE_NAME = process.env.DATABASE_NAME +const DATABASE_USERNAME = process.env.DATABASE_USERNAME +const DATABASE_PASSWORD = process.env.DATABASE_PASSWORD + +async function readStdin() { + let result = '' + process.stdin.setEncoding('utf8') + + for await (const chunk of process.stdin) { + result += chunk + } + + return result +} + +function parseSuccess(logData) { + return ( + logData.includes('Transmission completed successfully') + || /Success:\s*[1-9]/.test(logData) + ) +} + +function createClient() { + if (!DATABASE_NAME || !DATABASE_USERNAME || !DATABASE_PASSWORD) { + throw new Error('Database connection variables are not fully provided.') + } + + return new Client({ + host: DATABASE_HOST, + port: parseInt(DATABASE_PORT, 10), + database: DATABASE_NAME, + user: DATABASE_USERNAME, + password: DATABASE_PASSWORD + }) +} + +async function main() { + const logData = await readStdin() + const sucesso = parseSuccess(logData) + const client = createClient() + + try { + await client.connect() + await client.query("SET timezone TO 'America/Sao_Paulo'") + + const res = await client.query( + 'SELECT MAX("CatalogNumber") as ultimo_tombo_hcf FROM vw_splinker' + ) + const ultimoTomboHcf = res.rows[0]?.ultimo_tombo_hcf ?? null + + const insertQuery = ` + INSERT INTO splinker_execucoes (ultimo_tombo_hcf, sucesso, log_saida) + VALUES ($1, $2, $3) + ` + + await client.query( + insertQuery, + [ + ultimoTomboHcf, + sucesso, + logData + ] + ) + + console.log('Execução do Splinker registrada em splinker_execucoes.') + } catch (error) { + console.error('Erro ao salvar os logs:', error instanceof Error ? error.message : error) + process.exit(1) + } finally { + await client.end() + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error) + process.exit(1) +}) diff --git a/splinker/yarn.lock b/splinker/yarn.lock new file mode 100644 index 0000000..d5e66ed --- /dev/null +++ b/splinker/yarn.lock @@ -0,0 +1,91 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +pg-cloudflare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz#4b4c20e6d8ae531d400730f4804571a8d62f1497" + integrity sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A== + +pg-connection-string@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.13.0.tgz#8678113465a5af3cc977dcb51eadc847b27aa2de" + integrity sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.14.0.tgz#f35ae4eb846780cad71af24099b3edfa9781ad90" + integrity sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw== + +pg-protocol@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.14.0.tgz#c1f045b74274b007078c687147141f785f59b8de" + integrity sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA== + +pg-types@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.16.3: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.21.0.tgz#d7fa2118d960cec5cc7d2b24525f9850dd5932b0" + integrity sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA== + dependencies: + pg-connection-string "^2.13.0" + pg-pool "^3.14.0" + pg-protocol "^1.14.0" + pg-types "2.2.0" + pgpass "1.0.5" + optionalDependencies: + pg-cloudflare "^1.4.0" + +pgpass@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz#c40b3da0222c500ff1e51c5d7014b60b79697c7a" + integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== diff --git a/src/database/migration/20260530_cria_splinker_execucoes.ts b/src/database/migration/20260530_cria_splinker_execucoes.ts new file mode 100644 index 0000000..0254e16 --- /dev/null +++ b/src/database/migration/20260530_cria_splinker_execucoes.ts @@ -0,0 +1,14 @@ +import { Knex } from 'knex' + +export async function run(knex: Knex): Promise { + const hasTable = await knex.schema.hasTable('splinker_execucoes') + if (!hasTable) { + await knex.schema.createTable('splinker_execucoes', table => { + table.increments('id').primary() + table.timestamp('data_hora').defaultTo(knex.fn.now()) + table.integer('ultimo_tombo_hcf') + table.boolean('sucesso') + table.text('log_saida') + }) + } +}