From 25ee3cbbb61f861df583d62118a3be462fe795d8 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 26 Jan 2026 09:54:40 +0100 Subject: [PATCH] Add RelationshipChangeLog model and enhance character loading logic --- ...0126000000-add-relationship-change-log.cjs | 105 ++++++++++++++++++ .../falukant/log/relationship_change_log.js | 49 ++++++++ backend/models/index.js | 2 + frontend/src/components/Character3D.vue | 34 +++++- 4 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 backend/migrations/20260126000000-add-relationship-change-log.cjs create mode 100644 backend/models/falukant/log/relationship_change_log.js diff --git a/backend/migrations/20260126000000-add-relationship-change-log.cjs b/backend/migrations/20260126000000-add-relationship-change-log.cjs new file mode 100644 index 0000000..20bb204 --- /dev/null +++ b/backend/migrations/20260126000000-add-relationship-change-log.cjs @@ -0,0 +1,105 @@ +/* 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/models/falukant/log/relationship_change_log.js b/backend/models/falukant/log/relationship_change_log.js new file mode 100644 index 0000000..0ad7eed --- /dev/null +++ b/backend/models/falukant/log/relationship_change_log.js @@ -0,0 +1,49 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +/** + * Log aller Änderungen an relationship und marriage_proposals. + * Einträge werden ausschließlich durch DB-Trigger geschrieben und nicht gelöscht. + * Hilft zu analysieren, warum z.B. Werbungen um einen Partner verschwinden. + */ +class RelationshipChangeLog extends Model {} + +RelationshipChangeLog.init( + { + changedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + tableName: { + type: DataTypes.STRING(64), + allowNull: false + }, + operation: { + type: DataTypes.STRING(16), + allowNull: false + }, + recordId: { + type: DataTypes.INTEGER, + allowNull: true + }, + payloadOld: { + type: DataTypes.JSONB, + allowNull: true + }, + payloadNew: { + type: DataTypes.JSONB, + allowNull: true + } + }, + { + sequelize, + modelName: 'RelationshipChangeLog', + tableName: 'relationship_change_log', + schema: 'falukant_log', + timestamps: false, + underscored: true + } +); + +export default RelationshipChangeLog; diff --git a/backend/models/index.js b/backend/models/index.js index 5fed70c..6bd96f4 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -113,6 +113,7 @@ import Vote from './falukant/data/vote.js'; import ElectionResult from './falukant/data/election_result.js'; import PoliticalOfficeHistory from './falukant/log/political_office_history.js'; import ElectionHistory from './falukant/log/election_history.js'; +import RelationshipChangeLog from './falukant/log/relationship_change_log.js'; // — Kirchliche Ämter (Church) — import ChurchOfficeType from './falukant/type/church_office_type.js'; @@ -248,6 +249,7 @@ const models = { ElectionResult, PoliticalOfficeHistory, ElectionHistory, + RelationshipChangeLog, ChurchOfficeType, ChurchOfficeRequirement, ChurchOffice, diff --git a/frontend/src/components/Character3D.vue b/frontend/src/components/Character3D.vue index 8fc9054..deda777 100644 --- a/frontend/src/components/Character3D.vue +++ b/frontend/src/components/Character3D.vue @@ -82,6 +82,13 @@ export default { const base = getApiBaseURL(); const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH; return `${prefix}/${this.actualGender}_${this.ageGroup}.glb`; + }, + exactAgeModelPath() { + // Pfad für genaues Alter (z.B. female_1y.glb für Alter 1) + const age = this.actualAge; + const base = getApiBaseURL(); + const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH; + return `${prefix}/${this.actualGender}_${age}y.glb`; } }, watch: { @@ -207,15 +214,32 @@ export default { const base = getApiBaseURL(); const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH; - const modelPath = this.modelPath; + + // Fallback-Hierarchie: + // 1. Zuerst versuchen, Modell für genaues Alter zu laden (z.B. female_1y.glb) + // 2. Falls nicht vorhanden, Altersbereich verwenden (z.B. female_toddler.glb) + // 3. Falls auch nicht vorhanden, Basis-Modell verwenden (z.B. female.glb) + const exactAgePath = this.exactAgeModelPath; + const ageGroupPath = this.modelPath; const fallbackPath = `${prefix}/${this.actualGender}.glb`; let gltf; try { - gltf = await loader.loadAsync(modelPath); - } catch (error) { - console.warn(`Could not load ${modelPath}, trying fallback model`); - gltf = await loader.loadAsync(fallbackPath); + // Versuche zuerst genaues Alter + try { + gltf = await loader.loadAsync(exactAgePath); + console.log(`Loaded exact age model: ${exactAgePath}`); + } catch (exactAgeError) { + // Falls genaues Alter nicht existiert, versuche Altersbereich + try { + gltf = await loader.loadAsync(ageGroupPath); + console.log(`Loaded age group model: ${ageGroupPath}`); + } catch (ageGroupError) { + // Falls Altersbereich nicht existiert, verwende Basis-Modell + console.warn(`Could not load ${ageGroupPath}, trying fallback model`); + gltf = await loader.loadAsync(fallbackPath); + } + } } finally { dracoLoader.dispose(); }