Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4da572822e | |||
| ee23bb3ba3 | |||
| d002e340dd | |||
| 0e1d87ddab | |||
| 2a4928c1b6 | |||
| efe2bd57ab | |||
|
|
a0aa678e7d | ||
|
|
a1b6e6ab59 | ||
|
|
73acf1d1cd | ||
|
|
48110e9a6f | ||
|
|
642e215c69 | ||
|
|
091b9ff70a | ||
|
|
86f753c745 | ||
|
|
c28f8b1384 | ||
|
|
9b36297171 | ||
|
|
7beed235d7 | ||
|
|
a0206dc8cb | ||
|
|
bf0eed3b03 | ||
|
|
c8072b8052 | ||
|
|
c66fbf1a62 | ||
|
|
e13a711a60 | ||
|
|
346a326bfd | ||
|
|
addb8e9a6d | ||
|
|
ea8b9e661d | ||
|
|
339ae844e9 | ||
|
|
a0a7e81927 | ||
|
|
31c23a0c40 | ||
|
|
c1f22246ea | ||
|
|
0a1388bf06 | ||
|
|
1a69b83983 | ||
|
|
63f9443b77 | ||
|
|
6a9b2b8d1d | ||
|
|
8e1e0968ae | ||
|
|
a486292880 | ||
|
|
ee4b0ee7c2 | ||
|
|
43d86cce18 | ||
|
|
25d7c70058 | ||
|
|
71c62cf5e8 | ||
|
|
a7350282ee | ||
|
|
676629bd8d | ||
|
|
1892877b11 | ||
|
|
be218aabf7 | ||
|
|
856f7d56bf | ||
|
|
000ebbdc2b | ||
|
|
791314bef2 | ||
|
|
bcb0b01324 | ||
|
|
03e3a21a25 | ||
|
|
e97a2a62c9 | ||
|
|
814f972287 | ||
|
|
274c2a3292 | ||
|
|
4dbcebfab8 | ||
|
|
fadc301d41 | ||
|
|
b1d29f2083 | ||
|
|
e756b3692d | ||
|
|
74a3d59800 | ||
|
|
0544a3dfde | ||
|
|
656c821986 | ||
|
|
865ef81012 | ||
|
|
5ad27a87e5 | ||
|
|
085b851925 | ||
|
|
98dea7dd39 | ||
|
|
e5ef334f7c | ||
|
|
d6ea09b3e2 | ||
|
|
a51b8a1ff6 | ||
|
|
3c885b6ab9 | ||
|
|
6b3b30108b | ||
|
|
7fab23d22b | ||
|
|
def88f6486 | ||
|
|
1797ae3e58 | ||
|
|
f768ba3b27 | ||
|
|
b3e48a0b06 | ||
|
|
3f56939421 | ||
|
|
87c720c3fe | ||
|
|
90fbcaf31d | ||
|
|
56c3569b68 | ||
|
|
e2969c1837 | ||
|
|
fe14c7b9f5 | ||
|
|
5d01b24c2d | ||
|
|
4eeb5021ee | ||
|
|
6ec62af606 | ||
|
|
3d6fdc65d2 | ||
|
|
956418f5f3 | ||
|
|
e57de7f983 | ||
|
|
08e2c87de8 | ||
|
|
ba1a12402d | ||
|
|
39716b1f40 | ||
|
|
adc7132404 | ||
|
|
8c8841705c | ||
|
|
f7fdd8ab08 | ||
|
|
5807c6f3d3 | ||
|
|
7e0691eea3 | ||
|
|
17d4d21620 | ||
|
|
d19feb8bc1 | ||
|
|
ab1e4bec60 | ||
|
|
672cec9c2a | ||
|
|
c3ea7eecc2 | ||
|
|
608e62c2bd | ||
|
|
c1b69389c6 | ||
|
|
182f38597c | ||
|
|
06ea259dc9 | ||
|
|
29dd7ec80c | ||
|
|
3f043fc315 | ||
|
|
5ed27e5a6a | ||
|
|
23725c20ee | ||
|
|
29b6db7ee9 | ||
|
|
6e7165fe7f | ||
|
|
43131250ed | ||
|
|
c3beb029e5 | ||
|
|
9f10ac9e96 | ||
|
|
d36901aa2b | ||
|
|
4510aa3d14 | ||
|
|
3b8736acd7 | ||
|
|
735075d1bd | ||
|
|
dc7001a80c | ||
|
|
8a9acf6c4a | ||
|
|
5ca017950e | ||
|
|
eadec50e30 | ||
|
|
e7f5918013 | ||
|
|
27b675cb19 | ||
|
|
016a37c116 | ||
|
|
d8b1efc3ca | ||
|
|
d13fe19198 | ||
|
|
762a2e9cf0 | ||
|
|
44a2c525e7 | ||
|
|
507b0275d3 | ||
|
|
ccd8bfba0d | ||
|
|
47f5def67c |
23
backend/README_TAX.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Falukant Tax Migration & Configuration
|
||||
|
||||
This project now supports a per-region sales tax (`tax_percent`) for Falukant.
|
||||
|
||||
Migration
|
||||
- A SQL migration was added: `backend/migrations/20260101000000-add-tax-percent-to-region.cjs`.
|
||||
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
|
||||
|
||||
Runtime configuration
|
||||
- If you want taxes to be forwarded to a treasury account, set environment variable `TREASURY_FALUKANT_USER_ID` to a valid `falukant_user.id`.
|
||||
- If `TREASURY_FALUKANT_USER_ID` is not set, taxes will be calculated and currently not forwarded to any account.
|
||||
|
||||
Implementation notes
|
||||
- Backend service `sellProduct` and `sellAllProducts` now compute tax per-region and credit net to seller and tax to treasury (if configured).
|
||||
- Tax arithmetic uses rounding to 2 decimals. The current implementation performs two separate DB calls (seller, treasury). For strict ledger atomicity consider implementing DB-side booking.
|
||||
|
||||
Cumulative tax behavior
|
||||
- The system now sums `tax_percent` from the sale region and all ancestor regions (recursive up the region tree). This allows defining different tax rates on up to 6 region levels and summing them for final tax percent.
|
||||
- To avoid reducing seller net by taxes, sale prices are inflated by factor = 1 / (1 - cumulativeTax/100). This way the seller receives the original net and the tax is collected separately.
|
||||
|
||||
Testing
|
||||
- After running the migration, test with a small sale and verify `falukant_log.moneyflow` entries for seller and treasury.
|
||||
|
||||
@@ -16,6 +16,7 @@ import match3Router from './routers/match3Router.js';
|
||||
import taxiRouter from './routers/taxiRouter.js';
|
||||
import taxiMapRouter from './routers/taxiMapRouter.js';
|
||||
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
|
||||
import termineRouter from './routers/termineRouter.js';
|
||||
import cors from 'cors';
|
||||
import './jobs/sessionCleanup.js';
|
||||
|
||||
@@ -52,6 +53,7 @@ app.use('/api/forum', forumRouter);
|
||||
app.use('/api/falukant', falukantRouter);
|
||||
app.use('/api/friendships', friendshipRouter);
|
||||
app.use('/api/blog', blogRouter);
|
||||
app.use('/api/termine', termineRouter);
|
||||
|
||||
// Serve frontend SPA for non-API routes to support history mode clean URLs
|
||||
const frontendDir = path.join(__dirname, '../frontend');
|
||||
|
||||
@@ -27,6 +27,7 @@ class AdminController {
|
||||
// User administration
|
||||
this.searchUsers = this.searchUsers.bind(this);
|
||||
this.getUser = this.getUser.bind(this);
|
||||
this.getUsers = this.getUsers.bind(this);
|
||||
this.updateUser = this.updateUser.bind(this);
|
||||
|
||||
// Rights
|
||||
@@ -37,6 +38,11 @@ class AdminController {
|
||||
|
||||
// Statistics
|
||||
this.getUserStatistics = this.getUserStatistics.bind(this);
|
||||
this.getFalukantRegions = this.getFalukantRegions.bind(this);
|
||||
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
||||
this.getRegionDistances = this.getRegionDistances.bind(this);
|
||||
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
||||
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
|
||||
}
|
||||
|
||||
async getOpenInterests(req, res) {
|
||||
@@ -74,6 +80,30 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async getUsers(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
let { ids } = req.query;
|
||||
if (!ids) {
|
||||
return res.status(400).json({ error: 'ids query parameter is required' });
|
||||
}
|
||||
// Unterstütze sowohl Array-Format (ids[]=...) als auch komma-separierten String (ids=...)
|
||||
let hashedIds;
|
||||
if (Array.isArray(ids)) {
|
||||
hashedIds = ids;
|
||||
} else if (typeof ids === 'string') {
|
||||
hashedIds = ids.split(',').map(id => id.trim()).filter(id => id.length > 0);
|
||||
} else {
|
||||
return res.status(400).json({ error: 'ids must be an array or comma-separated string' });
|
||||
}
|
||||
const result = await AdminService.getUsersByHashedIds(requester, hashedIds);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
@@ -290,6 +320,69 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async getFalukantRegions(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const regions = await AdminService.getFalukantRegions(userId);
|
||||
res.status(200).json(regions);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async updateFalukantRegionMap(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const { id } = req.params;
|
||||
const { map } = req.body || {};
|
||||
const region = await AdminService.updateFalukantRegionMap(userId, id, map);
|
||||
res.status(200).json(region);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' ? 403 : (error.message === 'regionNotFound' ? 404 : 500);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRegionDistances(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const distances = await AdminService.getRegionDistances(userId);
|
||||
res.status(200).json(distances);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async upsertRegionDistance(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const record = await AdminService.upsertRegionDistance(userId, req.body || {});
|
||||
res.status(200).json(record);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' ? 403 : 400;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRegionDistance(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const { id } = req.params;
|
||||
const result = await AdminService.deleteRegionDistance(userId, id);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRoomTypes(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
|
||||
@@ -30,6 +30,7 @@ class FalukantController {
|
||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
|
||||
this.createProduction = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quantity } = req.body;
|
||||
return this.service.createProduction(userId, branchId, productId, quantity);
|
||||
@@ -91,6 +92,7 @@ class FalukantController {
|
||||
if (!result) throw { status: 404, message: 'No family data found' };
|
||||
return result;
|
||||
});
|
||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||
this.getGifts = this._wrapWithUser((userId) => {
|
||||
console.log('🔍 getGifts called with userId:', userId);
|
||||
@@ -143,6 +145,24 @@ class FalukantController {
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||
|
||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||
this.getProductPriceInRegion = this._wrapWithUser((userId, req) => {
|
||||
const productId = parseInt(req.query.productId, 10);
|
||||
const regionId = parseInt(req.query.regionId, 10);
|
||||
if (Number.isNaN(productId) || Number.isNaN(regionId)) {
|
||||
throw new Error('productId and regionId are required');
|
||||
}
|
||||
return this.service.getProductPriceInRegion(userId, productId, regionId);
|
||||
});
|
||||
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||
const productId = parseInt(req.query.productId, 10);
|
||||
const currentPrice = parseFloat(req.query.currentPrice);
|
||||
const currentRegionId = req.query.currentRegionId ? parseInt(req.query.currentRegionId, 10) : null;
|
||||
if (Number.isNaN(productId) || Number.isNaN(currentPrice)) {
|
||||
throw new Error('productId and currentPrice are required');
|
||||
}
|
||||
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
|
||||
});
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
|
||||
@@ -181,6 +201,33 @@ class FalukantController {
|
||||
});
|
||||
});
|
||||
|
||||
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
||||
this.buyVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.buyVehicles(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
);
|
||||
this.getVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
||||
);
|
||||
this.createTransport = this._wrapWithUser(
|
||||
(userId, req) => this.service.createTransport(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
);
|
||||
this.getTransportRoute = this._wrapWithUser(
|
||||
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
||||
);
|
||||
this.getBranchTransports = this._wrapWithUser(
|
||||
(userId, req) => this.service.getBranchTransports(userId, req.params.branchId)
|
||||
);
|
||||
this.repairVehicle = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
||||
{ successStatus: 200 }
|
||||
);
|
||||
this.repairAllVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
||||
{ successStatus: 200 }
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -117,10 +117,6 @@ const menuStructure = {
|
||||
visible: ["hasfalukantaccount"],
|
||||
path: "/falukant/branch"
|
||||
},
|
||||
directors: {
|
||||
visible: ["hasfalukantaccount"],
|
||||
path: "/falukant/directors"
|
||||
},
|
||||
family: {
|
||||
visible: ["hasfalukantaccount"],
|
||||
path: "/falukant/family"
|
||||
@@ -251,10 +247,14 @@ const menuStructure = {
|
||||
visible: ["mainadmin", "chatrooms"],
|
||||
path: "/admin/chatrooms"
|
||||
},
|
||||
servicesStatus: {
|
||||
visible: ["mainadmin"],
|
||||
path: "/admin/services/status"
|
||||
},
|
||||
interests: {
|
||||
visible: ["mainadmin", "interests"],
|
||||
path: "/admin/interests"
|
||||
},
|
||||
},
|
||||
falukant: {
|
||||
visible: ["mainadmin", "falukant"],
|
||||
children: {
|
||||
@@ -270,6 +270,10 @@ const menuStructure = {
|
||||
visible: ["mainadmin", "falukant"],
|
||||
path: "/admin/falukant/database"
|
||||
},
|
||||
mapEditor: {
|
||||
visible: ["mainadmin", "falukant"],
|
||||
path: "/admin/falukant/map"
|
||||
},
|
||||
}
|
||||
},
|
||||
minigames: {
|
||||
|
||||
43
backend/controllers/termineController.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
class TermineController {
|
||||
async getTermine(req, res) {
|
||||
try {
|
||||
const csvPath = path.join(__dirname, '../data/termine.csv');
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf-8');
|
||||
|
||||
const lines = csvContent.trim().split('\n');
|
||||
const headers = lines[0].split(',');
|
||||
|
||||
const termine = lines.slice(1).map(line => {
|
||||
const values = line.split(',');
|
||||
const termin = {};
|
||||
headers.forEach((header, index) => {
|
||||
termin[header] = values[index] || '';
|
||||
});
|
||||
return termin;
|
||||
});
|
||||
|
||||
// Sortiere nach Datum
|
||||
termine.sort((a, b) => new Date(a.datum) - new Date(b.datum));
|
||||
|
||||
// Filtere nur zukünftige Termine
|
||||
const heute = new Date();
|
||||
heute.setHours(0, 0, 0, 0);
|
||||
const zukuenftigeTermine = termine.filter(t => new Date(t.datum) >= heute);
|
||||
|
||||
res.status(200).json(zukuenftigeTermine);
|
||||
} catch (error) {
|
||||
console.error('Error reading termine.csv:', error);
|
||||
res.status(500).json({ error: 'Could not load termine' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TermineController();
|
||||
|
||||
98
backend/daemonServer.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
|
||||
const PORT = 4551;
|
||||
|
||||
// Einfache In-Memory-Struktur für Verbindungen (für spätere Erweiterungen)
|
||||
const connections = new Set();
|
||||
|
||||
function createServer() {
|
||||
const wss = new WebSocketServer({ port: PORT });
|
||||
|
||||
console.log(`[Daemon] WebSocket-Server startet auf Port ${PORT} ...`);
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const peer = req.socket.remoteAddress + ':' + req.socket.remotePort;
|
||||
ws.isAlive = true;
|
||||
ws.userId = null;
|
||||
connections.add(ws);
|
||||
|
||||
console.log(`[Daemon] Neue Verbindung von ${peer}`);
|
||||
|
||||
ws.on('message', (message) => {
|
||||
try {
|
||||
if (message.toString() === 'pong') {
|
||||
// Client-Pong für unser Ping
|
||||
ws.isAlive = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(message.toString());
|
||||
|
||||
// Vom Frontend gesendet nach Verbindungsaufbau
|
||||
if (data.event === 'setUserId' && data.data?.userId) {
|
||||
ws.userId = data.data.userId;
|
||||
console.log(`[Daemon] setUserId erhalten: ${ws.userId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin-Dialog: WebSocket-Log anfordern
|
||||
if (data.event === 'getWebsocketLog') {
|
||||
const response = {
|
||||
event: 'getWebsocketLogResponse',
|
||||
entries: [] // aktuell keine Log-Historie implementiert
|
||||
};
|
||||
ws.send(JSON.stringify(response));
|
||||
return;
|
||||
}
|
||||
|
||||
// Platzhalter für spätere Events
|
||||
// console.log('[Daemon] Unbekanntes Event:', data);
|
||||
} catch (err) {
|
||||
console.error('[Daemon] Fehler beim Verarbeiten einer Nachricht:', err);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
connections.delete(ws);
|
||||
console.log('[Daemon] Verbindung geschlossen');
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
console.error('[Daemon] WebSocket-Fehler (Verbindung):', err);
|
||||
});
|
||||
});
|
||||
|
||||
// Einfache Ping/Pong-Mechanik, damit Verbindungen sauber erkannt werden
|
||||
const interval = setInterval(() => {
|
||||
for (const ws of connections) {
|
||||
if (ws.isAlive === false) {
|
||||
console.log('[Daemon] Verbindung wegen fehlendem Pong beendet');
|
||||
ws.terminate();
|
||||
connections.delete(ws);
|
||||
continue;
|
||||
}
|
||||
ws.isAlive = false;
|
||||
try {
|
||||
ws.send('ping');
|
||||
} catch (err) {
|
||||
console.error('[Daemon] Fehler beim Senden von Ping:', err);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
wss.on('close', () => {
|
||||
clearInterval(interval);
|
||||
connections.clear();
|
||||
console.log('[Daemon] Server gestoppt');
|
||||
});
|
||||
|
||||
wss.on('error', (err) => {
|
||||
console.error('[Daemon] Server-Fehler:', err);
|
||||
});
|
||||
|
||||
return wss;
|
||||
}
|
||||
|
||||
createServer();
|
||||
|
||||
|
||||
7
backend/data/termine.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
datum,titel,beschreibung,ort,uhrzeit
|
||||
2025-10-07,Vereinsmeisterschaften 2025 Doppel,Die Vereinsmeisterschaften 2025 im Doppel finden im Rahmen des Erwachsenentrainings statt.,,,
|
||||
2026-01-17,Vereinsmeisterschaften 2025 Einzel,Die Vereinsmeisterschaften 2025 im Einzel finden in der Schulturnhalle statt. Bitte vormerken!,,10:00
|
||||
2025-12-18,Weihnachtsfeier 2025,Die Weihnachtsfeier 2025 findet im Gasthaus „Zum Einhorn" in FFM-Bonames statt. Beginn 19:00 Uhr (bitte vormerken),Gasthaus „Zum Einhorn" FFM-Bonames,19:00
|
||||
2025-09-14,VR-Cup,Zwei VR-Cups am 14.09.2025 (jeweils 12 und 16 Uhr),,12:00 und 16:00
|
||||
2025-10-19,VR-Cup,Zwei VR-Cups am 19.10.2025 (jeweils 12 und 16 Uhr),,12:00 und 16:00
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 91.
|
34
backend/fix-pgcrypto-extension.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function fixPgCryptoExtension() {
|
||||
try {
|
||||
console.log('🔧 Aktiviere pgcrypto Erweiterung...');
|
||||
|
||||
await sequelize.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
|
||||
|
||||
console.log('✅ pgcrypto Erweiterung erfolgreich aktiviert');
|
||||
|
||||
// Prüfe ob die Erweiterung aktiviert ist
|
||||
const result = await sequelize.query(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto'
|
||||
) as extension_exists;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
if (result[0]?.extension_exists) {
|
||||
console.log('✅ Bestätigung: pgcrypto Erweiterung ist aktiviert');
|
||||
} else {
|
||||
console.warn('⚠️ Warnung: pgcrypto Erweiterung konnte nicht aktiviert werden');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Aktivieren der pgcrypto Erweiterung:', error.message);
|
||||
console.error('Stack:', error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fixPgCryptoExtension();
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn(
|
||||
{
|
||||
tableName: 'falukant_user',
|
||||
schema: 'falukant_data'
|
||||
},
|
||||
'last_nobility_advance_at',
|
||||
{
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn(
|
||||
{
|
||||
tableName: 'falukant_user',
|
||||
schema: 'falukant_data'
|
||||
},
|
||||
'last_nobility_advance_at'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// 1) Add character_name column to notification table
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_log.notification
|
||||
ADD COLUMN IF NOT EXISTS character_name text;
|
||||
`);
|
||||
|
||||
// 1b) Add character_id column so triggers and application can set a reference
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_log.notification
|
||||
ADD COLUMN IF NOT EXISTS character_id integer;
|
||||
`);
|
||||
|
||||
// Create an index on character_id to speed lookups (if not exists)
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
|
||||
) THEN
|
||||
CREATE INDEX idx_notification_character_id ON falukant_log.notification (character_id);
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
|
||||
// 2) Create helper function to populate character_name from character_id or user_id
|
||||
// - Resolve name via character_id if present
|
||||
// - Fallback to a character for the same user_id when character_id is NULL
|
||||
// - Only set NEW.character_name when the column exists and is NULL
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION falukant_log.populate_notification_character_name()
|
||||
RETURNS TRIGGER AS $function$
|
||||
DECLARE
|
||||
v_first_name TEXT;
|
||||
v_last_name TEXT;
|
||||
v_char_id INTEGER;
|
||||
v_column_exists BOOLEAN;
|
||||
BEGIN
|
||||
-- check if target column exists in the notification table
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_log' AND table_name = 'notification' AND column_name = 'character_name'
|
||||
) INTO v_column_exists;
|
||||
|
||||
IF NOT v_column_exists THEN
|
||||
-- Nothing to do when target column absent
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- only populate when column is NULL
|
||||
IF NEW.character_name IS NOT NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- prefer explicit character_id
|
||||
v_char_id := NEW.character_id;
|
||||
|
||||
-- when character_id is null, try to find a character for the user_id
|
||||
IF v_char_id IS NULL AND NEW.user_id IS NOT NULL THEN
|
||||
-- choose a representative character: the one with highest id for this user (change if different policy required)
|
||||
SELECT id INTO v_char_id
|
||||
FROM falukant_data.character
|
||||
WHERE user_id = NEW.user_id
|
||||
ORDER BY id DESC
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
IF v_char_id IS NOT NULL THEN
|
||||
SELECT pf.name, pl.name
|
||||
INTO v_first_name, v_last_name
|
||||
FROM falukant_data.character c
|
||||
LEFT JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
|
||||
LEFT JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
|
||||
WHERE c.id = v_char_id;
|
||||
|
||||
IF v_first_name IS NOT NULL OR v_last_name IS NOT NULL THEN
|
||||
NEW.character_name := COALESCE(v_first_name, '') || CASE WHEN v_first_name IS NOT NULL AND v_last_name IS NOT NULL THEN ' ' ELSE '' END || COALESCE(v_last_name, '');
|
||||
ELSE
|
||||
NEW.character_name := ('#' || v_char_id::text);
|
||||
END IF;
|
||||
ELSE
|
||||
-- last resort fallback: use user_id as identifier if present
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
NEW.character_name := ('#u' || NEW.user_id::text);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
// 3) Create trigger that runs before insert to populate the column
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
|
||||
CREATE TRIGGER trg_populate_notification_character_name
|
||||
BEFORE INSERT ON falukant_log.notification
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION falukant_log.populate_notification_character_name();
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP FUNCTION IF EXISTS falukant_log.populate_notification_character_name();
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
-- drop index if exists
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
|
||||
) THEN
|
||||
EXECUTE 'DROP INDEX falukant_log.idx_notification_character_id';
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_log.notification
|
||||
DROP COLUMN IF EXISTS character_name;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_log.notification
|
||||
DROP COLUMN IF EXISTS character_id;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Add nullable weather_type_id column
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.production
|
||||
ADD COLUMN IF NOT EXISTS weather_type_id integer;
|
||||
`);
|
||||
|
||||
// Add foreign key constraint if not exists
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.constraint_schema = tc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.constraint_schema = 'falukant_data'
|
||||
AND tc.table_name = 'production'
|
||||
AND kcu.column_name = 'weather_type_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.production
|
||||
ADD CONSTRAINT fk_production_weather_type
|
||||
FOREIGN KEY (weather_type_id) REFERENCES falukant_type.weather(id);
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
|
||||
// create index to speed lookups
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
|
||||
) THEN
|
||||
CREATE INDEX idx_production_weather_type_id ON falukant_data.production (weather_type_id);
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.production
|
||||
DROP CONSTRAINT IF EXISTS fk_production_weather_type;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
|
||||
) THEN
|
||||
EXECUTE 'DROP INDEX falukant_data.idx_production_weather_type_id';
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.production
|
||||
DROP COLUMN IF EXISTS weather_type_id;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.stock
|
||||
ADD COLUMN IF NOT EXISTS product_quality integer;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.stock
|
||||
DROP COLUMN IF EXISTS product_quality;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.region
|
||||
ADD COLUMN IF NOT EXISTS tax_percent numeric NOT NULL DEFAULT 7;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_data.region
|
||||
DROP COLUMN IF EXISTS tax_percent;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// 1) add backup column for original sell_cost (idempotent)
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_type.product
|
||||
ADD COLUMN IF NOT EXISTS original_sell_cost numeric;
|
||||
`);
|
||||
|
||||
// 2) if original_sell_cost is not set, copy current sell_cost into it
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE falukant_type.product
|
||||
SET original_sell_cost = sell_cost
|
||||
WHERE original_sell_cost IS NULL;
|
||||
`);
|
||||
|
||||
// 3) compute max cumulative tax across regions and increase sell_cost accordingly
|
||||
// We use the maximum cumulative tax (worst-case) so sellers are neutral across regions.
|
||||
// Formula: neutral_sell = CEIL(original_sell_cost * (1 / (1 - max_total/100)))
|
||||
await queryInterface.sequelize.query(`
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
|
||||
UNION ALL
|
||||
SELECT a.start_id, r.id, r.parent_id, r.tax_percent
|
||||
FROM falukant_data.region r
|
||||
JOIN ancestors a ON r.id = a.parent_id
|
||||
), totals AS (
|
||||
SELECT start_id, COALESCE(SUM(tax_percent), 0) AS total FROM ancestors GROUP BY start_id
|
||||
), mm AS (
|
||||
SELECT COALESCE(MAX(total),0) AS max_total FROM totals
|
||||
)
|
||||
UPDATE falukant_type.product
|
||||
SET sell_cost = CEIL(original_sell_cost * (CASE WHEN (1 - mm.max_total/100) <= 0 THEN 1 ELSE (1 / (1 - mm.max_total/100)) END))
|
||||
FROM mm
|
||||
WHERE original_sell_cost IS NOT NULL;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_type.product
|
||||
DROP COLUMN IF EXISTS sell_cost_min_neutral;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE IF EXISTS falukant_type.product
|
||||
DROP COLUMN IF EXISTS sell_cost_max_neutral;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Create index on (user_id, shown) to optimize markNotificationsShown queries
|
||||
// This prevents deadlocks by allowing fast lookups and reducing lock contention
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind = 'i'
|
||||
AND c.relname = 'idx_notification_user_id_shown'
|
||||
AND n.nspname = 'falukant_log'
|
||||
) THEN
|
||||
CREATE INDEX idx_notification_user_id_shown
|
||||
ON falukant_log.notification (user_id, shown);
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP INDEX IF EXISTS falukant_log.idx_notification_user_id_shown;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
20
backend/migrations/add_condition_to_vehicle.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Migration: Add condition and available_from columns to vehicle table
|
||||
-- Date: 2024-12-02
|
||||
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ADD COLUMN IF NOT EXISTS condition INTEGER NOT NULL DEFAULT 100;
|
||||
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ADD COLUMN IF NOT EXISTS available_from TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
COMMENT ON COLUMN falukant_data.vehicle.condition IS 'Current condition of the vehicle (0-100)';
|
||||
COMMENT ON COLUMN falukant_data.vehicle.available_from IS 'Timestamp when the vehicle becomes available for use';
|
||||
|
||||
-- Migration: Add build_time_minutes column to vehicle type table
|
||||
-- Date: 2024-12-03
|
||||
|
||||
ALTER TABLE falukant_type.vehicle
|
||||
ADD COLUMN IF NOT EXISTS build_time_minutes INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN falukant_type.vehicle.build_time_minutes IS 'Time to construct the vehicle, in minutes';
|
||||
|
||||
9
backend/migrations/add_is_heir_to_child_relation.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Migration: Add is_heir column to child_relation table
|
||||
-- Date: 2025-12-08
|
||||
-- Description: Adds a boolean field to mark a child as the heir
|
||||
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS is_heir BOOLEAN DEFAULT false;
|
||||
|
||||
COMMENT ON COLUMN falukant_data.child_relation.is_heir IS 'Marks whether this child is set as the heir';
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Aktiviere die pgcrypto Erweiterung, die die digest() Funktion bereitstellt
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
|
||||
7
backend/migrations/make_transport_product_nullable.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Migration: Make productId and size nullable in transport table
|
||||
-- This allows empty transports (moving vehicles without products)
|
||||
|
||||
ALTER TABLE falukant_data.transport
|
||||
ALTER COLUMN product_id DROP NOT NULL,
|
||||
ALTER COLUMN size DROP NOT NULL;
|
||||
|
||||
@@ -95,6 +95,13 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||
import ElectionHistory from './falukant/log/election_history.js';
|
||||
import Underground from './falukant/data/underground.js';
|
||||
import UndergroundType from './falukant/type/underground.js';
|
||||
import VehicleType from './falukant/type/vehicle.js';
|
||||
import Vehicle from './falukant/data/vehicle.js';
|
||||
import Transport from './falukant/data/transport.js';
|
||||
import RegionDistance from './falukant/data/region_distance.js';
|
||||
import WeatherType from './falukant/type/weather.js';
|
||||
import Weather from './falukant/data/weather.js';
|
||||
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
|
||||
import Blog from './community/blog.js';
|
||||
import BlogPost from './community/blog_post.js';
|
||||
import Campaign from './match3/campaign.js';
|
||||
@@ -284,6 +291,21 @@ export default function setupAssociations() {
|
||||
RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' });
|
||||
RegionType.hasMany(RegionData, { foreignKey: 'regionTypeId', as: 'regions' });
|
||||
|
||||
Weather.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||
RegionData.hasOne(Weather, { foreignKey: 'regionId', as: 'weather' });
|
||||
|
||||
Weather.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
|
||||
WeatherType.hasMany(Weather, { foreignKey: 'weatherTypeId', as: 'weathers' });
|
||||
|
||||
ProductWeatherEffect.belongsTo(ProductType, { foreignKey: 'productId', as: 'product' });
|
||||
ProductType.hasMany(ProductWeatherEffect, { foreignKey: 'productId', as: 'weatherEffects' });
|
||||
|
||||
ProductWeatherEffect.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
|
||||
WeatherType.hasMany(ProductWeatherEffect, { foreignKey: 'weatherTypeId', as: 'productEffects' });
|
||||
|
||||
Production.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
|
||||
WeatherType.hasMany(Production, { foreignKey: 'weatherTypeId', as: 'productions' });
|
||||
|
||||
FalukantUser.belongsTo(RegionData, { foreignKey: 'mainBranchRegionId', as: 'mainBranchRegion' });
|
||||
RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' });
|
||||
|
||||
@@ -421,6 +443,89 @@ export default function setupAssociations() {
|
||||
PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' });
|
||||
FalukantCharacter.hasMany(PromotionalGiftLog, { foreignKey: 'recipientCharacterId', as: 'giftlogs' });
|
||||
|
||||
// Vehicles & Transports
|
||||
|
||||
VehicleType.hasMany(Vehicle, {
|
||||
foreignKey: 'vehicleTypeId',
|
||||
as: 'vehicles',
|
||||
});
|
||||
Vehicle.belongsTo(VehicleType, {
|
||||
foreignKey: 'vehicleTypeId',
|
||||
as: 'type',
|
||||
});
|
||||
|
||||
FalukantUser.hasMany(Vehicle, {
|
||||
foreignKey: 'falukantUserId',
|
||||
as: 'vehicles',
|
||||
});
|
||||
Vehicle.belongsTo(FalukantUser, {
|
||||
foreignKey: 'falukantUserId',
|
||||
as: 'owner',
|
||||
});
|
||||
|
||||
RegionData.hasMany(Vehicle, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'vehicles',
|
||||
});
|
||||
Vehicle.belongsTo(RegionData, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'region',
|
||||
});
|
||||
|
||||
// Region distances
|
||||
RegionData.hasMany(RegionDistance, {
|
||||
foreignKey: 'sourceRegionId',
|
||||
as: 'distancesFrom',
|
||||
});
|
||||
RegionData.hasMany(RegionDistance, {
|
||||
foreignKey: 'targetRegionId',
|
||||
as: 'distancesTo',
|
||||
});
|
||||
RegionDistance.belongsTo(RegionData, {
|
||||
foreignKey: 'sourceRegionId',
|
||||
as: 'sourceRegion',
|
||||
});
|
||||
RegionDistance.belongsTo(RegionData, {
|
||||
foreignKey: 'targetRegionId',
|
||||
as: 'targetRegion',
|
||||
});
|
||||
|
||||
Transport.belongsTo(RegionData, {
|
||||
foreignKey: 'sourceRegionId',
|
||||
as: 'sourceRegion',
|
||||
});
|
||||
Transport.belongsTo(RegionData, {
|
||||
foreignKey: 'targetRegionId',
|
||||
as: 'targetRegion',
|
||||
});
|
||||
|
||||
RegionData.hasMany(Transport, {
|
||||
foreignKey: 'sourceRegionId',
|
||||
as: 'outgoingTransports',
|
||||
});
|
||||
RegionData.hasMany(Transport, {
|
||||
foreignKey: 'targetRegionId',
|
||||
as: 'incomingTransports',
|
||||
});
|
||||
|
||||
Transport.belongsTo(ProductType, {
|
||||
foreignKey: 'productId',
|
||||
as: 'productType',
|
||||
});
|
||||
ProductType.hasMany(Transport, {
|
||||
foreignKey: 'productId',
|
||||
as: 'transports',
|
||||
});
|
||||
|
||||
Transport.belongsTo(Vehicle, {
|
||||
foreignKey: 'vehicleId',
|
||||
as: 'vehicle',
|
||||
});
|
||||
Vehicle.hasMany(Transport, {
|
||||
foreignKey: 'vehicleId',
|
||||
as: 'transports',
|
||||
});
|
||||
|
||||
PromotionalGift.hasMany(PromotionalGiftCharacterTrait, { foreignKey: 'gift_id', as: 'characterTraits' });
|
||||
PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' });
|
||||
|
||||
@@ -493,44 +598,52 @@ export default function setupAssociations() {
|
||||
|
||||
Learning.belongsTo(LearnRecipient, {
|
||||
foreignKey: 'learningRecipientId',
|
||||
as: 'recipient'
|
||||
as: 'recipient',
|
||||
constraints: false
|
||||
}
|
||||
);
|
||||
|
||||
LearnRecipient.hasMany(Learning, {
|
||||
foreignKey: 'learningRecipientId',
|
||||
as: 'learnings'
|
||||
as: 'learnings',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
Learning.belongsTo(FalukantUser, {
|
||||
foreignKey: 'associatedFalukantUserId',
|
||||
as: 'learner'
|
||||
as: 'learner',
|
||||
constraints: false
|
||||
}
|
||||
);
|
||||
|
||||
FalukantUser.hasMany(Learning, {
|
||||
foreignKey: 'associatedFalukantUserId',
|
||||
as: 'learnings'
|
||||
as: 'learnings',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
Learning.belongsTo(ProductType, {
|
||||
foreignKey: 'productId',
|
||||
as: 'productType'
|
||||
as: 'productType',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
ProductType.hasMany(Learning, {
|
||||
foreignKey: 'productId',
|
||||
as: 'learnings'
|
||||
as: 'learnings',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
Learning.belongsTo(FalukantCharacter, {
|
||||
foreignKey: 'associatedLearningCharacterId',
|
||||
as: 'learningCharacter'
|
||||
as: 'learningCharacter',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
FalukantCharacter.hasMany(Learning, {
|
||||
foreignKey: 'associatedLearningCharacterId',
|
||||
as: 'learningsCharacter'
|
||||
as: 'learningsCharacter',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
FalukantUser.hasMany(Credit, {
|
||||
|
||||
@@ -8,16 +8,12 @@ const Folder = sequelize.define('folder', {
|
||||
allowNull: false},
|
||||
parentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'folder',
|
||||
key: 'id'}},
|
||||
allowNull: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'}}}, {
|
||||
allowNull: false
|
||||
}}, {
|
||||
tableName: 'folder',
|
||||
schema: 'community',
|
||||
underscored: true,
|
||||
|
||||
@@ -10,22 +10,11 @@ const FolderImageVisibility = sequelize.define('folder_image_visibility', {
|
||||
},
|
||||
folderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'folder',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
visibilityTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: {
|
||||
schema: 'type',
|
||||
tableName: 'image_visibility_type'
|
||||
},
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'folder_image_visibility',
|
||||
|
||||
@@ -10,19 +10,11 @@ const FolderVisibilityUser = sequelize.define('folder_visibility_user', {
|
||||
},
|
||||
folderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'folder',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
visibilityUserId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'image_visibility_user',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'folder_visibility_user',
|
||||
|
||||
@@ -10,19 +10,11 @@ const GuestbookEntry = sequelize.define('guestbook_entry', {
|
||||
allowNull: false},
|
||||
recipientId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
senderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: true
|
||||
},
|
||||
senderUsername: {
|
||||
type: DataTypes.STRING,
|
||||
|
||||
@@ -18,16 +18,12 @@ const Image = sequelize.define('image', {
|
||||
unique: true},
|
||||
folderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'folder',
|
||||
key: 'id'}},
|
||||
allowNull: false
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'}}}, {
|
||||
allowNull: false
|
||||
}}, {
|
||||
tableName: 'image',
|
||||
schema: 'community',
|
||||
underscored: true,
|
||||
|
||||
@@ -10,22 +10,11 @@ const ImageImageVisibility = sequelize.define('image_image_visibility', {
|
||||
},
|
||||
imageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'image',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
visibilityTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: {
|
||||
schema: 'type',
|
||||
tableName: 'image_visibility_type'
|
||||
},
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'image_image_visibility',
|
||||
|
||||
@@ -7,19 +7,11 @@ import { encrypt, decrypt } from '../../utils/encryption.js';
|
||||
const UserParam = sequelize.define('user_param', {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id',
|
||||
},
|
||||
allowNull: false
|
||||
},
|
||||
paramTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: UserParamType,
|
||||
key: 'id',
|
||||
},
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
|
||||
@@ -6,19 +6,11 @@ import UserRightType from '../type/user_right.js';
|
||||
const UserRight = sequelize.define('user_right', {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
rightTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: UserRightType,
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
}}, {
|
||||
tableName: 'user_right',
|
||||
schema: 'community',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
import WeatherType from '../type/weather.js';
|
||||
|
||||
class Production extends Model { }
|
||||
|
||||
@@ -13,6 +14,11 @@ Production.init({
|
||||
quantity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false},
|
||||
weatherTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Wetter zum Zeitpunkt der Produktionserstellung'
|
||||
},
|
||||
startTimestamp: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
|
||||
@@ -10,26 +10,24 @@ RegionData.init({
|
||||
allowNull: false},
|
||||
regionTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: RegionType,
|
||||
key: 'id',
|
||||
schema: 'falukant_type'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
parentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'region',
|
||||
key: 'id',
|
||||
schema: 'falukant_data'}
|
||||
allowNull: true
|
||||
},
|
||||
map: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: {}
|
||||
}
|
||||
,
|
||||
taxPercent: {
|
||||
type: DataTypes.DECIMAL,
|
||||
allowNull: false,
|
||||
defaultValue: 7,
|
||||
field: 'tax_percent'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'RegionData',
|
||||
|
||||
41
backend/models/falukant/data/region_distance.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
import RegionData from './region.js';
|
||||
|
||||
class RegionDistance extends Model {}
|
||||
|
||||
RegionDistance.init(
|
||||
{
|
||||
sourceRegionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
targetRegionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
transportMode: {
|
||||
// e.g. 'land', 'water', 'air' – should match VehicleType.transportMode
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
distance: {
|
||||
// distance between regions (e.g. in abstract units, used for travel time etc.)
|
||||
type: DataTypes.DOUBLE,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'RegionDistance',
|
||||
tableName: 'region_distance',
|
||||
schema: 'falukant_data',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default RegionDistance;
|
||||
|
||||
|
||||
|
||||
@@ -8,18 +8,10 @@ Relationship.init(
|
||||
{
|
||||
character1Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: FalukantCharacter,
|
||||
key: 'id'},
|
||||
onDelete: 'CASCADE'},
|
||||
allowNull: false},
|
||||
character2Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: FalukantCharacter,
|
||||
key: 'id'},
|
||||
onDelete: 'CASCADE'},
|
||||
allowNull: false},
|
||||
relationshipTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
|
||||
@@ -6,15 +6,20 @@ class FalukantStock extends Model { }
|
||||
FalukantStock.init({
|
||||
branchId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
allowNull: false
|
||||
},
|
||||
stockTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false},
|
||||
quantity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false}}, {
|
||||
allowNull: false},
|
||||
productQuality: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Quality of the stored product (0-100)'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'StockData',
|
||||
tableName: 'stock',
|
||||
|
||||
41
backend/models/falukant/data/transport.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class Transport extends Model {}
|
||||
|
||||
Transport.init(
|
||||
{
|
||||
sourceRegionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
targetRegionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
productId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // Nullable für leere Transporte (nur Fahrzeuge bewegen)
|
||||
},
|
||||
size: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // Nullable für leere Transporte (nur Fahrzeuge bewegen)
|
||||
},
|
||||
vehicleId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Transport',
|
||||
tableName: 'transport',
|
||||
schema: 'falukant_data',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default Transport;
|
||||
|
||||
|
||||
@@ -8,13 +8,6 @@ FalukantUser.init({
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: {
|
||||
tableName: 'user',
|
||||
schema: 'community'
|
||||
},
|
||||
key: 'id'
|
||||
},
|
||||
unique: true},
|
||||
money: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
@@ -38,12 +31,11 @@ FalukantUser.init({
|
||||
defaultValue: 1},
|
||||
mainBranchRegionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: RegionData,
|
||||
key: 'id',
|
||||
schema: 'falukant_data'
|
||||
}
|
||||
allowNull: true
|
||||
},
|
||||
lastNobilityAdvanceAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
@@ -26,13 +26,11 @@ UserHouse.init({
|
||||
},
|
||||
houseTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
allowNull: false
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
45
backend/models/falukant/data/vehicle.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class Vehicle extends Model {}
|
||||
|
||||
Vehicle.init(
|
||||
{
|
||||
vehicleTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
falukantUserId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
regionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
condition: {
|
||||
// current condition of the vehicle (0–100)
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 100,
|
||||
},
|
||||
availableFrom: {
|
||||
// timestamp when the vehicle becomes available for use
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Vehicle',
|
||||
tableName: 'vehicle',
|
||||
schema: 'falukant_data',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default Vehicle;
|
||||
|
||||
|
||||
30
backend/models/falukant/data/weather.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
import RegionData from './region.js';
|
||||
import WeatherType from '../type/weather.js';
|
||||
|
||||
class Weather extends Model {}
|
||||
|
||||
Weather.init(
|
||||
{
|
||||
regionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
weatherTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Weather',
|
||||
tableName: 'weather',
|
||||
schema: 'falukant_data',
|
||||
timestamps: false,
|
||||
underscored: true}
|
||||
);
|
||||
|
||||
export default Weather;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class Notification extends Model { }
|
||||
class Notification extends Model {
|
||||
// Getter für characterName - wird nicht synchronisiert, da es kein Datenbankfeld ist
|
||||
get characterName() {
|
||||
return this.getDataValue('character_name') || null;
|
||||
}
|
||||
}
|
||||
|
||||
Notification.init({
|
||||
userId: {
|
||||
@@ -10,6 +15,11 @@ Notification.init({
|
||||
tr: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false},
|
||||
character_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'character_name'
|
||||
},
|
||||
shown: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
|
||||
@@ -10,13 +10,11 @@ PromotionalGiftCharacterTrait.init(
|
||||
giftId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'gift_id',
|
||||
references: { model: PromotionalGift, key: 'id' },
|
||||
allowNull: false
|
||||
},
|
||||
traitId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'trait_id',
|
||||
references: { model: CharacterTrait, key: 'id' },
|
||||
allowNull: false
|
||||
},
|
||||
suitability: {
|
||||
|
||||
@@ -10,19 +10,11 @@ PromotionalGiftMood.init(
|
||||
giftId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'gift_id',
|
||||
references: {
|
||||
model: PromotionalGift,
|
||||
key: 'id'
|
||||
},
|
||||
allowNull: false
|
||||
},
|
||||
moodId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'mood_id',
|
||||
references: {
|
||||
model: Mood,
|
||||
key: 'id'
|
||||
},
|
||||
allowNull: false
|
||||
},
|
||||
suitability: {
|
||||
|
||||
41
backend/models/falukant/type/product_weather_effect.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
import ProductType from './product.js';
|
||||
import WeatherType from './weather.js';
|
||||
|
||||
class ProductWeatherEffect extends Model {}
|
||||
|
||||
ProductWeatherEffect.init(
|
||||
{
|
||||
productId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
weatherTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
qualityEffect: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: 'Effekt auf Qualität: -2 (sehr negativ), -1 (negativ), 0 (neutral), 1 (positiv), 2 (sehr positiv)'
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'ProductWeatherEffect',
|
||||
tableName: 'product_weather_effect',
|
||||
schema: 'falukant_type',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['product_id', 'weather_type_id']
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
export default ProductWeatherEffect;
|
||||
|
||||
@@ -9,11 +9,7 @@ RegionType.init({
|
||||
allowNull: false},
|
||||
parentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'region',
|
||||
key: 'id',
|
||||
schema: 'falukant_type'}
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
52
backend/models/falukant/type/vehicle.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class VehicleType extends Model {}
|
||||
|
||||
VehicleType.init(
|
||||
{
|
||||
tr: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
cost: {
|
||||
// base purchase cost of the vehicle
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
buildTimeMinutes: {
|
||||
// time to construct the vehicle, in minutes
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
capacity: {
|
||||
// transport capacity (e.g. in units of goods)
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
transportMode: {
|
||||
// e.g. 'land', 'water', 'air'
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
speed: {
|
||||
// abstract speed value, higher = faster
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'VehicleType',
|
||||
tableName: 'vehicle',
|
||||
schema: 'falukant_type',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default VehicleType;
|
||||
|
||||
|
||||
25
backend/models/falukant/type/weather.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class WeatherType extends Model {}
|
||||
|
||||
WeatherType.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true},
|
||||
tr: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false}},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'WeatherType',
|
||||
tableName: 'weather',
|
||||
schema: 'falukant_type',
|
||||
timestamps: false,
|
||||
underscored: true}
|
||||
);
|
||||
|
||||
export default WeatherType;
|
||||
|
||||
@@ -4,19 +4,11 @@ import { DataTypes } from 'sequelize';
|
||||
const ForumForumPermission = sequelize.define('forum_forum_permission', {
|
||||
forumId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'forum',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
permissionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'forum_permission',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'forum_forum_permission',
|
||||
|
||||
@@ -113,6 +113,13 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||
import ElectionHistory from './falukant/log/election_history.js';
|
||||
import UndergroundType from './falukant/type/underground.js';
|
||||
import Underground from './falukant/data/underground.js';
|
||||
import VehicleType from './falukant/type/vehicle.js';
|
||||
import Vehicle from './falukant/data/vehicle.js';
|
||||
import Transport from './falukant/data/transport.js';
|
||||
import RegionDistance from './falukant/data/region_distance.js';
|
||||
import WeatherType from './falukant/type/weather.js';
|
||||
import Weather from './falukant/data/weather.js';
|
||||
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
|
||||
|
||||
import Room from './chat/room.js';
|
||||
import ChatUser from './chat/user.js';
|
||||
@@ -207,6 +214,10 @@ const models = {
|
||||
Credit,
|
||||
DebtorsPrism,
|
||||
HealthActivity,
|
||||
RegionDistance,
|
||||
VehicleType,
|
||||
Vehicle,
|
||||
Transport,
|
||||
PoliticalOfficeType,
|
||||
PoliticalOfficeRequirement,
|
||||
PoliticalOfficeBenefitType,
|
||||
@@ -220,6 +231,9 @@ const models = {
|
||||
ElectionHistory,
|
||||
UndergroundType,
|
||||
Underground,
|
||||
WeatherType,
|
||||
Weather,
|
||||
ProductWeatherEffect,
|
||||
Room,
|
||||
ChatUser,
|
||||
ChatRight,
|
||||
|
||||
@@ -9,11 +9,7 @@ const Match3Level = sequelize.define('Match3Level', {
|
||||
},
|
||||
campaignId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'match3_campaigns',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
|
||||
@@ -10,19 +10,11 @@ const Match3LevelTileType = sequelize.define('Match3LevelTileType', {
|
||||
levelId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'match3_levels',
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'Referenz auf den Level'
|
||||
},
|
||||
tileTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'match3_tile_types',
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'Referenz auf den Tile-Typ'
|
||||
},
|
||||
weight: {
|
||||
|
||||
@@ -9,14 +9,7 @@ const TaxiHighscore = sequelize.define('TaxiHighscore', {
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // Kann null sein, falls User gelöscht wird
|
||||
references: {
|
||||
model: {
|
||||
tableName: 'user',
|
||||
schema: 'community'
|
||||
},
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: true // Kann null sein, falls User gelöscht wird
|
||||
},
|
||||
nickname: {
|
||||
type: DataTypes.STRING(100),
|
||||
@@ -44,13 +37,6 @@ const TaxiHighscore = sequelize.define('TaxiHighscore', {
|
||||
mapId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: {
|
||||
tableName: 'taxi_map',
|
||||
schema: 'taxi'
|
||||
},
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'ID der gespielten Map'
|
||||
},
|
||||
mapName: {
|
||||
|
||||
@@ -378,16 +378,19 @@ export async function createTriggers() {
|
||||
tp.election_id,
|
||||
tp.tp_office_type_id,
|
||||
tp.tp_election_date,
|
||||
(
|
||||
SELECT json_agg(vr)
|
||||
FROM votes vr
|
||||
WHERE vr.election_id = tp.election_id
|
||||
COALESCE(
|
||||
(
|
||||
SELECT json_agg(vr)
|
||||
FROM votes vr
|
||||
WHERE vr.election_id = tp.election_id
|
||||
),
|
||||
'[]'::json -- oder '{}'::json, wenn dir ein Objekt lieber ist
|
||||
),
|
||||
NOW() AS created_at,
|
||||
NOW() AS updated_at
|
||||
FROM to_process tp
|
||||
),
|
||||
|
||||
|
||||
-- 10) Cleanup: Stimmen, Kandidaten und Wahlen löschen
|
||||
_del_votes AS (
|
||||
DELETE FROM falukant_data.vote
|
||||
|
||||
@@ -13,13 +13,7 @@ const interestTranslation = sequelize.define('interest_translation_type', {
|
||||
},
|
||||
interestsId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Interest,
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
allowNull: false
|
||||
}}, {
|
||||
tableName: 'interest_translation',
|
||||
schema: 'type',
|
||||
|
||||
@@ -21,11 +21,7 @@ const UserParamType = sequelize.define('user_param_type', {
|
||||
},
|
||||
settingsId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'settings',
|
||||
key: 'id'
|
||||
}
|
||||
allowNull: false
|
||||
},
|
||||
orderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
@@ -7,7 +7,7 @@ const UserRightType = sequelize.define('user_right_type', {
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'user_right_type',
|
||||
tableName: 'user_right',
|
||||
schema: 'type',
|
||||
underscored: true
|
||||
});
|
||||
|
||||
1590
backend/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "NODE_ENV=development node server.js",
|
||||
"start-daemon": "node daemonServer.js",
|
||||
"sync-db": "node sync-database.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -15,7 +16,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"amqplib": "^0.10.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-redis": "^7.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -26,9 +27,9 @@
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer": "^2.0.0",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"nodemailer": "^7.0.11",
|
||||
"pg": "^8.12.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"redis": "^4.7.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom);
|
||||
// --- Users Admin ---
|
||||
router.get('/users/search', authenticate, adminController.searchUsers);
|
||||
router.get('/users/statistics', authenticate, adminController.getUserStatistics);
|
||||
router.get('/users/batch', authenticate, adminController.getUsers);
|
||||
router.get('/users/:id', authenticate, adminController.getUser);
|
||||
router.put('/users/:id', authenticate, adminController.updateUser);
|
||||
|
||||
@@ -40,6 +41,11 @@ router.get('/falukant/branches/:falukantUserId', authenticate, adminController.g
|
||||
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
|
||||
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
|
||||
router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes);
|
||||
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
|
||||
router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
|
||||
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
|
||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
||||
|
||||
// --- Minigames Admin ---
|
||||
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);
|
||||
|
||||
@@ -15,6 +15,7 @@ router.get('/branches/types', falukantController.getBranchTypes);
|
||||
router.get('/branches/:branch', falukantController.getBranch);
|
||||
router.get('/branches', falukantController.getBranches);
|
||||
router.post('/branches', falukantController.createBranch);
|
||||
router.post('/branches/upgrade', falukantController.upgradeBranch);
|
||||
router.get('/productions', falukantController.getAllProductions);
|
||||
router.post('/production', falukantController.createProduction);
|
||||
router.get('/production/:branchId', falukantController.getProduction);
|
||||
@@ -37,6 +38,7 @@ router.get('/director/:branchId', falukantController.getDirectorForBranch);
|
||||
router.get('/directors', falukantController.getAllDirectors);
|
||||
router.post('/directors', falukantController.updateDirector);
|
||||
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
|
||||
router.post('/family/set-heir', falukantController.setHeir);
|
||||
router.get('/family/gifts', falukantController.getGifts);
|
||||
router.get('/family/children', falukantController.getChildren);
|
||||
router.post('/family/gift', falukantController.sendGift);
|
||||
@@ -69,6 +71,17 @@ router.post('/politics/elections', falukantController.vote);
|
||||
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-cities', falukantController.getProductPricesInCities);
|
||||
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
|
||||
router.get('/vehicles/types', falukantController.getVehicleTypes);
|
||||
router.post('/vehicles', falukantController.buyVehicles);
|
||||
router.get('/vehicles', falukantController.getVehicles);
|
||||
router.post('/vehicles/:vehicleId/repair', falukantController.repairVehicle);
|
||||
router.post('/vehicles/repair-all', falukantController.repairAllVehicles);
|
||||
router.post('/transports', falukantController.createTransport);
|
||||
router.get('/transports/route', falukantController.getTransportRoute);
|
||||
router.get('/transports/branch/:branchId', falukantController.getBranchTransports);
|
||||
router.get('/underground/types', falukantController.getUndergroundTypes);
|
||||
router.get('/notifications', falukantController.getNotifications);
|
||||
router.get('/notifications/all', falukantController.getAllNotifications);
|
||||
|
||||
9
backend/routers/termineRouter.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import express from 'express';
|
||||
import termineController from '../controllers/termineController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', termineController.getTermine);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -19,7 +19,9 @@ import Branch from "../models/falukant/data/branch.js";
|
||||
import FalukantStock from "../models/falukant/data/stock.js";
|
||||
import FalukantStockType from "../models/falukant/type/stock.js";
|
||||
import RegionData from "../models/falukant/data/region.js";
|
||||
import RegionType from "../models/falukant/type/region.js";
|
||||
import BranchType from "../models/falukant/type/branch.js";
|
||||
import RegionDistance from "../models/falukant/data/region_distance.js";
|
||||
import Room from '../models/chat/room.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
|
||||
@@ -298,6 +300,104 @@ class AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
async getFalukantRegions(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
const regions = await RegionData.findAll({
|
||||
attributes: ['id', 'name', 'map'],
|
||||
include: [
|
||||
{
|
||||
model: RegionType,
|
||||
as: 'regionType',
|
||||
where: { labelTr: 'city' },
|
||||
attributes: ['labelTr'],
|
||||
},
|
||||
],
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
async updateFalukantRegionMap(userId, regionId, map) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
const region = await RegionData.findByPk(regionId);
|
||||
if (!region) {
|
||||
throw new Error('regionNotFound');
|
||||
}
|
||||
|
||||
region.map = map || {};
|
||||
await region.save();
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
async getRegionDistances(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
const distances = await RegionDistance.findAll();
|
||||
return distances;
|
||||
}
|
||||
|
||||
async upsertRegionDistance(userId, { sourceRegionId, targetRegionId, transportMode, distance }) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
if (!sourceRegionId || !targetRegionId || !transportMode) {
|
||||
throw new Error('missingParameters');
|
||||
}
|
||||
|
||||
const src = await RegionData.findByPk(sourceRegionId);
|
||||
const tgt = await RegionData.findByPk(targetRegionId);
|
||||
if (!src || !tgt) {
|
||||
throw new Error('regionNotFound');
|
||||
}
|
||||
|
||||
const mode = String(transportMode);
|
||||
const dist = Number(distance);
|
||||
if (!Number.isFinite(dist) || dist <= 0) {
|
||||
throw new Error('invalidDistance');
|
||||
}
|
||||
|
||||
const [record] = await RegionDistance.findOrCreate({
|
||||
where: {
|
||||
sourceRegionId: src.id,
|
||||
targetRegionId: tgt.id,
|
||||
transportMode: mode,
|
||||
},
|
||||
defaults: {
|
||||
distance: dist,
|
||||
},
|
||||
});
|
||||
|
||||
if (record.distance !== dist) {
|
||||
record.distance = dist;
|
||||
await record.save();
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async deleteRegionDistance(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const record = await RegionDistance.findByPk(id);
|
||||
if (!record) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
await record.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async updateFalukantStock(userId, stockId, quantity) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
@@ -441,6 +541,30 @@ class AdminService {
|
||||
return { id: user.hashedId, username: user.username, active: user.active, registrationDate: user.registrationDate };
|
||||
}
|
||||
|
||||
async getUsersByHashedIds(requestingHashedUserId, targetHashedIds) {
|
||||
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
if (!Array.isArray(targetHashedIds) || targetHashedIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const users = await User.findAll({
|
||||
where: { hashedId: { [Op.in]: targetHashedIds } },
|
||||
attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate']
|
||||
});
|
||||
// Erstelle ein Map für schnellen Zugriff
|
||||
const userMap = {};
|
||||
users.forEach(user => {
|
||||
userMap[user.hashedId] = {
|
||||
id: user.hashedId,
|
||||
username: user.username,
|
||||
active: user.active,
|
||||
registrationDate: user.registrationDate
|
||||
};
|
||||
});
|
||||
return userMap;
|
||||
}
|
||||
|
||||
async updateUser(requestingHashedUserId, targetHashedId, data) {
|
||||
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import User from '../models/community/user.js';
|
||||
|
||||
@@ -328,7 +328,7 @@ class SettingsService extends BaseService{
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
const bcrypt = await import('bcrypt');
|
||||
const bcrypt = await import('bcryptjs');
|
||||
const match = await bcrypt.compare(settings.oldpassword, user.password);
|
||||
if (!match) {
|
||||
throw new Error('Old password is incorrect');
|
||||
|
||||
88
backend/sql/add_character_name_to_notification.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- Migration script: add_character_name_to_notification.sql
|
||||
-- Fügt character_name und character_id zur falukant_log.notification Tabelle hinzu,
|
||||
-- legt Index an, erzeugt die Helper-Funktion und den Trigger.
|
||||
-- Idempotent und mit Down-Schritten zum Entfernen.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) Spalten anlegen
|
||||
ALTER TABLE IF EXISTS falukant_log.notification
|
||||
ADD COLUMN IF NOT EXISTS character_name text;
|
||||
|
||||
ALTER TABLE IF EXISTS falukant_log.notification
|
||||
ADD COLUMN IF NOT EXISTS character_id integer;
|
||||
|
||||
-- 2) Index (idempotent)
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_character_id
|
||||
ON falukant_log.notification (character_id);
|
||||
|
||||
-- 3) Trigger-Funktion anlegen (idempotent)
|
||||
CREATE OR REPLACE FUNCTION falukant_log.populate_notification_character_name()
|
||||
RETURNS TRIGGER AS $function$
|
||||
DECLARE
|
||||
v_first_name TEXT;
|
||||
v_last_name TEXT;
|
||||
v_char_id INTEGER;
|
||||
v_column_exists BOOLEAN;
|
||||
BEGIN
|
||||
-- prüfen, ob Zielspalte existiert
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_log' AND table_name = 'notification' AND column_name = 'character_name'
|
||||
) INTO v_column_exists;
|
||||
|
||||
IF NOT v_column_exists THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW.character_name IS NOT NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
v_char_id := NEW.character_id;
|
||||
|
||||
IF v_char_id IS NULL AND NEW.user_id IS NOT NULL THEN
|
||||
SELECT id INTO v_char_id
|
||||
FROM falukant_data.character
|
||||
WHERE user_id = NEW.user_id
|
||||
ORDER BY id DESC
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
IF v_char_id IS NOT NULL THEN
|
||||
SELECT pf.name, pl.name
|
||||
INTO v_first_name, v_last_name
|
||||
FROM falukant_data.character c
|
||||
LEFT JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
|
||||
LEFT JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
|
||||
WHERE c.id = v_char_id;
|
||||
|
||||
IF v_first_name IS NOT NULL OR v_last_name IS NOT NULL THEN
|
||||
NEW.character_name := COALESCE(v_first_name, '') || CASE WHEN v_first_name IS NOT NULL AND v_last_name IS NOT NULL THEN ' ' ELSE '' END || COALESCE(v_last_name, '');
|
||||
ELSE
|
||||
NEW.character_name := ('#' || v_char_id::text);
|
||||
END IF;
|
||||
ELSE
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
NEW.character_name := ('#u' || NEW.user_id::text);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$ LANGUAGE plpgsql;
|
||||
|
||||
-- 4) Trigger anlegen (BEFORE INSERT)
|
||||
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
|
||||
CREATE TRIGGER trg_populate_notification_character_name
|
||||
BEFORE INSERT ON falukant_log.notification
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION falukant_log.populate_notification_character_name();
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Down / Rollback (falls benötigt):
|
||||
-- Die folgenden Statements entfernen Trigger, Funktion, Index und Spalten.
|
||||
|
||||
-- Hinweis: Ausführbar separat; zur Anwendung einfach die folgenden Zeilen verwenden:
|
||||
-- BEGIN; DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification; DROP FUNCTION IF EXISTS falukant_log.populate_notification_character_name(); DROP INDEX IF EXISTS falukant_log.idx_notification_character_id; ALTER TABLE IF EXISTS falukant_log.notification DROP COLUMN IF EXISTS character_name; ALTER TABLE IF EXISTS falukant_log.notification DROP COLUMN IF EXISTS character_id; COMMIT;
|
||||
11
backend/sql/add_product_quality_to_stock.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Migration script: add_product_quality_to_stock.sql
|
||||
-- Fügt die Spalte product_quality zur Tabelle falukant_data.stock hinzu (nullable, idempotent)
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE IF EXISTS falukant_data.stock
|
||||
ADD COLUMN IF NOT EXISTS product_quality integer;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Ende
|
||||
38
backend/sql/add_weather_type_to_production.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- Migration script: add_weather_type_to_production.sql
|
||||
-- Legt die Spalte weather_type_id in falukant_data.production an,
|
||||
-- fügt optional einen Foreign Key zu falukant_type.weather(id) hinzu
|
||||
-- und erstellt einen Index. Idempotent (mehrfaches Ausführen ist unproblematisch).
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) Spalte anlegen (nullable, idempotent)
|
||||
ALTER TABLE IF EXISTS falukant_data.production
|
||||
ADD COLUMN IF NOT EXISTS weather_type_id integer;
|
||||
|
||||
-- 2) Fremdschlüssel nur hinzufügen, falls noch kein FK für diese Spalte existiert
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON kcu.constraint_name = tc.constraint_name
|
||||
AND kcu.constraint_schema = tc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.constraint_schema = 'falukant_data'
|
||||
AND tc.table_name = 'production'
|
||||
AND kcu.column_name = 'weather_type_id'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.production
|
||||
ADD CONSTRAINT fk_production_weather_type
|
||||
FOREIGN KEY (weather_type_id) REFERENCES falukant_type.weather(id);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- 3) Index (Postgres: CREATE INDEX IF NOT EXISTS)
|
||||
CREATE INDEX IF NOT EXISTS idx_production_weather_type_id
|
||||
ON falukant_data.production (weather_type_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Ende
|
||||
23
backend/sql/cleanup_orphaned_user_param_visibility.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Cleanup script: Entfernt verwaiste Einträge aus user_param_visibility
|
||||
-- Diese Einträge verweisen auf nicht existierende user_param Einträge
|
||||
-- und verhindern das Hinzufügen des Foreign Key Constraints
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Lösche alle user_param_visibility Einträge, deren param_id nicht mehr in user_param existiert
|
||||
DELETE FROM community.user_param_visibility
|
||||
WHERE param_id NOT IN (
|
||||
SELECT id FROM community.user_param
|
||||
);
|
||||
|
||||
-- Zeige an, wie viele Einträge gelöscht wurden
|
||||
DO $$
|
||||
DECLARE
|
||||
deleted_count INTEGER;
|
||||
BEGIN
|
||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||
RAISE NOTICE 'Gelöschte verwaiste Einträge: %', deleted_count;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
69
backend/sql/update_product_sell_costs.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- Backup original sell_cost values (just in case)
|
||||
-- Run this once: will add a column original_sell_cost and copy existing sell_cost into it
|
||||
ALTER TABLE IF EXISTS falukant_type.product
|
||||
ADD COLUMN IF NOT EXISTS original_sell_cost numeric;
|
||||
|
||||
UPDATE falukant_type.product
|
||||
SET sell_cost = sell_cost * ((6 * 7 / 100) + 100);
|
||||
|
||||
-- Compute min and max cumulative tax across all regions
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
|
||||
UNION ALL
|
||||
SELECT a.start_id, r.id, r.parent_id, r.tax_percent
|
||||
FROM falukant_data.region r
|
||||
JOIN ancestors a ON r.id = a.parent_id
|
||||
), totals AS (
|
||||
SELECT start_id, COALESCE(SUM(tax_percent),0) AS total FROM ancestors GROUP BY start_id
|
||||
), mm AS (
|
||||
SELECT COALESCE(MIN(total),0) AS min_total, COALESCE(MAX(total),0) AS max_total FROM totals
|
||||
)
|
||||
SELECT * FROM mm;
|
||||
|
||||
-- Choose one of the following update blocks to run:
|
||||
|
||||
-- 1) MIN-STRATEGY: increase sell_cost so that taxes at the minimal cumulative tax have no effect
|
||||
-- (this will set sell_cost = CEIL(original_sell_cost * (1 / (1 - min_total/100))))
|
||||
|
||||
-- BEGIN MIN-STRATEGY
|
||||
-- WITH mm AS (
|
||||
-- WITH RECURSIVE ancestors AS (
|
||||
-- SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
|
||||
-- UNION ALL
|
||||
-- SELECT a.start_id, r.id, r.parent_id, r.tax_percent
|
||||
-- FROM falukant_data.region r
|
||||
-- JOIN ancestors a ON r.id = a.parent_id
|
||||
-- ), totals AS (
|
||||
-- SELECT start_id, COALESCE(SUM(tax_percent),0) AS total FROM ancestors GROUP BY start_id
|
||||
-- )
|
||||
-- SELECT COALESCE(MIN(total),0) AS min_total FROM totals
|
||||
-- )
|
||||
-- UPDATE falukant_type.product
|
||||
-- SET sell_cost = CEIL(original_sell_cost * (CASE WHEN (1 - (SELECT min_total FROM mm)/100) <= 0 THEN 1 ELSE (1 / (1 - (SELECT min_total FROM mm)/100)) END));
|
||||
-- END MIN-STRATEGY
|
||||
|
||||
-- 2) MAX-STRATEGY: increase sell_cost so that taxes at the maximal cumulative tax have no effect
|
||||
-- (this will set sell_cost = CEIL(original_sell_cost * (1 / (1 - max_total/100))))
|
||||
|
||||
-- BEGIN MAX-STRATEGY
|
||||
-- WITH mm AS (
|
||||
-- WITH RECURSIVE ancestors AS (
|
||||
-- SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
|
||||
-- UNION ALL
|
||||
-- SELECT a.start_id, r.id, r.parent_id, r.tax_percent
|
||||
-- FROM falukant_data.region r
|
||||
-- JOIN ancestors a ON r.id = a.parent_id
|
||||
-- ), totals AS (
|
||||
-- SELECT start_id, COALESCE(SUM(tax_percent),0) AS total FROM ancestors GROUP BY start_id
|
||||
-- )
|
||||
-- SELECT COALESCE(MAX(total),0) AS max_total FROM totals
|
||||
-- )
|
||||
-- UPDATE falukant_type.product
|
||||
-- SET sell_cost = CEIL(original_sell_cost * (CASE WHEN (1 - (SELECT max_total FROM mm)/100) <= 0 THEN 1 ELSE (1 / (1 - (SELECT max_total FROM mm)/100)) END));
|
||||
-- END MAX-STRATEGY
|
||||
|
||||
-- Notes:
|
||||
-- 1) Uncomment exactly one strategy block (MIN or MAX) and run the script.
|
||||
-- 2) The script creates `original_sell_cost` as a backup; keep it for safety.
|
||||
-- 3) CEIL is used to avoid undercompensating due to rounding. If you prefer ROUND use ROUND(...).
|
||||
-- 4) Test on a staging DB first.
|
||||
@@ -228,10 +228,29 @@ async function initializeFalukantStockTypes() {
|
||||
}
|
||||
|
||||
async function initializeFalukantProducts() {
|
||||
await ProductType.bulkCreate([
|
||||
// compute min/max cumulative tax across regions
|
||||
const taxRows = await sequelize.query(`
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
|
||||
UNION ALL
|
||||
SELECT a.start_id, r.id, r.parent_id, r.tax_percent
|
||||
FROM falukant_data.region r
|
||||
JOIN ancestors a ON r.id = a.parent_id
|
||||
), totals AS (
|
||||
SELECT start_id, COALESCE(SUM(tax_percent), 0) AS total FROM ancestors GROUP BY start_id
|
||||
)
|
||||
SELECT COALESCE(MIN(total),0) AS min_total, COALESCE(MAX(total),0) AS max_total FROM totals
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
const minTax = parseFloat(taxRows?.[0]?.min_total) || 0;
|
||||
const maxTax = parseFloat(taxRows?.[0]?.max_total) || 0;
|
||||
const factorMin = (minTax >= 100) ? 1 : (1 / (1 - minTax / 100));
|
||||
const factorMax = (maxTax >= 100) ? 1 : (1 / (1 - maxTax / 100));
|
||||
|
||||
const baseProducts = [
|
||||
{ labelTr: 'wheat', category: 1, productionTime: 2, sellCost: 7 },
|
||||
{ labelTr: 'grain', category: 1, productionTime: 2, sellCost: 7 },
|
||||
{ labelTr: 'carrot', category: 1, productionTime: 1, sellCost: 46},
|
||||
{ labelTr: 'carrot', category: 1, productionTime: 1, sellCost: 5},
|
||||
{ labelTr: 'fish', category: 1, productionTime: 2, sellCost: 7 },
|
||||
{ labelTr: 'meat', category: 1, productionTime: 2, sellCost: 7 },
|
||||
{ labelTr: 'leather', category: 1, productionTime: 2, sellCost: 7 },
|
||||
@@ -261,7 +280,11 @@ async function initializeFalukantProducts() {
|
||||
{ labelTr: 'shield', category: 4, productionTime: 5, sellCost: 60 },
|
||||
{ labelTr: 'horse', category: 5, productionTime: 5, sellCost: 60 },
|
||||
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
|
||||
], {
|
||||
];
|
||||
|
||||
const productsToInsert = baseProducts;
|
||||
|
||||
await ProductType.bulkCreate(productsToInsert, {
|
||||
ignoreDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,11 +12,16 @@ import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js";
|
||||
import PartyType from "../../models/falukant/type/party.js";
|
||||
import MusicType from "../../models/falukant/type/music.js";
|
||||
import BanquetteType from "../../models/falukant/type/banquette.js";
|
||||
import VehicleType from "../../models/falukant/type/vehicle.js";
|
||||
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
|
||||
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
|
||||
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
|
||||
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
|
||||
import UndergroundType from "../../models/falukant/type/underground.js";
|
||||
import WeatherType from "../../models/falukant/type/weather.js";
|
||||
import Weather from "../../models/falukant/data/weather.js";
|
||||
import ProductWeatherEffect from "../../models/falukant/type/product_weather_effect.js";
|
||||
import ProductType from "../../models/falukant/type/product.js";
|
||||
|
||||
// Debug-Flag: Nur wenn DEBUG_FALUKANT=1 gesetzt ist, werden ausführliche Logs ausgegeben.
|
||||
const falukantDebug = process.env.DEBUG_FALUKANT === '1';
|
||||
@@ -41,6 +46,10 @@ export const initializeFalukantTypes = async () => {
|
||||
await initializePoliticalOfficeTypes();
|
||||
await initializePoliticalOfficePrerequisites();
|
||||
await initializeUndergroundTypes();
|
||||
await initializeVehicleTypes();
|
||||
await initializeFalukantWeatherTypes();
|
||||
await initializeFalukantWeathers();
|
||||
await initializeFalukantProductWeatherEffects();
|
||||
};
|
||||
|
||||
const regionTypes = [];
|
||||
@@ -273,6 +282,17 @@ const learnerTypes = [
|
||||
{ tr: 'director', },
|
||||
];
|
||||
|
||||
const vehicleTypes = [
|
||||
// build times (in minutes): 60, 90, 180, 300, 720, 120, 1440
|
||||
{ tr: 'cargo_cart', name: 'Lastkarren', cost: 100, capacity: 20, transportMode: 'land', speed: 1, buildTimeMinutes: 60 },
|
||||
{ tr: 'ox_cart', name: 'Ochsenkarren', cost: 200, capacity: 50, transportMode: 'land', speed: 2, buildTimeMinutes: 90 },
|
||||
{ tr: 'small_carriage', name: 'kleine Pferdekutsche', cost: 300, capacity: 35, transportMode: 'land', speed: 3, buildTimeMinutes: 180 },
|
||||
{ tr: 'large_carriage', name: 'große Pferdekutsche', cost: 1000, capacity: 100, transportMode: 'land', speed: 3, buildTimeMinutes: 300 },
|
||||
{ tr: 'four_horse_carriage', name: 'Vierspänner', cost: 5000, capacity: 200, transportMode: 'land', speed: 4, buildTimeMinutes: 720 },
|
||||
{ tr: 'raft', name: 'Floß', cost: 100, capacity: 25, transportMode: 'water', speed: 1, buildTimeMinutes: 120 },
|
||||
{ tr: 'sailing_ship', name: 'Segelschiff', cost: 500, capacity: 200, transportMode: 'water', speed: 3, buildTimeMinutes: 1440 },
|
||||
];
|
||||
|
||||
const politicalOfficeBenefitTypes = [
|
||||
{ tr: 'salary' },
|
||||
{ tr: 'reputation' },
|
||||
@@ -282,6 +302,7 @@ const politicalOfficeBenefitTypes = [
|
||||
{ tr: 'tax_exemption' },
|
||||
{ tr: 'guard_protection' },
|
||||
{ tr: 'court_immunity' },
|
||||
{ tr: 'set_regionl_tax' },
|
||||
];
|
||||
|
||||
const politicalOffices = [
|
||||
@@ -883,6 +904,31 @@ export const initializeLearnerTypes = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeVehicleTypes = async () => {
|
||||
for (const v of vehicleTypes) {
|
||||
const existing = await VehicleType.findOne({ where: { tr: v.tr } });
|
||||
if (!existing) {
|
||||
await VehicleType.create({
|
||||
tr: v.tr,
|
||||
cost: v.cost,
|
||||
capacity: v.capacity,
|
||||
transportMode: v.transportMode,
|
||||
speed: v.speed,
|
||||
buildTimeMinutes: v.buildTimeMinutes,
|
||||
});
|
||||
} else {
|
||||
// ensure new fields like cost/buildTime are updated if missing
|
||||
await existing.update({
|
||||
cost: v.cost,
|
||||
capacity: v.capacity,
|
||||
transportMode: v.transportMode,
|
||||
speed: v.speed,
|
||||
buildTimeMinutes: v.buildTimeMinutes,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const initializePoliticalOfficeBenefitTypes = async () => {
|
||||
for (const benefitType of politicalOfficeBenefitTypes) {
|
||||
await PoliticalOfficeBenefitType.findOrCreate({
|
||||
@@ -972,3 +1018,445 @@ export const initializeFalukantTitlesOfNobility = async () => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const weatherTypes = [
|
||||
{ tr: "sunny" },
|
||||
{ tr: "cloudy" },
|
||||
{ tr: "rainy" },
|
||||
{ tr: "stormy" },
|
||||
{ tr: "snowy" },
|
||||
{ tr: "foggy" },
|
||||
{ tr: "windy" },
|
||||
{ tr: "clear" }
|
||||
];
|
||||
|
||||
export const initializeFalukantWeatherTypes = async () => {
|
||||
try {
|
||||
for (const weatherType of weatherTypes) {
|
||||
await WeatherType.findOrCreate({
|
||||
where: { tr: weatherType.tr },
|
||||
});
|
||||
}
|
||||
console.log(`[Falukant] Wettertypen initialisiert: ${weatherTypes.length} Typen`);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Initialisieren der Falukant-Wettertypen:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeFalukantWeathers = async () => {
|
||||
try {
|
||||
// Hole alle Städte (Regions vom Typ "city")
|
||||
const cityRegionType = await RegionType.findOne({ where: { labelTr: 'city' } });
|
||||
if (!cityRegionType) {
|
||||
console.warn('[Falukant] Kein RegionType "city" gefunden, überspringe Wetter-Initialisierung');
|
||||
return;
|
||||
}
|
||||
|
||||
const cities = await RegionData.findAll({
|
||||
where: { regionTypeId: cityRegionType.id },
|
||||
attributes: ['id', 'name']
|
||||
});
|
||||
|
||||
// Hole alle Wettertypen
|
||||
const allWeatherTypes = await WeatherType.findAll();
|
||||
if (allWeatherTypes.length === 0) {
|
||||
console.warn('[Falukant] Keine Wettertypen gefunden, überspringe Wetter-Initialisierung');
|
||||
return;
|
||||
}
|
||||
|
||||
// Weise jeder Stadt zufällig ein Wetter zu
|
||||
for (const city of cities) {
|
||||
const randomWeatherType = allWeatherTypes[Math.floor(Math.random() * allWeatherTypes.length)];
|
||||
await Weather.findOrCreate({
|
||||
where: { regionId: city.id },
|
||||
defaults: {
|
||||
weatherTypeId: randomWeatherType.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Falukant] Wetter für ${cities.length} Städte initialisiert`);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Initialisieren der Falukant-Wetter:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeFalukantProductWeatherEffects = async () => {
|
||||
try {
|
||||
// Hole alle Produkte und Wettertypen
|
||||
const products = await ProductType.findAll();
|
||||
const weatherTypes = await WeatherType.findAll();
|
||||
|
||||
if (products.length === 0 || weatherTypes.length === 0) {
|
||||
console.warn('[Falukant] Keine Produkte oder Wettertypen gefunden, überspringe Produkt-Wetter-Effekte');
|
||||
return;
|
||||
}
|
||||
|
||||
// Erstelle Map für schnellen Zugriff
|
||||
const productMap = new Map(products.map(p => [p.labelTr, p.id]));
|
||||
const weatherMap = new Map(weatherTypes.map(w => [w.tr, w.id]));
|
||||
|
||||
// Definiere Effekte für jedes Produkt-Wetter-Paar
|
||||
// Format: { productLabel: { weatherTr: effectValue } }
|
||||
// effectValue: -2 (sehr negativ), -1 (negativ), 0 (neutral), 1 (positiv), 2 (sehr positiv)
|
||||
const effects = {
|
||||
// Landwirtschaftliche Produkte
|
||||
wheat: {
|
||||
sunny: 1, // Gutes Wachstum
|
||||
cloudy: 0,
|
||||
rainy: 2, // Wasser ist essentiell
|
||||
stormy: -1, // Kann Ernte beschädigen
|
||||
snowy: -2, // Kein Wachstum
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
grain: {
|
||||
sunny: 1,
|
||||
cloudy: 0,
|
||||
rainy: 2,
|
||||
stormy: -1,
|
||||
snowy: -2,
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
carrot: {
|
||||
sunny: 1,
|
||||
cloudy: 0,
|
||||
rainy: 2,
|
||||
stormy: -1,
|
||||
snowy: -2,
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
fish: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: -2, // Gefährlich zu fischen
|
||||
snowy: -1, // Kaltes Wasser
|
||||
foggy: -1, // Schlechte Sicht
|
||||
windy: -1, // Schwierig zu fischen
|
||||
clear: 1
|
||||
},
|
||||
meat: {
|
||||
sunny: -1, // Kann verderben
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 1, // Kühlt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
leather: {
|
||||
sunny: -1, // Kann austrocknen
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 1, // Kühlt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
wood: {
|
||||
sunny: 1, // Trocknet gut
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -2, // Kann beschädigt werden
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
stone: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: 0,
|
||||
snowy: 0,
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
milk: {
|
||||
sunny: -1, // Kann sauer werden
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: -1,
|
||||
snowy: 1, // Kühlt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
cheese: {
|
||||
sunny: -1,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -1,
|
||||
snowy: 1, // Kühlt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
bread: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -1,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
beer: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: 0,
|
||||
snowy: 1, // Kühlt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
iron: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Rost
|
||||
stormy: -2, // Rost
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
copper: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Oxidation
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
spices: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -1,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
salt: {
|
||||
sunny: 1, // Trocknet gut
|
||||
cloudy: 0,
|
||||
rainy: -2, // Löst sich auf
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
sugar: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -2, // Löst sich auf
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
vinegar: {
|
||||
sunny: 1, // Heißes Wetter fördert Gärung
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: 0,
|
||||
snowy: -1, // Kaltes Wetter hemmt Gärung
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 1 // Heißes Wetter fördert Gärung
|
||||
},
|
||||
cotton: {
|
||||
sunny: 1, // Trocknet gut
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
wine: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: 0,
|
||||
snowy: 1, // Kühlt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
gold: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: 0,
|
||||
snowy: 0,
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
diamond: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: 0,
|
||||
stormy: 0,
|
||||
snowy: 0,
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
furniture: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
clothing: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
jewelry: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -2, // Kann beschädigt werden
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
painting: {
|
||||
sunny: -1, // Kann verblassen
|
||||
cloudy: 0,
|
||||
rainy: -2, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: -1
|
||||
},
|
||||
book: {
|
||||
sunny: -1, // Kann verblassen
|
||||
cloudy: 0,
|
||||
rainy: -2, // Feucht
|
||||
stormy: -2,
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: -1
|
||||
},
|
||||
weapon: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Rost
|
||||
stormy: -2, // Rost
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
armor: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Rost
|
||||
stormy: -2, // Rost
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
shield: {
|
||||
sunny: 0,
|
||||
cloudy: 0,
|
||||
rainy: -1, // Rost
|
||||
stormy: -2, // Rost
|
||||
snowy: 0,
|
||||
foggy: -1, // Feucht
|
||||
windy: 0,
|
||||
clear: 0
|
||||
},
|
||||
horse: {
|
||||
sunny: 1, // Gutes Wetter
|
||||
cloudy: 0,
|
||||
rainy: -1, // Nass
|
||||
stormy: -2, // Angst
|
||||
snowy: -1, // Kalt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 1
|
||||
},
|
||||
ox: {
|
||||
sunny: 1, // Gutes Wetter
|
||||
cloudy: 0,
|
||||
rainy: -1, // Nass
|
||||
stormy: -2, // Angst
|
||||
snowy: -1, // Kalt
|
||||
foggy: 0,
|
||||
windy: 0,
|
||||
clear: 1
|
||||
}
|
||||
};
|
||||
|
||||
// Erstelle alle Produkt-Wetter-Effekte
|
||||
const effectEntries = [];
|
||||
for (const [productLabel, weatherEffects] of Object.entries(effects)) {
|
||||
const productId = productMap.get(productLabel);
|
||||
if (!productId) {
|
||||
console.warn(`[Falukant] Produkt "${productLabel}" nicht gefunden, überspringe`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [weatherTr, effectValue] of Object.entries(weatherEffects)) {
|
||||
const weatherTypeId = weatherMap.get(weatherTr);
|
||||
if (!weatherTypeId) {
|
||||
console.warn(`[Falukant] Wettertyp "${weatherTr}" nicht gefunden, überspringe`);
|
||||
continue;
|
||||
}
|
||||
|
||||
effectEntries.push({
|
||||
productId,
|
||||
weatherTypeId,
|
||||
qualityEffect: effectValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk insert mit ignoreDuplicates
|
||||
await ProductWeatherEffect.bulkCreate(effectEntries, {
|
||||
ignoreDuplicates: true
|
||||
});
|
||||
|
||||
console.log(`[Falukant] Produkt-Wetter-Effekte initialisiert: ${effectEntries.length} Einträge`);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Initialisieren der Produkt-Wetter-Effekte:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,6 +45,16 @@ const createSchemas = async () => {
|
||||
|
||||
const initializeDatabase = async () => {
|
||||
await createSchemas();
|
||||
|
||||
// Aktiviere die pgcrypto Erweiterung für die digest() Funktion
|
||||
try {
|
||||
await sequelize.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
|
||||
console.log('✅ pgcrypto Erweiterung aktiviert');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Konnte pgcrypto Erweiterung nicht aktivieren:', error.message);
|
||||
// Fortfahren, da die Erweiterung möglicherweise bereits aktiviert ist
|
||||
}
|
||||
|
||||
// Modelle nur laden, aber an dieser Stelle NICHT syncen.
|
||||
// Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral
|
||||
// über syncModelsWithUpdates()/syncModelsAlways gesteuert.
|
||||
@@ -95,7 +105,8 @@ const syncModelsWithUpdates = async (models) => {
|
||||
if (needsUpdate) {
|
||||
console.log('🔄 Schema-Updates nötig - verwende alter: true');
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: true, force: false });
|
||||
// constraints: false verhindert, dass Sequelize Foreign Keys automatisch erstellt
|
||||
await model.sync({ alter: true, force: false, constraints: false });
|
||||
}
|
||||
console.log('✅ Schema-Updates abgeschlossen');
|
||||
} else {
|
||||
@@ -363,7 +374,8 @@ const getExpectedDefaultValue = (defaultValue) => {
|
||||
const updateSchema = async (models) => {
|
||||
console.log('🔄 Aktualisiere Datenbankschema...');
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: true, force: false });
|
||||
// constraints: false verhindert, dass Sequelize Foreign Keys automatisch erstellt
|
||||
await model.sync({ alter: true, force: false, constraints: false });
|
||||
}
|
||||
console.log('✅ Datenbankschema aktualisiert');
|
||||
};
|
||||
@@ -406,8 +418,311 @@ const syncModelsAlways = async (models) => {
|
||||
console.log('🔍 Deployment-Modus: Führe immer Schema-Updates durch...');
|
||||
|
||||
try {
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: true, force: false });
|
||||
for (const model of Object.values(models)) {
|
||||
// Temporarily remove VIRTUAL fields before sync to prevent sync errors
|
||||
const originalAttributes = model.rawAttributes;
|
||||
const virtualFields = {};
|
||||
|
||||
// Find and temporarily remove VIRTUAL fields
|
||||
// Check multiple ways to identify VIRTUAL fields
|
||||
for (const [key, attr] of Object.entries(originalAttributes)) {
|
||||
// Check if it's a VIRTUAL field by checking the type
|
||||
let isVirtual = false;
|
||||
|
||||
if (attr.type) {
|
||||
// Method 1: Check if type key is VIRTUAL (most reliable)
|
||||
if (attr.type.key === 'VIRTUAL') {
|
||||
isVirtual = true;
|
||||
}
|
||||
// Method 2: Direct comparison with DataTypes.VIRTUAL
|
||||
else if (attr.type === DataTypes.VIRTUAL) {
|
||||
isVirtual = true;
|
||||
}
|
||||
// Method 3: Check toString representation
|
||||
else if (typeof attr.type.toString === 'function') {
|
||||
const typeStr = attr.type.toString();
|
||||
if (typeStr === 'VIRTUAL' || typeStr.includes('VIRTUAL')) {
|
||||
isVirtual = true;
|
||||
}
|
||||
}
|
||||
// Method 4: Check constructor name
|
||||
else if (attr.type.constructor && attr.type.constructor.name === 'VIRTUAL') {
|
||||
isVirtual = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if field has a getter but no setter and no field mapping (common pattern for VIRTUAL fields)
|
||||
// But only if it doesn't have a 'field' property, which means it's not mapped to a database column
|
||||
if (!isVirtual && attr.get && !attr.set && !attr.field) {
|
||||
// This might be a VIRTUAL field, but be careful not to remove real fields
|
||||
// Only remove if we're certain it's VIRTUAL
|
||||
}
|
||||
|
||||
if (isVirtual) {
|
||||
virtualFields[key] = attr;
|
||||
delete model.rawAttributes[key];
|
||||
console.log(` ⚠️ Temporarily removed VIRTUAL field: ${key} from model ${model.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for Notification model: ensure characterName VIRTUAL field is removed
|
||||
// This is a workaround for Sequelize bug where it confuses characterName (VIRTUAL) with character_name (STRING)
|
||||
if (model.name === 'Notification' && model.rawAttributes.characterName) {
|
||||
if (!virtualFields.characterName) {
|
||||
virtualFields.characterName = model.rawAttributes.characterName;
|
||||
delete model.rawAttributes.characterName;
|
||||
console.log(` ⚠️ Explicitly removed VIRTUAL field: characterName from Notification model`);
|
||||
}
|
||||
}
|
||||
|
||||
// constraints: false wird von Sequelize ignoriert wenn Associations vorhanden sind
|
||||
// Wir müssen die Associations temporär entfernen, um Foreign Keys zu verhindern
|
||||
const originalAssociations = model.associations ? { ...model.associations } : {};
|
||||
const associationKeys = Object.keys(originalAssociations);
|
||||
|
||||
try {
|
||||
// Entferne temporär alle Associations, damit Sequelize keine Foreign Keys erstellt
|
||||
// Dies muss innerhalb des try Blocks sein, damit die Wiederherstellung im finally Block garantiert ist
|
||||
if (associationKeys.length > 0) {
|
||||
console.log(` ⚠️ Temporarily removing ${associationKeys.length} associations from ${model.name} to prevent FK creation`);
|
||||
// Lösche alle Associations temporär
|
||||
for (const key of associationKeys) {
|
||||
delete model.associations[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Entferne bestehende Foreign Keys vor dem Sync, damit Sequelize sie nicht aktualisiert
|
||||
try {
|
||||
const tableName = model.tableName;
|
||||
// Schema kann eine Funktion sein, daher prüfen wir model.options.schema direkt
|
||||
const schema = model.options?.schema || 'public';
|
||||
|
||||
console.log(` 🔍 Checking for foreign keys in ${schema}.${tableName}...`);
|
||||
const foreignKeys = await sequelize.query(`
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name = :tableName
|
||||
AND tc.table_schema = :schema
|
||||
`, {
|
||||
replacements: { tableName, schema },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (foreignKeys && foreignKeys.length > 0) {
|
||||
console.log(` ⚠️ Found ${foreignKeys.length} existing foreign keys:`, foreignKeys.map(fk => fk.constraint_name).join(', '));
|
||||
console.log(` ⚠️ Removing ${foreignKeys.length} existing foreign keys from ${model.name} (schema: ${schema}) before sync`);
|
||||
for (const fk of foreignKeys) {
|
||||
console.log(` 🗑️ Dropping constraint: ${fk.constraint_name}`);
|
||||
await sequelize.query(`
|
||||
ALTER TABLE "${schema}"."${tableName}"
|
||||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||||
`);
|
||||
}
|
||||
console.log(` ✅ All foreign keys removed for ${model.name}`);
|
||||
} else {
|
||||
console.log(` ✅ No foreign keys found for ${model.name}`);
|
||||
}
|
||||
} catch (fkError) {
|
||||
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message);
|
||||
console.warn(` ⚠️ Error details:`, fkError);
|
||||
}
|
||||
|
||||
console.log(` 🔄 Syncing model ${model.name} with constraints: false`);
|
||||
try {
|
||||
// Versuche doppelte pg_description Einträge vor dem Sync zu bereinigen
|
||||
// Hinweis: Benötigt Superuser-Rechte oder spezielle Berechtigungen
|
||||
try {
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
// Verwende direkte Parameter-Einsetzung, da DO $$ keine Parameterbindung unterstützt
|
||||
// Die Parameter sind sicher, da sie von Sequelize-Modell-Eigenschaften kommen
|
||||
await sequelize.query(`
|
||||
DELETE FROM pg_catalog.pg_description d1
|
||||
WHERE d1.objoid IN (
|
||||
SELECT c.oid
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relname = '${tableName.replace(/'/g, "''")}'
|
||||
AND n.nspname = '${schema.replace(/'/g, "''")}'
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_description d2
|
||||
WHERE d2.objoid = d1.objoid
|
||||
AND d2.objsubid = d1.objsubid
|
||||
AND d2.ctid < d1.ctid
|
||||
)
|
||||
`);
|
||||
} catch (descError) {
|
||||
// Ignoriere Berechtigungsfehler - das ist normal, wenn der Benutzer keine Superuser-Rechte hat
|
||||
if (descError.message && descError.message.includes('Berechtigung')) {
|
||||
console.log(` ℹ️ Cannot clean up duplicate pg_description entries (requires superuser privileges): ${model.name}`);
|
||||
} else {
|
||||
console.warn(` ⚠️ Could not clean up duplicate pg_description entries for ${model.name}:`, descError.message);
|
||||
}
|
||||
}
|
||||
|
||||
await model.sync({ alter: true, force: false, constraints: false });
|
||||
} catch (syncError) {
|
||||
// Wenn Sequelize einen "mehr als eine Zeile" Fehler hat, überspringe das Model
|
||||
// Dies kann durch doppelte pg_description Einträge oder mehrere Tabellen mit demselben Namen verursacht werden
|
||||
if (syncError.message && (syncError.message.includes('mehr als eine Zeile') || syncError.message.includes('more than one row'))) {
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
console.error(` ❌ Cannot sync ${model.name} (${schema}.${tableName}) due to Sequelize describeTable error`);
|
||||
console.error(` ❌ This is likely caused by multiple tables with the same name in different schemas`);
|
||||
console.error(` ❌ or duplicate pg_description entries (requires superuser to fix)`);
|
||||
console.error(` ⚠️ Skipping sync for ${model.name} - Schema is likely already correct`);
|
||||
// Überspringe dieses Model und fahre mit dem nächsten fort
|
||||
continue;
|
||||
}
|
||||
// Wenn eine referenzierte Tabelle noch nicht existiert, erstelle die Tabelle ohne Foreign Key
|
||||
else if (syncError.message && (syncError.message.includes('existiert nicht') || syncError.message.includes('does not exist') || syncError.message.includes('Relation'))) {
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
console.warn(` ⚠️ Cannot create ${model.name} (${schema}.${tableName}) with Foreign Key - referenced table does not exist yet`);
|
||||
console.warn(` ⚠️ Attempting to create table without Foreign Key constraint...`);
|
||||
|
||||
try {
|
||||
// Prüfe, ob die Tabelle bereits existiert
|
||||
const [tableExists] = await sequelize.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = :schema
|
||||
AND table_name = :tableName
|
||||
) as exists
|
||||
`, {
|
||||
replacements: { schema, tableName },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (tableExists && tableExists.exists) {
|
||||
console.log(` ℹ️ Table ${schema}.${tableName} already exists, skipping creation`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Erstelle die Tabelle manuell ohne Foreign Key
|
||||
// Verwende queryInterface.createTable mit den Attributen, aber ohne Foreign Keys
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
const attributes = {};
|
||||
|
||||
// Kopiere alle Attribute aus dem Model, aber entferne references
|
||||
for (const [key, attr] of Object.entries(model.rawAttributes)) {
|
||||
attributes[key] = { ...attr };
|
||||
// Entferne references, damit kein Foreign Key erstellt wird
|
||||
if (attributes[key].references) {
|
||||
delete attributes[key].references;
|
||||
}
|
||||
}
|
||||
|
||||
// Erstelle die Tabelle mit queryInterface.createTable ohne Foreign Keys
|
||||
await queryInterface.createTable(tableName, attributes, {
|
||||
schema,
|
||||
// Stelle sicher, dass keine Foreign Keys erstellt werden
|
||||
charset: model.options?.charset,
|
||||
collate: model.options?.collate
|
||||
});
|
||||
console.log(` ✅ Table ${schema}.${tableName} created successfully without Foreign Key`);
|
||||
} catch (createError) {
|
||||
console.error(` ❌ Failed to create table ${schema}.${tableName} without Foreign Key:`, createError.message);
|
||||
console.error(` ⚠️ Skipping ${model.name} - will retry after dependencies are created`);
|
||||
// Überspringe dieses Model und fahre mit dem nächsten fort
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Wenn Sequelize einen Foreign Key Constraint Fehler hat, entferne verwaiste Einträge oder überspringe das Model
|
||||
else if (syncError.name === 'SequelizeForeignKeyConstraintError' || (syncError.message && (syncError.message.includes('FOREIGN KEY') || syncError.message.includes('Fremdschlüssel')))) {
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
console.error(` ❌ Cannot sync ${model.name} (${schema}.${tableName}) due to Foreign Key Constraint Error`);
|
||||
console.error(` ❌ Detail: ${syncError.parent?.detail || syncError.message}`);
|
||||
console.error(` ⚠️ This usually means there are orphaned records. Cleanup should have removed them.`);
|
||||
console.error(` ⚠️ Skipping sync for ${model.name} - please check and fix orphaned records manually`);
|
||||
// Überspringe dieses Model und fahre mit dem nächsten fort
|
||||
continue;
|
||||
}
|
||||
// Wenn Sequelize versucht, Foreign Keys zu erstellen, entferne sie nach dem Fehler
|
||||
else if (syncError.message && syncError.message.includes('REFERENCES')) {
|
||||
console.log(` ⚠️ Sequelize tried to create FK despite constraints: false, removing any created FKs...`);
|
||||
try {
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
const foreignKeys = await sequelize.query(`
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name = :tableName
|
||||
AND tc.table_schema = :schema
|
||||
`, {
|
||||
replacements: { tableName, schema },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (foreignKeys && foreignKeys.length > 0) {
|
||||
for (const fk of foreignKeys) {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE "${schema}"."${tableName}"
|
||||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||||
`);
|
||||
}
|
||||
}
|
||||
// Versuche Sync erneut ohne Foreign Keys
|
||||
console.log(` 🔄 Retrying sync without foreign keys...`);
|
||||
await model.sync({ alter: true, force: false, constraints: false });
|
||||
} catch (retryError) {
|
||||
console.error(` ❌ Retry failed:`, retryError.message);
|
||||
console.error(` ❌ Original sync error:`, syncError.message);
|
||||
// Kombiniere beide Fehler für besseres Debugging
|
||||
const combinedError = new Error(`Sync failed: ${syncError.message}. Retry also failed: ${retryError.message}`);
|
||||
combinedError.originalError = syncError;
|
||||
combinedError.retryError = retryError;
|
||||
throw combinedError;
|
||||
}
|
||||
} else {
|
||||
throw syncError;
|
||||
}
|
||||
}
|
||||
|
||||
// Entferne alle Foreign Keys, die Sequelize möglicherweise trotzdem erstellt hat
|
||||
try {
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
const foreignKeys = await sequelize.query(`
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name = :tableName
|
||||
AND tc.table_schema = :schema
|
||||
`, {
|
||||
replacements: { tableName, schema },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (foreignKeys && foreignKeys.length > 0) {
|
||||
console.log(` ⚠️ Sequelize created ${foreignKeys.length} foreign keys despite constraints: false, removing them...`);
|
||||
for (const fk of foreignKeys) {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE "${schema}"."${tableName}"
|
||||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||||
`);
|
||||
}
|
||||
}
|
||||
} catch (fkError) {
|
||||
console.warn(` ⚠️ Could not check/remove foreign keys after sync:`, fkError.message);
|
||||
}
|
||||
} finally {
|
||||
// Stelle die Associations wieder her (IMMER, auch bei Fehlern)
|
||||
if (associationKeys.length > 0) {
|
||||
console.log(` ✅ Restoring ${associationKeys.length} associations for ${model.name}`);
|
||||
model.associations = originalAssociations;
|
||||
}
|
||||
|
||||
// Restore VIRTUAL fields after sync
|
||||
for (const [key, attr] of Object.entries(virtualFields)) {
|
||||
model.rawAttributes[key] = attr;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('✅ Schema-Updates für alle Models abgeschlossen');
|
||||
} catch (error) {
|
||||
|
||||
@@ -54,6 +54,112 @@ const syncDatabase = async () => {
|
||||
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Cleanup: Entferne verwaiste Einträge vor Schema-Updates (nur wenn Schema-Updates aktiviert)
|
||||
if (currentStage === 'dev') {
|
||||
console.log("Cleaning up orphaned entries...");
|
||||
try {
|
||||
// Cleanup user_param_visibility
|
||||
const result1 = await sequelize.query(`
|
||||
DELETE FROM community.user_param_visibility
|
||||
WHERE param_id NOT IN (
|
||||
SELECT id FROM community.user_param
|
||||
);
|
||||
`);
|
||||
const deletedCount1 = result1[1] || 0;
|
||||
if (deletedCount1 > 0) {
|
||||
console.log(`✅ ${deletedCount1} verwaiste user_param_visibility Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup stock mit ungültigen branch_id (0 oder nicht existierend)
|
||||
const result2 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.stock
|
||||
WHERE branch_id = 0 OR branch_id NOT IN (
|
||||
SELECT id FROM falukant_data.branch
|
||||
);
|
||||
`);
|
||||
const deletedCount2 = result2[1] || 0;
|
||||
if (deletedCount2 > 0) {
|
||||
console.log(`✅ ${deletedCount2} verwaiste stock Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup knowledge mit ungültigen character_id oder product_id
|
||||
const result3 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.knowledge
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR product_id NOT IN (
|
||||
SELECT id FROM falukant_type.product
|
||||
);
|
||||
`);
|
||||
const deletedCount3 = result3[1] || 0;
|
||||
if (deletedCount3 > 0) {
|
||||
console.log(`✅ ${deletedCount3} verwaiste knowledge Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup notification mit ungültigen user_id
|
||||
const result4 = await sequelize.query(`
|
||||
DELETE FROM falukant_log.notification
|
||||
WHERE user_id NOT IN (
|
||||
SELECT id FROM falukant_data.falukant_user
|
||||
);
|
||||
`);
|
||||
const deletedCount4 = result4[1] || 0;
|
||||
if (deletedCount4 > 0) {
|
||||
console.log(`✅ ${deletedCount4} verwaiste notification Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id
|
||||
const result5 = await sequelize.query(`
|
||||
DELETE FROM falukant_log.promotional_gift
|
||||
WHERE sender_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR recipient_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
);
|
||||
`);
|
||||
const deletedCount5 = result5[1] || 0;
|
||||
if (deletedCount5 > 0) {
|
||||
console.log(`✅ ${deletedCount5} verwaiste promotional_gift Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup user_house mit ungültigen house_type_id oder user_id
|
||||
const result6 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.user_house
|
||||
WHERE house_type_id NOT IN (
|
||||
SELECT id FROM falukant_type.house
|
||||
) OR user_id NOT IN (
|
||||
SELECT id FROM falukant_data.falukant_user
|
||||
);
|
||||
`);
|
||||
const deletedCount6 = result6[1] || 0;
|
||||
if (deletedCount6 > 0) {
|
||||
console.log(`✅ ${deletedCount6} verwaiste user_house Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id
|
||||
const result7 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.child_relation
|
||||
WHERE father_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR mother_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR child_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
);
|
||||
`);
|
||||
const deletedCount7 = result7[1] || 0;
|
||||
if (deletedCount7 > 0) {
|
||||
console.log(`✅ ${deletedCount7} verwaiste child_relation Einträge entfernt`);
|
||||
}
|
||||
|
||||
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0) {
|
||||
console.log("✅ Keine verwaisten Einträge gefunden");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte verwaiste Einträge nicht bereinigen:', e?.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Setting up associations...");
|
||||
setupAssociations();
|
||||
|
||||
@@ -104,6 +210,10 @@ const syncDatabase = async () => {
|
||||
// Deployment-Synchronisation (immer Schema-Updates)
|
||||
const syncDatabaseForDeployment = async () => {
|
||||
try {
|
||||
// WICHTIG: Bei Caching-Problemen das Script neu starten
|
||||
// Node.js cached ES-Module, daher müssen Models neu geladen werden
|
||||
console.log('📦 Lade Models neu (Node.js Module-Cache wird verwendet)...');
|
||||
|
||||
// Zeige den aktuellen Stage an
|
||||
const currentStage = process.env.STAGE || 'nicht gesetzt';
|
||||
console.log(`🚀 Starte Datenbank-Synchronisation für Deployment (Stage: ${currentStage})`);
|
||||
@@ -133,6 +243,165 @@ const syncDatabaseForDeployment = async () => {
|
||||
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Migration: Transport product_id und size nullable machen
|
||||
console.log("Making transport product_id and size nullable...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Prüfe ob product_id NOT NULL Constraint existiert
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'transport'
|
||||
AND column_name = 'product_id'
|
||||
AND is_nullable = 'NO'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.transport
|
||||
ALTER COLUMN product_id DROP NOT NULL;
|
||||
RAISE NOTICE 'product_id NOT NULL Constraint entfernt';
|
||||
END IF;
|
||||
|
||||
-- Prüfe ob size NOT NULL Constraint existiert
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'transport'
|
||||
AND column_name = 'size'
|
||||
AND is_nullable = 'NO'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.transport
|
||||
ALTER COLUMN size DROP NOT NULL;
|
||||
RAISE NOTICE 'size NOT NULL Constraint entfernt';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
console.log("✅ Transport product_id und size sind jetzt nullable");
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte Transport-Spalten nicht nullable machen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Cleanup: Entferne verwaiste Einträge vor Schema-Updates
|
||||
console.log("Cleaning up orphaned entries...");
|
||||
try {
|
||||
// Cleanup user_param_visibility
|
||||
const result1 = await sequelize.query(`
|
||||
DELETE FROM community.user_param_visibility
|
||||
WHERE param_id NOT IN (
|
||||
SELECT id FROM community.user_param
|
||||
);
|
||||
`);
|
||||
const deletedCount1 = result1[1] || 0;
|
||||
if (deletedCount1 > 0) {
|
||||
console.log(`✅ ${deletedCount1} verwaiste user_param_visibility Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup stock mit ungültigen branch_id (0 oder nicht existierend)
|
||||
const result2 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.stock
|
||||
WHERE branch_id = 0 OR branch_id NOT IN (
|
||||
SELECT id FROM falukant_data.branch
|
||||
);
|
||||
`);
|
||||
const deletedCount2 = result2[1] || 0;
|
||||
if (deletedCount2 > 0) {
|
||||
console.log(`✅ ${deletedCount2} verwaiste stock Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup knowledge mit ungültigen character_id oder product_id
|
||||
const result3 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.knowledge
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR product_id NOT IN (
|
||||
SELECT id FROM falukant_type.product
|
||||
);
|
||||
`);
|
||||
const deletedCount3 = result3[1] || 0;
|
||||
if (deletedCount3 > 0) {
|
||||
console.log(`✅ ${deletedCount3} verwaiste knowledge Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup notification mit ungültigen user_id
|
||||
const result4 = await sequelize.query(`
|
||||
DELETE FROM falukant_log.notification
|
||||
WHERE user_id NOT IN (
|
||||
SELECT id FROM falukant_data.falukant_user
|
||||
);
|
||||
`);
|
||||
const deletedCount4 = result4[1] || 0;
|
||||
if (deletedCount4 > 0) {
|
||||
console.log(`✅ ${deletedCount4} verwaiste notification Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id
|
||||
const result5 = await sequelize.query(`
|
||||
DELETE FROM falukant_log.promotional_gift
|
||||
WHERE sender_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR recipient_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
);
|
||||
`);
|
||||
const deletedCount5 = result5[1] || 0;
|
||||
if (deletedCount5 > 0) {
|
||||
console.log(`✅ ${deletedCount5} verwaiste promotional_gift Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup user_house mit ungültigen house_type_id oder user_id
|
||||
const result6 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.user_house
|
||||
WHERE house_type_id NOT IN (
|
||||
SELECT id FROM falukant_type.house
|
||||
) OR user_id NOT IN (
|
||||
SELECT id FROM falukant_data.falukant_user
|
||||
);
|
||||
`);
|
||||
const deletedCount6 = result6[1] || 0;
|
||||
if (deletedCount6 > 0) {
|
||||
console.log(`✅ ${deletedCount6} verwaiste user_house Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id
|
||||
const result7 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.child_relation
|
||||
WHERE father_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR mother_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR child_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
);
|
||||
`);
|
||||
const deletedCount7 = result7[1] || 0;
|
||||
if (deletedCount7 > 0) {
|
||||
console.log(`✅ ${deletedCount7} verwaiste child_relation Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup political_office mit ungültigen character_id, office_type_id oder region_id
|
||||
const result8 = await sequelize.query(`
|
||||
DELETE FROM falukant_data.political_office
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR office_type_id NOT IN (
|
||||
SELECT id FROM falukant_type.political_office_type
|
||||
) OR region_id NOT IN (
|
||||
SELECT id FROM falukant_data.region
|
||||
);
|
||||
`);
|
||||
const deletedCount8 = result8[1] || 0;
|
||||
if (deletedCount8 > 0) {
|
||||
console.log(`✅ ${deletedCount8} verwaiste political_office Einträge entfernt`);
|
||||
}
|
||||
|
||||
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0) {
|
||||
console.log("✅ Keine verwaisten Einträge gefunden");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte verwaiste Einträge nicht bereinigen:', e?.message || e);
|
||||
}
|
||||
|
||||
console.log("Setting up associations...");
|
||||
setupAssociations();
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ fi
|
||||
# 5. Frontend neu bauen – VITE_* aus Environment übernehmen oder Defaults setzen
|
||||
echo "Baue Frontend neu..."
|
||||
export VITE_API_BASE_URL=${VITE_API_BASE_URL:-https://www.your-part.de}
|
||||
# Standard: Daemon direkt auf Port 4551, nicht über Apache-Proxy
|
||||
export VITE_DAEMON_SOCKET=${VITE_DAEMON_SOCKET:-wss://www.your-part.de:4551}
|
||||
export VITE_CHAT_WS_URL=${VITE_CHAT_WS_URL:-wss://www.your-part.de:1235}
|
||||
|
||||
|
||||
142
deploy-with-config.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== YourPart Deployment mit vorhandener Konfiguration ==="
|
||||
|
||||
# Prüfen ob wir im richtigen Verzeichnis sind
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "Error: Bitte führen Sie dieses Skript aus dem YourPart3-Root-Verzeichnis aus"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Frontend bauen
|
||||
echo "Building frontend..."
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# Verzeichnisse erstellen
|
||||
echo "Creating directories..."
|
||||
sudo mkdir -p /opt/yourpart/{frontend,backend,images/{tmp,userimages,screenshots}}
|
||||
|
||||
# Frontend kopieren
|
||||
echo "Deploying frontend..."
|
||||
sudo cp -r frontend/dist /opt/yourpart/frontend/
|
||||
sudo chown -R www-data:www-data /opt/yourpart/frontend
|
||||
sudo chmod -R 755 /opt/yourpart/frontend
|
||||
|
||||
# Backend kopieren
|
||||
echo "Deploying backend..."
|
||||
sudo cp -r backend/* /opt/yourpart/backend/
|
||||
sudo chown -R www-data:www-data /opt/yourpart/backend
|
||||
sudo chmod -R 755 /opt/yourpart/backend
|
||||
|
||||
# .env-Datei erstellen
|
||||
echo "Creating .env file..."
|
||||
cat > /opt/yourpart/backend/.env << 'ENVEOF'
|
||||
# Datenbank-Konfiguration (aus alter App)
|
||||
DB_HOST=localhost
|
||||
DB_USER=yourpart
|
||||
DB_PASS=hitomisan
|
||||
DB_NAME=yp2
|
||||
DB_PORT=60000
|
||||
|
||||
# Anwendungskonfiguration
|
||||
NODE_ENV=production
|
||||
PORT=2020
|
||||
STAGE=production
|
||||
|
||||
# Redis-Konfiguration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASS=your_redis_password
|
||||
|
||||
# Session-Konfiguration (aus alter App)
|
||||
SECRET_KEY=k7e0CCw75PcmEGa
|
||||
SESSION_SECRET=k7e0CCw75PcmEGa
|
||||
|
||||
# E-Mail-Konfiguration (aus alter App)
|
||||
SMTP_HOST=smtp.1blu.de
|
||||
SMTP_PORT=465
|
||||
SMTP_USER=e226079_0-tsschulz
|
||||
SMTP_PASS=hitomisan
|
||||
SMTP_SECURE=true
|
||||
|
||||
# E-Mail-Einstellungen (aus alter App)
|
||||
SENDER_NAME=YourPart
|
||||
SENDER_EMAIL=kontakt@your-part.de
|
||||
|
||||
# AMQP-Konfiguration (aus alter App)
|
||||
AMQP_HOST=tsschulz.de
|
||||
AMQP_PORT=5672
|
||||
AMQP_EXCHANGE=yourpart
|
||||
AMQP_USERNAME=yourpart
|
||||
AMQP_PASSWORD=yourpart
|
||||
|
||||
# API-Keys (aus alter App)
|
||||
WEATHER_API_KEY=d0ddfcbc915f50263274211648a5dab0
|
||||
NEWS_API_KEY=pub_212733602779de7708a7374d67e363bd06af4
|
||||
|
||||
# Pfad-Konfiguration (aus alter App)
|
||||
ROOT_PATH=/opt/yourpart
|
||||
IMAGES_PATH=/images
|
||||
TMP_IMAGES_PATH=/images/tmp
|
||||
USER_IMAGES_PATH=/images/userimages
|
||||
SCREENSHOT_IMAGES_PATH=/images/screenshots
|
||||
|
||||
# URL-Konfiguration
|
||||
BASE_URL=https://www.your-part.de
|
||||
IMAGES_URL=https://www.your-part.de/images/
|
||||
ACTIVATION_URL=https://www.your-part.de/activate?code=
|
||||
PASSWORD_RESET_URL=https://www.your-part.de/setnewpassword?username=
|
||||
|
||||
# Spiel-Konfiguration (aus alter App)
|
||||
START_BUDGET=10
|
||||
TEST_MODE=false
|
||||
PAY_PER_DAY=100
|
||||
|
||||
# Debug-Einstellungen
|
||||
DEBUG_SQL=false
|
||||
DEBUG_MESSAGES=false
|
||||
DEBUG_RECREATE_DB=false
|
||||
ENVEOF
|
||||
|
||||
sudo chown www-data:www-data /opt/yourpart/backend/.env
|
||||
sudo chmod 600 /opt/yourpart/backend/.env
|
||||
|
||||
# Service installieren
|
||||
echo "Installing service..."
|
||||
sudo cp yourpart.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable yourpart.service
|
||||
|
||||
# Apache-Konfiguration
|
||||
echo "Configuring Apache..."
|
||||
sudo cp yourpart-http.conf /etc/apache2/sites-available/
|
||||
sudo cp yourpart-https.conf /etc/apache2/sites-available/
|
||||
|
||||
# Alte Konfiguration deaktivieren
|
||||
sudo a2dissite yourpart 2>/dev/null || true
|
||||
|
||||
# Neue Konfigurationen aktivieren
|
||||
sudo a2ensite yourpart-http
|
||||
sudo a2ensite yourpart-https
|
||||
|
||||
# Apache-Module aktivieren
|
||||
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite ssl
|
||||
sudo systemctl reload apache2
|
||||
|
||||
# Service starten
|
||||
echo "Starting service..."
|
||||
sudo systemctl start yourpart.service
|
||||
|
||||
echo ""
|
||||
echo "=== Deployment abgeschlossen! ==="
|
||||
echo "Frontend: /opt/yourpart/frontend/dist/"
|
||||
echo "Backend: /opt/yourpart/backend/"
|
||||
echo "Service: yourpart.service"
|
||||
echo ""
|
||||
echo "Status prüfen:"
|
||||
echo " sudo systemctl status yourpart.service"
|
||||
echo " sudo systemctl status apache2"
|
||||
echo " sudo journalctl -u yourpart.service -f"
|
||||
26
fix-api-urls.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== YourPart API-URL Fix ==="
|
||||
|
||||
cd ~/yourpart3/frontend
|
||||
|
||||
# 1. Alle localhost:3001 Referenzen finden
|
||||
echo "Suche nach localhost:3001 Referenzen..."
|
||||
grep -r "localhost:3001" src/ || echo "Keine localhost:3001 Referenzen gefunden"
|
||||
|
||||
# 2. Alle localhost Referenzen finden
|
||||
echo ""
|
||||
echo "Suche nach localhost Referenzen..."
|
||||
grep -r "localhost" src/ || echo "Keine localhost Referenzen gefunden"
|
||||
|
||||
# 3. API-Konfigurationsdateien finden
|
||||
echo ""
|
||||
echo "Suche nach API-Konfigurationsdateien..."
|
||||
find src/ -name "*.js" -exec grep -l "axios\|baseURL\|localhost" {} \;
|
||||
|
||||
echo ""
|
||||
echo "=== API-URL Fix abgeschlossen ==="
|
||||
echo "Bitte überprüfen Sie die gefundenen Dateien und ersetzen Sie:"
|
||||
echo " localhost:3001 → /api"
|
||||
echo " http://localhost:3001 → /api"
|
||||
echo " baseURL: 'http://localhost:3001' → baseURL: '/api'"
|
||||
22
fix-cors.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== YourPart CORS-Fix ==="
|
||||
|
||||
# Backup der ursprünglichen app.js erstellen
|
||||
sudo cp /opt/yourpart/backend/app.js /opt/yourpart/backend/app.js.backup
|
||||
|
||||
# CORS-Konfiguration aktualisieren
|
||||
sudo sed -i 's|origin: \[.*\]|origin: [\n "http://localhost:3000", \n "http://localhost:5173", \n "http://127.0.0.1:3000", \n "http://127.0.0.1:5173",\n "https://your-part.de",\n "https://www.your-part.de",\n "http://your-part.de",\n "http://www.your-part.de"\n ]|' /opt/yourpart/backend/app.js
|
||||
|
||||
echo "CORS-Konfiguration aktualisiert!"
|
||||
|
||||
# Service neu starten
|
||||
echo "Starte Backend-Service neu..."
|
||||
sudo systemctl restart yourpart.service
|
||||
|
||||
# Status prüfen
|
||||
echo "Service-Status:"
|
||||
sudo systemctl status yourpart.service
|
||||
|
||||
echo ""
|
||||
echo "CORS-Fix abgeschlossen! Testen Sie jetzt die Anwendung."
|
||||
5
frontend/.env.production
Normal file
@@ -0,0 +1,5 @@
|
||||
VITE_API_BASE_URL=https://www.your-part.de
|
||||
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
|
||||
VITE_DAEMON_SOCKET=wss://www.your-part.de:4551
|
||||
VITE_CHAT_WS_URL=wss://www.your-part.de:1235
|
||||
VITE_SOCKET_IO_URL=https://www.your-part.de:4443
|
||||
206
frontend/package-lock.json
generated
@@ -530,13 +530,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.7.tgz",
|
||||
"integrity": "sha512-mE71aUH5baH0me8duB4FY5qevUJizypHsYw3eCvmOx07QvmKppgOONx3dYINxuA89Z2qkAGb/K6Nrpi7aAMwew==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
|
||||
"integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "10.0.7",
|
||||
"@intlify/shared": "10.0.7"
|
||||
"@intlify/message-compiler": "10.0.8",
|
||||
"@intlify/shared": "10.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -546,12 +546,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.7.tgz",
|
||||
"integrity": "sha512-nrC4cDL/UHZSUqd8sRbVz+DPukzZ8NnG5OK+EB/nlxsH35deyzyVkXP/QuR8mFZrISJ+4hCd6VtCQCcT+RO+5g==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
|
||||
"integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "10.0.7",
|
||||
"@intlify/shared": "10.0.8",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -562,9 +562,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.7.tgz",
|
||||
"integrity": "sha512-oeoq0L5+5P4ShXa6jBQcx+BT+USe3MjX0xJexZO1y7rfDJdwZ9+QP3jO4tcS1nxhBYYdjvFTqe4bmnLijV0GxQ==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
|
||||
"integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -1494,7 +1494,8 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
@@ -1512,13 +1513,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -1565,6 +1566,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -1593,6 +1607,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -1674,6 +1689,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -1698,6 +1714,20 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
||||
@@ -1730,13 +1760,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -1745,7 +1772,33 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -1848,12 +1901,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1878,22 +1934,26 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"dev": true,
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1902,6 +1962,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@@ -1915,12 +1988,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -1938,23 +2011,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -1966,7 +2027,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
@@ -1981,7 +2041,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@@ -2151,6 +2210,15 @@
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -2161,6 +2229,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -2169,6 +2238,7 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -2864,9 +2934,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2987,13 +3057,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.7.tgz",
|
||||
"integrity": "sha512-bKsk0PYwP9gdYF4nqSAT0kDpnLu1gZzlxFl885VH4mHVhEnqP16+/mAU05r1U6NIrc0fGDWP89tZ8GzeJZpe+w==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
|
||||
"integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "10.0.7",
|
||||
"@intlify/shared": "10.0.7",
|
||||
"@intlify/core-base": "10.0.8",
|
||||
"@intlify/shared": "10.0.8",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/public/images/falukant/map_old.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 625 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 948 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 489 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.6 KiB |