Compare commits

...

27 Commits

Author SHA1 Message Date
Torsten Schulz (local)
cd739fb52e Refactor server.js for improved WebSocket and API configurations
- Updated API server to listen on a configurable port (2020) for better flexibility.
- Removed WebSocket setup from HTTP server, ensuring Socket.io is only available over HTTPS.
- Enhanced logging to clarify Socket.io availability based on TLS configuration.
2026-01-14 14:30:50 +01:00
Torsten Schulz (local)
9e845843d8 Update WebSocket and API configurations in yourpart-websocket-fixed.conf and daemonServer.js
- Adjusted WebSocket proxy settings in yourpart-websocket-fixed.conf to route traffic through port 4551 for both secure and non-secure connections.
- Enhanced daemonServer.js to listen on all interfaces (0.0.0.0) for both TLS and non-TLS WebSocket connections, improving accessibility.
2026-01-14 13:10:33 +01:00
Torsten Schulz (local)
0cc280ed55 Refactor WebSocket and API configurations in yourpart-https.conf and yourpart-websocket-fixed.conf
- Removed outdated WebSocket handling from yourpart-https.conf for improved clarity.
- Updated yourpart-websocket-fixed.conf to enable SSL and adjust WebSocket proxy settings.
- Streamlined fallback logic in frontend store to ensure direct connection to the daemon on port 4551.
- Enhanced logging for better debugging and monitoring of daemon connections.
2026-01-14 13:02:38 +01:00
Torsten Schulz (local)
b3707d21b2 Refactor yourpart-https.conf for enhanced WebSocket and API request handling
- Updated www redirect to exclude API and WebSocket paths for improved functionality.
- Organized WebSocket and API proxy settings within Location blocks for better clarity and maintainability.
- Consolidated WebSocket upgrade rules for Socket.io and daemon connections, ensuring consistent handling.
2026-01-14 12:07:04 +01:00
Torsten Schulz (local)
fbebd6c1c1 Refactor yourpart-https.conf for improved WebSocket handling and domain redirection
- Updated www redirect to exclude WebSocket paths for better functionality.
- Consolidated WebSocket upgrade rules for Socket.io and daemon connections.
- Enhanced organization of proxy settings for API and WebSocket requests, improving maintainability.
2026-01-14 12:02:49 +01:00
Torsten Schulz (local)
d7c2bda461 Enhance yourpart-https.conf with improved WebSocket and API configurations
- Added www redirect to ensure consistent domain usage.
- Consolidated WebSocket upgrade conditions for clarity.
- Streamlined API request forwarding and fallback proxy settings for better organization and maintainability.
2026-01-14 11:57:35 +01:00
Torsten Schulz (local)
2bf949513b Refactor yourpart-https.conf for improved WebSocket and API request handling
- Consolidated WebSocket upgrade conditions for clarity and consistency.
- Streamlined API request forwarding configuration.
- Removed redundant proxy settings to enhance organization and maintainability of the configuration file.
2026-01-14 10:28:23 +01:00
Torsten Schulz (local)
84619fb656 Update proxy settings in yourpart-https.conf for improved WebSocket and API handling
- Added ProxyPreserveHost and ProxyRequests directives for better request handling.
- Configured WebSocket upgrade headers for Socket.io and daemon connections.
- Established HTTP proxies for API and WebSocket requests to ensure fallback mechanisms are in place.
- Improved overall clarity and organization of proxy settings in the configuration file.
2026-01-12 16:55:09 +01:00
Torsten Schulz (local)
b600f16ecd Enhance MessagesDialog component and localization for overproduction notifications
- Updated MessagesDialog.vue to extract additional parameters (value, branch_id, region_id) for better handling of overproduction scenarios.
- Modified localization files (de/falukant.json and en/falukant.json) to reflect changes in the overproduction notification format, including branch information.
- Improved data formatting for clarity in notifications related to production levels.
2026-01-12 16:48:10 +01:00
Torsten Schulz (local)
9273066f61 Refactor trait handling in FalukantService and FamilyView for improved data consistency
- Updated trait loading in FalukantService to include trait types, enhancing the mapping of traits to characters.
- Adjusted FamilyView to utilize the new trait structure, ensuring accurate retrieval of traits associated with characters.
- Improved null checks for relationships to prevent potential errors during data access.
2026-01-12 13:48:37 +01:00
Torsten Schulz (local)
7d59dbcf84 Update mood and character traits handling in FalukantService to ensure default values are set when no data is available. This change improves robustness in data retrieval by preventing potential undefined values. 2026-01-12 13:44:29 +01:00
Torsten Schulz (local)
015d1ae95b Refactor getGiftCost method in FalukantService for improved performance
- Changed getGiftCost from an asynchronous to a synchronous method, eliminating unnecessary await for better efficiency.
- Updated comments for clarity regarding the synchronous nature of gift cost calculation.
- Adjusted related code to reflect the synchronous execution, enhancing overall performance in gift cost retrieval.
2026-01-12 12:02:46 +01:00
Torsten Schulz (local)
e2cd6e0e5e Refactor relationship retrieval in FalukantService for improved performance
- Optimized the process of finding relationships by using two separate queries for better index utilization, reducing data overhead.
- Enhanced loading of related characters and traits by implementing parallel data fetching, improving efficiency in data retrieval.
- Updated timing metrics to reflect changes in the relationship loading process, ensuring accurate performance tracking.
2026-01-12 11:57:17 +01:00
Torsten Schulz (local)
ec113058d0 Enhance getGifts method in FalukantService with detailed performance metrics and optimized data retrieval
- Implemented timing metrics to track performance across various steps of the getGifts method, improving traceability.
- Refactored data loading to optimize user, character, and relationship queries, ensuring only necessary fields are fetched.
- Enhanced error handling and logging for better debugging and performance insights during execution.
2026-01-12 11:49:49 +01:00
Torsten Schulz (local)
d2ac2bfdd8 Optimize gift retrieval in FalukantService by loading only necessary fields and implementing parallel data fetching. This change enhances performance and reduces data overhead during the gift and title of nobility retrieval process. 2026-01-12 11:46:16 +01:00
Torsten Schulz (local)
d75fe18e6a Optimize user and character loading in FalukantService by querying only necessary fields. This change enhances performance and reduces data overhead during retrieval. 2026-01-12 11:35:38 +01:00
Torsten Schulz (local)
479f222b54 Refactor character retrieval in FalukantService for improved clarity
- Changed the character retrieval method to directly access the user's character property instead of querying the database, enhancing code readability and performance.
2026-01-12 11:31:49 +01:00
Torsten Schulz (local)
013c536b47 Refactor firstNameMap creation in FalukantService for improved efficiency
- Simplified the construction of firstNameMap by removing unnecessary spread operator, enhancing performance and readability.
2026-01-12 11:28:32 +01:00
Torsten Schulz (local)
3b983a0db5 Update attribute mapping in FalukantService for mood data retrieval
- Changed mood attribute from 'labelTr' to 'tr' in the mood query to align with updated data structure.
- Adjusted mood mapping to reflect the new attribute, ensuring consistency in data handling.
2026-01-12 11:24:56 +01:00
Torsten Schulz (local)
5f9559ac8d Update FalukantService to utilize FalukantCharacterTrait for character relationships
- Added import for FalukantCharacterTrait to enhance data retrieval for character relationships.
- Refactored the relationship character query to use FalukantCharacterTrait instead of CharacterTrait, improving data accuracy and consistency.
2026-01-12 11:16:23 +01:00
Torsten Schulz (local)
f487e6d765 Enhance getFamily method in FalukantService for performance and data retrieval
- Implemented detailed timing metrics to track performance across various steps of the getFamily method.
- Refactored data retrieval to load relationships and children in parallel, improving efficiency.
- Enhanced mapping of relationship and child data, including additional attributes for characters.
- Introduced batch loading for related data to minimize database queries and optimize performance.
- Improved error handling and logging for better traceability during execution.
2026-01-12 11:09:21 +01:00
Torsten Schulz (local)
5e26422e9c Add batch price retrieval for products in region
- Implemented a new endpoint to fetch prices for multiple products in a specified region in a single request, improving efficiency.
- Added validation for input parameters to ensure proper data handling.
- Updated the FalukantService to calculate prices based on knowledge factors and worth percentages for each product.
- Modified the frontend to utilize the new batch endpoint, optimizing the loading of product prices.
2026-01-12 08:58:28 +01:00
Torsten Schulz (local)
64baebfaaa Optimize proposal generation in FalukantService using CTEs
- Replaced multiple SQL queries with a single query utilizing Common Table Expressions (CTEs) for improved performance.
- Streamlined the exclusion of existing proposals and active directors directly within the SQL query.
- Enhanced logging for SQL queries and results, providing better traceability during proposal generation.
- Simplified the process of calculating average knowledge for character proposals, ensuring more efficient data handling.
2026-01-12 08:46:54 +01:00
Torsten Schulz (local)
521dec24b2 Enhance proposal generation logic in FalukantService
- Implemented exclusion of existing proposals and active directors to avoid duplicate character selections.
- Added detailed logging for SQL queries and fallback mechanisms when insufficient older characters are found.
- Improved character selection process by combining excluded character IDs and ensuring a diverse set of proposals.
- Streamlined the batch creation of director proposals with average knowledge calculations for better income predictions.
2026-01-12 08:39:41 +01:00
Torsten Schulz (local)
36f0bd8eb9 Refactor MessagesDialog component to improve parameter interpolation and description formatting
- Enhanced the description formatting logic to ensure proper interpolation of parameters in both title and description fields.
- Updated the handling of monetary values to ensure they are correctly converted to numbers before formatting.
- Improved comments for clarity on the interpolation process and effects handling.
2026-01-09 14:44:20 +01:00
Torsten Schulz (local)
d0a2b122b2 Implement enhanced partner search and NPC creation logic in FalukantService
- Added detailed logging for partner search criteria and results, improving traceability.
- Refactored partner search logic to include a fallback mechanism for searching across all regions if no partners are found in the specified region.
- Introduced a new method for creating NPCs when no suitable partners are available, ensuring a continuous flow in the partner matching process.
- Improved the handling of character attributes such as age and title of nobility during partner searches and NPC creation.
2026-01-09 14:37:55 +01:00
Torsten Schulz (local)
c80cc8ec86 Enhance logging and error handling in FalukantService and FamilyView
- Added detailed logging for partner search and creation processes in FalukantService to improve traceability and debugging.
- Refactored the partner search logic to use a dynamic where clause for better readability and maintainability.
- Implemented error handling in FamilyView's loadGifts method to ensure an empty array is returned on API errors, enhancing user experience.
2026-01-09 14:32:27 +01:00
20 changed files with 1136 additions and 250 deletions

