diff --git a/backend/migrations/20260126000000-add-relationship-change-log.cjs b/backend/migrations/20260126000000-add-relationship-change-log.cjs deleted file mode 100644 index 20bb204..0000000 --- a/backend/migrations/20260126000000-add-relationship-change-log.cjs +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable */ -'use strict'; - -/** Log-Tabelle für alle Änderungen an relationship und marriage_proposals (keine Einträge werden gelöscht). - * Hilft zu analysieren, warum z.B. Werbungen um einen Partner über Nacht verschwinden. */ - -module.exports = { - async up(queryInterface, Sequelize) { - await queryInterface.sequelize.query(` - CREATE TABLE IF NOT EXISTS falukant_log.relationship_change_log ( - id serial PRIMARY KEY, - changed_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - table_name character varying(64) NOT NULL, - operation character varying(16) NOT NULL, - record_id integer, - payload_old jsonb, - payload_new jsonb - ); - `); - await queryInterface.sequelize.query(` - CREATE INDEX IF NOT EXISTS relationship_change_log_changed_at_idx - ON falukant_log.relationship_change_log (changed_at); - `); - await queryInterface.sequelize.query(` - CREATE INDEX IF NOT EXISTS relationship_change_log_table_operation_idx - ON falukant_log.relationship_change_log (table_name, operation); - `); - - const triggerFunction = ` - CREATE OR REPLACE FUNCTION falukant_log.log_relationship_change() - RETURNS TRIGGER AS $$ - DECLARE - v_record_id INTEGER; - v_payload_old JSONB; - v_payload_new JSONB; - BEGIN - IF TG_OP = 'INSERT' THEN - v_record_id := NEW.id; - v_payload_old := NULL; - v_payload_new := to_jsonb(NEW); - ELSIF TG_OP = 'UPDATE' THEN - v_record_id := NEW.id; - v_payload_old := to_jsonb(OLD); - v_payload_new := to_jsonb(NEW); - ELSIF TG_OP = 'DELETE' THEN - v_record_id := OLD.id; - v_payload_old := to_jsonb(OLD); - v_payload_new := NULL; - END IF; - - INSERT INTO falukant_log.relationship_change_log ( - table_name, - operation, - record_id, - payload_old, - payload_new - ) VALUES ( - TG_TABLE_NAME, - TG_OP, - v_record_id, - v_payload_old, - v_payload_new - ); - - IF TG_OP = 'DELETE' THEN - RETURN OLD; - ELSE - RETURN NEW; - END IF; - END; - $$ LANGUAGE plpgsql; - `; - await queryInterface.sequelize.query(triggerFunction); - - await queryInterface.sequelize.query(` - DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.relationship; - CREATE TRIGGER trg_log_relationship_change - AFTER INSERT OR UPDATE OR DELETE ON falukant_data.relationship - FOR EACH ROW - EXECUTE FUNCTION falukant_log.log_relationship_change(); - `); - await queryInterface.sequelize.query(` - DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.marriage_proposals; - CREATE TRIGGER trg_log_relationship_change - AFTER INSERT OR UPDATE OR DELETE ON falukant_data.marriage_proposals - FOR EACH ROW - EXECUTE FUNCTION falukant_log.log_relationship_change(); - `); - }, - - async down(queryInterface, Sequelize) { - await queryInterface.sequelize.query(` - DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.relationship; - `); - await queryInterface.sequelize.query(` - DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.marriage_proposals; - `); - await queryInterface.sequelize.query(` - DROP FUNCTION IF EXISTS falukant_log.log_relationship_change(); - `); - await queryInterface.sequelize.query(` - DROP TABLE IF EXISTS falukant_log.relationship_change_log; - `); - }, -}; diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 2bf5968..ee41149 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -3158,16 +3158,23 @@ class FalukantService extends BaseService { const step7Start = Date.now(); const ownAge = calcAge(character.birthdate); if (ownAge >= 12 && family.relationships.length === 0) { - family.possiblePartners = await this.getPossiblePartners(character.id); - if (family.possiblePartners.length === 0) { - // Asynchron erstellen, nicht blockieren - this.createPossiblePartners( - character.id, - character.gender, - character.regionId, - character.titleOfNobility, - ownAge - ).catch(err => console.error('[getFamily] Error creating partners (async):', err)); + try { + family.possiblePartners = await this.getPossiblePartners(character.id); + if (family.possiblePartners.length === 0) { + // Asynchron erstellen, nicht blockieren + this.createPossiblePartners( + character.id, + character.gender, + character.regionId, + character.titleOfNobility, + ownAge + ).catch(err => console.error('[getFamily] Error creating partners (async):', err)); + } + } catch (err) { + // Fehler beim Laden nicht an Frontend durchreichen – Seite bleibt nutzbar, Log für Analyse + console.error('[getFamily] getPossiblePartners failed (characterId=%s):', character.id, err?.message || err); + if (err?.stack) console.error('[getFamily] getPossiblePartners stack:', err.stack); + family.possiblePartners = []; } } timings.step7_possible_partners = Date.now() - step7Start; @@ -3399,32 +3406,43 @@ class FalukantService extends BaseService { { model: FalukantCharacter, as: 'proposedCharacter', + required: false, attributes: ['id', 'firstName', 'lastName', 'gender', 'regionId', 'birthdate'], include: [ - { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, - { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }, - { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }, + { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'], required: false }, + { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'], required: false }, + { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'], required: false }, ], }, ], }); - return proposals - .filter(proposal => proposal.proposedCharacter !== null) - .map(proposal => { - const birthdate = new Date(proposal.proposedCharacter.birthdate); - const age = calcAge(birthdate); - return { - id: proposal.id, - requesterCharacterId: proposal.requesterCharacterId, - proposedCharacterId: proposal.proposedCharacter.id, - proposedCharacterName: `${proposal.proposedCharacter.definedFirstName?.name} ${proposal.proposedCharacter.definedLastName?.name}`, - proposedCharacterGender: proposal.proposedCharacter.gender, - proposedCharacterRegionId: proposal.proposedCharacter.regionId, - proposedCharacterAge: age, - proposedCharacterNobleTitle: proposal.proposedCharacter.nobleTitle?.labelTr || null, + const valid = []; + for (const proposal of proposals) { + if (proposal.proposedCharacter == null) { + console.warn('[getPossiblePartners] Proposal id=%s filtered out (proposedCharacterId=%s missing)', proposal.id, proposal.proposedCharacterId); + continue; + } + const pc = proposal.proposedCharacter; + const birthdate = pc.birthdate ? new Date(pc.birthdate) : null; + const age = birthdate ? calcAge(birthdate) : 0; + const name = `${pc.definedFirstName?.name ?? ''} ${pc.definedLastName?.name ?? ''}`.trim() || null; + if (!name) { + console.warn('[getPossiblePartners] Proposal id=%s filtered out (proposedCharacterId=%s has no name)', proposal.id, pc.id); + continue; + } + valid.push({ + id: proposal.id, + requesterCharacterId: proposal.requesterCharacterId, + proposedCharacterId: pc.id, + proposedCharacterName: name, + proposedCharacterGender: pc.gender, + proposedCharacterRegionId: pc.regionId, + proposedCharacterAge: age, + proposedCharacterNobleTitle: pc.nobleTitle?.labelTr ?? null, cost: proposal.cost, - }; - }); + }); + } + return valid; } async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility, ownAge) { diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index a51fd35..7968f22 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -387,6 +387,77 @@ const syncDatabase = async () => { console.warn('⚠️ Konnte Vocab-Trainer Tabellen nicht sicherstellen:', e?.message || e); } + // Relationship-/Marriage-Proposal-Änderungen loggen (keine Einträge löschen; ohne db:migrate) + console.log("Ensuring relationship change log (falukant) exists..."); + try { + await sequelize.query(` + CREATE TABLE IF NOT EXISTS falukant_log.relationship_change_log ( + id serial PRIMARY KEY, + changed_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + table_name character varying(64) NOT NULL, + operation character varying(16) NOT NULL, + record_id integer, + payload_old jsonb, + payload_new jsonb + ); + `); + await sequelize.query(` + CREATE INDEX IF NOT EXISTS relationship_change_log_changed_at_idx + ON falukant_log.relationship_change_log (changed_at); + `); + await sequelize.query(` + CREATE INDEX IF NOT EXISTS relationship_change_log_table_operation_idx + ON falukant_log.relationship_change_log (table_name, operation); + `); + await sequelize.query(` + CREATE OR REPLACE FUNCTION falukant_log.log_relationship_change() + RETURNS TRIGGER AS $$ + DECLARE + v_record_id INTEGER; + v_payload_old JSONB; + v_payload_new JSONB; + BEGIN + IF TG_OP = 'INSERT' THEN + v_record_id := NEW.id; + v_payload_old := NULL; + v_payload_new := to_jsonb(NEW); + ELSIF TG_OP = 'UPDATE' THEN + v_record_id := NEW.id; + v_payload_old := to_jsonb(OLD); + v_payload_new := to_jsonb(NEW); + ELSIF TG_OP = 'DELETE' THEN + v_record_id := OLD.id; + v_payload_old := to_jsonb(OLD); + v_payload_new := NULL; + END IF; + INSERT INTO falukant_log.relationship_change_log ( + table_name, operation, record_id, payload_old, payload_new + ) VALUES ( + TG_TABLE_NAME, TG_OP, v_record_id, v_payload_old, v_payload_new + ); + IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; + END; + $$ LANGUAGE plpgsql; + `); + await sequelize.query(` + DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.relationship; + CREATE TRIGGER trg_log_relationship_change + AFTER INSERT OR UPDATE OR DELETE ON falukant_data.relationship + FOR EACH ROW + EXECUTE FUNCTION falukant_log.log_relationship_change(); + `); + await sequelize.query(` + DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.marriage_proposals; + CREATE TRIGGER trg_log_relationship_change + AFTER INSERT OR UPDATE OR DELETE ON falukant_data.marriage_proposals + FOR EACH ROW + EXECUTE FUNCTION falukant_log.log_relationship_change(); + `); + console.log("✅ relationship_change_log und Trigger sind vorhanden."); + } catch (e) { + console.warn('⚠️ relationship_change_log/Trigger konnten nicht sichergestellt werden:', e?.message || e); + } + // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt console.log("Pre-ensure Taxi columns (traffic_light) ..."); try {