Compare commits
27 Commits
falukant-3
...
cd739fb52e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd739fb52e | ||
|
|
9e845843d8 | ||
|
|
0cc280ed55 | ||
|
|
b3707d21b2 | ||
|
|
fbebd6c1c1 | ||
|
|
d7c2bda461 | ||
|
|
2bf949513b | ||
|
|
84619fb656 | ||
|
|
b600f16ecd | ||
|
|
9273066f61 | ||
|
|
7d59dbcf84 | ||
|
|
015d1ae95b | ||
|
|
e2cd6e0e5e | ||
|
|
ec113058d0 | ||
|
|
d2ac2bfdd8 | ||
|
|
d75fe18e6a | ||
|
|
479f222b54 | ||
|
|
013c536b47 | ||
|
|
3b983a0db5 | ||
|
|
5f9559ac8d | ||
|
|
f487e6d765 | ||
|
|
5e26422e9c | ||
|
|
64baebfaaa | ||
|
|
521dec24b2 | ||
|
|
36f0bd8eb9 | ||
|
|
d0a2b122b2 | ||
|
|
c80cc8ec86 |
@@ -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);
|
||||
|
||||
@@ -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} ...`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
45
check-apache-websocket.sh
Executable 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
35
debug-websocket-headers.sh
Executable 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!"
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
43
test-websocket-config.sh
Executable 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."
|
||||
74
yourpart-https-alternative.conf
Normal file
74
yourpart-https-alternative.conf
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
39
yourpart-websocket-fixed.conf
Normal file
39
yourpart-websocket-fixed.conf
Normal 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
29
yourpart-ws-fixed.conf
Normal 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>
|
||||
Reference in New Issue
Block a user