View File

@@ -162,6 +162,18 @@ class FalukantController {
}
return this.service.getProductPriceInRegion(userId, productId, regionId);
});
this.getProductPricesInRegionBatch = this._wrapWithUser((userId, req) => {
const productIds = req.query.productIds;
const regionId = parseInt(req.query.regionId, 10);
if (!productIds || Number.isNaN(regionId)) {
throw new Error('productIds (comma-separated) and regionId are required');
}
const productIdArray = productIds.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id));
if (productIdArray.length === 0) {
throw new Error('At least one valid productId is required');
}
return this.service.getProductPricesInRegionBatch(userId, productIdArray, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);

View File

@@ -25,11 +25,13 @@ function createServer() {
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
});
wss = new WebSocketServer({ server: httpsServer });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
httpsServer.listen(PORT, '0.0.0.0', () => {
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
});
} else {
wss = new WebSocketServer({ port: PORT });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
}

View File

@@ -0,0 +1,13 @@
-- Rollback: Remove indexes for director proposals and character queries
-- Created: 2026-01-12
DROP INDEX IF EXISTS falukant_data.idx_character_region_user_created;
DROP INDEX IF EXISTS falukant_data.idx_character_region_user;
DROP INDEX IF EXISTS falukant_data.idx_character_user_id;
DROP INDEX IF EXISTS falukant_data.idx_director_proposal_employer_character;
DROP INDEX IF EXISTS falukant_data.idx_director_character_id;
DROP INDEX IF EXISTS falukant_data.idx_director_employer_user_id;
DROP INDEX IF EXISTS falukant_data.idx_knowledge_character_id;
DROP INDEX IF EXISTS falukant_data.idx_relationship_character1_id;
DROP INDEX IF EXISTS falukant_data.idx_child_relation_father_id;
DROP INDEX IF EXISTS falukant_data.idx_child_relation_mother_id;

View File

@@ -0,0 +1,43 @@
-- Migration: Add indexes for director proposals and character queries
-- Created: 2026-01-12
-- Index für schnelle Suche nach NPCs in einer Region (mit Altersbeschränkung)
CREATE INDEX IF NOT EXISTS idx_character_region_user_created
ON falukant_data.character (region_id, user_id, created_at)
WHERE user_id IS NULL;
-- Index für schnelle Suche nach NPCs ohne Altersbeschränkung
CREATE INDEX IF NOT EXISTS idx_character_region_user
ON falukant_data.character (region_id, user_id)
WHERE user_id IS NULL;
-- Index für Character-Suche nach user_id (wichtig für getFamily, getDirectorForBranch)
CREATE INDEX IF NOT EXISTS idx_character_user_id
ON falukant_data.character (user_id);
-- Index für Director-Proposals
CREATE INDEX IF NOT EXISTS idx_director_proposal_employer_character
ON falukant_data.director_proposal (employer_user_id, director_character_id);
-- Index für aktive Direktoren
CREATE INDEX IF NOT EXISTS idx_director_character_id
ON falukant_data.director (director_character_id);
-- Index für Director-Suche nach employer_user_id
CREATE INDEX IF NOT EXISTS idx_director_employer_user_id
ON falukant_data.director (employer_user_id);
-- Index für Knowledge-Berechnung
CREATE INDEX IF NOT EXISTS idx_knowledge_character_id
ON falukant_data.knowledge (character_id);
-- Index für Relationships (getFamily)
CREATE INDEX IF NOT EXISTS idx_relationship_character1_id
ON falukant_data.relationship (character1_id);
-- Index für ChildRelations (getFamily)
CREATE INDEX IF NOT EXISTS idx_child_relation_father_id
ON falukant_data.child_relation (father_id);
CREATE INDEX IF NOT EXISTS idx_child_relation_mother_id
ON falukant_data.child_relation (mother_id);

View File

@@ -76,6 +76,7 @@ router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections);
router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region-batch', falukantController.getProductPricesInRegionBatch);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes);

View File

