Update environment configuration and enhance logging: Add support for loading a local .env file and improve logging behavior based on QUIET_ENV_LOGS settings. Introduce new diagnostic scripts in package.json for town worth and money flow analysis. Adjust production cost calculations in FalukantService to align with updated pricing logic and enhance product initialization parameters.

This commit is contained in:
Torsten Schulz (local)
2026-03-25 13:23:51 +01:00
parent 44991743d2
commit 8af726c65a
15 changed files with 498 additions and 46 deletions

View File

@@ -0,0 +1,142 @@
#!/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 (13650, 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);
}