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);
|
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) => {
|
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||||
const productId = parseInt(req.query.productId, 10);
|
const productId = parseInt(req.query.productId, 10);
|
||||||
const currentPrice = parseFloat(req.query.currentPrice);
|
const currentPrice = parseFloat(req.query.currentPrice);
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ function createServer() {
|
|||||||
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
||||||
});
|
});
|
||||||
wss = new WebSocketServer({ server: httpsServer });
|
wss = new WebSocketServer({ server: httpsServer });
|
||||||
|
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
||||||
httpsServer.listen(PORT, '0.0.0.0', () => {
|
httpsServer.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
|
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
|
||||||
});
|
});
|
||||||
} else {
|
} 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} ...`);
|
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.post('/politics/open', falukantController.applyForElections);
|
||||||
router.get('/cities', falukantController.getRegions);
|
router.get('/cities', falukantController.getRegions);
|
||||||
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
|
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('/products/prices-in-cities', falukantController.getProductPricesInCities);
|
||||||
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
|
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
|
||||||
router.get('/vehicles/types', falukantController.getVehicleTypes);
|
router.get('/vehicles/types', falukantController.getVehicleTypes);
|
||||||
|
|||||||
@@ -1,19 +1,55 @@
|
|||||||
import './config/loadEnv.js'; // .env deterministisch laden
|
import './config/loadEnv.js'; // .env deterministisch laden
|
||||||
|
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
import https from 'https';
|
||||||
|
import fs from 'fs';
|
||||||
import app from './app.js';
|
import app from './app.js';
|
||||||
import { setupWebSocket } from './utils/socket.js';
|
import { setupWebSocket } from './utils/socket.js';
|
||||||
import { syncDatabase } from './utils/syncDatabase.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(() => {
|
syncDatabase().then(() => {
|
||||||
const port = process.env.PORT || 3001;
|
// API-Server auf Port 2020 (intern, nur localhost)
|
||||||
server.listen(port, () => {
|
httpServer.listen(API_PORT, '127.0.0.1', () => {
|
||||||
console.log('Server is running on port', port);
|
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 => {
|
}).catch(err => {
|
||||||
console.error('Failed to sync database:', err);
|
console.error('Failed to sync database:', err);
|
||||||
process.exit(1);
|
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 PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
|
||||||
import PromotionalGiftLog from '../models/falukant/log/promotional_gift.js';
|
import PromotionalGiftLog from '../models/falukant/log/promotional_gift.js';
|
||||||
import CharacterTrait from '../models/falukant/type/character_trait.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 Mood from '../models/falukant/type/mood.js';
|
||||||
import UserHouse from '../models/falukant/data/user_house.js';
|
import UserHouse from '../models/falukant/data/user_house.js';
|
||||||
import HouseType from '../models/falukant/type/house.js';
|
import HouseType from '../models/falukant/type/house.js';
|
||||||
@@ -2462,36 +2463,95 @@ class FalukantService extends BaseService {
|
|||||||
try {
|
try {
|
||||||
const threeWeeksAgo = new Date(Date.now() - 21 * 24 * 60 * 60 * 1000);
|
const threeWeeksAgo = new Date(Date.now() - 21 * 24 * 60 * 60 * 1000);
|
||||||
const proposalCount = Math.floor(Math.random() * 3) + 3;
|
const proposalCount = Math.floor(Math.random() * 3) + 3;
|
||||||
for (let i = 0; i < proposalCount; i++) {
|
|
||||||
const directorCharacter = await FalukantCharacter.findOne({
|
// OPTIMIERUNG: Verwende eine einzige SQL-Query mit CTEs statt mehrerer separater Queries
|
||||||
where: {
|
// Dies ist viel schneller, da PostgreSQL die Query optimieren kann
|
||||||
regionId,
|
// Die Knowledge-Berechnung wird direkt in SQL gemacht (AVG)
|
||||||
createdAt: { [Op.lt]: threeWeeksAgo },
|
const sqlQuery = `
|
||||||
},
|
WITH excluded_characters AS (
|
||||||
include: [
|
SELECT DISTINCT director_character_id AS id
|
||||||
{
|
FROM falukant_data.director_proposal
|
||||||
model: TitleOfNobility,
|
WHERE employer_user_id = :falukantUserId
|
||||||
as: 'nobleTitle',
|
UNION
|
||||||
attributes: ['level'],
|
SELECT DISTINCT director_character_id AS id
|
||||||
},
|
FROM falukant_data.director
|
||||||
],
|
),
|
||||||
order: sequelize.literal('RANDOM()'),
|
older_characters AS (
|
||||||
});
|
SELECT
|
||||||
if (!directorCharacter) {
|
c.id,
|
||||||
throw new Error('No directors available for the region');
|
c.title_of_nobility,
|
||||||
}
|
t.level,
|
||||||
const avgKnowledge = await this.calculateAverageKnowledge(directorCharacter.id);
|
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(
|
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,
|
employerUserId: falukantUserId,
|
||||||
proposedIncome,
|
proposedIncome,
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
|
await DirectorProposal.bulkCreate(proposalsToCreate);
|
||||||
|
|
||||||
|
console.log(`[generateProposals] Created ${proposalsToCreate.length} director proposals for region ${regionId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error.message, error.stack);
|
console.error('[generateProposals] Error:', error.message, error.stack);
|
||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2749,9 +2809,17 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: director.id,
|
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,
|
satisfaction: director.satisfaction,
|
||||||
character: director.character,
|
|
||||||
age: calcAge(director.character.birthdate),
|
|
||||||
income: director.income,
|
income: director.income,
|
||||||
region: director.character.region.name,
|
region: director.character.region.name,
|
||||||
wishedIncome,
|
wishedIncome,
|
||||||
@@ -2813,109 +2881,229 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFamily(hashedUserId) {
|
async getFamily(hashedUserId) {
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const startTime = Date.now();
|
||||||
if (!user) throw new Error('User not found');
|
const timings = {};
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
|
||||||
if (!character) throw new Error('Character not found for this user');
|
try {
|
||||||
let relationships = await Relationship.findAll({
|
// 1. User und Character laden (optimiert: nur benötigte Felder)
|
||||||
where: { character1Id: character.id },
|
const step1Start = Date.now();
|
||||||
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress'],
|
const user = await FalukantUser.findOne({
|
||||||
include: [
|
include: [
|
||||||
{
|
{ model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
|
||||||
model: FalukantCharacter, as: 'character2',
|
{
|
||||||
attributes: ['id', 'birthdate', 'gender', 'moodId'],
|
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: [
|
include: [
|
||||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
{
|
||||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
|
model: FalukantCharacter, as: 'character2',
|
||||||
{ model: CharacterTrait, as: 'traits' },
|
attributes: ['id', 'birthdate', 'gender', 'moodId', 'firstName', 'lastName', 'titleOfNobility'],
|
||||||
{ model: Mood, as: 'mood' },
|
required: false
|
||||||
|
},
|
||||||
|
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'], required: false }
|
||||||
]
|
]
|
||||||
},
|
}),
|
||||||
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] }
|
FalukantCharacter.findAll({
|
||||||
]
|
where: { userId: user.id },
|
||||||
});
|
attributes: ['id'],
|
||||||
relationships = relationships.map(r => ({
|
include: [
|
||||||
createdAt: r.createdAt,
|
{
|
||||||
widowFirstName2: r.widowFirstName2,
|
model: ChildRelation,
|
||||||
progress: r.nextStepProgress,
|
as: 'childrenFather',
|
||||||
character2: {
|
attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
|
||||||
id: r.character2.id,
|
include: [{
|
||||||
age: calcAge(r.character2.birthdate),
|
model: FalukantCharacter,
|
||||||
gender: r.character2.gender,
|
as: 'child',
|
||||||
firstName: r.character2.definedFirstName?.name || 'Unknown',
|
attributes: ['id', 'birthdate', 'gender', 'firstName'],
|
||||||
nobleTitle: r.character2.nobleTitle?.labelTr || '',
|
required: false
|
||||||
mood: r.character2.mood,
|
}],
|
||||||
traits: r.character2.traits
|
required: false
|
||||||
},
|
},
|
||||||
relationshipType: r.relationshipType.tr
|
{
|
||||||
}));
|
model: ChildRelation,
|
||||||
const charsWithChildren = await FalukantCharacter.findAll({
|
as: 'childrenMother',
|
||||||
where: { userId: user.id },
|
attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
|
||||||
include: [
|
include: [{
|
||||||
{
|
model: FalukantCharacter,
|
||||||
model: ChildRelation,
|
as: 'child',
|
||||||
as: 'childrenFather',
|
attributes: ['id', 'birthdate', 'gender', 'firstName'],
|
||||||
include: [{
|
required: false
|
||||||
model: FalukantCharacter,
|
}],
|
||||||
as: 'child',
|
required: false
|
||||||
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
}
|
||||||
}]
|
]
|
||||||
},
|
})
|
||||||
{
|
]);
|
||||||
model: ChildRelation,
|
timings.step2_relationships_children = Date.now() - step2Start;
|
||||||
as: 'childrenMother',
|
|
||||||
include: [{
|
// 3. Batch-Loading für Relationship-Character-Daten
|
||||||
model: FalukantCharacter,
|
const step3Start = Date.now();
|
||||||
as: 'child',
|
const relationshipCharacters = relationshipsRaw
|
||||||
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
.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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
|
||||||
// Sort children globally by relation createdAt ascending (older first)
|
timings.step5_map_children = Date.now() - step5Start;
|
||||||
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
|
|
||||||
const inProgress = ['wooing', 'engaged', 'married'];
|
// 6. Family-Objekt erstellen
|
||||||
const family = {
|
const step6Start = Date.now();
|
||||||
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
|
const inProgress = ['wooing', 'engaged', 'married'];
|
||||||
lovers: relationships.filter(r => r.relationshipType === 'lover'),
|
const family = {
|
||||||
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
|
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
|
||||||
children: children.map(({ _createdAt, ...rest }) => rest),
|
lovers: relationships.filter(r => r.relationshipType === 'lover'),
|
||||||
possiblePartners: []
|
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
|
||||||
};
|
children: children.map(({ _createdAt, ...rest }) => rest),
|
||||||
const ownAge = calcAge(character.birthdate);
|
possiblePartners: []
|
||||||
if (ownAge >= 12 && family.relationships.length === 0) {
|
};
|
||||||
family.possiblePartners = await this.getPossiblePartners(character.id);
|
timings.step6_create_family = Date.now() - step6Start;
|
||||||
if (family.possiblePartners.length === 0) {
|
|
||||||
await this.createPossiblePartners(
|
// 7. Possible Partners (nur wenn nötig, asynchron)
|
||||||
character.id,
|
const step7Start = Date.now();
|
||||||
character.gender,
|
const ownAge = calcAge(character.birthdate);
|
||||||
character.regionId,
|
if (ownAge >= 12 && family.relationships.length === 0) {
|
||||||
character.titleOfNobility,
|
|
||||||
ownAge
|
|
||||||
);
|
|
||||||
family.possiblePartners = await this.getPossiblePartners(character.id);
|
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) {
|
async setHeir(hashedUserId, childCharacterId) {
|
||||||
@@ -3175,35 +3363,180 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
const minTitle = minTitleResult.id;
|
const minTitle = minTitleResult.id;
|
||||||
|
|
||||||
const potentialPartners = await FalukantCharacter.findAll({
|
// Logging für Debugging
|
||||||
where: {
|
console.log(`[createPossiblePartners] Searching for partners:`, {
|
||||||
id: { [Op.ne]: requestingCharacterId },
|
requestingCharacterId,
|
||||||
gender: { [Op.ne]: requestingCharacterGender },
|
requestingCharacterGender,
|
||||||
regionId: requestingRegionId,
|
requestingRegionId,
|
||||||
createdAt: { [Op.lt]: new Date(new Date() - 12 * 24 * 60 * 60 * 1000) },
|
requestingCharacterTitleOfNobility,
|
||||||
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
|
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: [
|
order: [
|
||||||
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
|
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
|
||||||
],
|
],
|
||||||
limit: 5,
|
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 proposals = potentialPartners.map(partner => {
|
||||||
const age = calcAge(partner.birthdate);
|
const age = calcAge(partner.birthdate);
|
||||||
return {
|
return {
|
||||||
requesterCharacterId: requestingCharacterId,
|
requesterCharacterId: requestingCharacterId,
|
||||||
proposedCharacterId: partner.id,
|
proposedCharacterId: partner.id,
|
||||||
cost: calculateMarriageCost(partner.titleOfNobility, age, minTitle),
|
cost: calculateMarriageCost(partner.titleOfNobility, age),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await MarriageProposal.bulkCreate(proposals);
|
await MarriageProposal.bulkCreate(proposals);
|
||||||
|
console.log(`[createPossiblePartners] Created ${proposals.length} marriage proposals`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating possible partners:', error);
|
console.error('Error creating possible partners:', error);
|
||||||
throw 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) {
|
async acceptMarriageProposal(hashedUserId, proposedCharacterId) {
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||||
@@ -3245,65 +3578,129 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getGifts(hashedUserId) {
|
async getGifts(hashedUserId) {
|
||||||
// 1) Mein User & Character
|
const startTime = Date.now();
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const timings = {};
|
||||||
const myChar = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
|
||||||
if (!myChar) throw new Error('Character not found');
|
try {
|
||||||
|
// 1) User & Character optimiert laden (nur benötigte Felder)
|
||||||
// 2) Beziehung finden und „anderen“ Character bestimmen
|
const step1Start = Date.now();
|
||||||
const rel = await Relationship.findOne({
|
const user = await FalukantUser.findOne({
|
||||||
where: {
|
include: [
|
||||||
[Op.or]: [
|
{ model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
|
||||||
{ character1Id: myChar.id },
|
{
|
||||||
{ character2Id: myChar.id }
|
model: FalukantCharacter,
|
||||||
|
as: 'character',
|
||||||
|
attributes: ['id', 'titleOfNobility'],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
});
|
||||||
include: [
|
if (!user) throw new Error('User not found');
|
||||||
{ model: FalukantCharacter, as: 'character1', include: [{ model: CharacterTrait, as: 'traits' }] },
|
const myChar = user.character;
|
||||||
{ model: FalukantCharacter, as: 'character2', include: [{ model: CharacterTrait, as: 'traits' }] }
|
if (!myChar) throw new Error('Character not found');
|
||||||
]
|
timings.step1_user_character = Date.now() - step1Start;
|
||||||
});
|
|
||||||
if (!rel) throw new Error('Beziehung nicht gefunden');
|
|
||||||
|
|
||||||
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
|
// 3) Related Character und Traits laden (nur wenn Relationship existiert)
|
||||||
const relatedTraitIds = relatedChar.traits.map(t => t.id);
|
const step3Start = Date.now();
|
||||||
const relatedMoodId = relatedChar.moodId;
|
let relatedTraitIds = [];
|
||||||
|
let relatedMoodId = null;
|
||||||
|
|
||||||
// 4) Gifts laden – aber nur die passenden Moods und Traits als Unter-Arrays
|
if (rel) {
|
||||||
const gifts = await PromotionalGift.findAll({
|
const relatedCharId = rel.character1Id === myChar.id ? rel.character2Id : rel.character1Id;
|
||||||
include: [
|
|
||||||
|
// 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,
|
model: PromotionalGiftMood,
|
||||||
as: 'promotionalgiftmoods',
|
as: 'promotionalgiftmoods',
|
||||||
attributes: ['mood_id', 'suitability'],
|
attributes: ['mood_id', 'suitability'],
|
||||||
where: { mood_id: relatedMoodId },
|
required: false
|
||||||
required: false // Gifts ohne Mood-Match bleiben erhalten, haben dann leeres Array
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: PromotionalGiftCharacterTrait,
|
model: PromotionalGiftCharacterTrait,
|
||||||
as: 'characterTraits',
|
as: 'characterTraits',
|
||||||
attributes: ['trait_id', 'suitability'],
|
attributes: ['trait_id', 'suitability'],
|
||||||
where: { trait_id: relatedTraitIds },
|
required: false
|
||||||
required: false // Gifts ohne Trait-Match bleiben erhalten
|
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
});
|
|
||||||
|
|
||||||
// 5) Rest wie gehabt: Kosten berechnen und zurückgeben
|
// Wenn Beziehung existiert, Filter anwenden
|
||||||
const lowestTitleOfNobility = await TitleOfNobility.findOne({ order: [['id', 'ASC']] });
|
if (rel && relatedMoodId) {
|
||||||
return Promise.all(gifts.map(async gift => ({
|
giftIncludes[0].where = { mood_id: relatedMoodId };
|
||||||
id: gift.id,
|
}
|
||||||
name: gift.name,
|
if (rel && relatedTraitIds.length > 0) {
|
||||||
cost: await this.getGiftCost(
|
giftIncludes[1].where = { trait_id: { [Op.in]: relatedTraitIds } };
|
||||||
gift.value,
|
}
|
||||||
myChar.titleOfNobility,
|
timings.step4_prepare_gift_includes = Date.now() - step4Start;
|
||||||
lowestTitleOfNobility.id
|
|
||||||
),
|
// 5) Parallel: Gifts und lowestTitleOfNobility laden
|
||||||
moodsAffects: gift.promotionalgiftmoods, // nur Einträge mit relatedMoodId
|
const step5Start = Date.now();
|
||||||
charactersAffects: gift.characterTraits // nur Einträge mit relatedTraitIds
|
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) {
|
async getChildren(hashedUserId) {
|
||||||
@@ -3401,7 +3798,7 @@ class FalukantService extends BaseService {
|
|||||||
if (!gift) {
|
if (!gift) {
|
||||||
throw new Error('notFound');
|
throw new Error('notFound');
|
||||||
}
|
}
|
||||||
const cost = await this.getGiftCost(
|
const cost = this.getGiftCost(
|
||||||
gift.value,
|
gift.value,
|
||||||
user.character.nobleTitle.id,
|
user.character.nobleTitle.id,
|
||||||
lowestTitle.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;
|
const titleLevel = titleOfNobility - lowestTitleOfNobility + 1;
|
||||||
return Math.round(value * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
|
return Math.round(value * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
|
||||||
}
|
}
|
||||||
@@ -4903,6 +5300,57 @@ class FalukantService extends BaseService {
|
|||||||
return regions;
|
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) {
|
async getProductPriceInRegion(hashedUserId, productId, regionId) {
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
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
|
// Extrahiere Parameter aus value und effects
|
||||||
params = this.extractParamsFromValue(value, n);
|
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) {
|
} catch (e) {
|
||||||
// Bei Parse-Fehler: Alte Struktur unterstützen
|
// Bei Parse-Fehler: Alte Struktur unterstützen
|
||||||
@@ -172,9 +185,12 @@ export default {
|
|||||||
if (value && value.title && value.description) {
|
if (value && value.title && value.description) {
|
||||||
// Parameter aus effects extrahieren und formatieren
|
// Parameter aus effects extrahieren und formatieren
|
||||||
const formattedParams = this.formatParams(params);
|
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 {
|
return {
|
||||||
title: this.interpolateString(value.title, formattedParams),
|
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);
|
const title = this.$t(titleKey, formattedParams);
|
||||||
let description = this.$t(descKey, 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
|
// Füge Effect-Details hinzu, falls vorhanden
|
||||||
if (value && value.effects) {
|
if (value && value.effects) {
|
||||||
description = this.formatDescriptionWithEffects(description, value.effects, formattedParams);
|
description = this.formatDescriptionWithEffects(description, value.effects, formattedParams);
|
||||||
@@ -234,15 +254,27 @@ export default {
|
|||||||
|
|
||||||
// Geldbeträge formatieren
|
// Geldbeträge formatieren
|
||||||
if (params.amount !== undefined && params.amount !== null) {
|
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) {
|
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) {
|
if (params.percent !== undefined && params.percent !== null) {
|
||||||
formatted.percent = `${params.percent > 0 ? '+' : ''}${params.percent.toFixed(1)}%`;
|
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
|
// Gesundheit formatieren
|
||||||
if (params.change !== undefined && params.change !== null) {
|
if (params.change !== undefined && params.change !== null) {
|
||||||
formatted.healthChange = params.change > 0 ? `+${params.change}` : `${params.change}`;
|
formatted.healthChange = params.change > 0 ? `+${params.change}` : `${params.change}`;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
|
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
|
||||||
"production": {
|
"production": {
|
||||||
"overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf."
|
"overproduction": "Überproduktion: Deine Produktion liegt {value} Einheiten über dem Bedarf{branch_info}."
|
||||||
},
|
},
|
||||||
"transport": {
|
"transport": {
|
||||||
"waiting": "Transport wartet"
|
"waiting": "Transport wartet"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"notify_election_created": "A new election has been scheduled.",
|
"notify_election_created": "A new election has been scheduled.",
|
||||||
"production": {
|
"production": {
|
||||||
"overproduction": "Overproduction: your production is {value}% above demand."
|
"overproduction": "Overproduction: your production is {value} units above demand{branch_info}."
|
||||||
},
|
},
|
||||||
"transport": {
|
"transport": {
|
||||||
"waiting": "Transport waiting"
|
"waiting": "Transport waiting"
|
||||||
|
|||||||
@@ -188,18 +188,29 @@ const store = createStore({
|
|||||||
socketIoUrl = 'http://localhost:3001';
|
socketIoUrl = 'http://localhost:3001';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalisiere URL (Env-Variablen enthalten teils Pfade wie /api; Port kann absichtlich gesetzt sein, z.B. :4443)
|
// Direkte Verbindung zu Socket.io (ohne Apache-Proxy)
|
||||||
try {
|
// In Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
|
||||||
if (socketIoUrl) {
|
const hostname = window.location.hostname;
|
||||||
const parsed = new URL(socketIoUrl, window.location.origin);
|
const isProduction = hostname === 'www.your-part.de' || hostname.includes('your-part.de');
|
||||||
// Falls /api oder ähnliche Pfade enthalten sind → auf Origin reduzieren (inkl. Port!)
|
|
||||||
socketIoUrl = parsed.origin;
|
if (isProduction) {
|
||||||
}
|
// Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
|
||||||
} catch (e) {
|
const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
|
||||||
// Wenn Parsing fehlschlägt: letzte Rettung ist der aktuelle Origin
|
socketIoUrl = `${protocol}//${hostname}:4443`;
|
||||||
try {
|
} 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;
|
socketIoUrl = window.location.origin;
|
||||||
} catch (_) {}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = io(socketIoUrl, {
|
const socket = io(socketIoUrl, {
|
||||||
@@ -284,12 +295,13 @@ const store = createStore({
|
|||||||
|
|
||||||
// Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik
|
// Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik
|
||||||
if (!daemonUrl || (typeof daemonUrl === 'string' && daemonUrl.trim() === '')) {
|
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:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
daemonUrl = `${protocol}//${hostname}:4551/`;
|
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 {
|
} 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);
|
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);
|
||||||
|
|||||||
@@ -572,27 +572,49 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lade Preise für alle Produkte in der aktuellen Region
|
if (!this.products || this.products.length === 0) {
|
||||||
const prices = {};
|
this.productPricesCache = {};
|
||||||
for (const product of this.products) {
|
return;
|
||||||
try {
|
}
|
||||||
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
|
|
||||||
params: {
|
// OPTIMIERUNG: Lade alle Preise in einem Batch-Request
|
||||||
productId: product.id,
|
try {
|
||||||
regionId: this.selectedBranch.regionId
|
const productIds = this.products.map(p => p.id).join(',');
|
||||||
}
|
const { data } = await apiClient.get('/api/falukant/products/prices-in-region-batch', {
|
||||||
});
|
params: {
|
||||||
prices[product.id] = data.price;
|
productIds: productIds,
|
||||||
} catch (error) {
|
regionId: this.selectedBranch.regionId
|
||||||
console.error(`Error loading price for product ${product.id}:`, error);
|
}
|
||||||
// Fallback auf Standard-Berechnung
|
});
|
||||||
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
|
this.productPricesCache = data || {};
|
||||||
const maxPrice = product.sellCost;
|
} catch (error) {
|
||||||
const minPrice = maxPrice * 0.6;
|
console.error('Error loading prices in batch:', error);
|
||||||
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
// 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) {
|
formatPercent(value) {
|
||||||
|
|||||||
@@ -274,12 +274,16 @@ export default {
|
|||||||
|
|
||||||
getEffect(gift) {
|
getEffect(gift) {
|
||||||
const relationship = this.relationships[0];
|
const relationship = this.relationships[0];
|
||||||
|
if (!relationship || !relationship.character2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const partner = relationship.character2;
|
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 moodEntry = gift.moodsAffects.find(ma => ma.mood_id === currentMoodId);
|
||||||
const moodValue = moodEntry ? moodEntry.suitability : 0;
|
const moodValue = moodEntry ? moodEntry.suitability : 0;
|
||||||
let highestCharacterValue = 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);
|
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
|
||||||
if (charEntry && charEntry.suitability > highestCharacterValue) {
|
if (charEntry && charEntry.suitability > highestCharacterValue) {
|
||||||
highestCharacterValue = charEntry.suitability;
|
highestCharacterValue = charEntry.suitability;
|
||||||
@@ -295,8 +299,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async loadGifts() {
|
async loadGifts() {
|
||||||
const response = await apiClient.get('/api/falukant/family/gifts');
|
try {
|
||||||
this.gifts = response.data;
|
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() {
|
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
|
FallbackResource /index.html
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# API-Requests an Backend weiterleiten
|
# Proxy-Einstellungen
|
||||||
ProxyPass "/api/" "http://localhost:2020/api/"
|
ProxyPreserveHost On
|
||||||
ProxyPassReverse "/api/" "http://localhost:2020/api/"
|
ProxyRequests Off
|
||||||
|
RequestHeader set X-Forwarded-Proto "https"
|
||||||
|
AllowEncodedSlashes NoDecode
|
||||||
|
|
||||||
# WebSocket-Requests an Backend weiterleiten
|
# www Redirect (muss zuerst kommen, aber nicht für API-Pfade)
|
||||||
ProxyPass "/socket.io/" "http://localhost:2020/socket.io/"
|
|
||||||
ProxyPassReverse "/socket.io/" "http://localhost:2020/socket.io/"
|
|
||||||
|
|
||||||
# WebSocket-Upgrade-Header für Socket.io
|
|
||||||
RewriteEngine on
|
RewriteEngine on
|
||||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
RewriteCond %{SERVER_NAME} =your-part.de
|
||||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
RewriteCond %{REQUEST_URI} !^/api/
|
||||||
RewriteRule ^/socket.io/(.*)$ "ws://localhost:2020/socket.io/$1" [P,L]
|
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
||||||
|
|
||||||
# WebSocket-Upgrade-Header für Daemon-Verbindungen
|
# API-Requests an Backend weiterleiten (Location-Block hat höhere Priorität)
|
||||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
<Location "/api/">
|
||||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
ProxyPass "http://localhost:2020/api/"
|
||||||
RewriteRule ^/ws/(.*)$ "ws://localhost:4551/$1" [P,L]
|
ProxyPassReverse "http://localhost:2020/api/"
|
||||||
|
</Location>
|
||||||
# WebSocket-Proxy für Daemon-Verbindungen mit benutzerdefiniertem Protokoll
|
|
||||||
ProxyPass "/ws/" "ws://localhost:4551/" upgrade=websocket
|
|
||||||
ProxyPassReverse "/ws/" "ws://localhost:4551/"
|
|
||||||
|
|
||||||
ErrorLog /var/log/apache2/yourpart.error.log
|
ErrorLog /var/log/apache2/yourpart.error.log
|
||||||
CustomLog /var/log/apache2/yourpart.access.log combined
|
CustomLog /var/log/apache2/yourpart.access.log combined
|
||||||
@@ -52,9 +47,5 @@
|
|||||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||||
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
|
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.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>
|
</VirtualHost>
|
||||||
</IfModule>
|
</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