@@ -1,19 +1,55 @@
import './config/loadEnv.js'; // .env deterministisch laden
import http from 'http';
import https from 'https';
import fs from 'fs';
import app from './app.js';
import { setupWebSocket } from './utils/socket.js';
import { syncDatabase } from './utils/syncDatabase.js';
const server = http.createServer(app);
// HTTP-Server für API (Port 2020, intern, über Apache-Proxy)
const API_PORT = Number.parseInt(process.env.PORT || '2020', 10);
const httpServer = http.createServer(app);
// Socket.io wird nur auf HTTPS-Server bereitgestellt, nicht auf HTTP-Server
// setupWebSocket(httpServer); // Entfernt: Socket.io nur über HTTPS
setupWebSocket(server);
// HTTPS-Server für Socket.io (Port 4443, direkt erreichbar)
let httpsServer = null;
const SOCKET_IO_PORT = Number.parseInt(process.env.SOCKET_IO_PORT || '4443', 10);
const USE_TLS = process.env.SOCKET_IO_TLS === '1';
const TLS_KEY_PATH = process.env.SOCKET_IO_TLS_KEY_PATH;
const TLS_CERT_PATH = process.env.SOCKET_IO_TLS_CERT_PATH;
const TLS_CA_PATH = process.env.SOCKET_IO_TLS_CA_PATH;
if (USE_TLS && TLS_KEY_PATH && TLS_CERT_PATH) {
try {
httpsServer = https.createServer({
key: fs.readFileSync(TLS_KEY_PATH),
cert: fs.readFileSync(TLS_CERT_PATH),
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
}, app);
setupWebSocket(httpsServer);
console.log(`[Socket.io] HTTPS-Server für Socket.io konfiguriert auf Port ${SOCKET_IO_PORT}`);
} catch (err) {
console.error('[Socket.io] Fehler beim Laden der TLS-Zertifikate:', err.message);
console.error('[Socket.io] Socket.io wird nicht verfügbar sein');
}
} else {
console.warn('[Socket.io] TLS nicht konfiguriert - Socket.io wird nicht verfügbar sein');
}
syncDatabase().then(() => {
const port = process.env.PORT || 3001;
server.listen(port, () => {
console.log('Server is running on port', port);
// API-Server auf Port 2020 (intern, nur localhost)
httpServer.listen(API_PORT, '127.0.0.1', () => {
console.log(`[API] HTTP-Server läuft auf localhost:${API_PORT} (intern, über Apache-Proxy)`);
});
// Socket.io-Server auf Port 4443 (extern, direkt erreichbar)
if (httpsServer) {
httpsServer.listen(SOCKET_IO_PORT, '0.0.0.0', () => {
console.log(`[Socket.io] HTTPS-Server läuft auf Port ${SOCKET_IO_PORT} (direkt erreichbar)`);
});
}
}).catch(err => {
console.error('Failed to sync database:', err);
process.exit(1);

View File

@@ -33,6 +33,7 @@ import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotio
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
import PromotionalGiftLog from '../models/falukant/log/promotional_gift.js';
import CharacterTrait from '../models/falukant/type/character_trait.js';
import FalukantCharacterTrait from '../models/falukant/data/falukant_character_trait.js';
import Mood from '../models/falukant/type/mood.js';
import UserHouse from '../models/falukant/data/user_house.js';
import HouseType from '../models/falukant/type/house.js';
@@ -2462,36 +2463,95 @@ class FalukantService extends BaseService {
try {
const threeWeeksAgo = new Date(Date.now() - 21 * 24 * 60 * 60 * 1000);
const proposalCount = Math.floor(Math.random() * 3) + 3;
for (let i = 0; i < proposalCount; i++) {
const directorCharacter = await FalukantCharacter.findOne({
where: {
regionId,
createdAt: { [Op.lt]: threeWeeksAgo },
},
include: [
{
model: TitleOfNobility,
as: 'nobleTitle',
attributes: ['level'],
},
],
order: sequelize.literal('RANDOM()'),
});
if (!directorCharacter) {
throw new Error('No directors available for the region');
}
const avgKnowledge = await this.calculateAverageKnowledge(directorCharacter.id);
// OPTIMIERUNG: Verwende eine einzige SQL-Query mit CTEs statt mehrerer separater Queries
// Dies ist viel schneller, da PostgreSQL die Query optimieren kann
// Die Knowledge-Berechnung wird direkt in SQL gemacht (AVG)
const sqlQuery = `
WITH excluded_characters AS (
SELECT DISTINCT director_character_id AS id
FROM falukant_data.director_proposal
WHERE employer_user_id = :falukantUserId
UNION
SELECT DISTINCT director_character_id AS id
FROM falukant_data.director
),
older_characters AS (
SELECT
c.id,
c.title_of_nobility,
t.level,
COALESCE(AVG(k.knowledge), 0) AS avg_knowledge
FROM falukant_data.character c
LEFT JOIN falukant_type.title_of_nobility t ON t.id = c.title_of_nobility
LEFT JOIN falukant_data.knowledge k ON k.character_id = c.id
WHERE c.region_id = :regionId
AND c.user_id IS NULL
AND c.created_at < :threeWeeksAgo
AND c.id NOT IN (SELECT id FROM excluded_characters)
GROUP BY c.id, c.title_of_nobility, t.level
ORDER BY RANDOM()
LIMIT :proposalCount
),
all_characters AS (
SELECT
c.id,
c.title_of_nobility,
t.level,
COALESCE(AVG(k.knowledge), 0) AS avg_knowledge
FROM falukant_data.character c
LEFT JOIN falukant_type.title_of_nobility t ON t.id = c.title_of_nobility
LEFT JOIN falukant_data.knowledge k ON k.character_id = c.id
WHERE c.region_id = :regionId
AND c.user_id IS NULL
AND c.id NOT IN (SELECT id FROM excluded_characters)
AND c.id NOT IN (SELECT id FROM older_characters)
GROUP BY c.id, c.title_of_nobility, t.level
ORDER BY RANDOM()
LIMIT GREATEST(0, :proposalCount - (SELECT COUNT(*) FROM older_characters))
)
SELECT * FROM older_characters
UNION ALL
SELECT * FROM all_characters
LIMIT :proposalCount
`;
const results = await sequelize.query(sqlQuery, {
replacements: {
falukantUserId,
regionId,
threeWeeksAgo,
proposalCount
},
type: sequelize.QueryTypes.SELECT
});
if (results.length === 0) {
console.error(`[generateProposals] No NPCs found in region ${regionId}`);
throw new Error('No directors available for the region');
}
console.log(`[generateProposals] Found ${results.length} available NPCs`);
// Erstelle alle Proposals in einem Batch
const proposalsToCreate = results.map(row => {
const avgKnowledge = parseFloat(row.avg_knowledge) || 0;
const proposedIncome = Math.round(
directorCharacter.nobleTitle.level * Math.pow(1.231, avgKnowledge / 1.5)
row.level * Math.pow(1.231, avgKnowledge / 1.5)
);
await DirectorProposal.create({
directorCharacterId: directorCharacter.id,
return {
directorCharacterId: row.id,
employerUserId: falukantUserId,
proposedIncome,
});
}
};
});
await DirectorProposal.bulkCreate(proposalsToCreate);
console.log(`[generateProposals] Created ${proposalsToCreate.length} director proposals for region ${regionId}`);
} catch (error) {
console.log(error.message, error.stack);
console.error('[generateProposals] Error:', error.message, error.stack);
throw new Error(error.message);
}
}
@@ -2749,9 +2809,17 @@ class FalukantService extends BaseService {
return {
id: director.id,
character: {
name: `${director.character.definedFirstName.name} ${director.character.definedLastName.name}`,
title: director.character.nobleTitle.labelTr,
age: Math.floor((Date.now() - new Date(director.character.birthdate)) / (24 * 60 * 60 * 1000)),
gender: director.character.gender,
nobleTitle: director.character.nobleTitle,
definedFirstName: director.character.definedFirstName,
definedLastName: director.character.definedLastName,
knowledges: director.character.knowledges,
},
satisfaction: director.satisfaction,
character: director.character,
age: calcAge(director.character.birthdate),
income: director.income,
region: director.character.region.name,
wishedIncome,
@@ -2813,109 +2881,229 @@ class FalukantService extends BaseService {
}
async getFamily(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user) throw new Error('User not found');
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) throw new Error('Character not found for this user');
let relationships = await Relationship.findAll({
where: { character1Id: character.id },
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress'],
include: [
{
model: FalukantCharacter, as: 'character2',
attributes: ['id', 'birthdate', 'gender', 'moodId'],
const startTime = Date.now();
const timings = {};
try {
// 1. User und Character laden (optimiert: nur benötigte Felder)
const step1Start = Date.now();
const user = await FalukantUser.findOne({
include: [
{ model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
{
model: FalukantCharacter,
as: 'character',
attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility'],
required: true
}
]
});
if (!user) throw new Error('User not found');
const character = user.character;
if (!character) throw new Error('Character not found for this user');
timings.step1_user_character = Date.now() - step1Start;
// 2. Relationships und Children parallel laden
const step2Start = Date.now();
const [relationshipsRaw, charsWithChildren] = await Promise.all([
Relationship.findAll({
where: { character1Id: character.id },
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress', 'relationshipTypeId'],
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
{ model: CharacterTrait, as: 'traits' },
{ model: Mood, as: 'mood' },
{
model: FalukantCharacter, as: 'character2',
attributes: ['id', 'birthdate', 'gender', 'moodId', 'firstName', 'lastName', 'titleOfNobility'],
required: false
},
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'], required: false }
]
},
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] }
]
});
relationships = relationships.map(r => ({
createdAt: r.createdAt,
widowFirstName2: r.widowFirstName2,
progress: r.nextStepProgress,
character2: {
id: r.character2.id,
age: calcAge(r.character2.birthdate),
gender: r.character2.gender,
firstName: r.character2.definedFirstName?.name || 'Unknown',
nobleTitle: r.character2.nobleTitle?.labelTr || '',
mood: r.character2.mood,
traits: r.character2.traits
},
relationshipType: r.relationshipType.tr
}));
const charsWithChildren = await FalukantCharacter.findAll({
where: { userId: user.id },
include: [
{
model: ChildRelation,
as: 'childrenFather',
include: [{
model: FalukantCharacter,
as: 'child',
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
}]
},
{
model: ChildRelation,
as: 'childrenMother',
include: [{
model: FalukantCharacter,
as: 'child',
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
}]
}),
FalukantCharacter.findAll({
where: { userId: user.id },
attributes: ['id'],
include: [
{
model: ChildRelation,
as: 'childrenFather',
attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
include: [{
model: FalukantCharacter,
as: 'child',
attributes: ['id', 'birthdate', 'gender', 'firstName'],
required: false
}],
required: false
},
{
model: ChildRelation,
as: 'childrenMother',
attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
include: [{
model: FalukantCharacter,
as: 'child',
attributes: ['id', 'birthdate', 'gender', 'firstName'],
required: false
}],
required: false
}
]
})
]);
timings.step2_relationships_children = Date.now() - step2Start;
// 3. Batch-Loading für Relationship-Character-Daten
const step3Start = Date.now();
const relationshipCharacters = relationshipsRaw
.filter(r => r.character2)
.map(r => r.character2);
const relationshipCharacterIds = relationshipCharacters.map(c => c.id);
const childCharacters = charsWithChildren
.flatMap(c => [
...(c.childrenFather || []).map(r => r.child).filter(Boolean),
...(c.childrenMother || []).map(r => r.child).filter(Boolean)
]);
const childCharacterIds = childCharacters.map(c => c.id);
// Sammle alle benötigten IDs
const relationshipFirstNameIds = [...new Set(relationshipCharacters.map(c => c.firstName).filter(Boolean))];
const relationshipLastNameIds = [...new Set(relationshipCharacters.map(c => c.lastName).filter(Boolean))];
const relationshipTitleIds = [...new Set(relationshipCharacters.map(c => c.titleOfNobility).filter(Boolean))];
const relationshipMoodIds = [...new Set(relationshipCharacters.map(c => c.moodId).filter(Boolean))];
const childFirstNameIds = [...new Set(childCharacters.map(c => c.firstName).filter(Boolean))];
const allFirstNameIds = [...new Set([...relationshipFirstNameIds, ...childFirstNameIds])];
// Batch-Load alle benötigten Daten parallel
const [firstNames, lastNames, titles, traitRelations, moods] = await Promise.all([
allFirstNameIds.length > 0 ? FalukantPredefineFirstname.findAll({
where: { id: { [Op.in]: allFirstNameIds } },
attributes: ['id', 'name']
}) : [],
relationshipLastNameIds.length > 0 ? FalukantPredefineLastname.findAll({
where: { id: { [Op.in]: relationshipLastNameIds } },
attributes: ['id', 'name']
}) : [],
relationshipTitleIds.length > 0 ? TitleOfNobility.findAll({
where: { id: { [Op.in]: relationshipTitleIds } },
attributes: ['id', 'labelTr']
}) : [],
relationshipCharacterIds.length > 0 ? FalukantCharacterTrait.findAll({
where: { characterId: { [Op.in]: relationshipCharacterIds } },
attributes: ['characterId', 'traitId']
}) : [],
relationshipMoodIds.length > 0 ? Mood.findAll({
where: { id: { [Op.in]: relationshipMoodIds } },
attributes: ['id', 'tr']
}) : []
]);
// Sammle alle eindeutigen Trait-IDs und lade die Trait-Types
const allTraitIds = [...new Set(traitRelations.map(t => t.traitId))];
const traitTypes = allTraitIds.length > 0 ? await CharacterTrait.findAll({
where: { id: { [Op.in]: allTraitIds } },
attributes: ['id', 'tr']
}) : [];
// Erstelle Maps für schnellen Zugriff
const firstNameMap = new Map(firstNames.map(fn => [fn.id, fn.name]));
const lastNameMap = new Map(lastNames.map(ln => [ln.id, ln.name]));
const titleMap = new Map(titles.map(t => [t.id, t.labelTr]));
const moodMap = new Map(moods.map(m => [m.id, m.tr]));
const traitTypeMap = new Map(traitTypes.map(t => [t.id, { id: t.id, tr: t.tr }]));
const traitsMap = new Map();
traitRelations.forEach(t => {
if (!traitsMap.has(t.characterId)) {
traitsMap.set(t.characterId, []);
}
const traitObj = traitTypeMap.get(t.traitId);
if (traitObj) {
traitsMap.get(t.characterId).push(traitObj);
}
});
timings.step3_batch_loading = Date.now() - step3Start;
// 4. Relationships mappen
const step4Start = Date.now();
const relationships = relationshipsRaw.map(r => {
const char2 = r.character2;
return {
createdAt: r.createdAt,
widowFirstName2: r.widowFirstName2,
progress: r.nextStepProgress,
character2: char2 ? {
id: char2.id,
age: calcAge(char2.birthdate),
gender: char2.gender,
firstName: firstNameMap.get(char2.firstName) || 'Unknown',
nobleTitle: titleMap.get(char2.titleOfNobility) || '',
mood: moodMap.get(char2.moodId) || null,
moodId: char2.moodId,
traits: traitsMap.get(char2.id) || []
} : null,
relationshipType: r.relationshipType?.tr || ''
};
}).filter(r => r.character2 !== null);
timings.step4_map_relationships = Date.now() - step4Start;
// 5. Children mappen
const step5Start = Date.now();
const children = [];
for (const parentChar of charsWithChildren) {
const allRels = [
...(parentChar.childrenFather || []),
...(parentChar.childrenMother || [])
];
for (const rel of allRels) {
const kid = rel.child;
if (!kid) continue;
children.push({
childCharacterId: kid.id,
name: firstNameMap.get(kid.firstName) || 'Unknown',
gender: kid.gender,
age: calcAge(kid.birthdate),
hasName: rel.nameSet,
isHeir: rel.isHeir || false,
_createdAt: rel.createdAt,
});
}
]
});
const children = [];
for (const parentChar of charsWithChildren) {
const allRels = [
...(parentChar.childrenFather || []),
...(parentChar.childrenMother || [])
];
for (const rel of allRels) {
const kid = rel.child;
children.push({
childCharacterId: kid.id,
name: kid.definedFirstName?.name || 'Unknown',
gender: kid.gender,
age: calcAge(kid.birthdate),
hasName: rel.nameSet,
isHeir: rel.isHeir || false,
_createdAt: rel.createdAt,
});
}
}
// Sort children globally by relation createdAt ascending (older first)
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
const inProgress = ['wooing', 'engaged', 'married'];
const family = {
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
lovers: relationships.filter(r => r.relationshipType === 'lover'),
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
children: children.map(({ _createdAt, ...rest }) => rest),
possiblePartners: []
};
const ownAge = calcAge(character.birthdate);
if (ownAge >= 12 && family.relationships.length === 0) {
family.possiblePartners = await this.getPossiblePartners(character.id);
if (family.possiblePartners.length === 0) {
await this.createPossiblePartners(
character.id,
character.gender,
character.regionId,
character.titleOfNobility,
ownAge
);
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
timings.step5_map_children = Date.now() - step5Start;
// 6. Family-Objekt erstellen
const step6Start = Date.now();
const inProgress = ['wooing', 'engaged', 'married'];
const family = {
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
lovers: relationships.filter(r => r.relationshipType === 'lover'),
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
children: children.map(({ _createdAt, ...rest }) => rest),
possiblePartners: []
};
timings.step6_create_family = Date.now() - step6Start;
// 7. Possible Partners (nur wenn nötig, asynchron)
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));
}
}
timings.step7_possible_partners = Date.now() - step7Start;
return family;
} catch (error) {
console.error('[getFamily] Error:', error);
throw error;
}
return family;
}
async setHeir(hashedUserId, childCharacterId) {
@@ -3175,35 +3363,180 @@ class FalukantService extends BaseService {
}
const minTitle = minTitleResult.id;
const potentialPartners = await FalukantCharacter.findAll({
where: {
id: { [Op.ne]: requestingCharacterId },
gender: { [Op.ne]: requestingCharacterGender },
regionId: requestingRegionId,
createdAt: { [Op.lt]: new Date(new Date() - 12 * 24 * 60 * 60 * 1000) },
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
},
// Logging für Debugging
console.log(`[createPossiblePartners] Searching for partners:`, {
requestingCharacterId,
requestingCharacterGender,
requestingRegionId,
requestingCharacterTitleOfNobility,
ownAge
});
const minAgeDate = new Date(new Date() - 12 * 24 * 60 * 60 * 1000);
const titleMin = Math.max(1, requestingCharacterTitleOfNobility - 1);
const titleMax = requestingCharacterTitleOfNobility + 1;
console.log(`[createPossiblePartners] Search criteria:`, {
excludeId: requestingCharacterId,
gender: `not ${requestingCharacterGender}`,
regionId: requestingRegionId,
minAge: '12 days old',
titleRange: `${titleMin}-${titleMax}`,
userId: 'null (NPCs only)'
});
const whereClause = {
id: { [Op.ne]: requestingCharacterId },
gender: { [Op.ne]: requestingCharacterGender },
regionId: requestingRegionId,
createdAt: { [Op.lt]: minAgeDate },
titleOfNobility: { [Op.between]: [titleMin, titleMax] },
userId: null // Nur NPCs suchen
};
let potentialPartners = await FalukantCharacter.findAll({
where: whereClause,
order: [
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
],
limit: 5,
});
console.log(`[createPossiblePartners] Found ${potentialPartners.length} potential partners in region ${requestingRegionId}`);
// Fallback: Wenn keine Partner in der gleichen Region gefunden werden, suche in allen Regionen
if (potentialPartners.length === 0) {
console.log(`[createPossiblePartners] No partners in region ${requestingRegionId}, trying all regions...`);
const fallbackWhereClause = {
id: { [Op.ne]: requestingCharacterId },
gender: { [Op.ne]: requestingCharacterGender },
createdAt: { [Op.lt]: minAgeDate },
titleOfNobility: { [Op.between]: [titleMin, titleMax] },
userId: null
};
potentialPartners = await FalukantCharacter.findAll({
where: fallbackWhereClause,
order: [
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
],
limit: 5,
});
console.log(`[createPossiblePartners] Found ${potentialPartners.length} potential partners in all regions`);
}
if (potentialPartners.length === 0) {
console.log(`[createPossiblePartners] No partners found, creating new NPCs...`);
// Erstelle automatisch 5 neue NPCs, die den Kriterien entsprechen
const targetGender = requestingCharacterGender === 'male' ? 'female' : 'male';
const createdNPCs = await this._createNPCsForMarriage(
requestingRegionId,
targetGender,
titleMin,
titleMax,
ownAge,
5
);
if (createdNPCs.length > 0) {
console.log(`[createPossiblePartners] Created ${createdNPCs.length} new NPCs, using them as partners`);
potentialPartners = createdNPCs;
} else {
console.warn(`[createPossiblePartners] Failed to create NPCs. Consider creating NPCs manually with:`);
console.warn(` - gender: ${targetGender}`);
console.warn(` - regionId: ${requestingRegionId}`);
console.warn(` - titleOfNobility: ${titleMin}-${titleMax}`);
console.warn(` - age: ~${ownAge} years`);
return; // Keine Partner gefunden, aber kein Fehler
}
}
const proposals = potentialPartners.map(partner => {
const age = calcAge(partner.birthdate);
return {
requesterCharacterId: requestingCharacterId,
proposedCharacterId: partner.id,
cost: calculateMarriageCost(partner.titleOfNobility, age, minTitle),
cost: calculateMarriageCost(partner.titleOfNobility, age),
};
});
await MarriageProposal.bulkCreate(proposals);
console.log(`[createPossiblePartners] Created ${proposals.length} marriage proposals`);
} catch (error) {
console.error('Error creating possible partners:', error);
throw error;
}
}
async _createNPCsForMarriage(regionId, gender, minTitle, maxTitle, targetAge, count = 5) {
try {
const sequelize = FalukantCharacter.sequelize;
const createdNPCs = [];
await sequelize.transaction(async (t) => {
for (let i = 0; i < count; i++) {
// Zufälliger Titel im Bereich
const randomTitle = Math.floor(Math.random() * (maxTitle - minTitle + 1)) + minTitle;
// Alter: ±2 Jahre um targetAge
const ageVariation = Math.floor(Math.random() * 5) - 2; // -2 bis +2
const randomAge = Math.max(12, targetAge + ageVariation); // Mindestens 12 Jahre
// Zufälliger Vorname für das Geschlecht
const firstName = await FalukantPredefineFirstname.findAll({
where: { gender },
order: sequelize.fn('RANDOM'),
limit: 1,
transaction: t
});
if (!firstName || firstName.length === 0) {
console.warn(`[_createNPCsForMarriage] No first names found for gender ${gender}`);
continue;
}
// Zufälliger Nachname
const lastName = await FalukantPredefineLastname.findAll({
order: sequelize.fn('RANDOM'),
limit: 1,
transaction: t
});
if (!lastName || lastName.length === 0) {
console.warn(`[_createNPCsForMarriage] No last names found`);
continue;
}
// Geburtsdatum berechnen (Alter in Tagen)
const birthdate = new Date();
birthdate.setDate(birthdate.getDate() - randomAge);
// Erstelle den NPC-Charakter
const npc = await FalukantCharacter.create({
userId: null, // Wichtig: null = NPC
regionId: regionId,
firstName: firstName[0].id,
lastName: lastName[0].id,
gender: gender,
birthdate: birthdate,
titleOfNobility: randomTitle,
health: 100,
moodId: 1
}, { transaction: t });
createdNPCs.push(npc);
}
});
console.log(`[_createNPCsForMarriage] Created ${createdNPCs.length} NPCs`);
return createdNPCs;
} catch (error) {
console.error('[_createNPCsForMarriage] Error creating NPCs:', error);
return []; // Bei Fehler leeres Array zurückgeben
}
}
async acceptMarriageProposal(hashedUserId, proposedCharacterId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
@@ -3245,65 +3578,129 @@ class FalukantService extends BaseService {
}
async getGifts(hashedUserId) {
// 1) Mein User & Character
const user = await this.getFalukantUserByHashedId(hashedUserId);
const myChar = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!myChar) throw new Error('Character not found');
// 2) Beziehung finden und „anderen“ Character bestimmen
const rel = await Relationship.findOne({
where: {
[Op.or]: [
{ character1Id: myChar.id },
{ character2Id: myChar.id }
const startTime = Date.now();
const timings = {};
try {
// 1) User & Character optimiert laden (nur benötigte Felder)
const step1Start = Date.now();
const user = await FalukantUser.findOne({
include: [
{ model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
{
model: FalukantCharacter,
as: 'character',
attributes: ['id', 'titleOfNobility'],
required: true
}
]
},
include: [
{ model: FalukantCharacter, as: 'character1', include: [{ model: CharacterTrait, as: 'traits' }] },
{ model: FalukantCharacter, as: 'character2', include: [{ model: CharacterTrait, as: 'traits' }] }
]
});
if (!rel) throw new Error('Beziehung nicht gefunden');
});
if (!user) throw new Error('User not found');
const myChar = user.character;
if (!myChar) throw new Error('Character not found');
timings.step1_user_character = Date.now() - step1Start;
const relatedChar = rel.character1.id === myChar.id ? rel.character2 : rel.character1;
// 2) Beziehung finden (zwei separate Queries für bessere Index-Nutzung)
const step2Start = Date.now();
const [relAsChar1, relAsChar2] = await Promise.all([
Relationship.findOne({
where: { character1Id: myChar.id },
attributes: ['character1Id', 'character2Id']
}),
Relationship.findOne({
where: { character2Id: myChar.id },
attributes: ['character1Id', 'character2Id']
})
]);
const rel = relAsChar1 || relAsChar2;
timings.step2_relationship = Date.now() - step2Start;
// 3) Trait-IDs und Mood des relatedChar
const relatedTraitIds = relatedChar.traits.map(t => t.id);
const relatedMoodId = relatedChar.moodId;
// 3) Related Character und Traits laden (nur wenn Relationship existiert)
const step3Start = Date.now();
let relatedTraitIds = [];
let relatedMoodId = null;
// 4) Gifts laden aber nur die passenden Moods und Traits als Unter-Arrays
const gifts = await PromotionalGift.findAll({
include: [
if (rel) {
const relatedCharId = rel.character1Id === myChar.id ? rel.character2Id : rel.character1Id;
// Parallel: Character (moodId) und Traits laden
const [relatedChar, traitRows] = await Promise.all([
FalukantCharacter.findOne({
where: { id: relatedCharId },
attributes: ['id', 'moodId']
}),
FalukantCharacterTrait.findAll({
where: { characterId: relatedCharId },
attributes: ['traitId']
})
]);
if (relatedChar) {
relatedMoodId = relatedChar.moodId;
relatedTraitIds = traitRows.map(t => t.traitId);
}
}
timings.step3_load_character_and_traits = Date.now() - step3Start;
// 4) Gifts laden mit Mood/Trait-Filter nur wenn Beziehung existiert
const step4Start = Date.now();
const giftIncludes = [
{
model: PromotionalGiftMood,
as: 'promotionalgiftmoods',
attributes: ['mood_id', 'suitability'],
where: { mood_id: relatedMoodId },
required: false // Gifts ohne Mood-Match bleiben erhalten, haben dann leeres Array
required: false
},
{
model: PromotionalGiftCharacterTrait,
as: 'characterTraits',
attributes: ['trait_id', 'suitability'],
where: { trait_id: relatedTraitIds },
required: false // Gifts ohne Trait-Match bleiben erhalten
required: false
}
]
});
];
// 5) Rest wie gehabt: Kosten berechnen und zurückgeben
const lowestTitleOfNobility = await TitleOfNobility.findOne({ order: [['id', 'ASC']] });
return Promise.all(gifts.map(async gift => ({
id: gift.id,
name: gift.name,
cost: await this.getGiftCost(
gift.value,
myChar.titleOfNobility,
lowestTitleOfNobility.id
),
moodsAffects: gift.promotionalgiftmoods, // nur Einträge mit relatedMoodId
charactersAffects: gift.characterTraits // nur Einträge mit relatedTraitIds
})));
// Wenn Beziehung existiert, Filter anwenden
if (rel && relatedMoodId) {
giftIncludes[0].where = { mood_id: relatedMoodId };
}
if (rel && relatedTraitIds.length > 0) {
giftIncludes[1].where = { trait_id: { [Op.in]: relatedTraitIds } };
}
timings.step4_prepare_gift_includes = Date.now() - step4Start;
// 5) Parallel: Gifts und lowestTitleOfNobility laden
const step5Start = Date.now();
const [gifts, lowestTitleOfNobility] = await Promise.all([
PromotionalGift.findAll({
include: giftIncludes
}),
TitleOfNobility.findOne({ order: [['id', 'ASC']] })
]);
timings.step5_load_gifts_and_title = Date.now() - step5Start;
// 6) Kosten berechnen (getGiftCost ist synchron)
const step6Start = Date.now();
const result = gifts.map(gift => ({
id: gift.id,
name: gift.name,
cost: this.getGiftCost(
gift.value,
myChar.titleOfNobility,
lowestTitleOfNobility.id
),
moodsAffects: gift.promotionalgiftmoods || [], // nur Einträge mit relatedMoodId (wenn Filter angewendet)
charactersAffects: gift.characterTraits || [] // nur Einträge mit relatedTraitIds (wenn Filter angewendet)
}));
timings.step6_calculate_costs = Date.now() - step6Start;
const totalTime = Date.now() - startTime;
console.log(`[getGifts] Performance: ${totalTime}ms total`, timings);
return result;
} catch (error) {
console.error('[getGifts] Error:', error);
throw error;
}
}
async getChildren(hashedUserId) {
@@ -3401,7 +3798,7 @@ class FalukantService extends BaseService {
if (!gift) {
throw new Error('notFound');
}
const cost = await this.getGiftCost(
const cost = this.getGiftCost(
gift.value,
user.character.nobleTitle.id,
lowestTitle.id
@@ -3457,7 +3854,7 @@ class FalukantService extends BaseService {
}
}
async getGiftCost(value, titleOfNobility, lowestTitleOfNobility) {
getGiftCost(value, titleOfNobility, lowestTitleOfNobility) {
const titleLevel = titleOfNobility - lowestTitleOfNobility + 1;
return Math.round(value * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
}
@@ -4903,6 +5300,57 @@ class FalukantService extends BaseService {
return regions;
}
async getProductPricesInRegionBatch(hashedUserId, productIds, regionId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) {
throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
}
if (!Array.isArray(productIds) || productIds.length === 0) {
return {};
}
// Hole alle Produkte auf einmal
const products = await ProductType.findAll({
where: { id: { [Op.in]: productIds } }
});
// Hole alle Knowledge-Werte auf einmal
const knowledges = await Knowledge.findAll({
where: {
characterId: character.id,
productId: { [Op.in]: productIds }
}
});
const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge]));
// Hole alle TownProductWorth-Werte auf einmal
const townWorths = await TownProductWorth.findAll({
where: {
productId: { [Op.in]: productIds },
regionId: regionId
}
});
const worthMap = new Map(townWorths.map(tw => [tw.productId, tw.worthPercent]));
// Berechne Preise für alle Produkte
const prices = {};
for (const product of products) {
const knowledgeFactor = knowledgeMap.get(product.id) || 0;
const worthPercent = worthMap.get(product.id) || 50;
const basePrice = product.sellCost * (worthPercent / 100);
const min = basePrice * 0.6;
const max = basePrice;
const price = min + (max - min) * (knowledgeFactor / 100);
prices[product.id] = Math.round(price * 100) / 100; // Auf 2 Dezimalstellen runden
}
return prices;
}
async getProductPriceInRegion(hashedUserId, productId, regionId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });

45
check-apache-websocket.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
echo "=== Apache WebSocket-Konfiguration prüfen ==="
echo ""
# Prüfe, welche Module aktiviert sind
echo "Aktivierte Apache-Module:"
apache2ctl -M 2>/dev/null | grep -E "(proxy|rewrite|ssl|headers)" || echo "Keine relevanten Module gefunden"
echo ""
# Prüfe, ob die benötigten Module aktiviert sind
REQUIRED_MODULES=("proxy" "proxy_http" "proxy_wstunnel" "rewrite" "ssl" "headers")
MISSING_MODULES=()
for module in "${REQUIRED_MODULES[@]}"; do
if ! apache2ctl -M 2>/dev/null | grep -q "${module}_module"; then
MISSING_MODULES+=("$module")
fi
done
if [ ${#MISSING_MODULES[@]} -eq 0 ]; then
echo "✅ Alle benötigten Module sind aktiviert"
else
echo "❌ Fehlende Module:"
for module in "${MISSING_MODULES[@]}"; do
echo " - $module"
done
echo ""
echo "Aktivieren mit:"
for module in "${MISSING_MODULES[@]}"; do
echo " sudo a2enmod $module"
done
fi
echo ""
echo "=== Apache-Konfiguration testen ==="
if sudo apache2ctl configtest 2>&1; then
echo "✅ Apache-Konfiguration ist gültig"
else
echo "❌ Apache-Konfiguration hat Fehler"
fi
echo ""
echo "=== Aktive VirtualHosts ==="
apache2ctl -S 2>/dev/null | grep -E "(443|4443|4551)" || echo "Keine relevanten VirtualHosts gefunden"

35
debug-websocket-headers.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
echo "=== WebSocket-Header Debug ==="
echo ""
echo "Prüfe Apache-Logs für WebSocket-Upgrade-Header..."
echo ""
# Prüfe die letzten 50 Zeilen des Access-Logs für /ws/ oder /socket.io/
echo "Access-Log Einträge für /ws/ und /socket.io/:"
sudo tail -50 /var/log/apache2/yourpart.access.log | grep -E "(/ws/|/socket.io/)" | tail -10
echo ""
echo "Prüfe Error-Log für WebSocket-Fehler:"
sudo tail -50 /var/log/apache2/yourpart.error.log | grep -iE "(websocket|upgrade|proxy)" | tail -10
echo ""
echo "=== Test mit curl ==="
echo ""
echo "Teste WebSocket-Upgrade für /ws/:"
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
https://www.your-part.de/ws/ 2>&1 | head -20
echo ""
echo "=== Prüfe Apache-Konfiguration ==="
echo ""
echo "Aktive Rewrite-Regeln für WebSocket:"
sudo apache2ctl -S 2>/dev/null | grep -A 5 "your-part.de:443" || echo "VirtualHost nicht gefunden"
echo ""
echo "Prüfe, ob mod_proxy_wstunnel aktiviert ist:"
apache2ctl -M 2>/dev/null | grep proxy_wstunnel || echo "mod_proxy_wstunnel NICHT aktiviert!"

View File

@@ -151,6 +151,19 @@ export default {
// Extrahiere Parameter aus value und effects
params = this.extractParamsFromValue(value, n);
// Wenn value eine einfache Zahl ist (z.B. für overproduction), als value-Parameter verwenden
if (typeof parsed.value === 'number') {
params.value = parsed.value;
}
// Weitere Parameter aus parsed extrahieren (z.B. branch_id)
if (parsed.branch_id !== undefined) {
params.branch_id = parsed.branch_id;
}
if (parsed.region_id !== undefined) {
params.region_id = parsed.region_id;
}
}
} catch (e) {
// Bei Parse-Fehler: Alte Struktur unterstützen
@@ -172,9 +185,12 @@ export default {
if (value && value.title && value.description) {
// Parameter aus effects extrahieren und formatieren
const formattedParams = this.formatParams(params);
// Zuerst Description interpolieren (für {amount} etc.), dann Effects hinzufügen
let description = this.interpolateString(value.description, formattedParams);
description = this.formatDescriptionWithEffects(description, value.effects || [], formattedParams);
return {
title: this.interpolateString(value.title, formattedParams),
description: this.formatDescriptionWithEffects(value.description, value.effects || [], formattedParams)
description: description
};
}
@@ -212,6 +228,10 @@ export default {
const title = this.$t(titleKey, formattedParams);
let description = this.$t(descKey, formattedParams);
// Stelle sicher, dass auch hier die Parameter interpoliert werden (für {amount} etc.)
// Vue i18n interpoliert bereits, aber wir müssen sicherstellen, dass formatParams korrekt formatiert
description = this.interpolateString(description, formattedParams);
// Füge Effect-Details hinzu, falls vorhanden
if (value && value.effects) {
description = this.formatDescriptionWithEffects(description, value.effects, formattedParams);
@@ -234,15 +254,27 @@ export default {
// Geldbeträge formatieren
if (params.amount !== undefined && params.amount !== null) {
formatted.amount = this.formatMoney(params.amount);
formatted.amount = this.formatMoney(Number(params.amount));
}
if (params.absolute !== undefined && params.absolute !== null) {
formatted.amount = this.formatMoney(params.absolute);
formatted.amount = this.formatMoney(Number(params.absolute));
}
if (params.percent !== undefined && params.percent !== null) {
formatted.percent = `${params.percent > 0 ? '+' : ''}${params.percent.toFixed(1)}%`;
}
// Einfache Werte (z.B. für overproduction)
if (params.value !== undefined && params.value !== null) {
formatted.value = Number(params.value);
}
// Filiale-Information
if (params.branch_id !== undefined && params.branch_id !== null) {
formatted.branch_info = ` (Filiale #${params.branch_id})`;
} else {
formatted.branch_info = '';
}
// Gesundheit formatieren
if (params.change !== undefined && params.change !== null) {
formatted.healthChange = params.change > 0 ? `+${params.change}` : `${params.change}`;

View File

@@ -33,7 +33,7 @@
"notifications": {
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
"production": {
"overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf."
"overproduction": "Überproduktion: Deine Produktion liegt {value} Einheiten über dem Bedarf{branch_info}."
},
"transport": {
"waiting": "Transport wartet"

View File

@@ -19,7 +19,7 @@
"notifications": {
"notify_election_created": "A new election has been scheduled.",
"production": {
"overproduction": "Overproduction: your production is {value}% above demand."
"overproduction": "Overproduction: your production is {value} units above demand{branch_info}."
},
"transport": {
"waiting": "Transport waiting"

View File

@@ -188,18 +188,29 @@ const store = createStore({
socketIoUrl = 'http://localhost:3001';
}
// Normalisiere URL (Env-Variablen enthalten teils Pfade wie /api; Port kann absichtlich gesetzt sein, z.B. :4443)
try {
if (socketIoUrl) {
const parsed = new URL(socketIoUrl, window.location.origin);
// Falls /api oder ähnliche Pfade enthalten sind → auf Origin reduzieren (inkl. Port!)
socketIoUrl = parsed.origin;
}
} catch (e) {
// Wenn Parsing fehlschlägt: letzte Rettung ist der aktuelle Origin
try {
// Direkte Verbindung zu Socket.io (ohne Apache-Proxy)
// In Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
const hostname = window.location.hostname;
const isProduction = hostname === 'www.your-part.de' || hostname.includes('your-part.de');
if (isProduction) {
// Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
socketIoUrl = `${protocol}//${hostname}:4443`;
} else {
// Lokale Entwicklung: direkte Backend-Verbindung
if (!socketIoUrl && (import.meta.env.DEV || hostname === 'localhost' || hostname === '127.0.0.1')) {
socketIoUrl = 'http://localhost:3001';
} else if (socketIoUrl) {
try {
const parsed = new URL(socketIoUrl, window.location.origin);
socketIoUrl = parsed.origin;
} catch (e) {
socketIoUrl = window.location.origin;
}
} else {
socketIoUrl = window.location.origin;
} catch (_) {}
}
}
const socket = io(socketIoUrl, {
@@ -284,12 +295,13 @@ const store = createStore({
// Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik
if (!daemonUrl || (typeof daemonUrl === 'string' && daemonUrl.trim() === '')) {
// Fallback: direkte Verbindung zum Daemon-Port 4551 (ohne Apache-Proxy)
// Immer direkte Verbindung zum Daemon-Port 4551 (verschlüsselt)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
daemonUrl = `${protocol}//${hostname}:4551/`;
console.log('[Daemon] Verwende Fallback basierend auf Hostname, Protokoll und Port 4551');
console.log('[Daemon] Verwende direkte Verbindung zu Port 4551');
} else {
console.log('[Daemon] Verwende Umgebungsvariable');
// Wenn Umgebungsvariable gesetzt ist, verwende sie direkt
console.log('[Daemon] Verwende Umgebungsvariable:', daemonUrl);
}
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);

View File

@@ -572,27 +572,49 @@ export default {
return;
}
// Lade Preise für alle Produkte in der aktuellen Region
const prices = {};
for (const product of this.products) {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
prices[product.id] = data.price;
} catch (error) {
console.error(`Error loading price for product ${product.id}:`, error);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
if (!this.products || this.products.length === 0) {
this.productPricesCache = {};
return;
}
// OPTIMIERUNG: Lade alle Preise in einem Batch-Request
try {
const productIds = this.products.map(p => p.id).join(',');
const { data } = await apiClient.get('/api/falukant/products/prices-in-region-batch', {
params: {
productIds: productIds,
regionId: this.selectedBranch.regionId
}
});
this.productPricesCache = data || {};
} catch (error) {
console.error('Error loading prices in batch:', error);
// Fallback: Lade Preise einzeln (aber parallel)
const pricePromises = this.products.map(async (product) => {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
return { productId: product.id, price: data.price };
} catch (err) {
console.error(`Error loading price for product ${product.id}:`, err);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
return { productId: product.id, price: minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100) };
}
});
const results = await Promise.all(pricePromises);
this.productPricesCache = {};
results.forEach(({ productId, price }) => {
this.productPricesCache[productId] = price;
});
}
this.productPricesCache = prices;
},
formatPercent(value) {

View File

@@ -274,12 +274,16 @@ export default {
getEffect(gift) {
const relationship = this.relationships[0];
if (!relationship || !relationship.character2) {
return 0;
}
const partner = relationship.character2;
const currentMoodId = partner.mood?.id ?? partner.mood_id;
const currentMoodId = partner.moodId;
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === currentMoodId);
const moodValue = moodEntry ? moodEntry.suitability : 0;
let highestCharacterValue = 0;
for (const trait of partner.characterTrait) {
// traits ist ein Array von Trait-Objekten mit id und tr
for (const trait of partner.traits || []) {
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
if (charEntry && charEntry.suitability > highestCharacterValue) {
highestCharacterValue = charEntry.suitability;
@@ -295,8 +299,13 @@ export default {
},
async loadGifts() {
const response = await apiClient.get('/api/falukant/family/gifts');
this.gifts = response.data;
try {
const response = await apiClient.get('/api/falukant/family/gifts');
this.gifts = response.data || [];
} catch (error) {
console.error('Error loading gifts:', error);
this.gifts = []; // Leeres Array bei Fehler
}
},
async sendGift() {

43
test-websocket-config.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
echo "=== WebSocket-Konfiguration testen ==="
echo ""
# Prüfe Apache-Module
echo "1. Prüfe Apache-Module:"
REQUIRED_MODULES=("proxy" "proxy_http" "proxy_wstunnel" "rewrite" "ssl" "headers")
for module in "${REQUIRED_MODULES[@]}"; do
if apache2ctl -M 2>/dev/null | grep -q "${module}_module"; then
echo "$module ist aktiviert"
else
echo "$module ist NICHT aktiviert"
fi
done
echo ""
echo "2. Prüfe Apache-Konfiguration:"
if sudo apache2ctl configtest 2>&1 | grep -q "Syntax OK"; then
echo " ✅ Konfiguration ist gültig"
else
echo " ❌ Konfiguration hat Fehler:"
sudo apache2ctl configtest 2>&1
fi
echo ""
echo "3. Prüfe aktive VirtualHosts:"
apache2ctl -S 2>/dev/null | grep -E "(443|4443|4551)" || echo " Keine relevanten VirtualHosts gefunden"
echo ""
echo "4. Prüfe Apache-Logs (letzte 20 Zeilen):"
echo " Error-Log:"
sudo tail -20 /var/log/apache2/yourpart.error.log 2>/dev/null || echo " Keine Fehler gefunden"
echo ""
echo " Access-Log (letzte 10 Zeilen mit /ws/ oder /socket.io/):"
sudo tail -100 /var/log/apache2/yourpart.access.log 2>/dev/null | grep -E "(/ws/|/socket.io/)" | tail -10 || echo " Keine relevanten Einträge gefunden"
echo ""
echo "5. Teste WebSocket-Verbindungen:"
echo " Socket.io: wss://www.your-part.de/socket.io/"
echo " Daemon: wss://www.your-part.de/ws/"
echo ""
echo " Bitte im Browser testen und dann die Logs prüfen."

View File

@@ -0,0 +1,74 @@
<IfModule mod_ssl.c>
<VirtualHost your-part.de:443>
ServerAdmin webmaster@your-part.de
ServerName your-part.de
ServerAlias www.your-part.de
DocumentRoot /opt/yourpart/frontend/dist
DirectoryIndex index.html
# Frontend statische Dateien
<Directory "/opt/yourpart/frontend/dist">
AllowOverride None
Options -Indexes +FollowSymLinks
Require all granted
# Fallback für Vue Router
FallbackResource /index.html
</Directory>
# www Redirect (muss zuerst kommen)
RewriteEngine on
RewriteCond %{SERVER_NAME} =your-part.de
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# API-Requests an Backend weiterleiten
ProxyPass "/api/" "http://localhost:2020/api/"
ProxyPassReverse "/api/" "http://localhost:2020/api/"
# Socket.io: WebSocket und HTTP-Polling mit Location-Blöcken
<LocationMatch "^/socket.io/">
# WebSocket-Upgrade
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule .* "ws://localhost:2020%{REQUEST_URI}" [P,L]
# HTTP-Fallback für Polling
ProxyPass "http://localhost:2020/socket.io/"
ProxyPassReverse "http://localhost:2020/socket.io/"
</LocationMatch>
# Daemon: WebSocket mit Location-Block
<LocationMatch "^/ws/">
# WebSocket-Upgrade
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule .* "ws://localhost:4551%{REQUEST_URI}" [P,L]
# HTTP-Fallback (sollte eigentlich nicht benötigt werden)
ProxyPass "http://localhost:4551/"
ProxyPassReverse "http://localhost:4551/"
</LocationMatch>
ErrorLog /var/log/apache2/yourpart.error.log
CustomLog /var/log/apache2/yourpart.access.log combined
HostnameLookups Off
UseCanonicalName Off
ServerSignature On
# SSL-Konfiguration
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
</VirtualHost>
</IfModule>

View File

@@ -18,28 +18,23 @@
FallbackResource /index.html
</Directory>
# API-Requests an Backend weiterleiten
ProxyPass "/api/" "http://localhost:2020/api/"
ProxyPassReverse "/api/" "http://localhost:2020/api/"
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# WebSocket-Requests an Backend weiterleiten
ProxyPass "/socket.io/" "http://localhost:2020/socket.io/"
ProxyPassReverse "/socket.io/" "http://localhost:2020/socket.io/"
# WebSocket-Upgrade-Header für Socket.io
# www Redirect (muss zuerst kommen, aber nicht für API-Pfade)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/socket.io/(.*)$ "ws://localhost:2020/socket.io/$1" [P,L]
RewriteCond %{SERVER_NAME} =your-part.de
RewriteCond %{REQUEST_URI} !^/api/
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
# WebSocket-Upgrade-Header für Daemon-Verbindungen
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/ws/(.*)$ "ws://localhost:4551/$1" [P,L]
# WebSocket-Proxy für Daemon-Verbindungen mit benutzerdefiniertem Protokoll
ProxyPass "/ws/" "ws://localhost:4551/" upgrade=websocket
ProxyPassReverse "/ws/" "ws://localhost:4551/"
# API-Requests an Backend weiterleiten (Location-Block hat höhere Priorität)
<Location "/api/">
ProxyPass "http://localhost:2020/api/"
ProxyPassReverse "http://localhost:2020/api/"
</Location>
ErrorLog /var/log/apache2/yourpart.error.log
CustomLog /var/log/apache2/yourpart.access.log combined
@@ -52,9 +47,5 @@
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
# www Redirect
RewriteCond %{SERVER_NAME} =your-part.de
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
</IfModule>

View File

@@ -0,0 +1,39 @@
# /etc/apache2/sites-available/yourpart-websocket.conf
<IfModule mod_ssl.c>
<VirtualHost *:4551>
ServerName www.your-part.de
# SSL aktivieren
SSLEngine on
Protocols http/1.1
# SSL-Konfiguration
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
AllowEncodedSlashes NoDecode
# WebSocket-Upgrade (muss VOR ProxyPass stehen)
# Apache lauscht auf Port 4551 (extern, verschlüsselt) und leitet an Daemon auf Port 4551 weiter (intern, unverschlüsselt)
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://localhost:4551/$1" [P,L]
# Fallback für normale HTTP-Requests (falls nötig)
ProxyPass / http://localhost:4551/
ProxyPassReverse / http://localhost:4551/
# CORS-Headers
Header always set Access-Control-Allow-Origin "https://www.your-part.de"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"
ErrorLog /var/log/apache2/yourpart-websocket.error.log
CustomLog /var/log/apache2/yourpart-websocket.access.log combined
</VirtualHost>
</IfModule>

29
yourpart-ws-fixed.conf Normal file
View File

@@ -0,0 +1,29 @@
<IfModule mod_ssl.c>
<VirtualHost *:4443>
ServerName www.your-part.de
SSLEngine on
Protocols http/1.1
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# WebSocket-Upgrade für Socket.io (muss VOR ProxyPass stehen)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/socket.io/(.*)$ "ws://127.0.0.1:2020/socket.io/$1" [P,L]
# HTTP-Proxy für Socket.io (Fallback für Polling)
ProxyPass "/socket.io/" "http://127.0.0.1:2020/socket.io/" retry=0
ProxyPassReverse "/socket.io/" "http://127.0.0.1:2020/socket.io/"
ErrorLog /var/log/apache2/yourpart-ws.error.log
CustomLog /var/log/apache2/yourpart-ws.access.log combined
</VirtualHost>
</IfModule>