#!/usr/bin/env node /** * Aggregiert falukant_log.moneyflow nach activity (reale Buchungen aus dem Spiel). * * cd backend && npm run diag:moneyflow * * Umgebung: * DIAG_DAYS=30 — Fenster in Tagen (1–3650, Standard 30) * DIAG_USER_ID=123 — optional: nur dieser falukant_user * DIAG_QUERY_TIMEOUT_MS, DIAG_AUTH_TIMEOUT_MS — wie diag:town-worth */ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const sqlByActivity = join(__dirname, '../sql/diagnostics/falukant_moneyflow_by_activity.sql'); const sqlTotals = join(__dirname, '../sql/diagnostics/falukant_moneyflow_window_totals.sql'); const QUERY_TIMEOUT_MS = Number.parseInt(process.env.DIAG_QUERY_TIMEOUT_MS || '60000', 10); const AUTH_TIMEOUT_MS = Number.parseInt(process.env.DIAG_AUTH_TIMEOUT_MS || '25000', 10); process.env.QUIET_ENV_LOGS = process.env.QUIET_ENV_LOGS || '1'; process.env.DOTENV_CONFIG_QUIET = process.env.DOTENV_CONFIG_QUIET || '1'; if (!process.env.DB_CONNECT_TIMEOUT_MS) { process.env.DB_CONNECT_TIMEOUT_MS = '15000'; } await import('../config/loadEnv.js'); const { sequelize } = await import('../utils/sequelize.js'); function withTimeout(promise, ms, onTimeoutError) { let timerId; const timeoutPromise = new Promise((_, reject) => { timerId = setTimeout(() => reject(new Error(onTimeoutError)), ms); }); return Promise.race([promise, timeoutPromise]).finally(() => { clearTimeout(timerId); }); } function raceQuery(sql) { return withTimeout( sequelize.query(sql), QUERY_TIMEOUT_MS, `Abfrage-Timeout nach ${QUERY_TIMEOUT_MS} ms (DIAG_QUERY_TIMEOUT_MS)` ); } function raceAuth() { return withTimeout( sequelize.authenticate(), AUTH_TIMEOUT_MS, `authenticate() Timeout nach ${AUTH_TIMEOUT_MS} ms (DIAG_AUTH_TIMEOUT_MS).` ); } function printConnectionHints() { const port = process.env.DB_PORT || '5432'; const host = process.env.DB_HOST || '?'; const local = host === '127.0.0.1' || host === 'localhost' || host === '::1'; console.error(''); console.error('[diag] Mögliche Ursachen:'); if (local) { console.error(' • SSH-Tunnel: Läuft z. B. ssh -L ' + port + ':127.0.0.1:5432 …? Dann DB_HOST=127.0.0.1 DB_PORT=' + port + ' (DB_SSL meist aus).'); console.error(' • Falscher lokaler Forward-Port in .env (DB_PORT).'); } else { console.error(' • PostgreSQL erwartet TLS: in .env DB_SSL=1 setzen (ggf. DB_SSL_REJECT_UNAUTHORIZED=0 bei selbstsigniert).'); console.error(' • Falscher Port: DB_PORT=' + port + ' prüfen.'); console.error(' • Server-Firewall: deine IP muss für Port', port, 'auf', host, 'freigeschaltet sein.'); } console.error(' • Test: nc -zv', host, port); console.error(''); } function parseDays() { const raw = process.env.DIAG_DAYS; const n = raw === undefined || raw === '' ? 30 : Number.parseInt(raw, 10); if (!Number.isFinite(n) || n < 1 || n > 3650) { throw new Error('DIAG_DAYS muss eine Ganzzahl zwischen 1 und 3650 sein'); } return n; } function parseOptionalUserFilter() { const raw = process.env.DIAG_USER_ID; if (raw === undefined || raw === '') { return ''; } const uid = Number.parseInt(raw, 10); if (!Number.isInteger(uid) || uid < 1) { throw new Error('DIAG_USER_ID muss eine positive Ganzzahl sein (falukant_user.id)'); } return ` AND m.falukant_user_id = ${uid}`; } function applyPlaceholders(sql, days, userFilter) { return sql .replace(/__DIAG_DAYS__/g, String(days)) .replace(/__DIAG_USER_FILTER__/g, userFilter); } try { const days = parseDays(); const userFilter = parseOptionalUserFilter(); const host = process.env.DB_HOST || '(unbekannt)'; const t0 = Date.now(); console.log(''); console.log('[diag] moneyflow — Fenster:', days, 'Tage', userFilter ? `(User ${process.env.DIAG_USER_ID})` : '(alle Nutzer)'); console.log('[diag] PostgreSQL: authenticate() … (Host:', host + ', Port:', process.env.DB_PORT || '5432', ', DB_SSL:', process.env.DB_SSL === '1' ? '1' : '0', ')'); console.log(''); await raceAuth(); console.log('[diag] authenticate() ok nach', Date.now() - t0, 'ms'); await sequelize.query("SET statement_timeout = '60s'"); const sql1 = applyPlaceholders(readFileSync(sqlByActivity, 'utf8'), days, userFilter); const sql2 = applyPlaceholders(readFileSync(sqlTotals, 'utf8'), days, userFilter); const t1 = Date.now(); const [totalsRows] = await raceQuery(sql2); console.log('[diag] Fenster-Totals nach', Date.now() - t1, 'ms'); console.table(totalsRows); const t2 = Date.now(); const [byActivity] = await raceQuery(sql1); console.log('[diag] Nach activity nach', Date.now() - t2, 'ms (gesamt', Date.now() - t0, 'ms)'); console.log(''); console.table(byActivity); await sequelize.close(); process.exit(0); } catch (err) { console.error(err.message || err); printConnectionHints(); try { await sequelize.close(); } catch (_) {} process.exit(1); }